From 72fc33fe201b767f905223fd41c93fb3350b84ed Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Thu, 10 Apr 2025 21:00:27 +0200 Subject: [PATCH 1/8] man/systemd.service: drop dangling reference to "!!" prefix Follow-up for 00a415fc8f9e3469549a56d29f448b8cf14b0598 --- man/systemd.service.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/man/systemd.service.xml b/man/systemd.service.xml index d855c12acb..b03e142725 100644 --- a/man/systemd.service.xml +++ b/man/systemd.service.xml @@ -1425,9 +1425,8 @@ @, -, :, and one of - +/!/!! may be used together and they can appear in any - order. However, only one of +, !, !! may be used at a - time. + +/! 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 From 72758ef117b63846953ee775d30864fa17b881cb Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Wed, 9 Apr 2025 20:54:42 +0200 Subject: [PATCH 2/8] env-util: add missing assertions --- src/basic/env-util.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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); From 91d5fad9fbbe91617c503bd48d3f16b1f47e3f50 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Thu, 17 Apr 2025 23:57:57 +0200 Subject: [PATCH 3/8] bus-unit-util: do not trigger assertion on "ExecStart=@" extract_first_word() normalizes empty string to NULL, triggering the assertion on input string in strv_split_full(). --- src/shared/bus-unit-util.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index c0572aff15..c7bb1b2533 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -354,6 +354,10 @@ static int bus_append_exec_command(sd_bus_message *m, const char *field, const c 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); From 398b3e44722e5a3a7a78ce16aa6ece3dc80e72a6 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Mon, 5 May 2025 20:12:05 +0200 Subject: [PATCH 4/8] path-util: introduce filename_or_absolute_path_is_valid() helper --- src/basic/path-util.h | 6 ++++++ src/core/dbus-execute.c | 2 +- src/core/load-fragment.c | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) 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..560594d8a1 100644 --- a/src/core/dbus-execute.c +++ b/src/core/dbus-execute.c @@ -1512,7 +1512,7 @@ int bus_set_transient_exec_command( if (r < 0) return r; - if (!path_is_absolute(path) && !filename_is_valid(path)) + 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); diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index e718dbc76b..ca95f33407 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -974,7 +974,7 @@ int config_parse_exec( return ignore ? 0 : -ENOEXEC; } - if (!(path_is_absolute(path) ? path_is_valid(path) : filename_is_valid(path))) { + 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); From 5b8bcbcf0032ed8b4b0161be842012b33d327b5b Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Wed, 9 Apr 2025 15:22:11 +0200 Subject: [PATCH 5/8] 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. --- TODO | 4 -- man/systemd.service.xml | 16 ++++--- src/core/dbus-execute.c | 75 ++++++++++++++++++++----------- src/core/exec-invoke.c | 60 ++++++++++++++++++++----- src/core/load-fragment.c | 92 +++++++++++++++++++++++--------------- src/shared/bus-unit-util.c | 28 ++++++++++-- src/shared/exec-util.c | 1 + src/shared/exec-util.h | 3 +- 8 files changed, 192 insertions(+), 87 deletions(-) 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); From fa02d58d858233e10b7524e6c7fcacfa6031aee6 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Wed, 7 May 2025 00:36:41 +0200 Subject: [PATCH 6/8] run0: support --chdir='~' for switching to target user's home dir parse_path_argument() unconditionally makes the passed path absolute, without handling '~' of any sort. I think this generally makes sense in most tools, since ~ expansion is typically done by the shell and we wouldn't be seeing them in the first place and hence special casing is not worth it. But in run0 let's explicit enable '~'. --- src/run/run.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/run/run.c b/src/run/run.c index 7b6fe01d56..4504c97df1 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -924,8 +924,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; From e61d2011d1304fedddf60e4ee21770d34d81d939 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Wed, 9 Apr 2025 00:55:27 +0200 Subject: [PATCH 7/8] run0: introduce --via-shell for invoking target user's shell, and -i shortcut -i/--login has exact sudo semantics. But we only document the short option and advertise expressly specifying --via-shell --chdir='~' otherwise. --- man/run0.xml | 27 +++++++++++++++++++++--- src/run/run.c | 57 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 69 insertions(+), 15 deletions(-) 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/src/run/run.c b/src/run/run.c index 4504c97df1..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) { @@ -981,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; @@ -1029,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"); @@ -1056,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); @@ -1271,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)); @@ -1461,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"); @@ -2821,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); From 66a3b69440745521b516707f59618ef68185f1b3 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Wed, 9 Apr 2025 17:35:33 +0200 Subject: [PATCH 8/8] test: add test cases for ExecStart= via-shell prefix --- .../TEST-07-PID1.units/prefix-shell.service | 8 +++++++ test/units/TEST-07-PID1.prefix-shell.sh | 22 +++++++++++++++++++ test/units/TEST-74-AUX-UTILS.run.sh | 10 +++++++++ 3 files changed, 40 insertions(+) create mode 100755 test/integration-tests/TEST-07-PID1/TEST-07-PID1.units/prefix-shell.service create mode 100755 test/units/TEST-07-PID1.prefix-shell.sh 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