* Re: [PATCH] sched_ext: Fix NULL sched deref in select_cpu_and sub-sched error path
2026-09-02 15:36 [PATCH] sched_ext: Fix NULL sched deref in select_cpu_and sub-sched error path Wanwu Li
@ 2026-09-02 16:14 ` Andrea Righi
2026-09-02 16:34 ` liwanwu
2026-09-02 17:07 ` [PATCH v2] sched_ext: Fix NULL sched deref in kfunc sub-sched error paths Wanwu Li
2026-09-03 6:06 ` [PATCH v3] " Wanwu Li
2 siblings, 1 reply; 9+ messages in thread
From: Andrea Righi @ 2026-09-02 16:14 UTC (permalink / raw)
To: Wanwu Li
Cc: Tejun Heo, David Vernet, Changwoo Min, sched-ext, linux-kernel, stable
Hi Wanwu,
On Wed, Sep 02, 2026 at 11:36:40PM +0800, Wanwu Li wrote:
> scx_bpf_select_cpu_and() errors out @p's scheduler when the root scheduler
> has sub-scheds attached:
>
> scx_error(scx_task_sched(p), "... must be used");
>
> scx_task_sched(p) is p->scx.sched, which is NULL for any task that is not
> on an scx scheduler: it is memset() by init_scx_entity() and cleared by
> scx_disable_and_exit_task() -- which sched_ext_dead() runs when a task
> exits. It is also an rcu_dereference_protected() that must be called with
> @p's pi_lock or rq lock held -- neither of which a BPF_PROG_TYPE_SYSCALL
> program holds.
>
> The wrapper is reachable from such a program -- scx_kfunc_context_filter()
> allows the select_cpu kfunc group for BPF_PROG_TYPE_SYSCALL -- and the
> program can pass any task, e.g. one obtained with bpf_task_from_pid() that
> exited in between. scx_error() then calls scx_vexit(), which dereferences
> sch->exit_info unconditionally, so passing NULL oopses the kernel.
>
> This was triggered live on a v7.2 based kernel with a
> BPF_PROG_TYPE_SYSCALL program calling the wrapper on an exited-but-not
> reaped task while a sub-scheduler was attached (faulting instruction is
> the scx_vexit() prologue "mov r15,[rdi+0x398]" with RDI=NULL and 0x398
> the offset of sch->exit_info):
>
> sched_ext: BPF scheduler "kfunc_subsched_null" enabled
> sched_ext: BPF sub-scheduler "kfunc_subsched_null" enabled
> sched_ext: Unassociated program run_select_cpu_ (id 76)
> BUG: kernel NULL pointer dereference, address: 0000000000000398
> #PF: supervisor read access in kernel mode
> #PF: error_code(0x0000) - not-present page
> Oops: Oops: 0000 [#1] SMP NOPTI
> CPU: 7 UID: 0 PID: 8201 Comm: kfunc_test_runn Tainted: G W
> RIP: 0010:scx_vexit+0x25/0xa0
> Code: ... <4c> 8b bf 98 03 00 00 ...
> CR2: 0000000000000398
> Call Trace:
> <TASK>
> __scx_exit+0x4f/0x70
> scx_bpf_select_cpu_and+0xab/0xb0
> bpf_prog_430ed61a7b66e03a_run_select_cpu_and+0x9c/0xe7
> ? __x64_sys_bpf+0x2c/0x40
> bpf_prog_test_run_syscall+0x130/0x2f0
> __sys_bpf+0x930/0x10d0
> ? __x64_sys_bpf+0x2c/0x40
> __x64_sys_bpf+0x2c/0x40
> do_syscall_64+0xbc/0x460
> ? rseq_set_ids_get_csaddr+0x81/0x140
> ? __rseq_handle_slowpath+0xd0/0x130
> ? switch_fpu_return+0x51/0xd0
> ? arch_exit_to_user_mode_prepare.constprop.0+0x87/0xb0
> ? do_syscall_64+0xf3/0x460
> ? irqentry_exit+0x48/0x740
> ? clear_bhb_loop+0x40/0x90
> ? do_syscall_64+0x35/0x460
> entry_SYSCALL_64_after_hwframe+0x76/0x7e
> </TASK>
>
> Keep the "error out @p's scheduler" attribution -- it is what every other
> kfunc error path does (select_cpu_from_kfunc()'s cross_task,
> scx_kf_arg_task_ok()) and it is correct for the callers that actually take
> this path: the only other callers are the struct_ops select_cpu/enqueue
> ops, where @p is the caller's own task and p->scx.sched is its scheduler.
> Just read it safely: use scx_task_sched_rcu() (valid under the guard(rcu)()
> the wrapper already holds, no @p lock required) and fall back to @sch -- the
> root scheduler, guaranteed non-NULL here -- when @p is not on an scx
> scheduler, which is precisely the case that used to be NULL.
>
> scx_bpf_dsq_insert_vtime() has the same error path but it is not reachable
> with a NULL @p: SYSCALL programs are rejected for its kfunc set and @p is
> always the calling scheduler's own task in the contexts where it runs, so
> it is left unchanged.
The reported crash looks valid to me, and using scx_task_sched_rcu() with the
root scheduler as fallback also looks correct.
However, as also pointed out by sashiko, the assumption above doesn't hold for
scx_bpf_dsq_insert_vtime(), although SYSCALL programs can't call it, STRUCT_OPS
programs can call it from ops.enqueue() and ops.dispatch().
Can you update scx_bpf_dsq_insert_vtime() as well with the same fallback?
Thanks,
-Andrea
>
> The proper endgame for these COMPAT wrappers is removal once the
> deprecation grace period is announced and elapsed; this fix only keeps
> the window from oopsing the kernel until that happens.
>
> Cc: <stable@vger.kernel.org>
> Fixes: a5fa0708cbfd ("sched_ext: Enforce scheduling authority in dispatch and select_cpu operations")
> Signed-off-by: Wanwu Li <liwanwu@kylinos.cn>
> ---
> kernel/sched/ext/idle.c | 9 +++++++--
> 1 file changed, 7 insertions(+), 2 deletions(-)
>
> diff --git a/kernel/sched/ext/idle.c b/kernel/sched/ext/idle.c
> index d2973fb3af6d..014599d82bb0 100644
> --- a/kernel/sched/ext/idle.c
> +++ b/kernel/sched/ext/idle.c
> @@ -1142,10 +1142,15 @@ __bpf_kfunc s32 scx_bpf_select_cpu_and(struct task_struct *p, s32 prev_cpu, u64
> #ifdef CONFIG_EXT_SUB_SCHED
> /*
> * Disallow if any sub-scheds are attached. There is no way to tell
> - * which scheduler called us, just error out @p's scheduler.
> + * which scheduler called us, so error out @p's scheduler -- but read
> + * it under RCU (@p's locks aren't held here) and fall back to @sch if
> + * @p isn't on an scx scheduler: a BPF_PROG_TYPE_SYSCALL prog can pass
> + * any task and p->scx.sched is NULL for one that has exited or is
> + * managed by another scheduler.
> */
> if (unlikely(!list_empty(&sch->children))) {
> - scx_error(scx_task_sched(p), "__scx_bpf_select_cpu_and() must be used");
> + scx_error(scx_task_sched_rcu(p) ?: sch,
> + "__scx_bpf_select_cpu_and() must be used");
> return -EINVAL;
> }
> #endif
> --
> 2.25.1
>
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH] sched_ext: Fix NULL sched deref in select_cpu_and sub-sched error path
2026-09-02 16:14 ` Andrea Righi
@ 2026-09-02 16:34 ` liwanwu
0 siblings, 0 replies; 9+ messages in thread
From: liwanwu @ 2026-09-02 16:34 UTC (permalink / raw)
To: Andrea Righi
Cc: Tejun Heo, David Vernet, Changwoo Min, sched-ext, linux-kernel, stable
Hi Andrea,
Thanks for picking this up so quickly, and for confirming the crash and
the fix direction.
This one is my miss: I reasoned about scx_bpf_dsq_insert_vtime() from
the SYSCALL rejection alone and overlooked that the same kfunc group is
reachable from struct_ops enqueue/dispatch with any KF_RCU task pointer
-- exactly as sashiko-bot pointed out (thanks to it as well). That is a
genuine reachability path and it deserves the same fallback.
v2 is coming shortly, updating scx_bpf_dsq_insert_vtime() as you asked.
Thanks,
Wanwu
在 2026/9/3 00:14, Andrea Righi 写道:
> Hi Wanwu,
>
> On Wed, Sep 02, 2026 at 11:36:40PM +0800, Wanwu Li wrote:
>> scx_bpf_select_cpu_and() errors out @p's scheduler when the root scheduler
>> has sub-scheds attached:
>>
>> scx_error(scx_task_sched(p), "... must be used");
>>
>> scx_task_sched(p) is p->scx.sched, which is NULL for any task that is not
>> on an scx scheduler: it is memset() by init_scx_entity() and cleared by
>> scx_disable_and_exit_task() -- which sched_ext_dead() runs when a task
>> exits. It is also an rcu_dereference_protected() that must be called with
>> @p's pi_lock or rq lock held -- neither of which a BPF_PROG_TYPE_SYSCALL
>> program holds.
>>
>> The wrapper is reachable from such a program -- scx_kfunc_context_filter()
>> allows the select_cpu kfunc group for BPF_PROG_TYPE_SYSCALL -- and the
>> program can pass any task, e.g. one obtained with bpf_task_from_pid() that
>> exited in between. scx_error() then calls scx_vexit(), which dereferences
>> sch->exit_info unconditionally, so passing NULL oopses the kernel.
>>
>> This was triggered live on a v7.2 based kernel with a
>> BPF_PROG_TYPE_SYSCALL program calling the wrapper on an exited-but-not
>> reaped task while a sub-scheduler was attached (faulting instruction is
>> the scx_vexit() prologue "mov r15,[rdi+0x398]" with RDI=NULL and 0x398
>> the offset of sch->exit_info):
>>
>> sched_ext: BPF scheduler "kfunc_subsched_null" enabled
>> sched_ext: BPF sub-scheduler "kfunc_subsched_null" enabled
>> sched_ext: Unassociated program run_select_cpu_ (id 76)
>> BUG: kernel NULL pointer dereference, address: 0000000000000398
>> #PF: supervisor read access in kernel mode
>> #PF: error_code(0x0000) - not-present page
>> Oops: Oops: 0000 [#1] SMP NOPTI
>> CPU: 7 UID: 0 PID: 8201 Comm: kfunc_test_runn Tainted: G W
>> RIP: 0010:scx_vexit+0x25/0xa0
>> Code: ... <4c> 8b bf 98 03 00 00 ...
>> CR2: 0000000000000398
>> Call Trace:
>> <TASK>
>> __scx_exit+0x4f/0x70
>> scx_bpf_select_cpu_and+0xab/0xb0
>> bpf_prog_430ed61a7b66e03a_run_select_cpu_and+0x9c/0xe7
>> ? __x64_sys_bpf+0x2c/0x40
>> bpf_prog_test_run_syscall+0x130/0x2f0
>> __sys_bpf+0x930/0x10d0
>> ? __x64_sys_bpf+0x2c/0x40
>> __x64_sys_bpf+0x2c/0x40
>> do_syscall_64+0xbc/0x460
>> ? rseq_set_ids_get_csaddr+0x81/0x140
>> ? __rseq_handle_slowpath+0xd0/0x130
>> ? switch_fpu_return+0x51/0xd0
>> ? arch_exit_to_user_mode_prepare.constprop.0+0x87/0xb0
>> ? do_syscall_64+0xf3/0x460
>> ? irqentry_exit+0x48/0x740
>> ? clear_bhb_loop+0x40/0x90
>> ? do_syscall_64+0x35/0x460
>> entry_SYSCALL_64_after_hwframe+0x76/0x7e
>> </TASK>
>>
>> Keep the "error out @p's scheduler" attribution -- it is what every other
>> kfunc error path does (select_cpu_from_kfunc()'s cross_task,
>> scx_kf_arg_task_ok()) and it is correct for the callers that actually take
>> this path: the only other callers are the struct_ops select_cpu/enqueue
>> ops, where @p is the caller's own task and p->scx.sched is its scheduler.
>> Just read it safely: use scx_task_sched_rcu() (valid under the guard(rcu)()
>> the wrapper already holds, no @p lock required) and fall back to @sch -- the
>> root scheduler, guaranteed non-NULL here -- when @p is not on an scx
>> scheduler, which is precisely the case that used to be NULL.
>>
>> scx_bpf_dsq_insert_vtime() has the same error path but it is not reachable
>> with a NULL @p: SYSCALL programs are rejected for its kfunc set and @p is
>> always the calling scheduler's own task in the contexts where it runs, so
>> it is left unchanged.
>
> The reported crash looks valid to me, and using scx_task_sched_rcu() with the
> root scheduler as fallback also looks correct.
>
> However, as also pointed out by sashiko, the assumption above doesn't hold for
> scx_bpf_dsq_insert_vtime(), although SYSCALL programs can't call it, STRUCT_OPS
> programs can call it from ops.enqueue() and ops.dispatch().
>
> Can you update scx_bpf_dsq_insert_vtime() as well with the same fallback?
>
> Thanks,
> -Andrea
>
>>
>> The proper endgame for these COMPAT wrappers is removal once the
>> deprecation grace period is announced and elapsed; this fix only keeps
>> the window from oopsing the kernel until that happens.
>>
>> Cc: <stable@vger.kernel.org>
>> Fixes: a5fa0708cbfd ("sched_ext: Enforce scheduling authority in dispatch and select_cpu operations")
>> Signed-off-by: Wanwu Li <liwanwu@kylinos.cn>
>> ---
>> kernel/sched/ext/idle.c | 9 +++++++--
>> 1 file changed, 7 insertions(+), 2 deletions(-)
>>
>> diff --git a/kernel/sched/ext/idle.c b/kernel/sched/ext/idle.c
>> index d2973fb3af6d..014599d82bb0 100644
>> --- a/kernel/sched/ext/idle.c
>> +++ b/kernel/sched/ext/idle.c
>> @@ -1142,10 +1142,15 @@ __bpf_kfunc s32 scx_bpf_select_cpu_and(struct task_struct *p, s32 prev_cpu, u64
>> #ifdef CONFIG_EXT_SUB_SCHED
>> /*
>> * Disallow if any sub-scheds are attached. There is no way to tell
>> - * which scheduler called us, just error out @p's scheduler.
>> + * which scheduler called us, so error out @p's scheduler -- but read
>> + * it under RCU (@p's locks aren't held here) and fall back to @sch if
>> + * @p isn't on an scx scheduler: a BPF_PROG_TYPE_SYSCALL prog can pass
>> + * any task and p->scx.sched is NULL for one that has exited or is
>> + * managed by another scheduler.
>> */
>> if (unlikely(!list_empty(&sch->children))) {
>> - scx_error(scx_task_sched(p), "__scx_bpf_select_cpu_and() must be used");
>> + scx_error(scx_task_sched_rcu(p) ?: sch,
>> + "__scx_bpf_select_cpu_and() must be used");
>> return -EINVAL;
>> }
>> #endif
>> --
>> 2.25.1
>>
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2] sched_ext: Fix NULL sched deref in kfunc sub-sched error paths
2026-09-02 15:36 [PATCH] sched_ext: Fix NULL sched deref in select_cpu_and sub-sched error path Wanwu Li
2026-09-02 16:14 ` Andrea Righi
@ 2026-09-02 17:07 ` Wanwu Li
2026-09-02 18:11 ` Andrea Righi
2026-09-02 19:37 ` Tejun Heo
2026-09-03 6:06 ` [PATCH v3] " Wanwu Li
2 siblings, 2 replies; 9+ messages in thread
From: Wanwu Li @ 2026-09-02 17:07 UTC (permalink / raw)
To: Tejun Heo, David Vernet, Andrea Righi, Changwoo Min
Cc: liwanwu, sched-ext, linux-kernel, stable
The COMPAT kfunc wrappers scx_bpf_select_cpu_and() and
scx_bpf_dsq_insert_vtime() error out @p's scheduler when the root
scheduler has sub-scheds attached:
scx_error(scx_task_sched(p), "... must be used");
scx_task_sched(p) is p->scx.sched, which is NULL for any task that is not
on an scx scheduler: it is memset() by init_scx_entity() and cleared by
scx_disable_and_exit_task() -- which sched_ext_dead() runs when a task
exits. It is also an rcu_dereference_protected() that must be called with
@p's pi_lock or rq lock held, which neither wrapper does. Passing NULL to
scx_error() reaches scx_vexit(), which dereferences sch->exit_info
unconditionally, oopsing the kernel.
Both wrappers are reachable with such a @p. scx_bpf_select_cpu_and() is in
the select_cpu kfunc group, which scx_kfunc_context_filter() opens to
BPF_PROG_TYPE_SYSCALL programs. scx_bpf_dsq_insert_vtime() is in the
enqueue_dispatch group, which ops.enqueue() and ops.dispatch() may call
with any KF_RCU task: the group has no kf_tasks validation, and
scx_dsq_insert_preamble() checks task ownership with scx_task_on_sched()
precisely because @p may be an arbitrary task.
Neither wrapper requires a contrived @p. Tasks that are never enabled --
kthreads and tasks of other classes under SCX_SWITCH_ALL=n -- keep
p->scx.sched NULL indefinitely; and a task handed over from
bpf_task_from_pid() can exit before the call lands, as its zombie stays
visible to pid lookups until it is reaped.
One concrete trigger exercised for this changelog: a SYSCALL program
calling the select_cpu_and wrapper on an exited-but-not-reaped task while
a sub-scheduler is attached (faulting instruction is the scx_vexit()
prologue "mov r15,[rdi+0x398]" with RDI=NULL and 0x398 the offset of
sch->exit_info):
sched_ext: BPF scheduler "kfunc_subsched_null" enabled
sched_ext: BPF sub-scheduler "kfunc_subsched_null" enabled
sched_ext: Unassociated program run_select_cpu_ (id 76)
BUG: kernel NULL pointer dereference, address: 0000000000000398
#PF: supervisor read access in kernel mode
#PF: error_code(0x0000) - not-present page
Oops: Oops: 0000 [#1] SMP NOPTI
CPU: 7 UID: 0 PID: 8201 Comm: kfunc_test_runn Tainted: G W
RIP: 0010:scx_vexit+0x25/0xa0
Code: ... <4c> 8b bf 98 03 00 00 ...
CR2: 0000000000000398
Call Trace:
<TASK>
__scx_exit+0x4f/0x70
scx_bpf_select_cpu_and+0xab/0xb0
bpf_prog_430ed61a7b66e03a_run_select_cpu_and+0x9c/0xe7
? __x64_sys_bpf+0x2c/0x40
bpf_prog_test_run_syscall+0x130/0x2f0
__sys_bpf+0x930/0x10d0
? __x64_sys_bpf+0x2c/0x40
__x64_sys_bpf+0x2c/0x40
do_syscall_64+0xbc/0x460
? rseq_set_ids_get_csaddr+0x81/0x140
? __rseq_handle_slowpath+0xd0/0x130
? switch_fpu_return+0x51/0xd0
? arch_exit_to_user_mode_prepare.constprop.0+0x87/0xb0
? do_syscall_64+0xf3/0x460
? irqentry_exit+0x48/0x740
? clear_bhb_loop+0x40/0x90
? do_syscall_64+0x35/0x460
entry_SYSCALL_64_after_hwframe+0x76/0x7e
</TASK>
Keep the "error out @p's scheduler" attribution -- it is what every other
kfunc error path does (select_cpu_from_kfunc()'s cross_task,
scx_kf_arg_task_ok()) and it is correct for the callers that take this
path with a live task: p->scx.sched is their scheduler. Just read it
safely: use scx_task_sched_rcu() (valid under the guard(rcu)() both
wrappers already hold, no @p lock required) and fall back to @sch -- the
root scheduler, guaranteed non-NULL here -- when @p is not on an scx
scheduler, which is precisely the case that used to be NULL.
These COMPAT wrappers are scheduled for eventual removal once the
deprecation grace period elapses, but until then -- and regardless of
their removal timeline -- they must not oops the kernel on a task they
are handed; this fix makes the error path safe.
Cc: <stable@vger.kernel.org>
Fixes: a5fa0708cbfd ("sched_ext: Enforce scheduling authority in dispatch and select_cpu operations")
Signed-off-by: Wanwu Li <liwanwu@kylinos.cn>
---
Changes v1 -> v2:
- Extend the same fallback to scx_bpf_dsq_insert_vtime() as suggested
by Andrea (and flagged by sashiko-bot); rewrite the reachability
argument in the commit message accordingly.
kernel/sched/ext/ext.c | 9 +++++++--
kernel/sched/ext/idle.c | 9 +++++++--
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
index 10af28a9f2c0..fdfaa7e9c8f5 100644
--- a/kernel/sched/ext/ext.c
+++ b/kernel/sched/ext/ext.c
@@ -8943,10 +8943,15 @@ __bpf_kfunc void scx_bpf_dsq_insert_vtime(struct task_struct *p, u64 dsq_id,
#ifdef CONFIG_EXT_SUB_SCHED
/*
* Disallow if any sub-scheds are attached. There is no way to tell
- * which scheduler called us, just error out @p's scheduler.
+ * which scheduler called us, so error out @p's scheduler -- but read
+ * it under RCU (@p's locks aren't necessarily held here) and fall
+ * back to @sch if @p isn't on an scx scheduler: enqueue/dispatch
+ * contexts may pass any KF_RCU task, and p->scx.sched is NULL for
+ * one that has exited or is managed by another scheduler.
*/
if (unlikely(!list_empty(&sch->children))) {
- scx_error(scx_task_sched(p), "__scx_bpf_dsq_insert_vtime() must be used");
+ scx_error(scx_task_sched_rcu(p) ?: sch,
+ "__scx_bpf_dsq_insert_vtime() must be used");
return;
}
#endif
diff --git a/kernel/sched/ext/idle.c b/kernel/sched/ext/idle.c
index d2973fb3af6d..014599d82bb0 100644
--- a/kernel/sched/ext/idle.c
+++ b/kernel/sched/ext/idle.c
@@ -1142,10 +1142,15 @@ __bpf_kfunc s32 scx_bpf_select_cpu_and(struct task_struct *p, s32 prev_cpu, u64
#ifdef CONFIG_EXT_SUB_SCHED
/*
* Disallow if any sub-scheds are attached. There is no way to tell
- * which scheduler called us, just error out @p's scheduler.
+ * which scheduler called us, so error out @p's scheduler -- but read
+ * it under RCU (@p's locks aren't held here) and fall back to @sch if
+ * @p isn't on an scx scheduler: a BPF_PROG_TYPE_SYSCALL prog can pass
+ * any task and p->scx.sched is NULL for one that has exited or is
+ * managed by another scheduler.
*/
if (unlikely(!list_empty(&sch->children))) {
- scx_error(scx_task_sched(p), "__scx_bpf_select_cpu_and() must be used");
+ scx_error(scx_task_sched_rcu(p) ?: sch,
+ "__scx_bpf_select_cpu_and() must be used");
return -EINVAL;
}
#endif
--
2.25.1
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH v2] sched_ext: Fix NULL sched deref in kfunc sub-sched error paths
2026-09-02 17:07 ` [PATCH v2] sched_ext: Fix NULL sched deref in kfunc sub-sched error paths Wanwu Li
@ 2026-09-02 18:11 ` Andrea Righi
2026-09-02 19:37 ` Tejun Heo
1 sibling, 0 replies; 9+ messages in thread
From: Andrea Righi @ 2026-09-02 18:11 UTC (permalink / raw)
To: Wanwu Li
Cc: Tejun Heo, David Vernet, Changwoo Min, sched-ext, linux-kernel, stable
Hi Wanwu,
On Thu, Sep 03, 2026 at 01:07:51AM +0800, Wanwu Li wrote:
> The COMPAT kfunc wrappers scx_bpf_select_cpu_and() and
> scx_bpf_dsq_insert_vtime() error out @p's scheduler when the root
> scheduler has sub-scheds attached:
>
> scx_error(scx_task_sched(p), "... must be used");
>
> scx_task_sched(p) is p->scx.sched, which is NULL for any task that is not
> on an scx scheduler: it is memset() by init_scx_entity() and cleared by
> scx_disable_and_exit_task() -- which sched_ext_dead() runs when a task
> exits. It is also an rcu_dereference_protected() that must be called with
> @p's pi_lock or rq lock held, which neither wrapper does. Passing NULL to
> scx_error() reaches scx_vexit(), which dereferences sch->exit_info
> unconditionally, oopsing the kernel.
>
> Both wrappers are reachable with such a @p. scx_bpf_select_cpu_and() is in
> the select_cpu kfunc group, which scx_kfunc_context_filter() opens to
> BPF_PROG_TYPE_SYSCALL programs. scx_bpf_dsq_insert_vtime() is in the
> enqueue_dispatch group, which ops.enqueue() and ops.dispatch() may call
> with any KF_RCU task: the group has no kf_tasks validation, and
> scx_dsq_insert_preamble() checks task ownership with scx_task_on_sched()
> precisely because @p may be an arbitrary task.
>
> Neither wrapper requires a contrived @p. Tasks that are never enabled --
> kthreads and tasks of other classes under SCX_SWITCH_ALL=n -- keep
> p->scx.sched NULL indefinitely; and a task handed over from
> bpf_task_from_pid() can exit before the call lands, as its zombie stays
> visible to pid lookups until it is reaped.
>
> One concrete trigger exercised for this changelog: a SYSCALL program
> calling the select_cpu_and wrapper on an exited-but-not-reaped task while
> a sub-scheduler is attached (faulting instruction is the scx_vexit()
> prologue "mov r15,[rdi+0x398]" with RDI=NULL and 0x398 the offset of
> sch->exit_info):
>
> sched_ext: BPF scheduler "kfunc_subsched_null" enabled
> sched_ext: BPF sub-scheduler "kfunc_subsched_null" enabled
> sched_ext: Unassociated program run_select_cpu_ (id 76)
> BUG: kernel NULL pointer dereference, address: 0000000000000398
> #PF: supervisor read access in kernel mode
> #PF: error_code(0x0000) - not-present page
> Oops: Oops: 0000 [#1] SMP NOPTI
> CPU: 7 UID: 0 PID: 8201 Comm: kfunc_test_runn Tainted: G W
> RIP: 0010:scx_vexit+0x25/0xa0
> Code: ... <4c> 8b bf 98 03 00 00 ...
> CR2: 0000000000000398
> Call Trace:
> <TASK>
> __scx_exit+0x4f/0x70
> scx_bpf_select_cpu_and+0xab/0xb0
> bpf_prog_430ed61a7b66e03a_run_select_cpu_and+0x9c/0xe7
> ? __x64_sys_bpf+0x2c/0x40
> bpf_prog_test_run_syscall+0x130/0x2f0
> __sys_bpf+0x930/0x10d0
> ? __x64_sys_bpf+0x2c/0x40
> __x64_sys_bpf+0x2c/0x40
> do_syscall_64+0xbc/0x460
> ? rseq_set_ids_get_csaddr+0x81/0x140
> ? __rseq_handle_slowpath+0xd0/0x130
> ? switch_fpu_return+0x51/0xd0
> ? arch_exit_to_user_mode_prepare.constprop.0+0x87/0xb0
> ? do_syscall_64+0xf3/0x460
> ? irqentry_exit+0x48/0x740
> ? clear_bhb_loop+0x40/0x90
> ? do_syscall_64+0x35/0x460
> entry_SYSCALL_64_after_hwframe+0x76/0x7e
> </TASK>
>
> Keep the "error out @p's scheduler" attribution -- it is what every other
> kfunc error path does (select_cpu_from_kfunc()'s cross_task,
> scx_kf_arg_task_ok()) and it is correct for the callers that take this
> path with a live task: p->scx.sched is their scheduler. Just read it
> safely: use scx_task_sched_rcu() (valid under the guard(rcu)() both
> wrappers already hold, no @p lock required) and fall back to @sch -- the
> root scheduler, guaranteed non-NULL here -- when @p is not on an scx
> scheduler, which is precisely the case that used to be NULL.
>
> These COMPAT wrappers are scheduled for eventual removal once the
> deprecation grace period elapses, but until then -- and regardless of
> their removal timeline -- they must not oops the kernel on a task they
> are handed; this fix makes the error path safe.
>
> Cc: <stable@vger.kernel.org>
> Fixes: a5fa0708cbfd ("sched_ext: Enforce scheduling authority in dispatch and select_cpu operations")
> Signed-off-by: Wanwu Li <liwanwu@kylinos.cn>
This looks good to me.
Reviewed-by: Andrea Righi <arighi@nvidia.com>
Thanks,
-Andrea
> ---
>
> Changes v1 -> v2:
> - Extend the same fallback to scx_bpf_dsq_insert_vtime() as suggested
> by Andrea (and flagged by sashiko-bot); rewrite the reachability
> argument in the commit message accordingly.
>
> kernel/sched/ext/ext.c | 9 +++++++--
> kernel/sched/ext/idle.c | 9 +++++++--
> 2 files changed, 14 insertions(+), 4 deletions(-)
>
> diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
> index 10af28a9f2c0..fdfaa7e9c8f5 100644
> --- a/kernel/sched/ext/ext.c
> +++ b/kernel/sched/ext/ext.c
> @@ -8943,10 +8943,15 @@ __bpf_kfunc void scx_bpf_dsq_insert_vtime(struct task_struct *p, u64 dsq_id,
> #ifdef CONFIG_EXT_SUB_SCHED
> /*
> * Disallow if any sub-scheds are attached. There is no way to tell
> - * which scheduler called us, just error out @p's scheduler.
> + * which scheduler called us, so error out @p's scheduler -- but read
> + * it under RCU (@p's locks aren't necessarily held here) and fall
> + * back to @sch if @p isn't on an scx scheduler: enqueue/dispatch
> + * contexts may pass any KF_RCU task, and p->scx.sched is NULL for
> + * one that has exited or is managed by another scheduler.
> */
> if (unlikely(!list_empty(&sch->children))) {
> - scx_error(scx_task_sched(p), "__scx_bpf_dsq_insert_vtime() must be used");
> + scx_error(scx_task_sched_rcu(p) ?: sch,
> + "__scx_bpf_dsq_insert_vtime() must be used");
> return;
> }
> #endif
> diff --git a/kernel/sched/ext/idle.c b/kernel/sched/ext/idle.c
> index d2973fb3af6d..014599d82bb0 100644
> --- a/kernel/sched/ext/idle.c
> +++ b/kernel/sched/ext/idle.c
> @@ -1142,10 +1142,15 @@ __bpf_kfunc s32 scx_bpf_select_cpu_and(struct task_struct *p, s32 prev_cpu, u64
> #ifdef CONFIG_EXT_SUB_SCHED
> /*
> * Disallow if any sub-scheds are attached. There is no way to tell
> - * which scheduler called us, just error out @p's scheduler.
> + * which scheduler called us, so error out @p's scheduler -- but read
> + * it under RCU (@p's locks aren't held here) and fall back to @sch if
> + * @p isn't on an scx scheduler: a BPF_PROG_TYPE_SYSCALL prog can pass
> + * any task and p->scx.sched is NULL for one that has exited or is
> + * managed by another scheduler.
> */
> if (unlikely(!list_empty(&sch->children))) {
> - scx_error(scx_task_sched(p), "__scx_bpf_select_cpu_and() must be used");
> + scx_error(scx_task_sched_rcu(p) ?: sch,
> + "__scx_bpf_select_cpu_and() must be used");
> return -EINVAL;
> }
> #endif
> --
> 2.25.1
>
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH v2] sched_ext: Fix NULL sched deref in kfunc sub-sched error paths
2026-09-02 17:07 ` [PATCH v2] sched_ext: Fix NULL sched deref in kfunc sub-sched error paths Wanwu Li
2026-09-02 18:11 ` Andrea Righi
@ 2026-09-02 19:37 ` Tejun Heo
2026-09-02 19:39 ` Tejun Heo
1 sibling, 1 reply; 9+ messages in thread
From: Tejun Heo @ 2026-09-02 19:37 UTC (permalink / raw)
To: Wanwu Li
Cc: David Vernet, Andrea Righi, Changwoo Min, sched-ext,
linux-kernel, stable
Hello,
On Thu, Sep 03, 2026 at 01:07:51AM +0800, Wanwu Li wrote:
> Neither wrapper requires a contrived @p. Tasks that are never enabled --
> kthreads and tasks of other classes under SCX_SWITCH_ALL=n -- keep
> p->scx.sched NULL indefinitely; and a task handed over from
This isn't accurate. p->scx.sched is set for every non-idle task on root
enable and on fork regardless of sched class. The only tasks with NULL
p->scx.sched are the ones past sched_ext_dead() and the idle tasks. The new
comments repeat the same claim with "managed by another scheduler".
> Keep the "error out @p's scheduler" attribution -- it is what every other
> kfunc error path does (select_cpu_from_kfunc()'s cross_task,
> scx_kf_arg_task_ok()) and it is correct for the callers that take this
Both of those error out the calling program's scheduler, not @p's. The
compat wrappers are the only ones using @p's scheduler and only because
they don't have @aux.
> + * which scheduler called us, so error out @p's scheduler -- but read
> + * it under RCU (@p's locks aren't held here) and fall back to @sch if
@p's locks are held when called from ops.select_cpu() or ops.enqueue(). The
ext.c comment's "aren't necessarily held" is the right wording.
> + scx_error(scx_task_sched_rcu(p) ?: sch,
> + "__scx_bpf_select_cpu_and() must be used");
As the fallback only triggers for tasks already past sched_ext_dead() (or
idle tasks), tearing down the root scheduler doesn't make sense. How about
adding a flag to the root sched and printing a warning once instead?
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2] sched_ext: Fix NULL sched deref in kfunc sub-sched error paths
2026-09-02 19:37 ` Tejun Heo
@ 2026-09-02 19:39 ` Tejun Heo
0 siblings, 0 replies; 9+ messages in thread
From: Tejun Heo @ 2026-09-02 19:39 UTC (permalink / raw)
To: Wanwu Li
Cc: David Vernet, Andrea Righi, Changwoo Min, sched-ext,
linux-kernel, stable
On Wed, Sep 02, 2026 at 09:37:39AM -1000, Tejun Heo wrote:
> > + scx_error(scx_task_sched_rcu(p) ?: sch,
> > + "__scx_bpf_select_cpu_and() must be used");
>
> As the fallback only triggers for tasks already past sched_ext_dead() (or
> idle tasks), tearing down the root scheduler doesn't make sense. How about
> adding a flag to the root sched and printing a warning once instead?
It'd be fine to just ignore it too. The scx_error() is there to flag cases
where things went obviously wrong. I don't think we'd lose anything
meaningful by just ignoring it when sch can't be determined.
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v3] sched_ext: Fix NULL sched deref in kfunc sub-sched error paths
2026-09-02 15:36 [PATCH] sched_ext: Fix NULL sched deref in select_cpu_and sub-sched error path Wanwu Li
2026-09-02 16:14 ` Andrea Righi
2026-09-02 17:07 ` [PATCH v2] sched_ext: Fix NULL sched deref in kfunc sub-sched error paths Wanwu Li
@ 2026-09-03 6:06 ` Wanwu Li
2026-09-03 17:56 ` Tejun Heo
2 siblings, 1 reply; 9+ messages in thread
From: Wanwu Li @ 2026-09-03 6:06 UTC (permalink / raw)
To: Tejun Heo
Cc: David Vernet, Andrea Righi, Changwoo Min, Wanwu Li, sched-ext,
linux-kernel, stable
When the root scheduler has sub-scheds attached, the COMPAT kfunc
wrappers scx_bpf_select_cpu_and() and scx_bpf_dsq_insert_vtime() refuse
the call and report to @p's scheduler:
scx_error(scx_task_sched(p), "... must be used");
The wrappers are reachable with tasks that have no scheduler.
scx_bpf_select_cpu_and() is in the select_cpu kfunc group, which
scx_kfunc_context_filter() opens to BPF_PROG_TYPE_SYSCALL programs;
scx_bpf_dsq_insert_vtime() is in the enqueue_dispatch group, which
ops.enqueue() and ops.dispatch() may call with any KF_RCU task -- the
group has no kf_tasks validation, and scx_dsq_insert_preamble() checks
task ownership with scx_task_on_sched() precisely because @p may be an
arbitrary task.
scx_task_sched(p) is p->scx.sched, which is NULL for tasks past
sched_ext_dead() -- which clears it via scx_disable_and_exit_task() on
exit -- and for idle tasks, which the enable paths skip as they are
never scheduled through SCX. It is also an rcu_dereference_protected()
that expects @p's pi_lock or rq lock, which neither wrapper holds.
Passing NULL to scx_error() reaches scx_vexit(), which dereferences
sch->exit_info, oopsing the kernel.
One concrete trigger exercised while developing the fix: a
BPF_PROG_TYPE_SYSCALL program calling the select_cpu_and wrapper on an
exited-but-not-reaped task while a sub-scheduler was attached (its pid
stays findable while the zombie is unreaped; faulting instruction is
the scx_vexit() prologue "mov r15,[rdi+0x398]" with RDI=NULL and 0x398
the offset of sch->exit_info):
sched_ext: BPF scheduler "kfunc_subsched_null" enabled
sched_ext: BPF sub-scheduler "kfunc_subsched_null" enabled
sched_ext: Unassociated program run_select_cpu_ (id 76)
BUG: kernel NULL pointer dereference, address: 0000000000000398
#PF: supervisor read access in kernel mode
#PF: error_code(0x0000) - not-present page
Oops: Oops: 0000 [#1] SMP NOPTI
CPU: 7 UID: 0 PID: 8201 Comm: kfunc_test_runn Tainted: G W
RIP: 0010:scx_vexit+0x25/0xa0
Code: ... <4c> 8b bf 98 03 00 00 ...
CR2: 0000000000000398
Call Trace:
<TASK>
__scx_exit+0x4f/0x70
scx_bpf_select_cpu_and+0xab/0xb0
bpf_prog_430ed61a7b66e03a_run_select_cpu_and+0x9c/0xe7
? __x64_sys_bpf+0x2c/0x40
bpf_prog_test_run_syscall+0x130/0x2f0
__sys_bpf+0x930/0x10d0
? __x64_sys_bpf+0x2c/0x40
__x64_sys_bpf+0x2c/0x40
do_syscall_64+0xbc/0x460
entry_SYSCALL_64_after_hwframe+0x76/0x7e
</TASK>
Read @p's scheduler under RCU instead, which the wrappers can do from
their guard(rcu)(): fault it when it can be determined, and when it
can't be determined -- @p is a task past sched_ext_dead() or an idle
task -- there is nothing obviously wrong to report, so just refuse the
call as before without faulting any scheduler.
These COMPAT wrappers are scheduled for eventual removal once the
deprecation grace period elapses, but until then -- and regardless of
their removal timeline -- they must not oops the kernel on a task they
are handed.
Cc: <stable@vger.kernel.org>
Fixes: a5fa0708cbfd ("sched_ext: Enforce scheduling authority in dispatch and select_cpu operations")
Suggested-by: Andrea Righi <arighi@nvidia.com>
Signed-off-by: Wanwu Li <liwanwu@kylinos.cn>
---
Changes v2 -> v3:
- Per Tejun: when the task's scheduler can't be determined (tasks past
sched_ext_dead() and idle tasks), refuse the call instead of faulting
the root scheduler; keep faulting @p's scheduler when determinable.
- Update the comments: fix the wording.
kernel/sched/ext/ext.c | 11 +++++++++--
kernel/sched/ext/idle.c | 11 +++++++++--
2 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
index 10af28a9f2c0..1c9e4eace89e 100644
--- a/kernel/sched/ext/ext.c
+++ b/kernel/sched/ext/ext.c
@@ -8943,10 +8943,17 @@ __bpf_kfunc void scx_bpf_dsq_insert_vtime(struct task_struct *p, u64 dsq_id,
#ifdef CONFIG_EXT_SUB_SCHED
/*
* Disallow if any sub-scheds are attached. There is no way to tell
- * which scheduler called us, just error out @p's scheduler.
+ * which scheduler called us, so error out @p's scheduler -- read it
+ * under RCU as @p's locks aren't necessarily held here. @p may be a
+ * task past sched_ext_dead() or an idle task, in which case its
+ * scheduler can't be determined and there is nothing obviously wrong
+ * to report; just refuse the call.
*/
if (unlikely(!list_empty(&sch->children))) {
- scx_error(scx_task_sched(p), "__scx_bpf_dsq_insert_vtime() must be used");
+ struct scx_sched *tsch = scx_task_sched_rcu(p);
+
+ if (tsch)
+ scx_error(tsch, "__scx_bpf_dsq_insert_vtime() must be used");
return;
}
#endif
diff --git a/kernel/sched/ext/idle.c b/kernel/sched/ext/idle.c
index d2973fb3af6d..aa9fb6de0ad6 100644
--- a/kernel/sched/ext/idle.c
+++ b/kernel/sched/ext/idle.c
@@ -1142,10 +1142,17 @@ __bpf_kfunc s32 scx_bpf_select_cpu_and(struct task_struct *p, s32 prev_cpu, u64
#ifdef CONFIG_EXT_SUB_SCHED
/*
* Disallow if any sub-scheds are attached. There is no way to tell
- * which scheduler called us, just error out @p's scheduler.
+ * which scheduler called us, so error out @p's scheduler -- read it
+ * under RCU as @p's locks aren't necessarily held here. @p may be a
+ * task past sched_ext_dead() or an idle task, in which case its
+ * scheduler can't be determined and there is nothing obviously wrong
+ * to report; just refuse the call.
*/
if (unlikely(!list_empty(&sch->children))) {
- scx_error(scx_task_sched(p), "__scx_bpf_select_cpu_and() must be used");
+ struct scx_sched *tsch = scx_task_sched_rcu(p);
+
+ if (tsch)
+ scx_error(tsch, "__scx_bpf_select_cpu_and() must be used");
return -EINVAL;
}
#endif
--
2.25.1
^ permalink raw reply [flat|nested] 9+ messages in thread