sched_ext: Skip sub-disable teardown for never-linked sub-schedulers

A sub-scheduler enable can fail before scx_link_sched() links the sched into
the hierarchy, e.g. when the parent is already being disabled, and cleanup
still runs the full scx_sub_disable().

That is racy against root disable: drain_descendants() is the only ordering
between a sub's disable-time task walk and root disable's all-task teardown,
and an unlinked sub is invisible to it. Root's teardown can thus run between
the never-linked sub's drain and its walk, exiting every task to no
scheduler.

The walk then trips the membership WARN and re-homes the exited tasks onto
the dying hierarchy, a use-after-free.

Skip the cgroup ownership reset and the task walk if @sch was never linked,
indicated by the empty ->sibling as unlinking only happens later in the same
function. The membership WARN remains valid: a linked sub is always waited
on by an ancestor's drain.

Fixes: 337ec00b1d ("sched_ext: Implement cgroup sub-sched enabling and disabling")
Signed-off-by: Tejun Heo <tj@kernel.org>
Reviewed-by: Andrea Righi <arighi@nvidia.com>
This commit is contained in:
Tejun Heo 2026-07-16 10:45:23 -10:00
parent 5f8b69642d
commit 8c13364db9

View File

@ -5937,6 +5937,15 @@ static void scx_sub_disable(struct scx_sched *sch)
percpu_down_write(&scx_fork_rwsem); percpu_down_write(&scx_fork_rwsem);
scx_cgroup_lock(); scx_cgroup_lock();
/*
* An enable that failed before scx_link_sched() never owned a cgroup or
* task and won't be waited on by an ancestor's drain_descendants().
* Nothing to reparent and walking the tasks can misbehave as the task
* ownership invariant (either owned by self or parent) does not hold.
*/
if (list_empty(&sch->sibling))
goto dump;
set_cgroup_sched(sch_cgroup(sch), parent); set_cgroup_sched(sch_cgroup(sch), parent);
scx_task_iter_start(&sti, sch->cgrp); scx_task_iter_start(&sti, sch->cgrp);
@ -5949,8 +5958,8 @@ static void scx_sub_disable(struct scx_sched *sch)
continue; continue;
/* /*
* By the time control reaches here, all descendant schedulers * By the time control reaches here, all linked descendant
* should already have been disabled. * schedulers should have been disabled.
*/ */
WARN_ON_ONCE(!scx_task_on_sched(sch, p)); WARN_ON_ONCE(!scx_task_on_sched(sch, p));
@ -6017,6 +6026,7 @@ static void scx_sub_disable(struct scx_sched *sch)
} }
scx_task_iter_stop(&sti); scx_task_iter_stop(&sti);
dump:
scx_disable_dump(sch); scx_disable_dump(sch);
scx_cgroup_unlock(); scx_cgroup_unlock();