Skip to content

bpf: Unwinding with unwind_goto and unwind_exit - #14435

Closed
kernel-patches-daemon-bpf[bot] wants to merge 5 commits into
bpf-next_basefrom
series/1188607=>bpf-next
Closed

kernel-patches-daemon-bpf[bot] wants to merge 5 commits into
bpf-next_basefrom
series/1188607=>bpf-next

Conversation

@kernel-patches-daemon-bpf

Copy link
Copy Markdown

Pull request for series with
subject: bpf: Unwinding with unwind_goto and unwind_exit
version: 2
url: https://patchwork.kernel.org/series/1188607/

@kernel-patches-daemon-bpf

Copy link
Copy Markdown
Author

Upstream branch: 4f84263
series: https://patchwork.kernel.org/series/1188607/
version: 2

@kernel-patches-review-bot

Copy link
Copy Markdown
commit af346b419ba3bf5093302af7d95bbb5b0e39e6d3
Author: Alexei Starovoitov <ast@kernel.org>

selftests/bpf: Add tests for unwind_goto and unwind_exit

This adds selftests for the unwind_goto and unwind_exit insns, written as
raw insns.  verifier_unwind.c covers verifier and JIT behavior, including
private stack, unwind_attach covers fentry, freplace and fexit attached to
a subprog that unwinds, and test_loader learns to run raw_tp progs.

> selftests/bpf: Add tests for unwind_goto and unwind_exit

TL;DR:
- Two selftests may not test what they claim: callee_never_returns and
  no_pruning_after_the_call can pass without the checks they target
- private_stack_unwinds __jited() patterns may fail with call depth accounting
  on x86-64

> diff --git a/tools/testing/selftests/bpf/progs/verifier_unwind.c b/tools/testing/selftests/bpf/progs/verifier_unwind.c
> new file mode 100644
> index 000000000000..a235c6143e10
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/verifier_unwind.c
> @@ -0,0 +1,1047 @@

[ ... ]

> +/* What follows unwind_goto is dead code. unwind_exit still has to find the pad. */
> +SEC("tc")
> +__description("unwind: the callee never returns")
> +__arch_x86_64
> +__arch_arm64
> +__load_if_JITed()
> +__success __retval(42)
> +__naked int callee_never_returns(void)
> +{
> +	asm volatile (
> +		"call always_unwinds;"
> +		unwind_goto(2)
> +		"r0 = 1;"
> +		"exit;"
> +		"r0 = 42;"
> +		"exit;"
> +		:
> +		: __imm_unwind_goto(2)
> +		: __clobber_all
> +	);
> +}

The comment says unwind_exit still has to find the pad.  That depends on
the exception added to bpf_opt_hard_wire_dead_code_branches():

    /* unwind_exit of a callee that never exits needs it too */
    if (bpf_is_unwind_goto(insn) && aux_data[i + 1 + insn->off].seen)
        continue;

Can __retval(42) tell whether that exception is present?

Without it, unwind_goto(2) would become 'ja +2', because the fall-through
insn was never seen.  bpf_opt_remove_dead_code() would then drop the dead
insns and bpf_opt_remove_nops() would drop the resulting 'ja +0', leaving
'call; r0 = 42; exit' with no extable entry.

At run time, unwind_exit in always_unwinds calls bpf_unwind_target(), finds
no entry, hits WARN_ON_ONCE(!e) and falls back to ret_addr:

    if (WARN_ON_ONCE(!e))
        return ret_addr;

After dead code removal ret_addr is the pad ('r0 = 42'), so the prog still
returns 42 and the test passes.  The only sign of the problem is a warning
splat, which test_progs does not check for.

callee_never_unwinds below already uses __xlated() to pin the opposite
case, where the pad goes away together with unwind_goto.  Would something
like __xlated("1: unwind_goto pc+0") here show that unwind_goto stays when
only the fall-through is dead?

[ ... ]

