* [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame
@ 2026-11-06 23:11 Ihor Solodrai
2026-11-06 23:11 ` [PATCH bpf v1 1/4] bpf: Scope the bpf_user_ringbuf_drain() dynptr to its callback frame Ihor Solodrai
` (5 more replies)
0 siblings, 6 replies; 8+ messages in thread
From: Ihor Solodrai @ 2026-11-06 23:11 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
A callback-calling helper can pass its callback a pointer that is only
valid for the callback invocation, and the verifier has no way to
express that lifetime.
Two helpers need fixing:
* bpf_user_ringbuf_drain() passes a CONST_PTR_TO_DYNPTR over a
sample it releases as soon as the callback returns. Saving this
pointer in callback_ctx leaves it pointing into reused kernel
stack and allows arbitrary kernel read and write since v7.3.
* bpf_for_each_map_elem() over an array or per-CPU array map passes
the address of a u32 held in bpf_for_each_array_elem()'s own frame,
leaking four bytes of kernel stack.
Give the callback frame an id and make it both the id and the
parent_id of such an argument, so that slices and clones descend from
it. Invalidate the id and its descendants when the frame is popped.
---
v2->v3:
* Replace REF_TYPE_FRAME with a frame_id in bpf_func_state and drop
the preparation and refactoring patches (Alexei)
* Rebase on bpf, and add Fixes: tags
v2: https://lore.kernel.org/r/20261009202759.50520-1-ihor.solodrai@linux.dev
v1->v2:
* Add the refactoring patches: release frame references in one scan,
and fold diagnostics into the REF_TYPE_FRAME introduction
* Replace temporary argument mask and deferred register scan with
reference acquisition directly in the owning verifier state during
callback setup (Eduard)
* Reuse callback_park_dynptr() for the nested drain test
v1: https://lore.kernel.org/r/20260922010333.1226537-1-ihor.solodrai@linux.dev
---
Ihor Solodrai (4):
bpf: Scope the bpf_user_ringbuf_drain() dynptr to its callback frame
selftests/bpf: Cover the user ringbuf callback dynptr lifetime
bpf: Scope the bpf_for_each_map_elem() array key to the callback frame
selftests/bpf: Cover callback-frame map key lifetime
include/linux/bpf.h | 2 +
include/linux/bpf_verifier.h | 2 +
kernel/bpf/arraymap.c | 19 ++-
kernel/bpf/states.c | 3 +
kernel/bpf/verifier.c | 16 +++
.../selftests/bpf/progs/user_ringbuf_fail.c | 135 ++++++++++++++++++
.../bpf/progs/verifier_iterating_callbacks.c | 91 ++++++++++++
7 files changed, 266 insertions(+), 2 deletions(-)
base-commit: 475a0cfd02f14d764023e926ac01fc7597134229
--
2.56.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH bpf v1 1/4] bpf: Scope the bpf_user_ringbuf_drain() dynptr to its callback frame
2026-11-06 23:11 [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame Ihor Solodrai
@ 2026-11-06 23:11 ` Ihor Solodrai
2026-11-06 23:11 ` [PATCH bpf v1 2/4] selftests/bpf: Cover the user ringbuf callback dynptr lifetime Ihor Solodrai
` (4 subsequent siblings)
5 siblings, 0 replies; 8+ messages in thread
From: Ihor Solodrai @ 2026-11-06 23:11 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
The dynptr argument to a bpf_user_ringbuf_drain() callback points to a
descriptor on the helper's stack. The descriptor and the sample it
describes are valid only during that callback invocation.
The callback can save the dynptr pointer, a slice, or a clone in the
caller's frame and use it after the drain returns. An escaped descriptor
pointer can allow arbitrary kernel read/write once the helper's stack
is reused.
The verifier assigns the dynptr a type and an id, but does not tie it
or its descendants to the callback's lifetime.
Give the callback frame an id and make it both the id and the parent_id
of R1, then invalidate that id and everything derived from it when the
frame is popped. R1 has to be its own parent: slices take the dynptr's
id as their parent_id, and bpf_dynptr_data() clobbers R1, so with a
separate dynptr id nothing would connect a slice to the frame.
Fixes: 205715673844 ("bpf: Add bpf_user_ringbuf_drain() helper")
Fixes: 1f737e46ca6a ("bpf: Remove artificial limitations on pointer types eligible for spilling")
Cc: stable@vger.kernel.org
Reported-by: Nicholas Carlini <npc@anthropic.com>
Suggested-by: Alexei Starovoitov <ast@kernel.org>
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
include/linux/bpf_verifier.h | 2 ++
kernel/bpf/states.c | 3 +++
kernel/bpf/verifier.c | 16 ++++++++++++++++
3 files changed, 21 insertions(+)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index b43eb97bcd44..dabdd286182c 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -377,6 +377,8 @@ struct bpf_func_state {
* reused after returns, while this ID is preserved across state clones.
*/
u32 diag_frame_id;
+ /* parent_id of the args that are gone when this frame returns */
+ u32 frame_id;
/* subprog number == index within subprog_info
* zero == main subprog
*/
diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c
index 50480c42bcad..d8ad497f9b4b 100644
--- a/kernel/bpf/states.c
+++ b/kernel/bpf/states.c
@@ -998,6 +998,9 @@ static bool func_states_equal(struct bpf_verifier_env *env, struct bpf_func_stat
if (old->in_rcu_callback_fn != cur->in_rcu_callback_fn)
return false;
+ if (!check_ids(old->frame_id, cur->frame_id, &env->idmap_scratch))
+ return false;
+
for (i = 0; i < MAX_BPF_REG; i++)
if (((1 << i) & live_regs) &&
!regsafe(env, &old->regs[i], &cur->regs[i],
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 5e4700fda66b..a801b8da2b63 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -11804,6 +11804,16 @@ int map_set_for_each_callback_args(struct bpf_verifier_env *env,
return 0;
}
+/* The arg points into the stack frame of the helper that calls the callback */
+static void mark_frame_scoped_arg(struct bpf_verifier_env *env,
+ struct bpf_func_state *callee, u32 regno)
+{
+ if (!callee->frame_id)
+ callee->frame_id = ++env->id_gen;
+ callee->regs[regno].id = callee->frame_id;
+ callee->regs[regno].parent_id = callee->frame_id;
+}
+
static int set_callee_state(struct bpf_verifier_env *env,
struct bpf_func_state *caller,
struct bpf_func_state *callee, int insn_idx)
@@ -11955,6 +11965,7 @@ static int set_user_ringbuf_callback_state(struct bpf_verifier_env *env,
*/
bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_0]);
mark_dynptr_cb_reg(env, &callee->regs[BPF_REG_1], BPF_DYNPTR_TYPE_LOCAL);
+ mark_frame_scoped_arg(env, callee, BPF_REG_1);
callee->regs[BPF_REG_2] = caller->regs[BPF_REG_3];
/* unused */
@@ -12223,6 +12234,11 @@ static int prepare_func_exit(struct bpf_verifier_env *env, int *insn_idx)
if (err)
return err;
invalidate_stack_slices(env, callee->frameno, INT_MIN, 0);
+ if (callee->frame_id) {
+ err = invalidate_reference(env, callee->frame_id);
+ if (err)
+ return err;
+ }
/* for callbacks like bpf_loop or bpf_for_each_map_elem go back to callsite,
* there function call logic would reschedule callback visit. If iteration
--
2.56.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH bpf v1 2/4] selftests/bpf: Cover the user ringbuf callback dynptr lifetime
2026-11-06 23:11 [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame Ihor Solodrai
2026-11-06 23:11 ` [PATCH bpf v1 1/4] bpf: Scope the bpf_user_ringbuf_drain() dynptr to its callback frame Ihor Solodrai
@ 2026-11-06 23:11 ` Ihor Solodrai
2026-11-06 23:11 ` [PATCH bpf v1 3/4] bpf: Scope the bpf_for_each_map_elem() array key to the callback frame Ihor Solodrai
` (3 subsequent siblings)
5 siblings, 0 replies; 8+ messages in thread
From: Ihor Solodrai @ 2026-11-06 23:11 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
The dynptr passed to a bpf_user_ringbuf_drain() callback, and values
derived from it, must not be used after that callback returns.
Exercise escapes through the callback context, slices returned by
bpf_dynptr_data() and bpf_dynptr_slice(), and clones stored in the
caller's frame. Also cover an inner callback's dynptr escaping into an
outer callback.
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
.../selftests/bpf/progs/user_ringbuf_fail.c | 135 ++++++++++++++++++
1 file changed, 135 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/user_ringbuf_fail.c b/tools/testing/selftests/bpf/progs/user_ringbuf_fail.c
index c0d0422b8030..80eb1e7802ad 100644
--- a/tools/testing/selftests/bpf/progs/user_ringbuf_fail.c
+++ b/tools/testing/selftests/bpf/progs/user_ringbuf_fail.c
@@ -1,9 +1,11 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2022 Meta Platforms, Inc. and affiliates. */
+#include <stdbool.h>
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include "bpf_misc.h"
+#include "bpf_kfuncs.h"
char _license[] SEC("license") = "GPL";
@@ -243,3 +245,136 @@ int user_ringbuf_callback_const_ptr_to_dynptr_reg_off(void *ctx)
callback_adjust_bpf_dynptr_reg_off, NULL, 0);
return 0;
}
+
+/* The sample goes back to the producer as soon as the callback returns. */
+struct dynptr_ctx {
+ struct bpf_dynptr *saved;
+};
+
+static long callback_park_dynptr(struct bpf_dynptr *dynptr, void *context)
+{
+ struct dynptr_ctx *c = context;
+
+ c->saved = dynptr;
+ return 0;
+}
+
+SEC("?raw_tp")
+__failure __msg("R3 type=scalar expected=fp, dynptr_ptr")
+int user_ringbuf_callback_park_dynptr(void *ctx)
+{
+ struct dynptr_ctx c = {};
+ char buf[8] = {};
+
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_dynptr, &c, 0);
+ if (c.saved)
+ bpf_dynptr_read(buf, sizeof(buf), c.saved, 0, 0);
+ return buf[0];
+}
+
+struct slice_ctx {
+ char *p;
+};
+
+static long callback_park_data_slice(struct bpf_dynptr *dynptr, void *context)
+{
+ struct slice_ctx *c = context;
+
+ c->p = bpf_dynptr_data(dynptr, 0, 8);
+ return 0;
+}
+
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int user_ringbuf_callback_park_data_slice(void *ctx)
+{
+ struct slice_ctx c = {};
+
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_data_slice, &c, 0);
+ if (c.p)
+ return c.p[0];
+ return 0;
+}
+
+static long callback_park_kfunc_slice(struct bpf_dynptr *dynptr, void *context)
+{
+ struct slice_ctx *c = context;
+
+ c->p = bpf_dynptr_slice(dynptr, 0, NULL, 8);
+ return 0;
+}
+
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int user_ringbuf_callback_park_kfunc_slice(void *ctx)
+{
+ struct slice_ctx c = {};
+
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_kfunc_slice, &c, 0);
+ if (c.p)
+ return c.p[0];
+ return 0;
+}
+
+struct clone_ctx {
+ struct bpf_dynptr clone;
+ __u64 armed;
+};
+
+static long callback_park_clone(struct bpf_dynptr *dynptr, void *context)
+{
+ struct clone_ctx *c = context;
+
+ bpf_dynptr_clone(dynptr, &c->clone);
+ c->armed = 1;
+ return 0;
+}
+
+SEC("?raw_tp")
+__failure __msg("Expected an initialized dynptr as R3")
+int user_ringbuf_callback_park_clone(void *ctx)
+{
+ struct clone_ctx c = {};
+ char buf[8] = {};
+
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_clone, &c, 0);
+ if (c.armed)
+ bpf_dynptr_read(buf, sizeof(buf), &c.clone, 0, 0);
+ return buf[0];
+}
+
+SEC("?raw_tp")
+__failure __msg("Expected an initialized dynptr as R1")
+int user_ringbuf_callback_park_clone_then_slice(void *ctx)
+{
+ struct clone_ctx c = {};
+ char *p;
+
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_clone, &c, 0);
+ if (c.armed) {
+ p = bpf_dynptr_data(&c.clone, 0, 8);
+ if (p)
+ return p[0];
+ }
+ return 0;
+}
+
+/* An inner drain's dynptr must not escape into the outer callback either. */
+static long callback_park_outer(struct bpf_dynptr *dynptr, void *context)
+{
+ struct dynptr_ctx inner = {};
+ char buf[8] = {};
+
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_dynptr, &inner, 0);
+ if (inner.saved)
+ bpf_dynptr_read(buf, sizeof(buf), inner.saved, 0, 0);
+ return buf[0] ? 1 : 0;
+}
+
+SEC("?raw_tp")
+__failure __msg("R3 type=scalar expected=fp, dynptr_ptr")
+int user_ringbuf_callback_nested_park_inner(void *ctx)
+{
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_outer, NULL, 0);
+ return 0;
+}
--
2.56.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH bpf v1 3/4] bpf: Scope the bpf_for_each_map_elem() array key to the callback frame
2026-11-06 23:11 [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame Ihor Solodrai
2026-11-06 23:11 ` [PATCH bpf v1 1/4] bpf: Scope the bpf_user_ringbuf_drain() dynptr to its callback frame Ihor Solodrai
2026-11-06 23:11 ` [PATCH bpf v1 2/4] selftests/bpf: Cover the user ringbuf callback dynptr lifetime Ihor Solodrai
@ 2026-11-06 23:11 ` Ihor Solodrai
2026-11-06 23:11 ` [PATCH bpf v1 4/4] selftests/bpf: Cover callback-frame map key lifetime Ihor Solodrai
` (2 subsequent siblings)
5 siblings, 0 replies; 8+ messages in thread
From: Ihor Solodrai @ 2026-11-06 23:11 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
bpf_for_each_map_elem() passes array and per-CPU array callbacks the
address of a key on the helper's stack.
A callback can save that pointer in its context and dereference it
after the helper returns. Loads through PTR_TO_MAP_KEY are not
fault-protected, allowing a four-byte read-only leak of reused kernel
stack.
The verifier tracks the pointer's type and key size, but does not tie
its lifetime to the callback invocation.
Scope R2 to the callback frame in the array map callback setup so the
key and its copies are invalidated on callback return. Leave hash keys
and map values unchanged, since their storage is not callback-local.
Fixes: 06dcdcd4b9e8 ("bpf: Add arraymap support for bpf_for_each_map_elem() helper")
Reported-by: Nicholas Carlini <npc@anthropic.com>
Suggested-by: Alexei Starovoitov <ast@kernel.org>
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
include/linux/bpf.h | 2 ++
kernel/bpf/arraymap.c | 19 +++++++++++++++++--
kernel/bpf/verifier.c | 4 ++--
3 files changed, 21 insertions(+), 4 deletions(-)
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index fdbf638300fc..0a41166c9fff 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -3236,6 +3236,8 @@ int bpf_iter_map_check_value_write(const struct bpf_prog *prog,
int map_set_for_each_callback_args(struct bpf_verifier_env *env,
struct bpf_func_state *caller,
struct bpf_func_state *callee);
+void mark_frame_scoped_arg(struct bpf_verifier_env *env,
+ struct bpf_func_state *callee, u32 regno);
int bpf_percpu_hash_copy(struct bpf_map *map, void *key, void *value, u64 flags);
int bpf_percpu_array_copy(struct bpf_map *map, void *key, void *value, u64 flags);
diff --git a/kernel/bpf/arraymap.c b/kernel/bpf/arraymap.c
index 133f189bf6d2..508706d0c360 100644
--- a/kernel/bpf/arraymap.c
+++ b/kernel/bpf/arraymap.c
@@ -855,6 +855,21 @@ static u64 array_map_mem_usage(const struct bpf_map *map)
return usage;
}
+static int array_map_set_for_each_callback_args(struct bpf_verifier_env *env,
+ struct bpf_func_state *caller,
+ struct bpf_func_state *callee)
+{
+ int err;
+
+ err = map_set_for_each_callback_args(env, caller, callee);
+ if (err)
+ return err;
+
+ /* the key is on the stack of bpf_for_each_array_elem() */
+ mark_frame_scoped_arg(env, callee, BPF_REG_2);
+ return 0;
+}
+
BTF_ID_LIST_SINGLE(array_map_btf_ids, struct, bpf_array)
const struct bpf_map_ops array_map_ops = {
.map_meta_equal = array_map_meta_equal,
@@ -875,7 +890,7 @@ const struct bpf_map_ops array_map_ops = {
.map_check_btf = array_map_check_btf,
.map_lookup_batch = generic_map_lookup_batch,
.map_update_batch = generic_map_update_batch,
- .map_set_for_each_callback_args = map_set_for_each_callback_args,
+ .map_set_for_each_callback_args = array_map_set_for_each_callback_args,
.map_for_each_callback = bpf_for_each_array_elem,
.map_mem_usage = array_map_mem_usage,
.map_btf_id = &array_map_btf_ids[0],
@@ -900,7 +915,7 @@ const struct bpf_map_ops percpu_array_map_ops = {
.map_check_btf = array_map_check_btf,
.map_lookup_batch = generic_map_lookup_batch,
.map_update_batch = generic_map_update_batch,
- .map_set_for_each_callback_args = map_set_for_each_callback_args,
+ .map_set_for_each_callback_args = array_map_set_for_each_callback_args,
.map_for_each_callback = bpf_for_each_array_elem,
.map_mem_usage = array_map_mem_usage,
.map_btf_id = &array_map_btf_ids[0],
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index a801b8da2b63..07348b9b88c9 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -11805,8 +11805,8 @@ int map_set_for_each_callback_args(struct bpf_verifier_env *env,
}
/* The arg points into the stack frame of the helper that calls the callback */
-static void mark_frame_scoped_arg(struct bpf_verifier_env *env,
- struct bpf_func_state *callee, u32 regno)
+void mark_frame_scoped_arg(struct bpf_verifier_env *env,
+ struct bpf_func_state *callee, u32 regno)
{
if (!callee->frame_id)
callee->frame_id = ++env->id_gen;
--
2.56.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH bpf v1 4/4] selftests/bpf: Cover callback-frame map key lifetime
2026-11-06 23:11 [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame Ihor Solodrai
` (2 preceding siblings ...)
2026-11-06 23:11 ` [PATCH bpf v1 3/4] bpf: Scope the bpf_for_each_map_elem() array key to the callback frame Ihor Solodrai
@ 2026-11-06 23:11 ` Ihor Solodrai
2026-11-06 23:15 ` [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame Ihor Solodrai
2026-11-06 23:32 ` Ihor Solodrai
5 siblings, 0 replies; 8+ messages in thread
From: Ihor Solodrai @ 2026-11-06 23:11 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
Array and per-CPU array callbacks to bpf_for_each_map_elem() receive
a key on the helper's stack.
Check that saving this key in the callback context and dereferencing
it after iteration is rejected.
Also require the same pattern to keep verifying for a hash key and
an array value, whose storage outlives the callback. These controls
guard the array-only and key-only scope of the lifetime restriction.
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
.../bpf/progs/verifier_iterating_callbacks.c | 91 +++++++++++++++++++
1 file changed, 91 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/verifier_iterating_callbacks.c b/tools/testing/selftests/bpf/progs/verifier_iterating_callbacks.c
index 4dd98acc6436..da8068a1f004 100644
--- a/tools/testing/selftests/bpf/progs/verifier_iterating_callbacks.c
+++ b/tools/testing/selftests/bpf/progs/verifier_iterating_callbacks.c
@@ -9,6 +9,20 @@ struct {
__type(value, __u64);
} map SEC(".maps");
+struct {
+ __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
+ __uint(max_entries, 8);
+ __type(key, __u32);
+ __type(value, __u64);
+} percpu_map SEC(".maps");
+
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 8);
+ __type(key, __u32);
+ __type(value, __u64);
+} hash_map SEC(".maps");
+
struct {
__uint(type, BPF_MAP_TYPE_USER_RINGBUF);
__uint(max_entries, 8);
@@ -804,4 +818,81 @@ __naked void check_add_const_regsafe_off(void)
: __clobber_common);
}
+struct key_ctx {
+ __u32 *key;
+};
+
+static long park_key_cb(struct bpf_map *map, __u32 *key, __u64 *value,
+ void *context)
+{
+ struct key_ctx *c = context;
+
+ c->key = key;
+ return 0;
+}
+
+/* bpf_for_each_array_elem() passes a key from its own stack frame. */
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int array_park_map_key(void *ctx)
+{
+ struct key_ctx c = {};
+
+ bpf_for_each_map_elem(&map, park_key_cb, &c, 0);
+ if (c.key)
+ return *c.key;
+ return 0;
+}
+
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int percpu_array_park_map_key(void *ctx)
+{
+ struct key_ctx c = {};
+
+ bpf_for_each_map_elem(&percpu_map, park_key_cb, &c, 0);
+ if (c.key)
+ return *c.key;
+ return 0;
+}
+
+/* A hash key points into the element, which outlives the callback. */
+SEC("?raw_tp")
+__success
+int hash_park_map_key(void *ctx)
+{
+ struct key_ctx c = {};
+
+ bpf_for_each_map_elem(&hash_map, park_key_cb, &c, 0);
+ if (c.key)
+ return *c.key;
+ return 0;
+}
+
+struct value_ctx {
+ __u64 *value;
+};
+
+static long park_value_cb(struct bpf_map *map, __u32 *key, __u64 *value,
+ void *context)
+{
+ struct value_ctx *c = context;
+
+ c->value = value;
+ return 0;
+}
+
+/* Only the key is frame-scoped; the element lives until map teardown. */
+SEC("?raw_tp")
+__success
+int array_park_map_value(void *ctx)
+{
+ struct value_ctx c = {};
+
+ bpf_for_each_map_elem(&map, park_value_cb, &c, 0);
+ if (c.value)
+ return *c.value;
+ return 0;
+}
+
char _license[] SEC("license") = "GPL";
--
2.56.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame
2026-11-06 23:11 [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame Ihor Solodrai
` (3 preceding siblings ...)
2026-11-06 23:11 ` [PATCH bpf v1 4/4] selftests/bpf: Cover callback-frame map key lifetime Ihor Solodrai
@ 2026-11-06 23:15 ` Ihor Solodrai
2026-11-06 23:22 ` Kumar Kartikeya Dwivedi
2026-11-06 23:32 ` Ihor Solodrai
5 siblings, 1 reply; 8+ messages in thread
From: Ihor Solodrai @ 2026-11-06 23:15 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
On 11/6/26 4:11 PM, Ihor Solodrai wrote:
> A callback-calling helper can pass its callback a pointer that is only
> valid for the callback invocation, and the verifier has no way to
> express that lifetime.
>
> Two helpers need fixing:
>
> * bpf_user_ringbuf_drain() passes a CONST_PTR_TO_DYNPTR over a
> sample it releases as soon as the callback returns. Saving this
> pointer in callback_ctx leaves it pointing into reused kernel
> stack and allows arbitrary kernel read and write since v7.3.
>
> * bpf_for_each_map_elem() over an array or per-CPU array map passes
> the address of a u32 held in bpf_for_each_array_elem()'s own frame,
> leaking four bytes of kernel stack.
>
> Give the callback frame an id and make it both the id and the
> parent_id of such an argument, so that slices and clones descend from
> it. Invalidate the id and its descendants when the frame is popped.
>
> ---
>
> v2->v3:
Sorry, I forgot -v3 when doing git format-patch
Should I resend, or is this fine?
> [...]
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame
2026-11-06 23:15 ` [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame Ihor Solodrai
@ 2026-11-06 23:22 ` Kumar Kartikeya Dwivedi
0 siblings, 0 replies; 8+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-11-06 23:22 UTC (permalink / raw)
To: Ihor Solodrai
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Amery Hung, Emil Tsalapatis, Nicholas Carlini,
bpf, linux-kernel, kernel-team
On Sat, 7 Nov 2026 at 00:15, Ihor Solodrai <ihor.solodrai@linux.dev> wrote:
>
> On 11/6/26 4:11 PM, Ihor Solodrai wrote:
> > A callback-calling helper can pass its callback a pointer that is only
> > valid for the callback invocation, and the verifier has no way to
> > express that lifetime.
> >
> > Two helpers need fixing:
> >
> > * bpf_user_ringbuf_drain() passes a CONST_PTR_TO_DYNPTR over a
> > sample it releases as soon as the callback returns. Saving this
> > pointer in callback_ctx leaves it pointing into reused kernel
> > stack and allows arbitrary kernel read and write since v7.3.
> >
> > * bpf_for_each_map_elem() over an array or per-CPU array map passes
> > the address of a u32 held in bpf_for_each_array_elem()'s own frame,
> > leaking four bytes of kernel stack.
> >
> > Give the callback frame an id and make it both the id and the
> > parent_id of such an argument, so that slices and clones descend from
> > it. Invalidate the id and its descendants when the frame is popped.
> >
> > ---
> >
> > v2->v3:
>
> Sorry, I forgot -v3 when doing git format-patch
>
> Should I resend, or is this fine?
>
Should be ok, can be ascertained from the cover letter.
> > [...]
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame
2026-11-06 23:11 [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame Ihor Solodrai
` (4 preceding siblings ...)
2026-11-06 23:15 ` [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame Ihor Solodrai
@ 2026-11-06 23:32 ` Ihor Solodrai
5 siblings, 0 replies; 8+ messages in thread
From: Ihor Solodrai @ 2026-11-06 23:32 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
On 11/6/26 4:11 PM, Ihor Solodrai wrote:
> [...]
>
> Ihor Solodrai (4):
> bpf: Scope the bpf_user_ringbuf_drain() dynptr to its callback frame
> selftests/bpf: Cover the user ringbuf callback dynptr lifetime
> bpf: Scope the bpf_for_each_map_elem() array key to the callback frame
> selftests/bpf: Cover callback-frame map key lifetime
Alexei, if this lands first, your pending verifier rewrite will need a
hunk like the one pasted below. Or the other way around.
Without it, a join can link a value to the wrong callback frame.
diff --git a/kernel/bpf/join.c b/kernel/bpf/join.c
index cc4494aef187..92b94e18c873 100644
--- a/kernel/bpf/join.c
+++ b/kernel/bpf/join.c
@@ -464,6 +464,9 @@ static bool join_func(struct bpf_join_state *js, struct bpf_func_state *fd,
return false;
if (!join_loop_stack(fa, fb))
return false;
+ jid_note(js, fa->frame_id, fb->frame_id, true);
+ } else if (!jid_strict_ok(js, fa->frame_id, fb->frame_id)) {
+ return false;
}
for (i = 0; i < MAX_BPF_REG; i++) {
>
> include/linux/bpf.h | 2 +
> include/linux/bpf_verifier.h | 2 +
> kernel/bpf/arraymap.c | 19 ++-
> kernel/bpf/states.c | 3 +
> kernel/bpf/verifier.c | 16 +++
> .../selftests/bpf/progs/user_ringbuf_fail.c | 135 ++++++++++++++++++
> .../bpf/progs/verifier_iterating_callbacks.c | 91 ++++++++++++
> 7 files changed, 266 insertions(+), 2 deletions(-)
>
>
> base-commit: 475a0cfd02f14d764023e926ac01fc7597134229
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-11-06 23:32 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-06 23:11 [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame Ihor Solodrai
2026-11-06 23:11 ` [PATCH bpf v1 1/4] bpf: Scope the bpf_user_ringbuf_drain() dynptr to its callback frame Ihor Solodrai
2026-11-06 23:11 ` [PATCH bpf v1 2/4] selftests/bpf: Cover the user ringbuf callback dynptr lifetime Ihor Solodrai
2026-11-06 23:11 ` [PATCH bpf v1 3/4] bpf: Scope the bpf_for_each_map_elem() array key to the callback frame Ihor Solodrai
2026-11-06 23:11 ` [PATCH bpf v1 4/4] selftests/bpf: Cover callback-frame map key lifetime Ihor Solodrai
2026-11-06 23:15 ` [PATCH bpf v1 0/4] bpf: Scope callback arguments to their frame Ihor Solodrai
2026-11-06 23:22 ` Kumar Kartikeya Dwivedi
2026-11-06 23:32 ` Ihor Solodrai
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®