]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
core: fix EBUSY on restart and clean of delegated services main
authorNandakumar Raghavan <naraghavan@microsoft.com>
Tue, 24 Mar 2026 13:42:42 +0000 (13:42 +0000)
committerLuca Boccassi <luca.boccassi@gmail.com>
Fri, 10 Apr 2026 23:07:05 +0000 (00:07 +0100)
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.

src/core/cgroup.c
src/core/cgroup.h
src/core/service.c

index ddca4afbc329b85b317b0147a071e7f8ddcad691..ae5874cd99daa49938f7345fb39f8db6ebc71003 100644 (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;
 
index ce98f4ba7cd3b2f8661b9ca63886a226db2482ed..d9a6ded110214b1788456bc37a5b5ac4421e49ea 100644 (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);
index 569a6871d602f41c14571ad505a74329c14d1f55..63e659942188f36ac8a30e632a47b2c1b99ae18f 100644 (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");