CVE-2026-98163: cgroup: Avoid iteration of dying tasks with zero refcount
In the Linux kernel, the following vulnerability has been resolved:
cgroup: Avoid iteration of dying tasks with zero refcount
The commit 260fbcb92bbea ("cgroup: Move dyingtasks cleanup from cgrouptaskrelease() to cgrouptaskfree()") extended the lifetime of tasks on the dyingtasks list. The iterators have provision to go through dyingtasks because of dying threadgroup leaders or explicit CSSTASKITERWITHDEAD, however, it was expected that such tasks can obtain a new reference (that is possible before cgrouptaskrelease()/puttaskstructrcuuser()). The tasks after cgrouptaskrelease() and before cgrouptaskfree() are subject to race when they may or may not have ->usage count > 0.
The race window is between csstaskiternext() invocations when csssetlock is released and we may arrive at a new ->taskpos. The iterator should not attempt to resurrect tasks whose ->usage count dropped to zero. (When that happens, puttaskstructrcucb() is already imminent and the returned taskstruct would could be used after free.)
As for the fix, we cannot simply check the signal->live count of a task on the dying list because that won't distinguish regular zombies waiting to be reaped from RCU remnant tasks that are going to be free'd. Therefore add an extra check to rule out ->usage==0 tasks from any iteration.
The repeat: loop in csstaskiteradvance() doesn't consider ->usage count, so add a new loop to csstaskiternext() to skip de-used tasks on the dyinglist.
Rough illustration of the possible race
R (reader of cgroup.procs) T (thread) L (group leader) --------------------------------- -------------------------------- -------------------------------- L exits, signal->live > 0 cgrouptaskdead(L) csssetskiptaskiters() // skips only cset->tasks listaddtail(&L->cglist, &cset->dyingtasks) csstaskiternext() take csssetlock csstaskiteradvance() leader && signal->live != 0 => it->taskpos = &L->cglist release csssetlock T exits --signal->live == 0 cgrouptaskdead(T) // csssetlock releasetask(T) cgrouptaskrelease(T) releasetask(L) // zapleader cgrouptaskrelease(L) puttaskstructrcuuser(L) ...RCU... puttaskstruct(L) L->usage = 0 / L still on dyingtasks / ...RCU... puttaskstruct(L) csstaskiternext() // another iteration take csssetlock it->taskpos = &L->cglist gettaskstruct(L) => addition on 0 drop csssetlock cgrouptaskfree(L) csssetskiptaskiters() // dying skip comes too late freetask(L) cgroupprocsshow() taskpidvnr(L)
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Compensating control
Modify css_task_iter_next() to skip tasks whose ->usage count has dropped to zero, including dying tasks on the dying_tasks list, so iterators do not resurrect or return tasks that are already imminent for freeing.
Event History
Frequently Asked Questions
What conditions are needed to trigger this issue?
The race involves cgroup task iteration over dying tasks, such as iteration related to dying thread-group leaders or use of CSS_TASK_ITER_WITH_DEAD. It can occur when css_task_iter_next() releases css_set_lock between calls and reaches a task whose usage count has dropped to zero.
What is the impact if the race occurs?
An iterator can attempt to acquire a new reference to a task that is already being freed. The returned task_struct can then be used after free.