core: fix EBUSY on restart and clean of delegated services

When a service is configured with Delegate=yes and DelegateSubgroup=sub,
the delegated container may write domain controllers (e.g. "pids") into the
service cgroup's cgroup.subtree_control via its cgroupns root. On container
exit the stale controllers remain, and on service restart clone3() with
CLONE_INTO_CGROUP fails with EBUSY because placing a process into a cgroup
that has domain controllers in subtree_control violates the no-internal-
processes rule. The same issue affects systemctl clean, where cg_attach()
fails with EBUSY for the same reason.

Add unit_cgroup_disable_all_controllers() helper in cgroup.c that clears
stale controllers via cg_enable(mask=0) and updates cgroup_enabled_mask to
keep internal tracking in sync. Call it from service_start() and
service_clean() right before spawning, so that resource control is preserved
for any lingering processes from the previous invocation as long as possible.
This commit is contained in:
Nandakumar Raghavan
2026-04-11 00:07:05 +01:00
committed by Luca Boccassi
parent 1016dd315f
commit 056bc106e1
3 changed files with 30 additions and 1 deletions
+23
View File
@@ -3988,6 +3988,29 @@ bool unit_cgroup_delegate(Unit *u) {
return c->delegate;
}
void unit_cgroup_disable_all_controllers(Unit *u) {
int r;
assert(u);
CGroupRuntime *crt = unit_get_cgroup_runtime(u);
if (!crt || !crt->cgroup_path)
return;
if (!unit_cgroup_delegate(u))
return;
/* For delegated units, the previous payload may have enabled controllers (e.g. "pids") in
* cgroup.subtree_control. These persist after the service stops and turn the cgroup into an
* "internal node", causing clone3(CLONE_INTO_CGROUP) to fail with EBUSY. Clear them now, right
* before the new start, so that resource control is preserved for lingering processes as long as
* possible. Ignore errors — if sub-cgroups still have live processes the write will fail, but so
* will the upcoming spawn. */
r = cg_enable(u->manager->cgroup_supported, /* mask= */ 0, crt->cgroup_path, &crt->cgroup_enabled_mask);
if (r < 0)
log_unit_debug_errno(u, r, "Failed to disable controllers on cgroup %s, ignoring: %m", empty_to_root(crt->cgroup_path));
}
void manager_invalidate_startup_units(Manager *m) {
Unit *u;
+2
View File
@@ -471,6 +471,8 @@ void unit_cgroup_catchup(Unit *u);
bool unit_cgroup_delegate(Unit *u);
void unit_cgroup_disable_all_controllers(Unit *u);
int unit_get_cpuset(Unit *u, CPUSet *cpus, const char *name);
int unit_cgroup_freezer_action(Unit *u, FreezerAction action);
+5 -1
View File
@@ -13,6 +13,7 @@
#include "bus-common-errors.h"
#include "bus-error.h"
#include "bus-util.h"
#include "cgroup.h"
#include "chase.h"
#include "dbus-service.h"
#include "dbus-unit.h"
@@ -3174,8 +3175,10 @@ static int service_start(Unit *u) {
exec_status_reset(&s->main_exec_status);
CGroupRuntime *crt = unit_get_cgroup_runtime(u);
if (crt)
if (crt) {
unit_cgroup_disable_all_controllers(u);
crt->reset_accounting = true;
}
service_enter_condition(s);
return 1;
@@ -5640,6 +5643,7 @@ static int service_clean(Unit *u, ExecCleanMask mask) {
goto fail;
}
unit_cgroup_disable_all_controllers(u);
r = unit_fork_and_watch_rm_rf(u, l, &s->control_pid);
if (r < 0) {
log_unit_warning_errno(u, r, "Failed to spawn cleaning task: %m");