core: Various fixes for cgroup and pid namespaces (#36815)

This commit is contained in:
Daan De Meyer
2025-06-05 15:21:59 +02:00
committed by GitHub
6 changed files with 185 additions and 36 deletions

View File

@@ -1177,14 +1177,56 @@ static int pam_close_session_and_delete_credentials(pam_handle_t *handle, int fl
}
#endif
static int attach_to_subcgroup(
const ExecContext *context,
const CGroupContext *cgroup_context,
const ExecParameters *params,
const char *prefix) {
_cleanup_free_ char *subgroup = NULL;
int r;
assert(context);
assert(cgroup_context);
assert(params);
/* If we're a control process that needs a subgroup, we've already been spawned into it as otherwise
* we'd violate the "no inner processes" rule, so no need to do anything. */
if (exec_params_needs_control_subcgroup(params))
return 0;
r = exec_params_get_cgroup_path(params, cgroup_context, prefix, &subgroup);
if (r < 0)
return log_error_errno(r, "Failed to acquire cgroup path: %m");
/* No subgroup required? Then there's nothing to do. */
if (r == 0)
return 0;
r = cg_attach(subgroup, 0);
if (r == -EUCLEAN)
return log_error_errno(r,
"Failed to attach process " PID_FMT " to cgroup '%s', "
"because the cgroup or one of its parents or "
"siblings is in the threaded mode.",
getpid_cached(), subgroup);
if (r < 0)
return log_error_errno(r,
"Failed to attach process " PID_FMT " to cgroup %s: %m",
getpid_cached(), subgroup);
return 0;
}
static int setup_pam(
const ExecContext *context,
const CGroupContext *cgroup_context,
ExecParameters *params,
const char *user,
uid_t uid,
gid_t gid,
char ***env, /* updated on success */
const int fds[], size_t n_fds,
bool needs_sandboxing,
int exec_fd) {
#if HAVE_PAM
@@ -1290,6 +1332,15 @@ static int setup_pam(
if (r == 0) {
int ret = EXIT_PAM;
if (needs_sandboxing && exec_needs_cgroup_namespace(context) && params->cgroup_path) {
/* Move PAM process into subgroup immediately if the main process hasn't been moved
* into the subgroup yet (when cgroup namespacing is enabled) and a subgroup is
* configured. */
r = attach_to_subcgroup(context, cgroup_context, params, params->cgroup_path);
if (r < 0)
return r;
}
/* The child's job is to reset the PAM session on termination */
barrier_set_role(&barrier, BARRIER_CHILD);
@@ -3541,7 +3592,7 @@ static int apply_mount_namespace(
.private_dev = needs_sandboxing && context->private_devices,
.private_network = needs_sandboxing && exec_needs_network_namespace(context),
.private_ipc = needs_sandboxing && exec_needs_ipc_namespace(context),
.private_pids = needs_sandboxing && exec_needs_pid_namespace(context) ? context->private_pids : PRIVATE_PIDS_NO,
.private_pids = needs_sandboxing && exec_needs_pid_namespace(context, params) ? context->private_pids : PRIVATE_PIDS_NO,
.private_tmp = needs_sandboxing ? context->private_tmp : PRIVATE_TMP_NO,
.private_var_tmp = needs_sandboxing ? context->private_var_tmp : PRIVATE_TMP_NO,
@@ -4102,7 +4153,7 @@ static void log_command_line(
LOG_EXEC_INVOCATION_ID(params));
}
static bool exec_context_needs_cap_sys_admin(const ExecContext *context) {
static bool exec_needs_cap_sys_admin(const ExecContext *context, const ExecParameters *params) {
assert(context);
return context->private_users != PRIVATE_USERS_NO ||
@@ -4121,7 +4172,7 @@ static bool exec_context_needs_cap_sys_admin(const ExecContext *context) {
!strv_isempty(context->extension_directories) ||
context->protect_system != PROTECT_SYSTEM_NO ||
context->protect_home != PROTECT_HOME_NO ||
exec_needs_pid_namespace(context) ||
exec_needs_pid_namespace(context, params) ||
context->protect_kernel_tunables ||
context->protect_kernel_modules ||
context->protect_kernel_logs ||
@@ -4166,7 +4217,7 @@ static bool exec_namespace_is_delegated(
/* If we need unprivileged private users, we've already unshared a user namespace by the time we call
* setup_delegated_namespaces() for the first time so let's make sure we do all other namespace
* unsharing in the first call to setup_delegated_namespaces() by returning false here. */
if (!have_cap_sys_admin && exec_context_needs_cap_sys_admin(context))
if (!have_cap_sys_admin && exec_needs_cap_sys_admin(context, params))
return false;
if (context->delegate_namespaces == NAMESPACE_FLAGS_INITIAL)
@@ -4269,7 +4320,7 @@ static int setup_delegated_namespaces(
/* Unshare a new PID namespace before setting up mounts to ensure /proc/ is mounted with only processes in PID namespace visible.
* Note PrivatePIDs=yes implies MountAPIVFS=yes so we'll always ensure procfs is remounted. */
if (needs_sandboxing && exec_needs_pid_namespace(context) &&
if (needs_sandboxing && exec_needs_pid_namespace(context, params) &&
exec_namespace_is_delegated(context, params, have_cap_sys_admin, CLONE_NEWPID) == delegate) {
if (params->pidref_transport_fd < 0) {
*reterr_exit_status = EXIT_NAMESPACE;
@@ -4911,28 +4962,48 @@ int exec_invoke(
if (socket_fd >= 0)
(void) fd_nonblock(socket_fd, false);
/* We need sandboxing if the caller asked us to apply it and the command isn't explicitly excepted
* from it. */
needs_sandboxing = (params->flags & EXEC_APPLY_SANDBOXING) && !(command->flags & EXEC_COMMAND_FULLY_PRIVILEGED);
/* Journald will try to look-up our cgroup in order to populate _SYSTEMD_CGROUP and _SYSTEMD_UNIT fields.
* Hence we need to migrate to the target cgroup from init.scope before connecting to journald */
if (params->cgroup_path) {
_cleanup_free_ char *p = NULL;
_cleanup_free_ char *subcgroup = NULL;
r = exec_params_get_cgroup_path(params, cgroup_context, &p);
r = exec_params_get_cgroup_path(params, cgroup_context, params->cgroup_path, &subcgroup);
if (r < 0) {
*exit_status = EXIT_CGROUP;
return log_error_errno(r, "Failed to acquire cgroup path: %m");
}
if (r > 0) {
/* If there is a subcgroup required, let's make sure to create it now. */
r = cg_create(subcgroup);
if (r < 0)
return log_error_errno(r, "Failed to create subcgroup '%s': %m", subcgroup);
}
r = cg_attach(p, 0);
/* If we need a cgroup namespace, we cannot yet move the service to its configured subgroup,
* as unsharing the cgroup namespace later on makes the current cgroup the root of the
* namespace and we want the root of the namespace to be the main service cgroup and not the
* subgroup. One edge case is if we're a control process that needs to be spawned in a
* subgroup, in this case, we have no choice as moving into the main service cgroup might
* violate the no inner processes rule of cgroupv2. */
const char *cgtarget = needs_sandboxing && exec_needs_cgroup_namespace(context) &&
!exec_params_needs_control_subcgroup(params)
? params->cgroup_path : subcgroup;
r = cg_attach(cgtarget, 0);
if (r == -EUCLEAN) {
*exit_status = EXIT_CGROUP;
return log_error_errno(r,
"Failed to attach process to cgroup '%s', "
"because the cgroup or one of its parents or "
"siblings is in the threaded mode.", p);
"siblings is in the threaded mode.", cgtarget);
}
if (r < 0) {
*exit_status = EXIT_CGROUP;
return log_error_errno(r, "Failed to attach to cgroup %s: %m", p);
return log_error_errno(r, "Failed to attach to cgroup %s: %m", cgtarget);
}
}
@@ -5128,10 +5199,6 @@ int exec_invoke(
}
}
/* We need sandboxing if the caller asked us to apply it and the command isn't explicitly excepted
* from it. */
needs_sandboxing = (params->flags & EXEC_APPLY_SANDBOXING) && !(command->flags & EXEC_COMMAND_FULLY_PRIVILEGED);
if (params->cgroup_path) {
/* If delegation is enabled we'll pass ownership of the cgroup to the user of the new process. On cgroup v1
* this is only about systemd's own hierarchy, i.e. not the controller hierarchies, simply because that's not
@@ -5147,7 +5214,7 @@ int exec_invoke(
return log_error_errno(r, "Failed to adjust control group access: %m");
}
r = exec_params_get_cgroup_path(params, cgroup_context, &p);
r = exec_params_get_cgroup_path(params, cgroup_context, params->cgroup_path, &p);
if (r < 0) {
*exit_status = EXIT_CGROUP;
return log_error_errno(r, "Failed to acquire cgroup path: %m");
@@ -5328,7 +5395,8 @@ int exec_invoke(
* wins here. (See above.) */
/* All fds passed in the fds array will be closed in the pam child process. */
r = setup_pam(context, params, username, uid, gid, &accum_env, params->fds, n_fds, params->exec_fd);
r = setup_pam(context, cgroup_context, params, username, uid, gid, &accum_env,
params->fds, n_fds, needs_sandboxing, params->exec_fd);
if (r < 0) {
*exit_status = EXIT_PAM;
return log_error_errno(r, "Failed to set up PAM session: %m");
@@ -5352,7 +5420,7 @@ int exec_invoke(
}
}
if (needs_sandboxing && !have_cap_sys_admin && exec_context_needs_cap_sys_admin(context)) {
if (needs_sandboxing && !have_cap_sys_admin && exec_needs_cap_sys_admin(context, params)) {
/* If we're unprivileged, set up the user namespace first to enable use of the other namespaces.
* Users with CAP_SYS_ADMIN can set up user namespaces last because they will be able to
* set up all of the other namespaces (i.e. network, mount, UTS) without a user namespace. */
@@ -5453,6 +5521,17 @@ int exec_invoke(
if (r < 0)
return r;
if (needs_sandboxing && exec_needs_cgroup_namespace(context) && params->cgroup_path) {
/* Move ourselves into the subcgroup now *after* we've unshared the cgroup namespace, which
* ensures the root of the cgroup namespace is the top level service cgroup and not the
* subcgroup. Adjust the prefix accordingly since we're in a cgroup namespace now. */
r = attach_to_subcgroup(context, cgroup_context, params, /* prefix= */ NULL);
if (r < 0) {
*exit_status = EXIT_CGROUP;
return r;
}
}
/* Now that the mount namespace has been set up and privileges adjusted, let's look for the thing we
* shall execute. */

View File

@@ -262,9 +262,13 @@ bool exec_is_cgroup_mount_read_only(const ExecContext *context) {
return IN_SET(exec_get_protect_control_groups(context), PROTECT_CONTROL_GROUPS_YES, PROTECT_CONTROL_GROUPS_STRICT);
}
bool exec_needs_pid_namespace(const ExecContext *context) {
bool exec_needs_pid_namespace(const ExecContext *context, const ExecParameters *params) {
assert(context);
/* PID namespaces don't really make sense for control processes so let's not use them for those. */
if (params && FLAGS_SET(params->flags, EXEC_IS_CONTROL))
return false;
return context->private_pids != PRIVATE_PIDS_NO && namespace_type_supported(NAMESPACE_PID);
}
@@ -321,7 +325,7 @@ bool exec_needs_mount_namespace(
context->protect_proc != PROTECT_PROC_DEFAULT ||
context->proc_subset != PROC_SUBSET_ALL ||
exec_needs_ipc_namespace(context) ||
exec_needs_pid_namespace(context))
exec_needs_pid_namespace(context, params))
return true;
if (context->root_directory) {
@@ -401,20 +405,24 @@ bool exec_directory_is_private(const ExecContext *context, ExecDirectoryType typ
return true;
}
int exec_params_needs_control_subcgroup(const ExecParameters *params) {
/* Keep this in sync with exec_params_get_cgroup_path(). */
return FLAGS_SET(params->flags, EXEC_CGROUP_DELEGATE|EXEC_CONTROL_CGROUP|EXEC_IS_CONTROL);
}
int exec_params_get_cgroup_path(
const ExecParameters *params,
const CGroupContext *c,
const char *prefix,
char **ret) {
const char *subgroup = NULL;
char *p;
assert(params);
assert(c);
assert(ret);
if (!params->cgroup_path)
return -EINVAL;
/* If we are called for a unit where cgroup delegation is on, and the payload created its own populated
* subcgroup (which we expect it to do, after all it asked for delegation), then we cannot place the control
* processes started after the main unit's process in the unit's main cgroup because it is now an inner one,
@@ -424,6 +432,7 @@ int exec_params_get_cgroup_path(
* this is not necessary, the cgroup is still empty. We distinguish these cases with the EXEC_CONTROL_CGROUP
* flag, which is only passed for the former statements, not for the latter. */
/* Keep this in sync with exec_params_needs_control_subcgroup(). */
if (FLAGS_SET(params->flags, EXEC_CGROUP_DELEGATE) && (FLAGS_SET(params->flags, EXEC_CONTROL_CGROUP) || c->delegate_subgroup)) {
if (FLAGS_SET(params->flags, EXEC_IS_CONTROL))
subgroup = ".control";
@@ -432,9 +441,9 @@ int exec_params_get_cgroup_path(
}
if (subgroup)
p = path_join(params->cgroup_path, subgroup);
p = path_join(prefix, subgroup);
else
p = strdup(params->cgroup_path);
p = strdup(strempty(prefix));
if (!p)
return -ENOMEM;
@@ -502,8 +511,16 @@ int exec_spawn(
want to log from the parent, so we use the possibly inaccurate path here. */
log_command_line(unit, "About to execute", command->path, command->argv);
if (params->cgroup_path) {
r = exec_params_get_cgroup_path(params, cgroup_context, &subcgroup_path);
/* We cannot spawn the main service process into the subcgroup as it might need to unshare the cgroup
* namespace first if one is configured to make sure the root of the cgroup namespace is the service
* cgroup and not the subcgroup. However, when running control commands on a live service, the
* commands have to be spawned inside a subcgroup, otherwise we violate the no inner processes rule
* of cgroupv2 as the main service process might already have enabled controllers by writing to
* cgroup.subtree_control. */
const char *cgtarget;
if (exec_params_needs_control_subcgroup(params)) {
r = exec_params_get_cgroup_path(params, cgroup_context, params->cgroup_path, &subcgroup_path);
if (r < 0)
return log_unit_error_errno(unit, r, "Failed to acquire subcgroup path: %m");
if (r > 0) {
@@ -514,7 +531,10 @@ int exec_spawn(
if (r < 0)
return log_unit_error_errno(unit, r, "Failed to create subcgroup '%s': %m", subcgroup_path);
}
}
cgtarget = subcgroup_path;
} else
cgtarget = params->cgroup_path;
/* In order to avoid copy-on-write traps and OOM-kills when pid1's memory.current is above the
* child's memory.max, serialize all the state needed to start the unit, and pass it to the
@@ -578,24 +598,24 @@ int exec_spawn(
"--log-level", max_log_levels,
"--log-target", log_target_to_string(manager_get_executor_log_target(unit->manager))),
environ,
subcgroup_path,
cgtarget,
&pidref);
/* Drop the ambient set again, so no processes other than sd-executore spawned from the manager inherit it. */
(void) capability_ambient_set_apply(0, /* also_inherit= */ false);
if (r == -EUCLEAN && subcgroup_path)
if (r == -EUCLEAN && cgtarget)
return log_unit_error_errno(unit, r,
"Failed to spawn process into cgroup '%s', because the cgroup "
"or one of its parents or siblings is in the threaded mode.",
subcgroup_path);
cgtarget);
if (r < 0)
return log_unit_error_errno(unit, r, "Failed to spawn executor: %m");
/* We add the new process to the cgroup both in the child (so that we can be sure that no user code is ever
* executed outside of the cgroup) and in the parent (so that we can be sure that when we kill the cgroup the
* process will be killed too). */
if (r == 0 && subcgroup_path)
(void) cg_attach(subcgroup_path, pidref.pid);
if (r == 0 && cgtarget)
(void) cg_attach(cgtarget, pidref.pid);
/* r > 0: Already in the right cgroup thanks to CLONE_INTO_CGROUP */
log_unit_debug(unit, "Forked %s as " PID_FMT " (%s CLONE_INTO_CGROUP)",

View File

@@ -551,7 +551,8 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(ExecRuntime*, exec_runtime_free);
ExecRuntime* exec_runtime_destroy(ExecRuntime *rt);
void exec_runtime_clear(ExecRuntime *rt);
int exec_params_get_cgroup_path(const ExecParameters *params, const CGroupContext *c, char **ret);
int exec_params_needs_control_subcgroup(const ExecParameters *params);
int exec_params_get_cgroup_path(const ExecParameters *params, const CGroupContext *c, const char *prefix, char **ret);
void exec_params_shallow_clear(ExecParameters *p);
void exec_params_dump(const ExecParameters *p, FILE* f, const char *prefix);
void exec_params_deep_clear(ExecParameters *p);
@@ -595,7 +596,7 @@ ExecDirectoryType exec_resource_type_from_string(const char *s) _pure_;
bool exec_needs_mount_namespace(const ExecContext *context, const ExecParameters *params, const ExecRuntime *runtime);
bool exec_needs_network_namespace(const ExecContext *context);
bool exec_needs_ipc_namespace(const ExecContext *context);
bool exec_needs_pid_namespace(const ExecContext *context);
bool exec_needs_pid_namespace(const ExecContext *context, const ExecParameters *params);
ProtectControlGroups exec_get_protect_control_groups(const ExecContext *context);
bool exec_needs_cgroup_namespace(const ExecContext *context);

View File

@@ -702,7 +702,7 @@ static int service_verify(Service *s) {
if (s->type == SERVICE_DBUS && !s->bus_name)
return log_unit_error_errno(UNIT(s), SYNTHETIC_ERRNO(ENOEXEC), "Service is of type D-Bus but no D-Bus service name has been specified. Refusing.");
if (s->type == SERVICE_FORKING && exec_needs_pid_namespace(&s->exec_context))
if (s->type == SERVICE_FORKING && exec_needs_pid_namespace(&s->exec_context, /* params= */ NULL))
return log_unit_error_errno(UNIT(s), SYNTHETIC_ERRNO(ENOEXEC), "Service of Type=forking does not support PrivatePIDs=yes. Refusing.");
if (s->usb_function_descriptors && !s->usb_function_strings)

View File

@@ -4252,7 +4252,7 @@ static int unit_verify_contexts(const Unit *u) {
exec_needs_mount_namespace(ec, /* params = */ NULL, /* runtime = */ NULL))
return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "WorkingDirectory= may not be below /proc/, /sys/ or /dev/ when using mount namespacing. Refusing.");
if (exec_needs_pid_namespace(ec) && !UNIT_VTABLE(u)->notify_pidref)
if (exec_needs_pid_namespace(ec, /* params= */ NULL) && !UNIT_VTABLE(u)->notify_pidref)
return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "PrivatePIDs= setting is only supported for service units. Refusing.");
const KillContext *kc = unit_get_kill_context(u);

View File

@@ -104,4 +104,53 @@ testcase_basic_strict() {
test_basic "strict" "yes" true "$READ_ONLY_MOUNT_FLAG"
}
testcase_delegate_subgroup() {
# Make sure the service cgroup is the root of the cgroup namespace when we use DelegateSubgroup.
systemd-run \
-p ProtectControlGroupsEx=private \
-p PrivateMounts=yes \
-p Delegate=yes \
-p DelegateSubgroup=supervisor \
--wait \
--pipe \
ls /sys/fs/cgroup/supervisor
}
testcase_delegate_subgroup_control() {
# Make sure control processes are namespaced, are put in the .control cgroup, have the .control group as
# the root of their cgroup namespace and don't violate the no inner processes rule. To ensure we don't
# violate the no inner processes rule, we make sure to enable a cgroup controller so that
# cgroup.subtree_control for the main service cgroup is not empty which will make any attempt to spawn
# processes into that cgroup fail with EBUSY.
assert_eq "$(
systemd-run \
--service-type=notify \
-p ProtectControlGroupsEx=private \
-p PrivateMounts=yes \
-p Delegate=yes \
-p DelegateSubgroup=supervisor \
-p ExecStartPost='sh -c "cat /proc/self/cgroup; kill $MAINPID"' \
--unit delegate-subgroup-control \
--wait \
--pipe \
sh -c 'echo +pids >/sys/fs/cgroup/cgroup.subtree_control; systemd-notify --ready; sleep infinity'
)" "0::/"
}
testcase_delegate_subgroup_pam() {
# Make sure any pam processes we spawn don't violate the no inner processes rule.
systemd-run \
--service-type=oneshot \
-p ProtectControlGroupsEx=private \
-p PrivateMounts=yes \
-p Delegate=yes \
-p DelegateSubgroup=supervisor \
-p User=testuser \
-p PAMName=systemd-user \
--unit delegate-subgroup-pam \
--wait \
--pipe \
sh -c 'echo +pids >/sys/fs/cgroup/cgroup.subtree_control'
}
run_testcases