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 v6 08/21] bpf: Refuse a landing pad that does not resume
Date: Fri, 25 Sep 2026 22:00:47 -0700 [thread overview]
Message-ID: <20260926050047.2216836-1-yonghong.song@linux.dev> (raw)
In-Reply-To: <20260926050006.2213110-1-yonghong.song@linux.dev>
A cleanup pad runs drop glue and calls bpf_unwind_resume(), so the frame
returns and the unwind goes on. A catch pad -- std::panic::catch_unwind and
its kin -- runs the same drops and then carries on in its frame, stopping
the unwind there. Only the first is supported: bpf_unwind() rewrites every
frame's return address in one pass, so a frame above a catch pad would
resume at a pad for an unwind that had already been caught.
Nothing in the record says which kind a pad is, but the code does, exactly
as LLVM emits it -- a cleanup pad reaches _Unwind_Resume and a catch pad
reaches a return. bpf_unwind() and the branch pushed at a covered call are
the only ways into a pad, and both mark the frame they enter.
bpf_exc_check_insn() asks that mark about every instruction of a program
that carries a table, and refuses:
- an exit, which is how a catch pad ends
- a tail call, which replaces the frame, and a BPF_LD_[ABS|IND], which on
a failed load leaves through the "r0 = 0; exit" gen_ld_abs() patches in
- an indirect jump, which nothing a frontend emits in a pad needs
- a bpf_unwind(), which starts a second walk over frames the first has
already rewritten, and a call to a global subprogram that might_unwind,
which is verified on its own and cannot be walked into from here
- a bpf_unwind_resume() outside a pad, where the fixups would lower it to
a bare return rather than to a resume
- an instruction reached both inside and outside a pad, which is a jump
into a pad: the cleanup would run with nothing to clean up after
The first three are about the pad's own frame, since a subprogram the pad
calls may do any of them and still come back; the unwind is refused
anywhere above a pad, since it never does. do_check() asks before it may
prune the path, so an instruction is in a pad or it is not, never both, and
the mark need not join the comparison in states_equal().
A callback that can unwind is refused here too: an unwind out of one stops
at the helper's own frame, which is C and has no landing pad, so the helper
would carry on as though nothing had happened.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
include/linux/bpf_verifier.h | 4 ++
kernel/bpf/exception.c | 81 ++++++++++++++++++++++++++++++++++++
kernel/bpf/exception.h | 3 ++
kernel/bpf/verifier.c | 18 ++++++++
4 files changed, 106 insertions(+)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 0143688896b0..4174c7d0177e 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -339,6 +339,8 @@ struct bpf_func_state {
bool in_async_callback_fn;
bool in_exception_callback_fn;
bool no_stack_arg_load;
+ /* an unwind reached this frame and its landing pad is running */
+ bool in_pad;
/* For callback calling functions that limit number of possible
* callback executions (e.g. bpf_loop) keeps track of current
* simulated iteration number.
@@ -698,6 +700,8 @@ struct bpf_insn_aux_data {
u64 non_stack_access:1; /* instruction can access non-stack memory */
/* true if some jump or call instruction targets this instruction */
u64 jump_target:1;
+ u64 in_cleanup_pad:1; /* reached with a landing pad running */
+ u64 outside_cleanup_pad:1; /* ... and the other way round */
unsigned int orig_idx; /* original instruction index, initialized once */
/*
diff --git a/kernel/bpf/exception.c b/kernel/bpf/exception.c
index 1ed0370a171b..86fdf847d33e 100644
--- a/kernel/bpf/exception.c
+++ b/kernel/bpf/exception.c
@@ -12,6 +12,15 @@
BTF_ID_LIST_SINGLE(bpf_unwind_id, func, bpf_unwind)
BTF_ID_LIST_SINGLE(bpf_unwind_resume_id, func, bpf_unwind_resume)
+int bpf_exc_check_callback(struct bpf_verifier_env *env, int subprog)
+{
+ if (!env->subprog_info[subprog].might_unwind)
+ return 0;
+
+ verbose(env, "subprog %d may unwind and is used as a callback\n", subprog);
+ return -EINVAL;
+}
+
static void mark_call_sites(struct bpf_verifier_env *env)
{
u32 i, j;
@@ -69,6 +78,78 @@ bool bpf_is_unwind_resume_kfunc(const struct bpf_insn *insn)
insn->imm == bpf_unwind_resume_id[0];
}
+/* Is an unwind in flight: is this frame a landing pad, or below one? */
+static bool unwinding(const struct bpf_verifier_state *state)
+{
+ u32 i;
+
+ for (i = 0; i <= state->curframe; i++)
+ if (state->frame[i]->in_pad)
+ return true;
+ return false;
+}
+
+int bpf_exc_check_insn(struct bpf_verifier_env *env, struct bpf_insn *insn)
+{
+ bool in_pad = cur_func(env)->in_pad;
+ struct bpf_insn_aux_data *aux;
+ u32 i = env->insn_idx;
+ const char *why = NULL;
+
+ if (unwinding(env->cur_state)) {
+ if (bpf_is_unwind_kfunc(insn)) {
+ verbose(env, "insn %u starts a second unwind while one is in flight\n", i);
+ return -EINVAL;
+ }
+ if (bpf_pseudo_call(insn)) {
+ int subprog = bpf_find_subprog(env, i + insn->imm + 1);
+
+ if (subprog >= 0 && bpf_subprog_is_global(env, subprog) &&
+ env->subprog_info[subprog].might_unwind) {
+ verbose(env,
+ "insn %u calls global subprog %d, which can unwind while an unwind is in flight\n",
+ i, subprog);
+ return -EINVAL;
+ }
+ }
+ }
+
+ aux = &env->insn_aux_data[i];
+
+ if (in_pad ? aux->outside_cleanup_pad : aux->in_cleanup_pad) {
+ verbose(env, "insn %u runs both inside and outside a landing pad\n", i);
+ return -EINVAL;
+ }
+ if (in_pad)
+ aux->in_cleanup_pad = true;
+ else
+ aux->outside_cleanup_pad = true;
+
+ if (!in_pad)
+ return 0;
+
+ if (insn->code == (BPF_JMP | BPF_EXIT)) {
+ verbose(env,
+ "exit at insn %u ends a landing pad: a catch pad is not supported yet, only cleanup pads that resume\n",
+ i);
+ return -EOPNOTSUPP;
+ }
+ if (bpf_helper_call(insn) && insn->imm == BPF_FUNC_tail_call)
+ why = "is a tail call, which replaces the frame";
+ else if (BPF_CLASS(insn->code) == BPF_LD &&
+ (BPF_MODE(insn->code) == BPF_ABS || BPF_MODE(insn->code) == BPF_IND))
+ why = "is a BPF_LD_[ABS|IND], which can leave through the epilogue";
+ else if (insn->code == (BPF_JMP | BPF_JA | BPF_X) ||
+ insn->code == (BPF_JMP32 | BPF_JA | BPF_X))
+ why = "is an indirect jump";
+
+ if (!why)
+ return 0;
+
+ verbose(env, "insn %u %s, and is in a landing pad\n", i, why);
+ return -EINVAL;
+}
+
int bpf_exc_pad_of_call(struct bpf_verifier_env *env, u32 idx)
{
u32 pad = env->insn_aux_data[idx].cleanup_pad;
diff --git a/kernel/bpf/exception.h b/kernel/bpf/exception.h
index b96b2c429e22..f3aff0fe8ecc 100644
--- a/kernel/bpf/exception.h
+++ b/kernel/bpf/exception.h
@@ -6,8 +6,11 @@
#include <linux/types.h>
struct bpf_verifier_env;
+struct bpf_insn;
int bpf_prepare_cleanup_exceptions(struct bpf_verifier_env *env);
int bpf_exc_pad_of_call(struct bpf_verifier_env *env, u32 idx);
+int bpf_exc_check_callback(struct bpf_verifier_env *env, int subprog);
+int bpf_exc_check_insn(struct bpf_verifier_env *env, struct bpf_insn *insn);
#endif /* _LINUX_BPF_EXCEPTION_H */
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 77176250f866..cc1ed776da15 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10985,6 +10985,10 @@ static int push_callback_call(struct bpf_verifier_env *env, struct bpf_insn *ins
* callbacks
*/
env->subprog_info[subprog].is_cb = true;
+ err = bpf_exc_check_callback(env, subprog);
+ if (err)
+ return err;
+
if (bpf_pseudo_kfunc_call(insn) &&
!is_callback_calling_kfunc(insn->imm)) {
verifier_bug(env, "kfunc %s#%d not marked as callback-calling",
@@ -19185,6 +19189,7 @@ static int push_cleanup_pad_branch(struct bpf_verifier_env *env, int insn_idx)
*/
clear_caller_saved_regs(env, frame->regs);
mark_reg_unknown(env, frame->regs, BPF_REG_0);
+ frame->in_pad = true;
return 0;
}
@@ -19197,6 +19202,7 @@ static int process_bpf_unwind(struct bpf_verifier_env *env, int *insn_idx)
return PROCESS_BPF_EXIT;
clear_caller_saved_regs(env, frame->regs);
mark_reg_unknown(env, frame->regs, BPF_REG_0);
+ frame->in_pad = true;
*insn_idx = pad;
return INSN_IDX_UPDATED;
}
@@ -19441,6 +19447,11 @@ static int do_check_insn(struct bpf_verifier_env *env, bool *do_print_state)
if (bpf_is_unwind_kfunc(insn))
return process_bpf_unwind(env, &env->insn_idx);
if (bpf_is_unwind_resume_kfunc(insn)) {
+ if (!cur_func(env)->in_pad) {
+ verbose(env, "resume at insn %d is not in a landing pad\n",
+ env->insn_idx);
+ return -EINVAL;
+ }
/*
* Mark r0 a known zero -- unknown first, as
* the known-zero helper keeps the type it
@@ -19450,6 +19461,7 @@ static int do_check_insn(struct bpf_verifier_env *env, bool *do_print_state)
*/
mark_reg_unknown(env, cur_regs(env), BPF_REG_0);
mark_reg_known_zero(env, cur_regs(env), BPF_REG_0);
+ cur_func(env)->in_pad = false;
return process_bpf_exit_full(env, do_print_state, false);
}
if (env->cur_state->active_locks) {
@@ -19578,6 +19590,12 @@ static int do_check(struct bpf_verifier_env *env)
}
}
+ if (unlikely(env->cleanup_info_cnt)) {
+ err = bpf_exc_check_insn(env, insn);
+ if (err)
+ return err;
+ }
+
if (bpf_is_prune_point(env, env->insn_idx)) {
err = bpf_is_state_visited(env, env->insn_idx);
if (err < 0)
--
2.53.0-Meta
next prev parent reply other threads:[~2026-09-26 5:00 UTC|newest]
Thread overview: 56+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-26 5:00 [PATCH bpf-next v6 00/21] bpf: Run exception cleanup landing pads when bpf_unwind() unwinds Yonghong Song
2026-09-26 5:00 ` [PATCH bpf-next v6 01/21] bpf: Pack bpf_insn_aux_data flags into bit fields Yonghong Song
2026-09-26 5:00 ` [PATCH bpf-next v6 02/21] bpf: Accept the compiler's exception cleanup table at program load Yonghong Song
2026-09-26 5:00 ` [PATCH bpf-next v6 03/21] bpf: Add the bpf_unwind() and bpf_unwind_resume() kfuncs Yonghong Song
2026-09-26 5:00 ` [PATCH bpf-next v6 04/21] bpf: Add lookups for exception cleanup resumes and landing pads Yonghong Song
2026-09-26 5:00 ` [PATCH bpf-next v6 05/21] bpf: Prepare for an exception cleanup table before the CFG walk Yonghong Song
2026-09-26 5:16 ` sashiko-bot
2026-09-26 23:54 ` Yonghong Song
2026-09-27 20:39 ` bot+bpf-ci
2026-09-28 0:01 ` Yonghong Song
2026-09-26 5:00 ` [PATCH bpf-next v6 06/21] bpf: Make exception landing pads reachable in the CFG Yonghong Song
2026-09-26 5:21 ` sashiko-bot
2026-09-27 0:02 ` Yonghong Song
2026-09-27 20:40 ` bot+bpf-ci
2026-09-28 0:12 ` Yonghong Song
2026-09-26 5:00 ` [PATCH bpf-next v6 07/21] bpf: Resume a covered call at its landing pad Yonghong Song
2026-09-26 5:15 ` sashiko-bot
2026-09-26 8:21 ` Alexei Starovoitov
2026-09-27 0:04 ` Yonghong Song
2026-09-27 0:41 ` Yonghong Song
2026-09-27 20:40 ` bot+bpf-ci
2026-09-28 0:17 ` Yonghong Song
2026-09-26 5:00 ` Yonghong Song [this message]
2026-09-26 5:17 ` [PATCH bpf-next v6 08/21] bpf: Refuse a landing pad that does not resume sashiko-bot
2026-09-27 3:06 ` Yonghong Song
2026-09-27 20:40 ` bot+bpf-ci
2026-09-28 0:29 ` Yonghong Song
2026-09-26 5:00 ` [PATCH bpf-next v6 09/21] bpf: Refuse a private stack for a program with an exception cleanup table Yonghong Song
2026-09-26 5:00 ` [PATCH bpf-next v6 10/21] bpf: Dispatch cleanup pads by rewriting return addresses Yonghong Song
2026-09-27 20:40 ` bot+bpf-ci
2026-09-28 1:08 ` Yonghong Song
2026-09-26 5:01 ` [PATCH bpf-next v6 11/21] bpf, x86: Dispatch exception cleanup pads at run time Yonghong Song
2026-09-26 5:15 ` sashiko-bot
2026-09-27 4:35 ` Yonghong Song
2026-09-27 20:39 ` bot+bpf-ci
2026-09-28 3:10 ` Yonghong Song
2026-09-26 5:01 ` [PATCH bpf-next v6 12/21] bpf, arm64: " Yonghong Song
2026-09-26 5:14 ` sashiko-bot
2026-09-27 20:40 ` bot+bpf-ci
2026-09-26 5:01 ` [PATCH bpf-next v6 13/21] libbpf: Resolve the compiler's _Unwind_Resume to the kernel's kfunc Yonghong Song
2026-09-26 5:01 ` [PATCH bpf-next v6 14/21] libbpf: Add cleanup_info to bpf_prog_load_opts Yonghong Song
2026-09-26 5:01 ` [PATCH bpf-next v6 15/21] libbpf: Collect .bpf_cleanup records and pass them to the kernel Yonghong Song
2026-09-27 20:39 ` bot+bpf-ci
2026-09-28 3:28 ` Yonghong Song
2026-09-26 5:01 ` [PATCH bpf-next v6 16/21] libbpf: Carry the exception cleanup table through the light skeleton Yonghong Song
2026-09-26 5:01 ` [PATCH bpf-next v6 17/21] libbpf: Let the static linker carry .bpf_cleanup relocations Yonghong Song
2026-09-26 5:01 ` [PATCH bpf-next v6 18/21] selftests/bpf: Add an end-to-end .bpf_cleanup exception test Yonghong Song
2026-09-26 5:01 ` [PATCH bpf-next v6 19/21] selftests/bpf: Add __set_global() and __ret_global() test tags Yonghong Song
2026-09-26 5:18 ` sashiko-bot
2026-09-27 4:58 ` Yonghong Song
2026-09-27 20:24 ` bot+bpf-ci
2026-09-28 3:36 ` Yonghong Song
2026-09-26 5:01 ` [PATCH bpf-next v6 20/21] selftests/bpf: Cover the exception cleanup shapes the chain does not reach Yonghong Song
2026-09-27 20:40 ` bot+bpf-ci
2026-09-28 3:49 ` Yonghong Song
2026-09-26 5:01 ` [PATCH bpf-next v6 21/21] 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=20260926050047.2216836-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.