> +/*
> + * After the call the two states differ only in the way the callee left.
> + * The second one must not be pruned, or nobody looks at the pad.
> + */
> +SEC("tc")
> +__description("unwind: the way the callee left is a part of the state")
> +__arch_x86_64
> +__arch_arm64
> +__load_if_JITed()
> +__failure __msg("R0 invalid mem access 'scalar'")
> +__naked int no_pruning_after_the_call(void)
> +{
> +	asm volatile (
> +		"r1 = *(u32 *)(r1 + %[len]);"
> +		"call same_regs;"
> +		unwind_goto(1)
> +		"exit;"
> +		"r0 = *(u64 *)(r0 + 0);"
> +		"exit;"
> +		:
> +		: __imm_const(len, offsetof(struct __sk_buff, len)),
> +		  __imm_unwind_goto(1)
> +		: __clobber_all
> +	);
> +}

Does this test exercise the new check in states_equal()?

    if (old->unwound != cur->unwound)
        return false;

It looks like it passes the same way with or without that check, because
the verifier never saves a checkpoint at the insn after the call in this
program.

unwind_goto is a prune point, but not a force checkpoint.  visit_insn()
only calls mark_force_checkpoint() for may_goto:

    mark_prune_point(env, t);
    if (bpf_is_may_goto_insn(insn))
        mark_force_checkpoint(env, t);

Without a force checkpoint, bpf_is_state_visited() only adds a new state
after at least 2 jumps and 8 insns since the last checkpoint:

    add_new_state = force_new_state;
    if (env->jmps_processed - env->prev_jmps_processed >= 2 &&
        env->insn_processed - env->prev_insn_processed >= 8)
        add_new_state = true;

With same_regs added by libbpf, the program is:

    0: r1 = *(u32 *)(r1 + len)
    1: call same_regs
    2: unwind_goto +1
    3: exit
    4: r0 = *(u64 *)(r0 + 0)
    5: exit
    6: r0 = 0               (same_regs)
    7: if r1 != 0 goto +1
    8: exit
    9: unwind_exit

check_cond_jmp_op() pushes the taken branch (insn 9) and continues with the
fall-through, so the first path reaches insn 2 after only 6 processed insns
(0, 1, 6, 7, 8, 2).  That is below 8, so no state is saved at insn 2, and
nothing before it saved one either.

When the unwound path comes back from insn 9 to insn 2, the explored state
list for insn 2 is empty.  Nothing can prune that path, so the verifier
always walks into the pad and reports "R0 invalid mem access 'scalar'".

If the unwound check were removed from states_equal(), would this test
still pass?  Today only pad_does_not_unlock would catch that, and only
because its first path happens to reach 8 insns before unwind_goto.

test_loader's default flags from testing_prog_flags() are
BPF_F_TEST_RND_HI32 and BPF_F_TEST_REG_INVARIANTS, not
BPF_F_TEST_STATE_FREQ.  The callx pruning tests in verifier_callx.c add
__flag(BPF_F_TEST_STATE_FREQ) for this reason.  Would adding the same flag
here save a state at insn 2 on the first path, so that a kernel without the
states_equal() check prunes the second path and the test fails as intended?

[ ... ]

> +/*
> + * x86-64 keeps the pointer to the private stack in r9 and saves it around a call.
> + * The value that is read back in the pad is there only when r9 is restored.
> + */
> +SEC("raw_tp")
> +__description("unwind: private stack, the callee unwinds")
> +__success __retval(47)
> +__load_if_JITed()
> +__arch_x86_64
> +__jited("	pushq	%r9")
> +__jited("	callq	0x{{.*}}")
> +__jited("	popq	%r9")
> +__jited("	jmp	L{{.*}}")
> +__jited("	popq	%r9")
> +__jited("	jmp	L{{.*}}")
> +__arch_arm64
> +__naked int private_stack_unwinds(void)

Can the x86-64 __jited() expectations fail on hosts that use call depth
accounting?

