core: accept "|" ExecStart= prefix to spawn target user's shell; teach run0 about the new logic (#37071)

I've always been reluctant to invoke the current user's shell in another
user's context, hence was fully grounded in `sudo -i`. With this bit in
place `run0` will finally be feature-complete on my side ;-)
This commit is contained in:
Mike Yuan
2025-05-12 16:10:03 +02:00
committed by GitHub
15 changed files with 322 additions and 108 deletions

4
TODO
View File

@@ -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

View File

@@ -167,6 +167,24 @@
</listitem>
</varlistentry>
<varlistentry>
<term><option>--via-shell</option></term>
<listitem><para>Invokes the target user's login shell and runs the specified command (if any) via it.</para>
<xi:include href="version-info.xml" xpointer="v258"/>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-i</option></term>
<listitem><para>Shortcut for <option>--via-shell --chdir='~'</option>.</para>
<xi:include href="version-info.xml" xpointer="v258"/>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--setenv=<replaceable>NAME</replaceable>[=<replaceable>VALUE</replaceable>]</option></term>
@@ -290,9 +308,12 @@
<para>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 <option>--setenv=SHELL=…</option> and currently defaults to the
<emphasis>originating user's</emphasis> shell (i.e. not the target user's!) if operating locally, or
<filename>/bin/sh</filename> when operating with <option>--machine=</option>.</para>
invoke may be controlled through <option>--via-shell</option> - when specified the target user's shell
is used - or <option>--setenv=SHELL=…</option>. By default, the <emphasis>originating user's</emphasis> shell
is executed if operating locally, or <filename>/bin/sh</filename> when operating with <option>--machine=</option>.</para>
<para>Note that unlike <command>sudo</command>, <command>run0</command> always spawns shells with login shell
semantics, regardless of <option>-i</option>.</para>
</refsect1>
<refsect1>

View File

@@ -1397,7 +1397,7 @@
<tbody>
<row>
<entry><literal>@</literal></entry>
<entry>If the executable path is prefixed with <literal>@</literal>, the second specified token will be passed as <constant>argv[0]</constant> to the executed process (instead of the actual filename), followed by the further arguments specified.</entry>
<entry>If the executable path is prefixed with <literal>@</literal>, the second specified token will be passed as <constant>argv[0]</constant> to the executed process (instead of the actual filename), followed by the further arguments specified, unless <literal>|</literal> is also specified, in which case it enables login shell semantics for the shell spawned by prefixing <literal>-</literal> to <constant>argv[0]</constant>.</entry>
</row>
<row>
@@ -1420,14 +1420,19 @@
<entry>Similar to the <literal>+</literal> character discussed above this permits invoking command lines with elevated privileges. However, unlike <literal>+</literal> the <literal>!</literal> character exclusively alters the effect of <varname>User=</varname>, <varname>Group=</varname> and <varname>SupplementaryGroups=</varname>, i.e. only the stanzas that affect user and group credentials. Note that this setting may be combined with <varname>DynamicUser=</varname>, 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.</entry>
</row>
<row>
<entry><literal>|</literal></entry>
<entry>If <literal>|</literal> is specified standalone as executable path, invoke the default shell of <varname>User=</varname>. If specified as a prefix, use the shell (<literal>-c</literal>) to spawn the executable. When <literal>@</literal> is used in conjunction, <constant>argv[0]</constant> of shell will be prefixed with <literal>-</literal> to enable login shell semantics.</entry>
</row>
</tbody>
</tgroup>
</table>
<para><literal>@</literal>, <literal>-</literal>, <literal>:</literal>, and one of
<literal>+</literal>/<literal>!</literal>/<literal>!!</literal> may be used together and they can appear in any
order. However, only one of <literal>+</literal>, <literal>!</literal>, <literal>!!</literal> may be used at a
time.</para>
<para><literal>@</literal>, <literal>|</literal>, <literal>-</literal>, <literal>:</literal>, and one of
<literal>+</literal>/<literal>!</literal> may be used together and they can appear in any order.
However, <literal>+</literal> and <literal>!</literal> may not be specified at the same time.</para>
<para>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</programlisting>
includes e.g. <varname>$USER</varname>, but not
<varname>$TERM</varname>).</para>
<para>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:</para>
<para>Note that shell command lines are not directly supported, and <literal>|</literal> invokes the user's
default shell which isn't deterministic. It's recommended to specify a shell implementation explicitly
if portability is desired. Example:</para>
<programlisting>ExecStart=sh -c 'dmesg | tac'</programlisting>
<para>Example:</para>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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"

View File

@@ -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