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/run0.xml b/man/run0.xml index 262fa45ddc..d853e2d463 100644 --- a/man/run0.xml +++ b/man/run0.xml @@ -167,6 +167,24 @@ + + + + Invokes the target user's login shell and runs the specified command (if any) via it. + + + + + + + + + Shortcut for . + + + + + @@ -290,9 +308,12 @@ 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 . + invoke may be controlled through - when specified the target user's shell + is used - or . By default, the originating user's shell + is executed if operating locally, or /bin/sh when operating with . + + Note that unlike sudo, run0 always spawns shells with login shell + semantics, regardless of . diff --git a/man/systemd.service.xml b/man/systemd.service.xml index d855c12acb..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,14 +1420,19 @@ 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 - +/!/!! may be used together and they can appear in any - order. However, only one of +, !, !! may be used at a - time. + @, |, -, :, and one of + +/! may be used together and they can appear in any order. + However, + and ! may not be specified at the same time. For each command, the first argument must be either an absolute path to an executable or a simple file name without any slashes. If the command is not a full (absolute) path, it will be resolved to a @@ -1490,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/basic/env-util.c b/src/basic/env-util.c index 77a8ccb388..b97eac070b 100644 --- a/src/basic/env-util.c +++ b/src/basic/env-util.c @@ -885,9 +885,12 @@ int replace_env_argv( char ***ret_bad_variables) { _cleanup_strv_free_ char **n = NULL, **unset_variables = NULL, **bad_variables = NULL; - size_t k = 0, l = 0; + size_t k = 0, l; int r; + assert(!strv_isempty(argv)); + assert(ret); + l = strv_length(argv); n = new(char*, l+1); diff --git a/src/basic/path-util.h b/src/basic/path-util.h index 647a884e39..671c1363c4 100644 --- a/src/basic/path-util.h +++ b/src/basic/path-util.h @@ -145,6 +145,12 @@ static inline bool path_is_safe(const char *p) { return path_is_valid_full(p, /* accept_dot_dot= */ false); } bool path_is_normalized(const char *p) _pure_; +static inline bool filename_or_absolute_path_is_valid(const char *p) { + if (path_is_absolute(p)) + return path_is_valid(p); + + return filename_is_valid(p); +} int file_in_same_dir(const char *path, const char *filename, char **ret); diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c index 34511181bf..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 (!path_is_absolute(path) && !filename_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 77fc647f7b..8de8660b32 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -4588,12 +4588,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 */ @@ -4823,7 +4822,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) { @@ -5422,17 +5421,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; @@ -5843,10 +5851,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"); @@ -5862,8 +5873,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 93270de82a..79eb1757d0 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 (!(path_is_absolute(path) ? path_is_valid(path) : filename_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/run/run.c b/src/run/run.c index 7b6fe01d56..002874d282 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -104,6 +104,7 @@ static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF; static char *arg_shell_prompt_prefix = NULL; static int arg_lightweight = -1; static char *arg_area = NULL; +static bool arg_via_shell = false; STATIC_DESTRUCTOR_REGISTER(arg_description, freep); STATIC_DESTRUCTOR_REGISTER(arg_environment, strv_freep); @@ -215,6 +216,8 @@ static int help_sudo_mode(void) { " -g --group=GROUP Run as system group\n" " --nice=NICE Nice level\n" " -D --chdir=PATH Set working directory\n" + " --via-shell Invoke command via target user's login shell\n" + " -i Shortcut for --via-shell --chdir='~'\n" " --setenv=NAME[=VALUE] Set environment variable\n" " --background=COLOR Set ANSI color for background\n" " --pty Request allocation of a pseudo TTY for stdio\n" @@ -257,7 +260,7 @@ static int add_timer_property(const char *name, const char *val) { return 0; } -static char **make_login_shell_cmdline(const char *shell) { +static char** make_login_shell_cmdline(const char *shell) { _cleanup_free_ char *argv0 = NULL; assert(shell); @@ -826,6 +829,7 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) { ARG_PIPE, ARG_SHELL_PROMPT_PREFIX, ARG_LIGHTWEIGHT, + ARG_VIA_SHELL, }; /* If invoked as "run0" binary, let's expose a more sudo-like interface. We add various extensions @@ -845,6 +849,8 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) { { "group", required_argument, NULL, 'g' }, { "nice", required_argument, NULL, ARG_NICE }, { "chdir", required_argument, NULL, 'D' }, + { "via-shell", no_argument, NULL, ARG_VIA_SHELL }, + { "login", no_argument, NULL, 'i' }, /* compat with sudo, --via-shell + --chdir='~' */ { "setenv", required_argument, NULL, ARG_SETENV }, { "background", required_argument, NULL, ARG_BACKGROUND }, { "pty", no_argument, NULL, ARG_PTY }, @@ -864,7 +870,7 @@ static int parse_argv_sudo_mode(int argc, char *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:a:", options, NULL)) >= 0) + while ((c = getopt_long(argc, argv, "+hVu:g:D:a:i", options, NULL)) >= 0) switch (c) { @@ -924,8 +930,11 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) { break; case 'D': - /* Root will be manually suppressed later. */ - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_working_directory); + if (streq(optarg, "~")) + r = free_and_strdup_warn(&arg_working_directory, optarg); + else + /* Root will be manually suppressed later. */ + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_working_directory); if (r < 0) return r; @@ -978,6 +987,16 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) { break; + case 'i': + r = free_and_strdup_warn(&arg_working_directory, "~"); + if (r < 0) + return r; + + _fallthrough_; + case ARG_VIA_SHELL: + arg_via_shell = true; + break; + case '?': return -EINVAL; @@ -1026,9 +1045,11 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) { arg_send_sighup = true; _cleanup_strv_free_ char **l = NULL; - if (argc > optind) + if (argc > optind) { l = strv_copy(argv + optind); - else { + if (!l) + return log_oom(); + } else if (!arg_via_shell) { const char *e; e = strv_env_get(arg_environment, "SHELL"); @@ -1053,9 +1074,19 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) { } l = make_login_shell_cmdline(arg_exec_path); + if (!l) + return log_oom(); + } + + if (arg_via_shell) { + arg_exec_path = strdup(_PATH_BSHELL); + if (!arg_exec_path) + return log_oom(); + + r = strv_prepend(&l, "-sh"); + if (r < 0) + return log_oom(); } - if (!l) - return log_oom(); strv_free_and_replace(arg_cmdline, l); @@ -1268,10 +1299,8 @@ static int transient_kill_set_properties(sd_bus_message *m) { static int transient_service_set_properties(sd_bus_message *m, const char *pty_path, int pty_fd) { int r, send_term; /* tri-state */ - /* We disable environment expansion on the server side via ExecStartEx=:. - * ExecStartEx was added relatively recently (v243), and some bugs were fixed only later. - * So use that feature only if required. It will fail with older systemds. */ - bool use_ex_prop = !arg_expand_environment; + /* Use ExecStartEx if new exec flags are required. */ + bool use_ex_prop = !arg_expand_environment || arg_via_shell; assert(m); assert((!!pty_path) == (pty_fd >= 0)); @@ -1458,7 +1487,9 @@ static int transient_service_set_properties(sd_bus_message *m, const char *pty_p _cleanup_strv_free_ char **opts = NULL; r = exec_command_flags_to_strv( - (arg_expand_environment ? 0 : EXEC_COMMAND_NO_ENV_EXPAND)|(arg_ignore_failure ? EXEC_COMMAND_IGNORE_FAILURE : 0), + (arg_expand_environment ? 0 : EXEC_COMMAND_NO_ENV_EXPAND)| + (arg_ignore_failure ? EXEC_COMMAND_IGNORE_FAILURE : 0)| + (arg_via_shell ? EXEC_COMMAND_VIA_SHELL : 0), &opts); if (r < 0) return log_error_errno(r, "Failed to format execute flags: %m"); @@ -2818,7 +2849,12 @@ static int run(int argc, char* argv[]) { if (strv_isempty(arg_cmdline)) t = strdup(arg_unit); - else if (startswith(arg_cmdline[0], "-")) { + else if (arg_via_shell) { + if (arg_cmdline[1]) + t = quote_command_line(arg_cmdline + 1, SHELL_ESCAPE_EMPTY); + else + t = strjoin("LOGIN", arg_exec_user ? ": " : NULL, arg_exec_user); + } else if (startswith(arg_cmdline[0], "-")) { /* Drop the login shell marker from the command line when generating the description, * in order to minimize user confusion. */ _cleanup_strv_free_ char **l = strv_copy(arg_cmdline); diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index c0572aff15..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,21 +361,36 @@ 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"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No executable path specified, refusing."); + if (isempty(eq)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Got empty command line, refusing."); } r = strv_split_full(&l, eq, NULL, EXTRACT_UNQUOTE|EXTRACT_CUNESCAPE); 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 99dc4d597c..acd476c692 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); diff --git a/test/integration-tests/TEST-07-PID1/TEST-07-PID1.units/prefix-shell.service b/test/integration-tests/TEST-07-PID1/TEST-07-PID1.units/prefix-shell.service new file mode 100755 index 0000000000..fcbf8e9b15 --- /dev/null +++ b/test/integration-tests/TEST-07-PID1/TEST-07-PID1.units/prefix-shell.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Service] +Type=oneshot +Environment=SHLVL=100 +ExecStartPre=-|false +ExecStart=|@echo with login shell $$SHELL: lvl $$SHLVL +ExecStart=:|"str='with normal shell'" printenv str +ExecStart=|echo YAY! >/tmp/TEST-07-PID1.prefix-shell.flag diff --git a/test/units/TEST-07-PID1.prefix-shell.sh b/test/units/TEST-07-PID1.prefix-shell.sh new file mode 100755 index 0000000000..c095ce7731 --- /dev/null +++ b/test/units/TEST-07-PID1.prefix-shell.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +systemd-run --wait --uid=nobody \ + -p ExecStartPre="|true" \ + -p ExecStartPre="|@echo a >/tmp/TEST-07-PID1.prefix-shell.flag" \ + true +assert_eq "$(cat /tmp/TEST-07-PID1.prefix-shell.flag)" "a" +rm /tmp/TEST-07-PID1.prefix-shell.flag + +systemctl start prefix-shell.service +assert_eq "$(cat /tmp/TEST-07-PID1.prefix-shell.flag)" "YAY!" + +journalctl --sync +journalctl -b -u prefix-shell.service --grep "with login shell .*: lvl 101" +journalctl -b -u prefix-shell.service --grep "with normal shell" diff --git a/test/units/TEST-74-AUX-UTILS.run.sh b/test/units/TEST-74-AUX-UTILS.run.sh index 3229a522b7..19b4f113bb 100755 --- a/test/units/TEST-74-AUX-UTILS.run.sh +++ b/test/units/TEST-74-AUX-UTILS.run.sh @@ -256,10 +256,20 @@ if [[ -e /usr/lib/pam.d/systemd-run0 ]] || [[ -e /etc/pam.d/systemd-run0 ]]; the # Validate that we actually went properly through PAM (XDG_SESSION_TYPE is set by pam_systemd) assert_eq "$(run0 ${tu:+"--user=$tu"} bash -c 'echo $XDG_SESSION_TYPE')" "unspecified" + # Test spawning via shell + assert_eq "$(run0 ${tu:+"--user=$tu"} --setenv=SHLVL=10 printenv SHLVL)" "10" + if [[ ! -v ASAN_OPTIONS ]]; then + assert_eq "$(run0 ${tu:+"--user=$tu"} --setenv=SHLVL=10 --via-shell echo \$SHLVL)" "11" + fi + if [[ -n "$tu" ]]; then # Validate that $SHELL is set to login shell of target user when cmdline is supplied (not invoking shell) TARGET_LOGIN_SHELL="$(getent passwd "$tu" | cut -d: -f7)" assert_eq "$(run0 --user="$tu" printenv SHELL)" "$TARGET_LOGIN_SHELL" + # ... or when the command is chained by login shell + if [[ ! -v ASAN_OPTIONS ]]; then + assert_eq "$(run0 --user="$tu" -i printenv SHELL)" "$TARGET_LOGIN_SHELL" + fi fi done # Let's chain a couple of run0 calls together, for fun