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 v8 09/22] bpf: Refuse a landing pad that does not resume
Date: Thu, 1 Oct 2026 06:30:52 -0700 [thread overview]
Message-ID: <20261001133052.1339921-1-yonghong.song@linux.dev> (raw)
In-Reply-To: <20261001133006.1335369-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 runs the same drops and then
carries on in its frame, stopping the unwind. Only the first is supported:
bpf_unwind() rewrites every frame's return address in one pass, so a
caller of a catch pad's frame would resume at a pad for an unwind already
caught.
Nothing in the record says which kind a pad is, but the code does, as LLVM
emits it: a cleanup pad reaches _Unwind_Resume, a catch pad reaches a
return. An unwind arriving at a pad, in the frame that raised it, in a
caller, or out of a call to a global subprog, is the only way in, and it
marks the frame it enters. bpf_exc_check_insn() asks that mark about every
instruction of a program carrying a table, and refuses:
- an exit, which is how a catch pad ends
- a tail call, and a BPF_LD_[ABS|IND], which leaves through an exit on a
failed load
- an indirect jump
- a bpf_unwind(), and a call to a global subprogram that might_unwind
- an instruction reached both inside and outside a pad
do_check_insn() refuses the other half of it, a bpf_unwind_resume() the
mark does not find in a pad. That one is asked of every program rather
than only those carrying a table, since a program with no table has no pad
to be in.
The first three concern the pad's own frame, since a subprogram it calls
may do any of them and still come back; an unwind is refused in a pad's
callees too, since it never does. do_check() asks before pruning, so the
mark stays out of states_equal().
A speculative walk can reach a pad too; that is answered as do_check()
answers anything it cannot allow speculatively, by marking the instruction
for a barrier and stopping rather than refusing the program. Such a visit
leaves no in-pad or outside-pad mark for a real path to be refused over,
and an exit it finds in a pad gets the barrier too. A callback that can
unwind is refused as well, its helper frame being C with no pad.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
include/linux/bpf_verifier.h | 4 ++
kernel/bpf/exception.c | 88 ++++++++++++++++++++++++++++++++++++
kernel/bpf/exception.h | 2 +
kernel/bpf/verifier.c | 28 ++++++++++++
4 files changed, 122 insertions(+)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index a625d96a5b80..ccac422737fb 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;
/*
* What the program held when this frame was entered. A frame an unwind
* leaves has to have put these back: a diagnostic, which refuses the
@@ -716,6 +718,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; /* reached the other way */
unsigned int orig_idx; /* original instruction index, initialized once */
/*
diff --git a/kernel/bpf/exception.c b/kernel/bpf/exception.c
index 8d48cf69bef0..e824883d3981 100644
--- a/kernel/bpf/exception.c
+++ b/kernel/bpf/exception.c
@@ -141,6 +141,15 @@ int bpf_exc_check_info(struct bpf_verifier_env *env, const union bpf_attr *attr,
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;
+}
+
void bpf_exc_record_frame_entry(const struct bpf_verifier_state *state,
struct bpf_func_state *frame, u32 id_gen)
{
@@ -308,6 +317,85 @@ 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 running a pad, or called from 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;
+ }
+ /*
+ * Only a real path marks the insn: a speculative one that finds the
+ * other mark gets a barrier, so it must not leave one for a real path
+ * to be refused over.
+ */
+ if (!env->cur_state->speculative) {
+ 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 615df30fdfdb..e72e68ebfe85 100644
--- a/kernel/bpf/exception.h
+++ b/kernel/bpf/exception.h
@@ -23,5 +23,7 @@ int bpf_exc_check_frame_balance(struct bpf_verifier_env *env, const char *prefix
int bpf_exc_pad_of_call(struct bpf_verifier_env *env, u32 idx);
bool bpf_is_unwind_kfunc(const struct bpf_insn *insn);
bool bpf_is_unwind_resume_kfunc(const struct bpf_insn *insn);
+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 /* __BPF_EXCEPTION_H */
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index a7b25ab04051..f3ed68960d70 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10945,6 +10945,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",
@@ -19163,6 +19167,10 @@ static int unwind_frames(struct bpf_verifier_env *env, bool *do_print_state)
while (state->curframe) {
callee = cur_func(env);
caller = state->frame[state->curframe - 1];
+ /* A subprog that can unwind is refused as a callback. */
+ if (verifier_bug_if(callee->in_callback_fn, env,
+ "unwind out of callback frame %d", state->curframe))
+ return -EFAULT;
pad = bpf_exc_pad_of_call(env, callee->callsite);
/* The caller is at its call now, not at this frame's insn. */
state->insn_idx = callee->callsite;
@@ -19182,6 +19190,7 @@ static int unwind_frames(struct bpf_verifier_env *env, bool *do_print_state)
return err;
clear_caller_saved_regs(env, caller->regs);
mark_reg_unknown(env, caller->regs, BPF_REG_0);
+ caller->in_pad = true;
env->insn_idx = pad;
*do_print_state = true;
return INSN_IDX_UPDATED;
@@ -19240,6 +19249,7 @@ static int unwind_out_of_global_call(struct bpf_verifier_env *env, int call_idx,
frame = cur_func(env);
clear_caller_saved_regs(env, frame->regs);
mark_reg_unknown(env, frame->regs, BPF_REG_0);
+ frame->in_pad = true;
env->insn_idx = pad;
*do_print_state = true;
return INSN_IDX_UPDATED;
@@ -19292,6 +19302,7 @@ static int process_bpf_unwind(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;
*insn_idx = pad;
return INSN_IDX_UPDATED;
}
@@ -19557,6 +19568,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,
do_print_state);
+ if (!cur_func(env)->in_pad) {
+ verbose(env, "resume at insn %d is not in a landing pad\n",
+ env->insn_idx);
+ return -EINVAL;
+ }
err = bpf_exc_check_frame_balance(env, "a resume");
if (err)
return err;
@@ -19681,6 +19697,18 @@ static int do_check(struct bpf_verifier_env *env)
}
}
+ if (unlikely(env->cleanup_info_cnt)) {
+ err = bpf_exc_check_insn(env, insn);
+ /* An exit in a pad is refused as unsupported, not invalid. */
+ if ((error_recoverable_with_nospec(err) || err == -EOPNOTSUPP) &&
+ state->speculative) {
+ insn_aux->nospec = true;
+ goto process_bpf_exit;
+ }
+ 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-10-01 13:31 UTC|newest]
Thread overview: 50+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-01 13:30 [PATCH bpf-next v8 00/22] bpf: Run exception cleanup landing pads when bpf_unwind() unwinds Yonghong Song
2026-10-01 13:30 ` [PATCH bpf-next v8 01/22] bpf: Pack bpf_insn_aux_data flags into bit fields Yonghong Song
2026-10-01 13:30 ` [PATCH bpf-next v8 02/22] bpf: Accept the compiler's exception cleanup table at program load Yonghong Song
2026-10-01 13:30 ` [PATCH bpf-next v8 03/22] bpf: Add the bpf_unwind() and bpf_unwind_resume() kfuncs Yonghong Song
2026-10-01 13:30 ` [PATCH bpf-next v8 04/22] bpf: Add lookups for exception cleanup resumes and landing pads Yonghong Song
2026-10-01 13:48 ` sashiko-bot
2026-10-02 18:17 ` Yonghong Song
2026-10-01 13:30 ` [PATCH bpf-next v8 05/22] bpf: Prepare for an exception cleanup table before the CFG walk Yonghong Song
2026-10-01 14:31 ` bot+bpf-ci
2026-10-02 19:06 ` Yonghong Song
2026-10-01 13:30 ` [PATCH bpf-next v8 06/22] bpf: Make exception landing pads reachable in the CFG Yonghong Song
2026-10-01 13:30 ` [PATCH bpf-next v8 07/22] bpf: Follow an unwind to its landing pad in the verifier Yonghong Song
2026-10-01 13:50 ` sashiko-bot
2026-10-02 19:31 ` Yonghong Song
2026-10-01 14:31 ` bot+bpf-ci
2026-10-02 20:49 ` Yonghong Song
2026-10-03 12:23 ` Alexei Starovoitov
2026-10-04 17:56 ` Yonghong Song
2026-10-01 13:30 ` [PATCH bpf-next v8 08/22] bpf: Require an unwind to leave a frame holding what it entered with Yonghong Song
2026-10-01 14:31 ` bot+bpf-ci
2026-10-02 21:10 ` Yonghong Song
2026-10-03 12:25 ` Alexei Starovoitov
2026-10-04 17:59 ` Yonghong Song
2026-10-01 13:30 ` Yonghong Song [this message]
2026-10-03 12:25 ` [PATCH bpf-next v8 09/22] bpf: Refuse a landing pad that does not resume Alexei Starovoitov
2026-10-04 18:26 ` Yonghong Song
2026-10-01 13:30 ` [PATCH bpf-next v8 10/22] bpf: Do not use a private stack for a program that can unwind Yonghong Song
2026-10-01 13:53 ` sashiko-bot
2026-10-02 21:38 ` Yonghong Song
2026-10-01 13:31 ` [PATCH bpf-next v8 11/22] bpf: Dispatch cleanup pads by rewriting return addresses Yonghong Song
2026-10-01 14:31 ` bot+bpf-ci
2026-10-02 21:48 ` Yonghong Song
2026-10-03 12:26 ` Alexei Starovoitov
2026-10-04 18:28 ` Yonghong Song
2026-10-04 18:29 ` Yonghong Song
2026-10-01 13:31 ` [PATCH bpf-next v8 12/22] bpf, x86: Dispatch exception cleanup pads at run time Yonghong Song
2026-10-01 13:49 ` sashiko-bot
2026-10-02 21:54 ` Yonghong Song
2026-10-01 13:31 ` [PATCH bpf-next v8 13/22] bpf, arm64: " Yonghong Song
2026-10-01 13:31 ` [PATCH bpf-next v8 14/22] libbpf: Resolve the compiler's _Unwind_Resume to the kernel's kfunc Yonghong Song
2026-10-01 13:31 ` [PATCH bpf-next v8 15/22] libbpf: Add cleanup_info to bpf_prog_load_opts Yonghong Song
2026-10-01 13:46 ` sashiko-bot
2026-10-02 22:09 ` Yonghong Song
2026-10-01 13:31 ` [PATCH bpf-next v8 16/22] libbpf: Collect .bpf_cleanup records and pass them to the kernel Yonghong Song
2026-10-01 13:31 ` [PATCH bpf-next v8 17/22] libbpf: Carry the exception cleanup table through the light skeleton Yonghong Song
2026-10-01 13:31 ` [PATCH bpf-next v8 18/22] libbpf: Let the static linker carry .bpf_cleanup relocations Yonghong Song
2026-10-01 13:31 ` [PATCH bpf-next v8 19/22] selftests/bpf: Add end-to-end and negative .bpf_cleanup exception tests Yonghong Song
2026-10-01 13:31 ` [PATCH bpf-next v8 20/22] selftests/bpf: Add __set_global() and __ret_global() test tags Yonghong Song
2026-10-01 13:31 ` [PATCH bpf-next v8 21/22] selftests/bpf: Cover more accepted .bpf_cleanup exception shapes Yonghong Song
2026-10-01 13:32 ` [PATCH bpf-next v8 22/22] 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=20261001133052.1339921-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.