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.

Attachment: signature.asc
Description: PGP signature

Reply via email to