* [PATCH bpf v3 1/2] bpf: Validate stack arg reads against caller state
@ 2026-10-26 1:52 Weiming Shi
2026-10-26 1:52 ` [PATCH bpf v3 2/2] selftests/bpf: Test missing and callback stack arguments Weiming Shi
2026-10-26 4:30 ` [PATCH bpf v3 1/2] bpf: Validate stack arg reads against caller state patchwork-bot+netdevbpf
0 siblings, 2 replies; 4+ messages in thread
From: Weiming Shi @ 2026-10-26 1:52 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, John Fastabend,
Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman,
Kumar Kartikeya Dwivedi, Song Liu, Yonghong Song, Jiri Olsa,
Shuah Khan
Cc: bpf, linux-kselftest, linux-kernel, co+eada64ce90ecebce,
Xiang Mei, Weiming Shi, Alexei Starovoitov
check_stack_arg_read() bounds a stack argument index with the callee's
BTF-derived argument count, then uses the index in the caller's
stack_arg_regs[] array. The array's actual logical length is recorded in
caller->out_stack_arg_cnt, so the bound and indexed object can diverge.
A crafted BPF_PROG_LOAD can use an argument mismatch to mark a static
subprogram's BTF information unreliable. Later calls return from
btf_check_subprog_call() before check_outgoing_stack_args(), while
check_func_call() continues the static call on -EINVAL. The callee can
then read a slot that the caller never allocated or initialized. The
earlier commit 4286f5deee14 ("bpf: Validate outgoing stack args when
btf_prepare_func_args fails") closed this hole for the first call, but
not for later calls after the subprogram has been marked unreliable.
Callbacks have a related problem. The verifier can make incoming stack
arguments readable from outgoing slots at the helper call site. However,
helpers and kfuncs invoke callbacks through an ABI that passes at most five
arguments, so the runtime callback receives no stack arguments. Async
callback verification can also reach check_stack_arg_read() without a
caller frame.
On x86-64, KASAN reports the direct-call out-of-bounds copy as:
BUG: KASAN: slab-out-of-bounds in check_load_mem+0xb51/0xc60
Read of size 80 at addr ffff8881009af550 by task poc/131
Call Trace:
check_stack_arg_read at kernel/bpf/verifier.c:4234
(inlined by) check_load_mem at kernel/bpf/verifier.c:6645
do_check_common at kernel/bpf/verifier.c:19568
bpf_check at kernel/bpf/verifier.c:21318
bpf_prog_load at kernel/bpf/syscall.c:3134
Reject callback subprograms with incoming stack arguments before their
state is pushed. For direct calls, retrieve the slot with
bpf_get_spilled_stack_arg(), which validates the index against the caller's
out_stack_arg_cnt and rejects uninitialized slots before copying the
register state.
Fixes: 0f6bd5e7a804 ("bpf: Support stack arguments for bpf functions")
Reported-by: <co+eada64ce90ecebce@bugs.sh>
Suggested-by: Alexei Starovoitov <alexei.starovoitov@gmail.com>
Assisted-by: LLM
Signed-off-by: Weiming Shi <bestswngs@gmail.com>
---
Changes in v3:
- Rebase onto the bpf tree at commit 00d2cdf1637e ("Merge branch
'bpf-track-borrowed-iterator-result-lifetimes'").
Changes in v2:
- Reject callback subprograms that declare stack arguments.
- Drop the curframe-zero check because only async callbacks can reach that
state, and callbacks with stack arguments are now rejected earlier.
- Explain that commit 4286f5deee14 closed the hole for the first call only.
kernel/bpf/verifier.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index c223bf3bc9cdb6..a0a5f075d2018a 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -4666,7 +4666,12 @@ static int check_stack_arg_read(struct bpf_verifier_env *env, struct bpf_func_st
}
caller = vstate->frame[vstate->curframe - 1];
- arg = &caller->stack_arg_regs[spi];
+ arg = bpf_get_spilled_stack_arg(spi, caller);
+ if (!arg) {
+ verbose(env, "invalid read from uninitialized stack arg off %d\n", off);
+ return -EACCES;
+ }
+
cur = vstate->frame[vstate->curframe];
bpf_diag_mod_begin(env, &cur->regs[dst_regno], arg, BPF_DIAG_MOD_WRITE);
cur->regs[dst_regno] = *arg;
@@ -11451,6 +11456,10 @@ static int push_callback_call(struct bpf_verifier_env *env, struct bpf_insn *ins
err = btf_check_subprog_call(env, subprog);
if (err == -EFAULT)
return err;
+ if (bpf_in_stack_arg_cnt(&env->subprog_info[subprog])) {
+ verbose(env, "callback subprog cannot have stack args\n");
+ return -EINVAL;
+ }
/* set_callee_state is used for direct subprog calls, but we are
* interested in validating only BPF helpers that can call subprogs as
--
2.56.0
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH bpf v3 2/2] selftests/bpf: Test missing and callback stack arguments 2026-10-26 1:52 [PATCH bpf v3 1/2] bpf: Validate stack arg reads against caller state Weiming Shi @ 2026-10-26 1:52 ` Weiming Shi 2026-10-26 2:28 ` bot+bpf-ci 2026-10-26 4:30 ` [PATCH bpf v3 1/2] bpf: Validate stack arg reads against caller state patchwork-bot+netdevbpf 1 sibling, 1 reply; 4+ messages in thread From: Weiming Shi @ 2026-10-26 1:52 UTC (permalink / raw) To: Alexei Starovoitov, Daniel Borkmann, John Fastabend, Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman, Kumar Kartikeya Dwivedi, Song Liu, Yonghong Song, Jiri Olsa, Shuah Khan Cc: bpf, linux-kselftest, linux-kernel, co+eada64ce90ecebce, Xiang Mei, Weiming Shi, Alexei Starovoitov Exercise the path where a static seven-argument subprogram has unreliable BTF and is called through a bridge with only one initialized outgoing stack argument. The callee expects two stack arguments, but the unreliable-BTF path skips check_outgoing_stack_args(). Expect the verifier to reject the second r11 load at offset 16. Also exercise synchronous and asynchronous callbacks whose BTF prototypes declare a sixth argument. Initialize the outgoing slot at each helper call site so the programs reach the callback-specific rejection. Keep a valid six-argument direct call in each program so bpf_fixup_call_args() records the stack-argument requirement and cannot become an unrelated rejection fallback. The fix also rejects the existing uninitialized-stack-argument test before the later unreadable-register check. Update that test's expected diagnostic accordingly. Suggested-by: Alexei Starovoitov <alexei.starovoitov@gmail.com> Assisted-by: LLM Signed-off-by: Weiming Shi <bestswngs@gmail.com> --- Changes in v3: - Rebase onto the bpf tree at commit 00d2cdf1637e ("Merge branch 'bpf-track-borrowed-iterator-result-lifetimes'"). Changes in v2: - Add synchronous bpf_loop callback coverage. - Add asynchronous bpf_timer callback coverage. - Add the six-argument callback BTF prototype and initialize the outgoing stack slot so both tests reach the callback-specific rejection. .../bpf/progs/btf__verifier_stack_arg_order.c | 10 ++ .../bpf/progs/verifier_stack_arg_order.c | 121 +++++++++++++++++- 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/bpf/progs/btf__verifier_stack_arg_order.c b/tools/testing/selftests/bpf/progs/btf__verifier_stack_arg_order.c index a980a3a399d3fe..f4fb8f81afa777 100644 --- a/tools/testing/selftests/bpf/progs/btf__verifier_stack_arg_order.c +++ b/tools/testing/selftests/bpf/progs/btf__verifier_stack_arg_order.c @@ -26,6 +26,11 @@ void subprog_bad_ptr_7args(long *a, int b, int c, int d, int e, int f, int g) { } +int callback_6args(long a, long b, long c, long d, long e, long f) +{ + return a + b + c + d + e + f; +} + #else int subprog_bad_order_6args(void) @@ -47,4 +52,9 @@ void subprog_bad_ptr_7args(void) { } +int callback_6args(void) +{ + return 0; +} + #endif diff --git a/tools/testing/selftests/bpf/progs/verifier_stack_arg_order.c b/tools/testing/selftests/bpf/progs/verifier_stack_arg_order.c index ab195585223343..5bfcd300c7a09a 100644 --- a/tools/testing/selftests/bpf/progs/verifier_stack_arg_order.c +++ b/tools/testing/selftests/bpf/progs/verifier_stack_arg_order.c @@ -5,6 +5,17 @@ #include <bpf/bpf_helpers.h> #include "bpf_misc.h" +struct timer_value { + struct bpf_timer timer; +}; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 1); + __type(key, int); + __type(value, struct timer_value); +} timer_map SEC(".maps"); + #if (defined(__TARGET_ARCH_x86) || defined(__TARGET_ARCH_arm64) || \ (defined(__TARGET_ARCH_riscv) && __riscv_xlen == 64)) && \ defined(__BPF_FEATURE_STACK_ARGUMENT) @@ -130,6 +141,25 @@ static void subprog_bad_ptr_7args(long *a, int b, int c, int d, int e, int f, in ); } +__noinline __used __naked +static int callback_6args(long a, long b, long c, long d, long e, long f) +{ + asm volatile ("r0 = *(u64 *)(r11 + 8);" + "exit;" + ::: __clobber_all + ); +} + +__noinline __used __naked +static void subprog_call_bad_ptr_7args(void) +{ + asm volatile ("*(u64 *)(r11 - 8) = 0;" + "call subprog_bad_ptr_7args;" + "exit;" + ::: __clobber_all + ); +} + SEC("tc") __description("stack_arg: read without caller write") __failure @@ -152,7 +182,7 @@ __naked void stack_arg_read_without_write_1(void) SEC("tc") __description("stack_arg: read with not-initialized caller write") __failure -__msg("R0 !read_ok") +__msg("invalid read from uninitialized stack arg off 8") __btf_func_path("btf__verifier_stack_arg_order.bpf.o") __naked void stack_arg_read_without_write_2(void) { @@ -171,6 +201,95 @@ __naked void stack_arg_read_without_write_2(void) ); } +SEC("tc") +__description("stack_arg: unreliable callee with too few stack args") +__failure +__msg("invalid read from uninitialized stack arg off 16") +__btf_func_path("btf__verifier_stack_arg_order.bpf.o") +__naked void stack_arg_read_with_short_bridge_write(void) +{ + asm volatile ("r1 = 0;" + "r2 = 0;" + "r3 = 0;" + "r4 = 0;" + "r5 = 0;" + "*(u64 *)(r11 - 8) = 0;" + "*(u64 *)(r11 - 16) = 0;" + "call subprog_bad_ptr_7args;" + "call subprog_call_bad_ptr_7args;" + "exit;" + ::: __clobber_all + ); +} + +SEC("tc") +__description("stack_arg: synchronous callback with stack args") +__failure +__msg("callback subprog cannot have stack args") +__btf_func_path("btf__verifier_stack_arg_order.bpf.o") +__naked void stack_arg_sync_callback(void) +{ + asm volatile ("r6 = 0;" + "*(u64 *)(r11 - 8) = r6;" + "r1 = 1;" + "r2 = %[callback_6args];" + "r3 = 0;" + "r4 = 0;" + "call %[bpf_loop];" + "r1 = 1;" + "r2 = 2;" + "r3 = 3;" + "r4 = 4;" + "r5 = 5;" + "*(u64 *)(r11 - 8) = 6;" + "call callback_6args;" + "r0 = 0;" + "exit;" + : + : __imm_ptr(callback_6args), + __imm(bpf_loop) + : __clobber_common, "r6" + ); +} + +SEC("tc") +__description("stack_arg: asynchronous callback with stack args") +__failure +__msg("callback subprog cannot have stack args") +__btf_func_path("btf__verifier_stack_arg_order.bpf.o") +__naked void stack_arg_async_callback(void) +{ + asm volatile ("r1 = 0;" + "*(u32 *)(r10 - 4) = r1;" + "r1 = %[timer_map] ll;" + "r2 = r10;" + "r2 += -4;" + "call %[bpf_map_lookup_elem];" + "if r0 == 0 goto l0_%=;" + "r6 = r0;" + "r7 = 0;" + "*(u64 *)(r11 - 8) = r7;" + "r1 = r6;" + "r2 = %[callback_6args];" + "call %[bpf_timer_set_callback];" + "r1 = 1;" + "r2 = 2;" + "r3 = 3;" + "r4 = 4;" + "r5 = 5;" + "*(u64 *)(r11 - 8) = 6;" + "call callback_6args;" + "l0_%=: r0 = 0;" + "exit;" + : + : __imm_addr(timer_map), + __imm_ptr(callback_6args), + __imm(bpf_map_lookup_elem), + __imm(bpf_timer_set_callback) + : __clobber_common, "r6", "r7" + ); +} + #else SEC("socket") -- 2.56.0 ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH bpf v3 2/2] selftests/bpf: Test missing and callback stack arguments 2026-10-26 1:52 ` [PATCH bpf v3 2/2] selftests/bpf: Test missing and callback stack arguments Weiming Shi @ 2026-10-26 2:28 ` bot+bpf-ci 0 siblings, 0 replies; 4+ messages in thread From: bot+bpf-ci @ 2026-10-26 2:28 UTC (permalink / raw) To: bestswngs, ast, daniel, john.fastabend, andrii, martin.lau, eddyz87, memxor, song, yonghong.song, jolsa, shuah Cc: bpf, linux-kselftest, linux-kernel, co+eada64ce90ecebce, xmei5, bestswngs, alexei.starovoitov, ast, andrii, daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai [-- Attachment #1: Type: text/plain, Size: 3239 bytes --] > selftests/bpf: Test missing and callback stack arguments TL;DR: Commit message only: the stated reason for the r11 stores before helper calls in the callback selftests looks inaccurate; the tests themselves look correct. Asks for the real reason. > selftests/bpf: Test missing and callback stack arguments > > Exercise the path where a static seven-argument subprogram has unreliable > BTF and is called through a bridge with only one initialized outgoing stack > argument. The callee expects two stack arguments, but the unreliable-BTF > path skips check_outgoing_stack_args(). Expect the verifier to reject the > second r11 load at offset 16. > > Also exercise synchronous and asynchronous callbacks whose BTF prototypes > declare a sixth argument. Initialize the outgoing slot at each helper call > site so the programs reach the callback-specific rejection. Keep a valid > six-argument direct call in each program so bpf_fixup_call_args() records > the stack-argument requirement and cannot become an unrelated rejection > fallback. > > The fix also rejects the existing uninitialized-stack-argument test before > the later unreadable-register check. Update that test's expected diagnostic > accordingly. > > Suggested-by: Alexei Starovoitov <alexei.starovoitov@gmail.com> > Assisted-by: LLM > Signed-off-by: Weiming Shi <bestswngs@gmail.com> This isn't a bug, but is the stated reason for the r11 stores before the helper calls accurate? The commit message says: Initialize the outgoing slot at each helper call site so the programs reach the callback-specific rejection. push_callback_call() calls btf_check_subprog_call() -> btf_check_func_arg_match() before the new "callback subprog cannot have stack args" check. callback_6args() is static and its cached arg_slot_cnt is 6, so btf_check_func_arg_match() first runs: for (i = 0; i < min_t(u32, sub->arg_slot_cnt, MAX_BPF_FUNC_REG_ARGS); i++) if (caller->regs[BPF_REG_1 + i].type == NOT_INIT) return -EINVAL; and returns -EINVAL before check_func_args() -> check_outgoing_stack_args() gets a chance to return -EFAULT. In stack_arg_sync_callback(), r5 is never written before "call %[bpf_loop]". In stack_arg_async_callback(), r3-r5 are cleared by the earlier bpf_map_lookup_elem() call. push_callback_call() ignores that -EINVAL (only -EFAULT is fatal) and goes on to reject on bpf_in_stack_arg_cnt(), with or without the stores. The stores only change what an unfixed kernel does. In the sync case they keep caller->stack_arg_regs non-NULL, so the old check_stack_arg_read() reads a real slot. In the async case they change nothing, because the old code reads frame[curframe - 1] with curframe == 0. The tests themselves look correct, and the expected messages match the fixed verifier on every explored path. Could the changelog give the real reason for the stores, or are they needed on some path not covered here? --- 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/40257528533 ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH bpf v3 1/2] bpf: Validate stack arg reads against caller state 2026-10-26 1:52 [PATCH bpf v3 1/2] bpf: Validate stack arg reads against caller state Weiming Shi 2026-10-26 1:52 ` [PATCH bpf v3 2/2] selftests/bpf: Test missing and callback stack arguments Weiming Shi @ 2026-10-26 4:30 ` patchwork-bot+netdevbpf 1 sibling, 0 replies; 4+ messages in thread From: patchwork-bot+netdevbpf @ 2026-10-26 4:30 UTC (permalink / raw) To: Weiming Shi Cc: ast, daniel, john.fastabend, andrii, martin.lau, eddyz87, memxor, song, yonghong.song, jolsa, shuah, bpf, linux-kselftest, linux-kernel, co+eada64ce90ecebce, xmei5, alexei.starovoitov Hello: This series was applied to bpf/bpf.git (master) by Alexei Starovoitov <ast@kernel.org>: On Sun, 25 Oct 2026 18:52:49 -0700 you wrote: > check_stack_arg_read() bounds a stack argument index with the callee's > BTF-derived argument count, then uses the index in the caller's > stack_arg_regs[] array. The array's actual logical length is recorded in > caller->out_stack_arg_cnt, so the bound and indexed object can diverge. > > A crafted BPF_PROG_LOAD can use an argument mismatch to mark a static > subprogram's BTF information unreliable. Later calls return from > btf_check_subprog_call() before check_outgoing_stack_args(), while > check_func_call() continues the static call on -EINVAL. The callee can > then read a slot that the caller never allocated or initialized. The > earlier commit 4286f5deee14 ("bpf: Validate outgoing stack args when > btf_prepare_func_args fails") closed this hole for the first call, but > not for later calls after the subprogram has been marked unreliable. > > [...] Here is the summary with links: - [bpf,v3,1/2] bpf: Validate stack arg reads against caller state https://git.kernel.org/bpf/bpf/c/90aedf8854c0 - [bpf,v3,2/2] selftests/bpf: Test missing and callback stack arguments https://git.kernel.org/bpf/bpf/c/195a3519ff27 You are awesome, thank you! -- Deet-doot-dot, I am a bot. https://korg.docs.kernel.org/patchwork/pwbot.html ^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-26 4:30 UTC | newest] Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-10-26 1:52 [PATCH bpf v3 1/2] bpf: Validate stack arg reads against caller state Weiming Shi 2026-10-26 1:52 ` [PATCH bpf v3 2/2] selftests/bpf: Test missing and callback stack arguments Weiming Shi 2026-10-26 2:28 ` bot+bpf-ci 2026-10-26 4:30 ` [PATCH bpf v3 1/2] bpf: Validate stack arg reads against caller state patchwork-bot+netdevbpf
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®