From: Mike Yuan Date: Wed, 9 Apr 2025 13:22:11 +0000 (+0200) Subject: core: accept "|" ExecStart= prefix to spawn target user's shell X-Git-Tag: v258-rc1~643^2~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5b8bcbcf0032ed8b4b0161be842012b33d327b5b;p=thirdparty%2Fsystemd.git core: accept "|" ExecStart= prefix to spawn target user's shell When switching to another user it's oftentimes desirable to also spawn the target user's shell. sudo supports this via -i flag, run0 currently doesn't. We don't want to proactively query NSS ourselves, since that would fall short when operating remotely. Let's instead teach the service manager to spawn the command using the user's default shell. I opted for "|" instead of "." in the end because the latter seems a bit obscure. But happy to change it to something else if a better option comes up. --- diff --git a/TODO b/TODO index d91dbcee286..436363e836a 100644 --- a/TODO +++ b/TODO @@ -720,10 +720,6 @@ Features: * machined: optionally track nspawn unix-export/ runtime for each machined, and then update systemd-ssh-proxy so that it can connect to that. -* add a new ExecStart= flag that inserts the configured user's shell as first - word in the command line. (maybe use character '.'). Usecase: tool such as - run0 can use that to spawn the target user's default shell. - * introduce mntid_t, and make it 64bit, as apparently the kernel switched to 64bit mount ids diff --git a/man/systemd.service.xml b/man/systemd.service.xml index b03e142725a..cc9350f5916 100644 --- a/man/systemd.service.xml +++ b/man/systemd.service.xml @@ -1397,7 +1397,7 @@ @ - If the executable path is prefixed with @, the second specified token will be passed as argv[0] to the executed process (instead of the actual filename), followed by the further arguments specified. + If the executable path is prefixed with @, the second specified token will be passed as argv[0] to the executed process (instead of the actual filename), followed by the further arguments specified, unless | is also specified, in which case it enables login shell semantics for the shell spawned by prefixing - to argv[0]. @@ -1420,11 +1420,17 @@ Similar to the + character discussed above this permits invoking command lines with elevated privileges. However, unlike + the ! character exclusively alters the effect of User=, Group= and SupplementaryGroups=, i.e. only the stanzas that affect user and group credentials. Note that this setting may be combined with DynamicUser=, in which case a dynamic user/group pair is allocated before the command is invoked, but credential changing is left to the executed process itself. + + + | + + If | is specified standalone as executable path, invoke the default shell of User=. If specified as a prefix, use the shell (-c) to spawn the executable. When @ is used in conjunction, argv[0] of shell will be prefixed with - to enable login shell semantics. + - @, -, :, and one of + @, |, -, :, and one of +/! may be used together and they can appear in any order. However, + and ! may not be specified at the same time. @@ -1489,9 +1495,9 @@ ExecStart=/bin/echo $ONE $TWO $THREE includes e.g. $USER, but not $TERM). - Note that shell command lines are not directly supported. If - shell command lines are to be used, they need to be passed - explicitly to a shell implementation of some kind. Example: + Note that shell command lines are not directly supported, and | invokes the user's + default shell which isn't deterministic. It's recommended to specify a shell implementation explicitly + if portability is desired. Example: ExecStart=sh -c 'dmesg | tac' Example: diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c index 560594d8a15..f7b2932e07a 100644 --- a/src/core/dbus-execute.c +++ b/src/core/dbus-execute.c @@ -1472,11 +1472,12 @@ int bus_property_get_exec_ex_command_list( return sd_bus_message_close_container(reply); } -static char *exec_command_flags_to_exec_chars(ExecCommandFlags flags) { +static char* exec_command_flags_to_exec_chars(ExecCommandFlags flags) { return strjoin(FLAGS_SET(flags, EXEC_COMMAND_IGNORE_FAILURE) ? "-" : "", FLAGS_SET(flags, EXEC_COMMAND_NO_ENV_EXPAND) ? ":" : "", FLAGS_SET(flags, EXEC_COMMAND_FULLY_PRIVILEGED) ? "+" : "", - FLAGS_SET(flags, EXEC_COMMAND_NO_SETUID) ? "!" : ""); + FLAGS_SET(flags, EXEC_COMMAND_NO_SETUID) ? "!" : "", + FLAGS_SET(flags, EXEC_COMMAND_VIA_SHELL) ? "|" : ""); } int bus_set_transient_exec_command( @@ -1504,30 +1505,58 @@ int bus_set_transient_exec_command( return r; while ((r = sd_bus_message_enter_container(message, 'r', ex_prop ? "sasas" : "sasb")) > 0) { - _cleanup_strv_free_ char **argv = NULL, **ex_opts = NULL; + _cleanup_strv_free_ char **argv = NULL; const char *path; - int b; + ExecCommandFlags command_flags; r = sd_bus_message_read(message, "s", &path); if (r < 0) return r; - if (!filename_or_absolute_path_is_valid(path)) - return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, - "\"%s\" is neither a valid executable name nor an absolute path", - path); - r = sd_bus_message_read_strv(message, &argv); if (r < 0) return r; - if (strv_isempty(argv)) - return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, - "\"%s\" argv cannot be empty", name); + if (ex_prop) { + _cleanup_strv_free_ char **ex_opts = NULL; - r = ex_prop ? sd_bus_message_read_strv(message, &ex_opts) : sd_bus_message_read(message, "b", &b); - if (r < 0) - return r; + r = sd_bus_message_read_strv(message, &ex_opts); + if (r < 0) + return r; + + r = exec_command_flags_from_strv(ex_opts, &command_flags); + if (r < 0) + return r; + } else { + int b; + + r = sd_bus_message_read(message, "b", &b); + if (r < 0) + return r; + + command_flags = b ? EXEC_COMMAND_IGNORE_FAILURE : 0; + } + + if (!FLAGS_SET(command_flags, EXEC_COMMAND_VIA_SHELL)) { + if (!filename_or_absolute_path_is_valid(path)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, + "\"%s\" is neither a valid executable name nor an absolute path", + path); + + if (strv_isempty(argv)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, + "\"%s\" argv cannot be empty", name); + } else { + /* Always normalize path and argv0 to be "sh" */ + path = _PATH_BSHELL; + + if (strv_isempty(argv)) + r = strv_extend(&argv, path); + else + r = free_and_strdup(&argv[0], argv[0][0] == '-' ? "-sh" : "sh"); + if (r < 0) + return r; + } r = sd_bus_message_exit_container(message); if (r < 0) @@ -1542,19 +1571,13 @@ int bus_set_transient_exec_command( *c = (ExecCommand) { .argv = TAKE_PTR(argv), + .flags = command_flags, }; r = path_simplify_alloc(path, &c->path); if (r < 0) return r; - if (ex_prop) { - r = exec_command_flags_from_strv(ex_opts, &c->flags); - if (r < 0) - return r; - } else if (b) - c->flags |= EXEC_COMMAND_IGNORE_FAILURE; - exec_command_append_list(exec_command, TAKE_PTR(c)); } @@ -1585,17 +1608,19 @@ int bus_set_transient_exec_command( _cleanup_free_ char *a = NULL, *exec_chars = NULL; UnitWriteFlags esc_flags = UNIT_ESCAPE_SPECIFIERS | (FLAGS_SET(c->flags, EXEC_COMMAND_NO_ENV_EXPAND) ? UNIT_ESCAPE_EXEC_SYNTAX : UNIT_ESCAPE_EXEC_SYNTAX_ENV); + bool via_shell = FLAGS_SET(c->flags, EXEC_COMMAND_VIA_SHELL); exec_chars = exec_command_flags_to_exec_chars(c->flags); if (!exec_chars) return -ENOMEM; - a = unit_concat_strv(c->argv, esc_flags); + a = unit_concat_strv(via_shell ? strv_skip(c->argv, 1) : c->argv, esc_flags); if (!a) return -ENOMEM; - if (streq_ptr(c->path, c->argv ? c->argv[0] : NULL)) - fprintf(f, "%s=%s%s\n", written_name, exec_chars, a); + if (via_shell || streq(c->path, c->argv[0])) + fprintf(f, "%s=%s%s%s\n", + written_name, exec_chars, via_shell && c->argv[0][0] == '-' ? "@" : "", a); else { _cleanup_free_ char *t = NULL; const char *p; diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index b0bf5025767..3b7e9aa626a 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -4572,12 +4572,11 @@ int exec_invoke( const CGroupContext *cgroup_context, int *exit_status) { - _cleanup_strv_free_ char **our_env = NULL, **pass_env = NULL, **joined_exec_search_path = NULL, **accum_env = NULL, **replaced_argv = NULL; + _cleanup_strv_free_ char **our_env = NULL, **pass_env = NULL, **joined_exec_search_path = NULL, **accum_env = NULL; int r; const char *username = NULL, *groupname = NULL; _cleanup_free_ char *home_buffer = NULL, *memory_pressure_path = NULL, *own_user = NULL; const char *pwent_home = NULL, *shell = NULL; - char **final_argv = NULL; dev_t journal_stream_dev = 0; ino_t journal_stream_ino = 0; bool needs_sandboxing, /* Do we need to set up full sandboxing? (i.e. all namespacing, all MAC stuff, caps, yadda yadda */ @@ -4807,7 +4806,7 @@ int exec_invoke( if (context->user) u = context->user; - else if (context->pam_name) { + else if (context->pam_name || FLAGS_SET(command->flags, EXEC_COMMAND_VIA_SHELL)) { /* If PAM is enabled but no user name is explicitly selected, then use our own one. */ own_user = getusername_malloc(); if (!own_user) { @@ -5406,17 +5405,26 @@ int exec_invoke( /* Now that the mount namespace has been set up and privileges adjusted, let's look for the thing we * shall execute. */ + const char *path = command->path; + + if (FLAGS_SET(command->flags, EXEC_COMMAND_VIA_SHELL)) { + if (shell_is_placeholder(shell)) { + log_debug("Shell prefixing requested for user without default shell, using /bin/sh: %s", + strna(username)); + assert(streq(path, _PATH_BSHELL)); + } else + path = shell; + } + _cleanup_free_ char *executable = NULL; _cleanup_close_ int executable_fd = -EBADF; - r = find_executable_full(command->path, /* root= */ NULL, context->exec_search_path, false, &executable, &executable_fd); + r = find_executable_full(path, /* root= */ NULL, context->exec_search_path, false, &executable, &executable_fd); if (r < 0) { *exit_status = EXIT_EXEC; log_struct_errno(LOG_NOTICE, r, LOG_MESSAGE_ID(SD_MESSAGE_SPAWN_FAILED_STR), - LOG_EXEC_MESSAGE(params, - "Unable to locate executable '%s': %m", - command->path), - LOG_ITEM("EXECUTABLE=%s", command->path)); + LOG_EXEC_MESSAGE(params, "Unable to locate executable '%s': %m", path), + LOG_ITEM("EXECUTABLE=%s", path)); /* If the error will be ignored by manager, tune down the log level here. Missing executable * is very much expected in this case. */ return r != -ENOMEM && FLAGS_SET(command->flags, EXEC_COMMAND_IGNORE_FAILURE) ? 1 : r; @@ -5827,10 +5835,13 @@ int exec_invoke( strv_free_and_replace(accum_env, ee); } - if (!FLAGS_SET(command->flags, EXEC_COMMAND_NO_ENV_EXPAND)) { + _cleanup_strv_free_ char **replaced_argv = NULL, **argv_via_shell = NULL; + char **final_argv = FLAGS_SET(command->flags, EXEC_COMMAND_VIA_SHELL) ? strv_skip(command->argv, 1) : command->argv; + + if (final_argv && !FLAGS_SET(command->flags, EXEC_COMMAND_NO_ENV_EXPAND)) { _cleanup_strv_free_ char **unset_variables = NULL, **bad_variables = NULL; - r = replace_env_argv(command->argv, accum_env, &replaced_argv, &unset_variables, &bad_variables); + r = replace_env_argv(final_argv, accum_env, &replaced_argv, &unset_variables, &bad_variables); if (r < 0) { *exit_status = EXIT_MEMORY; return log_error_errno(r, "Failed to replace environment variables: %m"); @@ -5846,8 +5857,33 @@ int exec_invoke( _cleanup_free_ char *jb = strv_join(bad_variables, ", "); log_warning("Invalid environment variable name evaluates to an empty string: %s", strna(jb)); } - } else - final_argv = command->argv; + } + + if (FLAGS_SET(command->flags, EXEC_COMMAND_VIA_SHELL)) { + r = strv_extendf(&argv_via_shell, "%s%s", command->argv[0][0] == '-' ? "-" : "", path); + if (r < 0) { + *exit_status = EXIT_MEMORY; + return log_oom(); + } + + if (!strv_isempty(final_argv)) { + _cleanup_free_ char *cmdline_joined = NULL; + + cmdline_joined = strv_join(final_argv, " "); + if (!cmdline_joined) { + *exit_status = EXIT_MEMORY; + return log_oom(); + } + + r = strv_extend_many(&argv_via_shell, "-c", cmdline_joined); + if (r < 0) { + *exit_status = EXIT_MEMORY; + return log_oom(); + } + } + + final_argv = argv_via_shell; + } log_command_line(context, params, "Executing", executable, final_argv); diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index ca95f334079..67048fcd2d5 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -888,7 +888,7 @@ int config_parse_exec( bool semicolon; do { - _cleanup_free_ char *path = NULL, *firstword = NULL; + _cleanup_free_ char *firstword = NULL; semicolon = false; @@ -915,6 +915,8 @@ int config_parse_exec( * * "-": Ignore if the path doesn't exist * "@": Allow overriding argv[0] (supplied as a separate argument) + * "|": Prefix the cmdline with target user's shell (when combined with "@" invoke + * login shell semantics) * ":": Disable environment variable substitution * "+": Run with full privileges and no sandboxing * "!": Apply sandboxing except for user/group credentials @@ -926,6 +928,8 @@ int config_parse_exec( separate_argv0 = true; else if (*f == ':' && !FLAGS_SET(flags, EXEC_COMMAND_NO_ENV_EXPAND)) flags |= EXEC_COMMAND_NO_ENV_EXPAND; + else if (*f == '|' && !FLAGS_SET(flags, EXEC_COMMAND_VIA_SHELL)) + flags |= EXEC_COMMAND_VIA_SHELL; else if (*f == '+' && !(flags & (EXEC_COMMAND_FULLY_PRIVILEGED|EXEC_COMMAND_NO_SETUID)) && !ambient_hack) flags |= EXEC_COMMAND_FULLY_PRIVILEGED; else if (*f == '!' && !(flags & (EXEC_COMMAND_FULLY_PRIVILEGED|EXEC_COMMAND_NO_SETUID)) && !ambient_hack) @@ -947,45 +951,59 @@ int config_parse_exec( ignore = FLAGS_SET(flags, EXEC_COMMAND_IGNORE_FAILURE); - r = unit_path_printf(u, f, &path); - if (r < 0) { - log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, r, - "Failed to resolve unit specifiers in '%s'%s: %m", - f, ignore ? ", ignoring" : ""); - return ignore ? 0 : -ENOEXEC; - } + _cleanup_strv_free_ char **args = NULL; + _cleanup_free_ char *path = NULL; + + if (FLAGS_SET(flags, EXEC_COMMAND_VIA_SHELL)) { + /* Use _PATH_BSHELL as placeholder since we can't do NSS lookups in pid1. This would + * be exported to various dbus properties and is used to determine SELinux label - + * which isn't accurate, but is a best-effort thing to assume all shells have more + * or less the same label. */ + path = strdup(_PATH_BSHELL); + if (!path) + return log_oom(); - if (isempty(path)) { - log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, 0, - "Empty path in command line%s: %s", - ignore ? ", ignoring" : "", rvalue); - return ignore ? 0 : -ENOEXEC; - } - if (!string_is_safe(path)) { - log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, 0, - "Executable path contains special characters%s: %s", - ignore ? ", ignoring" : "", path); - return ignore ? 0 : -ENOEXEC; - } - if (path_implies_directory(path)) { - log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, 0, - "Executable path specifies a directory%s: %s", - ignore ? ", ignoring" : "", path); - return ignore ? 0 : -ENOEXEC; - } + if (strv_extend_many(&args, separate_argv0 ? "-sh" : "sh", empty_to_null(f)) < 0) + return log_oom(); + } else { + r = unit_path_printf(u, f, &path); + if (r < 0) { + log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, r, + "Failed to resolve unit specifiers in '%s'%s: %m", + f, ignore ? ", ignoring" : ""); + return ignore ? 0 : -ENOEXEC; + } - if (!filename_or_absolute_path_is_valid(path)) { - log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, 0, - "Neither a valid executable name nor an absolute path%s: %s", - ignore ? ", ignoring" : "", path); - return ignore ? 0 : -ENOEXEC; - } + if (isempty(path)) { + log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, 0, + "Empty path in command line%s: %s", + ignore ? ", ignoring" : "", rvalue); + return ignore ? 0 : -ENOEXEC; + } + if (!string_is_safe(path)) { + log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, 0, + "Executable path contains special characters%s: %s", + ignore ? ", ignoring" : "", path); + return ignore ? 0 : -ENOEXEC; + } + if (path_implies_directory(path)) { + log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, 0, + "Executable path specifies a directory%s: %s", + ignore ? ", ignoring" : "", path); + return ignore ? 0 : -ENOEXEC; + } - _cleanup_strv_free_ char **args = NULL; + if (!filename_or_absolute_path_is_valid(path)) { + log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, 0, + "Neither a valid executable name nor an absolute path%s: %s", + ignore ? ", ignoring" : "", path); + return ignore ? 0 : -ENOEXEC; + } - if (!separate_argv0) - if (strv_extend(&args, path) < 0) - return log_oom(); + if (!separate_argv0) + if (strv_extend(&args, path) < 0) + return log_oom(); + } while (!isempty(p)) { _cleanup_free_ char *word = NULL, *resolved = NULL; diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index c7bb1b25334..63478664535 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -331,17 +331,28 @@ static int bus_append_exec_command(sd_bus_message *m, const char *field, const c } break; + case '|': + if (FLAGS_SET(flags, EXEC_COMMAND_VIA_SHELL)) + done = true; + else { + flags |= EXEC_COMMAND_VIA_SHELL; + eq++; + } + break; + default: done = true; } } while (!done); - if (!is_ex_prop && (flags & (EXEC_COMMAND_NO_ENV_EXPAND|EXEC_COMMAND_FULLY_PRIVILEGED|EXEC_COMMAND_NO_SETUID))) { + if (!is_ex_prop && (flags & (EXEC_COMMAND_NO_ENV_EXPAND|EXEC_COMMAND_FULLY_PRIVILEGED|EXEC_COMMAND_NO_SETUID|EXEC_COMMAND_VIA_SHELL))) { /* Upgrade the ExecXYZ= property to ExecXYZEx= for convenience */ is_ex_prop = true; + upgraded_name = strjoin(field, "Ex"); if (!upgraded_name) return log_oom(); + field = upgraded_name; } if (is_ex_prop) { @@ -350,7 +361,12 @@ static int bus_append_exec_command(sd_bus_message *m, const char *field, const c return log_error_errno(r, "Failed to convert ExecCommandFlags to strv: %m"); } - if (explicit_path) { + if (FLAGS_SET(flags, EXEC_COMMAND_VIA_SHELL)) { + path = strdup(_PATH_BSHELL); + if (!path) + return log_oom(); + + } else if (explicit_path) { r = extract_first_word(&eq, &path, NULL, EXTRACT_UNQUOTE|EXTRACT_CUNESCAPE); if (r < 0) return log_error_errno(r, "Failed to parse path: %m"); @@ -364,11 +380,17 @@ static int bus_append_exec_command(sd_bus_message *m, const char *field, const c if (r < 0) return log_error_errno(r, "Failed to parse command line: %m"); + if (FLAGS_SET(flags, EXEC_COMMAND_VIA_SHELL)) { + r = strv_prepend(&l, explicit_path ? "-sh" : "sh"); + if (r < 0) + return log_oom(); + } + r = sd_bus_message_open_container(m, SD_BUS_TYPE_STRUCT, "sv"); if (r < 0) return bus_log_create_error(r); - r = sd_bus_message_append_basic(m, SD_BUS_TYPE_STRING, upgraded_name ?: field); + r = sd_bus_message_append_basic(m, SD_BUS_TYPE_STRING, field); if (r < 0) return bus_log_create_error(r); diff --git a/src/shared/exec-util.c b/src/shared/exec-util.c index 75f24aef985..13cd99e74fa 100644 --- a/src/shared/exec-util.c +++ b/src/shared/exec-util.c @@ -487,6 +487,7 @@ static const char* const exec_command_strings[] = { "privileged", /* EXEC_COMMAND_FULLY_PRIVILEGED */ "no-setuid", /* EXEC_COMMAND_NO_SETUID */ "no-env-expand", /* EXEC_COMMAND_NO_ENV_EXPAND */ + "via-shell", /* EXEC_COMMAND_VIA_SHELL */ }; assert_cc((1 << ELEMENTSOF(exec_command_strings)) - 1 == _EXEC_COMMAND_FLAGS_ALL); diff --git a/src/shared/exec-util.h b/src/shared/exec-util.h index 93d9e8c1118..52acf342c33 100644 --- a/src/shared/exec-util.h +++ b/src/shared/exec-util.h @@ -49,8 +49,9 @@ typedef enum ExecCommandFlags { EXEC_COMMAND_FULLY_PRIVILEGED = 1 << 1, EXEC_COMMAND_NO_SETUID = 1 << 2, EXEC_COMMAND_NO_ENV_EXPAND = 1 << 3, + EXEC_COMMAND_VIA_SHELL = 1 << 4, _EXEC_COMMAND_FLAGS_INVALID = -EINVAL, - _EXEC_COMMAND_FLAGS_ALL = (1 << 4) -1, + _EXEC_COMMAND_FLAGS_ALL = (1 << 5) -1, } ExecCommandFlags; int exec_command_flags_from_strv(char * const *ex_opts, ExecCommandFlags *ret);