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.
This commit is contained in:
Mike Yuan
2025-05-07 18:32:19 +02:00
parent 398b3e4472
commit 5b8bcbcf00
8 changed files with 192 additions and 87 deletions
+50 -25
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 (!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;
+48 -12
View File
@@ -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);
+55 -37
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 (!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;