mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH bpf-next v2 0/3] bpf: Fix a struct_ops use-after-free behind a flaky assoc_in_timer test
@ 2026-10-10  6:13 Qiliang Yuan
  2026-10-10  6:13 ` [PATCH bpf-next v2 1/3] bpf: Defer freeing a struct_ops map until its progs are done Qiliang Yuan
                   ` (2 more replies)
  0 siblings, 3 replies; 5+ messages in thread
From: Qiliang Yuan @ 2026-10-10  6:13 UTC (permalink / raw)
  To: Andrii Nakryiko, Eduard Zingerman, Ihor Solodrai,
	Alexei Starovoitov, Daniel Borkmann, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, Shuah Khan, Amery Hung
  Cc: bpf, linux-kselftest, linux-kernel, Qiliang Yuan

st_ops_assoc_in_timer_no_uref drops every reference to a struct_ops map
and expects a timer callback of its prog, 500ms later, to find no
struct_ops associated anymore. It fails now and then while test_progs
runs other tests in parallel, and v1 [1] made the test wait until the
association goes away.

As the BPF CI bot pointed out on v1, the association goes away late
since commit 5db69b0fbdc8 ("bpf: Make struct_ops tasks_rcu grace period
optional"), which moved the RCU grace period of a struct_ops map in
front of clearing the association, so nothing waits for a prog that
reads the association in between. With that window widened, a timer
callback returns into a freed trampoline and the kernel hits an int3
Oops.

Patch 1 frees the map only after a grace period that follows clearing
the association, and patch 2 adds a test that catches a map freed under
a running .test_1. Patch 3 is v1 of the test fix, which can keep calling
through the association until it is cleared now that this is safe.

[1] https://lore.kernel.org/r/20261009-selftests-bpf-struct-ops-assoc-timer-v1-1-9a97a9b3d1db@gmail.com

Signed-off-by: Qiliang Yuan <odys.yuan@gmail.com>
---
V1 -> V2:
- Add patch 1, fixing the use-after-free behind the flaky test (CI bot)
- Add patch 2, a test for the use-after-free
- Point Fixes of patch 3 at 5db69b0fbdc8 (CI bot)
- Move the restarting of the timer into patch 2, which needs it too,
  so patch 3 only turns it on for st_ops_assoc_in_timer_no_uref
- Rebase onto current bpf-next

v1: https://lore.kernel.org/r/20261009-selftests-bpf-struct-ops-assoc-timer-v1-1-9a97a9b3d1db@gmail.com

---
Qiliang Yuan (3):
      bpf: Defer freeing a struct_ops map until its progs are done
      selftests/bpf: Test freeing a struct_ops map under a running prog
      selftests/bpf: Wait for the struct_ops map to be freed in assoc_in_timer

 kernel/bpf/bpf_struct_ops.c                        | 30 +++++++++++++++++++++-
 .../bpf/prog_tests/test_struct_ops_assoc.c         | 29 ++++++++++++++++++---
 .../bpf/progs/struct_ops_assoc_in_timer.c          | 16 ++++++++++++
 3 files changed, 70 insertions(+), 5 deletions(-)
---
base-commit: 15b578b1715d9a318c804350d98af87c203672bc
change-id: 20261009-selftests-bpf-struct-ops-assoc-timer-5eb0295ec300

Best regards,
-- 
Qiliang Yuan <odys.yuan@gmail.com>


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH bpf-next v2 1/3] bpf: Defer freeing a struct_ops map until its progs are done
  2026-10-10  6:13 [PATCH bpf-next v2 0/3] bpf: Fix a struct_ops use-after-free behind a flaky assoc_in_timer test Qiliang Yuan
@ 2026-10-10  6:13 ` Qiliang Yuan
  2026-10-10  7:12   ` bot+bpf-ci
  2026-10-10  6:13 ` [PATCH bpf-next v2 2/3] selftests/bpf: Test freeing a struct_ops map under a running prog Qiliang Yuan
  2026-10-10  6:13 ` [PATCH bpf-next v2 3/3] selftests/bpf: Wait for the struct_ops map to be freed in assoc_in_timer Qiliang Yuan
  2 siblings, 1 reply; 5+ messages in thread
From: Qiliang Yuan @ 2026-10-10  6:13 UTC (permalink / raw)
  To: Andrii Nakryiko, Eduard Zingerman, Ihor Solodrai,
	Alexei Starovoitov, Daniel Borkmann, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, Shuah Khan, Amery Hung
  Cc: bpf, linux-kselftest, linux-kernel, Qiliang Yuan

