Compare commits

...

9 Commits

Author SHA1 Message Date
Mike Yuan 18fe53b40a
Merge 7c78827c19 into 8e7ef6abb8 2025-04-18 03:46:25 +01:00
Mike Yuan 7c78827c19
test: add test cases for ExecStart= shell prefixing 2025-04-18 00:07:01 +02:00
Mike Yuan 1b88fa8ae4
run0: introduce -i/--login-environment for invoking target user's shell 2025-04-18 00:07:01 +02:00
Mike Yuan 2f7d51ee01
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.
2025-04-18 00:07:00 +02:00
Mike Yuan 74c2faff3d
run0: disable IgnoreSIGPIPE= for transient unit 2025-04-18 00:07:00 +02:00
Mike Yuan 354be2190f
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().
2025-04-18 00:07:00 +02:00
Mike Yuan d17ae799f2
env-util: add missing assertions 2025-04-18 00:02:36 +02:00
Mike Yuan 13d9fecdd0
man/systemd.service: drop dangling reference to "!!" prefix
Follow-up for 00a415fc8f
2025-04-18 00:02:36 +02:00
Mike Yuan d9ac4b925d
core/service: correct comment in service_deserialize_exec_command()
The index of ExecCommand is serialized, not PID.
2025-04-18 00:02:36 +02:00
15 changed files with 310 additions and 113 deletions

4
TODO
View File

@ -735,10 +735,6 @@ Features:
* machined: optionally track nspawn unix-export/ runtime for each machined, and * machined: optionally track nspawn unix-export/ runtime for each machined, and
then update systemd-ssh-proxy so that it can connect to that. 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 * introduce mntid_t, and make it 64bit, as apparently the kernel switched to
64bit mount ids 64bit mount ids

View File

@ -167,6 +167,18 @@
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><option>--login-environment</option></term>
<term><option>-i</option></term>
<listitem><para>Invokes the target user's login shell and runs the specified command (if any) via it.
If <option>-D/--chdir=</option> is not specified, additionally switch to the target user's home directory
even for the root user.</para>
<xi:include href="version-info.xml" xpointer="v258"/>
</listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term><option>--setenv=<replaceable>NAME</replaceable>[=<replaceable>VALUE</replaceable>]</option></term> <term><option>--setenv=<replaceable>NAME</replaceable>[=<replaceable>VALUE</replaceable>]</option></term>
@ -290,9 +302,12 @@
<para>All command line arguments after the first non-option argument become part of the command line of <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 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 invoke may be controlled via <option>-i/--login-environment</option> - when specified the target user's shell
<emphasis>originating user's</emphasis> shell (i.e. not the target user's!) if operating locally, or is used - or <option>--setenv=SHELL=…</option>. By default, the <emphasis>originating user's</emphasis> shell
<filename>/bin/sh</filename> when operating with <option>--machine=</option>.</para> 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>
<refsect1> <refsect1>

View File

@ -1397,7 +1397,7 @@
<tbody> <tbody>
<row> <row>
<entry><literal>@</literal></entry> <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>
<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> <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>
<row>
<entry><literal>|</literal></entry>
<entry>If <literal>|</literal> is specified standalone as executable path, invoke the user's default shell. 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> </tbody>
</tgroup> </tgroup>
</table> </table>
<para><literal>@</literal>, <literal>-</literal>, <literal>:</literal>, and one of <para><literal>@</literal>, <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 <literal>+</literal>/<literal>!</literal> may be used together and they can appear in any order.
order. However, only one of <literal>+</literal>, <literal>!</literal>, <literal>!!</literal> may be used at a However, <literal>+</literal> and <literal>!</literal> may not be specified at the same time.</para>
time.</para>
<para>For each command, the first argument must be either an absolute path to an executable or a simple <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 file name without any slashes. If the command is not a full (absolute) path, it will be resolved to a
@ -1490,11 +1495,6 @@ ExecStart=/bin/echo $ONE $TWO $THREE</programlisting>
includes e.g. <varname>$USER</varname>, but not includes e.g. <varname>$USER</varname>, but not
<varname>$TERM</varname>).</para> <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>
<programlisting>ExecStart=sh -c 'dmesg | tac'</programlisting>
<para>Example:</para> <para>Example:</para>
<programlisting>ExecStart=echo one <programlisting>ExecStart=echo one

View File

@ -884,9 +884,12 @@ int replace_env_argv(
char ***ret_bad_variables) { char ***ret_bad_variables) {
_cleanup_strv_free_ char **n = NULL, **unset_variables = NULL, **bad_variables = NULL; _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; int r;
assert(!strv_isempty(argv));
assert(ret);
l = strv_length(argv); l = strv_length(argv);
n = new(char*, l+1); n = new(char*, l+1);

View File

@ -1470,11 +1470,12 @@ int bus_property_get_exec_ex_command_list(
return sd_bus_message_close_container(reply); 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) ? "-" : "", return strjoin(FLAGS_SET(flags, EXEC_COMMAND_IGNORE_FAILURE) ? "-" : "",
FLAGS_SET(flags, EXEC_COMMAND_NO_ENV_EXPAND) ? ":" : "", FLAGS_SET(flags, EXEC_COMMAND_NO_ENV_EXPAND) ? ":" : "",
FLAGS_SET(flags, EXEC_COMMAND_FULLY_PRIVILEGED) ? "+" : "", 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_PREFIX_SHELL) ? "|" : "");
} }
int bus_set_transient_exec_command( int bus_set_transient_exec_command(
@ -1502,30 +1503,58 @@ int bus_set_transient_exec_command(
return r; return r;
while ((r = sd_bus_message_enter_container(message, 'r', ex_prop ? "sasas" : "sasb")) > 0) { 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; const char *path;
int b; ExecCommandFlags command_flags;
r = sd_bus_message_read(message, "s", &path); r = sd_bus_message_read(message, "s", &path);
if (r < 0) if (r < 0)
return r; 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); r = sd_bus_message_read_strv(message, &argv);
if (r < 0) if (r < 0)
return r; return r;
if (strv_isempty(argv)) if (ex_prop) {
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, _cleanup_strv_free_ char **ex_opts = NULL;
"\"%s\" argv cannot be empty", name);
r = ex_prop ? sd_bus_message_read_strv(message, &ex_opts) : sd_bus_message_read(message, "b", &b); r = sd_bus_message_read_strv(message, &ex_opts);
if (r < 0) if (r < 0)
return r; 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_PREFIX_SHELL)) {
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);
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 = "/bin/sh";
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); r = sd_bus_message_exit_container(message);
if (r < 0) if (r < 0)
@ -1540,19 +1569,13 @@ int bus_set_transient_exec_command(
*c = (ExecCommand) { *c = (ExecCommand) {
.argv = TAKE_PTR(argv), .argv = TAKE_PTR(argv),
.flags = command_flags,
}; };
r = path_simplify_alloc(path, &c->path); r = path_simplify_alloc(path, &c->path);
if (r < 0) if (r < 0)
return r; 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)); exec_command_append_list(exec_command, TAKE_PTR(c));
} }
@ -1583,17 +1606,19 @@ int bus_set_transient_exec_command(
_cleanup_free_ char *a = NULL, *exec_chars = NULL; _cleanup_free_ char *a = NULL, *exec_chars = NULL;
UnitWriteFlags esc_flags = UNIT_ESCAPE_SPECIFIERS | UnitWriteFlags esc_flags = UNIT_ESCAPE_SPECIFIERS |
(FLAGS_SET(c->flags, EXEC_COMMAND_NO_ENV_EXPAND) ? UNIT_ESCAPE_EXEC_SYNTAX : UNIT_ESCAPE_EXEC_SYNTAX_ENV); (FLAGS_SET(c->flags, EXEC_COMMAND_NO_ENV_EXPAND) ? UNIT_ESCAPE_EXEC_SYNTAX : UNIT_ESCAPE_EXEC_SYNTAX_ENV);
bool prefix_shell = FLAGS_SET(c->flags, EXEC_COMMAND_PREFIX_SHELL);
exec_chars = exec_command_flags_to_exec_chars(c->flags); exec_chars = exec_command_flags_to_exec_chars(c->flags);
if (!exec_chars) if (!exec_chars)
return -ENOMEM; return -ENOMEM;
a = unit_concat_strv(c->argv, esc_flags); a = unit_concat_strv(prefix_shell ? strv_skip(c->argv, 1) : c->argv, esc_flags);
if (!a) if (!a)
return -ENOMEM; return -ENOMEM;
if (streq_ptr(c->path, c->argv ? c->argv[0] : NULL)) if (prefix_shell || streq(c->path, c->argv[0]))
fprintf(f, "%s=%s%s\n", written_name, exec_chars, a); fprintf(f, "%s=%s%s%s\n",
written_name, exec_chars, prefix_shell && c->argv[0][0] == '-' ? "@" : "", a);
else { else {
_cleanup_free_ char *t = NULL; _cleanup_free_ char *t = NULL;
const char *p; const char *p;

View File

@ -4649,12 +4649,11 @@ int exec_invoke(
const CGroupContext *cgroup_context, const CGroupContext *cgroup_context,
int *exit_status) { 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; int r;
const char *username = NULL, *groupname = NULL; const char *username = NULL, *groupname = NULL;
_cleanup_free_ char *home_buffer = NULL, *memory_pressure_path = NULL, *own_user = NULL; _cleanup_free_ char *home_buffer = NULL, *memory_pressure_path = NULL, *own_user = NULL;
const char *pwent_home = NULL, *shell = NULL; const char *pwent_home = NULL, *shell = NULL;
char **final_argv = NULL;
dev_t journal_stream_dev = 0; dev_t journal_stream_dev = 0;
ino_t journal_stream_ino = 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 */ bool needs_sandboxing, /* Do we need to set up full sandboxing? (i.e. all namespacing, all MAC stuff, caps, yadda yadda */
@ -4896,7 +4895,7 @@ int exec_invoke(
if (context->user) if (context->user)
u = context->user; u = context->user;
else if (context->pam_name) { else if (context->pam_name || FLAGS_SET(command->flags, EXEC_COMMAND_PREFIX_SHELL)) {
/* If PAM is enabled but no user name is explicitly selected, then use our own one. */ /* If PAM is enabled but no user name is explicitly selected, then use our own one. */
own_user = getusername_malloc(); own_user = getusername_malloc();
if (!own_user) { if (!own_user) {
@ -5501,17 +5500,28 @@ int exec_invoke(
/* Now that the mount namespace has been set up and privileges adjusted, let's look for the thing we /* Now that the mount namespace has been set up and privileges adjusted, let's look for the thing we
* shall execute. */ * shall execute. */
const char *path = command->path;
if (FLAGS_SET(command->flags, EXEC_COMMAND_PREFIX_SHELL)) {
if (!shell) {
log_exec_debug(context, params,
"Shell prefixing requested for user without default shell, using /bin/sh: %s",
strna(username));
assert(streq(path, "/bin/sh"));
} else
path = shell;
}
_cleanup_free_ char *executable = NULL; _cleanup_free_ char *executable = NULL;
_cleanup_close_ int executable_fd = -EBADF; _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) { if (r < 0) {
*exit_status = EXIT_EXEC; *exit_status = EXIT_EXEC;
log_exec_struct_errno(context, params, LOG_NOTICE, r, log_exec_struct_errno(context, params, LOG_NOTICE, r,
LOG_MESSAGE_ID(SD_MESSAGE_SPAWN_FAILED_STR), LOG_MESSAGE_ID(SD_MESSAGE_SPAWN_FAILED_STR),
LOG_EXEC_MESSAGE(params, LOG_EXEC_MESSAGE(params,
"Unable to locate executable '%s': %m", "Unable to locate executable '%s': %m", path),
command->path), LOG_ITEM("EXECUTABLE=%s", path));
LOG_ITEM("EXECUTABLE=%s", command->path));
/* If the error will be ignored by manager, tune down the log level here. Missing executable /* If the error will be ignored by manager, tune down the log level here. Missing executable
* is very much expected in this case. */ * is very much expected in this case. */
return r != -ENOMEM && FLAGS_SET(command->flags, EXEC_COMMAND_IGNORE_FAILURE) ? 1 : r; return r != -ENOMEM && FLAGS_SET(command->flags, EXEC_COMMAND_IGNORE_FAILURE) ? 1 : r;
@ -5935,10 +5945,13 @@ int exec_invoke(
strv_free_and_replace(accum_env, ee); strv_free_and_replace(accum_env, ee);
} }
if (!FLAGS_SET(command->flags, EXEC_COMMAND_NO_ENV_EXPAND)) { _cleanup_strv_free_ char **replaced_argv = NULL, **prefixed_argv = NULL;
char **final_argv = FLAGS_SET(command->flags, EXEC_COMMAND_PREFIX_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; _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) { if (r < 0) {
*exit_status = EXIT_MEMORY; *exit_status = EXIT_MEMORY;
return log_exec_error_errno(context, return log_exec_error_errno(context,
@ -5963,8 +5976,33 @@ int exec_invoke(
"Invalid environment variable name evaluates to an empty string: %s", "Invalid environment variable name evaluates to an empty string: %s",
strna(jb)); strna(jb));
} }
} else }
final_argv = command->argv;
if (FLAGS_SET(command->flags, EXEC_COMMAND_PREFIX_SHELL)) {
r = strv_extendf(&prefixed_argv, "%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(&prefixed_argv, "-c", cmdline_joined);
if (r < 0) {
*exit_status = EXIT_MEMORY;
return log_oom();
}
}
final_argv = prefixed_argv;
}
log_command_line(context, params, "Executing", executable, final_argv); log_command_line(context, params, "Executing", executable, final_argv);

View File

@ -888,7 +888,7 @@ int config_parse_exec(
bool semicolon; bool semicolon;
do { do {
_cleanup_free_ char *path = NULL, *firstword = NULL; _cleanup_free_ char *firstword = NULL;
semicolon = false; semicolon = false;
@ -915,6 +915,8 @@ int config_parse_exec(
* *
* "-": Ignore if the path doesn't exist * "-": Ignore if the path doesn't exist
* "@": Allow overriding argv[0] (supplied as a separate argument) * "@": 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 * ":": Disable environment variable substitution
* "+": Run with full privileges and no sandboxing * "+": Run with full privileges and no sandboxing
* "!": Apply sandboxing except for user/group credentials * "!": Apply sandboxing except for user/group credentials
@ -926,6 +928,8 @@ int config_parse_exec(
separate_argv0 = true; separate_argv0 = true;
else if (*f == ':' && !FLAGS_SET(flags, EXEC_COMMAND_NO_ENV_EXPAND)) else if (*f == ':' && !FLAGS_SET(flags, EXEC_COMMAND_NO_ENV_EXPAND))
flags |= EXEC_COMMAND_NO_ENV_EXPAND; flags |= EXEC_COMMAND_NO_ENV_EXPAND;
else if (*f == '|' && !FLAGS_SET(flags, EXEC_COMMAND_PREFIX_SHELL))
flags |= EXEC_COMMAND_PREFIX_SHELL;
else if (*f == '+' && !(flags & (EXEC_COMMAND_FULLY_PRIVILEGED|EXEC_COMMAND_NO_SETUID)) && !ambient_hack) else if (*f == '+' && !(flags & (EXEC_COMMAND_FULLY_PRIVILEGED|EXEC_COMMAND_NO_SETUID)) && !ambient_hack)
flags |= EXEC_COMMAND_FULLY_PRIVILEGED; flags |= EXEC_COMMAND_FULLY_PRIVILEGED;
else if (*f == '!' && !(flags & (EXEC_COMMAND_FULLY_PRIVILEGED|EXEC_COMMAND_NO_SETUID)) && !ambient_hack) 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); 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_strv_free_ char **args = NULL;
_cleanup_free_ char *path = NULL;
if (!separate_argv0) if (FLAGS_SET(flags, EXEC_COMMAND_PREFIX_SHELL)) {
if (strv_extend(&args, path) < 0) /* Use /bin/sh 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("/bin/sh");
if (!path)
return log_oom(); 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 (!(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;
}
if (!separate_argv0)
if (strv_extend(&args, path) < 0)
return log_oom();
}
while (!isempty(p)) { while (!isempty(p)) {
_cleanup_free_ char *word = NULL, *resolved = NULL; _cleanup_free_ char *word = NULL, *resolved = NULL;

View File

@ -3204,7 +3204,7 @@ int service_deserialize_exec_command(
bool control, found = false, last = false; bool control, found = false, last = false;
int r; int r;
enum ExecCommandState { enum {
STATE_EXEC_COMMAND_TYPE, STATE_EXEC_COMMAND_TYPE,
STATE_EXEC_COMMAND_INDEX, STATE_EXEC_COMMAND_INDEX,
STATE_EXEC_COMMAND_PATH, STATE_EXEC_COMMAND_PATH,
@ -3230,6 +3230,7 @@ int service_deserialize_exec_command(
break; break;
switch (state) { switch (state) {
case STATE_EXEC_COMMAND_TYPE: case STATE_EXEC_COMMAND_TYPE:
id = service_exec_command_from_string(arg); id = service_exec_command_from_string(arg);
if (id < 0) if (id < 0)
@ -3237,11 +3238,12 @@ int service_deserialize_exec_command(
state = STATE_EXEC_COMMAND_INDEX; state = STATE_EXEC_COMMAND_INDEX;
break; break;
case STATE_EXEC_COMMAND_INDEX: case STATE_EXEC_COMMAND_INDEX:
/* PID 1234 is serialized as either '1234' or '+1234'. The second form is used to /* ExecCommand index 1234 is serialized as either '1234' or '+1234'. The second form
* mark the last command in a sequence. We warn if the deserialized command doesn't * is used to mark the last command in a sequence. We warn if the deserialized command
* match what we have loaded from the unit, but we don't need to warn if that is the * doesn't match what we have loaded from the unit, but we don't need to warn if
* last command. */ * that is the last command. */
r = safe_atou(arg, &idx); r = safe_atou(arg, &idx);
if (r < 0) if (r < 0)
@ -3250,15 +3252,18 @@ int service_deserialize_exec_command(
state = STATE_EXEC_COMMAND_PATH; state = STATE_EXEC_COMMAND_PATH;
break; break;
case STATE_EXEC_COMMAND_PATH: case STATE_EXEC_COMMAND_PATH:
path = TAKE_PTR(arg); path = TAKE_PTR(arg);
state = STATE_EXEC_COMMAND_ARGS; state = STATE_EXEC_COMMAND_ARGS;
break; break;
case STATE_EXEC_COMMAND_ARGS: case STATE_EXEC_COMMAND_ARGS:
r = strv_extend(&argv, arg); r = strv_extend(&argv, arg);
if (r < 0) if (r < 0)
return r; return r;
break; break;
default: default:
assert_not_reached(); assert_not_reached();
} }

View File

@ -101,6 +101,7 @@ static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
static char *arg_shell_prompt_prefix = NULL; static char *arg_shell_prompt_prefix = NULL;
static int arg_lightweight = -1; static int arg_lightweight = -1;
static char *arg_area = NULL; static char *arg_area = NULL;
static bool arg_login_environment = false;
STATIC_DESTRUCTOR_REGISTER(arg_description, freep); STATIC_DESTRUCTOR_REGISTER(arg_description, freep);
STATIC_DESTRUCTOR_REGISTER(arg_environment, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_environment, strv_freep);
@ -212,6 +213,7 @@ static int help_sudo_mode(void) {
" -g --group=GROUP Run as system group\n" " -g --group=GROUP Run as system group\n"
" --nice=NICE Nice level\n" " --nice=NICE Nice level\n"
" -D --chdir=PATH Set working directory\n" " -D --chdir=PATH Set working directory\n"
" -i --login-environment Invoke command via target user's login shell and home\n"
" --setenv=NAME[=VALUE] Set environment variable\n" " --setenv=NAME[=VALUE] Set environment variable\n"
" --background=COLOR Set ANSI color for background\n" " --background=COLOR Set ANSI color for background\n"
" --pty Request allocation of a pseudo TTY for stdio\n" " --pty Request allocation of a pseudo TTY for stdio\n"
@ -254,7 +256,7 @@ static int add_timer_property(const char *name, const char *val) {
return 0; 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; _cleanup_free_ char *argv0 = NULL;
assert(shell); assert(shell);
@ -842,6 +844,7 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) {
{ "group", required_argument, NULL, 'g' }, { "group", required_argument, NULL, 'g' },
{ "nice", required_argument, NULL, ARG_NICE }, { "nice", required_argument, NULL, ARG_NICE },
{ "chdir", required_argument, NULL, 'D' }, { "chdir", required_argument, NULL, 'D' },
{ "login-environment", no_argument, NULL, 'i' },
{ "setenv", required_argument, NULL, ARG_SETENV }, { "setenv", required_argument, NULL, ARG_SETENV },
{ "background", required_argument, NULL, ARG_BACKGROUND }, { "background", required_argument, NULL, ARG_BACKGROUND },
{ "pty", no_argument, NULL, ARG_PTY }, { "pty", no_argument, NULL, ARG_PTY },
@ -861,7 +864,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() /* 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). */ * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */
optind = 0; 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) { switch (c) {
@ -975,6 +978,10 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) {
break; break;
case 'i':
arg_login_environment = true;
break;
case '?': case '?':
return -EINVAL; return -EINVAL;
@ -991,7 +998,7 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) {
} }
if (!arg_working_directory) { if (!arg_working_directory) {
if (arg_exec_user) { if (arg_exec_user || arg_login_environment) {
/* When switching to a specific user, also switch to its home directory. */ /* When switching to a specific user, also switch to its home directory. */
arg_working_directory = strdup("~"); arg_working_directory = strdup("~");
if (!arg_working_directory) if (!arg_working_directory)
@ -1023,9 +1030,11 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) {
arg_send_sighup = true; arg_send_sighup = true;
_cleanup_strv_free_ char **l = NULL; _cleanup_strv_free_ char **l = NULL;
if (argc > optind) if (argc > optind) {
l = strv_copy(argv + optind); l = strv_copy(argv + optind);
else { if (!l)
return log_oom();
} else if (!arg_login_environment) {
const char *e; const char *e;
e = strv_env_get(arg_environment, "SHELL"); e = strv_env_get(arg_environment, "SHELL");
@ -1050,9 +1059,19 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) {
} }
l = make_login_shell_cmdline(arg_exec_path); l = make_login_shell_cmdline(arg_exec_path);
if (!l)
return log_oom();
}
if (arg_login_environment) {
arg_exec_path = strdup("/bin/sh");
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); strv_free_and_replace(arg_cmdline, l);
@ -1092,6 +1111,12 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) {
if (strv_extend(&arg_property, "PAMName=systemd-run0") < 0) if (strv_extend(&arg_property, "PAMName=systemd-run0") < 0)
return log_oom(); return log_oom();
/* The service manager ignores SIGPIPE for all spawned processes by default. Let's explicitly override
* that here, since we're primarily invoked in interactive environments, and the termination of
* local terminal session should be acknowledged by remote even for --pipe stdio. */
if (strv_extend(&arg_property, "IgnoreSIGPIPE=no") < 0)
return log_oom();
if (!arg_background && arg_stdio == ARG_STDIO_PTY && shall_tint_background()) { if (!arg_background && arg_stdio == ARG_STDIO_PTY && shall_tint_background()) {
double hue; double hue;
@ -1260,10 +1285,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) { static int transient_service_set_properties(sd_bus_message *m, const char *pty_path, int pty_fd) {
int r, send_term; /* tri-state */ int r, send_term; /* tri-state */
/* We disable environment expansion on the server side via ExecStartEx=:. /* Use ExecStartEx if new exec flags are required. */
* ExecStartEx was added relatively recently (v243), and some bugs were fixed only later. bool use_ex_prop = !arg_expand_environment || arg_login_environment;
* So use that feature only if required. It will fail with older systemds. */
bool use_ex_prop = !arg_expand_environment;
assert(m); assert(m);
assert((!!pty_path) == (pty_fd >= 0)); assert((!!pty_path) == (pty_fd >= 0));
@ -1450,7 +1473,9 @@ static int transient_service_set_properties(sd_bus_message *m, const char *pty_p
_cleanup_strv_free_ char **opts = NULL; _cleanup_strv_free_ char **opts = NULL;
r = exec_command_flags_to_strv( 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_login_environment ? EXEC_COMMAND_PREFIX_SHELL : 0),
&opts); &opts);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to format execute flags: %m"); return log_error_errno(r, "Failed to format execute flags: %m");
@ -2810,7 +2835,12 @@ static int run(int argc, char* argv[]) {
if (strv_isempty(arg_cmdline)) if (strv_isempty(arg_cmdline))
t = strdup(arg_unit); t = strdup(arg_unit);
else if (startswith(arg_cmdline[0], "-")) { else if (arg_login_environment) {
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, /* Drop the login shell marker from the command line when generating the description,
* in order to minimize user confusion. */ * in order to minimize user confusion. */
_cleanup_strv_free_ char **l = strv_copy(arg_cmdline); _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; break;
case '|':
if (FLAGS_SET(flags, EXEC_COMMAND_PREFIX_SHELL))
done = true;
else {
flags |= EXEC_COMMAND_PREFIX_SHELL;
eq++;
}
break;
default: default:
done = true; done = true;
} }
} while (!done); } 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_PREFIX_SHELL))) {
/* Upgrade the ExecXYZ= property to ExecXYZEx= for convenience */ /* Upgrade the ExecXYZ= property to ExecXYZEx= for convenience */
is_ex_prop = true; is_ex_prop = true;
upgraded_name = strjoin(field, "Ex"); upgraded_name = strjoin(field, "Ex");
if (!upgraded_name) if (!upgraded_name)
return log_oom(); return log_oom();
field = upgraded_name;
} }
if (is_ex_prop) { 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"); return log_error_errno(r, "Failed to convert ExecCommandFlags to strv: %m");
} }
if (explicit_path) { if (FLAGS_SET(flags, EXEC_COMMAND_PREFIX_SHELL)) {
path = strdup("/bin/sh");
if (!path)
return log_oom();
} else if (explicit_path) {
r = extract_first_word(&eq, &path, NULL, EXTRACT_UNQUOTE|EXTRACT_CUNESCAPE); r = extract_first_word(&eq, &path, NULL, EXTRACT_UNQUOTE|EXTRACT_CUNESCAPE);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to parse path: %m"); 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); r = strv_split_full(&l, eq, NULL, EXTRACT_UNQUOTE|EXTRACT_CUNESCAPE);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to parse command line: %m"); return log_error_errno(r, "Failed to parse command line: %m");
if (FLAGS_SET(flags, EXEC_COMMAND_PREFIX_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"); r = sd_bus_message_open_container(m, SD_BUS_TYPE_STRUCT, "sv");
if (r < 0) if (r < 0)
return bus_log_create_error(r); 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) if (r < 0)
return bus_log_create_error(r); return bus_log_create_error(r);

View File

@ -488,6 +488,7 @@ static const char* const exec_command_strings[] = {
"privileged", /* EXEC_COMMAND_FULLY_PRIVILEGED */ "privileged", /* EXEC_COMMAND_FULLY_PRIVILEGED */
"no-setuid", /* EXEC_COMMAND_NO_SETUID */ "no-setuid", /* EXEC_COMMAND_NO_SETUID */
"no-env-expand", /* EXEC_COMMAND_NO_ENV_EXPAND */ "no-env-expand", /* EXEC_COMMAND_NO_ENV_EXPAND */
"prefix-shell", /* EXEC_COMMAND_PREFIX_SHELL */
}; };
assert_cc((1 << ELEMENTSOF(exec_command_strings)) - 1 == _EXEC_COMMAND_FLAGS_ALL); 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_FULLY_PRIVILEGED = 1 << 1,
EXEC_COMMAND_NO_SETUID = 1 << 2, EXEC_COMMAND_NO_SETUID = 1 << 2,
EXEC_COMMAND_NO_ENV_EXPAND = 1 << 3, EXEC_COMMAND_NO_ENV_EXPAND = 1 << 3,
EXEC_COMMAND_PREFIX_SHELL = 1 << 4,
_EXEC_COMMAND_FLAGS_INVALID = -EINVAL, _EXEC_COMMAND_FLAGS_INVALID = -EINVAL,
_EXEC_COMMAND_FLAGS_ALL = (1 << 4) -1, _EXEC_COMMAND_FLAGS_ALL = (1 << 5) -1,
} ExecCommandFlags; } ExecCommandFlags;
int exec_command_flags_from_strv(char * const *ex_opts, ExecCommandFlags *ret); 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,21 @@
#!/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 -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) # 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" assert_eq "$(run0 ${tu:+"--user=$tu"} bash -c 'echo $XDG_SESSION_TYPE')" "unspecified"
# Test shell prefixing
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 -i echo \$SHLVL)" "11"
fi
if [[ -n "$tu" ]]; then if [[ -n "$tu" ]]; then
# Validate that $SHELL is set to login shell of target user when cmdline is supplied (not invoking shell) # 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)" TARGET_LOGIN_SHELL="$(getent passwd "$tu" | cut -d: -f7)"
assert_eq "$(run0 --user="$tu" printenv SHELL)" "$TARGET_LOGIN_SHELL" 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 fi
done done
# Let's chain a couple of run0 calls together, for fun # Let's chain a couple of run0 calls together, for fun