aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexei Starovoitov <ast@kernel.org>2026-10-20 23:19:41 +0000
committerAlexei Starovoitov <ast@kernel.org>2026-10-21 17:50:51 +0000
commit4c2cdb6156920b5fb578779b5da62edf3fc45556 (patch)
treee980ae8e914ececac5faa2f21e674524fbb9e357
parent9b27cd0554c5883318256c32b2aaf060a3e20e0a (diff)
selftests/bpf: Test unwind_exit of an exception callback
In both tests the prog that throws returns to a call that unwind_goto follows, and its exception callback ends with unwind_exit: - An extension replaces a subprog that cannot unwind. unwind_goto after the call stays, since it shares the pad with a call of a subprog that can unwind. - A subprog that can unwind does a tail call. unwind_exit of the callback has to be exit. Without the fix the callers go to the pad and return 2 instead of 3 and 42 instead of 7. Signed-off-by: Alexei Starovoitov <ast@kernel.org> Link: https://patch.msgid.link/20261020231942.2568163-4-alexei.starovoitov@gmail.com
-rw-r--r--tools/testing/selftests/bpf/prog_tests/unwind_attach.c22
-rw-r--r--tools/testing/selftests/bpf/progs/unwind_attach.c29
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_unwind.c29
3 files changed, 75 insertions, 5 deletions
diff --git a/tools/testing/selftests/bpf/prog_tests/unwind_attach.c b/tools/testing/selftests/bpf/prog_tests/unwind_attach.c
index ada8b3273ccb..37e03b3e0ee8 100644
--- a/tools/testing/selftests/bpf/prog_tests/unwind_attach.c
+++ b/tools/testing/selftests/bpf/prog_tests/unwind_attach.c
@@ -67,7 +67,7 @@ void test_unwind_attach(void)
{
struct verifier_unwind *tgt;
struct bpf_program *prog;
- int tgt_fd, plain_fd, err;
+ int tgt_fd, plain_fd, shared_fd, tail_fd, throws_fd, err, key = 0;
if (!is_jit_enabled()) {
printf("%s:SKIP:JIT is disabled\n", __func__);
@@ -79,7 +79,10 @@ void test_unwind_attach(void)
return;
bpf_object__for_each_program(prog, tgt->obj)
bpf_program__set_autoload(prog, prog == tgt->progs.calls_global_unwinds ||
- prog == tgt->progs.calls_global_never_unwinds);
+ prog == tgt->progs.calls_global_never_unwinds ||
+ prog == tgt->progs.shared_pad ||
+ prog == tgt->progs.tail_call_in_subprog ||
+ prog == tgt->progs.exception_callback_unwinds);
err = verifier_unwind__load(tgt);
if (err == -ENOTSUPP) {
printf("%s:SKIP:JIT does not support unwinding\n", __func__);
@@ -107,6 +110,21 @@ void test_unwind_attach(void)
attach(plain_fd, "global_never_unwinds", "freplace_unwinds",
"Extension program can unwind, while original does not", 0);
ASSERT_EQ(run(tgt_fd), 15, "retval");
+
+ /*
+ * An exception callback returns from the frame of the main prog. Here
+ * that's a return to a call that unwind_goto follows. unwind_exit of
+ * the callback has to be exit: the pad wasn't verified with it.
+ */
+ shared_fd = bpf_program__fd(tgt->progs.shared_pad);
+ attach(shared_fd, "global_never_unwinds", "freplace_throws", NULL, 3);
+
+ tail_fd = bpf_program__fd(tgt->progs.tail_call_in_subprog);
+ throws_fd = bpf_program__fd(tgt->progs.exception_callback_unwinds);
+ err = bpf_map__update_elem(tgt->maps.prog_array, &key, sizeof(key),
+ &throws_fd, sizeof(throws_fd), 0);
+ if (ASSERT_OK(err, "prog_array"))
+ ASSERT_EQ(run(tail_fd), 7, "retval");
out:
verifier_unwind__destroy(tgt);
}
diff --git a/tools/testing/selftests/bpf/progs/unwind_attach.c b/tools/testing/selftests/bpf/progs/unwind_attach.c
index df73c26c1ff4..9aff7c6bda23 100644
--- a/tools/testing/selftests/bpf/progs/unwind_attach.c
+++ b/tools/testing/selftests/bpf/progs/unwind_attach.c
@@ -7,6 +7,13 @@
/* The targets are global_unwinds() and global_never_unwinds() in verifier_unwind.c */
+#define unwind_exit ".8byte %[insn_unwind_exit];"
+#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_throw(__u64 cookie) __ksym;
+
int fentry_cnt;
SEC("?fentry/global_unwinds")
@@ -33,11 +40,27 @@ __naked unsigned long freplace_unwinds(void)
{
asm volatile (
"r0 = 3;"
- ".8byte %[unwind_exit];"
+ unwind_exit
:
- : __imm_insn(unwind_exit,
- BPF_RAW_INSN(BPF_JMP | BPF_EXIT, 0, BPF_UNWIND_EXIT, 0, 0))
+ : __imm_unwind_exit
);
}
+/* Not naked: the argument has to have a name in BTF */
+__noinline int throws_cb(__u64 cookie)
+{
+ if (cookie)
+ asm volatile ("r0 = 3;" unwind_exit :: __imm_unwind_exit : "r0");
+ return 0;
+}
+
+/* It has no unwind_exit, so it can replace a subprog that cannot unwind */
+SEC("?freplace/global_never_unwinds")
+__attribute__((btf_decl_tag("exception_callback:throws_cb")))
+unsigned long freplace_throws(void)
+{
+ bpf_throw(1);
+ return 0;
+}
+
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
index 0a75cb8af67f..5a21345c34d4 100644
--- a/tools/testing/selftests/bpf/progs/verifier_unwind.c
+++ b/tools/testing/selftests/bpf/progs/verifier_unwind.c
@@ -611,6 +611,35 @@ __naked int calls_global_never_unwinds(void)
);
}
+/*
+ * The verifier gets to the pad from the first call only, with r6 = 1. The second
+ * unwind_goto stays, since the pad is not dead code.
+ */
+SEC("tc")
+__description("unwind: a pad shared with a call of a subprog that cannot unwind")
+__arch_x86_64
+__arch_arm64
+__load_if_JITed()
+__success __retval(7)
+__naked int shared_pad(void)
+{
+ asm volatile (
+ "r6 = 1;"
+ "call global_returns;"
+ unwind_goto(4)
+ "r6 = 2;"
+ "call global_never_unwinds;"
+ unwind_goto(1)
+ "exit;"
+ "r0 = r6;"
+ "exit;"
+ :
+ : __imm_unwind_goto(4),
+ __imm_unwind_goto(1)
+ : __clobber_all
+ );
+}
+
SEC("tc")
__description("unwind: no unwind_goto after a call of a global subprog")
__arch_x86_64