A struct_ops prog is associated with its map without a reference on the
map, and finds the struct_ops through bpf_prog_get_assoc_struct_ops()
under RCU until bpf_struct_ops_map_free() clears the association. For a
struct_ops map, bpf_map_put() waits for an RCU grace period before it
calls bpf_struct_ops_map_free(), which clears the association and then
frees the trampoline and the kdata right away.

Nothing waits for a prog that reads the association after that grace
period started and before the association is cleared. A timer callback
of a struct_ops prog that calls test_1 of bpf_testmod's multi_st_ops
through the association while the map is freed returns into the freed
trampoline, whose image is filled with int3. RAX holds MAP_MAGIC (1234),
which test_1 has just returned:

  Oops: int3: 0000 [#1] SMP KASAN NOPTI
  CPU: 2 UID: 0 PID: 31 Comm: ksoftirqd/2 ...
  RIP: 0010:0xffffffffc043114d
  Code: cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc <cc> cc cc cc
  RAX: 00000000000004d2 ...
  Call Trace:
   bpf_prog_ec591f4f9d13f364_timer_cb+0x96/0x1e2
   bpf_timer_cb+0x15d/0x240
   __hrtimer_run_queues+0x264/0x5a0
   hrtimer_run_softirq+0x1a7/0x3b0
   handle_softirqs+0x18c/0x5b0

Free the map after a grace period that starts once the association is
cleared, a tasks trace one if the progs of the map may sleep. Clearing
the association before the grace period of bpf_map_put() doesn't work,
as it takes a mutex while bpf_map_put() may run in softirq, e.g. from
bpf_struct_ops_put() of a tcp congestion control.

The st_ops_assoc_in_timer_free subtest of test_progs reproduces it. Its
timer callback keeps calling test_1 through the association while the
map is freed, and test_1 spins in bpf_loop() when called from there:

  $ cd tools/testing/selftests/bpf
  $ make test_progs
  $ ./test_progs -t struct_ops_assoc/st_ops_assoc_in_timer_free

On kernels of the same bpf-next commit with KASAN enabled, x86_64 VM
with 32 vCPUs, before and after this patch:

            before                     after
  outcome   Oops (int3) on run 1       100 of 100 runs passed, no Oops

Fixes: 5db69b0fbdc8 ("bpf: Make struct_ops tasks_rcu grace period optional")
Signed-off-by: Qiliang Yuan <odys.yuan@gmail.com>
---
 kernel/bpf/bpf_struct_ops.c | 30 +++++++++++++++++++++++++++++-
 1 file changed, 29 insertions(+), 1 deletion(-)

diff --git a/kernel/bpf/bpf_struct_ops.c b/kernel/bpf/bpf_struct_ops.c
index 1178acd72296e..cd082339589b3 100644
--- a/kernel/bpf/bpf_struct_ops.c
+++ b/kernel/bpf/bpf_struct_ops.c
@@ -42,6 +42,9 @@ struct bpf_struct_ops_map {
 	void *image_pages[MAX_TRAMP_IMAGE_PAGES];
 	/* The owner moduler's btf. */
 	struct btf *btf;
+	/* free the map once the progs that used it have finished */
+	struct rcu_head free_rcu;
+	struct work_struct free_work;
 	/* uvalue->data stores the kernel struct
 	 * (e.g. tcp_congestion_ops) that is more useful
 	 * to userspace than the kvalue.  For example,
@@ -1032,6 +1035,23 @@ static void bpf_struct_ops_map_free_pre_rcu(struct bpf_map *map)
 	bpf_struct_ops_map_del_ksyms(st_map);
 }
 
+static void bpf_struct_ops_map_free_deferred(struct work_struct *work)
+{
+	struct bpf_struct_ops_map *st_map;
+
+	st_map = container_of(work, struct bpf_struct_ops_map, free_work);
+	__bpf_struct_ops_map_free(&st_map->map);
+}
+
+static void bpf_struct_ops_map_free_rcu_gp(struct rcu_head *rcu)
+{
+	struct bpf_struct_ops_map *st_map;
+
+	st_map = container_of(rcu, struct bpf_struct_ops_map, free_rcu);
+	INIT_WORK(&st_map->free_work, bpf_struct_ops_map_free_deferred);
+	queue_work(system_dfl_wq, &st_map->free_work);
+}
+
 static void bpf_struct_ops_map_free(struct bpf_map *map)
 {
 	struct bpf_struct_ops_map *st_map = (struct bpf_struct_ops_map *)map;
@@ -1050,7 +1070,15 @@ static void bpf_struct_ops_map_free(struct bpf_map *map)
 	if (tasks_rcu && IS_ENABLED(CONFIG_TASKS_RCU))
 		synchronize_rcu_tasks();
 
-	__bpf_struct_ops_map_free(map);
+	/*
+	 * A prog may have read the association before it was cleared above,
+	 * e.g. from a timer callback, and may still be running the struct_ops.
+	 * Free the map only after a grace period that waits for such a prog.
+	 */
+	if (map->free_after_mult_rcu_gp)
+		call_rcu_tasks_trace(&st_map->free_rcu, bpf_struct_ops_map_free_rcu_gp);
+	else
+		call_rcu(&st_map->free_rcu, bpf_struct_ops_map_free_rcu_gp);
 }
 
 static int bpf_struct_ops_map_alloc_check(union bpf_attr *attr)

-- 
2.43.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH bpf-next v2 2/3] selftests/bpf: Test freeing a struct_ops map under a running prog
  2026-10-10  6:13 [PATCH bpf-next v2 0/3] bpf: Fix a struct_ops use-after-free behind a flaky assoc_in_timer test Qiliang Yuan
  2026-10-10  6:13 ` [PATCH bpf-next v2 1/3] bpf: Defer freeing a struct_ops map until its progs are done Qiliang Yuan
@ 2026-10-10  6:13 ` Qiliang Yuan
  2026-10-10  6:13 ` [PATCH bpf-next v2 3/3] selftests/bpf: Wait for the struct_ops map to be freed in assoc_in_timer Qiliang Yuan
  2 siblings, 0 replies; 5+ messages in thread
From: Qiliang Yuan @ 2026-10-10  6:13 UTC (permalink / raw)
  To: Andrii Nakryiko, Eduard Zingerman, Ihor Solodrai,
	Alexei Starovoitov, Daniel Borkmann, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, Shuah Khan, Amery Hung
  Cc: bpf, linux-kselftest, linux-kernel, Qiliang Yuan

st_ops_assoc_in_timer_no_uref drops every reference to the struct_ops
map while a timer callback of its prog calls .test_1 through the
association, and checks that the callback ends up with no struct_ops.

The timer first fires 500ms later and .test_1 returns right away, so the
callback hardly ever runs .test_1 while the map is freed, and the test
can't catch a map that is freed under a running .test_1.

Add st_ops_assoc_in_timer_free, which starts the timer right away,
restarts it with no delay until the association is gone, and makes
.test_1 spin in bpf_loop() when the timer callback calls it. Share the
steps of st_ops_assoc_in_timer_no_uref with it through a helper.

st_ops_assoc_in_timer_free run in a loop on kernels of the same bpf-next
commit with KASAN enabled, x86_64 VM with 32 vCPUs, without and with
the previous fix:

              without the fix           with the fix
  outcome     Oops (int3) on run 1      100 of 100 passed, 0.28 s each

Signed-off-by: Qiliang Yuan <odys.yuan@gmail.com>
---
 .../bpf/prog_tests/test_struct_ops_assoc.c         | 29 +++++++++++++++++++---
 .../bpf/progs/struct_ops_assoc_in_timer.c          | 16 ++++++++++++
 2 files changed, 41 insertions(+), 4 deletions(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_assoc.c b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_assoc.c
index 461ded7223515..987117091c676 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_assoc.c
+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_assoc.c
@@ -136,7 +136,8 @@ static void test_st_ops_assoc_in_timer(void)
 	struct_ops_assoc_in_timer__destroy(skel);
 }
 
-static void test_st_ops_assoc_in_timer_no_uref(void)
+static void run_st_ops_assoc_in_timer_no_uref(int timer_ns, int wait_map_free,
+					      int spin_loops)
 {
 	struct struct_ops_assoc_in_timer *skel = NULL;
 	struct bpf_link *link;
@@ -157,10 +158,13 @@ static void test_st_ops_assoc_in_timer_no_uref(void)
 	/*
 	 * Run .test_1 by calling kfunc bpf_kfunc_multi_st_ops_test_1_prog_arg() and checks
 	 * the return value. .test_1 will also schedule timer_cb that runs .test_1 again.
-	 * timer_cb will run 500ms after syscall_prog runs, when the user space no longer
-	 * holds a reference to st_ops_map.
+	 * timer_cb will run timer_ns after syscall_prog runs and, with wait_map_free, every
+	 * timer_ns after that until the map is freed. With 500ms, it first runs when the
+	 * user space no longer holds a reference to st_ops_map.
 	 */
-	skel->bss->timer_ns = 500000000;
+	skel->bss->timer_ns = timer_ns;
+	skel->bss->wait_map_free = wait_map_free;
+	skel->bss->spin_loops = spin_loops;
 	err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.syscall_prog), NULL);
 	ASSERT_OK(err, "bpf_prog_test_run_opts");
 
@@ -178,6 +182,21 @@ static void test_st_ops_assoc_in_timer_no_uref(void)
 	struct_ops_assoc_in_timer__destroy(skel);
 }
 
+static void test_st_ops_assoc_in_timer_no_uref(void)
+{
+	run_st_ops_assoc_in_timer_no_uref(500000000, 0, 0);
+}
+
+/*
+ * Keep calling .test_1 through the association from the timer callback, and
+ * stay in it for a while each time, while the map is being freed. The map
+ * must not be freed under a running .test_1.
+ */
+static void test_st_ops_assoc_in_timer_free(void)
+{
+	run_st_ops_assoc_in_timer_no_uref(0, 1, 1 << 20);
+}
+
 void test_struct_ops_assoc(void)
 {
 	if (test__start_subtest("st_ops_assoc"))
@@ -188,4 +207,6 @@ void test_struct_ops_assoc(void)
 		test_st_ops_assoc_in_timer();
 	if (test__start_subtest("st_ops_assoc_in_timer_no_uref"))
 		test_st_ops_assoc_in_timer_no_uref();
+	if (test__start_subtest("st_ops_assoc_in_timer_free"))
+		test_st_ops_assoc_in_timer_free();
 }
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_assoc_in_timer.c b/tools/testing/selftests/bpf/progs/struct_ops_assoc_in_timer.c
index 0bed49e9f2170..47a44c8e8b114 100644
--- a/tools/testing/selftests/bpf/progs/struct_ops_assoc_in_timer.c
+++ b/tools/testing/selftests/bpf/progs/struct_ops_assoc_in_timer.c
@@ -25,6 +25,13 @@ int test_err;
 int timer_ns;
 int timer_test_1_ret;
 int timer_cb_run;
+int wait_map_free;
+int spin_loops;
+
+static int spin(u32 i, void *ctx)
+{
+	return 0;
+}
 
 __noinline static int timer_cb(void *map, int *key, struct bpf_timer *timer)
 {
@@ -34,6 +41,12 @@ __noinline static int timer_cb(void *map, int *key, struct bpf_timer *timer)
 	timer_test_1_ret = bpf_kfunc_multi_st_ops_test_1_assoc(&args);
 	recur--;
 
+	/* Try again later until the map is freed */
+	if (wait_map_free && timer_test_1_ret != -1) {
+		bpf_timer_start(timer, timer_ns, 0);
+		return 0;
+	}
+
 	timer_cb_run++;
 
 	return 0;
@@ -53,6 +66,9 @@ int BPF_PROG(test_1, struct st_ops_args *args)
 		bpf_timer_init(timer, &array_map, 1);
 		bpf_timer_set_callback(timer, timer_cb);
 		bpf_timer_start(timer, timer_ns, 0);
+	} else if (spin_loops) {
+		/* Stay in the trampoline while the map may be freed */
+		bpf_loop(spin_loops, spin, NULL, 0);
 	}
 
 	return MAP_MAGIC;

-- 
2.43.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH bpf-next v2 3/3] selftests/bpf: Wait for the struct_ops map to be freed in assoc_in_timer
  2026-10-10  6:13 [PATCH bpf-next v2 0/3] bpf: Fix a struct_ops use-after-free behind a flaky assoc_in_timer test Qiliang Yuan
  2026-10-10  6:13 ` [PATCH bpf-next v2 1/3] bpf: Defer freeing a struct_ops map until its progs are done Qiliang Yuan
  2026-10-10  6:13 ` [PATCH bpf-next v2 2/3] selftests/bpf: Test freeing a struct_ops map under a running prog Qiliang Yuan
@ 2026-10-10  6:13 ` Qiliang Yuan
  2 siblings, 0 replies; 5+ messages in thread
From: Qiliang Yuan @ 2026-10-10  6:13 UTC (permalink / raw)
  To: Andrii Nakryiko, Eduard Zingerman, Ihor Solodrai,
	Alexei Starovoitov, Daniel Borkmann, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, Shuah Khan, Amery Hung
  Cc: bpf, linux-kselftest, linux-kernel, Qiliang Yuan

st_ops_assoc_in_timer_no_uref runs .test_1, which starts a timer to
call it again 500ms later, drops every reference to the struct_ops map,
and expects the timer callback to find no struct_ops associated with
its program anymore.

The association goes away only when the map is freed, which happens in
a work item after an RCU grace period. With other tests running in
parallel, that can take longer than 500ms, and the callback still
reaches the struct_ops and gets MAP_MAGIC instead of -1.

Have the timer callback restart the timer until the map is gone, as it
does for st_ops_assoc_in_timer_free. Calling the struct_ops through the
association until then is safe, as the map is freed only after a grace
period that follows clearing the association.

st_ops_assoc_in_timer_no_uref run 100 times with test_progs -j32 running
in the background, on a KASAN kernel with the fix, x86_64 VM with 32
vCPUs, before and after this patch:

              before                       after
  outcome     2 of 100 failed (MAP_MAGIC)  100 of 100 passed

