From: Yonghong Song <yonghong.song@linux.dev>
To: bpf@vger.kernel.org
Cc: Alexei Starovoitov <ast@kernel.org>,
Andrii Nakryiko <andrii@kernel.org>,
Daniel Borkmann <daniel@iogearbox.net>,
Eduard Zingerman <eddyz87@gmail.com>,
kernel-team@fb.com
Subject: [PATCH bpf-next v2 19/20] selftests/bpf: Cover the exception cleanup shapes the chain does not reach
Date: Thu, 17 Sep 2026 21:43:37 -0700 [thread overview]
Message-ID: <20260918044337.3293731-1-yonghong.song@linux.dev> (raw)
In-Reply-To: <20260918044156.3283973-1-yonghong.song@linux.dev>
The end-to-end test walks one call chain with a pad in most of its frames.
This adds the shapes that chain does not reach: a callee called from both a
covered and an uncovered site, a pad that reads its frame's callee-saved
registers, a tail-call-reachable callee, a tail call that is taken, an
extension standing in for a covered call, a pad in the main program's own
frame, a record covering bpf_throw() itself, a record covering only a
nounwind call, a throwing subprogram named by a BPF_PSEUDO_FUNC, a region
ending on a 16-byte instruction, a pad that reloads from and writes to its
own frame, a pad two frames up, a pad that calls a subprogram which tail
calls, an extension carrying a table of its own, and a pad terminated by
_Unwind_Resume rather than bpf_unwind_resume.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
.../selftests/bpf/exceptions_cleanup.h | 20 +
.../bpf/prog_tests/exceptions_cleanup.c | 314 +++++++
.../bpf/progs/exceptions_cleanup_ext_table.c | 48 +
.../bpf/progs/exceptions_cleanup_freplace.c | 17 +
.../progs/exceptions_cleanup_pad_freplace.c | 17 +
.../bpf/progs/exceptions_cleanup_shapes.c | 863 ++++++++++++++++++
6 files changed, 1279 insertions(+)
create mode 100644 tools/testing/selftests/bpf/progs/exceptions_cleanup_ext_table.c
create mode 100644 tools/testing/selftests/bpf/progs/exceptions_cleanup_freplace.c
create mode 100644 tools/testing/selftests/bpf/progs/exceptions_cleanup_pad_freplace.c
create mode 100644 tools/testing/selftests/bpf/progs/exceptions_cleanup_shapes.c
diff --git a/tools/testing/selftests/bpf/exceptions_cleanup.h b/tools/testing/selftests/bpf/exceptions_cleanup.h
index 630d2e207119..5896cf83d15e 100644
--- a/tools/testing/selftests/bpf/exceptions_cleanup.h
+++ b/tools/testing/selftests/bpf/exceptions_cleanup.h
@@ -4,6 +4,7 @@
#define __EXCEPTIONS_CLEANUP_H__
#define THROW_COOKIE 0x100
+#define INNER_COOKIE 0x200
/* progs/exceptions_cleanup.c: one bit per frame that reports it ran. */
#define RAN_FOO3_PREEMPT 0x1
@@ -12,6 +13,25 @@
#define RAN_FOO2_DROP 0x8
#define RAN_BUMP 0x10
+/* progs/exceptions_cleanup_shapes.c: one bit per shape, numbered its own way. */
+#define RAN_SWEEP 0x1
+#define RAN_SHARED 0x2
+#define RAN_REGS 0x4
+#define RAN_TAIL_CALL 0x8
+#define RAN_MAIN_PAD 0x10
+#define RAN_TC_TAKEN 0x20
+#define RAN_FREPLACE 0x40
+#define RAN_ADDR_TAKEN 0x80
+#define RAN_NO_SUBPROG 0x100
+#define RAN_PAD_CALLS 0x200
+#define RAN_PAD_FIRST 0x400
+#define RAN_WIDE_REC 0x800
+#define RAN_PAD_STACK 0x1000
+#define RAN_DEEP_PAD 0x2000
+#define RAN_NOUNWIND_REC 0x4000
+#define RAN_RESUME_ALIAS 0x8000
+#define RAN_PAD_TAIL_CALL 0x10000
+
#define CLEANUP_REC(begin, end, landing_pad) \
".pushsection .bpf_cleanup,\"a\",@progbits;" \
".long " begin ";" \
diff --git a/tools/testing/selftests/bpf/prog_tests/exceptions_cleanup.c b/tools/testing/selftests/bpf/prog_tests/exceptions_cleanup.c
index d1e45b765af2..ffc0b9519168 100644
--- a/tools/testing/selftests/bpf/prog_tests/exceptions_cleanup.c
+++ b/tools/testing/selftests/bpf/prog_tests/exceptions_cleanup.c
@@ -4,6 +4,10 @@
#include "exceptions_cleanup.h"
#include "exceptions_cleanup.skel.h"
#include "exceptions_cleanup_fail.skel.h"
+#include "exceptions_cleanup_shapes.skel.h"
+#include "exceptions_cleanup_freplace.skel.h"
+#include "exceptions_cleanup_pad_freplace.skel.h"
+#include "exceptions_cleanup_ext_table.skel.h"
/* foo3 threw: every frame that has a pad ran it. */
#define PADS_FOO3_THREW \
@@ -35,6 +39,314 @@ static void run(struct exceptions_cleanup *skel, __u64 input, __u32 retval,
ASSERT_EQ(skel->bss->pads_ran, pads | RAN_BUMP, "pads_ran");
}
+static void run_shape(struct exceptions_cleanup_shapes *skel, struct bpf_program *prog,
+ __u64 input, __u32 retval, __u64 pads)
+{
+ __u64 ctx = 0;
+ int err;
+
+ LIBBPF_OPTS(bpf_test_run_opts, topts,
+ .ctx_in = &ctx,
+ .ctx_size_in = sizeof(ctx),
+ );
+
+ skel->bss->input = input;
+ skel->bss->pads_ran = 0;
+
+ err = bpf_prog_test_run_opts(bpf_program__fd(prog), &topts);
+ if (!ASSERT_OK(err, "run"))
+ return;
+ ASSERT_EQ(topts.retval, retval, "retval");
+ ASSERT_EQ(skel->bss->pads_ran, pads, "pads_ran");
+}
+
+static void test_freplace(struct exceptions_cleanup_shapes *skel)
+{
+ struct exceptions_cleanup_freplace *fr;
+ struct bpf_link *link;
+ int tgt_fd;
+
+ tgt_fd = bpf_program__fd(skel->progs.entry_freplace);
+
+ fr = exceptions_cleanup_freplace__open();
+ if (!ASSERT_OK_PTR(fr, "freplace open"))
+ return;
+
+ if (!ASSERT_OK(bpf_program__set_attach_target(fr->progs.new_fr_callee,
+ tgt_fd, "fr_callee"),
+ "set_attach_target"))
+ goto out;
+ if (!ASSERT_OK(exceptions_cleanup_freplace__load(fr), "freplace load"))
+ goto out;
+
+ link = bpf_program__attach_freplace(fr->progs.new_fr_callee, tgt_fd,
+ "fr_callee");
+ if (!ASSERT_OK_PTR(link, "attach_freplace"))
+ goto out;
+
+ run_shape(skel, skel->progs.entry_freplace, 101, THROW_COOKIE, 0);
+ bpf_link__destroy(link);
+out:
+ exceptions_cleanup_freplace__destroy(fr);
+}
+
+static void test_pad_calls_freplace(struct exceptions_cleanup_shapes *skel)
+{
+ struct exceptions_cleanup_pad_freplace *fr;
+ struct bpf_link *link;
+ __u64 ctx = 0;
+ int tgt_fd, err;
+
+ LIBBPF_OPTS(bpf_test_run_opts, topts,
+ .ctx_in = &ctx,
+ .ctx_size_in = sizeof(ctx),
+ );
+
+ tgt_fd = bpf_program__fd(skel->progs.entry_pad_calls);
+
+ fr = exceptions_cleanup_pad_freplace__open();
+ if (!ASSERT_OK_PTR(fr, "pad freplace open"))
+ return;
+
+ if (!ASSERT_OK(bpf_program__set_attach_target(fr->progs.new_pad_callee,
+ tgt_fd, "pad_callee"),
+ "set_attach_target"))
+ goto out;
+ if (!ASSERT_OK(exceptions_cleanup_pad_freplace__load(fr), "pad freplace load"))
+ goto out;
+
+ link = bpf_program__attach_freplace(fr->progs.new_pad_callee, tgt_fd,
+ "pad_callee");
+ if (!ASSERT_OK_PTR(link, "attach_freplace"))
+ goto out;
+
+ skel->bss->input = 101;
+ skel->bss->pads_ran = 0;
+ skel->bss->pad_runs = 0;
+
+ err = bpf_prog_test_run_opts(tgt_fd, &topts);
+ if (!ASSERT_OK(err, "run"))
+ goto out_link;
+
+ ASSERT_EQ(skel->bss->pad_runs, 1, "pad_runs");
+ ASSERT_EQ(skel->bss->pads_ran, RAN_PAD_CALLS, "pads_ran");
+ ASSERT_EQ(topts.retval, THROW_COOKIE, "retval");
+out_link:
+ bpf_link__destroy(link);
+out:
+ exceptions_cleanup_pad_freplace__destroy(fr);
+}
+
+static void test_ext_table(struct exceptions_cleanup_shapes *skel)
+{
+ struct exceptions_cleanup_ext_table *fr;
+ struct bpf_link *link;
+ int tgt_fd;
+
+ tgt_fd = bpf_program__fd(skel->progs.entry_freplace);
+
+ fr = exceptions_cleanup_ext_table__open();
+ if (!ASSERT_OK_PTR(fr, "ext table open"))
+ return;
+
+ if (!ASSERT_OK(bpf_program__set_attach_target(fr->progs.new_fr_callee,
+ tgt_fd, "fr_callee"),
+ "set_attach_target"))
+ goto out;
+ if (!ASSERT_OK(exceptions_cleanup_ext_table__load(fr), "ext table load"))
+ goto out;
+
+ link = bpf_program__attach_freplace(fr->progs.new_fr_callee, tgt_fd,
+ "fr_callee");
+ if (!ASSERT_OK_PTR(link, "attach_freplace"))
+ goto out;
+
+ fr->bss->ext_pad_ran = 0;
+ run_shape(skel, skel->progs.entry_freplace, 101, THROW_COOKIE, 0);
+ ASSERT_EQ(fr->bss->ext_pad_ran, 1, "ext_pad_ran");
+
+ bpf_link__destroy(link);
+out:
+ exceptions_cleanup_ext_table__destroy(fr);
+}
+
+static void test_shapes(void)
+{
+ struct exceptions_cleanup_shapes *skel;
+
+ skel = exceptions_cleanup_shapes__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "shapes open_and_load"))
+ return;
+
+ /* The frame loads at all only if everything unreachable in it went. */
+ if (test__start_subtest("sweep_no_throw"))
+ run_shape(skel, skel->progs.entry_sweep, 1, 0, 0);
+ if (test__start_subtest("sweep_throw"))
+ run_shape(skel, skel->progs.entry_sweep, 101, THROW_COOKIE, RAN_SWEEP);
+
+ /* The covered call unwinds to the pad; the uncovered one never does. */
+ if (test__start_subtest("shared_callee_no_throw"))
+ run_shape(skel, skel->progs.entry_shared, 1, 2, 0);
+ if (test__start_subtest("shared_callee_throw"))
+ run_shape(skel, skel->progs.entry_shared, 101, THROW_COOKIE, RAN_SHARED);
+
+ /* The pad only sets its bit if it got the frame's own r6-r9 back. */
+ if (test__start_subtest("pad_sees_callee_saved"))
+ run_shape(skel, skel->progs.entry_regs, 101, THROW_COOKIE, RAN_REGS);
+
+ /* Same check, with a tail-call-reachable callee: its spill moves. */
+ if (test__start_subtest("tail_call_no_throw"))
+ run_shape(skel, skel->progs.entry_tail_call, 1, 0, 0);
+ if (test__start_subtest("tail_call_throw"))
+ run_shape(skel, skel->progs.entry_tail_call, 101, THROW_COOKIE,
+ RAN_TAIL_CALL);
+
+ /* A region around a nounwind call: no pad dispatched, still loads. */
+ if (test__start_subtest("nounwind_region"))
+ run_shape(skel, skel->progs.entry_nounwind_rec, 1, 0, 0);
+
+ /* A pad in the main program's own frame, not in a subprogram. */
+ if (test__start_subtest("main_program_pad"))
+ run_shape(skel, skel->progs.entry_main_pad, 101, THROW_COOKIE,
+ RAN_MAIN_PAD);
+
+ /* The same call site either way: the subprogram's throw unwinds into
+ * this frame and runs its pad, an extension's stops at its own boundary.
+ */
+ if (test__start_subtest("freplace_subprog_throws"))
+ run_shape(skel, skel->progs.entry_freplace, 7, THROW_COOKIE,
+ RAN_FREPLACE);
+ if (test__start_subtest("freplace_extension_throws"))
+ test_freplace(skel);
+
+ /* A tail call that is taken: the walk ends at the target, so the cookie
+ * comes back from there and this frame's pad does not run.
+ */
+ if (test__start_subtest("tail_call_taken")) {
+ int key = 0, prog_fd = bpf_program__fd(skel->progs.tc_target);
+
+ if (ASSERT_OK(bpf_map_update_elem(bpf_map__fd(skel->maps.taken_table),
+ &key, &prog_fd, BPF_ANY),
+ "populate taken_table"))
+ run_shape(skel, skel->progs.entry_tail_taken, 101,
+ THROW_COOKIE, 0);
+ }
+
+ /* A throwing subprog named by a BPF_PSEUDO_FUNC no helper is handed: the
+ * callback check has to look at the bpf_loop(), not at the ld_imm64.
+ */
+ if (test__start_subtest("addr_taken_no_throw"))
+ run_shape(skel, skel->progs.entry_addr_taken, 1, 2, 0);
+ if (test__start_subtest("addr_taken_throw"))
+ run_shape(skel, skel->progs.entry_addr_taken, 101, THROW_COOKIE,
+ RAN_ADDR_TAKEN);
+
+ /* A record covering bpf_throw() itself rather than a call to a frame
+ * that throws: raised, caught up with and delivered in one frame.
+ */
+ if (test__start_subtest("no_subprog_no_throw"))
+ run_shape(skel, skel->progs.entry_no_subprog, 1, 0, 0);
+ if (test__start_subtest("no_subprog_throw"))
+ run_shape(skel, skel->progs.entry_no_subprog, 101, THROW_COOKIE,
+ RAN_NO_SUBPROG);
+
+ /* A pad that calls a subprogram; with a throwing extension in its place,
+ * the nested exception has to stop there, not restart this pad.
+ */
+ if (test__start_subtest("pad_calls_subprog")) {
+ skel->bss->pad_runs = 0;
+ run_shape(skel, skel->progs.entry_pad_calls, 101, THROW_COOKIE,
+ RAN_PAD_CALLS);
+ ASSERT_EQ(skel->bss->pad_runs, 1, "pad_runs");
+ }
+ if (test__start_subtest("pad_calls_throwing_extension"))
+ test_pad_calls_freplace(skel);
+
+ /* A covered throw the sweep leaves last, where the default exception
+ * callback is patched in; the pad's bit needs r6-r9 still spilled.
+ */
+ if (test__start_subtest("pad_before_throw"))
+ run_shape(skel, skel->progs.entry_pad_first, 101, THROW_COOKIE,
+ RAN_PAD_FIRST);
+
+ /* A region whose last instruction is a 16-byte one, so that end - 1
+ * names the half of it that is not an instruction.
+ */
+ if (test__start_subtest("region_ends_on_ldimm64"))
+ run_shape(skel, skel->progs.entry_wide_rec, 101, THROW_COOKIE,
+ RAN_WIDE_REC);
+
+ /* A pad that reloads from and writes to its own frame's stack, which a
+ * JIT addressing the frame through the stack pointer gets wrong.
+ */
+ if (test__start_subtest("pad_uses_own_frame"))
+ run_shape(skel, skel->progs.entry_pad_stack, 101, THROW_COOKIE,
+ RAN_PAD_STACK);
+
+ /* The same, with an uncovered frame between the throw and the pad. */
+ if (test__start_subtest("pad_two_frames_up"))
+ run_shape(skel, skel->progs.entry_deep_pad, 101, THROW_COOKIE,
+ RAN_DEEP_PAD);
+
+ /* An extension program with a cleanup table of its own. */
+ if (test__start_subtest("extension_carries_table"))
+ test_ext_table(skel);
+
+ /* A pad terminated by _Unwind_Resume, which libbpf maps onto the kfunc;
+ * every other program here calls bpf_unwind_resume directly.
+ */
+ if (test__start_subtest("resume_alias"))
+ run_shape(skel, skel->progs.entry_resume_alias, 101,
+ THROW_COOKIE, RAN_RESUME_ALIAS);
+
+ /* A pad that calls a subprogram which tail calls, array empty and then
+ * populated: the tail call releases only the callee's own prologue.
+ */
+ if (test__start_subtest("pad_callee_tail_call")) {
+ int key = 0, prog_fd = bpf_program__fd(skel->progs.pad_tc_target);
+
+ skel->bss->pad_tc_target_ran = 0;
+ skel->bss->pad_runs = 0;
+ run_shape(skel, skel->progs.entry_pad_tail_call, 101,
+ THROW_COOKIE, RAN_PAD_TAIL_CALL);
+ ASSERT_EQ(skel->bss->pad_tc_target_ran, 0, "target not run");
+ ASSERT_EQ(skel->bss->pad_runs, 1, "pad_runs");
+
+ if (ASSERT_OK(bpf_map_update_elem(bpf_map__fd(skel->maps.pad_tc_table),
+ &key, &prog_fd, BPF_ANY),
+ "populate pad_tc_table")) {
+ skel->bss->pad_runs = 0;
+ run_shape(skel, skel->progs.entry_pad_tail_call, 101,
+ THROW_COOKIE, RAN_PAD_TAIL_CALL);
+ ASSERT_EQ(skel->bss->pad_tc_target_ran, 1, "target ran");
+ ASSERT_EQ(skel->bss->pad_runs, 1, "pad_runs");
+ }
+ }
+
+ /* The same, into a target that carries a table and throws: that target
+ * is a boundary, so the outer pad runs once, not twice.
+ */
+ if (test__start_subtest("pad_callee_tail_call_throws")) {
+ int key = 0, prog_fd = bpf_program__fd(skel->progs.pad_tc_throw_target);
+
+ if (ASSERT_OK(bpf_map_update_elem(bpf_map__fd(skel->maps.pad_tc_table),
+ &key, &prog_fd, BPF_ANY),
+ "populate pad_tc_table")) {
+ skel->bss->pad_runs = 0;
+ skel->bss->tc_target_pad_runs = 0;
+ run_shape(skel, skel->progs.entry_pad_tail_call, 101,
+ THROW_COOKIE, RAN_PAD_TAIL_CALL);
+ /* The target cleaned up after itself, once. */
+ ASSERT_EQ(skel->bss->tc_target_pad_runs, 1,
+ "tc_target_pad_runs");
+ /* And the outer pad was not started over. */
+ ASSERT_EQ(skel->bss->pad_runs, 1, "pad_runs");
+ }
+ }
+
+ exceptions_cleanup_shapes__destroy(skel);
+}
+
void test_exceptions_cleanup(void)
{
char log[8192] = {};
@@ -75,5 +387,7 @@ void test_exceptions_cleanup(void)
exceptions_cleanup__destroy(skel);
+ test_shapes();
+
RUN_TESTS(exceptions_cleanup_fail);
}
diff --git a/tools/testing/selftests/bpf/progs/exceptions_cleanup_ext_table.c b/tools/testing/selftests/bpf/progs/exceptions_cleanup_ext_table.c
new file mode 100644
index 000000000000..d14db48d6b29
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/exceptions_cleanup_ext_table.c
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include "bpf_misc.h"
+#include "exceptions_cleanup.h"
+
+__u64 ext_pad_ran = 0;
+
+/* Without a 32-bit int in BTF, libbpf's dummy_ksym var gets type id 0. */
+int btf_int_anchor;
+
+static __used __noinline void __kfunc_btf_anchor(void)
+{
+ bpf_throw(0);
+ bpf_preempt_disable();
+ bpf_preempt_enable();
+ bpf_unwind_resume();
+}
+
+static __used __naked __noinline __u64 ext_frame(void)
+{
+ asm volatile (
+ "call bpf_preempt_disable;"
+ "r1 = %[cookie];"
+"1:" "call bpf_throw;" /* cleanup region */
+"2:"
+ "exit;"
+"3:" /* landing pad */
+ "call bpf_preempt_enable;"
+ "r1 = %[ext_pad_ran] ll;"
+ "r2 = 1;"
+ "*(u64 *)(r1 + 0) = r2;"
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [cookie]"i"(THROW_COOKIE), __imm_addr(ext_pad_ran)
+ : __clobber_all);
+}
+
+SEC("freplace/fr_callee")
+__u64 new_fr_callee(__u64 x)
+{
+ return ext_frame();
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/exceptions_cleanup_freplace.c b/tools/testing/selftests/bpf/progs/exceptions_cleanup_freplace.c
new file mode 100644
index 000000000000..afb358fd3d40
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/exceptions_cleanup_freplace.c
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include "exceptions_cleanup.h"
+
+/* Without a 32-bit int in BTF, libbpf's dummy_ksym var gets type id 0. */
+int btf_int_anchor;
+
+SEC("freplace/fr_callee")
+__u64 new_fr_callee(__u64 x)
+{
+ bpf_throw(THROW_COOKIE);
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/exceptions_cleanup_pad_freplace.c b/tools/testing/selftests/bpf/progs/exceptions_cleanup_pad_freplace.c
new file mode 100644
index 000000000000..eabac6baabb7
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/exceptions_cleanup_pad_freplace.c
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include "exceptions_cleanup.h"
+
+/* Without a 32-bit int in BTF, libbpf's dummy_ksym var gets type id 0. */
+int btf_int_anchor;
+
+SEC("freplace/pad_callee")
+__u64 new_pad_callee(__u64 x)
+{
+ bpf_throw(INNER_COOKIE);
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/exceptions_cleanup_shapes.c b/tools/testing/selftests/bpf/progs/exceptions_cleanup_shapes.c
new file mode 100644
index 000000000000..f5eb2ff15c89
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/exceptions_cleanup_shapes.c
@@ -0,0 +1,863 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include "bpf_misc.h"
+#include "exceptions_cleanup.h"
+
+#define PAD_COUNT \
+ "r1 = %[pad_runs] ll;" \
+ "r2 = *(u64 *)(r1 + 0);" \
+ "r2 += 1;" \
+ "*(u64 *)(r1 + 0) = r2;"
+
+static __used __noinline void __kfunc_btf_anchor(void)
+{
+ bpf_throw(0);
+ bpf_rcu_read_lock();
+ bpf_rcu_read_unlock();
+ bpf_preempt_disable();
+ bpf_preempt_enable();
+ bpf_unwind_resume();
+}
+
+__u64 input = 0;
+__u64 magic = 0x5eed;
+__u64 pads_ran = 0;
+__u64 pad_runs = 0;
+
+/*
+ * 1. Everything a cleanup table leaves dead: the continuation after a throw,
+ * the tail after a pad's resume, an ld_imm64 and a conditional branch inside
+ * that tail, and a block reached only by the dead continuation.
+ */
+static __used __naked __noinline __u64 sweep_frame(void)
+{
+ asm volatile (
+ "r1 = %[input] ll;"
+ "r6 = *(u64 *)(r1 + 0);"
+ "call bpf_preempt_disable;"
+ "if r6 < 101 goto 6f;"
+ "r1 = %[cookie];"
+"1:" "call bpf_throw;" /* cleanup region */
+"2:"
+ "goto 3f;"
+"4:" /* landing pad */
+ "r7 = r0;"
+ "call bpf_preempt_enable;"
+ PAD_RAN("%[ran]")
+ "r1 = r7;"
+ "call bpf_unwind_resume;"
+ "r1 = %[pads_ran] ll;"
+ "r2 = *(u64 *)(r1 + 0);"
+ "if r2 == 0 goto 5f;"
+ "call bpf_preempt_enable;"
+ "r0 = 7;"
+ "exit;"
+"5:"
+ "r0 = 8;"
+ "exit;"
+"3:" /* dead: only the dead goto reaches it */
+ "r0 = 9;"
+ "exit;"
+"6:" /* live: the ordinary return */
+ "call bpf_preempt_enable;"
+ "r0 = 0;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "4b")
+ :
+ : [cookie]"i"(THROW_COOKIE), [ran]"i"(RAN_SWEEP),
+ __imm_addr(input), __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+SEC("syscall")
+int entry_sweep(void *ctx)
+{
+ return sweep_frame();
+}
+
+/*
+ * 2. A callee called from both a covered and an uncovered site: the pad is
+ * recorded on the call site, not on the callee. The lock sits between the two
+ * calls because the frame really would leak it if the uncovered call unwound.
+ */
+static __used __noinline __u64 shared_callee(__u64 x)
+{
+ if (x > 100)
+ bpf_throw(THROW_COOKIE);
+ return x + 1;
+}
+
+static __used __naked __noinline __u64 shared_frame(void)
+{
+ asm volatile (
+ "r1 = %[input] ll;"
+ "r6 = *(u64 *)(r1 + 0);"
+ "r1 = 0;"
+ "call shared_callee;"
+ "call bpf_rcu_read_lock;"
+ "r1 = r6;"
+"1:" "call shared_callee;" /* cleanup region */
+"2:"
+ "r6 = r0;"
+ "call bpf_rcu_read_unlock;"
+ "r0 = r6;"
+ "exit;"
+"3:" /* landing pad */
+ "r7 = r0;"
+ "call bpf_rcu_read_unlock;"
+ PAD_RAN("%[ran]")
+ "r1 = r7;"
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_SHARED), __imm_addr(input), __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+SEC("syscall")
+int entry_shared(void *ctx)
+{
+ return shared_frame();
+}
+
+/*
+ * 3. A landing pad that reads its frame's callee-saved registers, which only
+ * the spill in the discarded callee's prologue still holds. The callee fills
+ * r6-r9 with something else before it throws, so the pad's check passes only
+ * if the walker found that spill.
+ */
+#define LOAD_MAGIC_REGS \
+ "r1 = %[magic] ll;" \
+ "r6 = *(u64 *)(r1 + 0);" \
+ "r7 = r6;" \
+ "r7 += 1;" \
+ "r8 = r6;" \
+ "r8 += 2;" \
+ "r9 = r6;" \
+ "r9 += 3;"
+
+/* Set @bit only if r6-r9 still hold what LOAD_MAGIC_REGS put there. */
+#define CHECK_MAGIC_REGS(bit) \
+ "r1 = %[magic] ll;" \
+ "r2 = *(u64 *)(r1 + 0);" \
+ "if r6 != r2 goto 9f;" \
+ "r2 += 1;" \
+ "if r7 != r2 goto 9f;" \
+ "r2 += 1;" \
+ "if r8 != r2 goto 9f;" \
+ "r2 += 1;" \
+ "if r9 != r2 goto 9f;" \
+ PAD_RAN(bit) \
+ "9:"
+
+static __used __naked __noinline __u64 regs_thrower(void)
+{
+ asm volatile (
+ /* Not this frame's to keep, and that is the point. */
+ "r6 = 0xdead;"
+ "r7 = 0xbeef;"
+ "r8 = 0xcafe;"
+ "r9 = 0xf00d;"
+ "r1 = %[cookie];"
+ "call bpf_throw;"
+ "r0 = 0;"
+ "exit;"
+ :
+ : [cookie]"i"(THROW_COOKIE)
+ : __clobber_all);
+}
+
+static __used __naked __noinline __u64 regs_frame(void)
+{
+ asm volatile (
+ LOAD_MAGIC_REGS
+ "call bpf_preempt_disable;"
+"1:" "call regs_thrower;" /* cleanup region */
+"2:"
+ "call bpf_preempt_enable;"
+ "r0 = 0;"
+ "exit;"
+"3:" /* landing pad */
+ "call bpf_preempt_enable;"
+ CHECK_MAGIC_REGS("%[ran]")
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [cookie]"i"(THROW_COOKIE), [ran]"i"(RAN_REGS),
+ __imm_addr(magic), __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+SEC("syscall")
+int entry_regs(void *ctx)
+{
+ return regs_frame();
+}
+
+/*
+ * 4. The same, with a tail-call-reachable callee: its prologue pushes the tail
+ * call counter between the program stack and the spill area, so the spill the
+ * walker reads moves. The array is left empty; being reachable is the point.
+ */
+struct {
+ __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
+ __uint(max_entries, 1);
+ __uint(key_size, sizeof(__u32));
+ __uint(value_size, sizeof(__u32));
+} jmp_table SEC(".maps");
+
+static __used __noinline __u64 tc_thrower(void *ctx)
+{
+ /* Never taken; its presence is what makes this frame, whose spill the
+ * walker reads, tail-call-reachable.
+ */
+ bpf_tail_call_static(ctx, &jmp_table, 0);
+ asm volatile (
+ "r6 = 0xdead;"
+ "r7 = 0xbeef;"
+ "r8 = 0xcafe;"
+ "r9 = 0xf00d;"
+ "r1 = %[cookie];"
+ "call bpf_throw;"
+ :
+ : [cookie]"i"(THROW_COOKIE)
+ : __clobber_all);
+ return 0;
+}
+
+/*
+ * The frame with the pad is the program itself, and __naked: r1 holds the
+ * context at entry, which is the only place to get one for bpf_tail_call().
+ */
+SEC("syscall")
+__naked int entry_tail_call(void)
+{
+ asm volatile (
+ "*(u64 *)(r10 - 8) = r1;" /* the context, straight from entry */
+ LOAD_MAGIC_REGS
+ "r1 = %[input] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+ "if r1 < 101 goto 8f;"
+ "r1 = *(u64 *)(r10 - 8);"
+"1:" "call tc_thrower;" /* cleanup region */
+"2:"
+"8:"
+ "r0 = 0;"
+ "exit;"
+"3:" /* landing pad */
+ CHECK_MAGIC_REGS("%[ran]")
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_TAIL_CALL), __imm_addr(input),
+ __imm_addr(magic), __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+/*
+ * 5. A landing pad in the main program's own frame. jit_subprogs() compiles it
+ * as func[0], but the ksym the walker finds is the outer bpf_prog's, so the
+ * table has to be handed over or the pad is never dispatched -- silently.
+ */
+SEC("syscall")
+__naked int entry_main_pad(void)
+{
+ asm volatile (
+ LOAD_MAGIC_REGS
+ "r1 = %[input] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+ "if r1 < 101 goto 8f;"
+"1:" "call regs_thrower;" /* cleanup region */
+"2:"
+"8:"
+ "r0 = 0;"
+ "exit;"
+"3:" /* landing pad */
+ CHECK_MAGIC_REGS("%[ran]")
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_MAIN_PAD), __imm_addr(input),
+ __imm_addr(magic), __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+/*
+ * 6. A tail call that is really taken: the target is a program in its own
+ * right, so the walk ends there and this frame's pad does not run. The callee
+ * can also throw on a path never taken, which keeps the pad out of the sweep.
+ */
+struct {
+ __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
+ __uint(max_entries, 1);
+ __uint(key_size, sizeof(__u32));
+ __uint(value_size, sizeof(__u32));
+} taken_table SEC(".maps");
+
+SEC("syscall")
+int tc_target(void *ctx)
+{
+ bpf_throw(THROW_COOKIE);
+ return 0;
+}
+
+static __used __noinline __u64 tc_taken_callee(void *ctx, __u64 x)
+{
+ /* Never true at run time; the verifier cannot know that, and its
+ * unwind out of here is what keeps the caller's pad alive.
+ */
+ if (x == 7)
+ bpf_throw(THROW_COOKIE);
+ bpf_tail_call_static(ctx, &taken_table, 0);
+ return 0;
+}
+
+SEC("syscall")
+__naked int entry_tail_taken(void)
+{
+ asm volatile (
+ "*(u64 *)(r10 - 8) = r1;" /* the context, straight from entry */
+ "r1 = %[input] ll;"
+ "r2 = *(u64 *)(r1 + 0);"
+ "if r2 < 101 goto 8f;"
+ "r1 = *(u64 *)(r10 - 8);"
+"1:" "call tc_taken_callee;" /* cleanup region */
+"2:"
+ "exit;" /* the cookie, delivered at tc_target */
+"8:"
+ "r0 = 0;"
+ "exit;"
+"3:" /* landing pad: must not run */
+ PAD_RAN("%[ran]")
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_TC_TAKEN), __imm_addr(input), __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+/*
+ * 7. An extension program over the callee of a covered call. The walk ends in
+ * the extension's frame, as it does for a tail call target, so the pad does
+ * not run; fr_callee() can also throw by itself, giving the same call site
+ * both answers.
+ */
+__noinline __u64 fr_callee(__u64 x)
+{
+ if (x == 7)
+ bpf_throw(THROW_COOKIE);
+ return x + 1;
+}
+
+SEC("syscall")
+__naked int entry_freplace(void)
+{
+ asm volatile (
+ "r1 = %[input] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+"1:" "call fr_callee;" /* cleanup region */
+"2:"
+ "exit;"
+"3:" /* landing pad */
+ PAD_RAN("%[ran]")
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_FREPLACE), __imm_addr(input), __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+/*
+ * 8. A throwing subprogram named by a BPF_PSEUDO_FUNC on a path never taken.
+ * Handing one to a helper is what is refused, not naming it, so anything going
+ * by the ld_imm64 alone turns this program away.
+ */
+static __used __noinline int cb_thrower(__u32 idx, void *ctx)
+{
+ bpf_throw(THROW_COOKIE);
+ return 0;
+}
+
+static __used __noinline __u64 addr_taken_callee(__u64 x)
+{
+ if (x <= 100)
+ return x + 1;
+ bpf_throw(THROW_COOKIE);
+ return bpf_loop(1, cb_thrower, NULL, 0);
+}
+
+SEC("syscall")
+__naked int entry_addr_taken(void)
+{
+ asm volatile (
+ "r1 = %[input] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+"1:" "call addr_taken_callee;" /* cleanup region */
+"2:"
+ "exit;"
+"3:" /* landing pad */
+ PAD_RAN("%[ran]")
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_ADDR_TAKEN), __imm_addr(input), __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+/*
+ * 9. A record that covers bpf_throw() itself: the frame that raises the
+ * exception is the frame the record covers and the boundary both, so the pad
+ * runs on the way to delivering the cookie out of the program it came from.
+ */
+SEC("syscall")
+__naked int entry_no_subprog(void)
+{
+ asm volatile (
+ "r1 = %[input] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+ "if r1 < 101 goto 8f;"
+ "call bpf_preempt_disable;"
+ "r1 = %[cookie];"
+"1:" "call bpf_throw;" /* cleanup region */
+"2:"
+ "exit;"
+"8:"
+ "r0 = 0;"
+ "exit;"
+"3:" /* landing pad */
+ "call bpf_preempt_enable;"
+ PAD_RAN("%[ran]")
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [cookie]"i"(THROW_COOKIE), [ran]"i"(RAN_NO_SUBPROG),
+ __imm_addr(input), __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+/*
+ * 10. A landing pad that calls a subprogram an extension can replace. The
+ * load-time rule cannot see the extension coming, so what stops a nested
+ * exception is the walk, which ends in the extension's own frame. pad_runs
+ * says the pad ran once rather than twice.
+ */
+__noinline __u64 pad_callee(__u64 x)
+{
+ return x + 1;
+}
+
+static __used __noinline __u64 pc_thrower(__u64 x)
+{
+ if (x > 100)
+ bpf_throw(THROW_COOKIE);
+ return x + 1;
+}
+
+static __used __naked __noinline __u64 pad_calls_frame(void)
+{
+ asm volatile (
+ "r1 = %[input] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+"1:" "call pc_thrower;" /* cleanup region */
+"2:"
+ "exit;"
+"3:" /* landing pad */
+ "r6 = r0;"
+ "r1 = 1;"
+ "call pad_callee;" /* an extension can stand in here */
+ PAD_COUNT
+ PAD_RAN("%[ran]")
+ "r1 = r6;"
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_PAD_CALLS), __imm_addr(input), __imm_addr(pads_ran),
+ __imm_addr(pad_runs)
+ : __clobber_all);
+}
+
+SEC("syscall")
+int entry_pad_calls(void *ctx)
+{
+ return pad_calls_frame();
+}
+
+/*
+ * 11. A covered bpf_throw() the sweep leaves as the last instruction, where
+ * the default exception callback is then patched in -- the one patchlet that
+ * does not keep the call it replaced in the last slot, so the marks have to
+ * follow it. The r6-r9 check is what reports a lost throw site mark.
+ */
+SEC("syscall")
+__naked int entry_pad_first(void)
+{
+ asm volatile (
+ LOAD_MAGIC_REGS
+ "r1 = %[input] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+ "if r1 < 101 goto 7f;"
+ "goto 4f;"
+"3:" /* landing pad, ahead of the call */
+ CHECK_MAGIC_REGS("%[ran]")
+ "call bpf_unwind_resume;"
+ "exit;"
+"7:"
+ "r0 = 0;"
+ "exit;"
+"4:"
+ "r1 = %[cookie];"
+"1:" "call bpf_throw;" /* cleanup region */
+"2:"
+ "exit;" /* dead: swept, leaving the call last */
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [cookie]"i"(THROW_COOKIE), [ran]"i"(RAN_PAD_FIRST),
+ __imm_addr(input), __imm_addr(magic), __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+/*
+ * 12. A cleanup region whose last instruction is a 16-byte one, so end - 1
+ * names the half that is not an instruction of its own. A well formed region
+ * that a rule against it would turn away.
+ */
+static __used __naked __noinline __u64 wide_rec_frame(void)
+{
+ asm volatile (
+ "r1 = %[input] ll;"
+ "r6 = *(u64 *)(r1 + 0);"
+ "call bpf_rcu_read_lock;"
+ "r1 = r6;"
+"1:" "call shared_callee;" /* cleanup region begins */
+ "r1 = %[magic] ll;" /* ... and ends on this pair */
+"2:"
+ "r6 = r0;"
+ "call bpf_rcu_read_unlock;"
+ "r0 = r6;"
+ "exit;"
+"3:" /* landing pad */
+ "r7 = r0;"
+ "call bpf_rcu_read_unlock;"
+ PAD_RAN("%[ran]")
+ "r1 = r7;"
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_WIDE_REC), __imm_addr(input), __imm_addr(magic),
+ __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+SEC("syscall")
+int entry_wide_rec(void *ctx)
+{
+ return wide_rec_frame();
+}
+
+/*
+ * 13. A pad that works out of its own frame's stack, the shape every
+ * compiler-generated pad has. A JIT that addresses the frame through the
+ * stack pointer -- arm64 -- has to address a pad's frame some other way. Both
+ * directions are here: the reload sees the frame, and the store lands in it.
+ */
+static __used __naked __noinline __u64 pad_stack_frame(void)
+{
+ asm volatile (
+ "r1 = %[magic] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+ "*(u64 *)(r10 - 8) = r1;" /* what the pad will want */
+ "r1 = %[input] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+"1:" "call pc_thrower;" /* cleanup region */
+"2:"
+ "r0 = 0;"
+ "exit;"
+"3:" /* landing pad */
+ "r6 = r0;"
+ "r7 = *(u64 *)(r10 - 8);" /* reload it out of the frame */
+ "*(u64 *)(r10 - 16) = r7;" /* and write the frame while here */
+ "r1 = %[magic] ll;"
+ "r2 = *(u64 *)(r1 + 0);"
+ "if r7 != r2 goto 9f;"
+ "r3 = *(u64 *)(r10 - 16);"
+ "if r3 != r2 goto 9f;"
+ PAD_RAN("%[ran]")
+"9:"
+ "r1 = r6;"
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_PAD_STACK), __imm_addr(input), __imm_addr(magic),
+ __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+SEC("syscall")
+int entry_pad_stack(void *ctx)
+{
+ return pad_stack_frame();
+}
+
+/*
+ * 14. The same, with a frame in between that has no pad of its own, so the
+ * liveness query for an outer frame has more than one frame to walk and the
+ * pad has to be counted at every step.
+ */
+static __used __noinline __u64 deep_mid(__u64 x)
+{
+ return pc_thrower(x) + 1;
+}
+
+static __used __naked __noinline __u64 deep_frame(void)
+{
+ asm volatile (
+ "r1 = %[magic] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+ "*(u64 *)(r10 - 8) = r1;" /* nothing but the pad reads this */
+ "r1 = %[input] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+"1:" "call deep_mid;" /* cleanup region */
+"2:"
+ "r0 = 0;"
+ "exit;"
+"3:" /* landing pad */
+ "r6 = r0;"
+ "r7 = *(u64 *)(r10 - 8);"
+ "r1 = %[magic] ll;"
+ "r2 = *(u64 *)(r1 + 0);"
+ "if r7 != r2 goto 9f;"
+ PAD_RAN("%[ran]")
+"9:"
+ "r1 = r6;"
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_DEEP_PAD), __imm_addr(input), __imm_addr(magic),
+ __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+SEC("syscall")
+int entry_deep_pad(void *ctx)
+{
+ return deep_frame();
+}
+
+/*
+ * 15. A region around a call the kernel knows cannot unwind: no call site is
+ * marked, nothing reaches the pad, and the sweep removes it. The program is
+ * otherwise ordinary and has to load.
+ */
+static __used __naked __noinline __u64 nounwind_rec_frame(void)
+{
+ asm volatile (
+ "call bpf_preempt_disable;"
+"1:" "call bpf_preempt_enable;" /* cleanup region: nounwind */
+"2:"
+ "r0 = 0;"
+ "exit;"
+"3:" /* landing pad, never dispatched */
+ PAD_RAN("%[ran]")
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_NOUNWIND_REC), __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+SEC("syscall")
+int entry_nounwind_rec(void *ctx)
+{
+ return nounwind_rec_frame();
+}
+
+/*
+ * 16. The name a frontend gives the resume. Every pad above calls
+ * bpf_unwind_resume(); LLVM emits _Unwind_Resume() and libbpf maps one onto
+ * the other, so this program is what keeps that mapping tested.
+ */
+extern void _Unwind_Resume(void) __ksym;
+
+static __used __noinline void __resume_alias_btf_anchor(void)
+{
+ _Unwind_Resume();
+}
+
+static __used __naked __noinline __u64 resume_alias_frame(void)
+{
+ asm volatile (
+"1:" "call regs_thrower;" /* cleanup region */
+"2:"
+ "r0 = 0;"
+ "exit;"
+"3:" /* landing pad */
+ PAD_RAN("%[ran]")
+ "call _Unwind_Resume;" /* the frontend's name for it */
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_RESUME_ALIAS), __imm_addr(pads_ran)
+ : __clobber_all);
+}
+
+SEC("syscall")
+int entry_resume_alias(void *ctx)
+{
+ return resume_alias_frame();
+}
+
+/*
+ * 17. A landing pad that calls a subprogram which tail calls. What a pad may
+ * not contain is a tail call of its own, which would unwind a prologue the
+ * walker's stack never held; a callee's prologue really did run there, so its
+ * tail call releases exactly that and the target returns into the pad. The
+ * tail call counter comes out of the unwinding frame, which is one of the
+ * pad's own subprogram and so really holds one.
+ */
+struct {
+ __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
+ __uint(max_entries, 1);
+ __uint(key_size, sizeof(__u32));
+ __uint(value_size, sizeof(__u32));
+} pad_tc_table SEC(".maps");
+
+__u64 pad_tc_target_ran = 0;
+
+SEC("syscall")
+int pad_tc_target(void *ctx)
+{
+ pad_tc_target_ran += 1;
+ return 0;
+}
+
+static __used __noinline __u64 pad_tc_callee(void *ctx)
+{
+ /* Taken only once the test has populated the array. */
+ bpf_tail_call_static(ctx, &pad_tc_table, 0);
+ return 0;
+}
+
+/*
+ * The frame with the pad is the program itself, and __naked: r1 holds the
+ * context at entry, which is the only place to get one for bpf_tail_call().
+ * The pad reloads it from its own frame's stack.
+ */
+SEC("syscall")
+__naked int entry_pad_tail_call(void)
+{
+ asm volatile (
+ "*(u64 *)(r10 - 8) = r1;" /* the context, straight from entry */
+ LOAD_MAGIC_REGS
+ "r1 = %[input] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+ "if r1 < 101 goto 8f;"
+"1:" "call regs_thrower;" /* cleanup region */
+"2:"
+"8:"
+ "r0 = 0;"
+ "exit;"
+"3:" /* landing pad */
+ PAD_COUNT
+ "r1 = *(u64 *)(r10 - 8);"
+ "call pad_tc_callee;"
+ /* Only if the frame survived the callee's tail call. */
+ CHECK_MAGIC_REGS("%[ran]")
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : [ran]"i"(RAN_PAD_TAIL_CALL), __imm_addr(input),
+ __imm_addr(magic), __imm_addr(pads_ran), __imm_addr(pad_runs)
+ : __clobber_all);
+}
+
+/*
+ * 18. The other target for that same tail call: a program carrying a cleanup
+ * table of its own, which throws while the outer exception is still in flight.
+ * The tail call made it a boundary, so the inner walk runs its pad and ends in
+ * its own frame, never reaching the walker's frames above it: the outer pad is
+ * not restarted and the outer cookie is still the one delivered. The outer
+ * pad's r6-r9, which this target overwrites, come back with its frame.
+ */
+__u64 tc_target_pad_runs = 0;
+__u64 inner_magic = 0xd00d;
+
+static __used __naked __noinline __u64 inner_thrower(void)
+{
+ asm volatile (
+ /* Not this frame's to keep, the same as regs_thrower. */
+ "r6 = 0xf00d;"
+ "r7 = 0xcafe;"
+ "r8 = 0xbeef;"
+ "r9 = 0xdead;"
+ "r1 = %[cookie];"
+ "call bpf_throw;"
+ "r0 = 0;"
+ "exit;"
+ :
+ : [cookie]"i"(INNER_COOKIE)
+ : __clobber_all);
+}
+
+SEC("syscall")
+__naked int pad_tc_throw_target(void)
+{
+ asm volatile (
+ /* Distinct from the outer pad's, so neither can stand in for it. */
+ "r1 = %[inner_magic] ll;"
+ "r6 = *(u64 *)(r1 + 0);"
+ "r7 = r6;"
+ "r7 += 1;"
+ "r8 = r6;"
+ "r8 += 2;"
+ "r9 = r6;"
+ "r9 += 3;"
+ "r1 = %[input] ll;"
+ "r1 = *(u64 *)(r1 + 0);"
+ "if r1 < 101 goto 8f;"
+"1:" "call inner_thrower;" /* cleanup region */
+"2:"
+"8:"
+ "r0 = 0;"
+ "exit;"
+"3:" /* landing pad */
+ /* This frame's own r6-r9, not the outer pad's. */
+ "r1 = %[inner_magic] ll;"
+ "r2 = *(u64 *)(r1 + 0);"
+ "if r6 != r2 goto 9f;"
+ "r2 += 1;"
+ "if r7 != r2 goto 9f;"
+ "r2 += 1;"
+ "if r8 != r2 goto 9f;"
+ "r2 += 1;"
+ "if r9 != r2 goto 9f;"
+ "r1 = %[tc_target_pad_runs] ll;"
+ "r2 = *(u64 *)(r1 + 0);"
+ "r2 += 1;"
+ "*(u64 *)(r1 + 0) = r2;"
+"9:"
+ "call bpf_unwind_resume;"
+ "exit;"
+ CLEANUP_REC("1b", "2b", "3b")
+ :
+ : __imm_addr(input), __imm_addr(inner_magic),
+ __imm_addr(tc_target_pad_runs)
+ : __clobber_all);
+}
+
+char _license[] SEC("license") = "GPL";
--
2.53.0-Meta
next prev parent reply other threads:[~2026-09-18 4:43 UTC|newest]
Thread overview: 56+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-18 4:41 [PATCH bpf-next v2 00/20] bpf: Run exception cleanup landing pads when bpf_throw() unwinds Yonghong Song
2026-09-18 4:42 ` [PATCH bpf-next v2 01/20] bpf: Accept the compiler's exception cleanup table at program load Yonghong Song
2026-09-18 4:42 ` [PATCH bpf-next v2 02/20] bpf: Add the bpf_unwind_resume() kfunc Yonghong Song
2026-09-18 4:42 ` [PATCH bpf-next v2 03/20] bpf: Add lookups for exception cleanup resumes and landing pads Yonghong Song
2026-09-18 5:44 ` bot+bpf-ci
2026-09-19 4:55 ` Alexei Starovoitov
2026-09-19 17:42 ` Yonghong Song
2026-09-18 4:42 ` [PATCH bpf-next v2 04/20] bpf: Mark the call sites an exception cleanup table covers Yonghong Song
2026-09-18 4:42 ` [PATCH bpf-next v2 05/20] bpf: Make exception landing pads reachable in the CFG Yonghong Song
2026-09-18 4:59 ` sashiko-bot
2026-09-19 19:17 ` Yonghong Song
2026-09-18 5:44 ` bot+bpf-ci
2026-09-19 19:32 ` Yonghong Song
2026-09-18 4:42 ` [PATCH bpf-next v2 06/20] bpf: Explore the landing pads no call site reaches Yonghong Song
2026-09-18 5:44 ` bot+bpf-ci
2026-09-19 19:32 ` Yonghong Song
2026-09-18 4:42 ` [PATCH bpf-next v2 07/20] bpf: Refuse exception cleanup shapes bpf_throw() cannot dispatch Yonghong Song
2026-09-18 4:42 ` [PATCH bpf-next v2 08/20] bpf: Walk the exception unwind in the verifier Yonghong Song
2026-09-18 4:42 ` [PATCH bpf-next v2 09/20] bpf: Refuse a private stack for a program with an exception cleanup table Yonghong Song
2026-09-18 5:44 ` bot+bpf-ci
2026-09-19 19:37 ` Yonghong Song
2026-09-18 4:42 ` [PATCH bpf-next v2 10/20] bpf: Dispatch exception cleanup pads from bpf_throw() Yonghong Song
2026-09-18 5:58 ` bot+bpf-ci
2026-09-19 19:54 ` Yonghong Song
2026-09-18 4:42 ` [PATCH bpf-next v2 11/20] bpf, x86: Dispatch exception cleanup pads at run time Yonghong Song
2026-09-18 5:03 ` sashiko-bot
2026-09-19 20:00 ` Yonghong Song
2026-09-18 5:44 ` bot+bpf-ci
2026-09-19 20:04 ` Yonghong Song
2026-09-18 4:43 ` [PATCH bpf-next v2 12/20] bpf, arm64: " Yonghong Song
2026-09-18 5:44 ` bot+bpf-ci
2026-09-19 20:07 ` Yonghong Song
2026-09-18 4:43 ` [PATCH bpf-next v2 13/20] libbpf: Resolve the compiler's _Unwind_Resume to the kernel's kfunc Yonghong Song
2026-09-18 4:43 ` [PATCH bpf-next v2 14/20] libbpf: Add cleanup_info to bpf_prog_load_opts Yonghong Song
2026-09-18 4:57 ` sashiko-bot
2026-09-19 20:18 ` Yonghong Song
2026-09-18 4:43 ` [PATCH bpf-next v2 15/20] libbpf: Collect .bpf_cleanup records and pass them to the kernel Yonghong Song
2026-09-18 5:00 ` sashiko-bot
2026-09-19 20:21 ` Yonghong Song
2026-09-18 4:43 ` [PATCH bpf-next v2 16/20] libbpf: Carry the exception cleanup table through the light skeleton Yonghong Song
2026-09-18 5:02 ` sashiko-bot
2026-09-19 20:27 ` Yonghong Song
2026-09-18 4:43 ` [PATCH bpf-next v2 17/20] libbpf: Let the static linker carry .bpf_cleanup relocations Yonghong Song
2026-09-18 5:01 ` sashiko-bot
2026-09-19 20:31 ` Yonghong Song
2026-09-18 5:44 ` bot+bpf-ci
2026-09-19 20:32 ` Yonghong Song
2026-09-18 4:43 ` [PATCH bpf-next v2 18/20] selftests/bpf: Add an end-to-end .bpf_cleanup exception test Yonghong Song
2026-09-18 4:59 ` sashiko-bot
2026-09-18 5:58 ` bot+bpf-ci
2026-09-19 20:34 ` Yonghong Song
2026-09-18 4:43 ` Yonghong Song [this message]
2026-09-18 5:01 ` [PATCH bpf-next v2 19/20] selftests/bpf: Cover the exception cleanup shapes the chain does not reach sashiko-bot
2026-09-18 5:44 ` bot+bpf-ci
2026-09-19 21:13 ` Yonghong Song
2026-09-18 4:43 ` [PATCH bpf-next v2 20/20] selftests/bpf: Load an exception cleanup program from a light skeleton Yonghong Song
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260918044337.3293731-1-yonghong.song@linux.dev \
--to=yonghong.song@linux.dev \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=kernel-team@fb.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.