diff --git a/TODO b/TODO index d91dbcee28..436363e836 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 b03e142725..cc9350f591 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 560594d8a1..f7b2932e07 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 b0bf502576..3b7e9aa626 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 ca95f33407..67048fcd2d 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,46 +951,60 @@ 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; - } - - 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 (!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; - } - _cleanup_strv_free_ char **args = NULL; + _cleanup_free_ char *path = NULL; - if (!separate_argv0) - if (strv_extend(&args, path) < 0) + 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 (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 (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 (!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(); + } + 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 c7bb1b2533..6347866453 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 75f24aef98..13cd99e74f 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 93d9e8c111..52acf342c3 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);