Fixes: 5db69b0fbdc8 ("bpf: Make struct_ops tasks_rcu grace period optional")
Signed-off-by: Qiliang Yuan <odys.yuan@gmail.com>
---
 tools/testing/selftests/bpf/prog_tests/test_struct_ops_assoc.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_assoc.c b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_assoc.c
index 987117091c676..10fd294a0af3c 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_assoc.c
+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_assoc.c
@@ -184,7 +184,7 @@ static void run_st_ops_assoc_in_timer_no_uref(int timer_ns, int wait_map_free,
 
 static void test_st_ops_assoc_in_timer_no_uref(void)
 {
-	run_st_ops_assoc_in_timer_no_uref(500000000, 0, 0);
+	run_st_ops_assoc_in_timer_no_uref(500000000, 1, 0);
 }
 
 /*

-- 
2.43.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH bpf-next v2 1/3] bpf: Defer freeing a struct_ops map until its progs are done
  2026-10-10  6:13 ` [PATCH bpf-next v2 1/3] bpf: Defer freeing a struct_ops map until its progs are done Qiliang Yuan
@ 2026-10-10  7:12   ` bot+bpf-ci
  0 siblings, 0 replies; 5+ messages in thread
From: bot+bpf-ci @ 2026-10-10  7:12 UTC (permalink / raw)
  To: odys.yuan, andrii, eddyz87, ihor.solodrai, ast, daniel, memxor,
	martin.lau, song, yonghong.song, jolsa, emil, shuah, ameryhung
  Cc: bpf, linux-kselftest, linux-kernel, odys.yuan, ast, andrii,
	daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai

[-- Attachment #1: Type: text/plain, Size: 4696 bytes --]

> bpf: Defer freeing a struct_ops map until its progs are done

TL;DR: Possible use-after-free of struct_ops kdata/trampoline: the deferred
free uses call_rcu(), which doesn't wait for rcu_read_lock_trace readers
(bpf_wq, task_work, sleepable progs); always using call_rcu_tasks_trace() may
fix it.

> diff --git a/kernel/bpf/bpf_struct_ops.c b/kernel/bpf/bpf_struct_ops.c
> --- a/kernel/bpf/bpf_struct_ops.c
> +++ b/kernel/bpf/bpf_struct_ops.c

[ ... ]

> @@ -1050,7 +1070,15 @@ static void bpf_struct_ops_map_free(struct bpf_map *map)
>  	if (tasks_rcu && IS_ENABLED(CONFIG_TASKS_RCU))
>  		synchronize_rcu_tasks();
>
> -	__bpf_struct_ops_map_free(map);
> +	/*
> +	 * A prog may have read the association before it was cleared above,
> +	 * e.g. from a timer callback, and may still be running the struct_ops.
> +	 * Free the map only after a grace period that waits for such a prog.
> +	 */
> +	if (map->free_after_mult_rcu_gp)
> +		call_rcu_tasks_trace(&st_map->free_rcu, bpf_struct_ops_map_free_rcu_gp);
> +	else
> +		call_rcu(&st_map->free_rcu, bpf_struct_ops_map_free_rcu_gp);
                ^^^^

Does this grace period wait for every reader of the association?

The commit message says the grace period is "a tasks trace one if the progs
of the map may sleep", but the code does not look at whether the progs may
sleep.  It tests map->free_after_mult_rcu_gp, which is copied from
st_ops->free_after_mult_rcu_gp in bpf_struct_ops_map_alloc().  git grep
finds no in-tree struct_ops that sets that flag (bpf_tcp_ca only sets
free_after_tasks_rcu_gp), so every in-tree struct_ops map, including
bpf_testmod's multi_st_ops and sched_ext, takes the call_rcu() branch.

bpf_prog_get_assoc_struct_ops() reads aux->st_ops_assoc under
bpf_rcu_lock_held(), which also accepts rcu_read_lock_trace().  Async
callbacks of a struct_ops prog run with only that lock held:

  static void bpf_wq_work(struct work_struct *work)
  {
      ...
      rcu_read_lock_trace();
      migrate_disable();
      callback_fn((u64)(long)map, (u64)(long)key, (u64)(long)value, 0, 0);
      migrate_enable();
      rcu_read_unlock_trace();
  }

bpf_task_work_callback() works the same way (guard(rcu_tasks_trace)() plus
migrate_disable()), and is_async_cb_sleepable() in verifier.c says "bpf_wq
and bpf_task_work callbacks are always sleepable".  The bpf_wq kfuncs are
in common_btf_ids, which is registered for BPF_PROG_TYPE_UNSPEC, and
bpf_kfunc_multi_st_ops_test_1_assoc() is registered for
BPF_PROG_TYPE_STRUCT_OPS.

Can this sequence occur on a CONFIG_PREEMPT_RCU kernel, with the
st_ops_assoc_in_timer_free test written with bpf_wq_start() instead of
bpf_timer_start()?

kworker (bpf_wq_work, rcu_read_lock_trace only)
    st_ops = bpf_prog_get_assoc_struct_ops(aux)
        -> &S->kvalue.data
    <preempted, or after __bpf_prog_exit()'s rcu_read_unlock() in the
     trampoline tail>

free path
    bpf_map_put(S) -> ... -> bpf_struct_ops_map_free()
        dissoc_progs();
        call_rcu(&st_map->free_rcu)
    GP ends: this reader is not an RCU reader
    bpf_struct_ops_map_free_rcu_gp() -> queue_work()
        __bpf_struct_ops_map_free()
            bpf_struct_ops_map_free_image();
            bpf_map_area_free(st_map)

kworker
    st_ops->test_1(args)    <- reads freed kdata, or returns into the
                               freed trampoline

sched_ext looks affected the same way, since scx_prog_sched() reads
ops->priv from the kdata that bpf_prog_get_assoc_struct_ops() returns.

struct_ops progs take no reference on the map when they are associated;
bpf_prog_assoc_struct_ops() calls bpf_map_inc() only for non-struct_ops
progs.  So the grace period here is the only thing keeping the map alive,
and call_rcu() does not wait for rcu_read_lock_trace() sections.

This also seems to fix less than the code before the Fixes: commit.  Before
5db69b0fbdc8, bpf_struct_ops_map_free() ran
synchronize_rcu_mult(call_rcu, call_rcu_tasks) after the dissoc, which also
waited for preempted tasks and so covered the trampoline tail for these
preemptible readers.

Since 57b23c0f612d ("bpf: Retire rcu_trace_implies_rcu_gp()") a tasks trace
grace period implies a regular RCU grace period.  Would always using
call_rcu_tasks_trace() here cover all of the readers: BH (bpf_timer), plain
RCU, and tasks trace (bpf_wq, bpf_task_work, sleepable progs)?

The two later commits in the series (49210cd1ca6f and 17c1f2ff6e23) only
change selftests and do not address this.


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/38031235772

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-10-10  7:12 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-10  6:13 [PATCH bpf-next v2 0/3] bpf: Fix a struct_ops use-after-free behind a flaky assoc_in_timer test Qiliang Yuan
2026-10-10  6:13 ` [PATCH bpf-next v2 1/3] bpf: Defer freeing a struct_ops map until its progs are done Qiliang Yuan
2026-10-10  7:12   ` bot+bpf-ci
2026-10-10  6:13 ` [PATCH bpf-next v2 2/3] selftests/bpf: Test freeing a struct_ops map under a running prog Qiliang Yuan
2026-10-10  6:13 ` [PATCH bpf-next v2 3/3] selftests/bpf: Wait for the struct_ops map to be freed in assoc_in_timer Qiliang Yuan

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®