CVE-2026-46223
In the Linux kernel, the following vulnerability has been resolved:
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")
Leer descripción completaMostrar menos
[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.
Detalles técnicos trazas, registros y código del informe original
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.CVSS
- Versión: 3.1
- Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
- Puntuación base: 5.5
Probabilidad de explotación (EPSS)
- Probabilidad de explotación en los próximos 30 días: 0.11%
- Percentil entre todas las CVEs puntuadas: 1
- Fecha de la puntuación: 6/10/2026
EPSS (Exploit Prediction Scoring System, de FIRST) estima la probabilidad de que una vulnerabilidad sea explotada en 30 días. Complementa a CVSS (impacto) y a CISA KEV (explotación confirmada).
Tecnologías afectadas (1)
CWE
- CWE-667
Referencias
JSON original (NVD)
Mostrar
{
"id": "CVE-2026-46223",
"cveTags": [],
"metrics": {
"cvssMetricV31": [
{
"type": "Primary",
"source": "nvd@nist.gov",
"cvssData": {
"scope": "UNCHANGED",
"version": "3.1",
"baseScore": 5.5,
"attackVector": "LOCAL",
"baseSeverity": "MEDIUM",
"vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H",
"integrityImpact": "NONE",
"userInteraction": "NONE",
"attackComplexity": "LOW",
"availabilityImpact": "HIGH",
"privilegesRequired": "LOW",
"confidentialityImpact": "NONE"
},
"impactScore": 3.6,
"exploitabilityScore": 1.8
}
]
},
"affected": [
{
"source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67",
"affectedData": [
{
"repo": "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git",
"vendor": "Linux",
"product": "Linux",
"versions": [
{
"status": "affected",
"version": "1b164b876c36c3eb5561dd9b37702b04401b0166",
"lessThan": "33fa2e6b1507a0a377a151a8826438bedad1d0b0",
"versionType": "git"
},
{
"status": "affected",
"version": "1b164b876c36c3eb5561dd9b37702b04401b0166",
"lessThan": "93618edf753838a727dbff63c7c291dee22d656b",
"versionType": "git"
},
{
"status": "affected",
"version": "78c72bce4a87819126211c0d24e18350010604fb",
"versionType": "git"
},
{
"status": "affected",
"version": "6.19.12",
"lessThan": "6.20",
"versionType": "semver"
}
],
"programFiles": [
"include/linux/cgroup-defs.h",
"kernel/cgroup/cgroup.c"
],
"defaultStatus": "unaffected"
},
{
"repo": "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git",
"vendor": "Linux",
"product": "Linux",
"versions": [
{
"status": "affected",
"version": "7.0"
},
{
"status": "unaffected",
"version": "0",
"lessThan": "7.0",
"versionType": "semver"
},
{
"status": "unaffected",
"version": "7.0.9",
"versionType": "semver",
"lessThanOrEqual": "7.0.*"
},
{
"status": "unaffected",
"version": "7.1",
"versionType": "original_commit_for_fix",
"lessThanOrEqual": "*"
}
],
"programFiles": [
"include/linux/cgroup-defs.h",
"kernel/cgroup/cgroup.c"
],
"defaultStatus": "affected"
}
]
}
],
"published": "2026-05-28T10:16:37.913",
"references": [
{
"url": "https://git.kernel.org/stable/c/33fa2e6b1507a0a377a151a8826438bedad1d0b0",
"tags": [
"Patch"
],
"source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67"
},
{
"url": "https://git.kernel.org/stable/c/93618edf753838a727dbff63c7c291dee22d656b",
"tags": [
"Patch"
],
"source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67"
}
],
"vulnStatus": "Analyzed",
"weaknesses": [
{
"type": "Primary",
"source": "nvd@nist.gov",
"description": [
{
"lang": "en",
"value": "CWE-667"
}
]
}
],
"descriptions": [
{
"lang": "en",
"value": "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 ->css_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->tasks 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: ->css_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 ->css_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 ->css_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."
}
],
"lastModified": "2026-06-17T10:53:21.750",
"configurations": [
{
"nodes": [
{
"negate": false,
"cpeMatch": [
{
"criteria": "cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*",
"vulnerable": true,
"matchCriteriaId": "3E8EC0D8-1F19-40EC-8F9E-9E4FCF84758C",
"versionEndExcluding": "7.0",
"versionStartIncluding": "6.19.12"
},
{
"criteria": "cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*",
"vulnerable": true,
"matchCriteriaId": "D623DD8A-11FD-499C-9170-FDCF8C2AB363",
"versionEndExcluding": "7.0.9",
"versionStartIncluding": "7.0.1"
},
{
"criteria": "cpe:2.3:o:linux:linux_kernel:7.0:-:*:*:*:*:*:*",
"vulnerable": true,
"matchCriteriaId": "EF897730-3F1E-47A2-8B07-22535202C487"
},
{
"criteria": "cpe:2.3:o:linux:linux_kernel:7.0:rc7:*:*:*:*:*:*",
"vulnerable": true,
"matchCriteriaId": "512EE3A8-A590-4501-9A94-5D4B268D6138"
},
{
"criteria": "cpe:2.3:o:linux:linux_kernel:7.1:rc1:*:*:*:*:*:*",
"vulnerable": true,
"matchCriteriaId": "B1EF7059-E670-45F4-B422-54C40FA86390"
},
{
"criteria": "cpe:2.3:o:linux:linux_kernel:7.1:rc2:*:*:*:*:*:*",
"vulnerable": true,
"matchCriteriaId": "0D38F0BF-A728-4133-A358-D44A2F7EE6D6"
}
],
"operator": "OR"
}
]
}
],
"sourceIdentifier": "416baaa9-dc9f-4396-8d5f-8c081fb06d67"
}