Skip to contentSkip to conversation

Unwinding for synchronous callbacks

RFC PATCH In review Kumar Kartikeya Dwivedi 5 participants 46 messages Oct 28 – Oct 29

Subsystems (10)
x86 Kselftest BPF arm64 +6 more subsystems
lore
Share & tools
lore

Kumar Kartikeya Dwivedi

author maintainer
#0 lore raw

cover Unwinding for synchronous callbacks

A callback that a helper calls cannot unwind: the helper is a C frame in between, and it holds things only its epilogue drops.

Code that unwinds due to panics or exceptions doesn't often control where they can be invoked, and reusing the same code in callbacks would lead to verifier rejections since unwinding across such synchronous callbacks is not supported.

With this series, we allow a helper gets a pad of its own. A new macro bpf_call_cb() calls the callback from an asm goto with an extable entry at the return address that points to a label in the helper. unwind_exit of the callback lands there, the helper drops what it holds, and bpf_unwind_leave() sends it on to the pad after its own call, with the r0 the callback unwound with. The normal path pays nothing.

To the verifier such a call is a call of something that can unwind:
unwind_goto has to follow it, and the pad gets the r0 of the callback with its state. bpf_loop(), bpf_for_each_map_elem(), bpf_find_vma(), bpf_user_ringbuf_drain() and the comparator of bpf_rbtree_add() are opened up one by one. A callback may unwind with what it may store into callback_ctx already, and the fix for the pointers that don't outlive the helper [1] will apply to r0 as well.

Tested on x86-64 with clang, with and without kCFI, and gcc for the objects. The arm64 objects compile, but the arm64 run is left to CI.

[1]: https://lore.kernel.org/bpf/[email protected]/

Kumar Kartikeya Dwivedi (11):
bpf: Give a helper a way to call its callback with a pad bpf, arm64: Let the pad after a helper call have r0
bpf: Let a callback unwind into the pad of its helper
bpf: Give the inlined bpf_loop a pad
bpf: Let the callback of bpf_loop unwind
bpf: Let the callback of bpf_for_each_map_elem unwind
bpf: Let the callback of bpf_find_vma unwind
bpf: Let the callback of bpf_user_ringbuf_drain unwind
bpf: Let the comparator of bpf_rbtree_add unwind selftests/bpf: Test callbacks that unwind selftests/bpf: Test what a callback unwinds with

 arch/arm64/net/bpf_jit_comp.c                 |  129 +-
 arch/x86/net/bpf_jit_comp.c                   |   25 +-
 include/linux/bpf_unwind.h                    |  282 +++++
 include/linux/bpf_verifier.h                  |    6 +-
 include/linux/filter.h                        |    1 +
 kernel/bpf/arraymap.c                         |   11 +-
 kernel/bpf/backtrack.c                        |   21 +-
 kernel/bpf/bpf_iter.c                         |   32 +-
 kernel/bpf/core.c                             |   10 +
 kernel/bpf/fixups.c                           |   43 +-
 kernel/bpf/hashtab.c                          |   26 +-
 kernel/bpf/helpers.c                          |   50 +-
 kernel/bpf/liveness.c                         |    7 +
 kernel/bpf/ringbuf.c                          |   23 +-
 kernel/bpf/task_iter.c                        |   21 +-
 kernel/bpf/verifier.c                         |  162 ++-
 .../selftests/bpf/prog_tests/unwind_drain.c   |   71 ++
 .../selftests/bpf/prog_tests/verifier.c       |    2 +
 .../selftests/bpf/progs/unwind_drain.c        |   69 ++
 .../selftests/bpf/progs/verifier_unwind.c     |    3 +-
 .../selftests/bpf/progs/verifier_unwind_cb.c  | 1064 +++++++++++++++++
 21 files changed, 1974 insertions(+), 84 deletions(-)
 create mode 100644 include/linux/bpf_unwind.h
 create mode 100644 tools/testing/selftests/bpf/prog_tests/unwind_drain.c
 create mode 100644 tools/testing/selftests/bpf/progs/unwind_drain.c
 create mode 100644 tools/testing/selftests/bpf/progs/verifier_unwind_cb.c

base-commit: 4169c9c90532df9585c0a2ae513337a58f5e59ab

Signature · 1 line
2.53.0
author maintainer
#1 lore raw

RFC PATCH 1/11 bpf: Give a helper a way to call its callback with a pad

A callback that bpf_loop() or another helper calls cannot unwind. The helper is a C frame between the callback and the BPF code that called the
helper: unwind_exit of the callback finds no pad at its return address and returns, the helper reads r0 and goes on, and the exception is lost in the middle of the loop. The callback cannot skip the frame either, since the helper holds things that only its epilogue drops: the mmap lock in bpf_find_vma(), the RCU section of the bucket in the hash iterators, the busy bit and the sample in bpf_user_ringbuf_drain().

Let the helper have a pad of its own. bpf_call_cb() calls the callback from inline asm with an extable entry at the return address that points to a label in the helper, and asm goto tells the compiler that control may leave for it. unwind_exit of the callback finds the entry and lands on the label, with the callee-saved registers and the stack as after a return. There the helper stops, drops what it holds, and leaves the way a BPF frame leaves at unwind_exit: bpf_unwind_leave() makes its return address the pad after the call of the helper, which the verifier insists on. Nothing is signalled anywhere: the callback leaves as any subprog does, and so does the helper.

The return address of the helper sits where the compiler knows it to be, below the canonical frame address, which __builtin_dwarf_cfa() gives as an offset from the stack pointer. So the normal path pays nothing: no stub, no frame set aside, no unwinder. On arm64 the frame record holds it, signed with the stack pointer at entry, which is that same address, so the new one is signed the same way, and written to the shadow call stack as well, which the epilogue prefers, when it is on: a dynamic one stays off where there is pointer authentication. clang has __builtin_dwarf_cfa() stand for the frame pointer on arm64, so the modifier is __builtin_sponentry() there. On x86-64 clang takes the canonical frame address through a frame pointer, so there the helpers get one: push, mov and pop, on a kernel that otherwise does without. That makes the pad part of the frame that the BPF code called, or of one called with a pad in turn: a body that two kfuncs share is inlined into both, not called by them, and a function with a pad is noinline, so that the compiler neither inlines it into another frame nor splits the pad, cold as it is, out of it into a part of its own, as gcc's ipa-split otherwise does. A miss at the return address of the helper is a bug then, and bpf_unwind_leave() warns of it, since the exception would come back as a return.

The call itself is the one the compiler would make, kCFI check included. The check is spelled out byte for byte, with the type id that clang emits for the declaration of __bpf_callback_fn, negated by the assembler, the hash where the JIT and the compiler put it, the trap in .kcfi_traps and the target in r11: apply_fineibt() and handle_cfi_failure() take the site for one of the compiler's. On arm64 it's the ldur, movz, movk, cmp and brk of the compiler, with the type in w17 and the target in x15.

The entries are EX_TYPE_BPF, since only bpf_unwind_target() looks them up, and they are keyed at the return address itself. No fault lands there as long as the insn after bpf_call_cb() carries no fixup of its own, which in the helpers it never does. objtool reads __ex_table as alternatives and wants the key to be an insn, which is why it isn't ret_addr - 1 as in the JITs. bpf_unwind_target() asks the kernel extables after the BPF ones, for entries of that kind only, so that a fixup of some caller's own at the return address of a main prog is not taken for a pad, and is shared with the helpers through filter.h, with a weak default for the arches whose JIT does no unwinding.

The pad leaves with the r0 the callback unwound with, as the pad of a BPF caller does: r0 is an output of the asm, and the outputs of an asm goto are valid on the way to a label as well as on the way out with every compiler that has them and that the kernel takes, see CC_HAS_ASM_GOTO_OUTPUT. On x86-64 that output is rax, the caller's variable. On arm64 it is x0, which only a register variable of the macro can name, so the macro lands at a label of its own first, assigns it there and goes on to the pad of the helper.

The registers the call clobbers are outputs of the asm, so that none of them is an operand. On arm64 x18 is among the clobbers unless it holds the shadow call stack, which the callee keeps and the compiler has reserved. That takes asm goto with outputs. Without it, or on an arch other than x86-64 and arm64, bpf_call_cb() is the plain call and BPF_HAS_CB_PADS is 0:
the verifier keeps refusing a callback that unwinds.

Nothing uses this yet. The verifier learns next what a callback that unwinds into such a pad means, and the helpers follow one by one, each opened as it gets its pad.

---
 arch/arm64/net/bpf_jit_comp.c |  32 +++-
 arch/x86/net/bpf_jit_comp.c   |  25 ++-
 include/linux/bpf_unwind.h    | 282 ++++++++++++++++++++++++++++++++++
 include/linux/filter.h        |   1 +
 kernel/bpf/core.c             |  10 ++
 5 files changed, 340 insertions(+), 10 deletions(-)
 create mode 100644 include/linux/bpf_unwind.h
arch/arm64/net/bpf_jit_comp.c +27 -5
index aaa34c739004..b8fb49b00063 100644
@@ -1266,16 +1266,38 @@ static int add_unwind_exentry(struct jit_ctx *ctx, int pad)
 	return 0;
 }
 
-static long bpf_unwind_fixup(u64 lr)
+/*
+ * The verifier made sure that unwind_goto follows a call in a prog. When the
+ * kernel called an async callback, it's exit. A helper that calls a callback
+ * has a pad when it wants one, see bpf_call_cb().
+ */
+u64 bpf_unwind_target(u64 ret_addr)
 {
 	const struct exception_table_entry *e;
 
-	e = search_bpf_extables(ptrauth_strip_kernel_insn_pac(lr) - 1);
+	e = search_bpf_extables(ret_addr - 1);
+	if (e)
+		return ret_addr + e->fixup;
 	/*
-	 * The verifier made sure that unwind_goto follows a call in a prog. When
-	 * the kernel called an async callback, it's exit.
+	 * The pad of a helper that called a callback with bpf_call_cb(): its
+	 * entries are the EX_TYPE_BPF ones of the kernel and module tables.
+	 * Anything else at a return address is some caller's own fixup, and
+	 * what a JIT keyed there is a fixup of the insn after the call.
 	 */
-	return e ? e->fixup : 0;
+	e = search_kernel_exception_table(ret_addr);
+	if (!e)
+		e = search_module_extables(ret_addr);
+	if (e && e->type == EX_TYPE_BPF)
+		return (unsigned long)&e->fixup + e->fixup;
+	return ret_addr;
+}
+
+/* The offset that the epilogue adds to LR once it is authenticated */
+static long bpf_unwind_fixup(u64 lr)
+{
+	u64 ret_addr = ptrauth_strip_kernel_insn_pac(lr);
+
+	return bpf_unwind_target(ret_addr) - ret_addr;
 }
 
 /*
arch/x86/net/bpf_jit_comp.c +20 -5
index 6a2fe3d0039d..a092a84157a9 100644
@@ -2546,15 +2546,30 @@ static int add_unwind_exentry(struct bpf_prog *prog, u8 *image, u8 *rw_image, in
 	return 0;
 }
 
-static u64 bpf_unwind_target(u64 ret_addr)
+/*
+ * The verifier made sure that unwind_goto follows a call in a prog. When the
+ * kernel called an async callback, it's exit. A helper that calls a callback
+ * has a pad when it wants one, see bpf_call_cb().
+ */
+u64 bpf_unwind_target(u64 ret_addr)
 {
-	const struct exception_table_entry *e = search_bpf_extables(ret_addr - 1);
+	const struct exception_table_entry *e;
 
+	e = search_bpf_extables(ret_addr - 1);
+	if (e)
+		return ret_addr + e->fixup;
 	/*
-	 * The verifier made sure that unwind_goto follows a call in a prog. When
-	 * the kernel called an async callback, it's exit.
+	 * The pad of a helper that called a callback with bpf_call_cb(): its
+	 * entries are the EX_TYPE_BPF ones of the kernel and module tables.
+	 * Anything else at a return address is some caller's own fixup, and
+	 * what a JIT keyed there is a fixup of the insn after the call.
 	 */
-	return e ? ret_addr + e->fixup : ret_addr;
+	e = search_kernel_exception_table(ret_addr);
+	if (!e)
+		e = search_module_extables(ret_addr);
+	if (e && (e->data & EX_DATA_TYPE_MASK) == EX_TYPE_BPF)
+		return (unsigned long)&e->fixup + e->fixup;
+	return ret_addr;
 }
 
 /* unwind_exit: fix the return address of this frame. r0 and r2 are returned. */
include/linux/bpf_unwind.h new file +282 -0

282 lines not on this page · raw message

include/linux/filter.h +1 -0
index e0218e677a1d..9f670b3d8375 100644
@@ -1281,6 +1281,7 @@ bool bpf_jit_supports_kfunc_call(void);
 bool bpf_jit_supports_callx(void);
 bool bpf_jit_supports_kfunc_ret_reg_pair(void);
 bool bpf_jit_supports_unwind(void);
+u64 bpf_unwind_target(u64 ret_addr);
 bool bpf_jit_supports_stack_args(void);
 bool bpf_jit_supports_arena_args(void);
 bool bpf_jit_supports_far_kfunc_call(void);
kernel/bpf/core.c +10 -0
index 3030beb531d9..1096b2adc695 100644
@@ -3405,6 +3405,16 @@ bool __weak bpf_jit_supports_unwind(void)
 	return false;
 }
 
+/*
+ * Where unwind_exit returns: to the pad of the caller when the caller has one,
+ * and where exit would otherwise. The JITs that support unwinding have the real
+ * one. Without it no callback unwinds, so a helper never asks.
+ */
+u64 __weak bpf_unwind_target(u64 ret_addr)
+{
+	return ret_addr;
+}
+
 /*
  * How this arch places a by-value kfunc argument, or NULL for one that has
  * not opted in and so only takes an argument of a single eightbyte, which
Signature · 1 line
2.53.0
#2 lore raw

Re: [RFC PATCH bpf-next v1 01/11] bpf: Give a helper a way to call its callback with a pad

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] bpf: clobber of local register variables during arg evaluation in __bpf_call_cb() - [High] bpf…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] bpf: clobber of local register variables during arg evaluation in __bpf_call_cb()
- [High] bpf: dead-store elimination of return address write in bpf_unwind_leave() --

commit d437def6fc298113c802447cf3c5d5281b1f3dd3
Author: Kumar Kartikeya Dwivedi <[email protected]>

bpf: Give a helper a way to call its callback with a pad

This commit introduces bpf_call_cb() and bpf_unwind_leave() in include/linux/bpf_unwind.h, along with bpf_unwind_target() in the x86-64 and arm64 JITs, so C helpers invoking BPF callbacks can catch callback unwinds via an EX_TYPE_BPF exception table entry and redirect their own return address to the caller's unwind pad.

Quoted 6 lines
include/linux/bpf_unwind.h new file +0 -0
new file mode 100644
index 0000000000000..190d6dbae8568
@@ -0,0 +1,282 @@

[ ... ]

Quoted 34 lines
include/linux/bpf_unwind.h new file +34 -0
+#define __bpf_call_cb(type, fn, a1, a2, a3, a4, a5, r0, unwound)		\
+do {										\
+	register u64 __fn asm("r11") = (u64)(long)(fn);				\
+	register u64 __a5 asm("r8") = (u64)(long)(a5);				\
+	register u64 __c5 asm("r8");						\
+	register u64 __c11 asm("r11");						\
+	u64 __c1, __c2, __c3, __c4;						\
+										\
+	asm_goto_output(BPF_KCFI_CHECK(type)					\
+		 "	" CALL_NOSPEC "\n"					\
+		 "1:\n"								\
+		 _ASM_EXTABLE_TYPE(1b, %l[unwound], EX_TYPE_BPF)		\
+		 : "=a"(r0), "=D"(__c1), "=S"(__c2), "=d"(__c3), "=c"(__c4),	\
+		   "=r"(__c5), "=r"(__c11)					\
+		 : [thunk_target] "r"(__fn), "D"((u64)(long)(a1)),		\
+		   "S"((u64)(long)(a2)), "d"((u64)(long)(a3)),			\
+		   "c"((u64)(long)(a4)), "r"(__a5)				\
+		 : "r9", "r10", "memory", "cc"					\
+		 : unwound);							\
+} while (0)
+
+/*
+ * The return address of the helper is the word below its canonical frame
+ * address. The pad is in the frame that BPF code called, see above, so there is
+ * a pad to go to: a miss is a bug, and the exception would be lost in a return.
+ */
+static __always_inline void bpf_unwind_leave(void)
+{
+	unsigned long *slot = (unsigned long *)__builtin_dwarf_cfa() - 1;
+	unsigned long to = bpf_unwind_target(*slot);
+
+	WARN_ON_ONCE(to == *slot);
+	*slot = to;
+}

[ ... ]

include/linux/bpf_unwind.h line 184

