cgroup: Defer css percpu_ref kill on rmdir until cgroup is depopulated
Subsystems (3)
Tejun Heo
maintainerPATCH v2 1/1 cgroup: Defer css percpu_ref kill on rmdir until cgroup is depopulated
A chain of commits going back to v7.0 reworked rmdir to satisfy the controller invariant that a subsystem's ->css_offline() must not run while tasks are still doing kernel-side work in the cgroup.
[1] d245698d727a ("cgroup: Defer task cgroup unlink until after the task is done switching out")
[2] a72f73c4dd9b ("cgroup: Don't expose dead tasks in cgroup")
[3] 1b164b876c36 ("cgroup: Wait for dying tasks to leave on rmdir")
[4] 4c56a8ac6869 ("cgroup: Fix cgroup_drain_dying() testing the wrong condition")
[5] 13e786b64bd3 ("cgroup: Increment nr_dying_subsys_* from rmdir context")
[1] moved task cset unlink from do_exit() to finish_task_switch() so a task's cset link drops only after the task has fully stopped scheduling. That made tasks past exit_signals() linger on cset->tasks until their final context switch, which led to a series of problems as what userspace expected to see after rmdir diverged from what the kernel needs to wait for. [2]-[5] tried to bridge that divergence: [2] filtered the exiting tasks from cgroup.procs; [3] had rmdir(2) sleep in TASK_UNINTERRUPTIBLE for them; [4] fixed the wait's condition; [5] made nr_dying_subsys_* visible synchronously.
The cgroup_drain_dying() wait in [3] turned out to be a dead end. When the rmdir caller is also the reaper of a zombie that pins a pidns teardown (e.g. host PID 1 systemd reaping orphan pids that were re-parented to it during the same teardown), rmdir blocks in TASK_UNINTERRUPTIBLE waiting for those pids to free, the pids can't free because PID 1 is the reaper and it's stuck in rmdir, and the system A-A deadlocks. No internal lock ordering breaks this; the wait itself is the bug.
The css killing side that drove the original reorder, however, can be made cleanly asynchronous: ->css_offline() is already async, run from css_killed_work_fn() driven by percpu_ref_kill_and_confirm(). The fix is to make that chain start only after all tasks have left the cgroup. rmdir's user-visible side then returns as soon as cgroup.procs and friends are empty, while ->css_offline() still runs only after the cgroup is fully drained.
Verified by the original reproducer (pidns teardown + zombie reaper, runs under vng) which hangs vanilla and succeeds here, and by per-commit deterministic repros for [2], [3], [4], [5] with a boot parameter that widens the post-exit_signals() window so each state is reliably reachable. Some stress tests on top of that.
cgroup_apply_control_disable() has the same shape of pre-existing race:
when a controller is disabled via subtree_control, kill_css() ran synchronously while tasks past exit_signals() could still be linked to the cgroup's csets, and ->css_offline() could fire before they drained. This patch preserves the existing synchronous behavior at that call site (kill_css_sync() + kill_css_finish() back-to-back) and a follow-up patch will defer kill_css_finish() there using a per-css trigger.
This seems like the right approach and I don't see problems with it. The changes are somewhat invasive but not excessively so, so backporting to -stable should be okay. If something does turn out to be wrong, the fallback is to revert the entire chain ([1]-[5]) and rework in the development branch instead.
v2: Pin cgrp across the deferred destroy work with explicit
cgroup_get()/cgroup_put() around queue_work() and the work_fn. v1 wasn't actually broken (ordered cgroup_offline_wq + queue_work order in cgroup_task_dead() saved it) but the explicit ref removes the dependency on those non-obvious invariants. Also note the pre-existing cgroup_apply_control_disable() race in the description; a follow-up will defer kill_css_finish() there.
---
Hello Martin,
Could you give v2 a try? Same defer-the-percpu_ref-kill mechanism as v1, with an explicit cgroup_get/put around the deferred work to make the lifetime invariant obvious (Sashiko bot review on v1; v1 wasn't broken but the explicit ref removes a dependency on non-obvious ordering). Fix should behave identically to v1 for your reproducer.
Thanks.
include/linux/cgroup-defs.h | 4 kernel/cgroup/cgroup.c | 250 ++++++++++++++++++++------------------------ 2 files changed, 119 insertions(+), 135 deletions(-)
include/linux/cgroup-defs.h +2 -2
@@ -611,8 +611,8 @@ struct cgroup {/* used to wait for offlining of csses */ wait_queue_head_t offline_waitq; -/* used by cgroup_rmdir() to wait for dying tasks to leave */ -wait_queue_head_t dying_populated_waitq; +/* defers killing csses after removal until cgroup is depopulated */ +struct work_struct finish_destroy_work;/* used to schedule release agent */ struct work_struct release_agent_work;
kernel/cgroup/cgroup.c +117 -133
368 lines not on this page · raw message
Hello Tejun and all,
Tejun Heo [2026-05-01 8:31 -1000]:
Quoted 12 lines
A chain of commits going back to v7.0 reworked rmdir to satisfy the controller invariant that a subsystem's ->css_offline() must not run while tasks are still doing kernel-side work in the cgroup.
[..]
v2: Pin cgrp across the deferred destroy work with explicit
cgroup_get()/cgroup_put() aroundqueue_work()and thework_fn. v1 wasn't actually broken (orderedcgroup_offline_wq+queue_workorder incgroup_task_dead()saved it) but the explicit ref removes the dependency on those non-obvious invariants. Also note the pre-existingcgroup_apply_control_disable()race in the description; a follow-up will deferkill_css_finish()there.
Quoted 1 line
Fixes 1b164b876c36 ("cgroup: Wait for dying tasks to leave on rmdir")
Quoted 5 lines
Could you give v2 a try? Same defer-the-percpu_ref-kill mechanism as v1, with an explicit cgroup_get/put around the deferred work to make the lifetime invariant obvious (Sashiko bot review on v1; v1 wasn't broken but the explicit ref removes a dependency on non-obvious ordering). Fix should behave identically to v1 for your reproducer.
Sorry for the delay, I haven't built a kernel in a decade and not ever for Fedora.
I applied the patch to the Rawhide 7.1.0-0.rc1 kernel, it applies cleanly there. (I first tried on top of 6.9.14, but there are conflicts.)
https://copr.fedorainfracloud.org/coprs/martinpitt/test-fixes/build/10419932/
Usage on Fedora 44:
dnf copr enable martinpitt/test-fixes
dnf update kernel-core kernel-modules-internal
(This assumes a cloud VM. If you use the full kernel, update the "kernel" package as well).
I ran the cockpit-podman test that originally triggered the bug, as well as my reduced variant, against that patched kernel for 50 rounds each, and it has consistently succeeded.
So this works great, thanks a lot!
Martin
Quoted 3 lines · Tejun Heo
A chain of commits going back to v7.0 reworked rmdir to satisfy the controller invariant that a subsystem's ->css_offline() must not run while tasks are still doing kernel-side work in the cgroup.
…
Quoted 8 lines
Fixes 1b164b876c36 ("cgroup: Wait for dying tasks to leave on rmdir")Cc [email protected] # v7.0+Reported-by Martin Pitt <[email protected]>Signed-off-by Tejun Heo <[email protected]>Cc Sebastian Andrzej Siewior <[email protected]>
I gave it a quick spin, everything looks fine.
Sebastian
Hello,
Applied to cgroup/for-7.1-fixes.
Thanks.
--
tejun