From 20f8b345db15382fe7c54db6b1da0dee2fb661a6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Dec 2023 19:09:17 +0100 Subject: [PATCH 01/13] env-util: add strv_env_assignf() helper --- src/basic/env-util.c | 29 +++++++++++++++++++++++++++++ src/basic/env-util.h | 1 + src/test/test-env-util.c | 21 +++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/src/basic/env-util.c b/src/basic/env-util.c index d3bf73385f..7ac47732ba 100644 --- a/src/basic/env-util.c +++ b/src/basic/env-util.c @@ -458,6 +458,35 @@ int strv_env_assign(char ***l, const char *key, const char *value) { return strv_env_replace_consume(l, p); } +int strv_env_assignf(char ***l, const char *key, const char *valuef, ...) { + int r; + + assert(l); + assert(key); + + if (!env_name_is_valid(key)) + return -EINVAL; + + if (!valuef) { + strv_env_unset(*l, key); + return 0; + } + + _cleanup_free_ char *value = NULL; + va_list ap; + va_start(ap, valuef); + r = vasprintf(&value, valuef, ap); + va_end(ap); + if (r < 0) + return -ENOMEM; + + char *p = strjoin(key, "=", value); + if (!p) + return -ENOMEM; + + return strv_env_replace_consume(l, p); +} + int _strv_env_assign_many(char ***l, ...) { va_list ap; int r; diff --git a/src/basic/env-util.h b/src/basic/env-util.h index f7fb1e9082..8e77cc71d6 100644 --- a/src/basic/env-util.h +++ b/src/basic/env-util.h @@ -49,6 +49,7 @@ int strv_env_replace_consume(char ***l, char *p); /* In place ... */ int strv_env_replace_strdup(char ***l, const char *assignment); int strv_env_replace_strdup_passthrough(char ***l, const char *assignment); int strv_env_assign(char ***l, const char *key, const char *value); +int strv_env_assignf(char ***l, const char *key, const char *valuef, ...) _printf_(3, 4); int _strv_env_assign_many(char ***l, ...) _sentinel_; #define strv_env_assign_many(l, ...) _strv_env_assign_many(l, __VA_ARGS__, NULL) diff --git a/src/test/test-env-util.c b/src/test/test-env-util.c index dffbad6327..c267c2e341 100644 --- a/src/test/test-env-util.c +++ b/src/test/test-env-util.c @@ -133,6 +133,27 @@ TEST(strv_env_assign) { assert_se(streq(a[0], "a=A")); } +TEST(strv_env_assignf) { + _cleanup_strv_free_ char **a = NULL; + + assert_se(strv_env_assignf(&a, "a", "a") > 0); + assert_se(strv_env_assignf(&a, "a", "%c", 'a') == 0); + + assert_se(strv_env_assignf(&a, "c", "xxx%iyyy", 5) > 0); + assert_se(strv_length(a) == 2); + assert_se(strv_equal(a, STRV_MAKE("a=a", "c=xxx5yyy"))); + assert_se(strv_env_assignf(&a, "c", NULL) == 0); + + assert_se(strv_env_assignf(&a, "b", "b") > 0); + assert_se(strv_env_assignf(&a, "a", "A") == 0); + assert_se(strv_env_assignf(&a, "b", NULL) == 0); + + assert_se(strv_env_assignf(&a, "a=", "B") == -EINVAL); + + assert_se(strv_length(a) == 1); + assert_se(streq(a[0], "a=A")); +} + TEST(strv_env_assign_many) { _cleanup_strv_free_ char **a = NULL; From 72eb3081b21a12252159e24188e25d04f14af83c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Dec 2023 19:10:52 +0100 Subject: [PATCH 02/13] run: when invoked as "uid0", expose some sudo-like behaviour This turns "systemd-run" into a multi-call binary. When invoked under the name "uid0", then it behaves a bit more like traditional "sudo". This mostly means defaults appropriuate for that, for example a PAM stack, interactivity and more. Fixes: #29199 --- man/rules/meson.build | 1 + man/systemd-run.xml | 3 +- man/uid0.xml | 214 ++++++++++++++++++++++++++++++++ src/run/meson.build | 14 +++ src/run/run.c | 269 +++++++++++++++++++++++++++++++++++++++- src/run/systemd-uid0.in | 23 ++++ 6 files changed, 521 insertions(+), 3 deletions(-) create mode 100644 man/uid0.xml create mode 100644 src/run/systemd-uid0.in diff --git a/man/rules/meson.build b/man/rules/meson.build index 3d63cf1131..622921f8d6 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1257,6 +1257,7 @@ manpages = [ ''], ['udev_new', '3', ['udev_ref', 'udev_unref'], ''], ['udevadm', '8', [], ''], + ['uid0', '1', [], ''], ['ukify', '1', [], 'ENABLE_UKIFY'], ['user@.service', '5', diff --git a/man/systemd-run.xml b/man/systemd-run.xml index 5be9823c37..bc77fd13ab 100644 --- a/man/systemd-run.xml +++ b/man/systemd-run.xml @@ -677,7 +677,8 @@ $ systemd-run --user --wait -p SuccessExitStatus=SIGUSR1 --expand-environment=no systemd.resource-control5, systemd.timer5, systemd-mount1, - machinectl1 + machinectl1, + uid01 diff --git a/man/uid0.xml b/man/uid0.xml new file mode 100644 index 0000000000..6ef868a8e5 --- /dev/null +++ b/man/uid0.xml @@ -0,0 +1,214 @@ + + + + + + + + uid0 + systemd + + + + uid0 + 1 + + + + uid0 + Elevate privileges + + + + + uid0 + OPTIONS + COMMAND + + + + + Description + + uid0 may be used to temporarily and interactively acquire elavated or different + privileges. It serves a similar purpose as sudo8, but + operates differently in a couple of key areas: + + + No execution or security context credentials are inherited from the caller into the + invoked commands, as they are invoked from a fresh, isolated service forked off the service + manager. + + Authentication takes place via polkit, thus isolating the + authentication prompt from the terminal (if possible). + + An independent pseudo-tty is allocated for the invoked command, detaching its lifecycle and + isolating it for security. + + No SetUID/SetGID file access bit functionality is used for the implementation. + + + Altogether this should provide a safer and more robust alternative to the sudo + mechanism, in particular in OS environments where SetUID/SetGID support is not available (for example by + setting the NoNewPrivileges= variable in + systemd-system.conf5). + + Any session invoked via uid0 will run through the + systemd-uid0 PAM stack. + + Note that uid0 is implemented as an alternative multi-call invocation of + systemd-run1. + + + + Options + + The following options are understood: + + + + + + Do not query the user for authentication for privileged operations. + + + + + + + + Use this unit name instead of an automatically generated one. + + + + + + + + Sets a property on the service unit that is created. This option takes an assignment + in the same format as + systemctl1's + set-property command. + + + + + + + + + Provide a description for the service unit that is invoked. If not specified, + the command itself will be used as a description. See Description= in + systemd.unit5. + + + + + + + + + Make the new .service unit part of the specified slice, instead + of user.slice. + + + + + + + + + Make the new .service unit part of the slice the + uid0 itself has been invoked in. This option may be combined with + , in which case the slice specified via is placed + within the slice the uid0 command is invoked in. + + Example: consider uid0 being invoked in the slice + foo.slice, and the argument is + bar. The unit will then be placed under + foo-bar.slice. + + + + + + + + + + + + + Switches to the specified user/group instead of root. + + + + + + + + + Runs the invoked session with the specified nice level. + + + + + + + + + + Runs the invoked session with the specified working directory. If not specified + defaults to the client's current working directory if switching to the root user, or the target + user's home directory otherwise. + + + + + + + + + Runs the invoked session with the specified environment variable set. This parameter + may be used more than once to set multiple variables. When = and + VALUE are omitted, the value of the variable with the same name in the + invoking environment will be used. + + + + + + + + + + + All command line arguments after the first non-option argument become part of the command line of + the launched process. If no command line is specified an interactive shell is invoked. The shell to + invoke may be controlled via and currently defaults to the + originating user's shell (i.e. not the target user's!) if operating locally, or + /bin/sh when operating with . + + + + Exit status + + On success, 0 is returned. If uid0 failed to start the session or the specified command fails, a + non-zero return value will be returned. + + + + See Also + + systemd1, + systemd-run1, + sudo8, + machinectl1 + + + + diff --git a/src/run/meson.build b/src/run/meson.build index 597a25abeb..95336b86fe 100644 --- a/src/run/meson.build +++ b/src/run/meson.build @@ -7,3 +7,17 @@ executables += [ 'sources' : files('run.c'), }, ] + +install_emptydir(bindir) + +meson.add_install_script(sh, '-c', + ln_s.format(bindir / 'systemd-run', + bindir / 'uid0')) + +custom_target( + 'systemd-uid0', + input : 'systemd-uid0.in', + output : 'systemd-uid0', + command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'], + install : pamconfdir != 'no', + install_dir : pamconfdir) diff --git a/src/run/run.c b/src/run/run.c index 06c00cbf56..335838c32f 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -73,6 +73,7 @@ static bool arg_aggressive_gc = false; static char *arg_working_directory = NULL; static bool arg_shell = false; static char **arg_cmdline = NULL; +static char *arg_exec_path = NULL; STATIC_DESTRUCTOR_REGISTER(arg_description, freep); STATIC_DESTRUCTOR_REGISTER(arg_environment, strv_freep); @@ -82,6 +83,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_socket_property, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_timer_property, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_working_directory, freep); STATIC_DESTRUCTOR_REGISTER(arg_cmdline, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_exec_path, freep); static int help(void) { _cleanup_free_ char *link = NULL; @@ -146,6 +148,39 @@ static int help(void) { return 0; } +static int help_sudo_mode(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("uid0", "1", &link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...] COMMAND [ARGUMENTS...]\n" + "\n%sElevate privileges interactively.%s\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " --no-ask-password Do not prompt for password\n" + " --machine=CONTAINER Operate on local container\n" + " --unit=UNIT Run under the specified unit name\n" + " --property=NAME=VALUE Set service or scope unit property\n" + " --description=TEXT Description for unit\n" + " --slice=SLICE Run in the specified slice\n" + " --slice-inherit Inherit the slice\n" + " -u --user=USER Run as system user\n" + " -g --group=GROUP Run as system group\n" + " --nice=NICE Nice level\n" + " -D --chdir=PATH Set working directory\n" + " --setenv=NAME[=VALUE] Set environment variable\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + link); + + return 0; +} + static int add_timer_property(const char *name, const char *val) { char *p; @@ -162,6 +197,18 @@ static int add_timer_property(const char *name, const char *val) { return 0; } +static char **make_login_shell_cmdline(const char *shell) { + _cleanup_free_ char *argv0 = NULL; + + assert(shell); + + argv0 = strjoin("-", shell); /* The - is how shells determine if they shall be consider login shells */ + if (!argv0) + return NULL; + + return strv_new(argv0); +} + static int parse_argv(int argc, char *argv[]) { enum { @@ -651,6 +698,219 @@ static int parse_argv(int argc, char *argv[]) { return 1; } +static int parse_argv_sudo_mode(int argc, char *argv[]) { + + enum { + ARG_NO_ASK_PASSWORD = 0x100, + ARG_HOST, + ARG_MACHINE, + ARG_UNIT, + ARG_PROPERTY, + ARG_DESCRIPTION, + ARG_SLICE, + ARG_SLICE_INHERIT, + ARG_NICE, + ARG_SETENV, + }; + + /* If invoked as "uid0" binary, let's expose a more sudo-like interface. We add various extensions + * though (but limit the extension to long options). */ + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, + { "machine", required_argument, NULL, ARG_MACHINE }, + { "unit", required_argument, NULL, ARG_UNIT }, + { "property", required_argument, NULL, ARG_PROPERTY }, + { "description", required_argument, NULL, ARG_DESCRIPTION }, + { "slice", required_argument, NULL, ARG_SLICE }, + { "slice-inherit", no_argument, NULL, ARG_SLICE_INHERIT }, + { "user", required_argument, NULL, 'u' }, + { "group", required_argument, NULL, 'g' }, + { "nice", required_argument, NULL, ARG_NICE }, + { "chdir", required_argument, NULL, 'D' }, + { "setenv", required_argument, NULL, ARG_SETENV }, + {}, + }; + + int r, c; + + assert(argc >= 0); + assert(argv); + + /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long() + * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */ + optind = 0; + while ((c = getopt_long(argc, argv, "+hVu:g:D:", options, NULL)) >= 0) + + switch (c) { + + case 'h': + return help_sudo_mode(); + + case 'V': + return version(); + + case ARG_NO_ASK_PASSWORD: + arg_ask_password = false; + break; + + case ARG_MACHINE: + arg_transport = BUS_TRANSPORT_MACHINE; + arg_host = optarg; + break; + + case ARG_UNIT: + arg_unit = optarg; + break; + + case ARG_PROPERTY: + if (strv_extend(&arg_property, optarg) < 0) + return log_oom(); + + break; + + case ARG_DESCRIPTION: + r = free_and_strdup_warn(&arg_description, optarg); + if (r < 0) + return r; + break; + + case ARG_SLICE: + arg_slice = optarg; + break; + + case ARG_SLICE_INHERIT: + arg_slice_inherit = true; + break; + + case 'u': + arg_exec_user = optarg; + break; + + case 'g': + arg_exec_group = optarg; + break; + + case ARG_NICE: + r = parse_nice(optarg, &arg_nice); + if (r < 0) + return log_error_errno(r, "Failed to parse nice value: %s", optarg); + + arg_nice_set = true; + break; + + case 'D': + r = parse_path_argument(optarg, true, &arg_working_directory); + if (r < 0) + return r; + + break; + + case ARG_SETENV: + r = strv_env_replace_strdup_passthrough(&arg_environment, optarg); + if (r < 0) + return log_error_errno(r, "Cannot assign environment variable %s: %m", optarg); + + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (!arg_working_directory) { + if (arg_exec_user) { + /* When switching to a specific user, also switch to its home directory. */ + arg_working_directory = strdup("~"); + if (!arg_working_directory) + return log_oom(); + } else { + /* When switching to root without this being specified, then stay in the current directory */ + r = safe_getcwd(&arg_working_directory); + if (r < 0) + return log_error_errno(r, "Failed to get current working directory: %m"); + } + } + + arg_service_type = "exec"; + arg_quiet = true; + arg_wait = true; + arg_aggressive_gc = true; + + arg_stdio = isatty(STDIN_FILENO) && isatty(STDOUT_FILENO) && isatty(STDERR_FILENO) ? ARG_STDIO_PTY : ARG_STDIO_DIRECT; + arg_expand_environment = false; + arg_send_sighup = true; + + _cleanup_strv_free_ char **l = NULL; + if (argc > optind) + l = strv_copy(argv + optind); + else { + const char *e; + + e = strv_env_get(arg_environment, "SHELL"); + if (e) + arg_exec_path = strdup(e); + else { + if (arg_transport == BUS_TRANSPORT_LOCAL) { + r = get_shell(&arg_exec_path); + if (r < 0) + return log_error_errno(r, "Failed to determine shell: %m"); + } else + arg_exec_path = strdup("/bin/sh"); + } + if (!arg_exec_path) + return log_oom(); + + l = make_login_shell_cmdline(arg_exec_path); + } + if (!l) + return log_oom(); + + strv_free_and_replace(arg_cmdline, l); + + if (!arg_slice) { + arg_slice = strdup("user.slice"); + if (!arg_slice) + return log_oom(); + } + + _cleanup_free_ char *un = NULL; + un = getusername_malloc(); + if (!un) + return log_oom(); + + /* Set a bunch of environment variables in a roughly sudo-compatible way */ + r = strv_env_assign(&arg_environment, "SUDO_USER", un); + if (r < 0) + return log_error_errno(r, "Failed to set $SUDO_USER environment variable: %m"); + + r = strv_env_assignf(&arg_environment, "SUDO_UID", UID_FMT, getuid()); + if (r < 0) + return log_error_errno(r, "Failed to set $SUDO_UID environment variable: %m"); + + r = strv_env_assignf(&arg_environment, "SUDO_GID", GID_FMT, getgid()); + if (r < 0) + return log_error_errno(r, "Failed to set $SUDO_GID environment variable: %m"); + + if (strv_extendf(&arg_property, "LogExtraFields=ELEVATED_UID=" UID_FMT, getuid()) < 0) + return log_oom(); + + if (strv_extendf(&arg_property, "LogExtraFields=ELEVATED_GID=" GID_FMT, getgid()) < 0) + return log_oom(); + + if (strv_extendf(&arg_property, "LogExtraFields=ELEVATED_USER=%s", un) < 0) + return log_oom(); + + if (strv_extend(&arg_property, "PAMName=systemd-uid0") < 0) + return log_oom(); + + return 1; +} + static int transient_unit_set_properties(sd_bus_message *m, UnitType t, char **properties) { int r; @@ -899,7 +1159,7 @@ static int transient_service_set_properties(sd_bus_message *m, const char *pty_p if (r < 0) return bus_log_create_error(r); - r = sd_bus_message_append(m, "s", arg_cmdline[0]); + r = sd_bus_message_append(m, "s", arg_exec_path ?: arg_cmdline[0]); if (r < 0) return bus_log_create_error(r); @@ -1899,6 +2159,8 @@ static int start_transient_trigger(sd_bus *bus, const char *suffix) { } static bool shall_make_executable_absolute(void) { + if (arg_exec_path) + return false; if (strv_isempty(arg_cmdline)) return false; if (arg_transport != BUS_TRANSPORT_LOCAL) @@ -1919,7 +2181,10 @@ static int run(int argc, char* argv[]) { log_parse_environment(); log_open(); - r = parse_argv(argc, argv); + if (invoked_as(argv, "uid0")) + r = parse_argv_sudo_mode(argc, argv); + else + r = parse_argv(argc, argv); if (r <= 0) return r; diff --git a/src/run/systemd-uid0.in b/src/run/systemd-uid0.in new file mode 100644 index 0000000000..57bd5e38b9 --- /dev/null +++ b/src/run/systemd-uid0.in @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# This file is part of systemd. +# +# Used by uid0 sessions + +{% if ENABLE_HOMED %} +-account sufficient pam_systemd_home.so +{% endif %} +account required pam_unix.so + +{% if HAVE_SELINUX %} +session required pam_selinux.so close +session required pam_selinux.so open +{% endif %} +session required pam_loginuid.so +session optional pam_keyinit.so force revoke +session required pam_namespace.so +{% if ENABLE_HOMED %} +-session optional pam_systemd_home.so +{% endif %} +session optional pam_umask.so silent +session optional pam_systemd.so +session required pam_unix.so From 1072d9473123ed174e033704fc5222216b655c9e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Dec 2023 21:39:50 +0100 Subject: [PATCH 03/13] run: optionally set the "ignore-failure" flag for ExecStart= lines --- man/systemd-run.xml | 11 +++++++++++ src/run/run.c | 13 +++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/man/systemd-run.xml b/man/systemd-run.xml index bc77fd13ab..a334ab94d5 100644 --- a/man/systemd-run.xml +++ b/man/systemd-run.xml @@ -485,6 +485,17 @@ + + + + By default, if the specified command fails the invoked unit will be marked failed + (though possibly still unloaded, see , above), and this is reported in the + logs. If this switch is specified this is suppressed and any non-success exit status/code of the + command is treated as success. + + + + diff --git a/src/run/run.c b/src/run/run.c index 335838c32f..d90dbbe411 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -74,6 +74,7 @@ static char *arg_working_directory = NULL; static bool arg_shell = false; static char **arg_cmdline = NULL; static char *arg_exec_path = NULL; +static bool arg_ignore_failure = false; STATIC_DESTRUCTOR_REGISTER(arg_description, freep); STATIC_DESTRUCTOR_REGISTER(arg_environment, strv_freep); @@ -125,6 +126,7 @@ static int help(void) { " -q --quiet Suppress information messages during runtime\n" " -G --collect Unload unit after it ran, even when failed\n" " -S --shell Invoke a $SHELL interactively\n" + " --ignore-failure Ignore the exit status of the invoked process\n" "\n%3$sPath options:%4$s\n" " --path-property=NAME=VALUE Set path unit property\n" "\n%3$sSocket options:%4$s\n" @@ -241,6 +243,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_WAIT, ARG_WORKING_DIRECTORY, ARG_SHELL, + ARG_IGNORE_FAILURE, }; static const struct option options[] = { @@ -286,6 +289,7 @@ static int parse_argv(int argc, char *argv[]) { { "working-directory", required_argument, NULL, ARG_WORKING_DIRECTORY }, { "same-dir", no_argument, NULL, 'd' }, { "shell", no_argument, NULL, 'S' }, + { "ignore-failure", no_argument, NULL, ARG_IGNORE_FAILURE }, {}, }; @@ -571,6 +575,10 @@ static int parse_argv(int argc, char *argv[]) { arg_shell = true; break; + case ARG_IGNORE_FAILURE: + arg_ignore_failure = true; + break; + case '?': return -EINVAL; @@ -1170,9 +1178,10 @@ static int transient_service_set_properties(sd_bus_message *m, const char *pty_p if (use_ex_prop) r = sd_bus_message_append_strv( m, - STRV_MAKE(arg_expand_environment > 0 ? NULL : "no-env-expand")); + STRV_MAKE(arg_expand_environment > 0 ? NULL : "no-env-expand", + arg_ignore_failure ? "ignore-failure" : NULL)); else - r = sd_bus_message_append(m, "b", false); + r = sd_bus_message_append(m, "b", arg_ignore_failure); if (r < 0) return bus_log_create_error(r); From b0936baf110a4ed7989062655812e72199f6ad00 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Dec 2023 21:40:32 +0100 Subject: [PATCH 04/13] mkosi: don't turn off installation of our PAM snippets Otherwise we don't get the new PAM snippet for the uid0 PAM stack installed. --- mkosi.images/base/mkosi.build.chroot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkosi.images/base/mkosi.build.chroot b/mkosi.images/base/mkosi.build.chroot index b60ed8d731..9524fdb103 100755 --- a/mkosi.images/base/mkosi.build.chroot +++ b/mkosi.images/base/mkosi.build.chroot @@ -70,7 +70,7 @@ if [ ! -f "$BUILDDIR"/build.ninja ]; then -D tests=unsafe -D slow-tests="${SLOW_TESTS:-false}" -D create-log-dirs=false - -D pamconfdir=no + -D pamconfdir=/usr/lib/pam.d/ -D utmp=true -D hibernate=true -D ldconfig=true From d02d4f83f018afc5fde1d9779fbaada362feb612 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 20 Dec 2023 12:01:32 +0100 Subject: [PATCH 05/13] terminal-util: add helper for disabling terminal echo in termios struct --- src/basic/terminal-util.c | 8 ++++++++ src/basic/terminal-util.h | 4 +++- src/shared/ask-password-api.c | 4 +--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 3355b749cc..63aca3684f 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -1551,3 +1551,11 @@ int set_terminal_cursor_position(int fd, unsigned int row, unsigned int column) return 0; } + +void termios_disable_echo(struct termios *termios) { + assert(termios); + + termios->c_lflag &= ~(ICANON|ECHO); + termios->c_cc[VMIN] = 1; + termios->c_cc[VTIME] = 0; +} diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index cae42887c4..d7537c23bf 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "macro.h" #include "time-util.h" @@ -168,7 +169,6 @@ bool underline_enabled(void); bool dev_console_colors_enabled(void); static inline bool colors_enabled(void) { - /* Returns true if colors are considered supported on our stdout. */ return get_color_mode() != COLOR_OFF; } @@ -286,3 +286,5 @@ static inline const char* ansi_highlight_green_red(bool b) { /* This assumes there is a 'tty' group */ #define TTY_MODE 0620 + +void termios_disable_echo(struct termios *termios); diff --git a/src/shared/ask-password-api.c b/src/shared/ask-password-api.c index 0e323f4644..6d71245549 100644 --- a/src/shared/ask-password-api.c +++ b/src/shared/ask-password-api.c @@ -443,9 +443,7 @@ int ask_password_tty( (void) loop_write(ttyfd, ANSI_NORMAL, SIZE_MAX); new_termios = old_termios; - new_termios.c_lflag &= ~(ICANON|ECHO); - new_termios.c_cc[VMIN] = 1; - new_termios.c_cc[VTIME] = 0; + termios_disable_echo(&new_termios); r = RET_NERRNO(tcsetattr(ttyfd, TCSADRAIN, &new_termios)); if (r < 0) From 63e9c38370b8c13d3006920a160adbe1577fc73e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 20 Dec 2023 12:02:01 +0100 Subject: [PATCH 06/13] terminal-util: add helper that determines terminal default bg color --- src/basic/terminal-util.c | 208 ++++++++++++++++++++++++++++++++++ src/basic/terminal-util.h | 2 + src/test/test-terminal-util.c | 11 ++ 3 files changed, 221 insertions(+) diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 63aca3684f..b740b49775 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -27,6 +27,7 @@ #include "fd-util.h" #include "fileio.h" #include "fs-util.h" +#include "hexdecoct.h" #include "inotify-util.h" #include "io-util.h" #include "log.h" @@ -1559,3 +1560,210 @@ void termios_disable_echo(struct termios *termios) { termios->c_cc[VMIN] = 1; termios->c_cc[VTIME] = 0; } + +typedef enum BackgroundColorState { + BACKGROUND_TEXT, + BACKGROUND_ESCAPE, + BACKGROUND_BRACKET, + BACKGROUND_FIRST_ONE, + BACKGROUND_SECOND_ONE, + BACKGROUND_SEMICOLON, + BACKGROUND_R, + BACKGROUND_G, + BACKGROUND_B, + BACKGROUND_RED, + BACKGROUND_GREEN, + BACKGROUND_BLUE, +} BackgroundColorState; + +typedef struct BackgroundColorContext { + BackgroundColorState state; + uint32_t red, green, blue; + unsigned red_bits, green_bits, blue_bits; +} BackgroundColorContext; + +static int scan_background_color_response( + BackgroundColorContext *context, + const char *buf, + size_t size) { + + assert(context); + assert(buf || size == 0); + + for (size_t i = 0; i < size; i++) { + char c = buf[i]; + + switch (context->state) { + + case BACKGROUND_TEXT: + context->state = c == '\x1B' ? BACKGROUND_ESCAPE : BACKGROUND_TEXT; + break; + + case BACKGROUND_ESCAPE: + context->state = c == ']' ? BACKGROUND_BRACKET : BACKGROUND_TEXT; + break; + + case BACKGROUND_BRACKET: + context->state = c == '1' ? BACKGROUND_FIRST_ONE : BACKGROUND_TEXT; + break; + + case BACKGROUND_FIRST_ONE: + context->state = c == '1' ? BACKGROUND_SECOND_ONE : BACKGROUND_TEXT; + break; + + case BACKGROUND_SECOND_ONE: + context->state = c == ';' ? BACKGROUND_SEMICOLON : BACKGROUND_TEXT; + break; + + case BACKGROUND_SEMICOLON: + context->state = c == 'r' ? BACKGROUND_R : BACKGROUND_TEXT; + break; + + case BACKGROUND_R: + context->state = c == 'g' ? BACKGROUND_G : BACKGROUND_TEXT; + break; + + case BACKGROUND_G: + context->state = c == 'b' ? BACKGROUND_B : BACKGROUND_TEXT; + break; + + case BACKGROUND_B: + context->state = c == ':' ? BACKGROUND_RED : BACKGROUND_TEXT; + break; + + case BACKGROUND_RED: + if (c == '/') + context->state = context->red_bits > 0 ? BACKGROUND_GREEN : BACKGROUND_TEXT; + else { + int d = unhexchar(c); + if (d < 0 || context->red_bits >= sizeof(context->red)*8) + context->state = BACKGROUND_TEXT; + else { + context->red = (context->red << 4) | d; + context->red_bits += 4; + } + } + break; + + case BACKGROUND_GREEN: + if (c == '/') + context->state = context->green_bits > 0 ? BACKGROUND_BLUE : BACKGROUND_TEXT; + else { + int d = unhexchar(c); + if (d < 0 || context->green_bits >= sizeof(context->green)*8) + context->state = BACKGROUND_TEXT; + else { + context->green = (context->green << 4) | d; + context->green_bits += 4; + } + } + break; + + case BACKGROUND_BLUE: + if (c == '\x07') { + if (context->blue_bits > 0) + return 1; /* success! */ + + context->state = BACKGROUND_TEXT; + } else { + int d = unhexchar(c); + if (d < 0 || context->blue_bits >= sizeof(context->blue)*8) + context->state = BACKGROUND_TEXT; + else { + context->blue = (context->blue << 4) | d; + context->blue_bits += 4; + } + } + break; + } + + /* Reset any colors we might have picked up */ + if (context->state == BACKGROUND_TEXT) { + /* reset color */ + context->red = context->green = context->blue = 0; + context->red_bits = context->green_bits = context->blue_bits = 0; + } + } + + return 0; /* all good, but not enough data yet */ +} + +int get_default_background_color(double *ret_red, double *ret_green, double *ret_blue) { + int r; + + assert(ret_red); + assert(ret_green); + assert(ret_blue); + + if (!colors_enabled()) + return -EOPNOTSUPP; + + if (isatty(STDOUT_FILENO) < 1 || isatty(STDIN_FILENO) < 1) + return -EOPNOTSUPP; + + if (streq_ptr(getenv("TERM"), "linux")) { + /* Linux console is black */ + *ret_red = *ret_green = *ret_blue = 0.0; + return 0; + } + + struct termios old_termios; + if (tcgetattr(STDIN_FILENO, &old_termios) < 0) + return -errno; + + struct termios new_termios = old_termios; + termios_disable_echo(&new_termios); + + if (tcsetattr(STDOUT_FILENO, TCSADRAIN, &new_termios) < 0) + return -errno; + + r = loop_write(STDOUT_FILENO, "\x1B]11;?\x07", SIZE_MAX); + if (r < 0) + goto finish; + + usec_t end = usec_add(now(CLOCK_MONOTONIC), 100 * USEC_PER_MSEC); + char buf[256]; + size_t buf_full = 0; + BackgroundColorContext context = {}; + + for (;;) { + usec_t n = now(CLOCK_MONOTONIC); + + if (n >= end) { + r = -EOPNOTSUPP; + goto finish; + } + + r = fd_wait_for_event(STDIN_FILENO, POLLIN, usec_sub_unsigned(end, n)); + if (r < 0) + goto finish; + + ssize_t l; + l = read(STDIN_FILENO, buf, sizeof(buf) - buf_full); + if (l < 0) { + r = -errno; + goto finish; + } + + buf_full += l; + assert(buf_full <= sizeof(buf)); + + r = scan_background_color_response(&context, buf, buf_full); + if (r < 0) + goto finish; + if (r > 0) { + assert(context.red_bits > 0); + *ret_red = (double) context.red / ((UINT64_C(1) << context.red_bits) - 1); + assert(context.green_bits > 0); + *ret_green = (double) context.green / ((UINT64_C(1) << context.green_bits) - 1); + assert(context.blue_bits > 0); + *ret_blue = (double) context.blue / ((UINT64_C(1) << context.blue_bits) - 1); + r = 0; + goto finish; + } + } + +finish: + (void) tcsetattr(STDOUT_FILENO, TCSADRAIN, &old_termios); + return r; +} diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index d7537c23bf..1ec057c2dd 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -288,3 +288,5 @@ static inline const char* ansi_highlight_green_red(bool b) { #define TTY_MODE 0620 void termios_disable_echo(struct termios *termios); + +int get_default_background_color(double *ret_red, double *ret_green, double *ret_blue); diff --git a/src/test/test-terminal-util.c b/src/test/test-terminal-util.c index a2b7101573..0b46cad982 100644 --- a/src/test/test-terminal-util.c +++ b/src/test/test-terminal-util.c @@ -165,4 +165,15 @@ TEST(get_ctty) { log_notice("Not invoked with stdin == ctty, cutting get_ctty() test short"); } +TEST(get_default_background_color) { + double red, green, blue; + int r; + + r = get_default_background_color(&red, &green, &blue); + if (r < 0) + log_notice_errno(r, "Can't get terminal default background color: %m"); + else + log_notice("R=%g G=%g B=%g", red, green, blue); +} + DEFINE_TEST_MAIN(LOG_INFO); From 23d9fcc3c9f347cd4759e87e8f1aafd8f39f170d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 20 Dec 2023 12:04:06 +0100 Subject: [PATCH 07/13] ptyfwd: optionally, change ANSI background color of forwarded terminals As the bytes flow through our terminal forwarder we can color the background of the terminal with a color of our choices, if that's desired. This will later allow us to color the background of the uid0 tool when running as root with a slightly alarming red color. This does two things: 1. When an ANSI sequence is seen that resets the background color, it is extended to immediately set the color to our choice. 2. When a newline is seen it is immeidately extended to set the background color again and clear the current line till the end. Net effect: all lines written while we forward the ttys will be shown with the background color of choice. --- src/shared/ptyfwd.c | 279 +++++++++++++++++++++++++++++++++++++++++++- src/shared/ptyfwd.h | 2 + 2 files changed, 276 insertions(+), 5 deletions(-) diff --git a/src/shared/ptyfwd.c b/src/shared/ptyfwd.c index 1a92d7ec26..03660ad417 100644 --- a/src/shared/ptyfwd.c +++ b/src/shared/ptyfwd.c @@ -18,15 +18,26 @@ #include "alloc-util.h" #include "errno-util.h" +#include "extract-word.h" #include "fd-util.h" #include "io-util.h" #include "log.h" #include "macro.h" #include "ptyfwd.h" #include "stat-util.h" +#include "strv.h" #include "terminal-util.h" #include "time-util.h" +typedef enum AnsiColorState { + ANSI_COLOR_STATE_TEXT, + ANSI_COLOR_STATE_ESC, + ANSI_COLOR_STATE_CSI_SEQUENCE, + ANSI_COLOR_STATE_NEWLINE, + _ANSI_COLOR_STATE_MAX, + _ANSI_COLOR_STATE_INVALID = -EINVAL, +} AnsiColorState; + struct PTYForward { sd_event *event; @@ -67,7 +78,8 @@ struct PTYForward { bool last_char_set:1; char last_char; - char in_buffer[LINE_MAX], out_buffer[LINE_MAX]; + char in_buffer[LINE_MAX], *out_buffer; + size_t out_buffer_size; size_t in_buffer_full, out_buffer_full; usec_t escape_timestamp; @@ -75,6 +87,10 @@ struct PTYForward { PTYForwardHandler handler; void *userdata; + + char *background_color; + AnsiColorState ansi_color_state; + char *csi_sequence; }; #define ESCAPE_USEC (1*USEC_PER_SEC) @@ -115,6 +131,14 @@ static void pty_forward_disconnect(PTYForward *f) { } f->saved_stdout = f->saved_stdin = false; + + f->out_buffer = mfree(f->out_buffer); + f->out_buffer_size = 0; + f->out_buffer_full = 0; + f->in_buffer_full = 0; + + f->csi_sequence = mfree(f->csi_sequence); + f->ansi_color_state = _ANSI_COLOR_STATE_INVALID; } static int pty_forward_done(PTYForward *f, int rcode) { @@ -202,10 +226,243 @@ static bool drained(PTYForward *f) { return true; } -static int shovel(PTYForward *f) { - ssize_t k; +static char *background_color_sequence(PTYForward *f) { + assert(f); + assert(f->background_color); + + /* This sets the background color to the desired one, and erase the rest of the line with it */ + + return strjoin("\x1B[", f->background_color, "m", ANSI_ERASE_TO_END_OF_LINE); +} + +static int insert_string(PTYForward *f, size_t offset, const char *s) { + assert(f); + assert(offset <= f->out_buffer_full); + assert(s); + + size_t l = strlen(s); + assert(l <= INT_MAX); /* Make sure we can still return this */ + + void *p = realloc(f->out_buffer, MAX(f->out_buffer_full + l, (size_t) LINE_MAX)); + if (!p) + return -ENOMEM; + + f->out_buffer = p; + f->out_buffer_size = MALLOC_SIZEOF_SAFE(f->out_buffer); + + memmove(f->out_buffer + offset + l, f->out_buffer + offset, f->out_buffer_full - offset); + memcpy(f->out_buffer + offset, s, l); + f->out_buffer_full += l; + + return (int) l; +} + +static int insert_erase_newline(PTYForward *f, size_t offset) { + _cleanup_free_ char *s = NULL; assert(f); + assert(f->background_color); + + s = background_color_sequence(f); + if (!s) + return -ENOMEM; + + return insert_string(f, offset, s); +} + +static int is_csi_background_reset_sequence(const char *seq) { + enum { + COLOR_TOKEN_NO, + COLOR_TOKEN_START, + COLOR_TOKEN_8BIT, + COLOR_TOKEN_24BIT_R, + COLOR_TOKEN_24BIT_G, + COLOR_TOKEN_24BIT_B, + } token_state = COLOR_TOKEN_NO; + + bool b = false; + int r; + + assert(seq); + + /* This parses CSI "m" sequences, and determines if they reset the background color. If so returns + * 1. This can then be used to insert another sequence that sets the color to the desired + * replacement. */ + + for (;;) { + _cleanup_free_ char *token = NULL; + + r = extract_first_word(&seq, &token, ";", EXTRACT_RELAX|EXTRACT_DONT_COALESCE_SEPARATORS|EXTRACT_RETAIN_ESCAPE); + if (r < 0) + return r; + if (r == 0) + break; + + switch (token_state) { + + case COLOR_TOKEN_NO: + + if (STR_IN_SET(token, "", "0", "00", "49")) + b = true; /* These tokens set the background back to normal */ + else if (STR_IN_SET(token, "40", "41", "42", "43", "44", "45", "46", "47", "48")) + b = false; /* And these tokens set them to something other than normal */ + + if (STR_IN_SET(token, "38", "48", "58")) + token_state = COLOR_TOKEN_START; /* These tokens mean an 8bit or 24bit color will follow */ + break; + + case COLOR_TOKEN_START: + + if (STR_IN_SET(token, "5", "05")) + token_state = COLOR_TOKEN_8BIT; /* 8bit color */ + else if (STR_IN_SET(token, "2", "02")) + token_state = COLOR_TOKEN_24BIT_R; /* 24bit color */ + else + token_state = COLOR_TOKEN_NO; /* something weird? */ + break; + + case COLOR_TOKEN_24BIT_R: + token_state = COLOR_TOKEN_24BIT_G; + break; + + case COLOR_TOKEN_24BIT_G: + token_state = COLOR_TOKEN_24BIT_B; + break; + + case COLOR_TOKEN_8BIT: + case COLOR_TOKEN_24BIT_B: + token_state = COLOR_TOKEN_NO; + break; + } + } + + return b; +} + +static int insert_background_fix(PTYForward *f, size_t offset) { + assert(f); + assert(f->background_color); + + if (!is_csi_background_reset_sequence(strempty(f->csi_sequence))) + return 0; + + _cleanup_free_ char *s = NULL; + s = strjoin(";", f->background_color); + if (!s) + return -ENOMEM; + + return insert_string(f, offset, s); +} + +static int pty_forward_ansi_process(PTYForward *f, size_t offset) { + int r; + + assert(f); + assert(offset <= f->out_buffer_full); + + if (!f->background_color) + return 0; + + for (size_t i = offset; i < f->out_buffer_full; i++) { + char c = f->out_buffer[i]; + + switch (f->ansi_color_state) { + + case ANSI_COLOR_STATE_TEXT: + if (c == '\n') + f->ansi_color_state = ANSI_COLOR_STATE_NEWLINE; + if (c == 0x1B) /* ESC */ + f->ansi_color_state = ANSI_COLOR_STATE_ESC; + break; + + case ANSI_COLOR_STATE_NEWLINE: { + /* Immediately after a newline insert an ANSI sequence to erase the line with a background color */ + + r = insert_erase_newline(f, i); + if (r < 0) + return r; + + i += r; + + f->ansi_color_state = ANSI_COLOR_STATE_TEXT; + break; + } + + case ANSI_COLOR_STATE_ESC: { + + if (c == '[') + f->ansi_color_state = ANSI_COLOR_STATE_CSI_SEQUENCE; + else + f->ansi_color_state = ANSI_COLOR_STATE_TEXT; + + break; + } + + case ANSI_COLOR_STATE_CSI_SEQUENCE: { + + if (c >= 0x20 && c <= 0x3F) { + /* If this is a "parameter" or "intermediary" byte (i.e. ranges 0x20…0x2F and + * 0x30…0x3F) then we are still in the CSI sequence */ + + if (strlen_ptr(f->csi_sequence) >= 64) { + /* Safety check: lets not accept unbounded CSI sequences */ + + f->csi_sequence = mfree(f->csi_sequence); + f->ansi_color_state = ANSI_COLOR_STATE_TEXT; + } else if (!strextend(&f->csi_sequence, CHAR_TO_STR(c))) + return -ENOMEM; + } else { + /* Otherwise, the CSI sequence is over */ + + if (c == 'm') { + /* This is an "SGR" (Select Graphic Rendition) sequence. Patch in our background color. */ + r = insert_background_fix(f, i); + if (r < 0) + return r; + + i += r; + } + + f->csi_sequence = mfree(f->csi_sequence); + f->ansi_color_state = ANSI_COLOR_STATE_TEXT; + } + + break; + } + + default: + assert_not_reached(); + } + } + + return 0; +} + +static int shovel(PTYForward *f) { + ssize_t k; + int r; + + assert(f); + + if (f->out_buffer_size == 0 && f->background_color) { + /* Erase the first line when we start */ + f->out_buffer = background_color_sequence(f); + if (!f->out_buffer) + return pty_forward_done(f, log_oom()); + + f->out_buffer_full = strlen(f->out_buffer); + f->out_buffer_size = MALLOC_SIZEOF_SAFE(f->out_buffer); + } + + if (f->out_buffer_size < LINE_MAX) { + /* Make sure we always have room for at least one "line" */ + void *p = realloc(f->out_buffer, LINE_MAX); + if (!p) + return pty_forward_done(f, log_oom()); + + f->out_buffer = p; + f->out_buffer_size = MALLOC_SIZEOF_SAFE(p); + } while ((f->stdin_readable && f->in_buffer_full <= 0) || (f->master_writable && f->in_buffer_full > 0) || @@ -267,9 +524,9 @@ static int shovel(PTYForward *f) { } } - if (f->master_readable && f->out_buffer_full < LINE_MAX) { + if (f->master_readable && f->out_buffer_full < MIN(f->out_buffer_size, (size_t) LINE_MAX)) { - k = read(f->master, f->out_buffer + f->out_buffer_full, LINE_MAX - f->out_buffer_full); + k = read(f->master, f->out_buffer + f->out_buffer_full, f->out_buffer_size - f->out_buffer_full); if (k < 0) { /* Note that EIO on the master device might be caused by vhangup() or @@ -289,7 +546,12 @@ static int shovel(PTYForward *f) { } } else { f->read_from_master = true; + size_t scan_index = f->out_buffer_full; f->out_buffer_full += (size_t) k; + + r = pty_forward_ansi_process(f, scan_index); + if (r < 0) + return pty_forward_done(f, log_error_errno(r, "Failed to scan for ANSI sequences: %m")); } } @@ -555,6 +817,7 @@ PTYForward *pty_forward_free(PTYForward *f) { if (!f) return NULL; pty_forward_disconnect(f); + free(f->background_color); return mfree(f); } @@ -689,3 +952,9 @@ int pty_forward_set_width_height(PTYForward *f, unsigned width, unsigned height) return 0; } + +int pty_forward_set_background_color(PTYForward *f, const char *color) { + assert(f); + + return free_and_strdup(&f->background_color, color); +} diff --git a/src/shared/ptyfwd.h b/src/shared/ptyfwd.h index f0ae6e949d..bae8d3591e 100644 --- a/src/shared/ptyfwd.h +++ b/src/shared/ptyfwd.h @@ -39,4 +39,6 @@ int pty_forward_set_priority(PTYForward *f, int64_t priority); int pty_forward_set_width_height(PTYForward *f, unsigned width, unsigned height); +int pty_forward_set_background_color(PTYForward *f, const char *color); + DEFINE_TRIVIAL_CLEANUP_FUNC(PTYForward*, pty_forward_free); From eee799fa86ff7cf67fca47bcb2bd17361fcaced4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 20 Dec 2023 12:07:37 +0100 Subject: [PATCH 08/13] color-util: split out HSV color conversion into color-util.[ch] --- src/pcrlock/pcrlock.c | 35 +---------------------------------- src/shared/color-util.c | 40 ++++++++++++++++++++++++++++++++++++++++ src/shared/color-util.h | 8 ++++++++ src/shared/meson.build | 1 + 4 files changed, 50 insertions(+), 34 deletions(-) create mode 100644 src/shared/color-util.c create mode 100644 src/shared/color-util.h diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index 5bac04519b..23bdab0bef 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -11,6 +11,7 @@ #include "blockdev-util.h" #include "build.h" #include "chase.h" +#include "color-util.h" #include "conf-files.h" #include "efi-api.h" #include "env-util.h" @@ -1932,40 +1933,6 @@ static int event_log_map_components(EventLog *el) { return event_log_validate_fully_recognized(el); } -static void hsv_to_rgb( - double h, double s, double v, - uint8_t* ret_r, uint8_t *ret_g, uint8_t *ret_b) { - - double c, x, m, r, g, b; - - assert(s >= 0 && s <= 100); - assert(v >= 0 && v <= 100); - assert(ret_r); - assert(ret_g); - assert(ret_b); - - c = (s / 100.0) * (v / 100.0); - x = c * (1 - fabs(fmod(h / 60.0, 2) - 1)); - m = (v / 100) - c; - - if (h >= 0 && h < 60) - r = c, g = x, b = 0.0; - else if (h >= 60 && h < 120) - r = x, g = c, b = 0.0; - else if (h >= 120 && h < 180) - r = 0.0, g = c, b = x; - else if (h >= 180 && h < 240) - r = 0.0, g = x, b = c; - else if (h >= 240 && h < 300) - r = x, g = 0.0, b = c; - else - r = c, g = 0.0, b = x; - - *ret_r = (uint8_t) ((r + m) * 255); - *ret_g = (uint8_t) ((g + m) * 255); - *ret_b = (uint8_t) ((b + m) * 255); -} - #define ANSI_TRUE_COLOR_MAX (7U + 3U + 1U + 3U + 1U + 3U + 2U) static const char *ansi_true_color(uint8_t r, uint8_t g, uint8_t b, char ret[static ANSI_TRUE_COLOR_MAX]) { diff --git a/src/shared/color-util.c b/src/shared/color-util.c new file mode 100644 index 0000000000..c02b4b62eb --- /dev/null +++ b/src/shared/color-util.c @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "color-util.h" +#include "macro.h" + +void hsv_to_rgb(double h, double s, double v, + uint8_t* ret_r, uint8_t *ret_g, uint8_t *ret_b) { + + double c, x, m, r, g, b; + + assert(s >= 0 && s <= 100); + assert(v >= 0 && v <= 100); + assert(ret_r); + assert(ret_g); + assert(ret_b); + + h = fmod(h, 360); + c = (s / 100.0) * (v / 100.0); + x = c * (1 - fabs(fmod(h / 60.0, 2) - 1)); + m = (v / 100) - c; + + if (h >= 0 && h < 60) + r = c, g = x, b = 0.0; + else if (h >= 60 && h < 120) + r = x, g = c, b = 0.0; + else if (h >= 120 && h < 180) + r = 0.0, g = c, b = x; + else if (h >= 180 && h < 240) + r = 0.0, g = x, b = c; + else if (h >= 240 && h < 300) + r = x, g = 0.0, b = c; + else + r = c, g = 0.0, b = x; + + *ret_r = (uint8_t) ((r + m) * 255); + *ret_g = (uint8_t) ((g + m) * 255); + *ret_b = (uint8_t) ((b + m) * 255); +} diff --git a/src/shared/color-util.h b/src/shared/color-util.h new file mode 100644 index 0000000000..d527794d09 --- /dev/null +++ b/src/shared/color-util.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +void hsv_to_rgb( + double h, double s, double v, + uint8_t* ret_r, uint8_t *ret_g, uint8_t *ret_b); diff --git a/src/shared/meson.build b/src/shared/meson.build index b24a541de5..0dc03e1358 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -39,6 +39,7 @@ shared_sources = files( 'chown-recursive.c', 'clean-ipc.c', 'clock-util.c', + 'color-util.c', 'common-signal.c', 'compare-operator.c', 'condition.c', From 447bcbfc903d56615809d7b578ddbec53704ccc5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 20 Dec 2023 12:08:32 +0100 Subject: [PATCH 09/13] =?UTF-8?q?color-util:=20add=20helper=20to=20convert?= =?UTF-8?q?=20RGB=20=E2=86=92=20HSV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We already have HSV → RGB, add the opposite operation. --- src/shared/color-util.c | 37 ++++++++++++++++++++++ src/shared/color-util.h | 3 ++ src/test/meson.build | 1 + src/test/test-color-util.c | 63 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 src/test/test-color-util.c diff --git a/src/shared/color-util.c b/src/shared/color-util.c index c02b4b62eb..776445ecfc 100644 --- a/src/shared/color-util.c +++ b/src/shared/color-util.c @@ -5,6 +5,43 @@ #include "color-util.h" #include "macro.h" +void rgb_to_hsv(double r, double g, double b, + double *ret_h, double *ret_s, double *ret_v) { + + assert(r >= 0 && r <= 1); + assert(g >= 0 && g <= 1); + assert(b >= 0 && b <= 1); + assert(ret_h); + assert(ret_s); + assert(ret_v); + + double max_color = fmax(r, fmax(g, b)); + double min_color = fmin(r, fmin(g, b)); + double delta = max_color - min_color; + + *ret_v = max_color * 100.0; + + if (max_color > 0) + *ret_s = delta / max_color * 100.0; + else { + *ret_s = 0; + *ret_h = NAN; + return; + } + + if (delta > 0) { + if (r >= max_color) + *ret_h = 60 * fmod((g - b) / delta, 6); + else if (g >= max_color) + *ret_h = 60 * (((b - r) / delta) + 2); + else if (b >= max_color) + *ret_h = 60 * (((r - g) / delta) + 4); + + *ret_h = fmod(*ret_h, 360); + } else + *ret_h = NAN; +} + void hsv_to_rgb(double h, double s, double v, uint8_t* ret_r, uint8_t *ret_g, uint8_t *ret_b) { diff --git a/src/shared/color-util.h b/src/shared/color-util.h index d527794d09..a4ae9eba09 100644 --- a/src/shared/color-util.h +++ b/src/shared/color-util.h @@ -3,6 +3,9 @@ #include +void rgb_to_hsv(double r, double g, double b, + double *ret_h, double *ret_s, double *ret_v); + void hsv_to_rgb( double h, double s, double v, uint8_t* ret_r, uint8_t *ret_g, uint8_t *ret_b); diff --git a/src/test/meson.build b/src/test/meson.build index b0a92bb43b..aec125d483 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -57,6 +57,7 @@ simple_tests += files( 'test-cgroup.c', 'test-chase.c', 'test-clock.c', + 'test-color-util.c', 'test-compare-operator.c', 'test-condition.c', 'test-conf-files.c', diff --git a/src/test/test-color-util.c b/src/test/test-color-util.c new file mode 100644 index 0000000000..3d00d8719d --- /dev/null +++ b/src/test/test-color-util.c @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "color-util.h" +#include "tests.h" + +TEST(hsv_to_rgb) { + uint8_t r, g, b; + + hsv_to_rgb(0, 0, 0, &r, &g, &b); + assert(r == 0 && g == 0 && b == 0); + + hsv_to_rgb(60, 0, 0, &r, &g, &b); + assert(r == 0 && g == 0 && b == 0); + + hsv_to_rgb(0, 0, 100, &r, &g, &b); + assert(r == 255 && g == 255 && b == 255); + + hsv_to_rgb(0, 100, 100, &r, &g, &b); + assert(r == 255 && g == 0 && b == 0); + + hsv_to_rgb(120, 100, 100, &r, &g, &b); + assert(r == 0 && g == 255 && b == 0); + + hsv_to_rgb(240, 100, 100, &r, &g, &b); + assert(r == 0 && g == 0 && b == 255); + + hsv_to_rgb(311, 52, 62, &r, &g, &b); + assert(r == 158 && g == 75 && b == 143); +} + +TEST(rgb_to_hsv) { + + double h, s, v; + rgb_to_hsv(0, 0, 0, &h, &s, &v); + assert(s <= 0); + assert(v <= 0); + + rgb_to_hsv(1, 1, 1, &h, &s, &v); + assert(s <= 0); + assert(v >= 100); + + rgb_to_hsv(1, 0, 0, &h, &s, &v); + assert(h >= 359 || h <= 1); + assert(s >= 100); + assert(v >= 100); + + rgb_to_hsv(0, 1, 0, &h, &s, &v); + assert(h >= 119 && h <= 121); + assert(s >= 100); + assert(v >= 100); + + rgb_to_hsv(0, 0, 1, &h, &s, &v); + assert(h >= 239 && h <= 241); + assert(s >= 100); + assert(v >= 100); + + rgb_to_hsv(0.5, 0.6, 0.7, &h, &s, &v); + assert(h >= 209 && h <= 211); + assert(s >= 28 && s <= 31); + assert(v >= 69 && v <= 71); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); From 2f0b4d578b7d3bb7fbaba0cdf3f9bc358a40800d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 20 Dec 2023 12:09:27 +0100 Subject: [PATCH 10/13] run/uid0: tint the terminal background color (and add new --background= switch) This adds a new --background= switch that allows specifiying a background color for the terminal while the tool runs. It also teaches the tool when invoked as uid0 to tint the terminal in a reddish hue when operating as root, and in a yellowish hue when operating as any other user. This should highlight nicely when the user is operating with elevated privileges, or changed privileges. --- man/systemd-run.xml | 14 +++++++++++ man/uid0.xml | 18 ++++++++++++++ src/run/run.c | 58 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/man/systemd-run.xml b/man/systemd-run.xml index a334ab94d5..7b508a462c 100644 --- a/man/systemd-run.xml +++ b/man/systemd-run.xml @@ -496,6 +496,20 @@ + + + + Change the terminal background color to the specified ANSI color as long as the + session lasts. The color specified should be an ANSI X3.64 SGR background color, i.e. strings such as + 40, 41, …, 47, 48;2;…, + 48;5;…. See ANSI + Escape Code (Wikipedia) for details. + + + + + diff --git a/man/uid0.xml b/man/uid0.xml index 6ef868a8e5..0d36ca77a4 100644 --- a/man/uid0.xml +++ b/man/uid0.xml @@ -182,6 +182,24 @@ + + + + Change the terminal background color to the specified ANSI color as long as the + session lasts. If not specified, the background will be tinted in a reddish tone when operating as + root, and in a yellowish tone when operating under another UID, as reminder of the changed + privileges. The color specified should be an ANSI X3.64 SGR background color, i.e. strings such as + 40, 41, …, 47, 48;2;…, + 48;5;…. See ANSI + Escape Code (Wikipedia) for details. Set to an empty string to disable. + + Example: --background=44 for a blue background. + + + + + diff --git a/src/run/run.c b/src/run/run.c index d90dbbe411..920e38af0c 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -17,6 +17,7 @@ #include "bus-unit-util.h" #include "bus-wait-for-jobs.h" #include "calendarspec.h" +#include "color-util.h" #include "env-util.h" #include "escape.h" #include "exit-status.h" @@ -75,6 +76,7 @@ static bool arg_shell = false; static char **arg_cmdline = NULL; static char *arg_exec_path = NULL; static bool arg_ignore_failure = false; +static char *arg_background = NULL; STATIC_DESTRUCTOR_REGISTER(arg_description, freep); STATIC_DESTRUCTOR_REGISTER(arg_environment, strv_freep); @@ -85,6 +87,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_timer_property, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_working_directory, freep); STATIC_DESTRUCTOR_REGISTER(arg_cmdline, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_exec_path, freep); +STATIC_DESTRUCTOR_REGISTER(arg_background, freep); static int help(void) { _cleanup_free_ char *link = NULL; @@ -127,6 +130,7 @@ static int help(void) { " -G --collect Unload unit after it ran, even when failed\n" " -S --shell Invoke a $SHELL interactively\n" " --ignore-failure Ignore the exit status of the invoked process\n" + " --background=COLOR Set ANSI color for background\n" "\n%3$sPath options:%4$s\n" " --path-property=NAME=VALUE Set path unit property\n" "\n%3$sSocket options:%4$s\n" @@ -174,6 +178,7 @@ static int help_sudo_mode(void) { " --nice=NICE Nice level\n" " -D --chdir=PATH Set working directory\n" " --setenv=NAME[=VALUE] Set environment variable\n" + " --background=COLOR Set ANSI color for background\n" "\nSee the %s for details.\n", program_invocation_short_name, ansi_highlight(), @@ -244,6 +249,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_WORKING_DIRECTORY, ARG_SHELL, ARG_IGNORE_FAILURE, + ARG_BACKGROUND, }; static const struct option options[] = { @@ -290,6 +296,7 @@ static int parse_argv(int argc, char *argv[]) { { "same-dir", no_argument, NULL, 'd' }, { "shell", no_argument, NULL, 'S' }, { "ignore-failure", no_argument, NULL, ARG_IGNORE_FAILURE }, + { "background", no_argument, NULL, ARG_BACKGROUND }, {}, }; @@ -333,7 +340,7 @@ static int parse_argv(int argc, char *argv[]) { break; case ARG_DESCRIPTION: - r = free_and_strdup(&arg_description, optarg); + r = free_and_strdup_warn(&arg_description, optarg); if (r < 0) return r; break; @@ -579,6 +586,12 @@ static int parse_argv(int argc, char *argv[]) { arg_ignore_failure = true; break; + case ARG_BACKGROUND: + r = free_and_strdup_warn(&arg_background, optarg); + if (r < 0) + return r; + break; + case '?': return -EINVAL; @@ -719,6 +732,7 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) { ARG_SLICE_INHERIT, ARG_NICE, ARG_SETENV, + ARG_BACKGROUND, }; /* If invoked as "uid0" binary, let's expose a more sudo-like interface. We add various extensions @@ -739,6 +753,7 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) { { "nice", required_argument, NULL, ARG_NICE }, { "chdir", required_argument, NULL, 'D' }, { "setenv", required_argument, NULL, ARG_SETENV }, + { "background", required_argument, NULL, ARG_BACKGROUND }, {}, }; @@ -823,6 +838,13 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) { break; + case ARG_BACKGROUND: + r = free_and_strdup_warn(&arg_background, optarg); + if (r < 0) + return r; + + break; + case '?': return -EINVAL; @@ -916,6 +938,37 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) { if (strv_extend(&arg_property, "PAMName=systemd-uid0") < 0) return log_oom(); + if (!arg_background && arg_stdio == ARG_STDIO_PTY) { + double red, green, blue; + + r = get_default_background_color(&red, &green, &blue); + if (r < 0) + log_debug_errno(r, "Unable to get terminal background color, not tinting background: %m"); + else { + double h, s, v; + + rgb_to_hsv(red, green, blue, &h, &s, &v); + + if (!arg_exec_user || STR_IN_SET(arg_exec_user, "root", "0")) + h = 0; /* red */ + else + h = 60 /* yellow */; + + if (v > 50) /* If the background is bright, then pull down saturation */ + s = 25; + else /* otherwise pump it up */ + s = 75; + + v = MAX(30, v); /* Make sure we don't hide the color in black */ + + uint8_t r8, g8, b8; + hsv_to_rgb(h, s, v, &r8, &g8, &b8); + + if (asprintf(&arg_background, "48;2;%u;%u;%u", r8, g8, b8) < 0) + return log_oom(); + } + } + return 1; } @@ -1701,6 +1754,9 @@ static int start_transient_service(sd_bus *bus) { /* Make sure to process any TTY events before we process bus events */ (void) pty_forward_set_priority(c.forward, SD_EVENT_PRIORITY_IMPORTANT); + + if (!isempty(arg_background)) + (void) pty_forward_set_background_color(c.forward, arg_background); } path = unit_dbus_path_from_name(service); From 2118b179ca0d99f44953e3913318bd20d7ba509b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 20 Dec 2023 15:01:52 +0100 Subject: [PATCH 11/13] man: try to improve wording on --slice-inherit docs --- man/systemd-run.xml | 11 +++++++---- src/run/run.c | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/man/systemd-run.xml b/man/systemd-run.xml index 7b508a462c..e6cf2a3bcb 100644 --- a/man/systemd-run.xml +++ b/man/systemd-run.xml @@ -176,11 +176,14 @@ Make the new .service or .scope unit part - of the inherited slice. This option can be combined with . + of the slice the systemd-run itself has been invoked in. This option may be + combined with , in which case the slice specified via + is placed within the slice the systemd-run command is + invoked in. - An inherited slice is located within systemd-run slice. Example: if - systemd-run slice is foo.slice, and the - argument is bar, the unit will be placed under the + Example: consider systemd-run being invoked in the slice + foo.slice, and the argument is + bar. The unit will then be placed under foo-bar.slice. diff --git a/src/run/run.c b/src/run/run.c index 920e38af0c..45da75f889 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -110,7 +110,7 @@ static int help(void) { " -p --property=NAME=VALUE Set service or scope unit property\n" " --description=TEXT Description for unit\n" " --slice=SLICE Run in the specified slice\n" - " --slice-inherit Inherit the slice\n" + " --slice-inherit Inherit the slice from the caller\n" " --expand-environment=BOOL Control expansion of environment variables\n" " --no-block Do not wait until operation finished\n" " -r --remain-after-exit Leave service around until explicitly stopped\n" From dd25a95763e6ee52d40f4012ffeb2ce719f26b8d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 20 Dec 2023 19:10:13 +0100 Subject: [PATCH 12/13] test: add minimal integration test coverage for uid0 tool --- test/units/testsuite-74.run.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/units/testsuite-74.run.sh b/test/units/testsuite-74.run.sh index 2caca7a5a2..d43d17ca61 100755 --- a/test/units/testsuite-74.run.sh +++ b/test/units/testsuite-74.run.sh @@ -227,5 +227,9 @@ B=$(systemd-run -q --wait --pipe -p ProtectProc=invisible cat /proc/version) assert_eq "$A" "$B" +# Check that invoking the tool under the uid0 alias name works +uid0 ls / +echo "$(uid0 echo foo)" = "foo" + umount /proc/version rm "$V" From 73d8041c9a943acde09fa55f650a8604321a78e9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 20 Dec 2023 11:58:02 +0100 Subject: [PATCH 13/13] update TODO --- TODO | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/TODO b/TODO index e480f70ef7..4d2fd411dc 100644 --- a/TODO +++ b/TODO @@ -132,6 +132,10 @@ Deprecations and removals: Features: +* add a new ExecStart= flag that inserts the configured user's shell as first + word in the comand line. (maybe use character '.'). Usecase: tool such as + uid0 can use that to spawn the target user's default shell. + * varlink: figure out how to do docs for our varlink interfaces. Idea: install interface files augmented with docs in /usr/share/ somewhere. And have functionality in varlinkctl to merge interface info extracted from binaries @@ -140,14 +144,6 @@ Features: * introduce mntid_t, and make it 64bit, as apparently the kernel switched to 64bit mount ids -* Add an alias to systemd-run maybe called "uid0" or so, which tries to mimic - the sudo/su command lines to some level, but is backed by transient services, - and proper security isolate/tty forwarding. This would then allow us to run - systems with NNP turned on reasonably nicely. To make this extra nice and - pretty, in ptyfwd rewrite every NL we pass through so that it resets the bg - color to some reddish tone, and erase the whole coming line first, so that - the background color indicates when operating with privileges and when not. - * use udev rule networkd ownership property to take ownership of network interfaces nspawn creates