Quoted 11 lines
include/linux/bpf_unwind.h new file +11 -0
+#define __bpf_call_cb(type, fn, a1, a2, a3, a4, a5, r0, unwound)		\
+	___bpf_call_cb(__UNIQUE_ID(unwound), type, fn, a1, a2, a3, a4, a5, r0, unwound)
+
+#define ___bpf_call_cb(lbl, type, fn, a1, a2, a3, a4, a5, r0, unwound)		\
+do {										\
+	register u64 __fn asm("x15") = (u64)(long)(fn);				\
+	register u64 __a0 asm("x0") = (u64)(long)(a1);				\
+	register u64 __a1 asm("x1") = (u64)(long)(a2);				\
+	register u64 __a2 asm("x2") = (u64)(long)(a3);				\
+	register u64 __a3 asm("x3") = (u64)(long)(a4);				\
+	register u64 __a4 asm("x4") = (u64)(long)(a5);				\

[Severity: High]
Can evaluating the macro arguments in ___bpf_call_cb() on arm64 and __bpf_call_cb() on x86-64 clobber the caller-saved local register variables before the asm_goto_output() statement is reached?

On arm64, ___bpf_call_cb() in include/linux/bpf_unwind.h initializes x15 and x0..x4 directly from the macro expressions in sequence:

	register u64 __fn asm("x15") = (u64)(long)(fn);
	register u64 __a0 asm("x0") = (u64)(long)(a1);
	register u64 __a1 asm("x1") = (u64)(long)(a2);
	register u64 __a2 asm("x2") = (u64)(long)(a3);
	register u64 __a3 asm("x3") = (u64)(long)(a4);
	register u64 __a4 asm("x4") = (u64)(long)(a5);

And on x86-64, __bpf_call_cb() assigns __fn (r11) and __a5 (r8) before evaluating a1..a4 in the asm_goto_output() input operands:

	register u64 __fn asm("r11") = (u64)(long)(fn);
	register u64 __a5 asm("r8") = (u64)(long)(a5);
	...
		 : [thunk_target] "r"(__fn), "D"((u64)(long)(a1)),
		   "S"((u64)(long)(a2)), "d"((u64)(long)(a3)),
		   "c"((u64)(long)(a4)), "r"(__a5)

Per GCC and Clang local register variable rules, local register variables do not reserve caller-saved registers across function calls that occur between their assignment and the extended asm statement.

Later in this series, commit "bpf: Let the callback of bpf_for_each_map_elem unwind" updates bpf_each_rhash_elem() in kernel/bpf/hashtab.c to pass a helper call and pointer dereferences as macro arguments:

	bpf_call_cb(callback_fn, map, elem->data,
		    rhtab_elem_value(elem, map->key_size), callback_ctx, 0,
		    ret, unwound);

If evaluating a3 or another argument invokes a function call -- such as an out-of-line call to rhtab_elem_value() (which is marked static inline rather than __always_inline) or sanitizer instrumentation like __asan_load4() under outline KASAN or __tsan_read4() under KCSAN -- that call executes after __fn (x15 / r11) and earlier argument registers (x0, x1 / r8) have already been populated, clobbering the callback target and arguments prior to blr x15 or CALL_NOSPEC.

Should fn and a1..a5 be evaluated into ordinary C temporary variables first before assigning them to the local register variables, similar to how include/linux/arm-smccc.h stages register arguments?

Quoted 6 lines
include/linux/bpf_unwind.h new file +6 -0
+	register u64 __c0 asm("x0");						\
+	register u64 __c1 asm("x1");						\
+	register u64 __c2 asm("x2");						\
+	register u64 __c3 asm("x3");						\
+	register u64 __c4 asm("x4");						\
+	register u64 __c15 asm("x15");						\

[ ... ]

Quoted 20 lines
include/linux/bpf_unwind.h new file +20 -0
+static __always_inline void bpf_unwind_leave(void)
+{
+	unsigned long *slot = (unsigned long *)__builtin_frame_address(0) + 1;
+	unsigned long from = ptrauth_strip_kernel_insn_pac(*slot);
+	unsigned long to = bpf_unwind_target(from);
+
+	WARN_ON_ONCE(to == from);
+#ifdef CONFIG_ARM64_PTR_AUTH_KERNEL
+	{
+		register unsigned long x17 asm("x17") = to;
+#ifdef __clang__
+		register unsigned long x16 asm("x16") = (unsigned long)__builtin_sponentry();
+#else
+		register unsigned long x16 asm("x16") = (unsigned long)__builtin_dwarf_cfa();
+#endif
+
+		asm("pacia1716" : "+r"(x17) : "r"(x16));
+		to = x17;
+	}
+#endif

include/linux/bpf_unwind.h line 253

Quoted 10 lines
include/linux/bpf_unwind.h new file +10 -0
+	*slot = to;
+#ifdef CONFIG_SHADOW_CALL_STACK
+	if (scs_is_enabled()) {
+		unsigned long *scs;
+
+		asm("mov %0, x18" : "=r"(scs));
+		scs[-1] = to;
+	}
+#endif
+}

[Severity: High]
Could the compiler eliminate the *slot = to store in bpf_unwind_leave() (on both x86-64 and arm64) as a dead store to the current function's stack frame right before returning?

In include/linux/bpf_unwind.h, both implementations of bpf_unwind_leave() compute slot from __builtin_dwarf_cfa() or __builtin_frame_address(0) as a non-volatile unsigned long * and perform a plain store:

	*slot = to;

Because bpf_unwind_leave() is __always_inline and runs at the end of the helper's unwound path immediately before return, nothing in C reads *slot before the function exits. In GCC's RTL dead-store elimination pass (pass_rtl_dse1, which runs before prologue/epilogue threading), non-volatile frame-related stores based on arg_pointer_rtx (__builtin_dwarf_cfa()) or hard_frame_pointer_rtx (__builtin_frame_address(0)) that are not read before EXIT_BLOCK are candidates for elimination. If *slot = to is elided, the helper returns to its normal call site instead of the caller's unwind pad, losing the BPF exception after the helper has already released its resources.

Existing kernel code that overwrites the stack return address via __builtin_frame_address(0) in drivers/misc/lkdtm/cfi.c:set_return_addr() explicitly marks the store volatile for this reason:

	/* Use of volatile is to make sure final write isn't seen as a dead store. */
	unsigned long * volatile *ret_addr =
		(unsigned long **)__builtin_frame_address(0) + FRAME_RA_OFFSET;

	/* Make sure we've found the right place on the stack before writing it. */
	if (no_pac_addr(*ret_addr) == expected)
		*ret_addr = (addr);

Should bpf_unwind_leave() use WRITE_ONCE(*slot, to) (and WRITE_ONCE(scs[-1], to) on arm64) or qualify slot as volatile so the return-address update cannot be optimized away?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
author maintainer
#3 lore raw

Re: [RFC PATCH bpf-next v1 01/11] bpf: Give a helper a way to call its callback with a pad

Quoted 5 lines · sashiko-bot

[...]
[Severity: High]
Can evaluating the macro arguments in ___bpf_call_cb() on arm64 and __bpf_call_cb() on x86-64 clobber the caller-saved local register variables before the asm_goto_output() statement is reached?

Right: the compiler keeps no local register variable across a call, and a call in the evaluation of a later argument clobbers the ones set before it. The next version evaluates every argument into a temporary first and sets the register variables from those.

Quoted 5 lines

[...]
[Severity: High]
Could the compiler eliminate the *slot = to store in bpf_unwind_leave() (on both x86-64 and arm64) as a dead store to the current function's stack frame right before returning?

Right, and gcc 15 does drop it, on arm64 and x86-64 alike: that is why the gcc runs of BPF CI returned from every helper but the array iterator instead of unwinding. The next version stores with WRITE_ONCE(), on the shadow call stack too.

Quoted 1 line

[...]

#4 lore raw

Re: [RFC PATCH bpf-next v1 01/11] bpf: Give a helper a way to call its callback with a pad

In reply to Kumar Kartikeya Dwivedi
TL;DR: - bpf_unwind_leave() may misbehave if the helper is ftrace/kretprobe/fexit traced: unwind lost, pointer or reference treated as scalar - With gcc, bpf_call_cb() register variables may be…
Quoted 1 line

bpf: Give a helper a way to call its callback with a pad

TL;DR:
- bpf_unwind_leave() may misbehave if the helper is ftrace/kretprobe/fexit
traced: unwind lost, pointer or reference treated as scalar
- With gcc, bpf_call_cb() register variables may be clobbered by KASAN/KCSAN calls, possibly jumping to a bad address

Quoted 6 lines
include/linux/bpf_unwind.h new file +0 -0
new file mode 100644
index 000000000000..190d6dbae856
@@ -0,0 +1,282 @@

[ ... ]

Quoted 17 lines
include/linux/bpf_unwind.h new file +17 -0
+/*
+ * The registers the call clobbers are outputs, so that none of them can be an
+ * operand of anything else. r0 is rax, on the way out and on the way to the pad.
+ * The return address stays the byte after the call through what
+ * apply_retpolines() and apply_fineibt() make of the call: they pad in front of
+ * it, never behind it.
+ */
+#define __bpf_call_cb(type, fn, a1, a2, a3, a4, a5, r0, unwound)		\
+do {										\
+	register u64 __fn asm("r11") = (u64)(long)(fn);				\
+	register u64 __a5 asm("r8") = (u64)(long)(a5);				\
+	register u64 __c5 asm("r8");						\
+	register u64 __c11 asm("r11");						\
+	u64 __c1, __c2, __c3, __c4;						\
+										\
+	asm_goto_output(BPF_KCFI_CHECK(type)					\
+		 "	" CALL_NOSPEC "\n"					\

include/linux/bpf_unwind.h line 112

Quoted 10 lines
include/linux/bpf_unwind.h new file +10 -0
+		 "1:\n"								\
+		 _ASM_EXTABLE_TYPE(1b, %l[unwound], EX_TYPE_BPF)		\
+		 : "=a"(r0), "=D"(__c1), "=S"(__c2), "=d"(__c3), "=c"(__c4),	\
+		   "=r"(__c5), "=r"(__c11)					\
+		 : [thunk_target] "r"(__fn), "D"((u64)(long)(a1)),		\
+		   "S"((u64)(long)(a2)), "d"((u64)(long)(a3)),			\
+		   "c"((u64)(long)(a4)), "r"(__a5)				\
+		 : "r9", "r10", "memory", "cc"					\
+		 : unwound);							\
+} while (0)

Can the argument evaluation here clobber the register variables?

__fn is bound to r11 and __a5 to r8 before a1 through a4 are evaluated as asm inputs. The arm64 version has the same shape, binding x15 and x0 through x4 one after another before later arguments are evaluated.

With gcc, a register variable is only guaranteed to hold its register at the asm statement itself. If evaluating a later macro argument involves a call, the earlier binding is not kept across it.

A later patch in this series (7a687425732b) calls:

    bpf_call_cb(callback_fn, map, elem->data,
                rhtab_elem_value(elem, map->key_size), callback_ctx, 0,
                ret, unwound);

The map->key_size load becomes a call to __tsan_read4 with CONFIG_KCSAN, or to __asan_load4_noabort with CONFIG_KASAN_OUTLINE, and kernel/bpf is not excluded from either. That call is placed after the register variables have been set.

On arm64 the call's own argument goes in x0, so the callback could get a garbage map pointer, and x15 (the call target) can be clobbered as well. On x86 r11 and r8 are call-clobbered, so the call __x86_indirect_thunk_r11 that follows could jump to a garbage address.

clang ties register variables only at the asm itself, so this would only affect gcc builds.

Would it be safer to evaluate fn and a1 through a5 into ordinary temporaries first, for example typeof(a) __t = a;, and only then declare the register variables, the way the arm-smccc.h macros do?

Quoted 4 lines
include/linux/bpf_unwind.h new file +4 -0
+
+/*
+ * The return address of the helper is the word below its canonical frame
+ * address. The pad is in the frame that BPF code called, see above, so there is

include/linux/bpf_unwind.h line 126

Quoted 10 lines
include/linux/bpf_unwind.h new file +10 -0
+ * a pad to go to: a miss is a bug, and the exception would be lost in a return.
+ */
+static __always_inline void bpf_unwind_leave(void)
+{
+	unsigned long *slot = (unsigned long *)__builtin_dwarf_cfa() - 1;
+	unsigned long to = bpf_unwind_target(*slot);
+
+	WARN_ON_ONCE(to == *slot);
+	*slot = to;
+}

Can *slot hold something other than the address in the bpf program that called the helper?

bpf_unwind_leave() assumes that it does, and the comment says a miss is a bug. While the helper is traced, though, the slot can be replaced:

- function_graph: prepare_ftrace_return()/ftrace_graph_func() put return_to_handler in *parent
- kretprobe/rethook: arch_rethook_trampoline is put there
- a bpf fexit/fmod_ret trampoline calls the original function, so the return address is inside the trampoline

On arm64 the same replacement is made to the link register at entry, and the prologue then saves it into the frame record that bpf_unwind_leave() reads.

kernel/bpf/Makefile removes CC_FLAGS_FTRACE only from percpu_freelist.o, bpf_lru_list.o, queue_stack_maps.o, lpm_trie.o, ringbuf.o and rqspinlock.o. So bpf_loop() (bpf_iter.c), the map iterators (hashtab.c, arraymap.c), bpf_find_vma() (task_iter.c) and bpf_rbtree_add (helpers.c) can all be graph-traced. They can also be kretprobed, and so can bpf_user_ringbuf_drain().

Later commits in this series give these helpers pads (4e61c40fe5c0, 7a687425732b, 59e3a600668d, bee41eec3c39, ecd4c98df2ee) and add no notrace, NOKPROBE or attach restriction.

If the callback unwinds in that state, bpf_unwind_target(return_to_handler) misses in the bpf table and in the kernel table, and returns its argument:

    unsigned long to = bpf_unwind_target(*slot);   /* to == *slot */

    WARN_ON_ONCE(to == *slot);                     /* fires */
    *slot = to;                                    /* unchanged */

The helper then returns through the tracer to the normal continuation after the call instead of to the pad, and the exception is lost after the helper has already stopped and dropped what it holds.

There is also a typing consequence. According to b2f03cf6322f the pad gets the callback's r0 bit for bit, "a pointer stays the pointer it was, with its type and its reference". On the fallthrough path the verifier instead types r0 as the helper's own return (RET_INTEGER, for example). So a pointer or a referenced object the callback unwound with reaches code that treats it as a scalar, and the pad that would have consumed or released it never runs.

bpf_check_attach_target() already refuses the analogous case for bpf
targets: "Only fentry can attach to %s, which can unwind". Is there something equivalent for the C helpers that have a pad?

Would translating the slot with ftrace_graph_ret_addr() and the rethook equivalent (and refusing fexit/fmod_ret on these helpers) cover this? Alternatively, could the helpers with pads be made notrace and NOKPROBE, with tracing attachment to them denied?

---
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/40612543702

author maintainer
#5 lore raw

Re: [RFC PATCH bpf-next v1 01/11] bpf: Give a helper a way to call its callback with a pad

Quoted 4 lines · bot+bpf-ci

[...]
TL;DR:
- bpf_unwind_leave() may misbehave if the helper is ftrace/kretprobe/fexit
traced: unwind lost, pointer or reference treated as scalar

Right. Every function with a pad is notrace and no kprobe target in the next version, as the verifier lets only fentry attach to a subprog that unwinds, with a test that fexit on bpf_loop() is refused.

Quoted 2 lines

- With gcc, bpf_call_cb() register variables may be clobbered by KASAN/KCSAN calls, possibly jumping to a bad address

Right, for a call in the evaluation of a later argument, which the compiler does not keep the register variables across. The next version evaluates every argument into a temporary first.

Quoted 1 line

[...]

author maintainer
#6 lore raw

RFC PATCH 2/11 bpf, arm64: Let the pad after a helper call have r0

In reply to Kumar Kartikeya Dwivedi

The JIT emits 'bl foo; mov x8, x0' for a call, and the extable entry of the unwind_goto that follows sends unwind_exit of the callee past the mov, into the pad. That is fine after the call of a subprog: r0 is x8 in the subprog too, its epilogue copies x8 to x0 and leaves x8 as it is. A helper that leaves to the pad from a pad of its own, see bpf_unwind_leave(), returns r0 in x0 alone, and x8 is whatever it last used it for.

So the pad after the call of a helper or a kfunc is entered through a landing of its own, 'mov x8, x0; b pad', past the epilogue where nothing falls into it, and the entry of the unwind_goto points there. The landings go in after the body, when the pads are placed and the entries are in, and each fills in the fixup of its entry. The normal path has nothing in its way: unwind_goto emits nothing, as before. The pad after the call of a subprog is as it was. x86-64 needs nothing: r0 is rax.

---
 arch/arm64/net/bpf_jit_comp.c | 97 +++++++++++++++++++++++++++++++----
 1 file changed, 86 insertions(+), 11 deletions(-)
arch/arm64/net/bpf_jit_comp.c +86 -11
index b8fb49b00063..284c1dd791ad 100644
@@ -81,6 +81,8 @@ struct jit_ctx {
 	int epilogue_offset;
 	int *offset;
 	int exentry_idx;
+	int *landing_ex;	/* the extable entry of each landing, see build_landings() */
+	int landing_idx;
 	int nr_used_callee_reg;
 	u8 used_callee_reg[8]; /* r6~r9, fp, arena_vm_start */
 	__le32 *image;
@@ -1227,19 +1229,24 @@ static int add_exception_handler(const struct bpf_insn *insn,
 	return 0;
 }
 
+/* unwind_goto after the call of a helper or a kfunc, which return r0 in x0 alone */
+static bool unwind_goto_after_helper(const struct bpf_insn *insn)
+{
+	const struct bpf_insn *call = insn - 1;
+
+	return call->code == (BPF_JMP | BPF_CALL) && call->src_reg != BPF_PSEUDO_CALL;
+}
+
 /*
  * There is no branch for unwind_goto, only an extable entry
- *   { .insn = ret_addr - 1, .fixup = pad - ret_addr }
- * where ret_addr is the return address of the call in front of it. unwind_exit
- * adds .fixup to its LR.
+ *   { .insn = ret_addr - 1, .fixup = target - ret_addr }
+ * where ret_addr is the return address of the call in front of it, and target
+ * the pad, or the way into it. unwind_exit adds .fixup to its LR.
  * ret_addr - 1, because what follows the call can have an entry of its own.
  * No insn starts there, so a fault cannot hit the entry.
- * 'bl foo; mov x8, x0' is in front of unwind_goto. The pad doesn't miss the mov:
- * r0 is x8 and the epilogue of the callee copies it to x0 and leaves x8 as it is.
  */
-static int add_unwind_exentry(struct jit_ctx *ctx, int pad)
+static int add_unwind_exentry(struct jit_ctx *ctx, int ret_addr, int target)
 {
-	int ret_addr = ctx->idx - 1;
 	struct exception_table_entry *ex;
 	off_t ins_offset;
 
@@ -1259,13 +1266,47 @@ static int add_unwind_exentry(struct jit_ctx *ctx, int pad)
 	/* switch ex to rw buffer for writing */
 	ex = (void *)ctx->image + ((void *)ex - (void *)ctx->ro_image);
 	ex->insn = ins_offset;
-	ex->fixup = (ctx->offset[pad] - ret_addr) * AARCH64_INSN_SIZE;
+	ex->fixup = (target - ret_addr) * AARCH64_INSN_SIZE;
 	ex->type = EX_TYPE_BPF;
 
 	ctx->exentry_idx++;
 	return 0;
 }
 
+/*
+ * The landings of the pads after the calls of helpers and kfuncs, past the
+ * epilogue, where nothing falls into them: 'mov x8, x0; b pad' each, see the
+ * unwind_goto case of build_insn(). They go in after the body, when the pads
+ * are placed and the entries are in, and each fills in the fixup of its entry.
+ */
+static int build_landings(struct jit_ctx *ctx)
+{
+	const struct bpf_prog *prog = ctx->prog;
+	int i, k = 0;
+
+	for (i = 1; i < prog->len; i++) {
+		const struct bpf_insn *insn = &prog->insnsi[i];
+		int ret_addr, jmp_offset;
+
+		if (!bpf_is_unwind_goto(insn) || !unwind_goto_after_helper(insn))
+			continue;
+		ret_addr = ctx->offset[i] - 1;
+		if (ctx->image) {
+			struct exception_table_entry *ex;
+
+			ex = &prog->aux->extable[ctx->landing_ex[k]];
+			ex = (void *)ctx->image + ((void *)ex - (void *)ctx->ro_image);
+			ex->fixup = (ctx->idx - ret_addr) * AARCH64_INSN_SIZE;
+		}
+		k++;
+		emit(A64_MOV(1, bpf2a64[BPF_REG_0], A64_R(0)), ctx);
+		jmp_offset = ctx->offset[i + 1 + insn->off] - ctx->idx;
+		check_imm26(jmp_offset);
+		emit(A64_B(jmp_offset), ctx);
+	}
+	return 0;
+}
+
 /*
  * The verifier made sure that unwind_goto follows a call in a prog. When the
  * kernel called an async callback, it's exit. A helper that calls a callback
@@ -1901,10 +1942,27 @@ static int build_insn(const struct bpf_verifier_env *env, const struct bpf_insn
 		break;
 	/* unwind_goto. The verifier expanded may_goto. */
 	case BPF_JMP | BPF_JCOND:
-		ret = add_unwind_exentry(ctx, i + 1 + off);
+	{
+		int ret_addr = ctx->idx - 1;
+		int target = ctx->offset[i + 1 + off];
+
+		/*
+		 * 'bl foo; mov x8, x0' is in front. After the call of a subprog
+		 * the pad doesn't miss the mov: r0 is x8 in the subprog too, its
+		 * epilogue copies x8 to x0 and leaves x8 as it is. A helper that
+		 * leaves to the pad returns r0 in x0 alone, so the pad after its
+		 * call is entered through a landing past the epilogue, see
+		 * build_landings(), which fills in the fixup of the entry.
+		 */
+		if (unwind_goto_after_helper(insn)) {
+			ctx->landing_ex[ctx->landing_idx++] = ctx->exentry_idx;
+			target = ret_addr;
+		}
+		ret = add_unwind_exentry(ctx, ret_addr, target);
 		if (ret)
 			return ret;
 		break;
+	}
 	/* function return */
 	case BPF_JMP | BPF_EXIT:
 		if (bpf_is_unwind_exit(insn) && bpf_prog_can_unwind(ctx->prog)) {
@@ -2342,7 +2400,9 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
 	u8 *ro_image_ptr;
 	int body_idx;
 	int exentry_idx;
+	int nr_landings = 0;
 	int out_cnt;
+	int i;
 
 	if (!prog->jit_requested)
 		return prog;
@@ -2387,6 +2447,16 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
 	if (ctx.offset == NULL)
 		goto out_off;
 
+	for (i = 1; i < prog->len; i++)
+		if (bpf_is_unwind_goto(&prog->insnsi[i]) &&
+		    unwind_goto_after_helper(&prog->insnsi[i]))
+			nr_landings++;
+	if (nr_landings) {
+		ctx.landing_ex = kvzalloc_objs(int, nr_landings);
+		if (ctx.landing_ex == NULL)
+			goto out_off;
+	}
+
 	if (bpf_prog_uses_arena_insns(prog)) {
 		ctx.user_vm_start = bpf_arena_get_user_vm_start(prog->aux->arena);
 		ctx.arena_vm_start = bpf_arena_get_kern_vm_start(prog->aux->arena);
@@ -2416,6 +2486,8 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
 
 	ctx.epilogue_offset = ctx.idx;
 	build_epilogue(&ctx, was_classic, false);
+	if (build_landings(&ctx))
+		goto out_off;
 	build_plt(&ctx);
 
 	extable_align = __alignof__(struct exception_table_entry);
@@ -2449,6 +2521,7 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
 skip_init_ctx:
 	ctx.idx = 0;
 	ctx.exentry_idx = 0;
+	ctx.landing_idx = 0;
 	ctx.write = true;
 
 	build_prologue(&ctx, was_classic);
@@ -2464,6 +2537,7 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
 
 	ctx.epilogue_offset = ctx.idx;
 	ctx.exentry_idx = exentry_idx;
+	ctx.landing_idx = 0;
 	ctx.idx = body_idx;
 	ctx.write = true;
 
@@ -2473,6 +2547,8 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
 		goto out_free_hdr;
 
 	build_epilogue(&ctx, was_classic, false);
+	if (build_landings(&ctx))
+		goto out_free_hdr;
 	build_plt(&ctx);
 
 	/* Extra pass to validate JITed code. */
@@ -2514,8 +2590,6 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
 	prog->jited_len = prog_size - cfi_get_offset();
 
 	if (!prog->is_func || extra_pass) {
-		int i;
-
 		/* offset[prog->len] is the size of program */
 		for (i = 0; i <= prog->len; i++)
 			ctx.offset[i] *= AARCH64_INSN_SIZE;
@@ -2533,6 +2607,7 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
 			prog->aux->priv_stack_ptr = NULL;
 		}
 		kvfree(ctx.offset);
+		kvfree(ctx.landing_ex);
 out_priv_stack:
 		kfree(jit_data);
 		prog->aux->jit_data = NULL;
Signature · 1 line
2.53.0
#7 lore raw

Re: [RFC PATCH bpf-next v1 02/11] bpf, arm64: Let the pad after a helper call have r0

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] bpf, arm64: memory leak of ctx.landing_ex in bpf_jit_free() --

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] bpf, arm64: memory leak of ctx.landing_ex in bpf_jit_free()
--

commit a9bbc452976e17e7771ffe7fb013d30ad6f17aba
Author: Kumar Kartikeya Dwivedi <[email protected]>

bpf, arm64: Let the pad after a helper call have r0

This commit adds landing pads after the epilogue for unwind_goto instructions that follow helper or kfunc calls on arm64, copying the return value from x0 into x8 before branching to the pad. The extable fixup entries for these calls are updated during a post-epilogue pass to point to the generated landings.

Quoted 4 lines
arch/arm64/net/bpf_jit_comp.c +0 -0
index b8fb49b00063c..284c1dd791ad7 100644

[ ... ]

Quoted 13 lines
arch/arm64/net/bpf_jit_comp.c +9 -0
@@ -2387,6 +2447,16 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
 	if (ctx.offset == NULL)
 		goto out_off;
 
+	for (i = 1; i < prog->len; i++)
+		if (bpf_is_unwind_goto(&prog->insnsi[i]) &&
+		    unwind_goto_after_helper(&prog->insnsi[i]))
+			nr_landings++;
+	if (nr_landings) {
+		ctx.landing_ex = kvzalloc_objs(int, nr_landings);
+		if (ctx.landing_ex == NULL)
+			goto out_off;
+	}

[ ... ]

arch/arm64/net/bpf_jit_comp.c struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr · line 2613

Quoted 8 lines
arch/arm64/net/bpf_jit_comp.c +1 -0
@@ -2533,6 +2607,7 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
 			prog->aux->priv_stack_ptr = NULL;
 		}
 		kvfree(ctx.offset);
+		kvfree(ctx.landing_ex);
 out_priv_stack:
 		kfree(jit_data);
 		prog->aux->jit_data = NULL;

[Severity: Medium]
Can this leak ctx.landing_ex if multi-function JIT compilation fails between passes?

During the first pass of jit_subprogs() -> bpf_int_jit_compile() for a subprogram (where prog->is_func is true and extra_pass is false), ctx is saved in prog->aux->jit_data and the cleanup block is skipped so that ctx.landing_ex and ctx.offset remain available for the second pass:

arch/arm64/net/bpf_jit_comp.c:bpf_int_jit_compile() {
    ...
	if (!prog->is_func || extra_pass) {
		...
	} else {
		jit_data->ctx = ctx;
		jit_data->ro_image = ro_image_ptr;
		jit_data->header = header;
		jit_data->ro_header = ro_header;
	}

	prog->bpf_func = (void *)ctx.ro_image + cfi_get_offset();
	prog->jited = 1;
	prog->jited_len = prog_size - cfi_get_offset();

	if (!prog->is_func || extra_pass) {
		...
		kvfree(ctx.offset);
		kvfree(ctx.landing_ex);
out_priv_stack:
		kfree(jit_data);
		prog->aux->jit_data = NULL;
	}
    ...
}

If a subsequent subprogram fails its initial JIT pass in jit_subprogs(), or if an earlier subprogram fails during the extra_pass loop, jit_subprogs() jumps to out_free and calls bpf_jit_free() on each subprogram. However, bpf_jit_free() frees jit_data without freeing jit_data->ctx.landing_ex (or the pre-existing jit_data->ctx.offset):

arch/arm64/net/bpf_jit_comp.c:bpf_jit_free() {
	if (prog->jited) {
		struct arm64_jit_data *jit_data = prog->aux->jit_data;
		...
		if (jit_data) {
			bpf_jit_binary_pack_finalize(jit_data->ro_header, jit_data->header);
			kfree(jit_data);
		}
    ...
}

Should bpf_jit_free() also call kvfree(jit_data->ctx.landing_ex) and kvfree(jit_data->ctx.offset) before freeing jit_data?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2
author maintainer
#8 lore raw

Re: [RFC PATCH bpf-next v1 02/11] bpf, arm64: Let the pad after a helper call have r0

Quoted 4 lines · sashiko-bot

[...]
[Severity: Medium]
Can this leak ctx.landing_ex if multi-function JIT compilation fails between passes?

Right. ctx.offset leaked the same way before, where x86-64 frees its addrs; the next version frees both in bpf_jit_free().

Quoted 1 line

[...]

author maintainer
#9 lore raw

RFC PATCH 3/11 bpf: Let a callback unwind into the pad of its helper

In reply to Kumar Kartikeya Dwivedi

A helper that calls a callback is a C frame between the callback and the BPF code that called the helper, and with bpf_call_cb() it has a pad: the unwind_exit of the callback lands there, the helper drops what it holds and leaves to the pad after its call with the r0 the callback unwound with. To the verifier the helper is transparent on that path as it is on the normal one: it models the call of a helper with a callback as the callback running in a frame of its own, and a callback that unwinds now leaves that frame the way a subprog does.

So the call of a helper or a kfunc with a callback that can unwind is a call of something that can unwind: unwind_goto may follow it and has to when the callback has unwind_exit, the same rule as for a subprog. At unwind_exit of the callback the verifier goes back to the call once more, as it does after a round of the callback, since the helper has effects of its own on its way to the pad: bpf_rbtree_add() releases the node whichever way it leaves. With the state marked unwound there is no further round of the callback, the next call with a callback in the frame starts from its first round, and the next insn is the unwind_goto. A callback that returns on one path to the call and unwinds on another is fine: the pad is there for whoever takes it.

The pad gets the state of the r0 of the callback, as the pad after a subprog call does. The return of a callback is a scalar in a range because the helper acts on it; what the callback unwinds with, the helper passes on bit for bit and doesn't look at. So a known value stays known, a pointer stays the pointer it was, with its type and its reference, and what the pad does with it is checked there and in every pad above that unwinds on with it, up to the main prog, whose return has to be a number. A pointer into the stack of the callback is refused as at any return. What the callback may leave in r0 is what it may store through callback_ctx already. That includes pointers that don't outlive the helper:
the dynptr of bpf_user_ringbuf_drain() and the key of the array iterator live in the frame of the helper, the vma of bpf_find_vma() is good under the lock the pad drops. The verifier lets the store through today, and the fix for that has to see to r0 too.

The effects of the helper on its way to the pad apply to that r0 like to any register: on the revisit of the call, check_helper_call() and check_kfunc_call() keep r0 across the scrub of the caller-saved registers and skip the return. Two passes see r0 flow through the call. Liveness:
r0 is live before a call with a callback and a pad, since the revisit passes it on, so that two unwound states at the call with a different r0 aren't one. Precision: the history entry of the revisit is marked INSN_F_UNWOUND, so backtracking doesn't take r0 for the helper's there, and unwind_exit of a callback hands the request on to the callback the way exit of a subprog does.

No helper has a pad yet: "callback cannot unwind" stays until a helper is opened, which the following patches do one by one, where BPF_HAS_CB_PADS.

---
 include/linux/bpf_verifier.h |   6 +-
 kernel/bpf/backtrack.c       |  21 +++++-
 kernel/bpf/liveness.c        |   7 ++
 kernel/bpf/verifier.c        | 138 ++++++++++++++++++++++++++++++-----
 4 files changed, 149 insertions(+), 23 deletions(-)
include/linux/bpf_verifier.h +4 -2
index 5926f13ff43c..8b4d4d6981f3 100644
@@ -434,6 +434,8 @@ enum {
 	INSN_F_SRC_REG_STACK = BIT(2), /* src_reg is PTR_TO_STACK */
 
 	INSN_F_STACK_ARG_ACCESS = BIT(3),
+
+	INSN_F_UNWOUND = BIT(4), /* the callback of this call unwound: r0 is the callback's */
 };
 
 /* Registers linked to one jump condition that a history entry can record */
@@ -444,8 +446,8 @@ struct bpf_jmp_history_entry {
 	u32 idx : 20;
 	u32 frame : 4;	/* stack access frame number */
 	/* special INSN_F_xxx flags */
-	u32 flags : 4;
-	u32 : 4;
+	u32 flags : 5;
+	u32 : 3;
 	u32 prev_idx : 20;
 	u32 spi : 12;	/* stack slot index */
 	/*
kernel/bpf/backtrack.c +17 -4
index 43d5740034c0..ce171be5e754 100644
@@ -530,8 +530,13 @@ static int backtrack_insn(struct bpf_verifier_env *env, int idx, int subseq_idx,
 			 */
 			if (insn->src_reg == BPF_PSEUDO_KFUNC_CALL && insn->imm == 0)
 				return -ENOTSUPP;
-			/* regular helper call sets R0 */
-			bt_clear_reg(bt, BPF_REG_0);
+			/*
+			 * A regular helper call sets R0. When its callback unwound,
+			 * R0 is the callback's, from the unwind_exit of the callback,
+			 * see the BPF_EXIT case.
+			 */
+			if (!hist || !(hist->flags & INSN_F_UNWOUND))
+				bt_clear_reg(bt, BPF_REG_0);
 			/* kfunc might also set R2 */
 			bt_clear_reg(bt, BPF_REG_2);
 			if (bt_reg_mask(bt) & BPF_REGMASK_ARGS) {
@@ -548,7 +553,7 @@ static int backtrack_insn(struct bpf_verifier_env *env, int idx, int subseq_idx,
 					return -EFAULT;
 			}
 		} else if (opcode == BPF_EXIT) {
-			bool from_subprog_call, r0_precise, r2_precise;
+			bool from_subprog_call, cb_unwinds, r0_precise, r2_precise;
 
 			/* BPF_EXIT in subprog or callback always returns
 			 * right after the call instruction, so by checking
@@ -562,8 +567,16 @@ static int backtrack_insn(struct bpf_verifier_env *env, int idx, int subseq_idx,
 			from_subprog_call = subseq_idx - 1 >= 0 &&
 					    (bpf_pseudo_call(&env->prog->insnsi[subseq_idx - 1]) ||
 					     bpf_is_callx(&env->prog->insnsi[subseq_idx - 1]));
+			/*
+			 * unwind_exit of a callback goes back to the call of the
+			 * helper, which passes R0 on to the pad after it: R0 is
+			 * the callback's, as it is the subprog's after a call.
+			 */
+			cb_unwinds = !from_subprog_call && bpf_is_unwind_exit(insn) &&
+				     subseq_idx >= 0 &&
+				     bpf_is_sync_callback_calling_insn(&env->prog->insnsi[subseq_idx]);
 
-			r0_precise = from_subprog_call && bt_is_reg_set(bt, BPF_REG_0);
+			r0_precise = (from_subprog_call || cb_unwinds) && bt_is_reg_set(bt, BPF_REG_0);
 			r2_precise = from_subprog_call && bt_is_reg_set(bt, BPF_REG_2);
 
 			/* Backtracking to a nested function call, 'idx' is a part of
kernel/bpf/liveness.c +7 -0
index e657e6d95408..22225d1a0fe7 100644
@@ -2637,6 +2637,13 @@ static void compute_insn_live_regs(struct bpf_verifier_env *env,
 			/* callx reads the address of the callee from dst_reg */
 			if (bpf_is_callx(insn))
 				use |= dst;
+			/*
+			 * When the callback unwinds, the revisit of the call passes
+			 * the r0 of the callback on to the pad after the call, see
+			 * check_helper_call(): live before the call.
+			 */
+			if (bpf_is_sync_callback_calling_insn(insn) && bpf_is_unwind_goto(insn + 1))
+				use |= r0;
 			break;
 		default:
 			def = 0;
kernel/bpf/verifier.c +121 -17

257 lines not on this page · raw message

Signature · 1 line
2.53.0
#10 lore raw

Re: [RFC PATCH bpf-next v1 03/11] bpf: Let a callback unwind into the pad of its helper

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [High] bpf: prepare_func_exit() clobbers caller R2 on ret_reg_pair cb unwind - [Medium] bpf…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] bpf: prepare_func_exit() clobbers caller R2 on ret_reg_pair cb unwind
- [Medium] bpf: verifier_bug_if() in do_check() when callback unwinds with const R0
- [Medium] bpf: inline_bpf_loop() leaves unwind_goto after BPF_LDX_MEM when callback cannot unwind --

commit 00033d1866979957f9c4c976952ca2e9b1a444c5
Author: Kumar Kartikeya Dwivedi <[email protected]>

bpf: Let a callback unwind into the pad of its helper

This commit extends the BPF verifier to allow synchronous callbacks invoked by helpers or kfuncs to exit via unwind_exit into an unwind pad following the helper or kfunc call. Upon unwind_exit, the verifier revisits the callsite with the callback's R0 preserved so helper effects are applied before proceeding to the unwind_goto instruction. It also updates register liveness and precision backtracking to account for R0 flowing from the callback across the call.

Quoted 24 lines
kernel/bpf/verifier.c +3 -0
index 5601d20562186..ac9dbe0821811 100644
@@ -10,6 +10,7 @@
 #include <linux/types.h>
 #include <linux/slab.h>
 #include <linux/bpf.h>
+#include <linux/bpf_unwind.h>
 #include <linux/btf.h>
 #include <linux/bpf_verifier.h>
 #include <linux/filter.h>
@@ -3435,6 +3436,8 @@ static int next_callx_callee(struct bpf_verifier_env *env, int caller, int from)
  * 'call foo; unwind_goto pad' goes to the pad when foo left with unwind_exit.
  * JITs don't emit a branch for it. unwind_exit finds the pad by its return
  * address. So unwind_goto has to follow the call and nothing may jump to it.
+ * The call may be of a helper or a kfunc that calls a callback: it leaves like
+ * the callback did, see check_cb_unwind().
  */
 static int check_unwind_insns(struct bpf_verifier_env *env)
 {
@@ -3450,8 +3453,10 @@ static int check_unwind_insns(struct bpf_verifier_env *env)
 		if (!bpf_is_unwind_goto(insn))
 			continue;

kernel/bpf/verifier.c static int check_unwind_insns(struct bpf_verifier_env *env) · line 3462

Quoted 10 lines
kernel/bpf/verifier.c +4 -2
 		seen = true;
-		if (!i || !(bpf_pseudo_call(insn - 1) || bpf_is_callx(insn - 1))) {
-			verbose(env, "unwind_goto at insn %d doesn't follow a call of a subprog\n",
+		if (!i || !(bpf_pseudo_call(insn - 1) || bpf_is_callx(insn - 1) ||
+			    bpf_is_sync_callback_calling_insn(insn - 1))) {
+			verbose(env,
+				"unwind_goto at insn %d doesn't follow a call of a subprog or of a helper with a callback\n",
 				i);
 			return -EINVAL;
 		}

[Severity: Medium]
What happens when unwind_goto follows a call to bpf_loop() that gets inlined by bpf_optimize_bpf_loop() -> inline_bpf_loop(), but the callback does not unwind (!env->subprog_info[subprog].can_unwind)?

Here check_unwind_insns() permits unwind_goto after any bpf_is_sync_callback_calling_insn(insn - 1), and check_cb_unwind() returns 0 when !env->subprog_info[subprog].can_unwind.

When bpf_loop() is inlined in kernel/bpf/fixups.c:inline_bpf_loop() (both here and after the later commit "bpf: Give the inlined bpf_loop a pad" when !can_unwind):

	insn_buf[cnt++] = BPF_JMP_IMM(BPF_JLE, BPF_REG_1, BPF_MAX_LOOPS, 2);
	insn_buf[cnt++] = BPF_MOV32_IMM(BPF_REG_0, -E2BIG);
	insn_buf[cnt++] = BPF_JMP_IMM(BPF_JA, 0, 0, 16);
	...
	insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_10, r8_offset);

the call instruction is replaced with 19 instructions ending in BPF_LDX_MEM without converting the trailing unwind_goto into BPF_JMP_A(0), and the BPF_JMP_IMM(BPF_JA, 0, 0, 16) instruction jumps directly to that unwind_goto.

If the target of that unwind_goto was marked seen during verification (for example, a shared pad or unwind_goto with offset 0) or when !is_priv, bpf_opt_hard_wire_dead_code_branches() leaves the unwind_goto instruction in place:

	if (bpf_is_unwind_goto(insn) && aux_data[i + 1 + insn->off].seen) {
		if (!aux_data[i + 1].seen) {
			memcpy(insn + 1, &trap, sizeof(trap));
			aux_data[i + 1].seen = env->pass_cnt;
		}
		continue;
	}

Wouldn't this leave unwind_goto immediately after BPF_LDX_MEM instead of a call instruction (and targeted by a jump), violating the JIT invariant in arch/x86/net/bpf_jit_comp.c and arch/arm64/net/bpf_jit_comp.c (including the x86-64 priv_frame_ptr path that assumes insn[i - 1] is a call ending in pop r9 and keys the exception table at addrs[i - 1] - 2)?

Conversely, in the later commit "bpf: Give the inlined bpf_loop a pad", if can_unwind is true for the callback subprog, but R1 (nr_loops) is known to be 0 at the bpf_loop() callsite, check_helper_call() skips push_callback_call() so the unwind pad is never verified. Yet inline_bpf_loop() sees subprog_info[callback_subprogno].can_unwind && bpf_is_unwind_goto(insn + 1) and emits an inlined pad jumping via BPF_JMP32_A to an unverified pad that bpf_opt_remove_dead_code() then strips.

Should inline_bpf_loop() rewrite a trailing unwind_goto to BPF_JMP_A(0) when the callback does not unwind during the call, or only emit the pad when the unwind_goto target was actually verified?

[ ... ]

Quoted 24 lines
kernel/bpf/verifier.c +10 -6
@@ -12111,12 +12178,16 @@ static int prepare_func_exit(struct bpf_verifier_env *env, int *insn_idx)
 	}
 
 	caller = state->frame[state->curframe - 1];
-	if (bpf_is_unwind_exit(&env->prog->insnsi[*insn_idx])) {
-		/* the helper or the kfunc that called it doesn't expect that */
-		if (callee->in_callback_fn) {
-			verbose(env, "callback cannot unwind\n");
-			return -EINVAL;
-		}
+	unwinds = bpf_is_unwind_exit(&env->prog->insnsi[*insn_idx]);
+	if (unwinds) {
+		/*
+		 * A callback returns into the helper that called it, and to the
+		 * pad of the helper when the helper has one. check_cb_unwind()
+		 * has seen to that, and to the unwind_goto after the call of
+		 * the helper, where the helper leaves to right away, with the
+		 * r0 of the callback as it is: the pad gets its state, as the
+		 * pad after a subprog call does.
+		 */
 		if (!bpf_is_unwind_goto(&env->prog->insnsi[callee->callsite + 1])) {
 			verbose(env, "unwind_exit, but no unwind_goto after the call at insn %d\n",
 				callee->callsite);

kernel/bpf/verifier.c static int prepare_func_exit(struct bpf_verifier_env *env, int *insn_idx) · line 12201

Quoted 8 lines
kernel/bpf/verifier.c +1 -1
@@ -12124,7 +12195,7 @@ static int prepare_func_exit(struct bpf_verifier_env *env, int *insn_idx)
 		}
 		state->unwound = true;
 	}
-	if (callee->in_callback_fn) {
+	if (callee->in_callback_fn && !unwinds) {
 		if (r0->type != SCALAR_VALUE) {
 			verbose(env, "R0 not a scalar value\n");
 			return -EACCES;

[Severity: High]
Can a callback with a 9..16 byte BTF return type clobber the caller's R2 argument register when it exits via unwind_exit?

In bpf_compute_subprog_ret_regs(), ret_reg_pair is set for any static subprog whose BTF return type is 9..16 bytes, and btf_check_subprog_call() in push_callback_call() does not reject this for callbacks. At the top of prepare_func_exit():

	callee = state->frame[state->curframe];
	r0 = &callee->regs[BPF_REG_0];
	nregs = bpf_ret_reg_pair(env, callee->subprogno) ? 2 : 1;

When callee->in_callback_fn && unwinds is true, prepare_func_exit() now takes the else branch:

	} else {
		/*
		 * return to the caller whatever the callee had in the
		 * return register(s)
		 */
		for (i = 0; i < nregs; i++) {
			u32 regno = ret_regs[i];

			bpf_diag_mod_begin(env, &caller->regs[regno], &callee->regs[regno],
					   BPF_DIAG_MOD_WRITE);
			caller->regs[regno] = callee->regs[regno];
			bpf_diag_mod_end(env);
		}
	}

If nregs == 2, this copies callee->regs[BPF_REG_2] into caller->regs[BPF_REG_2] before redirecting *insn_idx back to callee->callsite.

When the verifier revisits the call instruction at callee->callsite, check_helper_call() or check_kfunc_call() re-runs argument validation and side effects using the overwritten caller->regs[BPF_REG_2]. In particular, for bpf_rbtree_add_impl(root, node, less, ...), R2 is the owning reference to the node being added:

	if (is_bpf_list_push_kfunc(meta.func_id) || is_bpf_rbtree_add_kfunc(meta.func_id)) {
		id = regs[BPF_REG_2].id;
		insn_aux->insert_off = regs[BPF_REG_2].var_off.value;
		insn_aux->kptr_struct_meta = btf_find_struct_meta(meta.arg_btf, meta.arg_btf_id);
		ref_convert_owning_non_owning(env, id);
	}

If the callback places a different allocated node pointer in its R2 before unwind_exit, wouldn't the revisit of check_kfunc_call() release the wrong reference ID in the verifier while bpf_rbtree_add_impl() drops the original node at runtime, leaving a dangling owning reference to the freed original node in the caller's unwind pad?

Should nregs be forced to 1 when callee->in_callback_fn is true (or should callbacks with ret_reg_pair be rejected)?

[ ... ]

kernel/bpf/verifier.c static int do_check(struct bpf_verifier_env *env) · line 20699

Quoted 10 lines
kernel/bpf/verifier.c +2 -1
@@ -20589,7 +20692,8 @@ static int do_check(struct bpf_verifier_env *env)
 		}
 
 		if (bpf_is_jmp_point(env, env->insn_idx)) {
-			err = bpf_push_jmp_history(env, state, 0, 0, 0, NULL, 0);
+			err = bpf_push_jmp_history(env, state, state->unwound ? INSN_F_UNWOUND : 0,
+						   0, 0, NULL, 0);
 			if (err)
 				return err;
 		}

[Severity: Medium]
Can revisiting the call instruction with state->unwound set trigger verifier_bug_if() in do_check()'s precomputed constant register check?

Before do_check() runs, bpf_compute_const_regs() computes intraprocedural constant register values at each instruction. If the caller holds a known 32-bit constant C1 in R0 before a callback-calling helper or kfunc instruction (for instance, r0 = 0; call bpf_loop; unwind_goto +pad;), bpf_compute_const_regs() sets BIT(BPF_REG_0) in insn_aux->const_reg_mask and records C1 in insn_aux->const_reg_vals[BPF_REG_0].

When the callback unwinds with a different constant scalar C2 in R0, prepare_func_exit() sets state->unwound = true, copies C2 into caller->regs[BPF_REG_0], and sets *insn_idx = callee->callsite. On the revisit of the call instruction in do_check(), just below this hunk:

		/* Sanity check: precomputed constants must match verifier state */
		if (!state->speculative && insn_aux->const_reg_mask) {
			struct bpf_reg_state *regs = cur_regs(env);
			u16 mask = insn_aux->const_reg_mask;

			for (int r = 0; r < ARRAY_SIZE(insn_aux->const_reg_vals); r++) {
				u32 cval = insn_aux->const_reg_vals[r];

				if (!(mask & BIT(r)))
					continue;
				if (regs[r].type != SCALAR_VALUE)
					continue;
				if (!tnum_is_const(regs[r].var_off))
					continue;
				if (verifier_bug_if((u32)regs[r].var_off.value != cval,
						    env, "const R%d: %u != %llu",
						    r, cval, regs[r].var_off.value))
					return -EFAULT;
			}
		}

Because regs[BPF_REG_0] now holds the callback's unwound constant C2 rather than the caller's pre-call constant C1, wouldn't this fire verifier_bug_if() and fail verification with -EFAULT unless BPF_REG_0 is skipped when state->unwound is true?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=3
author maintainer
#11 lore raw

Re: [RFC PATCH bpf-next v1 03/11] bpf: Let a callback unwind into the pad of its helper

Quoted 5 lines · sashiko-bot

[...]
[Severity: Medium]
What happens when unwind_goto follows a call to bpf_loop() that gets inlined by bpf_optimize_bpf_loop() -> inline_bpf_loop(), but the callback does not unwind (!env->subprog_info[subprog].can_unwind)?

The next version turns the unwind_goto after the call of bpf_loop() into a no-op jump whether or not the inlined loop gets a pad, so it never follows the last insn of the loop, and the loop gets a pad only when the verifier saw the pad of the caller: with a loop of no rounds that pad is dead and goes with the dead code.

Quoted 4 lines

[...]
[Severity: High]
Can a callback with a 9..16 byte BTF return type clobber the caller's R2 argument register when it exits via unwind_exit?

Right. The pad of a helper carries r0 alone, so a callback hands over r0 alone in the next version, whatever its prototype says.

Quoted 4 lines

[...]
[Severity: Medium]
Can revisiting the call instruction with state->unwound set trigger verifier_bug_if() in do_check()'s precomputed constant register check?

Right. The revisit carries the r0 of the callback, so r0 is no constant of such an insn: bpf_compute_const_regs() skips it there in the next version, as liveness has it live there already.

Quoted 1 line

[...]

#12 lore raw

Re: [RFC PATCH bpf-next v1 03/11] bpf: Let a callback unwind into the pad of its helper

In reply to Kumar Kartikeya Dwivedi
TL;DR: - Tracing (ftrace, kretprobe, fexit) may redirect a pad helper's return address, so unwind could fall through, possibly causing use-after-free or bad r0; not yet reachable - Stale comment in…
Quoted 1 line

bpf: Let a callback unwind into the pad of its helper

TL;DR:
- Tracing (ftrace, kretprobe, fexit) may redirect a pad helper's return address, so unwind could fall through, possibly causing use-after-free or bad r0; not yet reachable
- Stale comment in backtrack.c (nit)

Quoted 4 lines
kernel/bpf/backtrack.c +0 -0
index 43d5740034c0..ce171be5e754 100644

[ ... ]

Quoted 6 lines
kernel/bpf/backtrack.c +2 -0
@@ -562,8 +567,16 @@ static int backtrack_insn(struct bpf_verifier_env *env, int idx, int subseq_idx,
 			from_subprog_call = subseq_idx - 1 >= 0 &&
 					    (bpf_pseudo_call(&env->prog->insnsi[subseq_idx - 1]) ||
 					     bpf_is_callx(&env->prog->insnsi[subseq_idx - 1]));
+			/*
+			 * unwind_exit of a callback goes back to the call of the

kernel/bpf/backtrack.c static int backtrack_insn(struct bpf_verifier_env *env, int idx, int subseq_idx, · line 580

Quoted 10 lines
kernel/bpf/backtrack.c +7 -1
+			 * helper, which passes R0 on to the pad after it: R0 is
+			 * the callback's, as it is the subprog's after a call.
+			 */
+			cb_unwinds = !from_subprog_call && bpf_is_unwind_exit(insn) &&
+				     subseq_idx >= 0 &&
+				     bpf_is_sync_callback_calling_insn(&env->prog->insnsi[subseq_idx]);
 
-			r0_precise = from_subprog_call && bt_is_reg_set(bt, BPF_REG_0);
+			r0_precise = (from_subprog_call || cb_unwinds) && bt_is_reg_set(bt, BPF_REG_0);
 			r2_precise = from_subprog_call && bt_is_reg_set(bt, BPF_REG_2);

This isn't a bug, but the comment at the top of the BPF_EXIT case says that the precision of the return registers is never propagated for an exit from a callback:

* case, we need to propagate the precision of the
* return registers, if necessary. In the latter we
* never do that.

With this change, unwind_exit of a callback sets cb_unwinds, and r0_precise = (from_subprog_call || cb_unwinds) && bt_is_reg_set(bt, BPF_REG_0) does propagate r0 to the callback frame.

The new comment above cb_unwinds describes this, but the old sentence is still there, so the same case has two comments that disagree.

Should the old comment say that the return registers of a callback are dropped unless the callback left with unwind_exit?

[ ... ]

Quoted 5 lines
kernel/bpf/verifier.c +0 -0
index 5601d2056218..ac9dbe082181 100644
@@ -11397,6 +11402,55 @@ static int btf_check_subprog_call(struct bpf_verifier_env *env, int subprog)

[ ... ]

Quoted 9 lines
kernel/bpf/verifier.c +9 -0
+/*
+ * A sync callback is called by a helper or a kfunc, a C frame between the
+ * callback and the BPF code that called it. unwind_exit of the callback returns
+ * to the pad of its caller when there is one, and a helper or a kfunc that
+ * calls a callback has one where BPF_HAS_CB_PADS and insn_has_cb_pad(): it
+ * calls the callback with bpf_call_cb(), drops what it holds when the callback
+ * unwinds and leaves to the pad of its own caller with the r0 of the callback.
+ * So a call with such a callback that can unwind is a call of something that
+ * can unwind, and unwind_goto has to follow it.

kernel/bpf/verifier.c static int btf_check_subprog_call(struct bpf_verifier_env *env, int subprog) · line 11441

Quoted 10 lines
kernel/bpf/verifier.c +10 -0
+ */
+static int check_cb_unwind(struct bpf_verifier_env *env, struct bpf_insn *insn,
+			   int insn_idx, int subprog)
+{
+	if (!env->subprog_info[subprog].can_unwind)
+		return 0;
+	if (!BPF_HAS_CB_PADS || !insn_has_cb_pad(insn)) {
+		verbose(env, "callback cannot unwind\n");
+		return -EINVAL;
+	}

Can a callback that unwinds through one of these helpers end up on the fall-through path of the call instead of at the pad?

check_cb_unwind() accepts the unwind based on BPF_HAS_CB_PADS and insn_has_cb_pad(), which are static properties. At run time the pad is only reached if the return address slot of the helper still holds the address in the BPF program. bpf_unwind_leave() does this:

    unsigned long *slot = (unsigned long *)__builtin_dwarf_cfa() - 1;
    unsigned long to = bpf_unwind_target(*slot);

    WARN_ON_ONCE(to == *slot);
    *slot = to;

bpf_unwind_target() looks up *slot in the BPF and kernel extables, and several kinds of tracing replace or move that return address:

- function_graph: prepare_ftrace_return() does *parent = return_hooker
- kretprobe/rethook and fprobe exit handlers (kprobe.multi return, session)
- a BPF fexit/fmod_ret trampoline: with BPF_TRAMP_F_CALL_ORIG it calls orig_call from the trampoline, so the helper returns into the trampoline

None of the helpers that get a pad later in the series is protected against this. bpf_loop, bpf_for_each_map_elem and bpf_find_vma become plain noinline functions through BPF_CALL_x, and bpf_for_each_array_elem, bpf_rbtree_add_impl and the others are ordinary traceable functions. None of them is in btf_id_deny and none is notrace.

If the slot was redirected, bpf_unwind_target() finds no entry and returns the address unchanged. WARN_ON_ONCE() fires, the helper does a normal return, and the tracer then returns to the instruction after the BPF call.

That instruction is unwind_goto, for which the arch backends emit no branch. Execution continues on the fall-through path, which the verifier modelled as a normal return of the helper.

On that path r0 holds what the callback unwound with, and the helper has already done its pad-side work:

- bpf_loop: the verifier thinks r0 is the scalar returned by bpf_loop(), but it can be a kernel pointer, which could be stored to a map, or a referenced pointer whose reference is never released.

- bpf_rbtree_add: on the unwind path the pad has already freed the node with __bpf_obj_drop_impl(). On the fall-through path the verifier ran ref_convert_owning_non_owning() and treats the node as a live non-owning reference, so the program can read and write freed memory.

bpf_check_attach_target() already refuses anything but fentry on a subprog that can unwind ("unwind_exit would return to the trampoline that called the target"). Is there an equivalent for the kernel helpers with pads? The verifier has no say over function_graph or kretprobes at all.

This commit only adds the gate, since helper_has_cb_pad() and kfunc_has_cb_pad() return false for now. The path is opened later in the series by:

4e61c40fe5c0 ("bpf: Let the callback of bpf_loop unwind") 7a687425732b ("bpf: Let the callback of bpf_for_each_map_elem unwind") 59e3a600668d ("bpf: Let the callback of bpf_find_vma unwind") bee41eec3c39 ("bpf: Let the callback of bpf_user_ringbuf_drain unwind") ecd4c98df2ee ("bpf: Let the comparator of bpf_rbtree_add unwind")

No commit up to 5646f059f1bb adds a fix: no notrace or NOKPROBE marking, no attach restriction, and no return_to_handler or trampoline handling in bpf_unwind_target().

Would it work to make the pad helpers untraceable (notrace plus NOKPROBE, and deny them in btf_id_deny for fexit/fmod_ret), or to have bpf_unwind_leave() resolve the real return address through the ftrace/rethook shadow stacks?

---
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/40612543702

author maintainer
#13 lore raw

Re: [RFC PATCH bpf-next v1 03/11] bpf: Let a callback unwind into the pad of its helper

Quoted 5 lines · bot+bpf-ci

[...]
TL;DR:
- Tracing (ftrace, kretprobe, fexit) may redirect a pad helper's return address, so unwind could fall through, possibly causing use-after-free or bad r0; not yet reachable

Right. Every function with a pad is notrace and no kprobe target in the next version, as the verifier lets only fentry attach to a subprog that unwinds, with a test that fexit on bpf_loop() is refused.

Quoted 1 line

- Stale comment in backtrack.c (nit)

Fixed in the next version.

Quoted 1 line

[...]

author maintainer
#14 lore raw

RFC PATCH 4/11 bpf: Give the inlined bpf_loop a pad

In reply to Kumar Kartikeya Dwivedi

The verifier inlines bpf_loop() when the flags are zero and the callback is the same at every visit. Once the loop is BPF insns the caller of the callback is BPF code, and the pad is the ordinary one: unwind_goto after the call of the callback, which costs nothing, and a pad that restores r6-r8 and jumps to the pad after the call of bpf_loop with the r0 of the callback, the one the verifier checked with the state of that r0. The unwind_goto that followed the call of bpf_loop follows no call anymore and becomes a no-op jump.

Only a callback with unwind_exit gets the pad, and only when an unwind_goto follows the call: a loop of no rounds never runs its callback, so the verifier asked for none, and the insn after the call is some other insn. The inlined loop of every other program is the one it was. Nothing takes the pad yet: the callback of bpf_loop() is let unwind next.

---
 kernel/bpf/fixups.c | 43 ++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 40 insertions(+), 3 deletions(-)
kernel/bpf/fixups.c +40 -3
index f062ef7dd943..94a379c94c67 100644
@@ -2836,6 +2836,15 @@ int bpf_do_misc_fixups(struct bpf_verifier_env *env)
 	return 0;
 }
 
+/*
+ * When the callback can unwind and unwind_goto follows the call of bpf_loop,
+ * unwind_goto follows the call of the callback, and the pad restores r6-r8 and
+ * jumps to the pad after the call of bpf_loop with the r0 of the callback, which
+ * the verifier has checked with the state of that r0. The unwind_goto that
+ * followed the call of bpf_loop doesn't follow a call anymore and becomes a
+ * no-op jump. A loop of no rounds never runs its callback, so the verifier asked
+ * for no unwind_goto there, and the insn after the call is some other insn.
+ */
 static struct bpf_prog *inline_bpf_loop(struct bpf_verifier_env *env,
 					int position,
 					s32 stack_base,
@@ -2848,6 +2857,8 @@ static struct bpf_prog *inline_bpf_loop(struct bpf_verifier_env *env,
 	int reg_loop_max = BPF_REG_6;
 	int reg_loop_cnt = BPF_REG_7;
 	int reg_loop_ctx = BPF_REG_8;
+	bool pad = env->subprog_info[callback_subprogno].can_unwind &&
+		   bpf_is_unwind_goto(&env->prog->insnsi[position + 1]);
 
 	struct bpf_insn *insn_buf = env->insn_buf;
 	struct bpf_prog *new_prog;
@@ -2865,7 +2876,7 @@ static struct bpf_prog *inline_bpf_loop(struct bpf_verifier_env *env,
 	 */
 	insn_buf[cnt++] = BPF_JMP_IMM(BPF_JLE, BPF_REG_1, BPF_MAX_LOOPS, 2);
 	insn_buf[cnt++] = BPF_MOV32_IMM(BPF_REG_0, -E2BIG);
-	insn_buf[cnt++] = BPF_JMP_IMM(BPF_JA, 0, 0, 16);
+	insn_buf[cnt++] = BPF_JMP_IMM(BPF_JA, 0, 0, pad ? 22 : 16);
 	/* spill R6, R7, R8 to use these as loop vars */
 	insn_buf[cnt++] = BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_6, r6_offset);
 	insn_buf[cnt++] = BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_7, r7_offset);
@@ -2877,17 +2888,20 @@ static struct bpf_prog *inline_bpf_loop(struct bpf_verifier_env *env,
 	/* loop header,
 	 * if reg_loop_cnt >= reg_loop_max skip the loop body
 	 */
-	insn_buf[cnt++] = BPF_JMP_REG(BPF_JGE, reg_loop_cnt, reg_loop_max, 5);
+	insn_buf[cnt++] = BPF_JMP_REG(BPF_JGE, reg_loop_cnt, reg_loop_max, pad ? 6 : 5);
 	/* callback call,
 	 * correct callback offset would be set after patching
 	 */
 	insn_buf[cnt++] = BPF_MOV64_REG(BPF_REG_1, reg_loop_cnt);
 	insn_buf[cnt++] = BPF_MOV64_REG(BPF_REG_2, reg_loop_ctx);
 	insn_buf[cnt++] = BPF_CALL_REL(0);
+	if (pad)
+		/* to the pad below when the callback left with unwind_exit */
+		insn_buf[cnt++] = BPF_RAW_INSN(BPF_JMP | BPF_JCOND, 0, BPF_UNWIND_GOTO, 7, 0);
 	/* increment loop counter */
 	insn_buf[cnt++] = BPF_ALU64_IMM(BPF_ADD, reg_loop_cnt, 1);
 	/* jump to loop header if callback returned 0 */
-	insn_buf[cnt++] = BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, -6);
+	insn_buf[cnt++] = BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, pad ? -7 : -6);
 	/* return value of bpf_loop,
 	 * set R0 to the number of iterations
 	 */
@@ -2896,12 +2910,35 @@ static struct bpf_prog *inline_bpf_loop(struct bpf_verifier_env *env,
 	insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_10, r6_offset);
 	insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_10, r7_offset);
 	insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_10, r8_offset);
+	if (pad) {
+		/* over the pad */
+		insn_buf[cnt++] = BPF_JMP_IMM(BPF_JA, 0, 0, 4);
+		/* the pad: r0 is what the callback unwound with */
+		insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_10, r6_offset);
+		insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_10, r7_offset);
+		insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_10, r8_offset);
+		/*
+		 * To the pad of the caller, where the unwind_goto that follows
+		 * the patch goes. Its offset is known once the patch is in and
+		 * the jumps across it are adjusted, see below.
+		 */
+		insn_buf[cnt++] = BPF_JMP32_A(0);
+	}
 
 	*total_cnt = cnt;
 	new_prog = bpf_patch_insn_data(env, position, insn_buf, cnt);
 	if (!new_prog)
 		return new_prog;
 
+	if (pad) {
+		struct bpf_insn *unwind_goto = &new_prog->insnsi[position + cnt];
+
+		/* the jump of the pad, one insn before, takes the target of the unwind_goto */
+		new_prog->insnsi[position + cnt - 1].imm = unwind_goto->off + 1;
+		/* and the unwind_goto after the call of bpf_loop follows no call anymore */
+		*unwind_goto = BPF_JMP_A(0);
+	}
+
 	/* callback start is known only after patching */
 	callback_start = env->subprog_info[callback_subprogno].start;
 	/* Note: insn_buf[12] is an offset of BPF_CALL_REL instruction */
Signature · 1 line
2.53.0
#15 lore raw

Re: [RFC PATCH bpf-next v1 04/11] bpf: Give the inlined bpf_loop a pad

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] bpf: inline_bpf_loop() emits pad to unseen target deleted by dead code removal - [High] bpf…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] bpf: inline_bpf_loop() emits pad to unseen target deleted by dead code removal
- [High] bpf: inline_bpf_loop() leaves orphaned unwind_goto after LDX_MEM when !pad --

commit f87a8cc4d4bc638d4f9d4773f7657f7aec4d7868
Author: Kumar Kartikeya Dwivedi <[email protected]>

bpf: Give the inlined bpf_loop a pad

This commit updates inline_bpf_loop() to emit an unwind landing pad when the callback subprogram can unwind and an unwind_goto instruction follows the bpf_loop() call. The inlined pad restores callee-saved registers R6-R8, jumps to the caller's landing pad, and rewrites the original unwind_goto after the bpf_loop() call into a no-op jump.

Quoted 21 lines
kernel/bpf/fixups.c +9 -0
index f062ef7dd943d..94a379c94c675 100644
@@ -2836,6 +2836,15 @@ int bpf_do_misc_fixups(struct bpf_verifier_env *env)
 	return 0;
 }
 
+/*
+ * When the callback can unwind and unwind_goto follows the call of bpf_loop,
+ * unwind_goto follows the call of the callback, and the pad restores r6-r8 and
+ * jumps to the pad after the call of bpf_loop with the r0 of the callback, which
+ * the verifier has checked with the state of that r0. The unwind_goto that
+ * followed the call of bpf_loop doesn't follow a call anymore and becomes a
+ * no-op jump. A loop of no rounds never runs its callback, so the verifier asked
+ * for no unwind_goto there, and the insn after the call is some other insn.
+ */
 static struct bpf_prog *inline_bpf_loop(struct bpf_verifier_env *env,
 					int position,
 					s32 stack_base,

kernel/bpf/fixups.c static struct bpf_prog *inline_bpf_loop(struct bpf_verifier_env *env, · line 2861

Quoted 5 lines
kernel/bpf/fixups.c +2 -0
@@ -2848,6 +2857,8 @@ static struct bpf_prog *inline_bpf_loop(struct bpf_verifier_env *env,
 	int reg_loop_max = BPF_REG_6;
 	int reg_loop_cnt = BPF_REG_7;
 	int reg_loop_ctx = BPF_REG_8;
+	bool pad = env->subprog_info[callback_subprogno].can_unwind &&
+		   bpf_is_unwind_goto(&env->prog->insnsi[position + 1]);

[Severity: High]
Can this leave a live BPF_JMP32_A instruction targeting a dead caller landing pad that gets removed by bpf_opt_remove_dead_code(), leading to an out-of-bounds read in addrs[] or ctx->offset[] during JIT compilation?

In inline_bpf_loop(), pad is enabled without checking whether the target of the unwind_goto at position + 1 was actually visited during verification:

    caller_pad = position + 2 + env->prog->insnsi[position + 1].off;
    env->insn_aux_data[caller_pad].seen

The caller's landing pad can remain unseen while can_unwind and bpf_is_unwind_goto(&env->prog->insnsi[position + 1]) are both true, for example when bpf_loop() is called with 0 iterations (r1 == 0, which skips push_callback_call() in check_helper_call() while still marking the call site fit_for_inline in update_loop_inline_state()) or when unwind_exit in the callback lies on a branch not taken for this call site.

In bpf_check(), bpf_optimize_bpf_loop() runs before bpf_opt_hard_wire_dead_code_branches() and bpf_opt_remove_dead_code():

bpf_check()
bpf_optimize_bpf_loop()
inline_bpf_loop()
bpf_patch_insn_data()
adjust_insn_aux_data()
bpf_opt_hard_wire_dead_code_branches()
bpf_opt_remove_dead_code()

    verifier_remove_insns()
      bpf_remove_insns()
        bpf_adj_branches()
          bpf_adj_delta_to_off()

When bpf_patch_insn_data() patches the inlined loop, adjust_insn_aux_data() marks all patched instructions in insn_buf as seen, including the inlined pad and its trailing BPF_JMP32_A instruction, while the original unwind_goto is overwritten with BPF_JMP_A(0).

When bpf_opt_hard_wire_dead_code_branches() runs next, it preserves the inlined BPF_UNWIND_GOTO because its target (the inlined pad) is marked seen, and ignores the BPF_JMP32_A instruction because it is an unconditional jump.

When bpf_opt_remove_dead_code() then removes the unseen caller landing pad range [pos, end_old), bpf_adj_delta_to_off() only adjusts forward jumps when curr + off + 1 >= end_old. Because BPF_JMP32_A targets caller_pad inside the removed range [pos, end_old), its imm offset is not adjusted. If caller_pad is at the end of a subprogram and preceded by other dead instructions (or followed by a subprogram where bpf_do_misc_fixups() calls adjust_jmp_off() for may_goto), BPF_JMP32_A jumps past the end of the subprogram, causing do_jit() on x86-64 to read past addrs[] and bpf2a64_offset() on arm64 to read past ctx->offset[].

Should pad also require env->insn_aux_data[position + 2 + env->prog->insnsi[position + 1].off].seen to be true?

Quoted 57 lines
kernel/bpf/fixups.c +20 -3
 	struct bpf_insn *insn_buf = env->insn_buf;
 	struct bpf_prog *new_prog;
@@ -2865,7 +2876,7 @@ static struct bpf_prog *inline_bpf_loop(struct bpf_verifier_env *env,
 	 */
 	insn_buf[cnt++] = BPF_JMP_IMM(BPF_JLE, BPF_REG_1, BPF_MAX_LOOPS, 2);
 	insn_buf[cnt++] = BPF_MOV32_IMM(BPF_REG_0, -E2BIG);
-	insn_buf[cnt++] = BPF_JMP_IMM(BPF_JA, 0, 0, 16);
+	insn_buf[cnt++] = BPF_JMP_IMM(BPF_JA, 0, 0, pad ? 22 : 16);
 	/* spill R6, R7, R8 to use these as loop vars */
 	insn_buf[cnt++] = BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_6, r6_offset);
 	insn_buf[cnt++] = BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_7, r7_offset);
@@ -2877,17 +2888,20 @@ static struct bpf_prog *inline_bpf_loop(struct bpf_verifier_env *env,
 	/* loop header,
 	 * if reg_loop_cnt >= reg_loop_max skip the loop body
 	 */
-	insn_buf[cnt++] = BPF_JMP_REG(BPF_JGE, reg_loop_cnt, reg_loop_max, 5);
+	insn_buf[cnt++] = BPF_JMP_REG(BPF_JGE, reg_loop_cnt, reg_loop_max, pad ? 6 : 5);
 	/* callback call,
 	 * correct callback offset would be set after patching
 	 */
 	insn_buf[cnt++] = BPF_MOV64_REG(BPF_REG_1, reg_loop_cnt);
 	insn_buf[cnt++] = BPF_MOV64_REG(BPF_REG_2, reg_loop_ctx);
 	insn_buf[cnt++] = BPF_CALL_REL(0);
+	if (pad)
+		/* to the pad below when the callback left with unwind_exit */
+		insn_buf[cnt++] = BPF_RAW_INSN(BPF_JMP | BPF_JCOND, 0, BPF_UNWIND_GOTO, 7, 0);
 	/* increment loop counter */
 	insn_buf[cnt++] = BPF_ALU64_IMM(BPF_ADD, reg_loop_cnt, 1);
 	/* jump to loop header if callback returned 0 */
-	insn_buf[cnt++] = BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, -6);
+	insn_buf[cnt++] = BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, pad ? -7 : -6);
 	/* return value of bpf_loop,
 	 * set R0 to the number of iterations
 	 */
@@ -2896,12 +2910,35 @@ static struct bpf_prog *inline_bpf_loop(struct bpf_verifier_env *env,
 	insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_10, r6_offset);
 	insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_10, r7_offset);
 	insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_10, r8_offset);
