* [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit
@ 2026-10-19 5:18 Alexei Starovoitov
2026-10-19 5:18 ` [PATCH v2 bpf-next 1/5] bpf: Add unwind_goto and unwind_exit insns Alexei Starovoitov
` (5 more replies)
0 siblings, 6 replies; 11+ messages in thread
From: Alexei Starovoitov @ 2026-10-19 5:18 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
v1 [1] said in r3 how a subprog returns. v2 has two new insns instead,
as Kumar suggested.
1. What is the problem?
A rust panic or a C++ exception has to run Drop impls or destructors of
every frame it passes through. bpf_throw() discards the frames, so the
verifier doesn't let a frame that owns a lock or a reference throw. In rust
every index and every unwrap() can panic.
2. Is it worth solving?
Without it rust-bpf [2] drops all landing pads and a panic is bpf_throw().
A prog that can panic with a guard held doesn't load.
3. Is there a simpler way?
[3] keeps the normal path free of unwind code. llvm emits a table, libbpf
passes it to the kernel, bpf_unwind() walks the stack and rewrites return
addresses, and the verifier models hidden edges from calls to pads. That's
1400 lines in the kernel, with new uapi and two kfuncs, and 440 lines in
libbpf.
Here the edge from a call to its pad is an insn, and so is the other way
to leave a frame [4]:
ret exit
invoke foo unwind pad call foo; unwind_goto pad
resume r0 = 0; unwind_exit
Pads are ordinary code. The verifier learns one bit: how the callee left.
The normal path is free of unwind code too: JITs emit nothing for
unwind_goto, and exit is what it was.
170 lines in patch 1 and 80 lines in each JIT.
4. Will the api and the layers last?
Two new insns: one more kind of BPF_JCOND and one more kind of BPF_EXIT.
No kfuncs, no BTF, no changes in libbpf and other loaders.
The source level entry point is _Unwind_RaiseException() of the Itanium
ABI. It's what the runtimes of rust and C++ call on other architectures.
unwind_exit returns r0 like exit does. Today it's 0. That's where an
exception object would go.
5. Is it good taste?
Every frame returns through its own epilogue. The only question is where it
returns to. That's one bit, and it's a bit of control flow. v1 kept it in
a register.
It's not specific to rust. A C++ prog with throw, catch and destructors
compiles, loads and runs. It's a selftest in patch 5.
v1 -> v2:
- unwind_goto and unwind_exit instead of 'if r3 != 0 goto pad' and
'r3 = 1; exit' (Kumar).
- JIT support is not an optimization anymore. There are no shape tests, no
look at jump_target, which was stale (sashiko), no exit that looks at r3
and no fallback to a branch.
- r3 is an argument again and nothing else. In v1 a subprog that didn't
write it passed it through, and fentry didn't restore it (bpf-ci).
- Async and exception callbacks cannot unwind (sashiko).
- No BTF decl_tag, which was matched by prefix (bpf-ci). It works without
BTF.
- x86 private stack is supported.
- Tail call, ld_abs and freplace are allowed.
- bpf_unwind_target() is in core.c. num_exentries is counted in
jit_subprogs().
- selftests: no #ifdef by arch, which broke the build on the rest of them.
The messages are checked. fentry and freplace are attached and the
target is run. fmod_ret is gone, it didn't test anything new. Skips say
why (bpf-ci).
- clang defines __BPF_FEATURE_UNWIND.
Tested in qemu only, x86-64 and arm64 with and without pointer
authentication: the selftests, C, C++ and rust progs built with [4],
with and without bpf_jit_harden=2. test_progs was run on x86-64. On arm64
the same objects were loaded and run by a small loader, and __jited
patterns were matched against the JIT output by a script.
[1] https://lore.kernel.org/bpf/20261018010246.1643877-1-alexei.starovoitov@gmail.com/
[2] https://github.com/4ast/rust-bpf
[3] https://lore.kernel.org/bpf/20261017025129.1914859-1-yonghong.song@linux.dev/
[4] https://github.com/llvm/llvm-project/pull/233140
Alexei Starovoitov (5):
bpf: Add unwind_goto and unwind_exit insns
bpf, x86: Support unwind_goto and unwind_exit
bpf, arm64: Support unwind_goto and unwind_exit
selftests/bpf: Add tests for unwind_goto and unwind_exit
selftests/bpf: Add a C++ prog with exceptions
arch/arm64/net/bpf_jit.h | 2 +
arch/arm64/net/bpf_jit_comp.c | 78 ++
arch/x86/net/bpf_jit_comp.c | 83 +-
include/linux/bpf.h | 1 +
include/linux/bpf_verifier.h | 3 +
include/linux/filter.h | 12 +
include/uapi/linux/bpf.h | 11 +
kernel/bpf/const_fold.c | 2 +-
kernel/bpf/core.c | 24 +
kernel/bpf/disasm.c | 7 +-
kernel/bpf/fixups.c | 7 +
kernel/bpf/states.c | 3 +
kernel/bpf/verifier.c | 93 +-
tools/include/uapi/linux/bpf.h | 11 +
tools/testing/selftests/bpf/Makefile.skel | 11 +
.../selftests/bpf/prog_tests/unwind_attach.c | 102 ++
.../selftests/bpf/prog_tests/unwind_cxx.c | 76 ++
.../selftests/bpf/prog_tests/verifier.c | 2 +
.../selftests/bpf/progs/unwind_attach.c | 29 +
.../selftests/bpf/progs/unwind_cxx.cpp | 115 ++
.../selftests/bpf/progs/verifier_unwind.c | 1047 +++++++++++++++++
tools/testing/selftests/bpf/test_loader.c | 3 +-
22 files changed, 1716 insertions(+), 6 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/unwind_attach.c
create mode 100644 tools/testing/selftests/bpf/prog_tests/unwind_cxx.c
create mode 100644 tools/testing/selftests/bpf/progs/unwind_attach.c
create mode 100644 tools/testing/selftests/bpf/progs/unwind_cxx.cpp
create mode 100644 tools/testing/selftests/bpf/progs/verifier_unwind.c
base-commit: 27dc4fc4ae7b01efb68561f98c5f373e55fb71c2
--
2.55.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v2 bpf-next 1/5] bpf: Add unwind_goto and unwind_exit insns
2026-10-19 5:18 [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit Alexei Starovoitov
@ 2026-10-19 5:18 ` Alexei Starovoitov
2026-10-19 5:45 ` sashiko-bot
2026-10-19 6:59 ` bot+bpf-ci
2026-10-19 5:18 ` [PATCH v2 bpf-next 2/5] bpf, x86: Support unwind_goto and unwind_exit Alexei Starovoitov
` (4 subsequent siblings)
5 siblings, 2 replies; 11+ messages in thread
From: Alexei Starovoitov @ 2026-10-19 5:18 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
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] https://github.com/llvm/llvm-project/pull/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>
---
include/linux/bpf.h | 1 +
include/linux/bpf_verifier.h | 3 ++
include/linux/filter.h | 12 +++++
include/uapi/linux/bpf.h | 11 ++++
kernel/bpf/const_fold.c | 2 +-
kernel/bpf/core.c | 24 +++++++++
kernel/bpf/disasm.c | 7 ++-
kernel/bpf/fixups.c | 7 +++
kernel/bpf/states.c | 3 ++
kernel/bpf/verifier.c | 93 +++++++++++++++++++++++++++++++++-
tools/include/uapi/linux/bpf.h | 11 ++++
11 files changed, 170 insertions(+), 4 deletions(-)
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index aeb8b9b97052..641d1e82aace 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -1895,6 +1895,7 @@ struct bpf_prog_aux {
bool priv_stack_requested;
bool changes_pkt_data;
bool might_sleep;
+ bool can_unwind;
bool kprobe_write_ctx;
struct {
s32 keyring_serial;
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 7a72bb06fe56..4974bea905a4 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -532,6 +532,8 @@ struct bpf_verifier_state {
bool speculative;
bool in_sleepable;
+ /* The callee left with unwind_exit. Next insn is unwind_goto. */
+ bool unwound;
/*
* First and last insn idx of this verifier state.
@@ -949,6 +951,7 @@ struct bpf_subprog_info {
s16 fastcall_stack_off;
bool has_tail_call: 1;
bool might_throw: 1;
+ bool can_unwind: 1; /* has unwind_exit */
bool tail_call_reachable: 1;
bool has_ld_abs: 1;
bool is_cb: 1;
diff --git a/include/linux/filter.h b/include/linux/filter.h
index 9339c6131f8f..62a6a2880de7 100644
--- a/include/linux/filter.h
+++ b/include/linux/filter.h
@@ -254,6 +254,16 @@ static inline bool insn_is_mov_percpu_addr(const struct bpf_insn *insn)
.off = 0, \
.imm = 1 })
+static inline bool bpf_is_unwind_goto(const struct bpf_insn *insn)
+{
+ return insn->code == (BPF_JMP | BPF_JCOND) && insn->src_reg == BPF_UNWIND_GOTO;
+}
+
+static inline bool bpf_is_unwind_exit(const struct bpf_insn *insn)
+{
+ return insn->code == (BPF_JMP | BPF_EXIT) && insn->src_reg == BPF_UNWIND_EXIT;
+}
+
static inline bool insn_is_zext(const struct bpf_insn *insn)
{
return insn->code == (BPF_ALU | BPF_MOV | BPF_X) && insn->imm == 1;
@@ -1244,6 +1254,8 @@ bool bpf_jit_supports_percpu_insn(void);
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);
diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
index 6f70db7515dd..6efe0ca03624 100644
--- a/include/uapi/linux/bpf.h
+++ b/include/uapi/linux/bpf.h
@@ -56,8 +56,19 @@
enum bpf_cond_pseudo_jmp {
BPF_MAY_GOTO = 0,
+ /*
+ * Follows a call of a bpf subprog. Taken when the subprog left with
+ * BPF_EXIT that has src_reg == BPF_UNWIND_EXIT.
+ */
+ BPF_UNWIND_GOTO = 1,
};
+/*
+ * When bpf_exit->src_reg == BPF_UNWIND_EXIT, it's the same as bpf_exit, except
+ * that BPF_UNWIND_GOTO after the call is taken.
+ */
+#define BPF_UNWIND_EXIT 1
+
/* Register numbers */
enum {
BPF_REG_0 = 0,
diff --git a/kernel/bpf/const_fold.c b/kernel/bpf/const_fold.c
index 8dd8c5958eb8..f9edfde965dc 100644
--- a/kernel/bpf/const_fold.c
+++ b/kernel/bpf/const_fold.c
@@ -357,7 +357,7 @@ int bpf_prune_dead_branches(struct bpf_verifier_env *env)
if (!bpf_insn_is_cond_jump(insn->code))
continue;
- if (bpf_is_may_goto_insn(insn))
+ if (BPF_OP(insn->code) == BPF_JCOND)
continue;
if (!(aux->const_reg_mask & BIT(insn->dst_reg)))
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;
+}
+
int bpf_get_kallsym(unsigned int symnum, unsigned long *value, char *type,
char *sym)
{
@@ -3342,6 +3360,12 @@ bool __weak bpf_jit_supports_kfunc_ret_reg_pair(void)
return false;
}
+/* unwind_goto and unwind_exit */
+bool __weak bpf_jit_supports_unwind(void)
+{
+ return false;
+}
+
/*
* 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
diff --git a/kernel/bpf/disasm.c b/kernel/bpf/disasm.c
index 36d3228d7745..889fc074496f 100644
--- a/kernel/bpf/disasm.c
+++ b/kernel/bpf/disasm.c
@@ -375,11 +375,16 @@ void print_bpf_insn(const struct bpf_insn_cbs *cbs,
insn->src_reg == BPF_MAY_GOTO) {
verbose(cbs->private_data, "(%02x) may_goto pc%+d",
insn->code, insn->off);
+ } else if (insn->code == (BPF_JMP | BPF_JCOND) &&
+ insn->src_reg == BPF_UNWIND_GOTO) {
+ verbose(cbs->private_data, "(%02x) unwind_goto pc%+d",
+ insn->code, insn->off);
} else if (insn->code == (BPF_JMP32 | BPF_JA)) {
verbose(cbs->private_data, "(%02x) gotol pc%+d",
insn->code, insn->imm);
} else if (insn->code == (BPF_JMP | BPF_EXIT)) {
- verbose(cbs->private_data, "(%02x) exit", insn->code);
+ verbose(cbs->private_data, "(%02x) %s", insn->code,
+ insn->src_reg == BPF_UNWIND_EXIT ? "unwind_exit" : "exit");
} else if (BPF_SRC(insn->code) == BPF_X) {
verbose(cbs->private_data,
"(%02x) if %c%d %s %c%d goto pc%+d",
diff --git a/kernel/bpf/fixups.c b/kernel/bpf/fixups.c
index 713f14d73b06..9e5439ae04f8 100644
--- a/kernel/bpf/fixups.c
+++ b/kernel/bpf/fixups.c
@@ -666,6 +666,10 @@ void bpf_opt_hard_wire_dead_code_branches(struct bpf_verifier_env *env)
if (!bpf_insn_is_cond_jump(insn->code))
continue;
+ /* 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;
+
if (!aux_data[i + 1].seen)
ja.off = insn->off;
else if (!aux_data[i + 1 + insn->off].seen)
@@ -1402,12 +1406,15 @@ static int jit_subprogs(struct bpf_verifier_env *env)
if (BPF_CLASS(insn->code) == BPF_STX &&
BPF_MODE(insn->code) == BPF_PROBE_ATOMIC)
num_exentries++;
+ if (bpf_is_unwind_goto(insn))
+ num_exentries++;
}
func[i]->aux->num_exentries = num_exentries;
func[i]->aux->tail_call_reachable = env->subprog_info[i].tail_call_reachable;
func[i]->aux->exception_cb = env->subprog_info[i].is_exception_cb;
func[i]->aux->changes_pkt_data = env->subprog_info[i].changes_pkt_data;
func[i]->aux->might_sleep = env->subprog_info[i].might_sleep;
+ func[i]->aux->can_unwind = env->subprog_info[i].can_unwind;
func[i]->aux->token = prog->aux->token;
if (!i)
func[i]->aux->exception_boundary = env->seen_exception;
diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c
index b3d250caaaa6..d7df6632a5c4 100644
--- a/kernel/bpf/states.c
+++ b/kernel/bpf/states.c
@@ -1046,6 +1046,9 @@ static bool states_equal(struct bpf_verifier_env *env,
if (old->in_sleepable != cur->in_sleepable)
return false;
+ if (old->unwound != cur->unwound)
+ return false;
+
if (!refsafe(old, cur, &env->idmap_scratch))
return false;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 74e5ac248a10..14bc69ec04e5 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -1769,6 +1769,7 @@ int bpf_copy_verifier_state(struct bpf_verifier_state *dst_state,
return err;
dst_state->speculative = src->speculative;
dst_state->in_sleepable = src->in_sleepable;
+ dst_state->unwound = src->unwound;
dst_state->curframe = src->curframe;
dst_state->branches = src->branches;
dst_state->parent = src->parent;
@@ -3430,6 +3431,45 @@ static int next_callx_callee(struct bpf_verifier_env *env, int caller, int from)
return bit < end ? bit - caller * cnt : -1;
}
+/*
+ * '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.
+ */
+static int check_unwind_insns(struct bpf_verifier_env *env)
+{
+ struct bpf_insn *insn = env->prog->insnsi;
+ bool seen = false;
+ int i;
+
+ for (i = 0; i < env->prog->len; i++, insn++) {
+ if (bpf_is_unwind_exit(insn)) {
+ bpf_find_containing_subprog(env, i)->can_unwind = true;
+ seen = true;
+ }
+ if (!bpf_is_unwind_goto(insn))
+ continue;
+ 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",
+ i);
+ return -EINVAL;
+ }
+ if (bpf_is_jump_target(env, i)) {
+ verbose(env, "jump to unwind_goto at insn %d\n", i);
+ return -EINVAL;
+ }
+ }
+ if (!seen)
+ return 0;
+ if (!bpf_jit_supports_unwind()) {
+ verbose(env, "JIT does not support unwinding\n");
+ return -ENOTSUPP;
+ }
+ env->prog->jit_required = 1;
+ return 0;
+}
+
/*
* Sort subprogs in topological order so that leaf subprogs come first and
* their callers come later. This is a DFS post-order traversal of the call
@@ -11489,6 +11529,19 @@ static int check_func_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
}
}
+ if (env->subprog_info[subprog].can_unwind) {
+ struct bpf_verifier_state *branch;
+
+ if (!bpf_is_unwind_goto(insn + 1)) {
+ verbose(env, "no unwind_goto after the call of %s()\n", sub_name);
+ return -EINVAL;
+ }
+ branch = push_stack(env, *insn_idx + 1, *insn_idx, false);
+ if (IS_ERR(branch))
+ return PTR_ERR(branch);
+ branch->unwound = true;
+ }
+
if (env->subprog_info[subprog].might_throw) {
struct bpf_verifier_state *branch;
@@ -11962,6 +12015,19 @@ 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;
+ }
+ 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);
+ return -EINVAL;
+ }
+ state->unwound = true;
+ }
if (callee->in_callback_fn) {
if (r0->type != SCALAR_VALUE) {
verbose(env, "R0 not a scalar value\n");
@@ -18663,6 +18729,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;
@@ -19890,6 +19963,12 @@ static int process_bpf_exit_full(struct bpf_verifier_env *env,
* Exit from async callback implies return code restrictions
* that depend on async scheduling mechanism.
*/
+ if ((cur_frame->in_async_callback_fn || cur_frame->in_exception_callback_fn) &&
+ bpf_is_unwind_exit(&env->prog->insnsi[env->insn_idx])) {
+ verbose(env, "callback cannot unwind\n");
+ return -EINVAL;
+ }
+
if (cur_frame->subprogno &&
!cur_frame->in_async_callback_fn &&
!cur_frame->in_exception_callback_fn)
@@ -21092,14 +21171,14 @@ static int check_jmp_fields(struct bpf_verifier_env *env, struct bpf_insn *insn)
return 0;
case BPF_EXIT:
if (BPF_SRC(insn->code) != BPF_K || insn->imm != 0 ||
- insn->src_reg != BPF_REG_0 || insn->dst_reg != BPF_REG_0 ||
+ insn->src_reg > BPF_UNWIND_EXIT || insn->dst_reg != BPF_REG_0 ||
class == BPF_JMP32) {
verbose(env, "BPF_EXIT uses reserved fields\n");
return -EINVAL;
}
return 0;
case BPF_JCOND:
- if (insn->code != (BPF_JMP | BPF_JCOND) || insn->src_reg != BPF_MAY_GOTO ||
+ if (insn->code != (BPF_JMP | BPF_JCOND) || insn->src_reg > BPF_UNWIND_GOTO ||
insn->dst_reg || insn->imm) {
verbose(env, "invalid may_goto imm %d\n", insn->imm);
return -EINVAL;
@@ -22299,6 +22378,12 @@ int bpf_check_attach_target(struct bpf_verifier_log *log,
prog_extension ? "Extension" : "Tracing");
return -EINVAL;
}
+ /* unwind_exit would return to the trampoline that called the subprog */
+ if (aux->func && subprog && aux->func[subprog]->aux->can_unwind &&
+ !prog_extension && prog->expected_attach_type != BPF_TRACE_FENTRY) {
+ bpf_log(log, "Only fentry can attach to %s, which can unwind\n", tname);
+ return -EINVAL;
+ }
conservative = aux->func_info_aux[subprog].unreliable;
if (prog_extension) {
if (aux->func_info_aux[subprog].linkage != BTF_FUNC_GLOBAL ||
@@ -23673,6 +23758,10 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,
if (ret < 0)
goto skip_full_check;
+ ret = check_unwind_insns(env);
+ if (ret < 0)
+ goto skip_full_check;
+
ret = bpf_compute_postorder(env);
if (ret < 0)
goto skip_full_check;
diff --git a/tools/include/uapi/linux/bpf.h b/tools/include/uapi/linux/bpf.h
index 6f70db7515dd..6efe0ca03624 100644
--- a/tools/include/uapi/linux/bpf.h
+++ b/tools/include/uapi/linux/bpf.h
@@ -56,8 +56,19 @@
enum bpf_cond_pseudo_jmp {
BPF_MAY_GOTO = 0,
+ /*
+ * Follows a call of a bpf subprog. Taken when the subprog left with
+ * BPF_EXIT that has src_reg == BPF_UNWIND_EXIT.
+ */
+ BPF_UNWIND_GOTO = 1,
};
+/*
+ * When bpf_exit->src_reg == BPF_UNWIND_EXIT, it's the same as bpf_exit, except
+ * that BPF_UNWIND_GOTO after the call is taken.
+ */
+#define BPF_UNWIND_EXIT 1
+
/* Register numbers */
enum {
BPF_REG_0 = 0,
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 bpf-next 2/5] bpf, x86: Support unwind_goto and unwind_exit
2026-10-19 5:18 [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit Alexei Starovoitov
2026-10-19 5:18 ` [PATCH v2 bpf-next 1/5] bpf: Add unwind_goto and unwind_exit insns Alexei Starovoitov
@ 2026-10-19 5:18 ` Alexei Starovoitov
2026-10-19 5:18 ` [PATCH v2 bpf-next 3/5] bpf, arm64: " Alexei Starovoitov
` (3 subsequent siblings)
5 siblings, 0 replies; 11+ messages in thread
From: Alexei Starovoitov @ 2026-10-19 5:18 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
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>
---
arch/x86/net/bpf_jit_comp.c | 83 ++++++++++++++++++++++++++++++++++++-
1 file changed, 82 insertions(+), 1 deletion(-)
diff --git a/arch/x86/net/bpf_jit_comp.c b/arch/x86/net/bpf_jit_comp.c
index d3843045dc71..abb870c77c49 100644
--- a/arch/x86/net/bpf_jit_comp.c
+++ b/arch/x86/net/bpf_jit_comp.c
@@ -2511,6 +2511,50 @@ static void adjust_cmp_imm(u32 *op, s32 *imm)
}
}
+/* unwind_goto: an entry for bpf_unwind_target() */
+static int add_unwind_exentry(struct bpf_prog *prog, u8 *image, u8 *rw_image, int *excnt,
+ int ret_addr, int target)
+{
+ struct exception_table_entry *ex;
+ s64 delta;
+
+ if (!prog->aux->extable)
+ return 0;
+ if (*excnt >= prog->aux->num_exentries) {
+ pr_err("unwind extable bug\n");
+ return -EFAULT;
+ }
+ ex = &prog->aux->extable[(*excnt)++];
+ delta = image + ret_addr - 1 - (u8 *)&ex->insn;
+ if (!is_simm32(delta)) {
+ pr_err("extable->insn doesn't fit into 32-bit\n");
+ return -EFAULT;
+ }
+ /* switch ex to rw buffer for writes */
+ ex = (void *)rw_image + ((void *)ex - (void *)image);
+ ex->insn = delta;
+ ex->fixup = target - ret_addr;
+ ex->data = EX_TYPE_BPF;
+ return 0;
+}
+
+/* unwind_exit: fix the return address of this frame. r0 and r2 are returned. */
+static int emit_unwind_ret_addr(u8 **pprog, u8 *ip)
+{
+ u8 *prog = *pprog, *start = prog;
+
+ EMIT2(0x50, 0x56); /* push rax; push rsi */
+ EMIT4(0x48, 0x8B, 0x7D, 0x08); /* mov rdi, [rbp + 8] */
+ ip += prog - start;
+ ip += x86_call_depth_emit_accounting(&prog, bpf_unwind_target, ip);
+ if (emit_call(&prog, bpf_unwind_target, ip))
+ return -EINVAL;
+ EMIT4(0x48, 0x89, 0x45, 0x08); /* mov [rbp + 8], rax */
+ EMIT2(0x5E, 0x58); /* pop rsi; pop rax */
+ *pprog = prog;
+ return 0;
+}
+
static int do_jit(struct bpf_verifier_env *env, struct bpf_prog *bpf_prog, int *addrs, u8 *image,
u8 *rw_image, int oldproglen, struct jit_context *ctx, bool jmp_padding)
{
@@ -3491,6 +3535,34 @@ static int do_jit(struct bpf_verifier_env *env, struct bpf_prog *bpf_prog, int *
pop_r9(&prog);
break;
+ /* unwind_goto. The verifier expanded may_goto. */
+ case BPF_JMP | BPF_JCOND:
+ if (!priv_frame_ptr) {
+ err = add_unwind_exentry(bpf_prog, image, rw_image, &excnt,
+ addrs[i - 1], addrs[i + insn->off]);
+ if (err)
+ return err;
+ break;
+ }
+ /*
+ * 'pop r9' after the call has to be done on the way to
+ * the pad too:
+ * call foo
+ * pop r9 <- exit returns here
+ * jmp 1f
+ * pop r9 <- unwind_exit returns here
+ * jmp pad
+ * 1:
+ */
+ err = add_unwind_exentry(bpf_prog, image, rw_image, &excnt,
+ addrs[i - 1] - 2, addrs[i - 1] + 2);
+ if (err)
+ return err;
+ EMIT2(0xEB, 7);
+ pop_r9(&prog);
+ EMIT1_off32(0xE9, addrs[i + insn->off] - (addrs[i - 1] + 9));
+ break;
+
case BPF_JMP | BPF_TAIL_CALL:
if (imm32)
emit_bpf_tail_call_direct(bpf_prog,
@@ -3772,13 +3844,17 @@ static int do_jit(struct bpf_verifier_env *env, struct bpf_prog *bpf_prog, int *
break;
case BPF_JMP | BPF_EXIT:
+ /* The caller of the main prog doesn't tell them apart */
+ if (src_reg == BPF_UNWIND_EXIT && bpf_is_subprog(bpf_prog) &&
+ emit_unwind_ret_addr(&prog, ip))
+ return -EINVAL;
if (seen_exit) {
jmp_offset = ctx->cleanup_addr - addrs[i];
goto emit_jmp;
}
seen_exit = true;
/* Update cleanup_addr */
- ctx->cleanup_addr = proglen;
+ ctx->cleanup_addr = proglen + (prog - temp);
if (bpf_prog_was_classic(bpf_prog) &&
!ns_capable_noaudit(&init_user_ns, CAP_SYS_ADMIN)) {
if (emit_spectre_bhb_barrier(&prog, ip, bpf_prog))
@@ -5025,6 +5101,11 @@ bool bpf_jit_supports_kfunc_ret_reg_pair(void)
return true;
}
+bool bpf_jit_supports_unwind(void)
+{
+ return true;
+}
+
const struct bpf_jit_arg_abi *bpf_jit_arg_abi(void)
{
return &x86_arg_abi;
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 bpf-next 3/5] bpf, arm64: Support unwind_goto and unwind_exit
2026-10-19 5:18 [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit Alexei Starovoitov
2026-10-19 5:18 ` [PATCH v2 bpf-next 1/5] bpf: Add unwind_goto and unwind_exit insns Alexei Starovoitov
2026-10-19 5:18 ` [PATCH v2 bpf-next 2/5] bpf, x86: Support unwind_goto and unwind_exit Alexei Starovoitov
@ 2026-10-19 5:18 ` Alexei Starovoitov
2026-10-19 5:18 ` [PATCH v2 bpf-next 4/5] selftests/bpf: Add tests for " Alexei Starovoitov
` (2 subsequent siblings)
5 siblings, 0 replies; 11+ messages in thread
From: Alexei Starovoitov @ 2026-10-19 5:18 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: 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>
---
arch/arm64/net/bpf_jit.h | 2 +
arch/arm64/net/bpf_jit_comp.c | 78 +++++++++++++++++++++++++++++++++++
2 files changed, 80 insertions(+)
diff --git a/arch/arm64/net/bpf_jit.h b/arch/arm64/net/bpf_jit.h
index b2fe6e6dcf44..50029dd9baa6 100644
--- a/arch/arm64/net/bpf_jit.h
+++ b/arch/arm64/net/bpf_jit.h
@@ -316,6 +316,8 @@
#define A64_PACIASP A64_HINT(AARCH64_INSN_HINT_PACIASP)
#define A64_AUTIASP A64_HINT(AARCH64_INSN_HINT_AUTIASP)
+#define A64_PACIA1716 A64_HINT(AARCH64_INSN_HINT_PACIA_1716)
+#define A64_AUTIA1716 A64_HINT(AARCH64_INSN_HINT_AUTIA_1716)
/* BTI */
#define A64_BTI_C A64_HINT(AARCH64_INSN_HINT_BTIC)
diff --git a/arch/arm64/net/bpf_jit_comp.c b/arch/arm64/net/bpf_jit_comp.c
index 506c4201fec8..182463b730b9 100644
--- a/arch/arm64/net/bpf_jit_comp.c
+++ b/arch/arm64/net/bpf_jit_comp.c
@@ -1221,6 +1221,70 @@ static int add_exception_handler(const struct bpf_insn *insn,
return 0;
}
+/*
+ * unwind_goto: an entry for bpf_unwind_target(). 'bl foo; mov x8, x0' is in front
+ * of it. 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)
+{
+ int ret_addr = ctx->idx - 1;
+ struct exception_table_entry *ex;
+ off_t ins_offset;
+
+ if (!ctx->image)
+ /* First pass */
+ return 0;
+
+ if (!ctx->prog->aux->extable ||
+ WARN_ON_ONCE(ctx->exentry_idx >= ctx->prog->aux->num_exentries))
+ return -EINVAL;
+
+ ex = &ctx->prog->aux->extable[ctx->exentry_idx];
+ ins_offset = (long)&ctx->ro_image[ret_addr] - 1 - (long)&ex->insn;
+ if (WARN_ON_ONCE(ins_offset >= 0 || ins_offset < INT_MIN))
+ return -ERANGE;
+
+ /* 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->type = EX_TYPE_BPF;
+
+ ctx->exentry_idx++;
+ return 0;
+}
+
+/*
+ * unwind_exit: fix LR in the frame record. It's signed with SP at the entry of
+ * the prog, which is FP + 16. The x16/x17 forms are hints like PACIASP: nops
+ * without pointer auth. r0 and r2 are returned.
+ */
+static void emit_unwind_ret_addr(struct jit_ctx *ctx)
+{
+ const bool ptr_auth = IS_ENABLED(CONFIG_ARM64_PTR_AUTH_KERNEL);
+ const u8 r0 = bpf2a64[BPF_REG_0];
+ const u8 r2 = bpf2a64[BPF_REG_2];
+
+ emit(A64_PUSH(r0, r2, A64_SP), ctx);
+
+ emit(A64_LDR64I(A64_R(17), A64_FP, 8), ctx);
+ if (ptr_auth) {
+ emit(A64_ADD_I(1, A64_R(16), A64_FP, 16), ctx);
+ emit(A64_AUTIA1716, ctx);
+ }
+ emit(A64_MOV(1, A64_R(0), A64_R(17)), ctx);
+ emit_call((u64)bpf_unwind_target, ctx);
+ emit(A64_MOV(1, A64_R(17), A64_R(0)), ctx);
+ if (ptr_auth) {
+ emit(A64_ADD_I(1, A64_R(16), A64_FP, 16), ctx);
+ emit(A64_PACIA1716, ctx);
+ }
+ emit(A64_STR64I(A64_R(17), A64_FP, 8), ctx);
+
+ emit(A64_POP(r0, r2, A64_SP), ctx);
+}
+
static const struct bpf_jit_arg_abi arm64_arg_abi = {
.nr_arg_regs = 8,
.even_reg_align = true,
@@ -1806,8 +1870,17 @@ static int build_insn(const struct bpf_verifier_env *env, const struct bpf_insn
if (emit_bpf_tail_call(ctx))
return -EFAULT;
break;
+ /* unwind_goto. The verifier expanded may_goto. */
+ case BPF_JMP | BPF_JCOND:
+ ret = add_unwind_exentry(ctx, i + 1 + off);
+ if (ret)
+ return ret;
+ break;
/* function return */
case BPF_JMP | BPF_EXIT:
+ /* The caller of the main prog doesn't tell them apart */
+ if (bpf_is_unwind_exit(insn) && bpf_is_subprog(ctx->prog))
+ emit_unwind_ret_addr(ctx);
/* Optimization: when last instruction is EXIT,
simply fallthrough to epilogue. */
if (i == ctx->prog->len - 1)
@@ -2464,6 +2537,11 @@ bool bpf_jit_supports_kfunc_ret_reg_pair(void)
return true;
}
+bool bpf_jit_supports_unwind(void)
+{
+ return true;
+}
+
const struct bpf_jit_arg_abi *bpf_jit_arg_abi(void)
{
return &arm64_arg_abi;
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 bpf-next 4/5] selftests/bpf: Add tests for unwind_goto and unwind_exit
2026-10-19 5:18 [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit Alexei Starovoitov
` (2 preceding siblings ...)
2026-10-19 5:18 ` [PATCH v2 bpf-next 3/5] bpf, arm64: " Alexei Starovoitov
@ 2026-10-19 5:18 ` Alexei Starovoitov
2026-10-19 6:00 ` bot+bpf-ci
2026-10-19 5:18 ` [PATCH v2 bpf-next 5/5] selftests/bpf: Add a C++ prog with exceptions Alexei Starovoitov
2026-10-19 20:32 ` [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit Eduard Zingerman
5 siblings, 1 reply; 11+ messages in thread
From: Alexei Starovoitov @ 2026-10-19 5:18 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
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>
---
.../selftests/bpf/prog_tests/unwind_attach.c | 102 ++
.../selftests/bpf/prog_tests/verifier.c | 2 +
.../selftests/bpf/progs/unwind_attach.c | 29 +
.../selftests/bpf/progs/verifier_unwind.c | 1047 +++++++++++++++++
tools/testing/selftests/bpf/test_loader.c | 3 +-
5 files changed, 1182 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/unwind_attach.c
create mode 100644 tools/testing/selftests/bpf/progs/unwind_attach.c
create mode 100644 tools/testing/selftests/bpf/progs/verifier_unwind.c
diff --git a/tools/testing/selftests/bpf/prog_tests/unwind_attach.c b/tools/testing/selftests/bpf/prog_tests/unwind_attach.c
new file mode 100644
index 000000000000..cdbf172e13bb
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/unwind_attach.c
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <test_progs.h>
+#include "verifier_unwind.skel.h"
+#include "unwind_attach.skel.h"
+
+#ifndef ENOTSUPP
+#define ENOTSUPP 524
+#endif
+
+static char log_buf[16 * 1024];
+
+static int run(int fd)
+{
+ char data[64] = {};
+ LIBBPF_OPTS(bpf_test_run_opts, opts,
+ .data_in = data,
+ .data_size_in = sizeof(data),
+ .repeat = 1,
+ );
+
+ ASSERT_OK(bpf_prog_test_run_opts(fd, &opts), "test_run");
+ return opts.retval;
+}
+
+/*
+ * The target calls global_unwinds(), which unwinds with r0 = 5, and returns
+ * r0 + 10 from the pad.
+ * msg: why it cannot be loaded. Otherwise retval is what the target returns
+ * with the prog attached.
+ */
+static void attach(int tgt_fd, const char *name, const char *msg, int retval)
+{
+ struct bpf_link *link = NULL;
+ struct unwind_attach *skel;
+ struct bpf_program *prog;
+ int err;
+
+ skel = unwind_attach__open();
+ if (!ASSERT_OK_PTR(skel, "unwind_attach__open"))
+ return;
+
+ prog = bpf_object__find_program_by_name(skel->obj, name);
+ if (!ASSERT_OK_PTR(prog, name))
+ goto out;
+ bpf_program__set_autoload(prog, true);
+ log_buf[0] = 0;
+ bpf_program__set_log_buf(prog, log_buf, sizeof(log_buf));
+ err = bpf_program__set_attach_target(prog, tgt_fd, "global_unwinds");
+ if (!ASSERT_OK(err, "set_attach_target"))
+ goto out;
+
+ err = unwind_attach__load(skel);
+ if (msg) {
+ ASSERT_ERR(err, name);
+ ASSERT_HAS_SUBSTR(log_buf, msg, name);
+ goto out;
+ }
+ if (!ASSERT_OK(err, name))
+ goto out;
+
+ link = bpf_program__attach(prog);
+ if (!ASSERT_OK_PTR(link, "attach"))
+ goto out;
+ ASSERT_EQ(run(tgt_fd), retval, "retval");
+ if (prog == skel->progs.fentry_unwind)
+ ASSERT_EQ(skel->bss->fentry_cnt, 1, "fentry_cnt");
+out:
+ bpf_link__destroy(link);
+ unwind_attach__destroy(skel);
+}
+
+void test_unwind_attach(void)
+{
+ struct verifier_unwind *tgt;
+ struct bpf_program *prog;
+ int tgt_fd, err;
+
+ tgt = verifier_unwind__open();
+ if (!ASSERT_OK_PTR(tgt, "verifier_unwind__open"))
+ return;
+ bpf_object__for_each_program(prog, tgt->obj)
+ bpf_program__set_autoload(prog, prog == tgt->progs.calls_global_unwinds);
+ err = verifier_unwind__load(tgt);
+ if (err == -ENOTSUPP) {
+ printf("%s:SKIP:JIT does not support unwinding\n", __func__);
+ test__skip();
+ goto out;
+ }
+ if (!ASSERT_OK(err, "verifier_unwind__load"))
+ goto out;
+ tgt_fd = bpf_program__fd(tgt->progs.calls_global_unwinds);
+ ASSERT_EQ(run(tgt_fd), 15, "retval");
+
+ attach(tgt_fd, "fentry_unwind", NULL, 15);
+ /* unwind_exit would return to the trampoline */
+ attach(tgt_fd, "fexit_unwind", "Only fentry can attach to global_unwinds", 0);
+ /* An extension doesn't unwind. The caller is ready for that. */
+ attach(tgt_fd, "freplace_unwind", NULL, 9);
+ ASSERT_EQ(run(tgt_fd), 15, "retval");
+out:
+ verifier_unwind__destroy(tgt);
+}
diff --git a/tools/testing/selftests/bpf/prog_tests/verifier.c b/tools/testing/selftests/bpf/prog_tests/verifier.c
index c69c0f7e9cc8..28d06a7f0ec6 100644
--- a/tools/testing/selftests/bpf/prog_tests/verifier.c
+++ b/tools/testing/selftests/bpf/prog_tests/verifier.c
@@ -129,6 +129,7 @@
#include "verifier_typedef.skel.h"
#include "verifier_uninit.skel.h"
#include "verifier_unpriv.skel.h"
+#include "verifier_unwind.skel.h"
#include "verifier_unpriv_perf.skel.h"
#include "verifier_value_adj_spill.skel.h"
#include "verifier_value.skel.h"
@@ -318,6 +319,7 @@ void test_verifier_tailcall_jit(void) { RUN(verifier_tailcall_jit); }
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_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); }
diff --git a/tools/testing/selftests/bpf/progs/unwind_attach.c b/tools/testing/selftests/bpf/progs/unwind_attach.c
new file mode 100644
index 000000000000..c39df02385e4
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/unwind_attach.c
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+/* The target is global_unwinds() in verifier_unwind.c */
+
+int fentry_cnt;
+
+SEC("?fentry/global_unwinds")
+int BPF_PROG(fentry_unwind)
+{
+ fentry_cnt++;
+ return 0;
+}
+
+SEC("?fexit/global_unwinds")
+int BPF_PROG(fexit_unwind)
+{
+ return 0;
+}
+
+SEC("?freplace/global_unwinds")
+unsigned long freplace_unwind(void)
+{
+ return 9;
+}
+
+char _license[] SEC("license") = "GPL";
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 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+#include "../../../include/linux/filter.h"
+#include "bpf_misc.h"
+
+/*
+ * llvm lowers unwinding to two insns:
+ * invoke foo unwind pad call foo; unwind_goto pad
+ * resume unwind_exit
+ * The tests spell them out as raw insns, so they don't depend on llvm version.
+ * The argument of unwind_goto() is the number of insns to skip.
+ */
+#define unwind_goto(off) ".8byte %[insn_unwind_goto_" #off "];"
+#define unwind_exit ".8byte %[insn_unwind_exit];"
+
+#define __imm_unwind_goto(off) \
+ __imm_insn(insn_unwind_goto_##off, \
+ BPF_RAW_INSN(BPF_JMP | BPF_JCOND, 0, BPF_UNWIND_GOTO, off, 0))
+#define __imm_unwind_exit \
+ __imm_insn(insn_unwind_exit, \
+ BPF_RAW_INSN(BPF_JMP | BPF_EXIT, 0, BPF_UNWIND_EXIT, 0, 0))
+
+extern void bpf_rcu_read_lock(void) __ksym;
+extern void bpf_rcu_read_unlock(void) __ksym;
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
+ __uint(max_entries, 1);
+ __uint(key_size, sizeof(int));
+ __uint(value_size, sizeof(int));
+} prog_array SEC(".maps");
+
+/* The verifier doesn't know what is in there */
+unsigned long zero;
+unsigned long one = 1;
+
+/*
+ * The verifier follows a static subprog, so it knows which way it leaves when
+ * it knows the argument. To have both ways at run time the tests pass skb->len,
+ * which is 64, or skb->len - 64.
+ *
+ * r1 == 0: returns 7, otherwise unwinds with r0 = 5
+ */
+__naked __noinline __used
+static unsigned long may_unwind(void)
+{
+ asm volatile (
+ "if r1 != 0 goto 1f;"
+ "r0 = 7;"
+ "exit;"
+ "1:"
+ "r0 = 5;"
+ unwind_exit
+ :
+ : __imm_unwind_exit
+ );
+}
+
+/* owns nothing: its pad only carries on */
+__naked __noinline __used
+static unsigned long passes_on(void)
+{
+ asm volatile (
+ "call may_unwind;"
+ unwind_goto(2)
+ "r0 += 1;"
+ "exit;"
+ unwind_exit
+ :
+ : __imm_unwind_goto(2),
+ __imm_unwind_exit
+ );
+}
+
+__naked __noinline __used
+static unsigned long always_unwinds(void)
+{
+ asm volatile (
+ "r0 = 5;"
+ unwind_exit
+ :
+ : __imm_unwind_exit
+ );
+}
+
+__naked __noinline __used
+static unsigned long never_unwinds(void)
+{
+ asm volatile (
+ "r0 = 7;"
+ "exit;"
+ );
+}
+
+/* Both can unwind. This one returns 7. */
+__naked __noinline __used
+unsigned long global_returns(void)
+{
+ asm volatile (
+ "r1 = %[zero] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+ "if r1 != 0 goto 1f;"
+ "r0 = 7;"
+ "exit;"
+ "1:"
+ "r0 = 5;"
+ unwind_exit
+ :
+ : __imm_addr(zero),
+ __imm_unwind_exit
+ );
+}
+
+/* This one unwinds with r0 = 5 */
+__naked __noinline __used
+unsigned long global_unwinds(void)
+{
+ asm volatile (
+ "r1 = %[one] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+ "if r1 != 0 goto 1f;"
+ "r0 = 7;"
+ "exit;"
+ "1:"
+ "r0 = 5;"
+ unwind_exit
+ :
+ : __imm_addr(one),
+ __imm_unwind_exit
+ );
+}
+
+SEC("tc")
+__description("unwind: the callee returns")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(7)
+__naked int callee_returns(void)
+{
+ asm volatile (
+ "r1 = *(u32 *)(r1 + %[len]);"
+ "r1 -= 64;"
+ "call may_unwind;"
+ unwind_goto(1)
+ "exit;"
+ "r0 = 42;"
+ "exit;"
+ :
+ : __imm_const(len, offsetof(struct __sk_buff, len)),
+ __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: the callee unwinds")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(42)
+__naked int callee_unwinds(void)
+{
+ asm volatile (
+ "r1 = *(u32 *)(r1 + %[len]);"
+ "call may_unwind;"
+ unwind_goto(1)
+ "exit;"
+ "r0 = 42;"
+ "exit;"
+ :
+ : __imm_const(len, offsetof(struct __sk_buff, len)),
+ __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: return through two frames")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(8)
+__naked int two_frames_return(void)
+{
+ asm volatile (
+ "r1 = *(u32 *)(r1 + %[len]);"
+ "r1 -= 64;"
+ "call passes_on;"
+ unwind_goto(1)
+ "exit;"
+ "r0 = 42;"
+ "exit;"
+ :
+ : __imm_const(len, offsetof(struct __sk_buff, len)),
+ __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+/* unwind_exit returns r0 like exit does */
+SEC("tc")
+__description("unwind: unwind through two frames, r0 reaches the pad")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(15)
+__naked int two_frames_unwind(void)
+{
+ asm volatile (
+ "r1 = *(u32 *)(r1 + %[len]);"
+ "call passes_on;"
+ unwind_goto(1)
+ "exit;"
+ "r0 += 10;"
+ "exit;"
+ :
+ : __imm_const(len, offsetof(struct __sk_buff, len)),
+ __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: r6-r9 and the stack are intact in the pad")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(1111)
+__naked int pad_sees_callee_saved(void)
+{
+ asm volatile (
+ "r1 = *(u32 *)(r1 + %[len]);"
+ "r6 = 1;"
+ "r7 = 10;"
+ "r8 = 100;"
+ "r9 = 400;"
+ "*(u64 *)(r10 - 8) = 600;"
+ "call passes_on;"
+ unwind_goto(2)
+ "r0 = 0;"
+ "exit;"
+ "r0 = r6;"
+ "r0 += r7;"
+ "r0 += r8;"
+ "r0 += r9;"
+ "r1 = *(u64 *)(r10 - 8);"
+ "r0 += r1;"
+ "exit;"
+ :
+ : __imm_const(len, offsetof(struct __sk_buff, len)),
+ __imm_unwind_goto(2)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: r1-r5 are not readable in the pad")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("R3 !read_ok")
+__naked int pad_reads_r3(void)
+{
+ asm volatile (
+ "r3 = 0;"
+ "call always_unwinds;"
+ unwind_goto(1)
+ "exit;"
+ "r0 = r3;"
+ "exit;"
+ :
+ : __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: unwind_exit of the main prog is exit")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(3)
+__naked int main_prog_unwinds(void)
+{
+ asm volatile (
+ "r0 = 3;"
+ unwind_exit
+ :
+ : __imm_unwind_exit
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: unwind_exit of the main prog needs r0")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("R0 !read_ok")
+__naked int main_prog_unwinds_no_r0(void)
+{
+ asm volatile (
+ unwind_exit
+ :
+ : __imm_unwind_exit
+ : __clobber_all
+ );
+}
+
+/* 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 pad is dead code and goes away together with unwind_goto */
+SEC("tc")
+__description("unwind: the callee never unwinds")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(7)
+__xlated("0: call pc+1")
+__xlated("1: exit")
+__xlated("2: r0 = 7")
+__xlated("3: exit")
+__naked int callee_never_unwinds(void)
+{
+ asm volatile (
+ "call never_unwinds;"
+ unwind_goto(1)
+ "exit;"
+ "r0 = *(u64 *)(r0 + 0);"
+ "exit;"
+ :
+ : __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: the pad drops what the frame owns")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(42)
+__naked int pad_unlocks(void)
+{
+ asm volatile (
+ "r6 = *(u32 *)(r1 + %[len]);"
+ "call %[bpf_rcu_read_lock];"
+ "r1 = r6;"
+ "call may_unwind;"
+ unwind_goto(3)
+ "call %[bpf_rcu_read_unlock];"
+ "r0 = 0;"
+ "exit;"
+ "call %[bpf_rcu_read_unlock];"
+ "r0 = 42;"
+ "exit;"
+ :
+ : __imm_const(len, offsetof(struct __sk_buff, len)),
+ __imm(bpf_rcu_read_lock),
+ __imm(bpf_rcu_read_unlock),
+ __imm_unwind_goto(3)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: the pad is checked like any other code")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("in main prog cannot be used inside bpf_rcu_read_lock-ed region")
+__naked int pad_does_not_unlock(void)
+{
+ asm volatile (
+ "r6 = *(u32 *)(r1 + %[len]);"
+ "call %[bpf_rcu_read_lock];"
+ "r1 = r6;"
+ "call may_unwind;"
+ unwind_goto(3)
+ "call %[bpf_rcu_read_unlock];"
+ "r0 = 0;"
+ "exit;"
+ "r0 = 42;"
+ "exit;"
+ :
+ : __imm_const(len, offsetof(struct __sk_buff, len)),
+ __imm(bpf_rcu_read_lock),
+ __imm(bpf_rcu_read_unlock),
+ __imm_unwind_goto(3)
+ : __clobber_all
+ );
+}
+
+/* Leaves either way with the same registers */
+__naked __noinline __used
+static unsigned long same_regs(void)
+{
+ asm volatile (
+ "r0 = 0;"
+ "if r1 != 0 goto 1f;"
+ "exit;"
+ "1:"
+ unwind_exit
+ :
+ : __imm_unwind_exit
+ );
+}
+
+/*
+ * 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
+ );
+}
+
+SEC("tc")
+__description("unwind: a global subprog returns")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(7)
+__naked int calls_global_returns(void)
+{
+ asm volatile (
+ "call global_returns;"
+ unwind_goto(1)
+ "exit;"
+ "r0 = 42;"
+ "exit;"
+ :
+ : __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: a global subprog unwinds")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(15)
+__naked int calls_global_unwinds(void)
+{
+ asm volatile (
+ "call global_unwinds;"
+ unwind_goto(1)
+ "exit;"
+ "r0 += 10;"
+ "exit;"
+ :
+ : __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+/* The verifier doesn't look inside */
+SEC("tc")
+__description("unwind: the pad of a call of a global subprog is always checked")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("R0 invalid mem access 'scalar'")
+__naked int global_pad_is_checked(void)
+{
+ asm volatile (
+ "call global_returns;"
+ unwind_goto(1)
+ "exit;"
+ "r0 = *(u64 *)(r0 + 0);"
+ "exit;"
+ :
+ : __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: no unwind_goto after a call of a global subprog")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("no unwind_goto after the call of global_returns()")
+__naked int global_no_unwind_goto(void)
+{
+ asm volatile (
+ "call global_returns;"
+ "exit;"
+ ::: __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: no unwind_goto after a call of a static subprog")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("unwind_exit, but no unwind_goto after the call at insn 1")
+__naked int static_no_unwind_goto(void)
+{
+ asm volatile (
+ "r1 = *(u32 *)(r1 + %[len]);"
+ "call may_unwind;"
+ "exit;"
+ :
+ : __imm_const(len, offsetof(struct __sk_buff, len))
+ : __clobber_all
+ );
+}
+
+/* The verifier sees that it doesn't get to unwind_exit */
+SEC("tc")
+__description("unwind: no unwind_goto is fine when the callee doesn't unwind")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(7)
+__naked int static_does_not_get_there(void)
+{
+ asm volatile (
+ "r1 = 0;"
+ "call may_unwind;"
+ "exit;"
+ ::: __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: callx, the callee returns")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(7)
+__naked int callx_returns(void)
+{
+ asm volatile (
+ "r1 = *(u32 *)(r1 + %[len]);"
+ "r1 -= 64;"
+ "r6 = %[may_unwind] ll;"
+ "callx r6;"
+ unwind_goto(1)
+ "exit;"
+ "r0 = 42;"
+ "exit;"
+ :
+ : __imm_const(len, offsetof(struct __sk_buff, len)),
+ __imm_addr(may_unwind),
+ __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: callx, the callee unwinds")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(42)
+__naked int callx_unwinds(void)
+{
+ asm volatile (
+ "r1 = *(u32 *)(r1 + %[len]);"
+ "r6 = %[may_unwind] ll;"
+ "callx r6;"
+ unwind_goto(1)
+ "exit;"
+ "r0 = 42;"
+ "exit;"
+ :
+ : __imm_const(len, offsetof(struct __sk_buff, len)),
+ __imm_addr(may_unwind),
+ __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: unwind_goto after a plain insn")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("unwind_goto at insn 1 doesn't follow a call of a subprog")
+__naked int unwind_goto_after_mov(void)
+{
+ asm volatile (
+ "r0 = 0;"
+ unwind_goto(0)
+ "exit;"
+ :
+ : __imm_unwind_goto(0)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: unwind_goto is the first insn")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("unwind_goto at insn 0 doesn't follow a call of a subprog")
+__naked int unwind_goto_first(void)
+{
+ asm volatile (
+ unwind_goto(0)
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm_unwind_goto(0)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: unwind_goto after a call of a helper")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("unwind_goto at insn 1 doesn't follow a call of a subprog")
+__naked int unwind_goto_after_helper(void)
+{
+ asm volatile (
+ "call %[bpf_get_prandom_u32];"
+ unwind_goto(0)
+ "exit;"
+ :
+ : __imm(bpf_get_prandom_u32),
+ __imm_unwind_goto(0)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: unwind_goto after a call of a kfunc")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("unwind_goto at insn 1 doesn't follow a call of a subprog")
+__naked int unwind_goto_after_kfunc(void)
+{
+ asm volatile (
+ "call %[bpf_rcu_read_lock];"
+ unwind_goto(0)
+ "call %[bpf_rcu_read_unlock];"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm(bpf_rcu_read_lock),
+ __imm(bpf_rcu_read_unlock),
+ __imm_unwind_goto(0)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: jump to unwind_goto")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("jump to unwind_goto at insn 3")
+__naked int jump_to_unwind_goto(void)
+{
+ asm volatile (
+ "r0 = 0;"
+ "if r1 != 0 goto 1f;"
+ "call never_unwinds;"
+ "1:"
+ unwind_goto(0)
+ "exit;"
+ :
+ : __imm_unwind_goto(0)
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: reserved fields of unwind_goto")
+__failure __msg("invalid may_goto imm 1")
+__naked int unwind_goto_reserved(void)
+{
+ asm volatile (
+ "call never_unwinds;"
+ ".8byte %[bad];"
+ "exit;"
+ :
+ : __imm_insn(bad, BPF_RAW_INSN(BPF_JMP | BPF_JCOND, 0, BPF_UNWIND_GOTO, 0, 1))
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: unknown kind of jcond")
+__failure __msg("invalid may_goto imm 0")
+__naked int jcond_unknown(void)
+{
+ asm volatile (
+ "call never_unwinds;"
+ ".8byte %[bad];"
+ "exit;"
+ :
+ : __imm_insn(bad, BPF_RAW_INSN(BPF_JMP | BPF_JCOND, 0, BPF_UNWIND_GOTO + 1, 0, 0))
+ : __clobber_all
+ );
+}
+
+SEC("tc")
+__description("unwind: unknown kind of exit")
+__failure __msg("BPF_EXIT uses reserved fields")
+__naked int exit_unknown(void)
+{
+ asm volatile (
+ "r0 = 0;"
+ ".8byte %[bad];"
+ :
+ : __imm_insn(bad, BPF_RAW_INSN(BPF_JMP | BPF_EXIT, 0, BPF_UNWIND_EXIT + 1, 0, 0))
+ : __clobber_all
+ );
+}
+
+/* r1 is the index: unwinds in the second iteration */
+__naked __noinline __used
+static unsigned long cb_unwinds(void)
+{
+ asm volatile (
+ "r0 = 0;"
+ "if r1 != 0 goto 1f;"
+ "exit;"
+ "1:"
+ unwind_exit
+ :
+ : __imm_unwind_exit
+ );
+}
+
+SEC("tc")
+__description("unwind: callback unwinds")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__failure __msg("callback cannot unwind")
+__naked int callback_unwinds(void)
+{
+ asm volatile (
+ "r1 = 3;"
+ "r2 = %[cb_unwinds];"
+ "r3 = 0;"
+ "r4 = 0;"
+ "call %[bpf_loop];"
+ "exit;"
+ :
+ : __imm_ptr(cb_unwinds),
+ __imm(bpf_loop)
+ : __clobber_common
+ );
+}
+
+/* What it calls can unwind as long as it doesn't let it out */
+__naked __noinline __used
+static unsigned long cb_catches(void)
+{
+ asm volatile (
+ "call may_unwind;"
+ unwind_goto(0)
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm_unwind_goto(0)
+ );
+}
+
+SEC("tc")
+__description("unwind: callback catches")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(3)
+__naked int callback_catches(void)
+{
+ asm volatile (
+ "r1 = 3;"
+ "r2 = %[cb_catches];"
+ "r3 = 0;"
+ "r4 = 0;"
+ "call %[bpf_loop];"
+ "exit;"
+ :
+ : __imm_ptr(cb_catches),
+ __imm(bpf_loop)
+ : __clobber_common
+ );
+}
+
+/* Both leave the subprog like exit does */
+__naked __noinline __used
+static unsigned long does_tail_call(void)
+{
+ asm volatile (
+ "r6 = *(u32 *)(r1 + %[len]);"
+ "r2 = %[prog_array] ll;"
+ "r3 = 0;"
+ "call %[bpf_tail_call];"
+ "r0 = 7;"
+ "if r6 == 64 goto 1f;"
+ unwind_exit
+ "1:"
+ "exit;"
+ :
+ : __imm_const(len, offsetof(struct __sk_buff, len)),
+ __imm(bpf_tail_call),
+ __imm_addr(prog_array),
+ __imm_unwind_exit
+ );
+}
+
+SEC("tc")
+__description("unwind: tail call in a subprog that can unwind")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(7)
+__naked int tail_call_in_subprog(void)
+{
+ asm volatile (
+ "call does_tail_call;"
+ unwind_goto(1)
+ "exit;"
+ "r0 = 42;"
+ "exit;"
+ :
+ : __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+/* The packet is shorter than that: ld_abs leaves the subprog with r0 = 0 */
+__naked __noinline __used
+static unsigned long does_ld_abs(void)
+{
+ asm volatile (
+ "r6 = r1;"
+ "r0 = *(u8 *)skb[1000];"
+ "r0 = 5;"
+ unwind_exit
+ :
+ : __imm_unwind_exit
+ );
+}
+
+SEC("tc")
+__description("unwind: ld_abs in a subprog that can unwind")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(7)
+__naked int ld_abs_in_subprog(void)
+{
+ asm volatile (
+ "call does_ld_abs;"
+ unwind_goto(2)
+ "r0 += 7;"
+ "exit;"
+ "r0 = 42;"
+ "exit;"
+ :
+ : __imm_unwind_goto(2)
+ : __clobber_all
+ );
+}
+
+/*
+ * JITs don't emit anything for unwind_goto. It's an extable entry, where
+ * unwind_exit looks its return address up.
+ */
+SEC("tc")
+__description("unwind: jit")
+__success __retval(42)
+__load_if_JITed()
+__arch_x86_64
+__jited(" callq 0x{{.*}}")
+__jited(" movl $0x7, %eax")
+__jited("...")
+__jited(" movl $0x5, %eax")
+__jited(" pushq %rax")
+__jited(" pushq %rsi")
+__jited(" movq 0x8(%rbp), %rdi")
+__jited(" callq 0x{{.*}}")
+__jited(" movq %rax, 0x8(%rbp)")
+__jited(" popq %rsi")
+__jited(" popq %rax")
+__arch_arm64
+__jited(" bl {{.*}}")
+__jited(" mov x8, x0")
+__jited(" mov x8, #0x7")
+__jited("...")
+__jited(" mov x8, #0x5")
+__jited(" stp x8, x1, [sp, #-0x10]!")
+__jited(" ldr x17, [x29, #0x8]")
+__jited("...")
+__jited(" str x17, [x29, #0x8]")
+__jited(" ldp x8, x1, [sp], #0x10")
+__naked int jit(void)
+{
+ asm volatile (
+ "r1 = *(u32 *)(r1 + %[len]);"
+ "call may_unwind;"
+ unwind_goto(2)
+ "r0 = 7;"
+ "exit;"
+ "r0 = 42;"
+ "exit;"
+ :
+ : __imm_const(len, offsetof(struct __sk_buff, len)),
+ __imm_unwind_goto(2)
+ : __clobber_all
+ );
+}
+
+/* r1 == 0: returns 7, otherwise unwinds with r0 = 5 */
+__naked __noinline __used
+static unsigned long may_unwind_big_stack(void)
+{
+ asm volatile (
+ "*(u64 *)(r10 - 200) = r1;"
+ "if r1 != 0 goto 1f;"
+ "r0 = 7;"
+ "exit;"
+ "1:"
+ "r0 = 5;"
+ unwind_exit
+ :
+ : __imm_unwind_exit
+ );
+}
+
+/*
+ * 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)
+{
+ asm volatile (
+ "*(u64 *)(r10 - 200) = 42;"
+ "r1 = %[one] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+ "call may_unwind_big_stack;"
+ unwind_goto(1)
+ "exit;"
+ "r1 = *(u64 *)(r10 - 200);"
+ "r0 += r1;"
+ "exit;"
+ :
+ : __imm_addr(one),
+ __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
+SEC("raw_tp")
+__description("unwind: private stack, the callee returns")
+__success __retval(49)
+__load_if_JITed()
+__arch_x86_64
+__arch_arm64
+__naked int private_stack_returns(void)
+{
+ asm volatile (
+ "*(u64 *)(r10 - 200) = 42;"
+ "r1 = %[zero] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+ "call may_unwind_big_stack;"
+ unwind_goto(3)
+ "r1 = *(u64 *)(r10 - 200);"
+ "r0 += r1;"
+ "exit;"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm_addr(zero),
+ __imm_unwind_goto(3)
+ : __clobber_all
+ );
+}
+
+/*
+ * BTF FUNC records are not generated for kfuncs referenced
+ * from inline assembly. These records are necessary for
+ * libbpf to link the program. The function below is a hack
+ * to ensure that BTF FUNC records are generated.
+ */
+void __kfunc_btf_root(void)
+{
+ bpf_rcu_read_lock();
+ bpf_rcu_read_unlock();
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/test_loader.c b/tools/testing/selftests/bpf/test_loader.c
index 18d47e925400..824e5912ebcb 100644
--- a/tools/testing/selftests/bpf/test_loader.c
+++ b/tools/testing/selftests/bpf/test_loader.c
@@ -1546,7 +1546,8 @@ void run_subtest(struct test_loader *tester,
}
err = do_prog_test_run(bpf_program__fd(tprog), &retval,
- bpf_program__type(tprog) == BPF_PROG_TYPE_SYSCALL ? true : false,
+ bpf_program__type(tprog) == BPF_PROG_TYPE_SYSCALL ||
+ bpf_program__type(tprog) == BPF_PROG_TYPE_RAW_TRACEPOINT,
spec->linear_sz);
if (!err && retval != subspec->retval && subspec->retval != POINTER_VALUE) {
PRINT_FAIL("Unexpected retval: %d != %d\n", retval, subspec->retval);
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 bpf-next 5/5] selftests/bpf: Add a C++ prog with exceptions
2026-10-19 5:18 [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit Alexei Starovoitov
` (3 preceding siblings ...)
2026-10-19 5:18 ` [PATCH v2 bpf-next 4/5] selftests/bpf: Add tests for " Alexei Starovoitov
@ 2026-10-19 5:18 ` Alexei Starovoitov
2026-10-19 20:32 ` [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit Eduard Zingerman
5 siblings, 0 replies; 11+ messages in thread
From: Alexei Starovoitov @ 2026-10-19 5:18 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: 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] https://github.com/llvm/llvm-project/pull/233140
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
tools/testing/selftests/bpf/Makefile.skel | 11 ++
.../selftests/bpf/prog_tests/unwind_cxx.c | 76 ++++++++++++
.../selftests/bpf/progs/unwind_cxx.cpp | 115 ++++++++++++++++++
3 files changed, 202 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/unwind_cxx.c
create mode 100644 tools/testing/selftests/bpf/progs/unwind_cxx.cpp
diff --git a/tools/testing/selftests/bpf/Makefile.skel b/tools/testing/selftests/bpf/Makefile.skel
index 06e297a17156..6570c48f92b0 100644
--- a/tools/testing/selftests/bpf/Makefile.skel
+++ b/tools/testing/selftests/bpf/Makefile.skel
@@ -153,4 +153,15 @@ $(RUST_OBJS): $(RDIR)/%.bpf.o: progs/%.rs $(RUST_CORE) | $(RDIR)
endif
endif
+# Programs in C++. No skeletons: gcc doesn't build them.
+ifeq ($(BPF_CC),$(CLANG))
+CXX_OBJS := $(patsubst progs/%.cpp,$(RDIR)/%.bpf.o,$(wildcard progs/*.cpp))
+BPF_OBJS += $(CXX_OBJS)
+
+$(CXX_OBJS): $(RDIR)/%.bpf.o: progs/%.cpp | $(RDIR)
+ $(call msg,$(BPF_CC_MSG),$(BINARY),$@)
+ $(Q)$(BPF_CC) -x c++ -g -Wall -Werror -fexceptions -funwind-tables -fno-rtti \
+ $(BPF_CC_FLAGS) -c $< -o $@ $(call skip_on_fail,BPF)
+endif
+
endif # BPF_CC
diff --git a/tools/testing/selftests/bpf/prog_tests/unwind_cxx.c b/tools/testing/selftests/bpf/prog_tests/unwind_cxx.c
new file mode 100644
index 000000000000..36e52766022c
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/unwind_cxx.c
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <test_progs.h>
+#include <bpf/btf.h>
+
+#ifndef ENOTSUPP
+#define ENOTSUPP 524
+#endif
+
+/* The progs are under #ifdef __BPF_FEATURE_UNWIND */
+static bool llvm_lowers_unwinding(const char *file)
+{
+ struct btf *btf = btf__parse(file, NULL);
+ bool ret;
+
+ ret = btf && btf__find_by_name_kind(btf, "catches", BTF_KIND_FUNC) > 0;
+ btf__free(btf);
+ return ret;
+}
+
+static void run(struct bpf_object *obj, const char *name, int len, int retval, int dtors)
+{
+ struct bpf_program *prog = bpf_object__find_program_by_name(obj, name);
+ struct bpf_map *map = bpf_object__find_map_by_name(obj, ".data.dtors");
+ char data[256] = {};
+ LIBBPF_OPTS(bpf_test_run_opts, opts,
+ .data_in = data,
+ .data_size_in = len,
+ .repeat = 1,
+ );
+ int key = 0, val = 0;
+
+ if (!ASSERT_OK_PTR(prog, name) || !ASSERT_OK_PTR(map, "dtors"))
+ return;
+ ASSERT_OK(bpf_map__update_elem(map, &key, sizeof(key), &val, sizeof(val), 0), "clear");
+ ASSERT_OK(bpf_prog_test_run_opts(bpf_program__fd(prog), &opts), "test_run");
+ ASSERT_EQ(opts.retval, retval, "retval");
+ ASSERT_OK(bpf_map__lookup_elem(map, &key, sizeof(key), &val, sizeof(val), 0), "lookup");
+ ASSERT_EQ(val, dtors, "dtors");
+}
+
+void test_unwind_cxx(void)
+{
+ const char *file = "./unwind_cxx.bpf.o";
+ struct bpf_object *obj;
+ int err;
+
+ if (access(file, R_OK)) {
+ printf("%s:SKIP:gcc doesn't build %s\n", __func__, file);
+ test__skip();
+ return;
+ }
+ if (!llvm_lowers_unwinding(file)) {
+ printf("%s:SKIP:llvm doesn't lower unwinding\n", __func__);
+ test__skip();
+ return;
+ }
+ obj = bpf_object__open_file(file, NULL);
+ if (!ASSERT_OK_PTR(obj, "open"))
+ return;
+ err = bpf_object__load(obj);
+ if (err == -ENOTSUPP) {
+ printf("%s:SKIP:JIT does not support unwinding\n", __func__);
+ test__skip();
+ goto out;
+ }
+ if (!ASSERT_OK(err, "load"))
+ goto out;
+
+ /* inner's destructor, then middle's */
+ run(obj, "catches", 64, 65, 12);
+ run(obj, "catches", 200, 42, 12);
+ run(obj, "does_not_catch", 64, 65, 12);
+ run(obj, "does_not_catch", 200, 0, 12);
+out:
+ bpf_object__close(obj);
+}
diff --git a/tools/testing/selftests/bpf/progs/unwind_cxx.cpp b/tools/testing/selftests/bpf/progs/unwind_cxx.cpp
new file mode 100644
index 000000000000..d46ee41fef85
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/unwind_cxx.cpp
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * C++ exceptions. llvm lowers unwinding to two insns:
+ * invoke foo unwind pad call foo; unwind_goto pad
+ * resume r0 = 0; unwind_exit
+ * so 'throw' is a jump or an exit, 'catch (...)' and the destructors that run on
+ * the way are ordinary blocks.
+ *
+ * vmlinux.h and bpf_helpers.h are not C++.
+ */
+#define SEC(name) __attribute__((section(name), used))
+#define __ksym __attribute__((section(".ksyms")))
+#define __noinline __attribute__((noinline))
+#define __always_inline inline __attribute__((always_inline))
+
+#ifdef __BPF_FEATURE_UNWIND
+
+struct __sk_buff {
+ unsigned int len;
+};
+
+extern "C" {
+void bpf_rcu_read_lock(void) noexcept __ksym;
+void bpf_rcu_read_unlock(void) noexcept __ksym;
+void bpf_preempt_disable(void) noexcept __ksym;
+void bpf_preempt_enable(void) noexcept __ksym;
+
+/* Starts an unwind. Not a kfunc: llvm lowers the call and it doesn't come back. */
+int _Unwind_RaiseException(void *exc);
+
+/* All that is left of the C++ runtime. Nobody looks at the exception object. */
+static char exc_obj[16];
+
+__always_inline void *__cxa_allocate_exception(unsigned long size) noexcept
+{
+ return exc_obj;
+}
+
+__always_inline void __cxa_throw(void *exc, void *tinfo, void (*dtor)(void *))
+{
+ _Unwind_RaiseException(exc);
+}
+
+__always_inline void *__cxa_begin_catch(void *exc) noexcept
+{
+ return exc;
+}
+
+__always_inline void __cxa_end_catch(void) noexcept
+{
+}
+}
+
+/* One decimal digit per destructor, in the order they ran */
+volatile int dtors SEC(".data.dtors");
+
+/* The verifier insists that both are undone on every path out of the prog */
+struct rcu_guard {
+ __always_inline rcu_guard()
+ {
+ bpf_rcu_read_lock();
+ }
+ __always_inline ~rcu_guard()
+ {
+ bpf_rcu_read_unlock();
+ dtors = dtors * 10 + 1;
+ }
+};
+
+struct preempt_guard {
+ __always_inline preempt_guard()
+ {
+ bpf_preempt_disable();
+ }
+ __always_inline ~preempt_guard()
+ {
+ bpf_preempt_enable();
+ dtors = dtors * 10 + 2;
+ }
+};
+
+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;
+}
+
+extern "C" SEC("tc") int catches(struct __sk_buff *skb)
+{
+ try {
+ return middle(skb->len);
+ } catch (...) {
+ return 42;
+ }
+}
+
+/* The exception leaves the prog: it returns 0 */
+extern "C" SEC("tc") int does_not_catch(struct __sk_buff *skb)
+{
+ return middle(skb->len);
+}
+
+#endif
+
+char _license[] SEC("license") = "GPL";
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v2 bpf-next 1/5] bpf: Add unwind_goto and unwind_exit insns
2026-10-19 5:18 ` [PATCH v2 bpf-next 1/5] bpf: Add unwind_goto and unwind_exit insns Alexei Starovoitov
@ 2026-10-19 5:45 ` sashiko-bot
2026-10-19 6:59 ` bot+bpf-ci
1 sibling, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-19 5:45 UTC (permalink / raw)
To: Alexei Starovoitov, bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] bpf: build failure in bpf_unwind_target() on MIPS due to missing fixup field
--
commit c2ccff0580e3d9f39dd46bc86407477749d28180
Author: Alexei Starovoitov <ast@kernel.org>
bpf: Add unwind_goto and unwind_exit insns
This patch introduces unwind_goto and unwind_exit BPF instructions to
support frame unwinding for Rust Drop, C++ destructors, and C cleanup
attributes. The verifier validates landing pads and unwind paths, while
JITs use exception table entries and bpf_unwind_target() to redirect return
addresses on unwind_exit.
> diff --git a/kernel/bpf/core.c b/kernel/bpf/core.c
> index 6e32db9777c2a..da3691b47cc72 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;
> +}
[Severity: Low]
Does bpf_unwind_target() cause a build regression on MIPS when
CONFIG_BPF_JIT is enabled?
In kernel/bpf/core.c, bpf_unwind_target() is compiled whenever
CONFIG_BPF_JIT is enabled and accesses e->fixup directly.
MIPS selects HAVE_EBPF_JIT (when !CPU_MICROMIPS in arch/mips/Kconfig),
allowing CONFIG_BPF_JIT to be enabled, but arch/mips/include/asm/extable.h
defines struct exception_table_entry with nextinsn instead of fixup:
struct exception_table_entry
{
unsigned long insn;
unsigned long nextinsn;
};
None of the subsequent patches in the series guard bpf_unwind_target() or
provide an architecture-specific accessor for this field.
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261019051846.3012089-1-alexei.starovoitov@gmail.com?part=1
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v2 bpf-next 4/5] selftests/bpf: Add tests for unwind_goto and unwind_exit
2026-10-19 5:18 ` [PATCH v2 bpf-next 4/5] selftests/bpf: Add tests for " Alexei Starovoitov
@ 2026-10-19 6:00 ` bot+bpf-ci
0 siblings, 0 replies; 11+ messages in thread
From: bot+bpf-ci @ 2026-10-19 6:00 UTC (permalink / raw)
To: alexei.starovoitov, bpf
Cc: daniel, andrii, eddyz87, memxor, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 7336 bytes --]
> 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
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/39205301464
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v2 bpf-next 1/5] bpf: Add unwind_goto and unwind_exit insns
2026-10-19 5:18 ` [PATCH v2 bpf-next 1/5] bpf: Add unwind_goto and unwind_exit insns Alexei Starovoitov
2026-10-19 5:45 ` sashiko-bot
@ 2026-10-19 6:59 ` bot+bpf-ci
1 sibling, 0 replies; 11+ messages in thread
From: bot+bpf-ci @ 2026-10-19 6:59 UTC (permalink / raw)
To: alexei.starovoitov, bpf
Cc: daniel, andrii, eddyz87, memxor, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 5327 bytes --]
> 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
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/39205301464
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit
2026-10-19 5:18 [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit Alexei Starovoitov
` (4 preceding siblings ...)
2026-10-19 5:18 ` [PATCH v2 bpf-next 5/5] selftests/bpf: Add a C++ prog with exceptions Alexei Starovoitov
@ 2026-10-19 20:32 ` Eduard Zingerman
2026-10-20 0:35 ` Alexei Starovoitov
5 siblings, 1 reply; 11+ messages in thread
From: Eduard Zingerman @ 2026-10-19 20:32 UTC (permalink / raw)
To: Alexei Starovoitov, bpf; +Cc: daniel, andrii, memxor
On Mon, 2026-10-19 at 05:18 +0000, Alexei Starovoitov wrote:
...
> 3. Is there a simpler way?
>
> [3] keeps the normal path free of unwind code. llvm emits a table, libbpf
> passes it to the kernel, bpf_unwind() walks the stack and rewrites return
> addresses, and the verifier models hidden edges from calls to pads. That's
> 1400 lines in the kernel, with new uapi and two kfuncs, and 440 lines in
> libbpf.
>
> Here the edge from a call to its pad is an insn, and so is the other way
> to leave a frame [4]:
>
> ret exit
> invoke foo unwind pad call foo; unwind_goto pad
> resume r0 = 0; unwind_exit
>
> Pads are ordinary code. The verifier learns one bit: how the callee left.
> The normal path is free of unwind code too: JITs emit nothing for
> unwind_goto, and exit is what it was.
> 170 lines in patch 1 and 80 lines in each JIT.
As far as I understand, the main difference from what was discussed in [1]
is that for nested calls of functions that can unwind, each function
in a chain must be called as call/unwind_goto pair, right?
LLVM IR does not have such limitation: `call` and `invoke` calls can
be mixed in a chain. What the limitation buys is an ability to skip
the loop like [2] in the verifier.
Is it planned to add an LLVM pass that converts all `call`s to
`invoke`s for unwinding functions?
[1] https://lore.kernel.org/bpf/a9c4ebe0c1a08d7025179b1ec2297a4d975cd072.camel@gmail.com/
[2] https://github.com/kernel-patches/bpf/blob/fbc89e90cacd207b4271da25ed7a380f6b56125e/kernel/bpf/verifier.c#L18564
---
Aside from that, one minor thing caught my attention (hate freplace programs).
Suppose there is a main program:
caller:
call foo
unwind_goto pad
... normal continuation ...
pad:
... unwind continuation ...
foo: // global
r0 = 0
unwind_exit
And then there is an freplace program:
foo-replacement:
r0 = 0
unwind_exit
If I read the series correctly before the replacement runtime control
flow would follow `unwind_goto pad`, but after the replacement it
would follow the normal continuation flow.
Is it intentional? Seem to be a surprising behavior.
Commit message says:
> So is freplace: an extension leaves with exit and
> the callers are ready for that.
But I'm not sure I understand that.
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit
2026-10-19 20:32 ` [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit Eduard Zingerman
@ 2026-10-20 0:35 ` Alexei Starovoitov
0 siblings, 0 replies; 11+ messages in thread
From: Alexei Starovoitov @ 2026-10-20 0:35 UTC (permalink / raw)
To: Eduard Zingerman, bpf; +Cc: daniel, andrii, memxor
On Mon, Oct 19, 2026 at 01:32 PM Eduard Zingerman <eddyz87@gmail.com> wrote:
> As far as I understand, the main difference from what was discussed in [1]
> is that for nested calls of functions that can unwind, each function
> in a chain must be called as call/unwind_goto pair, right?
Right. Otherwise prepare_func_exit() rejects unwind_exit of the callee.
> LLVM IR does not have such limitation: `call` and `invoke` calls can
> be mixed in a chain. What the limitation buys is an ability to skip
> the loop like [2] in the verifier.
It buys more than that. Every frame returns through its own epilogue,
so r6-r9 are restored by the frame that saved them, and there is no
stack walk at run time.
> Is it planned to add an LLVM pass that converts all `call`s to
> `invoke`s for unwinding functions?
No need for a new pass. The llvm PR does it already: a plain call that
can unwind gets unwind_goto to a pad with 'r0 = 0; unwind_exit'.
does_not_catch() in patch 5 is such a case.
> If I read the series correctly before the replacement runtime control
> flow would follow `unwind_goto pad`, but after the replacement it
> would follow the normal continuation flow.
> Is it intentional? Seem to be a surprising behavior.
Yes, you read it right. An extension is a main prog of its own and
unwind_exit of the main prog is exit: it's frame 0 in the verifier and
bpf_is_subprog() is false in JITs. So the exception stops at the
extension, and the caller continues as if foo returned r0.
> Commit message says:
>
> > So is freplace: an extension leaves with exit and
> > the callers are ready for that.
>
> But I'm not sure I understand that.
foo can unwind, so check_func_call() made the verifier explore both
ways out of the call. The normal continuation is checked with an
unknown r0, so it's safe whatever the extension does.
Works out of the box, but maybe it's easier to disable freplace for now.
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-10-20 0:35 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-19 5:18 [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit Alexei Starovoitov
2026-10-19 5:18 ` [PATCH v2 bpf-next 1/5] bpf: Add unwind_goto and unwind_exit insns Alexei Starovoitov
2026-10-19 5:45 ` sashiko-bot
2026-10-19 6:59 ` bot+bpf-ci
2026-10-19 5:18 ` [PATCH v2 bpf-next 2/5] bpf, x86: Support unwind_goto and unwind_exit Alexei Starovoitov
2026-10-19 5:18 ` [PATCH v2 bpf-next 3/5] bpf, arm64: " Alexei Starovoitov
2026-10-19 5:18 ` [PATCH v2 bpf-next 4/5] selftests/bpf: Add tests for " Alexei Starovoitov
2026-10-19 6:00 ` bot+bpf-ci
2026-10-19 5:18 ` [PATCH v2 bpf-next 5/5] selftests/bpf: Add a C++ prog with exceptions Alexei Starovoitov
2026-10-19 20:32 ` [PATCH v2 bpf-next 0/5] bpf: Unwinding with unwind_goto and unwind_exit Eduard Zingerman
2026-10-20 0:35 ` Alexei Starovoitov
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox