sched_ext: Fix sched_ext_dead() races with task initialization
Subsystems (2)
Tejun Heo
reviewerPATCH 1/1 sched_ext: Fix sched_ext_dead() races with task initialization
Hello,
zhidao su reported a NULL deref and an ops.init_task() leak when sched_ext_dead() races scx_root_enable_workfn() in CONFIG_EXT_SUB_SCHED kernels [1]. The same race window also affects the analogous sub-sched paths (scx_sub_enable_workfn()'s per-task init pass and scx_sub_disable()'s migration loop), and the wrapper-disable paths trip on the NONE state that scx_fail_parent() leaves behind. Closing all of these calls for a state-machine extension rather than a localized fix.
The series introduces SCX_TASK_INIT_BEGIN as an explicit intermediate state between NONE and INIT, and replaces the SCX_TASK_OFF_TASKS marker flag with a real SCX_TASK_DEAD terminal state. With the state machine in place, every init path uses the same handshake: write INIT_BEGIN under rq lock, init outside the lock, recheck DEAD under rq lock, unwind via scx_sub_init_cancel_task() on hit. The wrapper-disable and switched_from_scx() paths get NONE early-returns to handle the scx_fail_parent() residue.
It is more invasive than zhidao's patches but covers the related races uniformly and avoids the implicit list_empty() check his approach relies on. Credit to him for finding and reporting the bug.
0001 sched_ext: Cleanups in preparation for the SCX_TASK_INIT_BEGIN/DEAD work 0002 sched_ext: Inline scx_init_task() and move RESET_RUNNABLE_AT into scx_set_task_state() 0003 sched_ext: Replace SCX_TASK_OFF_TASKS flag with SCX_TASK_DEAD state 0004 sched_ext: Close root-enable vs sched_ext_dead() race with SCX_TASK_INIT_BEGIN 0005 sched_ext: Close sub-sched init race with post-init DEAD recheck
0006 sched_ext: Handle SCX_TASK_NONE in disable/switched_from paths
Based on sched_ext/for-7.1-fixes (ab28a0673daa).
Git tree: git://git.kernel.org/pub/scm/linux/kernel/git/tj/sched_ext.git for-7.1-fixes-dead-race
Verified with a debug patch that widens the unlocked init windows on the root and sub-sched paths and counts post-init DEAD-recheck hits. Reproducers exercise each of the original races plus the scx_fail_parent NONE-state regression, followed by a multi-iteration stress under fork churn. Counters show the windows are hit and no BUG/WARNING/Oops/Invalid-task-state appears.
[1] https://lore.kernel.org/all/[email protected]/
include/linux/sched/ext.h | 17 ++-- kernel/sched/ext.c | 221 +++++++++++++++++++++++++++++++--------------- 2 files changed, 162 insertions(+), 76 deletions(-)
Thanks.
--
tejun
Tejun Heo
reviewerPATCH 1/6 sched_ext: Cleanups in preparation for the SCX_TASK_INIT_BEGIN/DEAD work
Cleanups in preparation for the state-machine work that follows:
- Convert three sub-sched call sites that open-code rcu_assign_pointer(p->scx.sched, ...) to scx_set_task_sched().
- Move scx_get_task_state()/scx_set_task_state() above the SCX task iter section so scx_task_iter_next_locked() can use them without a forward declaration.
No functional change.
--- kernel/sched/ext.c | 76 +++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 38 deletions(-)
kernel/sched/ext.c +38 -38
@@ -711,6 +711,41 @@ struct bpf_iter_scx_dsq { } __attribute__((aligned(8))); +static u32 scx_get_task_state (const struct task_struct *p) +{ +return p->scx.flags &SCX_TASK_STATE_MASK ; +} + +static void scx_set_task_state (struct task_struct *p,u32 state) +{ +u32 prev_state =scx_get_task_state (p); +bool warn = false; + +switch (state) { +case SCX_TASK_NONE : +break ; +case SCX_TASK_INIT : + warn = prev_state !=SCX_TASK_NONE ; +break ; +case SCX_TASK_READY : + warn = prev_state ==SCX_TASK_NONE ; +break ; +case SCX_TASK_ENABLED : + warn = prev_state !=SCX_TASK_READY ; +break ; +default : +WARN_ONCE (1 ,"sched_ext: Invalid task state %d -> %d for %s[%d]" , + prev_state, state, p->comm, p->pid); +return ; + } + +WARN_ONCE (warn,"sched_ext: Invalid task state transition 0x%x -> 0x%x for %s[%d]" , + prev_state, state, p->comm, p->pid); + + p->scx.flags &= ~SCX_TASK_STATE_MASK ; + p->scx.flags |= state; +} +/* * SCX task iterator. */
@@ -3499,41 +3534,6 @@ static struct cgroup *tg_cgrp(struct task_group *tg)#endif /* CONFIG_EXT_GROUP_SCHED */ -static u32 scx_get_task_state (const struct task_struct *p) -{ -return p->scx.flags &SCX_TASK_STATE_MASK ; -} - -static void scx_set_task_state (struct task_struct *p,u32 state) -{ -u32 prev_state =scx_get_task_state (p); -bool warn = false; - -switch (state) { -case SCX_TASK_NONE : -break ; -case SCX_TASK_INIT : - warn = prev_state !=SCX_TASK_NONE ; -break ; -case SCX_TASK_READY : - warn = prev_state ==SCX_TASK_NONE ; -break ; -case SCX_TASK_ENABLED : - warn = prev_state !=SCX_TASK_READY ; -break ; -default : -WARN_ONCE (1 ,"sched_ext: Invalid task state %d -> %d for %s[%d]" , - prev_state, state, p->comm, p->pid); -return ; - } - -WARN_ONCE (warn,"sched_ext: Invalid task state transition 0x%x -> 0x%x for %s[%d]" , - prev_state, state, p->comm, p->pid); - - p->scx.flags &= ~SCX_TASK_STATE_MASK ; - p->scx.flags |= state; -} -static int __scx_init_task (struct scx_sched *sch,struct task_struct *p,bool fork) {int ret;
@@ -5696,7 +5696,7 @@ static void scx_fail_parent(struct scx_sched *sch, scoped_guard (sched_change, p, DEQUEUE_SAVE | DEQUEUE_MOVE) {scx_disable_and_exit_task (sch, p); -rcu_assign_pointer (p->scx.sched, parent); +scx_set_task_sched (p, parent); } }scx_task_iter_stop (&sti);
@@ -5783,7 +5783,7 @@ static void scx_sub_disable(struct scx_sched *sch)*/ scx_disable_and_exit_task (sch, p);scx_set_task_state (p,SCX_TASK_INIT ); -rcu_assign_pointer (p->scx.sched, parent); +scx_set_task_sched (p, parent);scx_set_task_state (p,SCX_TASK_READY );scx_enable_task (parent, p); }
@@ -7212,7 +7212,7 @@ static void scx_sub_enable_workfn(struct kthread_work *work)* $p is now only initialized for @sch and READY, which * is what we want. Assign it to @sch and enable. */ -rcu_assign_pointer (p->scx.sched, sch); +scx_set_task_sched (p, sch);scx_enable_task (sch, p); p->scx.flags &= ~SCX_TASK_SUB_INIT ;
Signature · 1 line
2.54.0
Tejun Heo
reviewerPATCH 2/6 sched_ext: Inline scx_init_task() and move RESET_RUNNABLE_AT into scx_set_task_state()
In reply to Tejun HeoPrepare for the SCX_TASK_INIT_BEGIN/DEAD work that follows by collapsing the scx_init_task() helper. Move the SCX_TASK_RESET_RUNNABLE_AT setting into scx_set_task_state() on the INIT transition (it was set unconditionally at every INIT site through the scx_init_task() helper), inline scx_init_task() into scx_fork() and scx_root_enable_workfn(), and drop the helper.
As a side effect, scx_sub_disable() migration sequence now also sets RESET_RUNNABLE_AT (it previously wrote INIT directly without going through scx_init_task()). The flag triggers a runnable_at reset on the next set_task_runnable(), which is harmless on a task that has just been moved between scheds.
--- kernel/sched/ext.c | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-)
kernel/sched/ext.c +9 -22
@@ -726,6 +726,7 @@ static void scx_set_task_state(struct task_struct *p, u32 state)break ;case SCX_TASK_INIT : warn = prev_state !=SCX_TASK_NONE ; + p->scx.flags |=SCX_TASK_RESET_RUNNABLE_AT ;break ;case SCX_TASK_READY : warn = prev_state ==SCX_TASK_NONE ;
@@ -3585,22 +3586,6 @@ static int __scx_init_task(struct scx_sched *sch, struct task_struct *p, bool foreturn 0 ; } -static int scx_init_task (struct scx_sched *sch,struct task_struct *p,bool fork) -{ -int ret; - - ret =__scx_init_task (sch, p, fork); -if (!ret) { -/* -* While @p's rq is not locked. @p is not visible to the rest of -* SCX yet and it's safe to update the flags and state. -*/ - p->scx.flags |=SCX_TASK_RESET_RUNNABLE_AT ; -scx_set_task_state (p,SCX_TASK_INIT ); - } -return ret; -} -static void __scx_enable_task (struct scx_sched *sch,struct task_struct *p) {struct rq *rq =task_rq (p);
@@ -3763,10 +3748,11 @@ int scx_fork(struct task_struct *p, struct kernel_clone_args *kargs)#else struct scx_sched *sch = scx_root;#endif - ret =scx_init_task (sch, p, true); -if (!ret) -scx_set_task_sched (p, sch); -return ret; + ret =__scx_init_task (sch, p, true); +if (unlikely (ret)) +return ret; +scx_set_task_state (p,SCX_TASK_INIT ); +scx_set_task_sched (p, sch); }return 0 ;
@@ -6897,8 +6883,8 @@ static void scx_root_enable_workfn(struct kthread_work *work)scx_task_iter_unlock (&sti); - ret =scx_init_task (sch, p, false); -if (ret) { + ret =__scx_init_task (sch, p, false); +if (unlikely (ret)) {put_task_struct (p);scx_task_iter_stop (&sti); scx_error(sch, "ops.init_task() failed (%d) for %s[%d]",
@@ -6906,6 +6892,7 @@ static void scx_root_enable_workfn(struct kthread_work *work) goto err_disable_unlock_all; } +scx_set_task_state (p,SCX_TASK_INIT );scx_set_task_sched (p, sch);scx_set_task_state (p,SCX_TASK_READY );
Signature · 1 line
2.54.0
Andrea Righi
reviewerRe: [PATCH 2/6] sched_ext: Inline scx_init_task() and move RESET_RUNNABLE_AT into scx_set_task_state()
Hi Tejun,
Quoted 22 lines · Tejun Heo
Prepare for the SCX_TASK_INIT_BEGIN/DEAD work that follows by collapsing the
scx_init_task()helper. Move theSCX_TASK_RESET_RUNNABLE_ATsetting intoscx_set_task_state()on the INIT transition (it was set unconditionally at every INIT site through thescx_init_task()helper), inlinescx_init_task()intoscx_fork()andscx_root_enable_workfn(), and drop the helper.As a side effect,
scx_sub_disable()migration sequence now also setsRESET_RUNNABLE_AT(it previously wrote INIT directly without going throughscx_init_task()). The flag triggers arunnable_atreset on the nextset_task_runnable(), which is harmless on a task that has just been moved between scheds.Signed-off-by Tejun Heo <[email protected]>--- kernel/sched/ext.c | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-)kernel/sched/ext.c +0
-0
kernel/sched/ext.c static void scx_set_task_state(struct task_struct *p, u32 state) · line 729
Quoted 4 lines
kernel/sched/ext.c +1
-0@@ -726,6 +726,7 @@ static void scx_set_task_state(struct task_struct *p, u32 state)break ;case SCX_TASK_INIT : warn = prev_state !=SCX_TASK_NONE ; + p->scx.flags |=SCX_TASK_RESET_RUNNABLE_AT ;
IIUC enable path may touch p->scx.flags without the task rq lock held, while struct sched_ext_entity documents flags as rq lock protected. But this whould be fixed later by the follow-up INIT_BEGIN on the root-enable path right?
Maybe we can add a small comment in the patch description (something along these lines)?
Note: root enable may touch p->scx.flags without the task's rq lock; this is addressed by follow-up patches (SCX_TASK_INIT_BEGIN).
Thanks,
-Andrea
Quoted 64 lines
kernel/sched/ext.c +8
-22break ;case SCX_TASK_READY : warn = prev_state ==SCX_TASK_NONE ;@@ -3585,22 +3586,6 @@ static int __scx_init_task(struct scx_sched *sch, struct task_struct *p, bool foreturn 0 ; } -static int scx_init_task (struct scx_sched *sch,struct task_struct *p,bool fork) -{ -int ret; - - ret =__scx_init_task (sch, p, fork); -if (!ret) { -/* -* While @p's rq is not locked. @p is not visible to the rest of -* SCX yet and it's safe to update the flags and state. -*/ - p->scx.flags |=SCX_TASK_RESET_RUNNABLE_AT ; -scx_set_task_state (p,SCX_TASK_INIT ); - } -return ret; -} -static void __scx_enable_task (struct scx_sched *sch,struct task_struct *p) {struct rq *rq =task_rq (p);@@ -3763,10 +3748,11 @@ int scx_fork(struct task_struct *p, struct kernel_clone_args *kargs)#else struct scx_sched *sch = scx_root;#endif - ret =scx_init_task (sch, p, true); -if (!ret) -scx_set_task_sched (p, sch); -return ret; + ret =__scx_init_task (sch, p, true); +if (unlikely (ret)) +return ret; +scx_set_task_state (p,SCX_TASK_INIT ); +scx_set_task_sched (p, sch); }return 0 ;@@ -6897,8 +6883,8 @@ static void scx_root_enable_workfn(struct kthread_work *work)scx_task_iter_unlock (&sti); - ret =scx_init_task (sch, p, false); -if (ret) { + ret =__scx_init_task (sch, p, false); +if (unlikely (ret)) {put_task_struct (p);scx_task_iter_stop (&sti); scx_error(sch, "ops.init_task() failed (%d) for %s[%d]",@@ -6906,6 +6892,7 @@ static void scx_root_enable_workfn(struct kthread_work *work) goto err_disable_unlock_all; } +scx_set_task_state (p,SCX_TASK_INIT );scx_set_task_sched (p, sch);scx_set_task_state (p,SCX_TASK_READY );Signature · 1 line
2.54.0
Tejun Heo
reviewerPATCH v2 2/6 sched_ext: Inline scx_init_task() and move RESET_RUNNABLE_AT into scx_set_task_state()
In reply to Tejun HeoPrepare for the SCX_TASK_INIT_BEGIN/DEAD work that follows by collapsing the scx_init_task() helper. Move the SCX_TASK_RESET_RUNNABLE_AT setting into scx_set_task_state() on the INIT transition (it was set unconditionally at every INIT site through the scx_init_task() helper), inline scx_init_task() into scx_fork() and scx_root_enable_workfn(), and drop the helper.
As a side effect, scx_sub_disable() migration sequence now also sets RESET_RUNNABLE_AT (it previously wrote INIT directly without going through scx_init_task()). The flag triggers a runnable_at reset on the next set_task_runnable(), which is harmless on a task that has just been moved between scheds.
On root-enable, p->scx.flags is written without the task's rq lock. The task isn't visible to scx yet, and a follow-up patch restores the lock-held write.
v2: Note p->scx.flags rq-lock relaxation on root-enable path. (Andrea)
--- kernel/sched/ext.c | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-)
kernel/sched/ext.c +9 -22
@@ -726,6 +726,7 @@ static void scx_set_task_state(struct task_struct *p, u32 state)break ;case SCX_TASK_INIT : warn = prev_state !=SCX_TASK_NONE ; + p->scx.flags |=SCX_TASK_RESET_RUNNABLE_AT ;break ;case SCX_TASK_READY : warn = prev_state ==SCX_TASK_NONE ;
@@ -3585,22 +3586,6 @@ static int __scx_init_task(struct scx_sched *sch, struct task_struct *p, bool foreturn 0 ; } -static int scx_init_task (struct scx_sched *sch,struct task_struct *p,bool fork) -{ -int ret; - - ret =__scx_init_task (sch, p, fork); -if (!ret) { -/* -* While @p's rq is not locked. @p is not visible to the rest of -* SCX yet and it's safe to update the flags and state. -*/ - p->scx.flags |=SCX_TASK_RESET_RUNNABLE_AT ; -scx_set_task_state (p,SCX_TASK_INIT ); - } -return ret; -} -static void __scx_enable_task (struct scx_sched *sch,struct task_struct *p) {struct rq *rq =task_rq (p);
@@ -3763,10 +3748,11 @@ int scx_fork(struct task_struct *p, struct kernel_clone_args *kargs)#else struct scx_sched *sch = scx_root;#endif - ret =scx_init_task (sch, p, true); -if (!ret) -scx_set_task_sched (p, sch); -return ret; + ret =__scx_init_task (sch, p, true); +if (unlikely (ret)) +return ret; +scx_set_task_state (p,SCX_TASK_INIT ); +scx_set_task_sched (p, sch); }return 0 ;
@@ -6897,8 +6883,8 @@ static void scx_root_enable_workfn(struct kthread_work *work)scx_task_iter_unlock (&sti); - ret =scx_init_task (sch, p, false); -if (ret) { + ret =__scx_init_task (sch, p, false); +if (unlikely (ret)) {put_task_struct (p);scx_task_iter_stop (&sti); scx_error(sch, "ops.init_task() failed (%d) for %s[%d]",
@@ -6906,6 +6892,7 @@ static void scx_root_enable_workfn(struct kthread_work *work) goto err_disable_unlock_all; } +scx_set_task_state (p,SCX_TASK_INIT );scx_set_task_sched (p, sch);scx_set_task_state (p,SCX_TASK_READY );
Signature · 1 line
2.54.0
[email protected]
AI reviewRe: [PATCH 2/6] sched_ext: Inline scx_init_task() and move RESET_RUNNABLE_AT into scx_set_task_state()
In reply to Tejun HeoThank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [High] Use-After-Free (UAF) of task_struct fields `p->comm` and `p->pid` in the error path of…
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Use-After-Free (UAF) of task_struct fields p->comm and p->pid in the error path of scx_root_enable_workfn(). --
commit ff748fbf4ba914d4e370692e8c52959bf3619890
Author: Tejun Heo <[email protected]>
sched_ext: Inline scx_init_task() and move RESET_RUNNABLE_AT into scx_set_task_state()
This commit prepares for the SCX_TASK_INIT_BEGIN/DEAD work by collapsing the scx_init_task() helper and moving the SCX_TASK_RESET_RUNNABLE_AT setting into scx_set_task_state(). It also inlines scx_init_task() into scx_fork() and scx_root_enable_workfn(), dropping the helper.
Quoted 3 lines
kernel/sched/ext.c +0
-0
[ ... ]
Quoted 11 lines
kernel/sched/ext.c +2
-2@@ -6884,8 +6870,8 @@ static void scx_root_enable_workfn(struct kthread_work *work)scx_task_iter_unlock (&sti); - ret =scx_init_task (sch, p, false); -if (ret) { + ret =__scx_init_task (sch, p, false); +if (unlikely (ret)) {put_task_struct (p);scx_task_iter_stop (&sti); scx_error(sch, "ops.init_task() failed (%d) for %s[%d]",
Quoted 1 line
ret,
p->comm,p->pid);
This isn't a bug introduced by this commit, but could this lead to a use-after-free of the task struct?
If put_task_struct(p) drops the last reference (for example, if the task is concurrently exiting), it will schedule the task struct for RCU-delayed freeing.
Immediately after, scx_task_iter_stop() releases the task iterator locks, which can re-enable preemption and interrupts. Since the thread holds sleeping locks, an RCU grace period could potentially elapse before execution resumes.
If that happens, accessing p->comm and p->pid in scx_error() would dereference freed memory.
Should put_task_struct(p) be moved after scx_error() to prevent this?
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2
Tejun Heo
reviewerPATCH 3/6 sched_ext: Replace SCX_TASK_OFF_TASKS flag with SCX_TASK_DEAD state
In reply to Tejun HeoSCX_TASK_OFF_TASKS marked tasks already through sched_ext_dead() so cgroup task iteration would skip them. This can be expressed better with a task state. Replace the flag with SCX_TASK_DEAD.
scx_disable_and_exit_task() resets state to NONE on its way out, so sched_ext_dead() now sets DEAD after the wrapper returns. The validation matrix grows NONE -> DEAD, warns on DEAD -> NONE, and tightens READY's predecessor to INIT or ENABLED so the new DEAD value cannot silently transition to READY.
Prepares for the following enable vs dead race fix.
--- include/linux/sched/ext.h | 9 +++++---- kernel/sched/ext.c | 17 +++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-)
include/linux/sched/ext.h +5 -4
@@ -101,24 +101,25 @@ enum scx_ent_flags {SCX_TASK_DEQD_FOR_SLEEP =1 <<3 ,/* last dequeue was for SLEEP */ SCX_TASK_SUB_INIT =1 <<4 ,/* task being initialized for a sub sched */ SCX_TASK_IMMED =1 <<5 ,/* task is on local DSQ with %SCX_ENQ_IMMED */ -SCX_TASK_OFF_TASKS =1 <<6 ,/* removed from scx_tasks by sched_ext_dead() */ /* -* Bits 8 and 9 are used to carry task state: +* Bits 8 to 10 are used to carry task state: * * NONE ops.init_task() not called yet * INIT ops.init_task() succeeded, but task can be cancelled * READY fully initialized, but not in sched_ext * ENABLED fully initialized and in sched_ext +* DEAD terminal state set by sched_ext_dead() */ -SCX_TASK_STATE_SHIFT =8 ,/* bits 8 and 9 are used to carry task state */ -SCX_TASK_STATE_BITS =2 , +SCX_TASK_STATE_SHIFT =8 , +SCX_TASK_STATE_BITS =3 ,SCX_TASK_STATE_MASK = ((1 <<SCX_TASK_STATE_BITS ) -1 ) <<SCX_TASK_STATE_SHIFT ,SCX_TASK_NONE =0 <<SCX_TASK_STATE_SHIFT ,SCX_TASK_INIT =1 <<SCX_TASK_STATE_SHIFT ,SCX_TASK_READY =2 <<SCX_TASK_STATE_SHIFT ,SCX_TASK_ENABLED =3 <<SCX_TASK_STATE_SHIFT , +SCX_TASK_DEAD =4 <<SCX_TASK_STATE_SHIFT ,/* * Bits 12 and 13 are used to carry reenqueue reason. In addition to
kernel/sched/ext.c +11 -6
@@ -723,17 +723,22 @@ static void scx_set_task_state(struct task_struct *p, u32 state)switch (state) {case SCX_TASK_NONE : + warn = prev_state ==SCX_TASK_DEAD ;break ;case SCX_TASK_INIT : warn = prev_state !=SCX_TASK_NONE ; p->scx.flags |=SCX_TASK_RESET_RUNNABLE_AT ;break ;case SCX_TASK_READY : - warn = prev_state ==SCX_TASK_NONE ; + warn = !(prev_state ==SCX_TASK_INIT || + prev_state ==SCX_TASK_ENABLED );break ;case SCX_TASK_ENABLED : warn = prev_state !=SCX_TASK_READY ;break ; +case SCX_TASK_DEAD : + warn = prev_state !=SCX_TASK_NONE ; +break ;default :WARN_ONCE (1 ,"sched_ext: Invalid task state %d -> %d for %s[%d]" , prev_state, state, p->comm, p->pid);
@@ -972,11 +977,11 @@ static struct task_struct *scx_task_iter_next_locked(struct scx_task_iter *iter)/* * cgroup_task_dead() removes the dead tasks from cset->tasks * after sched_ext_dead() and cgroup iteration may see tasks -* which already finished sched_ext_dead(). %SCX_TASK_OFF_TASKS -* is set by sched_ext_dead() under @p's rq lock. Test it to +* which already finished sched_ext_dead(). %SCX_TASK_DEAD is +* set by sched_ext_dead() under @p's rq lock. Test it to * avoid visiting tasks which are already dead from SCX POV. */ -if (p->scx.flags &SCX_TASK_OFF_TASKS ) { +if (scx_get_task_state (p) ==SCX_TASK_DEAD ) {__scx_task_iter_rq_unlock (iter);continue ; }
@@ -3847,7 +3852,7 @@ void sched_ext_dead(struct task_struct *p)* @p is off scx_tasks and wholly ours. scx_root_enable()'s READY -> * ENABLED transitions can't race us. Disable ops for @p. * -* %SCX_TASK_OFF_TASKS synchronizes against cgroup task iteration - see +* %SCX_TASK_DEAD synchronizes against cgroup task iteration - see * scx_task_iter_next_locked(). NONE tasks need no marking: cgroup * iteration is only used from sub-sched paths, which require root * enabled. Root enable transitions every live task to at least READY.
@@ -3858,7 +3863,7 @@ void sched_ext_dead(struct task_struct *p) rq =task_rq_lock (p, &rf);scx_disable_and_exit_task (scx_task_sched (p), p); - p->scx.flags |=SCX_TASK_OFF_TASKS ; +scx_set_task_state (p,SCX_TASK_DEAD );task_rq_unlock (rq, p, &rf); } }
Signature · 1 line
2.54.0
Tejun Heo
reviewerPATCH 4/6 sched_ext: Close root-enable vs sched_ext_dead() race with SCX_TASK_INIT_BEGIN
In reply to Tejun Heoscx_root_enable_workfn() drops the iter rq lock for ops.init_task() and a TASK_DEAD @p can fall through sched_ext_dead() in that window. The race hits when sched_ext_dead() observes SCX_TASK_INIT (the intermediate state before @p->scx.sched is published) and dereferences NULL via SCX_HAS_OP(NULL, exit_task), or observes SCX_TASK_NONE during the unlocked init window and skips cleanup so exit_task() never runs.
Add SCX_TASK_INIT_BEGIN. The enable path writes NONE -> INIT_BEGIN under the iter rq lock, then takes the rq lock again after init to walk INIT_BEGIN -> INIT -> READY. sched_ext_dead() that wins the rq-lock race observes INIT_BEGIN and sets DEAD without calling into ops; the post-init recheck unwinds via scx_sub_init_cancel_task().
scx_fork() runs single-threaded against sched_ext_dead() (the task is not on scx_tasks until scx_post_fork() adds it) so its INIT_BEGIN -> INIT walk needs no rq-lock pairing; it rolls back to NONE on ops.init_task() failure.
The validation matrix grows the INIT_BEGIN row and the INIT_BEGIN -> DEAD edge; INIT now requires INIT_BEGIN as the predecessor. scx_sub_disable()'s migration writes INIT_BEGIN as a synthetic predecessor to satisfy the tightened verification.
The sub-sched paths still race with sched_ext_dead() during the unlocked init window. This will be fixed by the next patch.
--- include/linux/sched/ext.h | 10 ++++--- kernel/sched/ext.c | 56 ++++++++++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 11 deletions(-)
include/linux/sched/ext.h +6 -4
@@ -106,6 +106,7 @@ enum scx_ent_flags {* Bits 8 to 10 are used to carry task state: * * NONE ops.init_task() not called yet +* INIT_BEGIN ops.init_task() in flight; see sched_ext_dead() * INIT ops.init_task() succeeded, but task can be cancelled * READY fully initialized, but not in sched_ext * ENABLED fully initialized and in sched_ext
@@ -116,10 +117,11 @@ enum scx_ent_flags {SCX_TASK_STATE_MASK = ((1 <<SCX_TASK_STATE_BITS ) -1 ) <<SCX_TASK_STATE_SHIFT ,SCX_TASK_NONE =0 <<SCX_TASK_STATE_SHIFT , -SCX_TASK_INIT =1 <<SCX_TASK_STATE_SHIFT , -SCX_TASK_READY =2 <<SCX_TASK_STATE_SHIFT , -SCX_TASK_ENABLED =3 <<SCX_TASK_STATE_SHIFT , -SCX_TASK_DEAD =4 <<SCX_TASK_STATE_SHIFT , +SCX_TASK_INIT_BEGIN =1 <<SCX_TASK_STATE_SHIFT , +SCX_TASK_INIT =2 <<SCX_TASK_STATE_SHIFT , +SCX_TASK_READY =3 <<SCX_TASK_STATE_SHIFT , +SCX_TASK_ENABLED =4 <<SCX_TASK_STATE_SHIFT , +SCX_TASK_DEAD =5 <<SCX_TASK_STATE_SHIFT ,/* * Bits 12 and 13 are used to carry reenqueue reason. In addition to
kernel/sched/ext.c +49 -7
118 lines not on this page · raw message
Signature · 1 line
2.54.0
Tejun Heo
reviewerPATCH 5/6 sched_ext: Close sub-sched init race with post-init DEAD recheck
In reply to Tejun Heoscx_sub_enable_workfn()'s init pass and scx_sub_disable() migration both drop the rq lock to call __scx_init_task() against the other sched. A TASK_DEAD @p can fall through sched_ext_dead() in that window. sched_ext_dead() runs ops.exit_task() on the sched @p was attached to, not on the sched whose init just completed, so the new allocation leaks.
Reuse the DEAD signal set by sched_ext_dead(). After __scx_init_task() returns, take task_rq_lock(p) and check for DEAD; on hit, call scx_sub_init_cancel_task() against the sub sched the init ran for and drop @p; on miss, proceed as before.
--- kernel/sched/ext.c | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-)
kernel/sched/ext.c +31 -1
@@ -5777,6 +5777,21 @@ static void scx_sub_disable(struct scx_sched *sch) } rq =task_rq_lock (p, &rf); + +if (scx_get_task_state (p) ==SCX_TASK_DEAD ) { +/* +* sched_ext_dead() raced us between __scx_init_task() +* and this rq lock and ran exit_task() on @sch (the +* sched @p was on at that point), not on $parent. +* $parent's just-completed init is owed an exit_task() +* and we issue it here. +*/ +scx_sub_init_cancel_task (parent, p); +task_rq_unlock (rq, p, &rf); +put_task_struct (p); +continue ; + } + scoped_guard (sched_change, p, DEQUEUE_SAVE | DEQUEUE_MOVE) {/* * $p is initialized for $parent and still attached to
@@ -5791,8 +5806,8 @@ static void scx_sub_disable(struct scx_sched *sch)scx_set_task_state (p,SCX_TASK_READY );scx_enable_task (parent, p); } -task_rq_unlock (rq, p, &rf); +task_rq_unlock (rq, p, &rf);put_task_struct (p); }scx_task_iter_stop (&sti);
@@ -7212,6 +7227,21 @@ static void scx_sub_enable_workfn(struct kthread_work *work) goto abort; rq =task_rq_lock (p, &rf); + +if (scx_get_task_state (p) ==SCX_TASK_DEAD ) { +/* +* sched_ext_dead() raced us between __scx_init_task() +* and this rq lock and ran exit_task() on $parent (the +* sched @p was on at that point), not on @sch. @sch's +* just-completed init is owed an exit_task() and we +* issue it here. +*/ +scx_sub_init_cancel_task (sch, p); +task_rq_unlock (rq, p, &rf); +put_task_struct (p); +continue ; + } + p->scx.flags |=SCX_TASK_SUB_INIT ;task_rq_unlock (rq, p, &rf);
Signature · 1 line
2.54.0
Tejun Heo
reviewerPATCH 6/6 sched_ext: Handle SCX_TASK_NONE in disable/switched_from paths
In reply to Tejun Heoscx_fail_parent() leaves cgroup tasks at (state=NONE, sched=parent, sched_class=ext) until the parent itself is torn down by the scx_error() it raised. When the later root_disable iterates them, two paths trip on NONE.
scx_disable_and_exit_task() re-enters the wrapper at NONE: the inner switch returns early but the trailing scx_set_task_sched(p, NULL) clobbers the parent sched left by scx_fail_parent(), and scx_set_task_state(p, NONE) wastes a write on an already-NONE task. switched_from_scx() then calls scx_disable_task(), which WARNs on non-ENABLED state and writes state=READY, producing a NONE -> READY transition the validation matrix rejects.
Treat NONE as "nothing to do" in both paths. Add a NONE early-return at the top of scx_disable_and_exit_task() and a parallel NONE check in switched_from_scx() next to task_dead_and_done().
--- kernel/sched/ext.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+)
kernel/sched/ext.c +19 -0
@@ -3703,6 +3703,15 @@ static void scx_sub_init_cancel_task(struct scx_sched *sch, struct task_struct *static void scx_disable_and_exit_task (struct scx_sched *sch,struct task_struct *p) { +/* +* %NONE means @p is already detached at the SCX level (e.g. handed +* back to the parent by scx_fail_parent() with no init to undo). +* Skip to avoid clobbering scx_task_sched() and writing %NONE again +* on a state that's already %NONE. +*/ +if (scx_get_task_state (p) ==SCX_TASK_NONE ) +return ; +__scx_disable_and_exit_task (sch, p);/*
@@ -3921,6 +3930,16 @@ static void switched_from_scx(struct rq *rq, struct task_struct *p)if (task_dead_and_done (p))return ; +/* +* %NONE means SCX is no longer tracking @p at the task level (e.g. +* scx_fail_parent() handed @p back to the parent at NONE pending the +* parent's own teardown). There is nothing to disable; calling +* scx_disable_task() would WARN on the non-%ENABLED state and trigger a +* NONE -> READY validation failure. +*/ +if (scx_get_task_state (p) ==SCX_TASK_NONE ) +return ; +scx_disable_task (scx_task_sched (p), p); }
Signature · 1 line
2.54.0
[email protected]
AI reviewRe: [PATCH 6/6] sched_ext: Handle SCX_TASK_NONE in disable/switched_from paths
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Critical] The newly added early return in `scx_disable_and_exit_task()` skips clearing the…
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] The newly added early return in scx_disable_and_exit_task() skips clearing the p->scx.sched pointer for tasks in the SCX_TASK_NONE state, creating a deterministic Use-After-Free (UAF) vulnerability. --
commit edcdc86f04b3e413339b79eef811a59560178144
Author: Tejun Heo <[email protected]>
sched_ext: Handle SCX_TASK_NONE in disable/switched_from paths
This commit adds early returns for tasks in the SCX_TASK_NONE state during scx_disable_and_exit_task() and switched_from_scx(). It prevents writing state=READY on already detached tasks and clobbering the parent scheduler left by scx_fail_parent().
Quoted 7 lines
kernel/sched/ext.c +0
-0@@ -3704,6 +3704,15 @@ static void scx_sub_init_cancel_task(struct scx_sched *sch, struct task_struct *static void scx_disable_and_exit_task (struct scx_sched *sch,struct task_struct *p)
kernel/sched/ext.c static void scx_sub_init_cancel_task(struct scx_sched *sch, struct task_struct * · line 3715
Quoted 10 lines
kernel/sched/ext.c +9
-0{ +/* +* %NONE means @p is already detached at the SCX level (e.g. handed +* back to the parent by scx_fail_parent() with no init to undo). +* Skip to avoid clobbering scx_task_sched() and writing %NONE again +* on a state that's already %NONE. +*/ +if (scx_get_task_state (p) ==SCX_TASK_NONE ) +return ; +
Does this early return leave a dangling pointer in p->scx.sched?
When scx_fail_parent() hands a task back to the parent scheduler, it transitions the task to SCX_TASK_NONE and sets p->scx.sched = parent.
If the parent scheduler is subsequently torn down, it iterates over all tasks to detach them. Because the state is SCX_TASK_NONE, this early return bypasses the trailing scx_set_task_sched(p, NULL) cleanup step at the end of this function.
The task is migrated back to CFS but retains a dangling p->scx.sched pointer to the freed parent scheduler. If /proc/self/sched is read afterward, print_scx_info() retrieves this pointer via scx_task_sched_rcu(p) and dereferences it to print sch->ops.name, causing a use-after-free read.
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=6
Hi Tejun,
Quoted 23 lines · Tejun Heo
Hello,
zhidao su reported a NULL deref and an
ops.init_task()leak whensched_ext_dead()racesscx_root_enable_workfn()inCONFIG_EXT_SUB_SCHEDkernels [1]. The same race window also affects the analogous sub-sched paths (scx_sub_enable_workfn()'s per-task init pass and scx_sub_disable()'s migration loop), and the wrapper-disable paths trip on the NONE state thatscx_fail_parent()leaves behind. Closing all of these calls for a state-machine extension rather than a localized fix.The series introduces
SCX_TASK_INIT_BEGINas an explicit intermediate state between NONE and INIT, and replaces theSCX_TASK_OFF_TASKSmarker flag with a realSCX_TASK_DEADterminal state. With the state machine in place, every init path uses the same handshake: writeINIT_BEGINunder rq lock, init outside the lock, recheck DEAD under rq lock, unwind viascx_sub_init_cancel_task()on hit. The wrapper-disable andswitched_from_scx()paths get NONE early-returns to handle thescx_fail_parent()residue.It is more invasive than zhidao's patches but covers the related races uniformly and avoids the implicit
list_empty()check his approach relies on. Credit to him for finding and reporting the bug.
Quoted 6 lines
0001
sched_ext: Cleanups in preparation for the SCX_TASK_INIT_BEGIN/DEAD work 0002sched_ext: Inlinescx_init_task()and moveRESET_RUNNABLE_ATintoscx_set_task_state()0003sched_ext: ReplaceSCX_TASK_OFF_TASKSflag withSCX_TASK_DEADstate 0004sched_ext: Close root-enable vssched_ext_dead()race withSCX_TASK_INIT_BEGIN0005sched_ext: Close sub-sched init race with post-init DEAD recheck
0006sched_ext: HandleSCX_TASK_NONEin disable/switched_from paths
Apart than a small comment about PATCH 2/6, I haven't found any issues with this series. Looks good to me.
Thanks,
-Andrea
Quoted 22 lines
Based on sched_ext/for-7.1-fixes (
ab28a0673daa).Git tree:
git://git.kernel.org/pub/scm/linux/kernel/git/tj/sched_ext.gitfor-7.1-fixes-dead-raceVerified with a debug patch that widens the unlocked init windows on the root and sub-sched paths and counts post-init DEAD-recheck hits. Reproducers exercise each of the original races plus the
scx_fail_parentNONE-state regression, followed by a multi-iteration stress under fork churn. Counters show the windows are hit and no BUG/WARNING/Oops/Invalid-task-state appears.[1] https://lore.kernel.org/all/[email protected]/
include/linux/sched/ext.h | 17 ++-- kernel/sched/ext.c | 221 +++++++++++++++++++++++++++++++--------------- 2 files changed, 162 insertions(+), 76 deletions(-)Thanks.
--
tejun
Tejun Heo
reviewerHello,
Applied 1-6 to sched_ext/for-7.1-fixes. The merge into for-7.2 has a lock-ordering complication around the post-init block; addressing with a prep patch on for-7.2 soon.
Thanks.
--
tejun