mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [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

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®