{"id":"CVE-2026-46223","summary":"cgroup: Defer css percpu_ref kill on rmdir until cgroup is depopulated","details":"In the Linux kernel, the following vulnerability has been resolved:\n\ncgroup: Defer css percpu_ref kill on rmdir until cgroup is depopulated\n\nA chain of commits going back to v7.0 reworked rmdir to satisfy the\ncontroller invariant that a subsystem's -\u003ecss_offline() must not run while\ntasks are still doing kernel-side work in the cgroup.\n\n[1] d245698d727a (\"cgroup: Defer task cgroup unlink until after the task is done switching out\")\n[2] a72f73c4dd9b (\"cgroup: Don't expose dead tasks in cgroup\")\n[3] 1b164b876c36 (\"cgroup: Wait for dying tasks to leave on rmdir\")\n[4] 4c56a8ac6869 (\"cgroup: Fix cgroup_drain_dying() testing the wrong condition\")\n[5] 13e786b64bd3 (\"cgroup: Increment nr_dying_subsys_* from rmdir context\")\n\n[1] moved task cset unlink from do_exit() to finish_task_switch() so a\ntask's cset link drops only after the task has fully stopped scheduling.\nThat made tasks past exit_signals() linger on cset-\u003etasks until their final\ncontext switch, which led to a series of problems as what userspace expected\nto see after rmdir diverged from what the kernel needs to wait for. [2]-[5]\ntried to bridge that divergence: [2] filtered the exiting tasks from\ncgroup.procs; [3] had rmdir(2) sleep in TASK_UNINTERRUPTIBLE for them; [4]\nfixed the wait's condition; [5] made nr_dying_subsys_* visible\nsynchronously.\n\nThe cgroup_drain_dying() wait in [3] turned out to be a dead end. When the\nrmdir caller is also the reaper of a zombie that pins a pidns teardown (e.g.\nhost PID 1 systemd reaping orphan pids that were re-parented to it during\nthe same teardown), rmdir blocks in TASK_UNINTERRUPTIBLE waiting for those\npids to free, the pids can't free because PID 1 is the reaper and it's stuck\nin rmdir, and the system A-A deadlocks. No internal lock ordering breaks\nthis; the wait itself is the bug.\n\nThe css killing side that drove the original reorder, however, can be made\ncleanly asynchronous: -\u003ecss_offline() is already async, run from\ncss_killed_work_fn() driven by percpu_ref_kill_and_confirm(). The fix is to\nmake that chain start only after all tasks have left the cgroup. rmdir's\nuser-visible side then returns as soon as cgroup.procs and friends are\nempty, while -\u003ecss_offline() still runs only after the cgroup is fully\ndrained.\n\nVerified by the original reproducer (pidns teardown + zombie reaper, runs\nunder vng) which hangs vanilla and succeeds here, and by per-commit\ndeterministic repros for [2], [3], [4], [5] with a boot parameter that\nwidens the post-exit_signals() window so each state is reliably reachable.\nSome stress tests on top of that.\n\ncgroup_apply_control_disable() has the same shape of pre-existing race:\nwhen a controller is disabled via subtree_control, kill_css() ran\nsynchronously while tasks past exit_signals() could still be linked to\nthe cgroup's csets, and -\u003ecss_offline() could fire before they drained.\nThis patch preserves the existing synchronous behavior at that call site\n(kill_css_sync() + kill_css_finish() back-to-back) and a follow-up patch\nwill defer kill_css_finish() there using a per-css trigger.\n\nThis seems like the right approach and I don't see problems with it. The\nchanges are somewhat invasive but not excessively so, so backporting to\n-stable should be okay. If something does turn out to be wrong, the fallback\nis to revert the entire chain ([1]-[5]) and rework in the development branch\ninstead.\n\nv2: Pin cgrp across the deferred destroy work with explicit\n    cgroup_get()/cgroup_put() around queue_work() and the work_fn. v1\n    wasn't actually broken (ordered cgroup_offline_wq + queue_work order\n    in cgroup_task_dead() saved it) but the explicit ref removes the\n    dependency on those non-obvious invariants. Also note the\n    pre-existing cgroup_apply_control_disable() race in the description;\n    a follow-up will defer kill_css_finish() there.","modified":"2026-10-28T02:31:01.441053404Z","published":"2026-05-28T09:40:40.791Z","related":["openSUSE-SU-2026:10954-1"],"database_specific":{"osv_generated_from":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/46xxx/CVE-2026-46223.json","cna_assigner":"Linux"},"references":[{"type":"WEB","url":"https://git.kernel.org/stable/c/33fa2e6b1507a0a377a151a8826438bedad1d0b0"},{"type":"WEB","url":"https://git.kernel.org/stable/c/93618edf753838a727dbff63c7c291dee22d656b"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/46xxx/CVE-2026-46223.json"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-46223"},{"type":"PACKAGE","url":"https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git"}],"affected":[{"ranges":[{"type":"GIT","repo":"https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git","events":[{"introduced":"1b164b876c36c3eb5561dd9b37702b04401b0166"},{"fixed":"33fa2e6b1507a0a377a151a8826438bedad1d0b0"},{"fixed":"93618edf753838a727dbff63c7c291dee22d656b"}]},{"type":"GIT","repo":"https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git","events":[{"introduced":"6.19.12"},{"fixed":"6.20"}]},{"type":"GIT","repo":"https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git","events":[{"introduced":"78c72bce4a87819126211c0d24e18350010604fb"}]}],"versions":["v6.19.14","v6.19.13","v6.19.12"],"database_specific":{"source":"https://storage.googleapis.com/cve-osv-conversion/osv-output/CVE-2026-46223.json"}},{"package":{"name":"Kernel","ecosystem":"Linux"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"7.0.0"},{"fixed":"7.0.9"}]}],"database_specific":{"source":"https://storage.googleapis.com/cve-osv-conversion/osv-output/CVE-2026-46223.json"}}],"schema_version":"1.9.2"}