They require callq to be on the line right after pushq %r9.  In do_jit()
the BPF_CALL case emits push_r9(), then x86_call_depth_emit_accounting(),
then emit_call():

    if (priv_frame_ptr) {
        push_r9(&prog);
        ip += 2;
    }
    ip += x86_call_depth_emit_accounting(&prog, func, ip);
    if (emit_call(&prog, func, ip))

With CONFIG_MITIGATION_CALL_DEPTH_TRACKING=y and retbleed=stuff
(thunks_initialized), x86_call_depth_emit_accounting() adds a 'sarq $0x5,
%gs:...' instruction before calls to a BPF subprog.  A JITed subprog like
may_unwind_big_stack has no call thunk in its padding, so is_callthunk()
returns false and the accounting is emitted.

The disassembly then reads 'pushq %r9; sarq ...; callq ...', and
validate_msgs() reports a line mismatch for the callq pattern, so this
subtest fails on retbleed-affected machines such as Skylake booted with
retbleed=stuff.  The kernel itself is fine there.

Commit bf29346fc393 ("selftests/bpf: ignore call depth accounting for
retbleed in verifier tests") fixed the same pattern in
verifier_private_stack.c by putting __jited("...") between "pushq %r9" and
"callq".  Should this test do the same?

    __jited("\tpushq\t%r9")
    __jited("...")
    __jited("\tcallq\t0x{{.*}}")



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

In-Reply-To-Subject: selftests/bpf: Add tests for unwind_goto and unwind_exit
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/39205301464

@kernel-patches-daemon-bpf

Copy link
Copy Markdown
Author

@kernel-patches-review-bot

Copy link
Copy Markdown
commit 88088981426d6916b32ed5b2f5fa6ffbe9dcc1f0
Author: Alexei Starovoitov <ast@kernel.org>

bpf: Add unwind_goto and unwind_exit insns

This commit adds two new BPF instructions, unwind_goto (a BPF_JCOND form)
and unwind_exit (a BPF_EXIT form), so that a callee can leave through
unwind_exit and the caller continues at a landing pad via the unwind_goto
that follows the call.  The verifier tracks how the callee left in a new
per-state "unwound" bit, and JITs implement the transfer with an extable
entry plus a rewritten return address instead of a branch.

> bpf: Add unwind_goto and unwind_exit insns

TL;DR:
- Possible build failure on MIPS with CONFIG_BPF_JIT=y: bpf_unwind_target()
  uses e->fixup, which MIPS lacks
- Verifier may skip checking the fall-through after unwind_goto, allowing
  possible Spectre-v1-style speculative execution

> diff --git a/kernel/bpf/core.c b/kernel/bpf/core.c
> index 6e32db9777c2..da3691b47cc7 100644
> --- a/kernel/bpf/core.c
> +++ b/kernel/bpf/core.c
> @@ -804,6 +804,24 @@ const struct exception_table_entry *search_bpf_extables(unsigned long addr)
>  	return e;
>  }
>
> +/*
> + * JITs don't emit a branch for unwind_goto. They add 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
> + * replaces the return address of its frame with what this function returns.
> + * 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.
> + */
> +u64 bpf_unwind_target(u64 ret_addr)
> +{
> +	const struct exception_table_entry *e = search_bpf_extables(ret_addr - 1);
> +
> +	/* The verifier made sure that unwind_goto follows the call */
> +	if (WARN_ON_ONCE(!e))
> +		return ret_addr;
> +	return ret_addr + e->fixup;
                          ^^^^^^^^

Does this still build on MIPS with CONFIG_BPF_JIT=y?

bpf_unwind_target() sits in the CONFIG_BPF_JIT section of kernel/bpf/core.c,
so it is compiled for every architecture that has a JIT, and it reads
e->fixup.  On MIPS, struct exception_table_entry has no fixup member:

arch/mips/include/asm/extable.h:
    struct exception_table_entry
    {
            unsigned long insn;
            unsigned long nextinsn;
    };

MIPS selects HAVE_EBPF_JIT and some defconfigs (loongson3_defconfig for
example) set CONFIG_BPF_JIT=y, so this looks like it would fail with
"'const struct exception_table_entry' has no member named 'fixup'".  Until
now generic code only used e->insn.

None of the later patches in this series touch kernel/bpf/core.c or
arch/mips, so the build would stay broken through the end of the series.

Should bpf_unwind_target() only be built for JITs that opt in through
bpf_jit_supports_unwind(), or should the arch decode the target (a helper
next to bpf_jit_supports_unwind()), or should the JIT do the lookup itself?

> +}
> +
>  int bpf_get_kallsym(unsigned int symnum, unsigned long *value, char *type,
>  		    char *sym)
>  {

[ ... ]

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index d13868846823..dd2d200a73c3 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c

[ ... ]

> @@ -18697,6 +18763,13 @@ static int check_cond_jmp_op(struct bpf_verifier_env *env,
>  		return -EINVAL;
>  	}
>
> +	if (bpf_is_unwind_goto(insn)) {
> +		if (env->cur_state->unwound)
> +			*insn_idx += insn->off;
> +		env->cur_state->unwound = false;
> +		return 0;
> +	}
> +
>  	if (opcode == BPF_JCOND) {
>  		struct bpf_verifier_state *cur_st = env->cur_state, *queued_st, *prev_st;
>  		int idx = *insn_idx;

When the callee left with unwind_exit, this only follows the pad.  The
fall-through insn after the call is never verified with the unwound state,
not even as a speculative path.

Is that enough given how the JITs implement it?  The x86 and arm64 patches
later in this series emit no branch for unwind_goto.  unwind_exit rewrites
the saved return address and then executes a normal ret, so the CPU's
return-address predictor still points at the insn after the call.  Each
unwind would then run the fall-through code speculatively before it is
redirected to the pad.

For a static subprog, prepare_func_exit() copies the callee's r0/r2 at
unwind_exit into the caller, and the callee may also have written caller
stack slots differently on that path.  So the fall-through code can run
speculatively with register and stack state the verifier never checked for
it.

check_cond_jmp_op() already handles the equivalent case for branches whose
outcome is known when !env->bypass_spec_v1:

    if (pred == 1) {
            ...
            if (!env->bypass_spec_v1) {
                    err = sanitize_speculative_path(env, insn, *insn_idx + 1, *insn_idx);
                    ...

but the unwind_goto path returns before reaching that logic and has no
equivalent.  This would matter for loaders with CAP_BPF but without
CAP_PERFMON (including BPF token setups) on x86-64 and arm64, where
bpf_bypass_spec_v1() is false and bpf-to-bpf calls are allowed.  For global
subprogs the gap does not arise, since check_func_call() gives both paths
the same state apart from the unwound bit.

Should the unwound case push a speculative fall-through state when
!env->bypass_spec_v1, for example with:

    sanitize_speculative_path(env, NULL, *insn_idx + 1, *insn_idx);

Alternatively, could the commit message explain why speculative execution
of the fall-through after a return mispredict is not a concern here?
Neither the commit message nor the selftests later in the series cover
speculation, and no later patch in the series changes this.



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

In-Reply-To-Subject: bpf: Add unwind_goto and unwind_exit insns
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/39205301464

@kernel-patches-daemon-bpf

Copy link
Copy Markdown
Author

@kernel-patches-daemon-bpf

Copy link
Copy Markdown
Author

Upstream branch: 868e56d
series: https://patchwork.kernel.org/series/1188607/
version: 2

@kernel-patches-daemon-bpf

Copy link
Copy Markdown
Author

Upstream branch: 4f84263
series: https://patchwork.kernel.org/series/1188607/
version: 2

@kernel-patches-daemon-bpf

Copy link
Copy Markdown
Author

Upstream branch: 25bb03f
series: https://patchwork.kernel.org/series/1188607/
version: 2

@kernel-patches-daemon-bpf

Copy link
Copy Markdown
Author

Upstream branch: 0867b7b
series: https://patchwork.kernel.org/series/1188607/
version: 2

@kernel-patches-daemon-bpf

Copy link
Copy Markdown
Author

Upstream branch: 1907a7d
series: https://patchwork.kernel.org/series/1188607/
version: 2

Rust Drop, C++ destructors and __attribute__((cleanup)) in C have to run
when a panic or an exception passes through a frame.

v1 [1] did it with a flag in r3 and 'if r3 != 0 goto pad' after the call.
Three bugs that the bots found there had the same cause: r3 is an ordinary
register, and JITs guessed the meaning from the shape of the code.
Kumar suggested new insns instead [2]:

  unwind_goto pad    BPF_JMP | BPF_JCOND, src_reg = BPF_UNWIND_GOTO
  unwind_exit        BPF_JMP | BPF_EXIT,  src_reg = BPF_UNWIND_EXIT

'call foo; unwind_goto pad' goes to the pad when foo left with unwind_exit
and falls through when it left with exit. No register says which one it
was. In everything else unwind_exit is exit.

llvm lowers unwinding to them [3]:

  ret                            exit
  invoke foo unwind pad          call foo; unwind_goto pad
  resume                         r0 = 0; unwind_exit
  call _Unwind_RaiseException    r0 = 0; unwind_exit
  invoke _Unwind_RaiseException  goto pad

Landing pads are ordinary blocks, so the verifier checks them like any
other code. A pad that forgets to drop a lock or a reference is a leak on
that path. There are no tables, no stack walk, no kfuncs, no BTF and
nothing to do for libbpf and other loaders.

In the verifier:
- unwind_goto has to follow a call or callx of a subprog and cannot be
  a jump target. For cfg, liveness and the rest it's one more conditional
  jump, like may_goto.
- How the callee left is one bit of the verifier state. It lives from
  the exit of the callee to the next insn. states_equal() looks at it,
  since the registers can be the same either way.
- unwind_exit is rejected when unwind_goto doesn't follow the call.
- A global subprog that has unwind_exit can leave either way. Both are
  explored after the call, like it's done for might_throw.
- Callbacks cannot unwind: sync, async and exception ones. Whoever calls
  them doesn't expect that.
- unwind_exit of the main prog is exit.
- Of tracing progs only fentry can attach to a subprog that has
  unwind_exit. It would return to the trampoline that called it.
- unwind_goto goes away together with a pad that turned out to be dead
  code. It stays when what follows it is dead code.

JITs don't emit a branch:

  unwind_goto:
	extable[n++] = { ret_addr - 1 -> pad }
  unwind_exit:
	my_ret_addr = bpf_unwind_target(my_ret_addr)
	epilogue

So exit and the normal path after the call cost nothing. Every frame fixes
only its own return address. There is no stack walk. The lookup is what
the page fault handler does for bpf progs.
The interpreter cannot do that, so the prog has to be JITed, and JITs opt
in with bpf_jit_supports_unwind(). x86-64 and arm64 are in the next
patches.

Compared to v1 there is no BTF decl_tag and no 'r3 = 0' before every exit.
Tail call and ld_abs are fine in a subprog that can unwind, since they
leave it like exit does. So is freplace: an extension leaves with exit and
the callers are ready for that.

In [4] I wrote that unwind_exit sets r0 = 0. It doesn't. An implicit
definition of r0 needs special cases in backtracking, in liveness and in
the check of the return value of the main prog. A plain 'r0 = 0' in front
of it needs none.

Rust

rust-bpf [5] builds with that llvm as it is. The programs are built with
-C panic=unwind. The panic handler prints the message and calls
_Unwind_RaiseException().
exc_cleanup.rs panics three frames deep, with preemption disabled in one
frame and rcu_read_lock held in another. All Drop impls run and the prog
returns 0.
scx_simple.rs is 2195 insns with 48 unwind_goto, 20 of them after callx,
and 16 unwind_exit. With r3 it was 2216 insns. scx_cosmos.rs is 3694 insns
with 68 and 18. It was 3716.

C++

Exceptions work too. This compiles, loads and runs:

  struct rcu_guard {
          rcu_guard() { bpf_rcu_read_lock(); }
          ~rcu_guard() { bpf_rcu_read_unlock(); }
  };

  static __noinline int inner(int len)
  {
          rcu_guard guard;

          if (len > 100)
                  throw 1;
          return len;
  }

  SEC("tc") int prog(struct __sk_buff *skb)
  {
          try {
                  return inner(skb->len);
          } catch (...) {
                  return 42;
          }
  }

It returns 64 for a packet of 64 bytes and 42 for a packet of 200 bytes.
The destructor runs in both cases, otherwise the verifier would reject
the prog. 'throw' is a jump to the pad with the destructor. 'catch' is
unwind_goto:

  inner:
    w6 = w1
    call bpf_rcu_read_lock
    if w6 s< 101 goto ret
    r1 = exc_obj ll
    *(u32 *)(r1 + 0) = 1
    goto pad
  ret:
    call bpf_rcu_read_unlock
    w0 = w6
    exit
  pad:
    call bpf_rcu_read_unlock
    r0 = 0
    unwind_exit

  prog:
    w6 = *(u32 *)(r1 + 0)
    w1 = w6
    call inner
    unwind_goto catch
  out:
    w0 = w6
    exit
  catch:
    w6 = 42
    goto out

It needs -fexceptions -funwind-tables and four one line __cxa_*()
functions, where __cxa_throw() calls _Unwind_RaiseException().
Every pad is entered whatever was thrown, so llvm takes catch (...) only.
It's a selftest in the last patch.

[1] https://lore.kernel.org/bpf/20261018010246.1643877-1-alexei.starovoitov@gmail.com/
[2] https://lore.kernel.org/bpf/DM7KR7J8VY2T.1CZIRUF6QRCWG@gmail.com/
[3] llvm/llvm-project#233140
[4] https://lore.kernel.org/bpf/DM8G3O0ZTUM9.MMBWXXQ7OP98@gmail.com/
[5] https://github.com/4ast/rust-bpf

Suggested-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
@kernel-patches-daemon-bpf

Copy link
Copy Markdown
Author

Upstream branch: f25f631
series: https://patchwork.kernel.org/series/1188607/
version: 2

Alexei Starovoitov added 2 commits October 19, 2026 17:32
  bpf                         x86
  call inner                  call inner
  unwind_goto pad
  w0 = w6                     mov eax, ebx

  r0 = 0                      xor eax, eax
  unwind_exit                 push rax
                              push rsi
                              mov rdi, [rbp + 8]
                              call bpf_unwind_target
                              mov [rbp + 8], rax
                              pop rsi
                              pop rax
                              leave
                              ret

unwind_goto is an extable entry and no code. unwind_exit looks its return
address up there and returns to the pad. r0 and r2 are what a subprog
returns, so rax and rsi are saved around the lookup.

The caller of the main prog doesn't tell exit from unwind_exit, so there
it's exit.

With private stack r9 is saved around a call, and 'pop r9' has to be done
on the way to the pad too. In such progs unwind_goto is:

  push r9
  call foo
  pop r9       <- exit returns here
  jmp 1f
  pop r9       <- unwind_exit returns here
  jmp pad
1:

Signed-off-by: Alexei Starovoitov <ast@kernel.org>
  bpf                         arm64
  call inner                  bl inner
                              mov x8, x0
  unwind_goto pad
  w0 = w6                     mov w8, w19

Like on x86 unwind_goto is an extable entry and no code.
unwind_exit returns to the pad past 'mov x8, x0'. That's fine: r0 is x8,
and the epilogue of the callee copies it to x0 and leaves x8 as it is.

unwind_exit fixes LR in its frame record. With pointer authentication it's
signed with SP at the entry of the prog, which is FP + 16:

  stp x8, x1, [sp, #-16]!
  ldr x17, [x29, #8]
  add x16, x29, #16
  autia1716
  mov x0, x17
  bl bpf_unwind_target
  mov x17, x0
  add x16, x29, #16
  pacia1716
  str x17, [x29, #8]
  ldp x8, x1, [sp], #16

So the lookup gets an authenticated address, and autiasp in the epilogue
checks the new one. autia1716 and pacia1716 are hints like paciasp and
autiasp: nops when the cpu doesn't have pointer authentication.
RET is not subject to BTI.
r0 and r2 are what a subprog returns, so x8 and x1 are saved around the
lookup.

The private stack pointer is in a callee saved register. Unlike x86 there
is nothing to undo after the call.

Tested in qemu with -cpu max and -cpu cortex-a72, i.e. with and without
pointer authentication.

Signed-off-by: Alexei Starovoitov <ast@kernel.org>
Alexei Starovoitov added 2 commits October 19, 2026 17:32
The tests spell the two insns out as raw insns, so they don't depend on
llvm version.

verifier_unwind:
- A static subprog, a global one and a callee of callx leave either way at
  run time, through one and two frames. The verifier follows a static
  subprog, so the tests derive the argument from skb->len or read it from
  a map.
- r0 of unwind_exit, r6-r9 and the stack are there in the pad. r1-r5
  are not readable.
- A pad is checked like any other code: it has to drop rcu_read_lock.
- How the callee left is a part of the verifier state: two states that
  differ only in that are not pruned.
- The pad of a call of a global subprog is always checked.
- A callee that never returns, and one that never unwinds, where the pad
  goes away.
- unwind_exit without unwind_goto after the call.
- unwind_goto that doesn't follow a call of a subprog or is a jump target.
- Reserved fields.
- A callback cannot unwind. What it calls can.
- Tail call and ld_abs in a subprog that can unwind.
- unwind_exit of the main prog is exit.
- What x86-64 and arm64 JITs emit.
- Private stack. On x86-64 r9 has to be restored on the way to the pad.

The last one has to look at the return value of a prog that uses private
stack. raw_tp is such a prog. Its test_run doesn't take a packet, so let
test_loader run it like it runs syscall progs.

unwind_attach: fentry and freplace are attached to a subprog that unwinds
and the caller is run. fexit is refused.

Signed-off-by: Alexei Starovoitov <ast@kernel.org>
  static __noinline int inner(int len)
  {
          rcu_guard guard;

          if (len > 100)
                  throw 1;
          return len;
  }

  static __noinline int middle(int len)
  {
          preempt_guard guard;

          return inner(len) + 1;
  }

  SEC("tc") int catches(struct __sk_buff *skb)
  {
          try {
                  return middle(skb->len);
          } catch (...) {
                  return 42;
          }
  }

The exception passes through two frames. The destructors call
bpf_rcu_read_unlock() and bpf_preempt_enable(). The verifier insists on
both, and the test checks that they ran in that order.
In the second prog nobody catches. The destructors run and the prog
returns 0.

All that is left of the C++ runtime is four one line functions.
__cxa_throw() calls _Unwind_RaiseException(), which llvm lowers.

progs/*.cpp are built like progs/*.rs: by clang only and without skeletons.
vmlinux.h and bpf_helpers.h are not C++, so the prog declares what it
needs.

clang that lowers unwinding [1] defines __BPF_FEATURE_UNWIND. Without it
the object has no progs and the test is skipped.

[1] llvm/llvm-project#233140

Signed-off-by: Alexei Starovoitov <ast@kernel.org>
@kernel-patches-daemon-bpf
kernel-patches-daemon-bpf Bot deleted the series/1188607=>bpf-next branch October 22, 2026 04:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants