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);