Re: refcount_t: addition on 0; use-after-free, regression from 260fbcb92bbe ("cgroup: Move dying_tasks cleanup from cgroup_task_release() to cgroup_task_free()")
Michal Koutný <[email protected]>
| Newsgroups | org.kernel.vger.stable,dev.linux.lists.regressions,org.kernel.vger.cgroups,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
Hi Salvatore, thanks for the nice report and sorry for not so prompt response. On Sat, Aug 15, 2026 at 09:41:14PM +0200, Salvatore Bonaccorso <[email protected]> wrote: > With an additional reproducer provided by Noah, I could bisect the > change down to Good job. > > commit 260fbcb92bbeacfcd050410fdc2d24ab15044400 > Author: Tejun Heo <[email protected]> > Date: Tue Oct 28 20:19:16 2025 -1000 > > cgroup: Move dying_tasks cleanup from cgroup_task_release() to cgroup_task_free() > > Currently, cgroup_task_exit() adds thread group leaders with live member > threads to their css_set's dying_tasks list (so cgroup.procs iteration can > still see the leader), and cgroup_task_release() later removes them with > list_del_init(&task->cg_list). > > An upcoming patch will defer the dying_tasks list addition, moving it from > cgroup_task_exit() (called from do_exit()) to a new function called from > finish_task_switch(). However, release_task() (which calls > cgroup_task_release()) can run either before or after finish_task_switch(), > creating a race where cgroup_task_release() might try to remove the task from > dying_tasks before or while it's being added. > > Move the list_del_init() from cgroup_task_release() to cgroup_task_free() to > fix this race. cgroup_task_free() runs from __put_task_struct(), which is > always after both paths, making the cleanup safe. > > Cc: Dan Schatzberg <[email protected]> > Cc: Peter Zijlstra <[email protected]> > Signed-off-by: Tejun Heo <[email protected]> > > But there was the suspect that the matching commit might be > d245698d727a ("cgroup: Defer task cgroup unlink until after the task > is done switching out"). I see that after 260fbcb92bbea ("cgroup: Move dying_tasks cleanup from cgroup_task_release() to cgroup_task_free()") it may be possible that tasks on the dying_tasks list may drop their ->usage to zero (since the actual unlinking only happens in __put_task_struct). Most often those would be skipped due to PF_EXITING except for the case of thread group leaders (which the reproducer stresses) whose refcount apparently can drop to zero after task->signal->live > 0 made them iterable :-/ A band-aid fix could be to use tryget_task_struct() in css_task_iter_next() (I got that hint from a LLM) and "skip" zeroed tasks. I see that commit fbe3fb103596b ("sched_ext: Replace tryget_task_struct() with get_task_struct()"), assumes the iterator always succeeds in obtaining the task reference (which was the justification of tryget removal). I expect that sched_ext should still be fine if dying_tasks with zero references are skipped. (What are they? Tasks which literally no one should be interested in and they're only waiting for __put_task_struct_rcu_cb() to be called [*]). (I'm calling that band-aid because it'd resurrect usage of tryget_task_struct() and it keeps the dying_tasks list a weird place to be. If anyone has a better idea?) The commit d245698d727a ("cgroup: Defer task cgroup unlink until after the task is done switching out") seems a reasonable separation of the stages to me. Regards, Michal [*] Except for io_uring_drop_tctx_refs() that calls __put_task_struct() directly (no RCU) but I'd argue the same, that those should not be possibly iterated.
signature.asc
(application/pgp-signature, 265 B)
-----BEGIN PGP SIGNATURE----- iJEEABYKADkWIQRCE24Fn/AcRjnLivR+PQLnlNv4CAUCaoiD6hsUgAAAAAAEAA5t YW51MiwyLjUrMS4xMiwyLDIACgkQfj0C55Tb+AiB/QD/Qx0ZJRbpqviaym2eUhdy AMfR99jU7RJJ+BnLU3zCy5oA/0JzSShj4XvDjSj7Pk9/fS4IW6CghDVOxoYWP5Od 1XkA =22up -----END PGP SIGNATURE-----