* [PATCH 6.18.y 0/2] sched_ext: Fix bpf_scx_unreg() teardown race
@ 2026-11-03 19:07 Kuba Piecuch
2026-11-03 19:07 ` [PATCH 6.18.y 1/2] sched_ext: sync disable_irq_work in bpf_scx_unreg() Kuba Piecuch
2026-11-03 19:07 ` [PATCH 6.18.y 2/2] sched_ext: Fix scx_flush_disable_work() UAF race Kuba Piecuch
0 siblings, 2 replies; 3+ messages in thread
From: Kuba Piecuch @ 2026-11-03 19:07 UTC (permalink / raw)
To: stable
Cc: Kuba Piecuch, Tejun Heo, Andrea Righi, Richard Cheng,
Cheng-Yang Chou, Sasha Levin, Greg Kroah-Hartman, sched-ext,
linux-kernel
6.18-stable commit f786e6652b93 ("sched_ext: Reorganize enable/disable
path for multi-scheduler support", upstream dbd542a8fac7, v6.18.46) made
scx_disable() queue disable_work only if it wins scx_claim_exit(). If
scx_vexit() has already claimed the exit and error_irq_work has not yet
queued disable_work, kthread_flush_work() in bpf_scx_unreg() is a no-op
and the last sch->kobj reference is dropped while the scheduler is still
enabled:
CPU A CPU B
----- -----
scx_vexit()
scx_claim_exit() -> true
irq_work_queue()
scx_error_irq_workfn()
scx_dump_state()
bpf_scx_unreg()
scx_disable()
scx_claim_exit() -> false
kthread_flush_work() // no-op
kobject_put(&sch->kobj)
kthread_queue_work()
scx_disable_workfn()
scx_root_disable()
kobject_del(&sch->kobj)
kobject_put() has already done __kobject_del(), so the second
kobject_del() drops another scx_kset reference and frees
/sys/kernel/sched_ext. The next attach then hits:
BUG: KASAN: slab-use-after-free in kobject_get+0xcb/0xe0
...
Freed by task 210:
kfree
kobject_put
__kobject_del
kobject_del
scx_root_disable.cold
scx_disable_workfn
Returning early also lets the struct_ops maps be freed while ops can still
be called, which is how mainline hit the same race (see patch 1).
Mainline fixed this with 510a27055446 and d99f7a32f09d. They didn't reach
6.18.y because they fix f4a6c506d118, which 6.18.y doesn't have. This
series backports them, with error_irq_work in place of disable_irq_work:
bpf_scx_unreg() and the enable error path now wait until exit_kind is
SCX_EXIT_NONE or SCX_EXIT_DONE.
I didn't revert f786e6652b93: that also fixes the reproducer, but brings
back scx_disable() acting on scx_root rather than the link's scheduler.
Detaching the leftover link of a scheduler whose ops.init() failed would
then disable a scheduler loaded after it.
Tested in vng with KASAN:
- v6.18.60: KASAN slab-use-after-free on re-attach, 3/3 runs.
- v6.18.60 + this series: reproducer passes 5/5, no warnings;
sched_ext selftests 23/23 pass.
Reproducer: a syscall prog calls scx_bpf_error() on CPU 1, ops.dump()
spins to hold error_irq_work, CPU 0 detaches the link, then the
scheduler is attached again.
bpftool btf dump file vmlinux format c > vmlinux.h
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 -c repro.bpf.c
bpftool gen skeleton repro.bpf.o > repro.skel.h
cc -O2 repro.c -o repro -lbpf
vng -r arch/x86/boot/bzImage --user root --cpus 4 -e ./repro
---8<--- repro.bpf.c ---
// SPDX-License-Identifier: GPL-2.0
#define BPF_NO_KFUNC_PROTOTYPES
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char _license[] SEC("license") = "GPL";
void scx_bpf_error_bstr(char *fmt, unsigned long long *data,
u32 data__sz) __ksym;
volatile __u32 dump_entered, unreg_done, exit_called;
SEC("syscall")
int trigger_err(void *ctx)
{
static char msg[] = "repro";
unsigned long long param[1] = {};
scx_bpf_error_bstr(msg, param, sizeof(param));
return 0;
}
/* Hold scx_error_irq_workfn() before it queues disable_work. */
SEC("struct_ops/repro_dump")
void BPF_PROG(repro_dump, struct scx_dump_ctx *dctx)
{
u64 start = bpf_ktime_get_ns();
u32 i;
dump_entered = 1;
bpf_for(i, 0, 8000000) /* bpf_iter_num caps at BPF_MAX_LOOPS */
if (unreg_done || bpf_ktime_get_ns() - start > 500000000ULL)
break;
}
SEC("struct_ops.s/repro_exit")
void BPF_PROG(repro_exit, struct scx_exit_info *ei)
{
exit_called = 1;
}
SEC(".struct_ops.link")
struct sched_ext_ops repro_ops = {
.dump = (void *)repro_dump,
.exit = (void *)repro_exit,
.flags = SCX_OPS_SWITCH_PARTIAL,
.name = "repro",
};
---8<--- repro.c ---
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <unistd.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include "repro.skel.h"
static void pin(int cpu)
{
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(cpu, &set);
sched_setaffinity(0, sizeof(set), &set);
}
static void *err_fn(void *arg)
{
LIBBPF_OPTS(bpf_test_run_opts, opts);
pin(1);
bpf_prog_test_run_opts(*(int *)arg, &opts);
return NULL;
}
int main(void)
{
struct repro_bpf *skel;
struct bpf_link *link;
pthread_t tid;
int fd, exited;
pin(0);
skel = repro_bpf__open_and_load();
if (!skel)
return 1;
link = bpf_map__attach_struct_ops(skel->maps.repro_ops);
if (!link)
return 1;
/* CPU 1: claim the exit and enter ops.dump() from error_irq_work */
fd = bpf_program__fd(skel->progs.trigger_err);
pthread_create(&tid, NULL, err_fn, &fd);
while (!skel->bss->dump_entered)
sched_yield();
/* CPU 0: unregister while disable_work is not yet queued */
bpf_link__destroy(link);
exited = skel->bss->exit_called;
skel->bss->unreg_done = 1;
pthread_join(tid, NULL);
usleep(350000);
repro_bpf__destroy(skel);
/* re-attach touches scx_kset, freed on buggy kernels */
skel = repro_bpf__open_and_load();
if (skel) {
link = bpf_map__attach_struct_ops(skel->maps.repro_ops);
bpf_link__destroy(link);
repro_bpf__destroy(skel);
}
printf("%s: ops.exit() %s before unreg returned\n",
exited ? "PASS" : "FAIL", exited ? "ran" : "did not run");
return !exited;
}
---8<---
Cheng-Yang Chou (1):
sched_ext: Fix scx_flush_disable_work() UAF race
Richard Cheng (1):
sched_ext: sync disable_irq_work in bpf_scx_unreg()
kernel/sched/ext.c | 23 +++++++++++++++++++++--
1 file changed, 21 insertions(+), 2 deletions(-)
--
2.56.0.385.gac671c819a-goog
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH 6.18.y 1/2] sched_ext: sync disable_irq_work in bpf_scx_unreg()
2026-11-03 19:07 [PATCH 6.18.y 0/2] sched_ext: Fix bpf_scx_unreg() teardown race Kuba Piecuch
@ 2026-11-03 19:07 ` Kuba Piecuch
2026-11-03 19:07 ` [PATCH 6.18.y 2/2] sched_ext: Fix scx_flush_disable_work() UAF race Kuba Piecuch
1 sibling, 0 replies; 3+ messages in thread
From: Kuba Piecuch @ 2026-11-03 19:07 UTC (permalink / raw)
To: stable
Cc: Richard Cheng, Tejun Heo, Andrea Righi, Cheng-Yang Chou,
Sasha Levin, Greg Kroah-Hartman, sched-ext, linux-kernel,
Kuba Piecuch
From: Richard Cheng <icheng@nvidia.com>
[ Upstream commit 510a27055446b8f0d29487ca8b8d2033dc2b6ca6 ]
When unregistered my self-written scx scheduler, the following panic
occurs.
[ 229.923133] Kernel text patching generated an invalid instruction at 0xffff80009bc2c1f8!
[ 229.923146] Internal error: Oops - BRK: 00000000f2000100 [#1] SMP
[ 230.077871] CPU: 48 UID: 0 PID: 1760 Comm: kworker/u583:7 Not tainted 7.0.0+ #3 PREEMPT(full)
[ 230.086677] Hardware name: NVIDIA GB200 NVL/P3809-BMC, BIOS 02.05.12 20251107
[ 230.093972] Workqueue: events_unbound bpf_map_free_deferred
[ 230.099675] Sched_ext: invariant_0.1.0_aarch64_unknown_linux_gnu_debug (disabling), task: runnable_at=-174ms
[ 230.116843] pc : 0xffff80009bc2c1f8
[ 230.120406] lr : dequeue_task_scx+0x270/0x2d0
[ 230.217749] Call trace:
[ 230.228515] 0xffff80009bc2c1f8 (P)
[ 230.232077] dequeue_task+0x84/0x188
[ 230.235728] sched_change_begin+0x1dc/0x250
[ 230.240000] __set_cpus_allowed_ptr_locked+0x17c/0x240
[ 230.245250] __set_cpus_allowed_ptr+0x74/0xf0
[ 230.249701] ___migrate_enable+0x4c/0xa0
[ 230.253707] bpf_map_free_deferred+0x1a4/0x1b0
[ 230.258246] process_one_work+0x184/0x540
[ 230.262342] worker_thread+0x19c/0x348
[ 230.266170] kthread+0x13c/0x150
[ 230.269465] ret_from_fork+0x10/0x20
[ 230.281393] Code: d4202000 d4202000 d4202000 d4202000 (d4202000)
[ 230.287621] ---[ end trace 0000000000000000 ]---
[ 231.160046] Kernel panic - not syncing: Oops - BRK: Fatal exception in interrupt
The root cause is that the JIT page backing ops->quiescent() is freed
before all callers of that function have stopped.
The expected ordering during teardown is:
bitmap_zero(sch->has_op) + synchronize_rcu()
-> guarantees no CPU will ever call sch->ops.* again
-> only THEN free the BPF struct_ops JIT page
bpf_scx_unreg() is supposed to enforce the order, but after
commit f4a6c506d118 ("sched_ext: Always bounce scx_disable() through
irq_work"), disable_work is no longer queued directly, causing
kthread_flush_work() to be a noop. Thus, the caller drops the struct_ops
map too early and poisoned with AARCH64_BREAK_FAULT before
disable_workfn ever execute.
So the subsequent dequeue_task() still sees SCX_HAS_OP(sch, quiescent)
as true and calls ops.quiescent, which hit on the poisoned page and BRK
panic.
Add a helper scx_flush_disable_work() so the future use cases that want
to flush disable_work can use it.
Also amend the call for scx_root_enable_workfn() and
scx_sub_enable_workfn() which have similar pattern in the error path.
Fixes: f4a6c506d118 ("sched_ext: Always bounce scx_disable() through irq_work")
Signed-off-by: Richard Cheng <icheng@nvidia.com>
Reviewed-by: Andrea Righi <arighi@nvidia.com>
Reviewed-by: Cheng-Yang Chou <yphbchou0911@gmail.com>
Signed-off-by: Tejun Heo <tj@kernel.org>
[ 6.18.y has neither f4a6c506d118 nor sub-schedulers. Here, the flush is
a no-op when scx_disable() loses the exit claim to scx_vexit(), whose
error_irq_work queues disable_work later. Sync error_irq_work instead
of disable_irq_work and drop the scx_sub_enable_workfn() hunk. ]
Signed-off-by: Kuba Piecuch <jpiecuch@google.com>
---
kernel/sched/ext.c | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/kernel/sched/ext.c b/kernel/sched/ext.c
index f3e3146e5feb..d2460a88fba3 100644
--- a/kernel/sched/ext.c
+++ b/kernel/sched/ext.c
@@ -4207,6 +4207,20 @@ static void scx_disable(struct scx_sched *sch, enum scx_exit_kind kind)
kthread_queue_work(sch->helper, &sch->disable_work);
}
+/**
+ * scx_flush_disable_work - flush the disable work and wait for it to finish
+ * @sch: the scheduler
+ *
+ * sch->disable_work might still not queued, causing kthread_flush_work()
+ * as a noop. Syncing the irq_work first is required to guarantee the
+ * kthread work has been queued before waiting for it.
+ */
+static void scx_flush_disable_work(struct scx_sched *sch)
+{
+ irq_work_sync(&sch->error_irq_work);
+ kthread_flush_work(&sch->disable_work);
+}
+
static void dump_newline(struct seq_buf *s)
{
trace_sched_ext_dump("");
@@ -4968,7 +4982,7 @@ err_disable:
* completion. sch's base reference will be put by bpf_scx_unreg().
*/
scx_error(sch, "scx_root_enable() failed (%d)", ret);
- kthread_flush_work(&sch->disable_work);
+ scx_flush_disable_work(sch);
cmd->ret = 0;
}
@@ -5142,7 +5156,7 @@ static void bpf_scx_unreg(void *kdata, struct bpf_link *link)
struct scx_sched *sch = ops->priv;
scx_disable(sch, SCX_EXIT_UNREG);
- kthread_flush_work(&sch->disable_work);
+ scx_flush_disable_work(sch);
kobject_put(&sch->kobj);
}
--
2.56.0.385.gac671c819a-goog
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH 6.18.y 2/2] sched_ext: Fix scx_flush_disable_work() UAF race
2026-11-03 19:07 [PATCH 6.18.y 0/2] sched_ext: Fix bpf_scx_unreg() teardown race Kuba Piecuch
2026-11-03 19:07 ` [PATCH 6.18.y 1/2] sched_ext: sync disable_irq_work in bpf_scx_unreg() Kuba Piecuch
@ 2026-11-03 19:07 ` Kuba Piecuch
1 sibling, 0 replies; 3+ messages in thread
From: Kuba Piecuch @ 2026-11-03 19:07 UTC (permalink / raw)
To: stable
Cc: Cheng-Yang Chou, Tejun Heo, Andrea Righi, Richard Cheng,
Sasha Levin, Greg Kroah-Hartman, sched-ext, linux-kernel,
Kuba Piecuch
From: Cheng-Yang Chou <yphbchou0911@gmail.com>
[ Upstream commit d99f7a32f09dccbe396187370ec1a74a31b73d7e ]
scx_flush_disable_work() calls irq_work_sync() followed by
kthread_flush_work() to ensure that the disable kthread work has
fully completed before bpf_scx_unreg() frees the SCX scheduler.
However, a concurrent scx_vexit() (e.g., triggered by a watchdog stall)
creates a race window between scx_claim_exit() and irq_work_queue():
CPU A (scx_vexit (watchdog)) CPU B (bpf_scx_unreg)
---- ----
scx_claim_exit()
atomic_try_cmpxchg(NONE->kind)
stack_trace_save()
vscnprintf()
scx_disable()
scx_claim_exit() -> FAIL
scx_flush_disable_work()
irq_work_sync() // no-op: not queued yet
kthread_flush_work() // no-op: not queued yet
kobject_put(&sch->kobj) -> free %sch
irq_work_queue() -> UAF on %sch
scx_disable_irq_workfn()
kthread_queue_work() -> UAF
The root cause is that CPU B's scx_flush_disable_work() returns after
syncing an irq_work that has not yet been queued, while CPU A is still
executing the code between scx_claim_exit() and irq_work_queue().
Loop until exit_kind reaches SCX_EXIT_DONE or SCX_EXIT_NONE, draining
disable_irq_work and disable_work in each pass. This ensures that any
work queued after the previous check is caught, while also correctly
handling cases where no disable was triggered (e.g., the
scx_sub_enable_workfn() abort path).
Fixes: 510a27055446 ("sched_ext: sync disable_irq_work in bpf_scx_unreg()")
Reported-by: https://sashiko.dev/#/patchset/20260424100221.32407-1-icheng%40nvidia.com
Suggested-by: Tejun Heo <tj@kernel.org>
Signed-off-by: Cheng-Yang Chou <yphbchou0911@gmail.com>
Signed-off-by: Tejun Heo <tj@kernel.org>
[ Sync error_irq_work instead of disable_irq_work. ]
Signed-off-by: Kuba Piecuch <jpiecuch@google.com>
---
kernel/sched/ext.c | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/kernel/sched/ext.c b/kernel/sched/ext.c
index d2460a88fba3..78f15ddc0aa5 100644
--- a/kernel/sched/ext.c
+++ b/kernel/sched/ext.c
@@ -4217,8 +4217,13 @@ static void scx_disable(struct scx_sched *sch, enum scx_exit_kind kind)
*/
static void scx_flush_disable_work(struct scx_sched *sch)
{
- irq_work_sync(&sch->error_irq_work);
- kthread_flush_work(&sch->disable_work);
+ int kind;
+
+ do {
+ irq_work_sync(&sch->error_irq_work);
+ kthread_flush_work(&sch->disable_work);
+ kind = atomic_read(&sch->exit_kind);
+ } while (kind != SCX_EXIT_NONE && kind != SCX_EXIT_DONE);
}
static void dump_newline(struct seq_buf *s)
--
2.56.0.385.gac671c819a-goog
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-03 19:08 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-03 19:07 [PATCH 6.18.y 0/2] sched_ext: Fix bpf_scx_unreg() teardown race Kuba Piecuch
2026-11-03 19:07 ` [PATCH 6.18.y 1/2] sched_ext: sync disable_irq_work in bpf_scx_unreg() Kuba Piecuch
2026-11-03 19:07 ` [PATCH 6.18.y 2/2] sched_ext: Fix scx_flush_disable_work() UAF race Kuba Piecuch
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®