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