+	if (pad) {
+		/* over the pad */
+		insn_buf[cnt++] = BPF_JMP_IMM(BPF_JA, 0, 0, 4);
+		/* the pad: r0 is what the callback unwound with */
+		insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_10, r6_offset);
+		insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_10, r7_offset);
+		insn_buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_10, r8_offset);
+		/*
+		 * To the pad of the caller, where the unwind_goto that follows
+		 * the patch goes. Its offset is known once the patch is in and
+		 * the jumps across it are adjusted, see below.
+		 */
+		insn_buf[cnt++] = BPF_JMP32_A(0);
+	}
 
 	*total_cnt = cnt;
 	new_prog = bpf_patch_insn_data(env, position, insn_buf, cnt);
 	if (!new_prog)

kernel/bpf/fixups.c static struct bpf_prog *inline_bpf_loop(struct bpf_verifier_env *env, · line 2940

Quoted 10 lines
kernel/bpf/fixups.c +8 -0
 		return new_prog;
 
+	if (pad) {
+		struct bpf_insn *unwind_goto = &new_prog->insnsi[position + cnt];
+
+		/* the jump of the pad, one insn before, takes the target of the unwind_goto */
+		new_prog->insnsi[position + cnt - 1].imm = unwind_goto->off + 1;
+		/* and the unwind_goto after the call of bpf_loop follows no call anymore */
+		*unwind_goto = BPF_JMP_A(0);
+	}

[Severity: High]
What happens if bpf_is_unwind_goto(&new_prog->insnsi[position + cnt]) is true, but pad is false (for example, when env->subprog_info[callback_subprogno].can_unwind is false, or when the caller's landing pad was not reached from this call site)?

In check_unwind_insns() and check_cb_unwind(), an unwind_goto instruction is permitted after a bpf_loop() call even when the callback subprogram has can_unwind == false:

kernel/bpf/verifier.c:check_cb_unwind() {
    if (!env->subprog_info[subprog].can_unwind)
        return 0;
    ...
}

When inline_bpf_loop() runs with pad == false, it replaces the bpf_loop() call with 19 instructions ending in BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_10, r8_offset), and the error branch BPF_JMP_IMM(BPF_JA, 0, 0, 16) jumps directly to new_prog->insnsi[position + cnt].

Because the rewrite of *unwind_goto = BPF_JMP_A(0) is guarded by if (pad), an unwind_goto following the bpf_loop() call is left in place when !pad. If the landing pad targeted by that unwind_goto was marked seen via another unwinding call in the same subprogram, bpf_opt_hard_wire_dead_code_branches() sees:

    if (bpf_is_unwind_goto(insn) && aux_data[i + 1 + insn->off].seen)

and preserves the orphaned unwind_goto instruction. That leaves BPF_UNWIND_GOTO immediately following BPF_LDX_MEM instead of a call instruction, causing the x86-64 and arm64 JITs (which assume insn[i - 1] is a call instruction) to emit bogus EX_TYPE_BPF exception table entries and unwind stubs keyed inside the BPF_LDX_MEM instruction.

Should new_prog->insnsi[position + cnt] be rewritten to BPF_JMP_A(0) whenever bpf_is_unwind_goto(&new_prog->insnsi[position + cnt]) is true, regardless of whether pad is true?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=4
author maintainer
#16 lore raw

Re: [RFC PATCH bpf-next v1 04/11] bpf: Give the inlined bpf_loop a pad

Quoted 5 lines · sashiko-bot

[...]
[Severity: High]
Can this leave a live BPF_JMP32_A instruction targeting a dead caller landing pad that gets removed by bpf_opt_remove_dead_code(), leading to an out-of-bounds read in addrs[] or ctx->offset[] during JIT compilation?

Right: the pad of the caller is dead with a loop of no rounds, or with an unwind_exit on a path the verifier never took, and the jump of the inlined pad was left pointing into what the removal of dead code took away. KASAN sees do_jit() read addrs[] past the end. The next version gives the inlined loop a pad only when the verifier saw the pad of the caller, with a test for the loop of no rounds.

Quoted 6 lines

[...]
[Severity: High]
What happens if bpf_is_unwind_goto(&new_prog->insnsi[position + cnt]) is true, but pad is false (for example, when env->subprog_info[callback_subprogno].can_unwind is false, or when the caller's landing pad was not reached from this call site)?

The unwind_goto after the call becomes a no-op jump either way in the next version, so it never follows the last insn of the loop.

Quoted 1 line

[...]

author maintainer
#17 lore raw

RFC PATCH 5/11 bpf: Let the callback of bpf_loop unwind

In reply to Kumar Kartikeya Dwivedi

The loop holds nothing, so the pad only leaves, with the r0 of the callback. The inlined loop has its pad already, so the verifier lets the callback of bpf_loop() unwind from here on.

---
 kernel/bpf/bpf_iter.c | 10 ++++++++--
 kernel/bpf/verifier.c |  9 +++++++--
 2 files changed, 15 insertions(+), 4 deletions(-)
kernel/bpf/bpf_iter.c +8 -2
index f0b29a170935..efd23c973115 100644
@@ -5,6 +5,7 @@
 #include <linux/anon_inodes.h>
 #include <linux/filter.h>
 #include <linux/bpf.h>
+#include <linux/bpf_unwind.h>
 #include <linux/rcupdate_trace.h>
 
 struct bpf_iter_target_info {
@@ -749,7 +750,7 @@ const struct bpf_func_proto bpf_for_each_map_elem_proto = {
 	.arg4_type	= ARG_ANYTHING,
 };
 
-BPF_CALL_4(bpf_loop, u32, nr_loops, void *, callback_fn, void *, callback_ctx,
+BPF_CALL_x(4, noinline, bpf_loop, u32, nr_loops, void *, callback_fn, void *, callback_ctx,
 	   u64, flags)
 {
 	bpf_callback_t callback = (bpf_callback_t)callback_fn;
@@ -766,13 +767,18 @@ BPF_CALL_4(bpf_loop, u32, nr_loops, void *, callback_fn, void *, callback_ctx,
 		return -E2BIG;
 
 	for (i = 0; i < nr_loops; i++) {
-		ret = callback((u64)i, (u64)(long)callback_ctx, 0, 0, 0);
+		bpf_call_cb(callback, i, callback_ctx, 0, 0, 0, ret, unwound);
 		/* return value: 0 - continue, 1 - stop and return */
 		if (ret)
 			return i + 1;
 	}
 
 	return i;
+
+unwound:
+	/* The callback left with unwind_exit: the loop is over, and so is the helper */
+	bpf_unwind_leave();
+	return ret;
 }
 
 const struct bpf_func_proto bpf_loop_proto = {
kernel/bpf/verifier.c +7 -2
index ac9dbe082181..4b4d8479fa87 100644
@@ -11402,10 +11402,15 @@ static int btf_check_subprog_call(struct bpf_verifier_env *env, int subprog)
 	return err;
 }
 
-/* The helper calls its callback with a pad, see bpf_call_cb(). None yet. */
+/* The helper calls its callback with a pad, see bpf_call_cb() */
 static bool helper_has_cb_pad(enum bpf_func_id func_id)
 {
-	return false;
+	switch (func_id) {
+	case BPF_FUNC_loop:
+		return true;
+	default:
+		return false;
+	}
 }
 
 /* The kfunc calls its callback with a pad. None yet. */
Signature · 1 line
2.53.0
#18 lore raw

Re: [RFC PATCH bpf-next v1 05/11] bpf: Let the callback of bpf_loop unwind

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Critical] bpf: inline_bpf_loop() emits dangling jump to dead unwind pad - [Medium] bpf: clobbering of…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] bpf: inline_bpf_loop() emits dangling jump to dead unwind pad
- [Medium] bpf: clobbering of caller R2 in prepare_func_exit() on callback unwind --

commit 342597e9c392b90ccb12e07eaf21a9e820d7f364
Author: Kumar Kartikeya Dwivedi <[email protected]>

bpf: Let the callback of bpf_loop unwind

This commit adds an unwind landing pad to bpf_loop() using bpf_call_cb() and bpf_unwind_leave(), and updates helper_has_cb_pad() so the verifier permits bpf_loop() callbacks to unwind.

Quoted 10 lines
kernel/bpf/verifier.c +1 -1
index ac9dbe0821811..4b4d8479fa87b 100644
@@ -11402,10 +11402,15 @@ static int btf_check_subprog_call(struct bpf_verifier_env *env, int subprog)
 	return err;
 }
 
-/* The helper calls its callback with a pad, see bpf_call_cb(). None yet. */
+/* The helper calls its callback with a pad, see bpf_call_cb() */

kernel/bpf/verifier.c static int btf_check_subprog_call(struct bpf_verifier_env *env, int subprog) · line 11414

Quoted 10 lines
kernel/bpf/verifier.c +6 -1
 static bool helper_has_cb_pad(enum bpf_func_id func_id)
 {
-	return false;
+	switch (func_id) {
+	case BPF_FUNC_loop:
+		return true;
+	default:
+		return false;
+	}
 }

[Severity: Critical]
When helper_has_cb_pad(BPF_FUNC_loop) returns true, can inline_bpf_loop() in kernel/bpf/fixups.c emit a live BPF_JMP32_A instruction that jumps to a dead landing pad which is subsequently removed by dead code elimination, or leave a stray BPF_UNWIND_GOTO after a load instruction?

In kernel/bpf/verifier.c:check_unwind_insns(), subprog_info[subprog].can_unwind is set statically whenever the callback contains an unwind_exit instruction, and check_cb_unwind() then requires an unwind_goto instruction at position + 1 immediately after the bpf_loop() call.

If a privileged program calls bpf_loop() with flags == 0 and a single callback target (so fit_for_inline is true), and the callback does not dynamically take its unwind_exit path during verification of that call (for example, when guarded by callback_ctx or loop index, or when nr_loops == 0), state->unwound is never set at position + 1. In check_cond_jmp_op(), the unwind_goto branch to the caller's landing pad (caller_pad = position + 2 + unwind_goto->off) is never taken, leaving env->insn_aux_data[caller_pad].seen == 0.

In kernel/bpf/verifier.c:bpf_check(), bpf_optimize_bpf_loop() runs before dead code elimination:

	if (ret == 0)
		ret = bpf_optimize_bpf_loop(env);

	if (is_priv) {
		if (ret == 0)
			bpf_opt_hard_wire_dead_code_branches(env);
		if (ret == 0)
			ret = bpf_opt_remove_dead_code(env);

Inside kernel/bpf/fixups.c:inline_bpf_loop(), pad is computed without checking whether the target of unwind_goto at position + 1 was actually visited:

	bool pad = env->subprog_info[callback_subprogno].can_unwind &&
		   bpf_is_unwind_goto(&env->prog->insnsi[position + 1]);
	...
	if (pad) {
		struct bpf_insn *unwind_goto = &new_prog->insnsi[position + cnt];

		/* the jump of the pad, one insn before, takes the target of the unwind_goto */
		new_prog->insnsi[position + cnt - 1].imm = unwind_goto->off + 1;
		/* and the unwind_goto after the call of bpf_loop follows no call anymore */
		*unwind_goto = BPF_JMP_A(0);
	}

Because adjust_insn_aux_data() copies the call instruction's non-zero seen count to all patched instructions, the inlined pad and its trailing BPF_JMP32_A are marked seen != 0. bpf_opt_hard_wire_dead_code_branches() does not fold the inlined BPF_UNWIND_GOTO (its target is the inlined pad, which has seen != 0) and ignores the unconditional BPF_JMP32_A.

When bpf_opt_remove_dead_code() then deletes the unvisited caller_pad via verifier_remove_insns() -> bpf_remove_insns() -> bpf_adj_branches() -> bpf_adj_delta_to_off() in kernel/bpf/core.c:

	if (curr < pos && curr + off + 1 >= end_old)
		off += delta;
	else if (curr >= end_new && curr + off + 1 < end_new)
		off -= delta;

BPF_JMP32_A's offset is not adjusted because its target falls inside the removed range [pos, end_old). If caller_pad was at the end of the subprogram, func[i]->len shrinks while BPF_JMP32_A still jumps past the end of the subprogram, causing out-of-bounds reads of addrs[i + insn->imm] in do_jit() in arch/x86/net/bpf_jit_comp.c and ctx->offset[bpf_insn + off] in bpf2a64_offset() in arch/arm64/net/bpf_jit_comp.c, or jumping into post-return int3 padding or subsequent instructions.

Additionally, if the callback has can_unwind == false and an unwind_goto follows the bpf_loop() call (which check_unwind_insns() and check_cb_unwind() allow), pad is false in inline_bpf_loop(), so *unwind_goto = BPF_JMP_A(0) is skipped, leaving BPF_UNWIND_GOTO immediately after BPF_LDX_MEM instead of a call instruction when the landing pad is shared (seen != 0) or !is_priv.

[Severity: Medium]
What happens in prepare_func_exit() in kernel/bpf/verifier.c when a bpf_loop() callback subprogram with a 9 to 16 byte BTF return type (ret_reg_pair == true) unwinds via unwind_exit?

In kernel/bpf/verifier.c:bpf_compute_subprog_ret_regs(), any static subprogram whose BTF return type has size in [9, 16] bytes gets ret_reg_pair = true, and neither check_func_args() nor btf_check_subprog_call() rejects such a subprogram when passed as ARG_PTR_TO_FUNC.

With helper_has_cb_pad(BPF_FUNC_loop) now returning true, the callee->in_callback_fn && unwinds path in prepare_func_exit() becomes reachable:

	callee = state->frame[state->curframe];
	r0 = &callee->regs[BPF_REG_0];
	nregs = bpf_ret_reg_pair(env, callee->subprogno) ? 2 : 1;
	for (i = 0; i < nregs; i++) {
		if (callee->regs[ret_regs[i]].type != PTR_TO_STACK)
			continue;
		verbose(env, "cannot return stack pointer to the caller\n");
		return -EINVAL;
	}
	...
		for (i = 0; i < nregs; i++) {
			u32 regno = ret_regs[i];

			bpf_diag_mod_begin(env, &caller->regs[regno], &callee->regs[regno],
					   BPF_DIAG_MOD_WRITE);
			caller->regs[regno] = callee->regs[regno];
			bpf_diag_mod_end(env);
		}

Even though bpf_call_cb() in bpf_loop() only propagates R0 to the landing pad, prepare_func_exit() uses nregs = 2 (ret_regs = { BPF_REG_0, BPF_REG_2 }) without checking !callee->in_callback_fn.

First, because set_loop_callback_state() initializes the callback's R2 to caller->regs[BPF_REG_3] (callback_ctx), passing a stack pointer as callback_ctx causes the PTR_TO_STACK check on callee->regs[BPF_REG_2] to falsely reject the unwind if the callback did not overwrite R2.

Second, prepare_func_exit() copies callee->regs[BPF_REG_2] into caller->regs[BPF_REG_2] before setting *insn_idx = callee->callsite to revisit the bpf_loop() call instruction. When check_helper_call() re-runs with env->cur_state->unwound == true, check_func_args() re-validates caller->regs[BPF_REG_1..5] before caller-saved registers are scrubbed. Because caller->regs[BPF_REG_2] (the callback_fn argument) was overwritten with the callback's R2, could check_func_args() fail verification or update meta.subprogno in update_loop_inline_state() to a different subprogram?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=5
author maintainer
#19 lore raw

Re: [RFC PATCH bpf-next v1 05/11] bpf: Let the callback of bpf_loop unwind

Quoted 6 lines · sashiko-bot

[...]
[Severity: Critical]
When helper_has_cb_pad(BPF_FUNC_loop) returns true, can inline_bpf_loop() in kernel/bpf/fixups.c emit a live BPF_JMP32_A instruction that jumps to a dead landing pad which is subsequently removed by dead code elimination, or leave a stray BPF_UNWIND_GOTO after a load instruction?

Same as on 04/11, fixed in the next version: the inlined loop gets a pad only when the verifier saw the pad of the caller, and the unwind_goto after the call becomes a no-op jump either way.

Quoted 5 lines

[...]
[Severity: Medium]
What happens in prepare_func_exit() in kernel/bpf/verifier.c when a bpf_loop() callback subprogram with a 9 to 16 byte BTF return type (ret_reg_pair == true) unwinds via unwind_exit?

Same as on 03/11: a callback hands over r0 alone in the next version, whatever its prototype says.

Quoted 1 line

[...]

#20 lore raw

Re: [RFC PATCH bpf-next v1 05/11] bpf: Let the callback of bpf_loop unwind

In reply to Kumar Kartikeya Dwivedi
TL;DR: If bpf_loop() is traced (fexit, kretprobe, function_graph), bpf_unwind_leave() may not find the BPF return address, so it may WARN and resume the program in a state the verifier never…
Quoted 1 line

bpf: Let the callback of bpf_loop unwind

TL;DR: If bpf_loop() is traced (fexit, kretprobe, function_graph), bpf_unwind_leave() may not find the BPF return address, so it may WARN and resume the program in a state the verifier never checked, possibly leaking pointers or references.

Quoted 3 lines
kernel/bpf/bpf_iter.c +0 -0

[ ... ]

Quoted 9 lines
kernel/bpf/bpf_iter.c +1 -1
@@ -749,7 +750,7 @@ const struct bpf_func_proto bpf_for_each_map_elem_proto = {
 	.arg4_type	= ARG_ANYTHING,
 };
 
-BPF_CALL_4(bpf_loop, u32, nr_loops, void *, callback_fn, void *, callback_ctx,
+BPF_CALL_x(4, noinline, bpf_loop, u32, nr_loops, void *, callback_fn, void *, callback_ctx,
 	   u64, flags)
 {
 	bpf_callback_t callback = (bpf_callback_t)callback_fn;

[ ... ]

Quoted 10 lines
kernel/bpf/bpf_iter.c +1 -1
@@ -766,13 +767,18 @@ BPF_CALL_4(bpf_loop, u32, nr_loops, void *, callback_fn, void *, callback_ctx,
 		return -E2BIG;
 
 	for (i = 0; i < nr_loops; i++) {
-		ret = callback((u64)i, (u64)(long)callback_ctx, 0, 0, 0);
+		bpf_call_cb(callback, i, callback_ctx, 0, 0, 0, ret, unwound);
 		/* return value: 0 - continue, 1 - stop and return */
 		if (ret)
 			return i + 1;
 	}

kernel/bpf/bpf_iter.c BPF_CALL_4(bpf_loop, u32, nr_loops, void *, callback_fn, void *, callback_ctx, · line 784

Quoted 10 lines
kernel/bpf/bpf_iter.c +5 -0
 
 	return i;
+
+unwound:
+	/* The callback left with unwind_exit: the loop is over, and so is the helper */
+	bpf_unwind_leave();
+	return ret;
 }
 
 const struct bpf_func_proto bpf_loop_proto = {

Can bpf_unwind_leave() find the BPF caller's return address here when bpf_loop() is being traced?

bpf_unwind_leave() looks for the return address of the BPF caller in bpf_loop()'s own return-address slot (__builtin_dwarf_cfa() - 1 on x86, the frame record LR on arm64). But bpf_loop() is a regular traceable kernel function. bpf_iter.o is built with the ftrace flags (kernel/bpf/ Makefile only strips them for percpu_freelist, bpf_lru_list, queue_stack_maps, lpm_trie, ringbuf and rqspinlock), bpf_loop() has no NOKPROBE_SYMBOL, and it is not in btf_id_deny. Several tracers change that slot or add a frame:

- An fexit/fsession BPF trampoline calls the original function with emit_rsb_call(&prog, orig_call, ...) in arch/x86/net/bpf_jit_comp.c, so the return address of bpf_loop() points into the trampoline image.

- kretprobe/rethook: arch_rethook_prepare() does
stack[0] = arch_rethook_trampoline.

- The function_graph tracer and fprobe exit handlers replace the slot with return_to_handler.

In each case bpf_unwind_target(*slot) does not find a BPF address. search_bpf_extables() goes through bpf_prog_ksym_find(), which returns NULL for a trampoline ksym (ksym->prog is false), and the kernel and module tables have no EX_TYPE_BPF entry there, so the address comes back unchanged. Then WARN_ON_ONCE(to == *slot) fires, which panics with panic_on_warn, and bpf_loop() returns ret through the tracer to the instruction after "call bpf_loop". On x86 and arm64 the JIT emits no code for unwind_goto, so the program continues along the normal path.

The verifier checked that path only against callback states that returned normally, where r0 is RET_INTEGER and the stack and references are as left by a normal return. It checked the unwind_exit state only against the pad. So the BPF program can run in a state the verifier never checked. For example:

- r0 can be the pointer or reference the callback unwound with (b2f03cf6322f lets a pointer and its reference reach the pad), now seen as a scalar. The pointer leaks and the reference is never released.

- Callback writes through callback_ctx on the unwind path only (for example a scalar over a spilled pointer) are seen by code that assumes the old slot type.

The trigger is a bpf_loop() call that the verifier does not inline (flags not provably 0, or two callbacks at one call site) whose callback has unwind_exit, while kretprobe:bpf_loop, an fexit program on bpf_loop(), or the function_graph tracer is active.

bpf_check_attach_target() already treats this hazard as real for BPF targets:

    /* unwind_exit would return to the trampoline that called the target */
    ...
    "Only fentry can attach to %s, which can unwind"

Nothing equivalent covers bpf_loop(), which this patch turns into a frame that unwinds. No later patch in the series addresses this (no notrace, NOKPROBE_SYMBOL or btf_id_deny change, and no ftrace_graph_ret_addr() or rethook lookup in bpf_unwind_leave()). The helpers that get pads later in the series (bpf_for_each_map_elem and its ops, bpf_find_vma, bpf_user_ringbuf_drain, bpf_rbtree_add) look to have the same problem.

Could bpf_loop() and the other helpers with pads be made untraceable (notrace plus NOKPROBE_SYMBOL, and a btf_id_deny entry for fexit/fmod_ret), or could bpf_unwind_leave() resolve a hijacked return address?

---
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/40612543702

author maintainer
#21 lore raw

Re: [RFC PATCH bpf-next v1 05/11] bpf: Let the callback of bpf_loop unwind

Quoted 5 lines · bot+bpf-ci

[...]
TL;DR: If bpf_loop() is traced (fexit, kretprobe, function_graph), bpf_unwind_leave() may not find the BPF return address, so it may WARN and resume the program in a state the verifier never checked, possibly leaking pointers or references.

Right. Every function with a pad is notrace and no kprobe target in the next version, as the verifier lets only fentry attach to a subprog that unwinds, with a test that fexit on bpf_loop() is refused.

Quoted 1 line

[...]

author maintainer
#22 lore raw

RFC PATCH 6/11 bpf: Let the callback of bpf_for_each_map_elem unwind

In reply to Kumar Kartikeya Dwivedi

The iterators of the array, the hash and the rhash maps get a pad. The array holds nothing. The hash and the rhash run the callback in an RCU section, which the pad drops the way the exit on a nonzero return does.

The verifier calls the op directly when it knows the map, and the pad of the op leaves to the pad after that call. bpf_for_each_map_elem(), for a map the verifier didn't know, calls the op the way it would call a callback, with bpf_call_cb() and a pad of its own, so that the op can leave to it, with the r0 of the callback either way. Without pads the helper calls the op the plain way, with the type of the op: the fallback of bpf_call_cb() has the type of a callback, which the op is not, and the kCFI check of such a kernel would say so. With that the verifier lets the callback of bpf_for_each_map_elem() unwind.

---
 kernel/bpf/arraymap.c | 11 ++++++++---
 kernel/bpf/bpf_iter.c | 22 +++++++++++++++++++++-
 kernel/bpf/hashtab.c  | 26 ++++++++++++++++++--------
 kernel/bpf/verifier.c |  1 +
 4 files changed, 48 insertions(+), 12 deletions(-)
kernel/bpf/arraymap.c +8 -3
index 133f189bf6d2..4c81c7c76145 100644
@@ -3,6 +3,7 @@
  * Copyright (c) 2016,2017 Facebook
  */
 #include <linux/bpf.h>
+#include <linux/bpf_unwind.h>
 #include <linux/btf.h>
 #include <linux/err.h>
 #include <linux/slab.h>
@@ -800,7 +801,7 @@ static const struct bpf_iter_seq_info iter_seq_info = {
 	.seq_priv_size		= sizeof(struct bpf_iter_seq_array_map_info),
 };
 
-static long bpf_for_each_array_elem(struct bpf_map *map, bpf_callback_t callback_fn,
+static noinline long bpf_for_each_array_elem(struct bpf_map *map, bpf_callback_t callback_fn,
 				    void *callback_ctx, u64 flags)
 {
 	u32 i, key, num_elems = 0;
@@ -823,14 +824,18 @@ static long bpf_for_each_array_elem(struct bpf_map *map, bpf_callback_t callback
 			val = array_map_elem_ptr(array, i);
 		num_elems++;
 		key = i;
-		ret = callback_fn((u64)(long)map, (u64)(long)&key,
-				  (u64)(long)val, (u64)(long)callback_ctx, 0);
+		bpf_call_cb(callback_fn, map, &key, val, callback_ctx, 0, ret, unwound);
 		/* return value: 0 - continue, 1 - stop and return */
 		if (ret)
 			break;
 	}
 
 	return num_elems;
+
+unwound:
+	/* The callback left with unwind_exit: nothing to drop here */
+	bpf_unwind_leave();
+	return ret;
 }
 
 static u64 array_map_mem_usage(const struct bpf_map *map)
kernel/bpf/bpf_iter.c +21 -1
index efd23c973115..4a1bf5a1b8d9 100644
@@ -734,10 +734,30 @@ int bpf_iter_run_prog(struct bpf_prog *prog, void *ctx)
 	return ret == 0 ? 0 : -EAGAIN;
 }
 
-BPF_CALL_4(bpf_for_each_map_elem, struct bpf_map *, map, void *, callback_fn,
+/*
+ * The op has a pad for the callback and leaves like a callback does when it is
+ * taken, so it is called like one, with the kCFI check for its own type. The
+ * verifier calls the op directly when it knows the map.
+ */
+BPF_CB_PAD_TYPE(__bpf_for_each_op, typeof(*((struct bpf_map_ops *)0)->map_for_each_callback));
+
+BPF_CALL_x(4, noinline, bpf_for_each_map_elem, struct bpf_map *, map, void *, callback_fn,
 	   void *, callback_ctx, u64, flags)
 {
+#if BPF_HAS_CB_PADS
+	u64 ret;
+
+	__bpf_call_cb(__bpf_for_each_op, map->ops->map_for_each_callback, map, callback_fn,
+		      callback_ctx, flags, 0, ret, unwound);
+	return ret;
+
+unwound:
+	bpf_unwind_leave();
+	return ret;
+#else
+	/* The plain call, with the type of the op: the fallback of bpf_call_cb() has a callback's */
 	return map->ops->map_for_each_callback(map, callback_fn, callback_ctx, flags);
+#endif
 }
 
 const struct bpf_func_proto bpf_for_each_map_elem_proto = {
kernel/bpf/hashtab.c +18 -8
index bf78e09f4d51..3e995dacfaa0 100644
@@ -3,6 +3,7 @@
  * Copyright (c) 2016 Facebook
  */
 #include <linux/bpf.h>
+#include <linux/bpf_unwind.h>
 #include <linux/btf.h>
 #include <linux/jhash.h>
 #include <linux/filter.h>
@@ -2498,7 +2499,7 @@ static const struct bpf_iter_seq_info iter_seq_info = {
 	.seq_priv_size		= sizeof(struct bpf_iter_seq_hash_map_info),
 };
 
-static long bpf_for_each_hash_elem(struct bpf_map *map, bpf_callback_t callback_fn,
+static noinline long bpf_for_each_hash_elem(struct bpf_map *map, bpf_callback_t callback_fn,
 				   void *callback_ctx, u64 flags)
 {
 	struct bpf_htab *htab = container_of(map, struct bpf_htab, map);
@@ -2537,8 +2538,7 @@ static long bpf_for_each_hash_elem(struct bpf_map *map, bpf_callback_t callback_
 				val = htab_elem_value(elem, map->key_size);
 			}
 			num_elems++;
-			ret = callback_fn((u64)(long)map, (u64)(long)key,
-					  (u64)(long)val, (u64)(long)callback_ctx, 0);
+			bpf_call_cb(callback_fn, map, key, val, callback_ctx, 0, ret, unwound);
 			/* return value: 0 - continue, 1 - stop and return */
 			if (ret) {
 				rcu_read_unlock();
@@ -2549,6 +2549,12 @@ static long bpf_for_each_hash_elem(struct bpf_map *map, bpf_callback_t callback_
 	}
 out:
 	return num_elems;
+
+unwound:
+	/* The callback left with unwind_exit, inside the RCU section of the bucket */
+	rcu_read_unlock();
+	bpf_unwind_leave();
+	return ret;
 }
 
 static u64 htab_map_mem_usage(const struct bpf_map *map)
@@ -3409,7 +3415,7 @@ static void rhtab_map_seq_show_elem(struct bpf_map *map, void *key, struct seq_f
 	seq_putc(m, '\n');
 }
 
-static long bpf_each_rhash_elem(struct bpf_map *map, bpf_callback_t callback_fn,
+static noinline long bpf_each_rhash_elem(struct bpf_map *map, bpf_callback_t callback_fn,
 				void *callback_ctx, u64 flags)
 {
 	struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
@@ -3433,10 +3439,8 @@ static long bpf_each_rhash_elem(struct bpf_map *map, bpf_callback_t callback_fn,
 		if (IS_ERR(elem))
 			break;
 		num_elems++;
-		ret = callback_fn((u64)(long)map,
-				  (u64)(long)elem->data,
-				  (u64)(long)rhtab_elem_value(elem, map->key_size),
-				  (u64)(long)callback_ctx, 0);
+		bpf_call_cb(callback_fn, map, elem->data, rhtab_elem_value(elem, map->key_size),
+			    callback_ctx, 0, ret, unwound);
 		if (ret)
 			break;
 
@@ -3445,6 +3449,12 @@ static long bpf_each_rhash_elem(struct bpf_map *map, bpf_callback_t callback_fn,
 	rcu_read_unlock();
 
 	return num_elems;
+
+unwound:
+	/* The callback left with unwind_exit, under RCU */
+	rcu_read_unlock();
+	bpf_unwind_leave();
+	return ret;
 }
 
 static u64 rhtab_map_mem_usage(const struct bpf_map *map)
kernel/bpf/verifier.c +1 -0
index 4b4d8479fa87..b2b72b3b32ac 100644
@@ -11407,6 +11407,7 @@ static bool helper_has_cb_pad(enum bpf_func_id func_id)
 {
 	switch (func_id) {
 	case BPF_FUNC_loop:
+	case BPF_FUNC_for_each_map_elem:
 		return true;
 	default:
 		return false;
Signature · 1 line
2.53.0
#23 lore raw

Re: [RFC PATCH bpf-next v1 06/11] bpf: Let the callback of bpf_for_each_map_elem unwind

TL;DR: - Array map callback may return a PTR_TO_MAP_KEY to dead kernel stack after unwind, possibly leaking stale stack - bpf_unwind_leave() may misbehave (WARN, wrong return) if the helper is traced
Quoted 1 line

bpf: Let the callback of bpf_for_each_map_elem unwind

TL;DR:
- Array map callback may return a PTR_TO_MAP_KEY to dead kernel stack after unwind, possibly leaking stale stack
- bpf_unwind_leave() may misbehave (WARN, wrong return) if the helper is traced

Quoted 4 lines
kernel/bpf/arraymap.c +0 -0
index 133f189bf6d2..4c81c7c76145 100644

[ ... ]

kernel/bpf/arraymap.c static long bpf_for_each_array_elem(struct bpf_map *map, bpf_callback_t callback · line 827

Quoted 7 lines
kernel/bpf/arraymap.c +1 -2
@@ -823,14 +824,18 @@ static long bpf_for_each_array_elem(struct bpf_map *map, bpf_callback_t callback
 			val = array_map_elem_ptr(array, i);
 		num_elems++;
 		key = i;
-		ret = callback_fn((u64)(long)map, (u64)(long)&key,
-				  (u64)(long)val, (u64)(long)callback_ctx, 0);
+		bpf_call_cb(callback_fn, map, &key, val, callback_ctx, 0, ret, unwound);

Can the PTR_TO_MAP_KEY handed to the callback here outlive the frame it points into?

&key is a local in the frame of bpf_for_each_array_elem(), and map_set_for_each_callback_args() types it as PTR_TO_MAP_KEY in R2 of the callback. With BPF_FUNC_for_each_map_elem now in helper_has_cb_pad(), the callback may leave with unwind_exit, and prepare_func_exit() from b2f03cf6322f hands the callback's r0 to the pad unchanged, except for PTR_TO_STACK, which it refuses.

A PTR_TO_MAP_KEY in r0 therefore reaches the pad after the call, where check_mem_access() still allows reads of key_size bytes through it. By then the helper frame has been popped (and, on the indirect path, the frame of bpf_for_each_map_elem() too), so the pointer refers to dead kernel stack that any later call or interrupt can overwrite. A program without allow_ptr_leaks/allow_uninit_stack would be able to read stale kernel stack through it.

The changelog of b2f03cf6322f mentions this case: "the key of the array iterator live in the frame of the helper ... the fix for that has to see to r0 too". Nothing between b2f03cf6322f and the end of this series adds that fix. Hash and rhash keys point into map elements rather than the helper frame, so only the array iterator is affected.

Should a PTR_TO_MAP_KEY r0 (or any pointer whose lifetime ends with the helper frame) be refused or turned into a scalar at the callback's unwind_exit before for_each_map_elem is opened up for array maps? Alternatively, could the key live somewhere that outlives the helper?

kernel/bpf/arraymap.c static long bpf_for_each_array_elem(struct bpf_map *map, bpf_callback_t callback · line 838

Quoted 11 lines
kernel/bpf/arraymap.c +5 -0
 		/* return value: 0 - continue, 1 - stop and return */
 		if (ret)
 			break;
 	}
 
 	return num_elems;
+
+unwound:
+	/* The callback left with unwind_exit: nothing to drop here */
+	bpf_unwind_leave();
+	return ret;

Does bpf_unwind_leave() work if the helper itself is being traced?

It reads the return-address slot of bpf_for_each_array_elem() and looks it up with bpf_unwind_target(), which only works if the slot still holds the address the BPF caller pushed. The same leave path is added to bpf_for_each_hash_elem(), bpf_each_rhash_elem() and bpf_for_each_map_elem(). All four are ordinary traceable functions: arraymap.o, hashtab.o and bpf_iter.o are not in the CFLAGS_REMOVE list of $(CC_FLAGS_FTRACE) in kernel/bpf/Makefile, none of the four is notrace, and none is in btf_id_deny.

If the function-graph tracer, a kretprobe/rethook, or a BPF fexit/fmod_ret trampoline is attached to one of them, the slot would hold return_to_handler, the rethook trampoline, or a trampoline address, and bpf_unwind_target() would find no entry. Then WARN_ON_ONCE(to == *slot) fires (fatal with panic_on_warn), and the exception comes back as a plain return. The BPF program carries on past the unwind_goto on the normal path, in a state the verifier only explored on the unwind edge.

bpf_check_attach_target() already guards the BPF-target version of this ("unwind_exit would return to the trampoline that called the target", "Only fentry can attach to %s, which can unwind"), but kernel-function targets that now carry pads get no equivalent check. bpf_loop() from 4e61c40fe5c0 has the same exposure.

Should these helpers be marked notrace and added to btf_id_deny, or should bpf_unwind_leave() recover the original return address (as ftrace_graph_ret_addr() and rethook do for unwinders) before the lookup?

---
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/40612543702

author maintainer
#24 lore raw

Re: [RFC PATCH bpf-next v1 06/11] bpf: Let the callback of bpf_for_each_map_elem unwind

Quoted 4 lines · bot+bpf-ci

[...]
TL;DR:
- Array map callback may return a PTR_TO_MAP_KEY to dead kernel stack after unwind, possibly leaking stale stack

Known class: the callback can store the key through callback_ctx today just the same. Ihor is fixing that for the arguments of a callback, and the fix has to see to r0 too.

Quoted 2 lines

- bpf_unwind_leave() may misbehave (WARN, wrong return) if the helper is traced

Right. Every function with a pad is notrace and no kprobe target in the next version, as the verifier lets only fentry attach to a subprog that unwinds, with a test that fexit on bpf_loop() is refused.

Quoted 1 line

[...]

author maintainer
#25 lore raw

RFC PATCH 7/11 bpf: Let the callback of bpf_find_vma unwind

In reply to Kumar Kartikeya Dwivedi

bpf_find_vma() runs the callback under the mmap lock and in an RCU section. The pad drops both and the reference on a foreign mm, the same way out as a return, and leaves with the r0 of the callback. With that the verifier lets the callback of bpf_find_vma() unwind.

---
 kernel/bpf/task_iter.c | 21 ++++++++++++++++++---
 kernel/bpf/verifier.c  |  1 +
 2 files changed, 19 insertions(+), 3 deletions(-)
kernel/bpf/task_iter.c +18 -3
index 2ee7b3b3c10b..ae67c97fe845 100644
@@ -6,6 +6,7 @@
 #include <linux/pid_namespace.h>
 #include <linux/fs.h>
 #include <linux/filter.h>
+#include <linux/bpf_unwind.h>
 #include <linux/bpf_mem_alloc.h>
 #include <linux/btf_ids.h>
 #include <linux/mm_types.h>
@@ -754,7 +755,7 @@ static struct bpf_iter_reg task_vma_reg_info = {
 	.show_fdinfo		= bpf_iter_task_show_fdinfo,
 };
 
-BPF_CALL_5(bpf_find_vma, struct task_struct *, task, u64, start,
+BPF_CALL_x(5, noinline, bpf_find_vma, struct task_struct *, task, u64, start,
 	   bpf_callback_t, callback_fn, void *, callback_ctx, u64, flags)
 {
 	struct mmap_unlock_irq_work *work;
@@ -762,6 +763,7 @@ BPF_CALL_5(bpf_find_vma, struct task_struct *, task, u64, start,
 	bool __maybe_unused mmput_needed = false;
 	struct mm_struct *mm;
 	int ret = -ENOENT;
+	u64 r0;
 
 	if (flags)
 		return -EINVAL;
@@ -815,8 +817,7 @@ BPF_CALL_5(bpf_find_vma, struct task_struct *, task, u64, start,
 		 * verifier checks it as an RCU read-side critical section.
 		 */
 		rcu_read_lock();
-		callback_fn((u64)(long)task, (u64)(long)vma,
-			    (u64)(long)callback_ctx, 0, 0);
+		bpf_call_cb(callback_fn, task, vma, callback_ctx, 0, 0, r0, unwound);
 		rcu_read_unlock();
 		ret = 0;
 	}
@@ -827,6 +828,20 @@ BPF_CALL_5(bpf_find_vma, struct task_struct *, task, u64, start,
 		mmput_async(mm);
 #endif
 	return ret;
+
+unwound:
+	/*
+	 * The callback left with unwind_exit, under the mmap lock and in the
+	 * RCU section. The way out is the same as after a return.
+	 */
+	rcu_read_unlock();
+	bpf_mmap_unlock_mm(work, mm);
+#ifdef CONFIG_MMU
+	if (mmput_needed)
+		mmput_async(mm);
+#endif
+	bpf_unwind_leave();
+	return r0;
 }
 
 const struct bpf_func_proto bpf_find_vma_proto = {
kernel/bpf/verifier.c +1 -0
index b2b72b3b32ac..3c39cc0f358a 100644
@@ -11408,6 +11408,7 @@ static bool helper_has_cb_pad(enum bpf_func_id func_id)
 	switch (func_id) {
 	case BPF_FUNC_loop:
 	case BPF_FUNC_for_each_map_elem:
+	case BPF_FUNC_find_vma:
 		return true;
 	default:
 		return false;
Signature · 1 line
2.53.0
#26 lore raw

Re: [RFC PATCH bpf-next v1 07/11] bpf: Let the callback of bpf_find_vma unwind

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] bpf: bpf_find_vma() unwound pad unlocks mmap_lock with IRQs disabled - [High] bpf: vma pointer…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] bpf: bpf_find_vma() unwound pad unlocks mmap_lock with IRQs disabled
- [High] bpf: vma pointer escapes bpf_find_vma() lock scope via r0 on unwind_exit --

commit d84887aac74ddde9372c2538e6e860df21d923db
Author: Kumar Kartikeya Dwivedi <[email protected]>

bpf: Let the callback of bpf_find_vma unwind

This commit adds an unwind landing pad to bpf_find_vma() that releases the RCU read lock, unlocks mmap_lock, drops any foreign mm reference, and returns the callback's r0 when the callback leaves via unwind_exit.

Quoted 4 lines
kernel/bpf/task_iter.c +0 -0
index 2ee7b3b3c10be..ae67c97fe845e 100644

[ ... ]

Quoted 6 lines
kernel/bpf/task_iter.c +2 -0
@@ -827,6 +828,20 @@ BPF_CALL_5(bpf_find_vma, struct task_struct *, task, u64, start,
 		mmput_async(mm);
 #endif
 	return ret;
+
+unwound:

kernel/bpf/task_iter.c BPF_CALL_5(bpf_find_vma, struct task_struct *, task, u64, start, · line 842

Quoted 10 lines
kernel/bpf/task_iter.c +10 -0
+	/*
+	 * The callback left with unwind_exit, under the mmap lock and in the
+	 * RCU section. The way out is the same as after a return.
+	 */
+	rcu_read_unlock();
+	bpf_mmap_unlock_mm(work, mm);
+#ifdef CONFIG_MMU
+	if (mmput_needed)
+		mmput_async(mm);
+#endif

[Severity: High]
Can the unwound pad end up calling mmap_read_unlock() and mmput_async() with hardware interrupts disabled?

When bpf_find_vma() is entered with interrupts enabled for a foreign task, the irqs_disabled() check passes, mmput_needed is set to true, and bpf_mmap_unlock_guard_get() returns NULL because !irqs_disabled():

kernel/bpf/mmap_unlock_work.h:bpf_mmap_unlock_guard_get() {
	struct mmap_unlock_irq_work *work;

	if (!irqs_disabled())
		return NULL;
    ...
}

Inside the callback, callback_ctx (R3) can point to a stack slot in the caller's frame. If the callback calls bpf_local_irq_save() on that caller stack slot and then executes unwind_exit, process_bpf_exit_full() calls check_resource_leak() with check_lock set to false because curframe > 0:

kernel/bpf/verifier.c:process_bpf_exit_full() {
	int err = check_resource_leak(env, exception_exit,
				      exception_exit || !env->cur_state->curframe,
				      exception_exit ? "bpf_throw" :
				      "BPF_EXIT instruction in main prog");
    ...
}

Unlike a normal callback return, unwind_exit sets state->unwound = true in prepare_func_exit(), so push_callback_call() returns 0 on revisit without verifying a second callback round and jumps via unwind_goto to the caller's pad where bpf_local_irq_restore() restores the caller's stack slot.

At runtime, execution enters the unwound label in bpf_find_vma() with hardware interrupts disabled while work is NULL and mmput_needed is true. bpf_mmap_unlock_mm(NULL, mm) then calls mmap_read_unlock(mm) directly with IRQs disabled instead of deferring via irq_work, and mmput_async(mm) is also called with IRQs disabled, risking a deadlock on rq_lock during reader wakeup and triggering an invalid wait context under CONFIG_PREEMPT_RT.

kernel/bpf/task_iter.c BPF_CALL_5(bpf_find_vma, struct task_struct *, task, u64, start, · line 845

Quoted 3 lines
kernel/bpf/task_iter.c +2 -0
+	bpf_unwind_leave();
+	return r0;
 }

[Severity: High]
What prevents the callback from escaping the vma pointer (or pointers walked from it, such as vma->vm_file, &vma->vm_file->f_path, or vma->vm_file->f_inode) in r0 via unwind_exit after mmap_lock, rcu_read_lock(), and the foreign mm reference have been dropped?

In set_find_vma_callback_state(), R2 (struct vm_area_struct *vma) is marked as a plain PTR_TO_BTF_ID:

kernel/bpf/verifier.c:set_find_vma_callback_state() {
    ...
	callee->regs[BPF_REG_2].type = PTR_TO_BTF_ID;
	__mark_reg_known_zero(&callee->regs[BPF_REG_2]);
	callee->regs[BPF_REG_2].btf =  btf_vmlinux;
	callee->regs[BPF_REG_2].btf_id = btf_tracing_ids[BTF_TRACING_TYPE_VMA];
    ...
}

When the callback leaves via unwind_exit, prepare_func_exit() skips the SCALAR_VALUE check on R0 and copies callee->regs[BPF_REG_0] into caller->regs[BPF_REG_0]:

kernel/bpf/verifier.c:prepare_func_exit() {
    ...
	if (callee->in_callback_fn && !unwinds) {
		if (r0->type != SCALAR_VALUE) {
			verbose(env, "R0 not a scalar value\n");
			return -EACCES;
		}
    ...
	} else {
		for (i = 0; i < nregs; i++) {
			u32 regno = ret_regs[i];

			bpf_diag_mod_begin(env, &caller->regs[regno], &callee->regs[regno],
					   BPF_DIAG_MOD_WRITE);
			caller->regs[regno] = callee->regs[regno];
			bpf_diag_mod_end(env);
		}
	}
    ...
}

Once the unwound pad in bpf_find_vma() runs rcu_read_unlock(), bpf_mmap_unlock_mm(work, mm), and mmput_async(mm), the caller's unwind pad receives the plain PTR_TO_BTF_ID in R0 outside the lock and RCU scope. If the VMA and its backing file or inode are concurrently unmapped and freed, passing that dangling R0 pointer to helpers accepting ARG_PTR_TO_BTF_ID (such as bpf_ima_file_hash(), bpf_ima_inode_hash(), bpf_d_path(), or bpf_inode_storage_get()) can trigger a use-after-free.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=7
author maintainer
#27 lore raw

Re: [RFC PATCH bpf-next v1 07/11] bpf: Let the callback of bpf_find_vma unwind

Quoted 4 lines · sashiko-bot

[...]
[Severity: High]
Can the unwound pad end up calling mmap_read_unlock() and mmput_async() with hardware interrupts disabled?

A callback can return into the helper with IRQs disabled today, with the flags saved through callback_ctx into the frame of the caller: the exit of a callback does not check the IRQ state, and the helper unlocks the same way on that path. The pad changes nothing there.

Quoted 6 lines

[...]
[Severity: High]
What prevents the callback from escaping the vma pointer (or pointers walked from it, such as vma->vm_file, &vma->vm_file->f_path, or vma->vm_file->f_inode) in r0 via unwind_exit after mmap_lock, rcu_read_lock(), and the foreign mm reference have been dropped?

Known class: the callback can store the vma through callback_ctx today just the same. Ihor is fixing that for the arguments of a callback, and the fix has to see to r0 too.

Quoted 1 line

[...]

author maintainer
#28 lore raw

RFC PATCH 8/11 bpf: Let the callback of bpf_user_ringbuf_drain unwind

In reply to Kumar Kartikeya Dwivedi

bpf_user_ringbuf_drain() runs the callback with the busy bit set and a sample peeked. The pad consumes the sample as a return does, and counts it, since the wakeup at the end is sent for samples consumed. Then it leaves the loop through the door the error paths use, which clears the busy bit and sends the wakeup, and leaves with the r0 of the callback. With that the verifier lets the callback of bpf_user_ringbuf_drain() unwind.

---
 kernel/bpf/ringbuf.c  | 23 +++++++++++++++++++++--
 kernel/bpf/verifier.c |  1 +
 2 files changed, 22 insertions(+), 2 deletions(-)
kernel/bpf/ringbuf.c +21 -2
index 447863ece26a..fdd997f36f73 100644
@@ -1,5 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0
 #include <linux/bpf.h>
+#include <linux/bpf_unwind.h>
 #include <linux/btf.h>
 #include <linux/err.h>
 #include <linux/irq_work.h>
@@ -814,13 +815,14 @@ static void __bpf_user_ringbuf_sample_release(struct bpf_ringbuf *rb, size_t siz
 	smp_store_release(&rb->consumer_pos, consumer_pos + rounded_size);
 }
 
-BPF_CALL_4(bpf_user_ringbuf_drain, struct bpf_map *, map,
+BPF_CALL_x(4, noinline, bpf_user_ringbuf_drain, struct bpf_map *, map,
 	   void *, callback_fn, void *, callback_ctx, u64, flags)
 {
 	struct bpf_ringbuf *rb;
 	long samples, discarded_samples = 0, ret = 0;
 	bpf_callback_t callback = (bpf_callback_t)callback_fn;
 	u64 wakeup_flags = BPF_RB_NO_WAKEUP | BPF_RB_FORCE_WAKEUP;
+	bool unwound = false;
 	int busy = 0;
 
 	if (unlikely(flags & ~wakeup_flags))
@@ -852,8 +854,23 @@ BPF_CALL_4(bpf_user_ringbuf_drain, struct bpf_map *, map,
 		}
 
 		bpf_dynptr_init(&dynptr, sample, BPF_DYNPTR_TYPE_LOCAL, 0, size);
-		ret = callback((uintptr_t)&dynptr, (uintptr_t)callback_ctx, 0, 0, 0);
+		bpf_call_cb(callback, &dynptr, callback_ctx, 0, 0, 0, ret, unwound_at);
+		if (0) {
+unwound_at:
+			/*
+			 * The callback left with unwind_exit. The sample is
+			 * consumed and counted like after a return, so that
+			 * the wakeup below is sent for it, and the helper
+			 * leaves through the door below, as the error paths
+			 * do, with the r0 of the callback.
+			 */
+			unwound = true;
+		}
 		__bpf_user_ringbuf_sample_release(rb, size, flags);
+		if (unwound) {
+			samples++;
+			goto schedule_work_return;
+		}
 	}
 	ret = samples - discarded_samples;
 
@@ -867,6 +884,8 @@ BPF_CALL_4(bpf_user_ringbuf_drain, struct bpf_map *, map,
 		irq_work_queue(&rb->work);
 	else if (!(flags & BPF_RB_NO_WAKEUP) && samples > 0)
 		irq_work_queue(&rb->work);
+	if (unwound)
+		bpf_unwind_leave();
 	return ret;
 }
 
kernel/bpf/verifier.c +1 -0
index 3c39cc0f358a..fd63ff36eb90 100644
@@ -11409,6 +11409,7 @@ static bool helper_has_cb_pad(enum bpf_func_id func_id)
 	case BPF_FUNC_loop:
 	case BPF_FUNC_for_each_map_elem:
 	case BPF_FUNC_find_vma:
+	case BPF_FUNC_user_ringbuf_drain:
 		return true;
 	default:
 		return false;
Signature · 1 line
2.53.0
#29 lore raw

Re: [RFC PATCH bpf-next v1 08/11] bpf: Let the callback of bpf_user_ringbuf_drain unwind

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Critical] bpf: missing callback dynptr invalidation on bpf_user_ringbuf_drain() unwind --

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] bpf: missing callback dynptr invalidation on bpf_user_ringbuf_drain() unwind --

commit 5e41f31a219e475ec387e971ce587d69bc948017
Author: Kumar Kartikeya Dwivedi <[email protected]>

bpf: Let the callback of bpf_user_ringbuf_drain unwind

This commit adds an unwind pad to bpf_user_ringbuf_drain() so that its callback can exit via unwind_exit while releasing the sample, clearing the busy bit, and queuing any pending wakeup. It also adds BPF_FUNC_user_ringbuf_drain to helper_has_cb_pad() in the verifier.

Quoted 4 lines
kernel/bpf/ringbuf.c +0 -0
index 447863ece26a3..fdd997f36f73d 100644

[ ... ]

Quoted 39 lines
kernel/bpf/ringbuf.c +18 -1
@@ -852,8 +854,23 @@ BPF_CALL_4(bpf_user_ringbuf_drain, struct bpf_map *, map,
 		}
 
 		bpf_dynptr_init(&dynptr, sample, BPF_DYNPTR_TYPE_LOCAL, 0, size);
-		ret = callback((uintptr_t)&dynptr, (uintptr_t)callback_ctx, 0, 0, 0);
+		bpf_call_cb(callback, &dynptr, callback_ctx, 0, 0, 0, ret, unwound_at);
+		if (0) {
+unwound_at:
+			/*
+			 * The callback left with unwind_exit. The sample is
+			 * consumed and counted like after a return, so that
+			 * the wakeup below is sent for it, and the helper
+			 * leaves through the door below, as the error paths
+			 * do, with the r0 of the callback.
+			 */
+			unwound = true;
+		}
 		__bpf_user_ringbuf_sample_release(rb, size, flags);
+		if (unwound) {
+			samples++;
+			goto schedule_work_return;
+		}
 	}
 	ret = samples - discarded_samples;
 
@@ -867,6 +884,8 @@ BPF_CALL_4(bpf_user_ringbuf_drain, struct bpf_map *, map,
 		irq_work_queue(&rb->work);
 	else if (!(flags & BPF_RB_NO_WAKEUP) && samples > 0)
 		irq_work_queue(&rb->work);
+	if (unwound)
+		bpf_unwind_leave();
 	return ret;
 }
 
kernel/bpf/verifier.c +0 -0
index 3c39cc0f358a1..fd63ff36eb909 100644

kernel/bpf/verifier.c static bool helper_has_cb_pad(enum bpf_func_id func_id) · line 11413

Quoted 5 lines
kernel/bpf/verifier.c +1 -0
@@ -11409,6 +11409,7 @@ static bool helper_has_cb_pad(enum bpf_func_id func_id)
 	case BPF_FUNC_loop:
 	case BPF_FUNC_for_each_map_elem:
 	case BPF_FUNC_find_vma:
+	case BPF_FUNC_user_ringbuf_drain:
 		return true;

[Severity: Critical]
Can a callback for bpf_user_ringbuf_drain() smuggle its R1 dynptr (or a slice derived from it via bpf_dynptr_data() or bpf_dynptr_slice()) out to the caller's unwind pad in R0 when exiting via unwind_exit?

In bpf_user_ringbuf_drain(), struct bpf_dynptr_kern dynptr lives on the C stack frame of bpf_user_ringbuf_drain(). When the callback exits via unwind_exit and jumps to unwound_at, bpf_user_ringbuf_drain() releases the sample with __bpf_user_ringbuf_sample_release(), clears rb->busy, and returns ret (the callback's R0) to the caller's unwind pad after its stack frame is popped.

In the verifier, set_user_ringbuf_callback_state() initializes the callback's R1 using mark_dynptr_cb_reg(), which sets R1 to CONST_PTR_TO_DYNPTR with a fresh id that is not backed by a STACK_DYNPTR slot in the callee's BPF stack:

kernel/bpf/verifier.c:set_user_ringbuf_callback_state() {
	...
	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);
	callee->regs[BPF_REG_2] = caller->regs[BPF_REG_3];
	...
}

When the callback executes unwind_exit, prepare_func_exit() only rejects PTR_TO_STACK in R0, copies callee->regs[BPF_REG_0] into caller->regs[BPF_REG_0], and calls destroy_dynptrs_in_stack_slots(), which only invalidates STACK_DYNPTR slots in the callee's stack frame:

kernel/bpf/verifier.c:prepare_func_exit() {
	...
			caller->regs[regno] = callee->regs[regno];
			bpf_diag_mod_end(env);
		}
	}

	/* Invalidate callee-local dynptrs and their slices before the frame goes away. */
	err = destroy_dynptrs_in_stack_slots(env, callee, 0,
					     callee->allocated_stack / BPF_REG_SIZE - 1);
	...
}

On the revisit of BPF_FUNC_user_ringbuf_drain in check_helper_call(), unwound_r0 is preserved across caller-saved register scrubbing without invalidating the callback's dynptr id:

kernel/bpf/verifier.c:check_helper_call() {
	...
	if (env->cur_state->unwound) {
		regs[BPF_REG_0] = unwound_r0;
		goto unwound;
	}
	...
}

Wouldn't this leave the caller's unwind pad with a live CONST_PTR_TO_DYNPTR pointing to the popped C stack frame of bpf_user_ringbuf_drain(), or a live PTR_TO_MEM slice pointing to the already-released user ringbuf sample?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=8
author maintainer
#30 lore raw

Re: [RFC PATCH bpf-next v1 08/11] bpf: Let the callback of bpf_user_ringbuf_drain unwind

Quoted 121 lines · On Wed, 28 Oct 2026 at 02:01, <[email protected]> wrote:

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] bpf: missing callback dynptr invalidation on bpf_user_ringbuf_drain() unwind --

commit 5e41f31a219e475ec387e971ce587d69bc948017
Author: Kumar Kartikeya Dwivedi <[email protected]>

bpf: Let the callback of bpf_user_ringbuf_drain unwind

This commit adds an unwind pad to bpf_user_ringbuf_drain() so that its callback can exit via unwind_exit while releasing the sample, clearing the busy bit, and queuing any pending wakeup. It also adds BPF_FUNC_user_ringbuf_drain to helper_has_cb_pad() in the verifier.

Quoted 4 lines
kernel/bpf/ringbuf.c +0 -0
index 447863ece26a3..fdd997f36f73d 100644

[ ... ]

Quoted 39 lines
kernel/bpf/ringbuf.c +18 -1
@@ -852,8 +854,23 @@ BPF_CALL_4(bpf_user_ringbuf_drain, struct bpf_map *, map,
              }
 
              bpf_dynptr_init(&dynptr, sample, BPF_DYNPTR_TYPE_LOCAL, 0, size);
-             ret = callback((uintptr_t)&dynptr, (uintptr_t)callback_ctx, 0, 0, 0);
+             bpf_call_cb(callback, &dynptr, callback_ctx, 0, 0, 0, ret, unwound_at);
+             if (0) {
+unwound_at:
+                     /*
+                      * The callback left with unwind_exit. The sample is
+                      * consumed and counted like after a return, so that
+                      * the wakeup below is sent for it, and the helper
+                      * leaves through the door below, as the error paths
+                      * do, with the r0 of the callback.
+                      */
+                     unwound = true;
+             }
              __bpf_user_ringbuf_sample_release(rb, size, flags);
+             if (unwound) {
+                     samples++;
+                     goto schedule_work_return;
+             }
      }
      ret = samples - discarded_samples;
 
@@ -867,6 +884,8 @@ BPF_CALL_4(bpf_user_ringbuf_drain, struct bpf_map *, map,
              irq_work_queue(&rb->work);
      else if (!(flags & BPF_RB_NO_WAKEUP) && samples > 0)
              irq_work_queue(&rb->work);
+     if (unwound)
+             bpf_unwind_leave();
      return ret;
 }
 
kernel/bpf/verifier.c +0 -0
index 3c39cc0f358a1..fd63ff36eb909 100644

kernel/bpf/verifier.c static bool helper_has_cb_pad(enum bpf_func_id func_id) · line 11413

Quoted 5 lines
kernel/bpf/verifier.c +1 -0
@@ -11409,6 +11409,7 @@ static bool helper_has_cb_pad(enum bpf_func_id func_id)
      case BPF_FUNC_loop:
      case BPF_FUNC_for_each_map_elem:
      case BPF_FUNC_find_vma:
+     case BPF_FUNC_user_ringbuf_drain:
              return true;

[Severity: Critical]
Can a callback for bpf_user_ringbuf_drain() smuggle its R1 dynptr (or a slice derived from it via bpf_dynptr_data() or bpf_dynptr_slice()) out to the caller's unwind pad in R0 when exiting via unwind_exit?

In bpf_user_ringbuf_drain(), struct bpf_dynptr_kern dynptr lives on the C stack frame of bpf_user_ringbuf_drain(). When the callback exits via unwind_exit and jumps to unwound_at, bpf_user_ringbuf_drain() releases the sample with __bpf_user_ringbuf_sample_release(), clears rb->busy, and returns ret (the callback's R0) to the caller's unwind pad after its stack frame is popped.

In the verifier, set_user_ringbuf_callback_state() initializes the callback's R1 using mark_dynptr_cb_reg(), which sets R1 to CONST_PTR_TO_DYNPTR with a fresh id that is not backed by a STACK_DYNPTR slot in the callee's BPF stack:

kernel/bpf/verifier.c:set_user_ringbuf_callback_state() {
        ...
        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);
        callee->regs[BPF_REG_2] = caller->regs[BPF_REG_3];
        ...
}

When the callback executes unwind_exit, prepare_func_exit() only rejects PTR_TO_STACK in R0, copies callee->regs[BPF_REG_0] into caller->regs[BPF_REG_0], and calls destroy_dynptrs_in_stack_slots(), which only invalidates STACK_DYNPTR slots in the callee's stack frame:

kernel/bpf/verifier.c:prepare_func_exit() {
        ...
                        caller->regs[regno] = callee->regs[regno];
                        bpf_diag_mod_end(env);
                }
        }

        /* Invalidate callee-local dynptrs and their slices before the frame goes away. */
        err = destroy_dynptrs_in_stack_slots(env, callee, 0,
                                             callee->allocated_stack / BPF_REG_SIZE - 1);
        ...
}

On the revisit of BPF_FUNC_user_ringbuf_drain in check_helper_call(), unwound_r0 is preserved across caller-saved register scrubbing without invalidating the callback's dynptr id:

kernel/bpf/verifier.c:check_helper_call() {
        ...
        if (env->cur_state->unwound) {
                regs[BPF_REG_0] = unwound_r0;
                goto unwound;
        }
        ...
}
Quoted 3 lines

Wouldn't this leave the caller's unwind pad with a live CONST_PTR_TO_DYNPTR pointing to the popped C stack frame of bpf_user_ringbuf_drain(), or a live PTR_TO_MEM slice pointing to the already-released user ringbuf sample?

Known bug which Ihor is already fixing.

Quoted 3 lines

--
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=8

author maintainer
#31 lore raw

RFC PATCH 9/11 bpf: Let the comparator of bpf_rbtree_add unwind

In reply to Kumar Kartikeya Dwivedi

When the comparator unwinds the node is not in the tree, and the caller has let go of it: the kfunc releases the node whichever way it returns. The pad drops it, as the error path for a node that is already in a tree does, and leaves. The lock on the root is the caller's, and its pad is checked for it like any exit. Both spellings of the kfunc get it, each with its own copy of the body: the pad leaves from the frame that the BPF code called, and bpf_rbtree_add_impl() calling bpf_rbtree_add() is a tail jump on x86-64 but a call with a frame of its own on arm64, where the pad would rewrite a return address into that frame and the exception would come back as a return of 0.

The kfunc returns long in both spellings: the pad leaves with the r0 of the comparator, and an int would cut it to 32 bits. The change shows in BTF, and libbpf takes integers of any width as compatible, so a declaration that says int keeps loading.

On the revisit of the call after the comparator unwound, the node is not in the tree: the kfunc dropped it. So the verifier releases the reference, and every register that held the node is gone with it, rather than turning it into a non-owning reference to a node in the tree. The comparator was handed the node too, as a non-owning reference, and may have unwound with it in r0, so every non-owning reference goes, as it does at an unlock. With that the verifier lets the comparator of bpf_rbtree_add() unwind, in both spellings.

---
 kernel/bpf/helpers.c  | 50 +++++++++++++++++++++++++++++--------------
 kernel/bpf/verifier.c | 20 ++++++++++++++---
 2 files changed, 51 insertions(+), 19 deletions(-)
kernel/bpf/helpers.c +34 -16
index 648abe1ba63a..f8708b7181d7 100644
@@ -2,6 +2,7 @@
 /* Copyright (c) 2011-2014 PLUMgrid, http://plumgrid.com
  */
 #include <linux/bpf.h>
+#include <linux/bpf_unwind.h>
 #include <linux/btf.h>
 #include <linux/bpf-cgroup.h>
 #include <linux/cgroup.h>
@@ -2761,17 +2762,20 @@ __bpf_kfunc struct bpf_rb_node *bpf_rbtree_remove(struct bpf_rb_root *root,
 	return (struct bpf_rb_node *)n;
 }
 
-/* Need to copy rbtree_add_cached's logic here because our 'less' is a BPF
- * program
+/*
+ * Need to copy rbtree_add_cached's logic here because our 'less' is a BPF
+ * program. The pad leaves from the frame that the BPF code called, so the body
+ * is inlined into both spellings of the kfunc.
  */
-static int __bpf_rbtree_add(struct bpf_rb_root *root,
-			    struct bpf_rb_node_kern *node,
-			    void *less, struct btf_record *rec, u64 off)
+static __always_inline long __bpf_rbtree_add(struct bpf_rb_root *root,
+					     struct bpf_rb_node_kern *node,
+					     void *less, struct btf_record *rec, u64 off)
 {
 	struct rb_node **link = &((struct rb_root_cached *)root)->rb_root.rb_node;
 	struct rb_node *parent = NULL, *n = &node->rb_node;
 	bpf_callback_t cb = (bpf_callback_t)less;
 	bool leftmost = true;
+	u64 r0;
 
 	/* node->owner != NULL implies !RB_EMPTY_NODE(n), no need to separately
 	 * check the latter
@@ -2784,7 +2788,8 @@ static int __bpf_rbtree_add(struct bpf_rb_root *root,
 
 	while (*link) {
 		parent = *link;
-		if (cb((uintptr_t)node, (uintptr_t)parent, 0, 0, 0)) {
+		bpf_call_cb(cb, node, parent, 0, 0, 0, r0, unwound);
+		if (r0) {
 			link = &parent->rb_left;
 		} else {
 			link = &parent->rb_right;
@@ -2796,6 +2801,15 @@ static int __bpf_rbtree_add(struct bpf_rb_root *root,
 	rb_insert_color_cached(n, (struct rb_root_cached *)root, leftmost);
 	WRITE_ONCE(node->owner, root);
 	return 0;
+
+unwound:
+	/*
+	 * The comparator left with unwind_exit. The node is not in the tree, and
+	 * the caller has let go of it: drop it, as the error path above does.
+	 */
+	__bpf_obj_drop_impl((void *)n - off, rec, false);
+	bpf_unwind_leave();
+	return r0;
 }
 
 /**
@@ -2809,24 +2823,28 @@ static int __bpf_rbtree_add(struct bpf_rb_root *root,
  * Insert @node into @root using @less. The verifier rewrites @meta and @off;
  * BPF programs do not set them.
  *
- * Return: 0 on success, or %-EINVAL if @node is already linked in a tree.
+ * Return: 0 on success, or %-EINVAL if @node is already linked in a tree. When
+ * @less unwinds, what it unwound with, to the pad after the call.
  */
-__bpf_kfunc int bpf_rbtree_add(struct bpf_rb_root *root,
-			       struct bpf_rb_node *node,
-			       bool (less)(struct bpf_rb_node *a, const struct bpf_rb_node *b),
-			       struct btf_struct_meta *meta,
-			       u64 off)
+__bpf_kfunc long bpf_rbtree_add(struct bpf_rb_root *root,
+				struct bpf_rb_node *node,
+				bool (less)(struct bpf_rb_node *a, const struct bpf_rb_node *b),
+				struct btf_struct_meta *meta,
+				u64 off)
 {
 	struct bpf_rb_node_kern *n = (void *)node;
 
 	return __bpf_rbtree_add(root, n, (void *)less, meta ? meta->record : NULL, off);
 }
 
-__bpf_kfunc int bpf_rbtree_add_impl(struct bpf_rb_root *root, struct bpf_rb_node *node,
-				    bool (less)(struct bpf_rb_node *a, const struct bpf_rb_node *b),
-				    void *meta__ign, u64 off)
+__bpf_kfunc long bpf_rbtree_add_impl(struct bpf_rb_root *root, struct bpf_rb_node *node,
+				     bool (less)(struct bpf_rb_node *a, const struct bpf_rb_node *b),
+				     void *meta__ign, u64 off)
 {
-	return bpf_rbtree_add(root, node, less, meta__ign, off);
+	struct btf_struct_meta *meta = meta__ign;
+	struct bpf_rb_node_kern *n = (void *)node;
+
+	return __bpf_rbtree_add(root, n, (void *)less, meta ? meta->record : NULL, off);
 }
 
 __bpf_kfunc struct bpf_rb_node *bpf_rbtree_first(struct bpf_rb_root *root)
kernel/bpf/verifier.c +17 -3
index fd63ff36eb90..50c3d44c37e1 100644
@@ -11416,10 +11416,10 @@ static bool helper_has_cb_pad(enum bpf_func_id func_id)
 	}
 }
 
-/* The kfunc calls its callback with a pad. None yet. */
+/* The kfunc calls its callback with a pad */
 static bool kfunc_has_cb_pad(u32 btf_id)
 {
-	return false;
+	return is_bpf_rbtree_add_kfunc(btf_id);
 }
 
 static bool insn_has_cb_pad(struct bpf_insn *insn)
@@ -15738,7 +15738,21 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
 		id = regs[BPF_REG_2].id;
 		insn_aux->insert_off = regs[BPF_REG_2].var_off.value;
 		insn_aux->kptr_struct_meta = btf_find_struct_meta(meta.arg_btf, meta.arg_btf_id);
-		ref_convert_owning_non_owning(env, id);
+		/*
+		 * The comparator unwound: the node never went in, and the
+		 * kfunc dropped it on its way to the pad. The comparator was
+		 * handed the node too, and may have unwound with it in r0, so
+		 * every non-owning reference goes. Otherwise the node is in
+		 * the tree now.
+		 */
+		if (env->cur_state->unwound) {
+			invalidate_non_owning_refs(env);
+			err = release_reference(env, id);
+			if (err)
+				return err;
+		} else {
+			ref_convert_owning_non_owning(env, id);
+		}
 	}
 
 	if (meta.func_id == special_kfunc_list[KF_bpf_throw]) {
Signature · 1 line
2.53.0
#32 lore raw

Re: [RFC PATCH bpf-next v1 09/11] bpf: Let the comparator of bpf_rbtree_add unwind

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Critical] bpf: clobbered R2 on bpf_rbtree_add() callback unwind causes UAF - [High] bpf…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] bpf: clobbered R2 on bpf_rbtree_add() callback unwind causes UAF
- [High] bpf: __bpf_rbtree_add() leaves node->owner poisoned on comparator unwind --

commit 55e1518aab2a62c50b8098afe20794d027a69689
Author: Kumar Kartikeya Dwivedi <[email protected]>

bpf: Let the comparator of bpf_rbtree_add unwind

This commit allows the comparator callback of bpf_rbtree_add() and bpf_rbtree_add_impl() to unwind via unwind_exit. When the comparator unwinds, the kfunc drops the uninserted node before leaving to the landing pad, and the verifier invalidates non-owning references and releases the caller's owning reference on revisit.

Quoted 4 lines
kernel/bpf/helpers.c +0 -0
index 648abe1ba63ae..f8708b7181d72 100644

[ ... ]

Quoted 14 lines
kernel/bpf/helpers.c +2 -1
@@ -2784,7 +2788,8 @@ static int __bpf_rbtree_add(struct bpf_rb_root *root,
 
 	while (*link) {
 		parent = *link;
-		if (cb((uintptr_t)node, (uintptr_t)parent, 0, 0, 0)) {
+		bpf_call_cb(cb, node, parent, 0, 0, 0, r0, unwound);
+		if (r0) {
 			link = &parent->rb_left;
 		} else {
 			link = &parent->rb_right;
@@ -2796,6 +2801,15 @@ static int __bpf_rbtree_add(struct bpf_rb_root *root,
 	rb_insert_color_cached(n, (struct rb_root_cached *)root, leftmost);
 	WRITE_ONCE(node->owner, root);
 	return 0;

kernel/bpf/helpers.c static int __bpf_rbtree_add(struct bpf_rb_root *root, · line 2813

Quoted 10 lines
kernel/bpf/helpers.c +9 -0
+
+unwound:
+	/*
+	 * The comparator left with unwind_exit. The node is not in the tree, and
+	 * the caller has let go of it: drop it, as the error path above does.
+	 */
+	__bpf_obj_drop_impl((void *)n - off, rec, false);
+	bpf_unwind_leave();
+	return r0;
 }

[Severity: High]
Does the unwound path in __bpf_rbtree_add() leave node->owner poisoned with BPF_PTR_POISON when the object survives __bpf_obj_drop_impl()?

At the start of __bpf_rbtree_add() in kernel/bpf/helpers.c, node->owner is transitioned from NULL to BPF_PTR_POISON before the traversal loop:

	if (cmpxchg(&node->owner, NULL, BPF_PTR_POISON)) {
		/* Only called from BPF prog, no need to migrate_disable */
		__bpf_obj_drop_impl((void *)n - off, rec, false);
		return -EINVAL;
	}

When the comparator callback cb exits via unwind_exit during the while (*link) loop, control jumps to unwound after cmpxchg() has already set node->owner to BPF_PTR_POISON, and before WRITE_ONCE(node->owner, root) is reached.

If the object contains a bpf_refcount field and another active reference to it exists (for example via bpf_refcount_acquire() before calling bpf_rbtree_add() or inside the comparator before unwinding), __bpf_obj_drop_impl() decrements the refcount and returns without freeing the object:

	if (rec && rec->refcount_off >= 0 &&
	    !refcount_dec_and_test((refcount_t *)(p + rec->refcount_off))) {
		return;
	}

Unlike bpf_rb_root_free(), which clears node->owner back to NULL before dropping the reference:

	/* Ensure __bpf_rbtree_add() sees the node as unlinked. */
	smp_store_release(&node->owner, NULL);
	__bpf_obj_drop_impl(obj, field->graph_root.value_rec, false);

the unwound path in __bpf_rbtree_add() leaves node->owner set to BPF_PTR_POISON on the surviving unlinked object, causing any subsequent bpf_rbtree_add() or bpf_rbtree_add_impl() call on that object to fail the cmpxchg() check with -EINVAL. Should node->owner be reset to NULL before calling __bpf_obj_drop_impl() in the unwound path?

Quoted 4 lines
kernel/bpf/verifier.c +0 -0
index fd63ff36eb909..50c3d44c37e15 100644

[ ... ]

Quoted 11 lines
kernel/bpf/verifier.c +6 -1
@@ -15738,7 +15738,21 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
 		id = regs[BPF_REG_2].id;
 		insn_aux->insert_off = regs[BPF_REG_2].var_off.value;
 		insn_aux->kptr_struct_meta = btf_find_struct_meta(meta.arg_btf, meta.arg_btf_id);
-		ref_convert_owning_non_owning(env, id);
+		/*
+		 * The comparator unwound: the node never went in, and the
+		 * kfunc dropped it on its way to the pad. The comparator was
+		 * handed the node too, and may have unwound with it in r0, so
+		 * every non-owning reference goes. Otherwise the node is in
+		 * the tree now.

kernel/bpf/verifier.c static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn, · line 15756

Quoted 10 lines
kernel/bpf/verifier.c +9 -0
+		 */
+		if (env->cur_state->unwound) {
+			invalidate_non_owning_refs(env);
+			err = release_reference(env, id);
+			if (err)
+				return err;
+		} else {
+			ref_convert_owning_non_owning(env, id);
+		}
 	}

[Severity: Critical]
Can an unwinding comparator callback with a two-register return type clobber caller->regs[BPF_REG_2] in prepare_func_exit(), causing check_kfunc_call() to release the wrong reference ID and leave a dangling owning pointer to the dropped node?

During verifier setup, bpf_compute_subprog_ret_regs() sets ret_reg_pair = true for any subprogram whose BTF return type is a 9-16 byte struct, and btf_validate_return_type() allows static subprograms to return structs up to 16 bytes. Neither check_func_arg(), push_callback_call(), nor check_cb_unwind() prevents the less callback passed to bpf_rbtree_add() or bpf_rbtree_add_impl() from having ret_reg_pair == true.

When such a callback executes unwind_exit, prepare_func_exit() in kernel/bpf/verifier.c computes nregs = 2 (where ret_regs[] = { BPF_REG_0, BPF_REG_2 }) and copies both registers from the callee frame into the caller frame before rewinding *insn_idx to callee->callsite:

	callee = state->frame[state->curframe];
	r0 = &callee->regs[BPF_REG_0];
	nregs = bpf_ret_reg_pair(env, callee->subprogno) ? 2 : 1;
	...
	if (callee->in_callback_fn && !unwinds) {
		...
	} else {
		for (i = 0; i < nregs; i++) {
			u32 regno = ret_regs[i];

			bpf_diag_mod_begin(env, &caller->regs[regno], &callee->regs[regno],
					   BPF_DIAG_MOD_WRITE);
			caller->regs[regno] = callee->regs[regno];
			bpf_diag_mod_end(env);
		}
	}

If the caller passed &n1->node (with id = n1_id) in BPF_REG_2 while keeping a copy of n1 in a callee-saved register (R6-R9) or stack spill, and the comparator callback acquires an owning reference to a second node n2 of the same BTF type (for example via bpf_refcount_acquire() or bpf_list_pop_front()), places &n2->node (with id = n2_id) into BPF_REG_2, and executes unwind_exit:

1. prepare_func_exit() overwrites caller->regs[BPF_REG_2] with &n2->node (id = n2_id).

2. On the revisit of check_kfunc_call() at bpf_rbtree_add() with env->cur_state->unwound == true, check_func_args() succeeds because caller->regs[BPF_REG_2] holds a valid owning pointer of the expected node type, and release_reference(env, id) releases n2_id instead of n1_id.

3. At runtime, __bpf_rbtree_add() drops the caller's original node n1 via __bpf_obj_drop_impl(), while in the verifier state at the landing pad after unwind_goto, n1_id was never released and the caller's saved pointer to n1 remains a valid owning PTR_TO_BTF_ID | MEM_ALLOC pointer to freed memory.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=9
author maintainer
#33 lore raw

Re: [RFC PATCH bpf-next v1 09/11] bpf: Let the comparator of bpf_rbtree_add unwind

Quoted 4 lines · sashiko-bot

[...]
[Severity: High]
Does the unwound path in __bpf_rbtree_add() leave node->owner poisoned with BPF_PTR_POISON when the object survives __bpf_obj_drop_impl()?

Right: with another reference holding the node, the drop left it with its owner poisoned, and no add would take it again. The next version unlinks it first, as bpf_rb_root_free() does.

Quoted 6 lines

[...]
[Severity: Critical]
Can an unwinding comparator callback with a two-register return type clobber caller->regs[BPF_REG_2] in prepare_func_exit(), causing check_kfunc_call() to release the wrong reference ID and leave a dangling owning pointer to the dropped node?

Same as on 03/11: a callback hands over r0 alone in the next version, so r2 stays the node the revisit releases.

Quoted 1 line

[...]

#34 lore raw

Re: [RFC PATCH bpf-next v1 09/11] bpf: Let the comparator of bpf_rbtree_add unwind

In reply to Kumar Kartikeya Dwivedi
TL;DR: - bpf_rbtree_add() unwind pad may be skipped under fexit/kretprobe/graph tracing, leaving a freed node usable by the program - unwound path may leave node->owner poisoned, and may run kptr…
Quoted 1 line

bpf: Let the comparator of bpf_rbtree_add unwind

TL;DR:
- bpf_rbtree_add() unwind pad may be skipped under fexit/kretprobe/graph tracing, leaving a freed node usable by the program
- unwound path may leave node->owner poisoned, and may run kptr destructors in NMI-unsafe contexts

Quoted 4 lines
kernel/bpf/helpers.c +0 -0
index 648abe1ba63a..f8708b7181d7 100644

[ ... ]

kernel/bpf/helpers.c static int __bpf_rbtree_add(struct bpf_rb_root *root, · line 2810

Quoted 11 lines
kernel/bpf/helpers.c +7 -0
@@ -2796,6 +2801,15 @@ static int __bpf_rbtree_add(struct bpf_rb_root *root,
 	rb_insert_color_cached(n, (struct rb_root_cached *)root, leftmost);
 	WRITE_ONCE(node->owner, root);
 	return 0;
+
+unwound:
+	/*
+	 * The comparator left with unwind_exit. The node is not in the tree, and
+	 * the caller has let go of it: drop it, as the error path above does.
+	 */
+	__bpf_obj_drop_impl((void *)n - off, rec, false);

Does this path leave node->owner set to BPF_PTR_POISON? The function claims the node before the loop:

	if (cmpxchg(&node->owner, NULL, BPF_PTR_POISON)) {

The normal exit replaces that with WRITE_ONCE(node->owner, root), but the unwound exit never clears the claim. For a non-refcounted node the drop frees it, so that does not matter.

For a refcounted node with another live reference, __bpf_obj_drop_impl() only drops the count and the object lives on with owner still set to BPF_PTR_POISON. Nothing resets it later, since bpf_obj_free_fields() does nothing for BPF_RB_NODE and only bpf_rbtree_remove() or a successful add writes owner.

The other reference can come from m = bpf_refcount_acquire(n) before the add, or from the comparator calling bpf_refcount_acquire() on its argument (the kfunc is KF_SPINLOCK_SAFE). The verifier keeps that other reference as owning after the unwind, but every later bpf_rbtree_add() of the object fails at the cmpxchg, drops the caller's reference and returns -EINVAL.

The comment says "drop it, as the error path above does", but the error path above never took the claim and this path did. Would a WRITE_ONCE(node->owner, NULL) before the __bpf_obj_drop_impl() call be needed here?

Can this drop also run in a context where the verifier refuses bpf_obj_drop()? check_kfunc_call() rejects bpf_obj_drop() and bpf_percpu_obj_drop() when btf_record_has_nmi_unsafe_fields() is true for tracing program types, per commit 94c8d1c21be4 ("bpf: Reject bpf_obj_drop() from tracing progs"). Every rbtree node type is covered, because btf_field_is_nmi_safe() returns false for BPF_RB_NODE.

Before this patch, the only drop in bpf_rbtree_add() was the error path for a node already in a collection, and that collection holds a reference of its own. Now a comparator that ends in unwind_exit makes the kfunc call __bpf_obj_drop_impl() every time, and for a non-refcounted node that is the last reference, so bpf_obj_free_fields() runs inline.

Nothing restricts this by program type: BPF_PROG_TYPE_TRACING gets generic_kfunc_set, check_map_prog_compatibility() refuses BPF_SPIN_LOCK only for is_tracing_prog_type() (which does not include BPF_PROG_TYPE_TRACING), and neither check_cb_unwind() nor the new unwound branch in check_kfunc_call() tests btf_record_has_nmi_unsafe_fields() or the program type.

For example, a non-sleepable tp_btf or fentry program attached somewhere NMI-reachable could allocate:

	struct { struct bpf_rb_node node; struct task_struct __kptr *t; }

store an acquired task in t, take the lock and call bpf_rbtree_add() with a comparator that unwinds. The verifier accepts this although it refuses bpf_obj_drop() on the same object, and the pad then runs the task kptr destructor (put_task_struct()) from that context.

Should the same btf_record_has_nmi_unsafe_fields() and program type check be applied to a bpf_rbtree_add() whose comparator can unwind, for example in check_cb_unwind() or in the unwound branch of check_kfunc_call()?

kernel/bpf/helpers.c static int __bpf_rbtree_add(struct bpf_rb_root *root, · line 2813

Quoted 3 lines
kernel/bpf/helpers.c +2 -0
+	bpf_unwind_leave();
+	return r0;
 }

Can this pad rely on the slot at the frame address of bpf_rbtree_add() or bpf_rbtree_add_impl() still holding the BPF caller's return address? Both are ordinary traceable functions: helpers.o is built with ftrace flags and btf_id_deny does not list them.

Several tracers that hook the return rewrite that slot:

- an fexit, fmod_ret or fsession trampoline, which uses BPF_TRAMP_F_CALL_ORIG and calls orig_call + X86_PATCH_SIZE, so the body returns into the trampoline
- a kretprobe or fprobe exit (the rethook trampoline)
- the function_graph tracer, where ftrace_graph_func() sets *parent = return_to_handler

In each case bpf_unwind_leave() reads that address. bpf_unwind_target() finds no BPF extable entry for it and no EX_TYPE_BPF kernel entry, and returns the address unchanged. WARN_ON_ONCE(to == *slot) fires (a panic with panic_on_warn), and the kfunc returns r0 the normal way.

The tracer then sends control to the BPF program's normal return address, and the JIT emits nothing for unwind_goto, so the program carries on along the path the verifier checked as "comparator returned". On that path ref_convert_owning_non_owning() has made the node a non-owning reference that is valid in the tree until unlock, but the kfunc has already called __bpf_obj_drop_impl() on it:

	__bpf_obj_drop_impl((void *)n - off, rec, false);
	bpf_unwind_leave();
	return r0;

Can the program then read and write a freed node? For example, it could bpf_kptr_xchg() an acquired reference into a kptr field of the dead object (kptr_xchg_dest_types allows NON_OWN_REF), and that reference is never released.

For helpers with pads, a missed unwind only loses the exception. bpf_rbtree_add() is the first one whose two exits leave the object in different states (in the tree, or freed), so for it a missed unwind leaves the verifier's state wrong.

bpf_check_attach_target() already refuses everything but fentry on a subprog that can unwind ("unwind_exit would return to the trampoline that called the target"), but kernel functions with a pad get no such restriction, and the rest of the series (through 5646f059f1bb) adds none. Should return-hooking tracers be kept off these kfuncs (notrace, a btf_id_deny-style list for fexit/fmod_ret/fsession, and no kretprobe or graph tracing), or should the pad tolerate a rewritten return address?

---
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/40612543702

author maintainer
#35 lore raw

Re: [RFC PATCH bpf-next v1 09/11] bpf: Let the comparator of bpf_rbtree_add unwind

Quoted 4 lines · bot+bpf-ci

[...]
TL;DR:
- bpf_rbtree_add() unwind pad may be skipped under fexit/kretprobe/graph tracing, leaving a freed node usable by the program

Right. Every function with a pad is notrace and no kprobe target in the next version, as the verifier lets only fentry attach to a subprog that unwinds, with a test that fexit on bpf_loop() is refused.

Quoted 2 lines

- unwound path may leave node->owner poisoned, and may run kptr destructors in NMI-unsafe contexts

Right on both. The next version unlinks the node before dropping it, as bpf_rb_root_free() does, and refuses bpf_rbtree_add() in tracing programs on types with NMI unsafe fields when its comparator can unwind, as bpf_obj_drop() is refused there, with tests for both.

Quoted 1 line

[...]

author maintainer
#36 lore raw

RFC PATCH 10/11 selftests/bpf: Test callbacks that unwind

In reply to Kumar Kartikeya Dwivedi

One file for the callbacks of bpf_loop(), bpf_for_each_map_elem() over an array, a hash and an rhash the prog fills first, bpf_find_vma() on the stack of the current task, and the comparator of bpf_rbtree_add(), in both spellings, meeting the one node that is in the tree. Each with a callback that unwinds on its first run, where the pad after the call returns the number of runs plus what the helper left in r0, which is what the callback unwound with, plus 41. One callback of bpf_loop() unwinds on its third run, one unwinds through its own pad, one call sees a callback that unwinds on one path and one that returns on the other, and the pad after a callback that returns is never taken. Refused is the call without unwind_goto after it. A loop of no rounds never runs its callback: it needs no unwind_goto, and the inlined loop leaves the insn after the call alone. The normal path of the helpers is what their own tests run.

The file includes vmlinux.h for the offsets the find_vma tests need, which rules out BPF_RAW_INSN(), so unwind_goto and unwind_exit are raw words. The rbtree test is in C with a naked subprog around the call of the kfunc, since the unwind_goto has to follow the call.

The callback of bpf_user_ringbuf_drain() needs samples from user space, so its test is a prog_tests one. The callback unwinds on the sample it is given, with r0 = 5, and the pad after the call returns the number of runs plus 5 plus 41. The drain before any sample returns 0, the one after the unwind finds the sample consumed and the busy bit dropped, and the one after the next sample unwinds again.

The existing "callback unwinds" test used bpf_loop() to get "callback cannot unwind". bpf_loop() has a pad now, so what it lacks is the unwind_goto after the call, and that is what the test expects.

---
 .../selftests/bpf/prog_tests/unwind_drain.c   |  71 +++
 .../selftests/bpf/prog_tests/verifier.c       |   2 +
 .../selftests/bpf/progs/unwind_drain.c        |  69 +++
 .../selftests/bpf/progs/verifier_unwind.c     |   3 +-
 .../selftests/bpf/progs/verifier_unwind_cb.c  | 569 ++++++++++++++++++
 5 files changed, 713 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/unwind_drain.c
 create mode 100644 tools/testing/selftests/bpf/progs/unwind_drain.c
 create mode 100644 tools/testing/selftests/bpf/progs/verifier_unwind_cb.c
tools/testing/selftests/bpf/prog_tests/unwind_drain.c new file +71 -0
new file mode 100644
index 000000000000..54b9cfbef440
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <test_progs.h>
+#include "unwind_drain.skel.h"
+
+static int run(struct bpf_program *prog)
+{
+	char data[64] = {};
+	LIBBPF_OPTS(bpf_test_run_opts, opts,
+		.data_in = data,
+		.data_size_in = sizeof(data),
+		.repeat = 1,
+	);
+
+	if (!ASSERT_OK(bpf_prog_test_run_opts(bpf_program__fd(prog), &opts), "test_run"))
+		return -1;
+	return opts.retval;
+}
+
+static int submit(struct user_ring_buffer *rb)
+{
+	void *sample = user_ring_buffer__reserve(rb, 8);
+
+	if (!ASSERT_OK_PTR(sample, "reserve"))
+		return -1;
+	user_ring_buffer__submit(rb, sample);
+	return 0;
+}
+
+/*
+ * The callback unwinds on the sample it is given, with r0 = 5. The pad after
+ * the call of bpf_user_ringbuf_drain() returns the number of runs plus 5 plus
+ * 41. The sample is consumed and the busy bit dropped on the way there: the
+ * next drain finds an empty ring buffer, and the one after that another
+ * sample.
+ */
+void test_unwind_drain(void)
+{
+	struct user_ring_buffer *rb = NULL;
+	struct unwind_drain *skel;
+	int err;
+
+	if (!is_jit_enabled()) {
+		printf("%s:SKIP:JIT is disabled\n", __func__);
+		test__skip();
+		return;
+	}
+	skel = unwind_drain__open_and_load();
+	err = libbpf_get_error(skel);
+	if (err == -ENOTSUPP) {
+		printf("%s:SKIP:JIT does not support unwinding\n", __func__);
+		test__skip();
+		return;
+	}
+	if (!ASSERT_OK_PTR(skel, "open_and_load"))
+		return;
+	rb = user_ring_buffer__new(bpf_map__fd(skel->maps.user_ringbuf), NULL);
+	if (!ASSERT_OK_PTR(rb, "user_ring_buffer__new"))
+		goto out;
+
+	ASSERT_EQ(run(skel->progs.drain), 0, "empty");
+	if (submit(rb))
+		goto out;
+	ASSERT_EQ(run(skel->progs.drain), 47, "unwinds");
+	ASSERT_EQ(run(skel->progs.drain), 0, "consumed");
+	if (submit(rb))
+		goto out;
+	ASSERT_EQ(run(skel->progs.drain), 48, "unwinds again");
+out:
+	user_ring_buffer__free(rb);
+	unwind_drain__destroy(skel);
+}
tools/testing/selftests/bpf/prog_tests/verifier.c +2 -0
index 04740ba37e91..10beba6bed20 100644
@@ -131,6 +131,7 @@
 #include "verifier_uninit.skel.h"
 #include "verifier_unpriv.skel.h"
 #include "verifier_unwind.skel.h"
+#include "verifier_unwind_cb.skel.h"
 #include "verifier_unpriv_perf.skel.h"
 #include "verifier_value_adj_spill.skel.h"
 #include "verifier_value.skel.h"
@@ -322,6 +323,7 @@ void test_verifier_typedef(void)              { RUN(verifier_typedef); }
 void test_verifier_uninit(void)               { RUN(verifier_uninit); }
 void test_verifier_unpriv(void)               { RUN(verifier_unpriv); }
 void test_verifier_unwind(void)               { RUN(verifier_unwind); }
+void test_verifier_unwind_cb(void)            { RUN(verifier_unwind_cb); }
 void test_verifier_unpriv_perf(void)          { RUN(verifier_unpriv_perf); }
 void test_verifier_value_adj_spill(void)      { RUN(verifier_value_adj_spill); }
 void test_verifier_value(void)                { RUN(verifier_value); }
tools/testing/selftests/bpf/progs/unwind_drain.c new file +69 -0

69 lines not on this page · raw message

tools/testing/selftests/bpf/progs/verifier_unwind.c +2 -1
index 5a21345c34d4..3de145a2d065 100644
@@ -896,12 +896,13 @@ static unsigned long cb_unwinds(void)
 	);
 }
 
+/* bpf_loop() has a pad for it, so what's missing is the one after the call */
 SEC("tc")
 __description("unwind: callback unwinds")
 __arch_x86_64
 __arch_arm64
 __load_if_JITed()
-__failure __msg("callback cannot unwind")
+__failure __msg("no unwind_goto after the call of bpf_loop()")
 __naked int callback_unwinds(void)
 {
 	asm volatile (
tools/testing/selftests/bpf/progs/verifier_unwind_cb.c new file +569 -0

569 lines not on this page · raw message

Signature · 1 line
2.53.0
#37 lore raw

Re: [RFC PATCH bpf-next v1 10/11] selftests/bpf: Test callbacks that unwind

TL;DR: Selftests may fail instead of skip on x86_64/arm64 kernels built with older gcc (no asm goto output), where callback pads are absent: unwind_drain load error and the changed callback_unwinds…
Quoted 1 line

selftests/bpf: Test callbacks that unwind

TL;DR: Selftests may fail instead of skip on x86_64/arm64 kernels built with older gcc (no asm goto output), where callback pads are absent: unwind_drain load error and the changed callback_unwinds message expectation.

Quoted 6 lines
tools/testing/selftests/bpf/prog_tests/unwind_drain.c new file +0 -0
new file mode 100644
index 000000000000..54b9cfbef440
@@ -0,0 +1,71 @@

[ ... ]

Quoted 10 lines
tools/testing/selftests/bpf/prog_tests/unwind_drain.c new file +10 -0
+void test_unwind_drain(void)
+{
+	struct user_ring_buffer *rb = NULL;
+	struct unwind_drain *skel;
+	int err;
+
+	if (!is_jit_enabled()) {
+		printf("%s:SKIP:JIT is disabled\n", __func__);
+		test__skip();
+		return;

tools/testing/selftests/bpf/prog_tests/unwind_drain.c line 55

Quoted 10 lines
tools/testing/selftests/bpf/prog_tests/unwind_drain.c new file +10 -0
+	}
+	skel = unwind_drain__open_and_load();
+	err = libbpf_get_error(skel);
+	if (err == -ENOTSUPP) {
+		printf("%s:SKIP:JIT does not support unwinding\n", __func__);
+		test__skip();
+		return;
+	}
+	if (!ASSERT_OK_PTR(skel, "open_and_load"))
+		return;

The test skips only on -ENOTSUPP, which is what check_unwind_insns() returns when !bpf_jit_supports_unwind(). Is that enough on a kernel whose JIT supports unwinding but whose helpers have no callback pads?

On x86_64 or arm64 without CONFIG_CC_HAS_ASM_GOTO_OUTPUT (gcc < 11.5, 12.0-12.3 and 13.0-13.2, see GCC_ASM_GOTO_OUTPUT_BROKEN in scripts/Kconfig.toolchain), BPF_HAS_CB_PADS is 0 in include/linux/bpf_unwind.h while bpf_jit_supports_unwind() still returns true.

Loading drain then goes through:

push_callback_call()
check_cb_unwind()

    verbose(env, "callback cannot unwind\n");
    return -EINVAL;

unwind_drain__open_and_load() returns NULL with errno = EINVAL, so the -ENOTSUPP check is false and ASSERT_OK_PTR(skel, "open_and_load") fails the test where it should skip, since the kernel does not offer the feature in that configuration.

Should this also skip when the load fails with "callback cannot unwind" in the verifier log (for example via bpf_program__set_log_buf()), or should the kernel return -ENOTSUPP for that case so that this check catches it?

[ ... ]

Quoted 9 lines
tools/testing/selftests/bpf/progs/verifier_unwind.c +1 -0
index 5a21345c34d4..3de145a2d065 100644
@@ -896,12 +896,13 @@ static unsigned long cb_unwinds(void)
 	);
 }
 
+/* bpf_loop() has a pad for it, so what's missing is the one after the call */

tools/testing/selftests/bpf/progs/verifier_unwind.c static unsigned long cb_unwinds(void) · line 908

Quoted 10 lines
tools/testing/selftests/bpf/progs/verifier_unwind.c +1 -1
 SEC("tc")
 __description("unwind: callback unwinds")
 __arch_x86_64
 __arch_arm64
 __load_if_JITed()
-__failure __msg("callback cannot unwind")
+__failure __msg("no unwind_goto after the call of bpf_loop()")
 __naked int callback_unwinds(void)
 {
 	asm volatile (

The new expected message is only what the verifier prints when BPF_HAS_CB_PADS is 1. In include/linux/bpf_unwind.h that is the case only for defined(CONFIG_X86_64) && defined(CONFIG_CC_HAS_ASM_GOTO_OUTPUT) and the arm64 equivalent; the #else branch sets it to 0 and calls the callback the plain way.

check_cb_unwind() in kernel/bpf/verifier.c tests that first:

    if (!BPF_HAS_CB_PADS || !insn_has_cb_pad(insn)) {
        verbose(env, "callback cannot unwind\n");
        return -EINVAL;
    }

so on such a kernel the log for this program still says "callback cannot unwind" and never reaches "no unwind_goto after the call of bpf_loop()".

CC_HAS_ASM_GOTO_OUTPUT depends on !GCC_ASM_GOTO_OUTPUT_BROKEN (scripts/Kconfig.toolchain), which is y for GCC_VERSION < 11.5, 12.0-12.3 and 13.0-13.2, while the minimum gcc is 8.1 (scripts/min-tool-version.sh). An x86_64 or arm64 kernel built with gcc 11.4 (Ubuntu 22.04), 12.2 (Debian 12) or 13.2 therefore still has bpf_jit_supports_unwind() == true.

check_unwind_insns() does not refuse the program there, so __arch_x86_64, __arch_arm64 and __load_if_JITed() still let it load, and verifier_unwind/callback_unwinds now fails. Before this hunk the expectation was correct on that configuration.

Nothing in the test can express the compiler dependency. The new verifier_unwind_cb.c tests in this commit depend on the compiler in the same way.

Can the expectation be made to hold in either configuration, for example with a regex that accepts either message, or can the test skip when the helpers have no pads?

---
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/40612543702

author maintainer
#38 lore raw

Re: [RFC PATCH bpf-next v1 10/11] selftests/bpf: Test callbacks that unwind

Quoted 4 lines · bot+bpf-ci

[...]
TL;DR: Selftests may fail instead of skip on x86_64/arm64 kernels built with older gcc (no asm goto output), where callback pads are absent: unwind_drain load error and the changed callback_unwinds message expectation.

Right: without asm goto outputs the helpers have no pads while the JIT unwinds. In the next version check_cb_unwind() says -ENOTSUPP for that, as for a JIT that does not unwind, and the tests skip on it; the check comes after the one for the unwind_goto, so the message for a missing unwind_goto is the same everywhere.

Quoted 1 line

[...]

author maintainer
#39 lore raw

RFC PATCH 11/11 selftests/bpf: Test what a callback unwinds with

In reply to Kumar Kartikeya Dwivedi

What a callback unwinds with reaches the pad as it is. A callback that unwinds with a pointer to an element of the array loads, and the pad reads through it: 6, which the callback put there, plus 41. A callback that unwinds with a reference it took hands it to the pad, which drops it, or is refused for leaking it. A pointer into the stack of the callback is refused as at any return.

The pad knows what the callback unwound with: it adds the 5 of cb_unwinds to fp and reads the slot that is there, which an unknown r0 could not do, and the index of the run, which the callback may unwind with, it does not know. Two callbacks on two paths into one call unwind with 5 and with 100, and the pad is checked with each: with 100 the read is out of the frame. A loop of one unwinds on its only round. A helper in the pad, after a callback unwound on the first of two rounds, starts its rounds afresh, so its callback is checked and its call needs an unwind_goto of its own. The pad before the call of an inlined bpf_loop() is reached from the inlined loop.

The comparator of bpf_rbtree_add() that unwinds with the node being added gets its pad refused for reading through it, and so is a read of the node after the call: the kfunc dropped it. What the caller holds of the tree is gone with the unwind as well, as at an unlock.

---
 .../selftests/bpf/progs/verifier_unwind_cb.c  | 497 +++++++++++++++++-
 1 file changed, 496 insertions(+), 1 deletion(-)
tools/testing/selftests/bpf/progs/verifier_unwind_cb.c +496 -1
index 8a3dee4330ac..d9db8084b843 100644
@@ -11,7 +11,7 @@
  * BPF_RAW_INSN(), so here they are raw words: the code, src_reg = 1 in the
  * high nibble of the second byte, off in the next two bytes.
  */
-#define unwind_goto(off) ".8byte (" #off " << 16) | 0x10e5;"	/* BPF_JMP | BPF_JCOND */
+#define unwind_goto(off) ".8byte ((" #off " & 0xffff) << 16) | 0x10e5;"	/* BPF_JMP | BPF_JCOND */
 #define unwind_exit ".8byte 0x1095;"				/* BPF_JMP | BPF_EXIT */
 
 struct {
@@ -259,6 +259,374 @@ __naked int loop_cb_unwinds_no_pad(void)
 	);
 }
 
+/* Unwinds with a pointer in r0: the element of the array, with 6 in it */
+__naked __noinline __used
+static unsigned long cb_unwinds_with_ptr(void)
+{
+	asm volatile (
+		"r1 = 0;"
+		"*(u32 *)(r10 - 4) = r1;"
+		"r1 = %[array] ll;"
+		"r2 = r10;"
+		"r2 += -4;"
+		"call %[bpf_map_lookup_elem];"
+		"if r0 == 0 goto 1f;"
+		"r1 = 6;"
+		"*(u32 *)(r0 + 0) = r1;"
+		unwind_exit
+	"1:"
+		"exit;"
+		:
+		: __imm_addr(array),
+		  __imm(bpf_map_lookup_elem)
+		: __clobber_all
+	);
+}
+
+/* The helper hands r0 on as it is: the pad reads through the pointer */
+SEC("tc")
+__description("unwind cb: the callback unwinds with a pointer in r0")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(47)
+__naked int loop_cb_unwinds_with_ptr(void)
+{
+	asm volatile (
+		"r1 = 10;"
+		"r2 = %[cb_unwinds_with_ptr];"
+		"r3 = 0;"
+		"r4 = 0;"
+		"call %[bpf_loop];"
+		unwind_goto(1)
+		"exit;"
+		"r0 = *(u32 *)(r0 + 0);"
+		"r0 += 41;"
+		"exit;"
+		:
+		: __imm_ptr(cb_unwinds_with_ptr),
+		  __imm(bpf_loop)
+		: __clobber_common
+	);
+}
+
+/* Unwinds with a pointer into its own stack */
+__naked __noinline __used
+static unsigned long cb_unwinds_with_fp(void)
+{
+	asm volatile (
+		"r0 = r10;"
+		"r0 += -8;"
+		unwind_exit
+	);
+}
+
+SEC("tc")
+__description("unwind cb: the callback unwinds with a pointer into its stack")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("cannot return stack pointer to the caller")
+__naked int loop_cb_unwinds_with_fp(void)
+{
+	asm volatile (
+		"r1 = 10;"
+		"r2 = %[cb_unwinds_with_fp];"
+		"r3 = 0;"
+		"r4 = 0;"
+		"call %[bpf_loop];"
+		unwind_goto(1)
+		"exit;"
+		"exit;"
+		:
+		: __imm_ptr(cb_unwinds_with_fp),
+		  __imm(bpf_loop)
+		: __clobber_common
+	);
+}
+
+/*
+ * The pad knows what the callback unwound with: 5, which cb_unwinds leaves in
+ * r0, and reads the slot through it. An unknown r0 would be no offset for fp.
+ */
+SEC("tc")
+__description("unwind cb: the pad knows what the callback unwound with")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(9)
+__naked int loop_cb_r0_known(void)
+{
+	asm volatile (
+		"r1 = 9;"
+		"*(u64 *)(r10 - 8) = r1;"
+		"r1 = 10;"
+		"r2 = %[cb_unwinds];"
+		"r3 = 0;"
+		"r4 = 0;"
+		"call %[bpf_loop];"
+		unwind_goto(1)
+		"exit;"
+		"r1 = r10;"
+		"r1 += r0;"
+		"r0 = *(u8 *)(r1 - 13);"
+		"exit;"
+		:
+		: __imm_ptr(cb_unwinds),
+		  __imm(bpf_loop)
+		: __clobber_common
+	);
+}
+
+/* Allocates a node and unwinds with it: the reference comes back in r0 */
+static __noinline long cb_unwinds_with_obj(u64 i, void *ctx)
+{
+	struct node_data *n = bpf_obj_new(typeof(*n));
+
+	if (n)
+		asm volatile ("r0 = %[n];" unwind_exit :: [n] "r"(n) : "r0");
+	return 0;
+}
+
+/* The pad drops what came back */
+SEC("tc")
+__description("unwind cb: the callback unwinds with a reference, which the pad drops")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(7)
+__naked int loop_cb_obj_dropped(void)
+{
+	asm volatile (
+		"r1 = 10;"
+		"r2 = %[cb_unwinds_with_obj];"
+		"r3 = 0;"
+		"r4 = 0;"
+		"call %[bpf_loop];"
+		unwind_goto(1)
+		"exit;"
+		"r1 = r0;"
+		"call %[bpf_obj_drop];"
+		"r0 = 7;"
+		"exit;"
+		:
+		: __imm_ptr(cb_unwinds_with_obj),
+		  __imm(bpf_loop),
+		  __imm(bpf_obj_drop)
+		: __clobber_common
+	);
+}
+
+/* The pad may not leave without dropping it: the reference is the prog's now */
+SEC("tc")
+__description("unwind cb: the callback unwinds with a reference, which the pad leaks")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("Unreleased reference")
+__naked int loop_cb_obj_leaked(void)
+{
+	asm volatile (
+		"r1 = 10;"
+		"r2 = %[cb_unwinds_with_obj];"
+		"r3 = 0;"
+		"r4 = 0;"
+		"call %[bpf_loop];"
+		unwind_goto(1)
+		"exit;"
+		"r0 = 7;"
+		"exit;"
+		:
+		: __imm_ptr(cb_unwinds_with_obj),
+		  __imm(bpf_loop)
+		: __clobber_common
+	);
+}
+
+/* Unwinds with the index of the run, which the pad doesn't know */
+__naked __noinline __used
+static unsigned long cb_unwinds_with_index(void)
+{
+	asm volatile (
+		"r0 = r1;"
+		count_run()
+		unwind_exit
+		:
+		: __imm_addr(runs)
+	);
+}
+
+SEC("tc")
+__description("unwind cb: the pad doesn't know the index the callback unwound with")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("math between fp pointer and register with unbounded min value is not allowed")
+__naked int loop_cb_index_unknown(void)
+{
+	asm volatile (
+		"r1 = 9;"
+		"*(u64 *)(r10 - 8) = r1;"
+		"r1 = 10;"
+		"r2 = %[cb_unwinds_with_index];"
+		"r3 = 0;"
+		"r4 = 0;"
+		"call %[bpf_loop];"
+		unwind_goto(1)
+		"exit;"
+		"r1 = r10;"
+		"r1 += r0;"
+		"r0 = *(u8 *)(r1 - 13);"
+		"exit;"
+		:
+		: __imm_ptr(cb_unwinds_with_index),
+		  __imm(bpf_loop)
+		: __clobber_common
+	);
+}
+
+/* Unwinds with r0 = 100, no offset for the slot */
+__naked __noinline __used
+static unsigned long cb_unwinds_100(void)
+{
+	asm volatile (
+		"r0 = 100;"
+		unwind_exit
+	);
+}
+
+/*
+ * Two callbacks on two paths into one call, unwinding with 5 and with 100. The
+ * pad is checked with each: the second one puts the read out of the frame.
+ */
+SEC("tc")
+__description("unwind cb: the pad is checked with what each callback unwound with")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("invalid read from stack")
+__naked int loop_cbs_differ_pad_checked(void)
+{
+	asm volatile (
+		"r6 = *(u32 *)(r1 + %[len]);"
+		"r2 = %[cb_unwinds];"
+		"if r6 == 64 goto 1f;"
+		"r2 = %[cb_unwinds_100];"
+	"1:"
+		"r1 = 9;"
+		"*(u64 *)(r10 - 8) = r1;"
+		"r1 = 10;"
+		"r3 = 0;"
+		"r4 = 0;"
+		"call %[bpf_loop];"
+		unwind_goto(1)
+		"exit;"
+		"r1 = r10;"
+		"r1 += r0;"
+		"r0 = *(u8 *)(r1 - 13);"
+		"exit;"
+		:
+		: __imm_const(len, offsetof(struct __sk_buff, len)),
+		  __imm_ptr(cb_unwinds),
+		  __imm_ptr(cb_unwinds_100),
+		  __imm(bpf_loop)
+		: __clobber_common, "r6"
+	);
+}
+
+/* A loop of one: the round that unwinds is the last the loop had */
+SEC("tc")
+__description("unwind cb: the callback of a loop of one unwinds")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(47)
+__naked int loop_once_cb_unwinds(void)
+{
+	asm volatile (
+		"r1 = 1;"
+		"r2 = %[cb_unwinds];"
+		"r3 = 0;"
+		"r4 = 0;"
+		"call %[bpf_loop];"
+		unwind_goto(1)
+		"exit;"
+		PAD
+		:
+		: __imm_ptr(cb_unwinds),
+		  __imm_addr(runs),
+		  __imm(bpf_loop)
+		: __clobber_common
+	);
+}
+
+/*
+ * The callback unwinds on the first of two rounds. The helper in the pad starts
+ * its rounds afresh, so its callback is checked, and the call needs its own
+ * unwind_goto.
+ */
+SEC("tc")
+__description("unwind cb: a helper in the pad starts its rounds afresh")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("no unwind_goto after the call of bpf_loop()")
+__naked int loop_cb_unwinds_then_loops(void)
+{
+	asm volatile (
+		"r1 = 2;"
+		"r2 = %[cb_unwinds];"
+		"r3 = 0;"
+		"r4 = 0;"
+		"call %[bpf_loop];"
+		unwind_goto(1)
+		"exit;"
+		"r1 = 1;"
+		"r2 = %[cb_unwinds];"
+		"r3 = 0;"
+		"r4 = 0;"
+		"call %[bpf_loop];"
+		"exit;"
+		:
+		: __imm_ptr(cb_unwinds),
+		  __imm(bpf_loop)
+		: __clobber_common
+	);
+}
+
+/*
+ * The pad before the call: the inlined loop jumps back to it. Insn 13 is the
+ * unwind_goto, insn 1 the pad: the callback is loaded in the asm, so that the
+ * compiler puts nothing in front of it.
+ */
+SEC("tc")
+__description("unwind cb: the pad before the call of the inlined bpf_loop")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(47)
+__xlated("unwind_goto pc+7")
+__naked int loop_cb_pad_before_call(void)
+{
+	asm volatile (
+		"goto 1f;"
+		PAD
+	"1:"
+		"r1 = 10;"
+		"r2 = %[cb_unwinds] ll;"
+		"r3 = 0;"
+		"r4 = 0;"
+		"call %[bpf_loop];"
+		unwind_goto(-13)
+		"exit;"
+		:
+		: __imm_addr(cb_unwinds),
+		  __imm_addr(runs),
+		  __imm(bpf_loop)
+		: __clobber_common
+	);
+}
+
 /*
  * A loop of no rounds never runs its callback, so the call needs no unwind_goto,
  * and the inlined loop gets no pad: the insn after the call is what it was, not
@@ -566,4 +934,131 @@ int rbtree_impl_cb_unwinds(void *ctx)
 	return rbtree_cb_unwinds_via(true);
 }
 
+/* Unwinds with the node it is handed first: the one being added */
+__naked __noinline __used
+static bool less_unwinds_with_node(struct bpf_rb_node *a, const struct bpf_rb_node *b)
+{
+	asm volatile (
+		"r0 = r1;"
+		unwind_exit
+	);
+}
+
+/* The pad reads the key of the node that came back in r0 */
+__naked __noinline __used
+static int rbtree_add_pad_reads_node(void *root, void *node, void *less)
+{
+	asm volatile (
+		"call %[bpf_rbtree_add];"
+		unwind_goto(1)
+		"exit;"
+		"r0 = *(u64 *)(r0 - 8);"
+		"exit;"
+		:
+		: __imm(bpf_rbtree_add)
+		: __clobber_all
+	);
+}
+
+/*
+ * The comparator unwinds with the node being added, which the kfunc drops on
+ * its way to the pad: there is nothing left to read through it.
+ */
+SEC("tc")
+__description("unwind cb: the comparator unwinds with the node the kfunc dropped")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("invalid mem access")
+int rbtree_cb_unwinds_with_node(void *ctx)
+{
+	struct node_data *n, *m;
+	int ret;
+
+	n = bpf_obj_new(typeof(*n));
+	if (!n)
+		return 1;
+	m = bpf_obj_new(typeof(*m));
+	if (!m) {
+		bpf_obj_drop(n);
+		return 2;
+	}
+
+	bpf_spin_lock(&glock);
+	bpf_rbtree_add(&groot, &n->node, less_returns);
+	ret = rbtree_add_pad_reads_node(&groot, &m->node, (void *)less_unwinds_with_node);
+	bpf_spin_unlock(&glock);
+	return ret;
+}
+
+/*
+ * The node the comparator unwound on is not in the tree: the kfunc dropped it
+ * on its way to the pad, and the caller has nothing left to read.
+ */
+SEC("tc")
+__description("unwind cb: the node the comparator unwound on is gone")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("invalid mem access")
+int rbtree_cb_unwinds_node_gone(void *ctx)
+{
+	struct node_data *n, *m;
+	long key;
+	int ret;
+
+	n = bpf_obj_new(typeof(*n));
+	if (!n)
+		return 1;
+	m = bpf_obj_new(typeof(*m));
+	if (!m) {
+		bpf_obj_drop(n);
+		return 2;
+	}
+
+	bpf_spin_lock(&glock);
+	bpf_rbtree_add(&groot, &n->node, less_returns);
+	ret = rbtree_add_with_pad(&groot, &m->node, (void *)less_unwinds);
+	key = m->key;
+	bpf_spin_unlock(&glock);
+	return ret + key;
+}
+
+/*
+ * The comparator was handed the node in the tree as well, and could have
+ * unwound with it, so what the caller holds of the tree is gone too, as at an
+ * unlock.
+ */
+SEC("tc")
+__description("unwind cb: what the caller holds of the tree is gone when the comparator unwinds")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("invalid mem access")
+int rbtree_cb_unwinds_first_gone(void *ctx)
+{
+	struct node_data *n, *m;
+	struct bpf_rb_node *f;
+	long key = 0;
+	int ret;
+
+	n = bpf_obj_new(typeof(*n));
+	if (!n)
+		return 1;
+	m = bpf_obj_new(typeof(*m));
+	if (!m) {
+		bpf_obj_drop(n);
+		return 2;
+	}
+
+	bpf_spin_lock(&glock);
+	bpf_rbtree_add(&groot, &n->node, less_returns);
+	f = bpf_rbtree_first(&groot);
+	ret = rbtree_add_with_pad(&groot, &m->node, (void *)less_unwinds);
+	if (f)
+		key = ((struct node_data *)((void *)f - offsetof(struct node_data, node)))->key;
+	bpf_spin_unlock(&glock);
+	return ret + key;
+}
+
 char _license[] SEC("license") = "GPL";
Signature · 1 line
2.53.0
#40 lore raw
In reply to Kumar Kartikeya Dwivedi
Quoted 4 lines · Kumar Kartikeya Dwivedi

Code that unwinds due to panics or exceptions doesn't often control where they can be invoked, and reusing the same code in callbacks would lead to verifier rejections since unwinding across such synchronous callbacks is not supported.

I feel that you're too attached to bpf_throw concept. The code was quite intrusive earlier and it is still today. I told my agents that bpf_throw is a dead end. sorry. I really think it's not a kernel job to solve it.

author maintainer
#41 lore raw
Quoted 11 lines · Alexei Starovoitov
Quoted 4 lines · Kumar Kartikeya Dwivedi

Code that unwinds due to panics or exceptions doesn't often control where they can be invoked, and reusing the same code in callbacks would lead to verifier rejections since unwinding across such synchronous callbacks is not supported.

I feel that you're too attached to bpf_throw concept. The code was quite intrusive earlier and it is still today. I told my agents that bpf_throw is a dead end. sorry. I really think it's not a kernel job to solve it.

Huh? This set is about making unwind_goto / unwind_exit work.

#42 lore raw
Quoted 8 lines · Kumar Kartikeya Dwivedi
Quoted 4 lines · Alexei Starovoitov

I feel that you're too attached to bpf_throw concept. The code was quite intrusive earlier and it is still today. I told my agents that bpf_throw is a dead end. sorry. I really think it's not a kernel job to solve it.

Huh? This set is about making unwind_goto / unwind_exit work.

Sorry. Mixed it up.
I meant it in the "Eliminate spill overhead for bpf_throw()" thread. I don't see the point doing any work for bpf_throw().
imo it's mainly in the way of any verifier work.
I'd rather delete all the bpf_throw related code and make it nop. It didn't get any traction.
Originally it was a mechanism to enable bpf_assert() which in turn was an idea to stop fighting the verifier.
That didn't materialize.
So when I see the title "Eliminate spill overhead for bpf_throw" it makes me react the way I did... in the wrong thread :(

author maintainer
#43 lore raw
Quoted 11 lines · Alexei Starovoitov
Quoted 8 lines · Kumar Kartikeya Dwivedi
Quoted 4 lines · Alexei Starovoitov

I feel that you're too attached to bpf_throw concept. The code was quite intrusive earlier and it is still today. I told my agents that bpf_throw is a dead end. sorry. I really think it's not a kernel job to solve it.

Huh? This set is about making unwind_goto / unwind_exit work.

Quoted 11 lines

Sorry. Mixed it up.
I meant it in the "Eliminate spill overhead for bpf_throw()" thread. I don't see the point doing any work for bpf_throw().
imo it's mainly in the way of any verifier work.
I'd rather delete all the bpf_throw related code and make it nop. It didn't get any traction.
Originally it was a mechanism to enable bpf_assert() which in turn was an idea to stop fighting the verifier.
That didn't materialize.
So when I see the title "Eliminate spill overhead for bpf_throw" it makes me react the way I did... in the wrong thread :(

I've already expressed in the past that I'm fine with it, feel free to go ahead with it (alternatively, I can also send the series dropping it). In its current state upstream, it is useless.

I mostly sent the one you are reacting to out to clear my queue, and wasn't tied to my own stuff, but without any expectations (same for this one as well). The less I need to rebase the better, etc.

That said, on the topic, this one seemed useful to me, but you are free to disagree and not take it either.

-------------8<-------------

(Mostly for documentation and public record; I don't have the energy to argue about the merits / demerits).

Since we plan to sunset bpf_throw(), here is some information for other readers:

I carry out-of-tree patches since ~2024 to complete the support, and it works well whenever I need it in any academic project. All of the concerns for the original approach + spilling as a fallback turned out to be mostly theoretical. Across all programs in selftests, scx, cilium, there are a handful of spills in total where the merging logic is unable to reconcile differing states; naturally, in practice, the compiler already keeps resources the program releases itself in known stack locations or registers since the code gen is better.

So in practice, no program gets rejected; the worst case is having extra spills for contrived examples. I personally don't buy the LoC argument either, the whole thing is contained in 2k LoC on the kernel side, including comments and various boilerplate. Given the rate at which we add code today, it's a non-sequitur.

In return, we get various appealing properties:
- Do the equivalent of bail!() or assert!() to prove a property, or just not handle a given condition, regardless of the language used (and for BPF objects linking across languages).
- A building block to insert runtime conditions from the verifier that the program needs at a given point (bounds check, etc.), but cannot be proven due to the limitations of the static analysis, or losing precision. It does not depend on whether the program properly releases resources at that point, which we cannot assume to be the case.
- On arena faults, or may_goto timeouts, stopping execution of the program and ejecting it from the machine. This is done manually by scx schedulers scraping streams and unloading the scheduler today.
- Programs that hog CPU using very long loops get killed asynchronously at the next cancellation point (loop backedge) using NMI watchdogs, and get ejected from the system. The counting implementation of may_goto can be replaced with a load of a bit in prog->aux for non-NMI programs (majority).

There are other various other misc. cases where it is more appropriate or necessary:
- Software CFI-checked indirect calls to subprogs (basically, emitting a check on an indirect call (say with pointer from arena) and aborting the program when the CFI-like check fails).
- If runtime stack probing or recursion limits are hit, the program is aborted, instead of return 0 (as done in rust-bpf).
- Making bpf_spin_lock() just work and aborting on deadlocks.
- When writing actual programs in C++ or Rust, inevitably, panic() may be seen (but may not be exercised) in code that is unwinding. The verifier will just reject such cases. In both C++ or Rust the behavior is to panic=abort or std::terminate(), which is a good analogue for simply aborting the program.

In general, it serves many purposes, has analogue in every other virtual machine runtime, thus is a well understood idea, and encompasses what various alternative proposals for some of the covered use cases do (fast-execute, etc.) but with better semantics.

maintainer
#44 lore raw

On Wed, 2026-10-28 at 22:42 +0100, Kumar Kartikeya Dwivedi wrote:

...

Quoted 14 lines

There are other various other misc. cases where it is more appropriate or necessary:
- Software CFI-checked indirect calls to subprogs (basically, emitting a check on an indirect call (say with pointer from arena) and aborting the program when the CFI-like check fails).
- If runtime stack probing or recursion limits are hit, the program is aborted, instead of return 0 (as done in rust-bpf).
- Making bpf_spin_lock() just work and aborting on deadlocks.
- When writing actual programs in C++ or Rust, inevitably, panic() may be seen (but may not be exercised) in code that is unwinding. The verifier will just reject such cases. In both C++ or Rust the behavior is to panic=abort or std::terminate(), which is a good analogue for simply aborting the program.

Quoted 4 lines

In general, it serves many purposes, has analogue in every other virtual machine runtime, thus is a well understood idea, and encompasses what various alternative proposals for some of the covered use cases do (fast-execute, etc.) but with better semantics.

I agree with Kartikeya on these points.
Unwinding and terminating execution is a well understood and common concept. It is "in distribution", so to speak.

The new verifier series would benefit from it instead of taking quite unusual and unexpected actions, like inserting additional control flow exits, or returning 0 from a subprogram that doesn't have stack space to recur. "If loop runs for too long the program is terminated" is a much simpler concept to work with compared to "if loop runs for too long the program is taking some exit from a loop".

2K LOC is basically nothing, if one assumes that code is maintained by LLM. There are much more invasive verification concepts atm, like freplace for parts of the program, or tail calls.

...

#45 lore raw
Quoted 18 lines · Eduard Zingerman

On Wed, 2026-10-28 at 22:42 +0100, Kumar Kartikeya Dwivedi wrote:

...

Quoted 14 lines

There are other various other misc. cases where it is more appropriate or necessary:
- Software CFI-checked indirect calls to subprogs (basically, emitting a check on an indirect call (say with pointer from arena) and aborting the program when the CFI-like check fails).
- If runtime stack probing or recursion limits are hit, the program is aborted, instead of return 0 (as done in rust-bpf).
- Making bpf_spin_lock() just work and aborting on deadlocks.
- When writing actual programs in C++ or Rust, inevitably, panic() may be seen (but may not be exercised) in code that is unwinding. The verifier will just reject such cases. In both C++ or Rust the behavior is to panic=abort or std::terminate(), which is a good analogue for simply aborting the program.

Quoted 7 lines
Quoted 4 lines

In general, it serves many purposes, has analogue in every other virtual machine runtime, thus is a well understood idea, and encompasses what various alternative proposals for some of the covered use cases do (fast-execute, etc.) but with better semantics.

I agree with Kartikeya on these points.
Unwinding and terminating execution is a well understood and common concept.

I agree with it as well.
My objection was to 'Eliminate spill overhead for bpf_throw()'
as a pointless step _today_. If we can make bpf_throw() into works-everywhere mechanism then let's do it, but it has to be that first step. Polishing 'spill oveheard' is very much optional follow up.

Quoted 1 line

It is "in distribution", so to speak.

'in distribution' is so early AI lingvo. I hope it's obvious now that 'in distribution' is the opposite of what is needed to build secure sw stack.

Quoted 6 lines

The new verifier series would benefit from it instead of taking quite unusual and unexpected actions, like inserting additional control flow exits, or returning 0 from a subprogram that doesn't have stack space to recur. "If loop runs for too long the program is terminated" is a much simpler concept to work with compared to "if loop runs for too long the program is taking some exit from a loop".

Yes, but the base line is different now. goal posts have moved a lot.

Quoted 3 lines

2K LOC is basically nothing, if one assumes that code is maintained by LLM. There are much more invasive verification concepts atm, like freplace for parts of the program, or tail calls.

it's fine if it's 2k->20k lines too.

Before we do the happy dance pls read section 8
https://lore.kernel.org/all/[email protected]/
It needs to support all 3:

1.
do { p = lookup(hash map); } while (!p);
return *p;

2.
lock(&v->l);
do { v->cnt++; } while (v->flag);
unlock(&v->l);
return 1;

3.
rcu_read_lock();
do { p = lookup(hash map); } while (!p);
x = *p;
rcu_read_unlock();

The patches I sent don't support 3rd yet. I left it for the follow up.

I'm more than happy to drop may_goto approach,
but I want to see the patches for bpf_throw-that-works-everywhere, including 3rd case above.
And bpf_throw shouldn't skip rust landing pads.
That's a bar.

Also time is an essence.
We cannot sit and debate things like we did in the past.
If there is code that satisfies the goals that we set, we should be landing it. How it achieves the gaols is a minor detail at this point.