* [PATCH bpf-next v2 2/4] bpf: Fix ld_{abs,ind} failure path analysis in subprogs
2026-04-08 19:12 [PATCH bpf-next v2 1/4] bpf: Propagate error from visit_tailcall_insn Daniel Borkmann
@ 2026-04-08 19:12 ` Daniel Borkmann
2026-04-08 19:12 ` [PATCH bpf-next v2 3/4] bpf: Remove static qualifier from local subprog pointer Daniel Borkmann
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Daniel Borkmann @ 2026-04-08 19:12 UTC (permalink / raw)
To: bpf; +Cc: ast, eddyz87, info
Usage of ld_{abs,ind} instructions got extended into subprogs some time
ago via commit 09b28d76eac4 ("bpf: Add abnormal return checks."). These
are only allowed in subprograms when the latter are BTF annotated and
have scalar return types.
The code generator in bpf_gen_ld_abs() has an abnormal exit path (r0=0 +
exit) from legacy cBPF times. While the enforcement is on scalar return
types, the verifier must also simulate the path of abnormal exit if the
packet data load via ld_{abs,ind} failed.
This is currently not the case. Fix it by having the verifier simulate
both success and failure paths, and extend it in similar ways as we do
for tail calls. The success path (r0=unknown, continue to next insn) is
pushed onto stack for later validation and the r0=0 and return to the
caller is done on the fall-through side.
Fixes: 09b28d76eac4 ("bpf: Add abnormal return checks.")
Reported-by: STAR Labs SG <info@starlabs.sg>
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
v1->v2:
- Added mark_reg_scratched
kernel/bpf/verifier.c | 33 +++++++++++++++++++++++++++++++--
1 file changed, 31 insertions(+), 2 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index db009d509ade..fffb38a441e0 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -18357,6 +18357,23 @@ static int check_ld_abs(struct bpf_verifier_env *env, struct bpf_insn *insn)
mark_reg_unknown(env, regs, BPF_REG_0);
/* ld_abs load up to 32-bit skb data. */
regs[BPF_REG_0].subreg_def = env->insn_idx + 1;
+ /*
+ * See bpf_gen_ld_abs() which emits a hidden BPF_EXIT with r0=0
+ * which must be explored by the verifier when in a subprog.
+ */
+ if (env->cur_state->curframe) {
+ struct bpf_verifier_state *branch;
+
+ mark_reg_scratched(env, BPF_REG_0);
+ branch = push_stack(env, env->insn_idx + 1, env->insn_idx, false);
+ if (IS_ERR(branch))
+ return PTR_ERR(branch);
+ mark_reg_known_zero(env, regs, BPF_REG_0);
+ err = prepare_func_exit(env, &env->insn_idx);
+ if (err)
+ return err;
+ env->insn_idx--;
+ }
return 0;
}
@@ -19276,7 +19293,12 @@ static int visit_gotox_insn(int t, struct bpf_verifier_env *env)
return keep_exploring ? KEEP_EXPLORING : DONE_EXPLORING;
}
-static int visit_tailcall_insn(struct bpf_verifier_env *env, int t)
+/*
+ * Instructions that can abnormally return from a subprog (tail_call
+ * upon success, ld_{abs,ind} upon load failure) have a hidden exit
+ * that the verifier must account for.
+ */
+static int visit_abnormal_return_insn(struct bpf_verifier_env *env, int t)
{
static struct bpf_subprog_info *subprog;
struct bpf_iarray *jt;
@@ -19311,6 +19333,13 @@ static int visit_insn(int t, struct bpf_verifier_env *env)
/* All non-branch instructions have a single fall-through edge. */
if (BPF_CLASS(insn->code) != BPF_JMP &&
BPF_CLASS(insn->code) != BPF_JMP32) {
+ if (BPF_CLASS(insn->code) == BPF_LD &&
+ (BPF_MODE(insn->code) == BPF_ABS ||
+ BPF_MODE(insn->code) == BPF_IND)) {
+ ret = visit_abnormal_return_insn(env, t);
+ if (ret)
+ return ret;
+ }
insn_sz = bpf_is_ldimm64(insn) ? 2 : 1;
return push_insn(t, t + insn_sz, FALLTHROUGH, env);
}
@@ -19356,7 +19385,7 @@ static int visit_insn(int t, struct bpf_verifier_env *env)
if (bpf_helper_changes_pkt_data(insn->imm))
mark_subprog_changes_pkt_data(env, t);
if (insn->imm == BPF_FUNC_tail_call) {
- ret = visit_tailcall_insn(env, t);
+ ret = visit_abnormal_return_insn(env, t);
if (ret)
return ret;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH bpf-next v2 3/4] bpf: Remove static qualifier from local subprog pointer
2026-04-08 19:12 [PATCH bpf-next v2 1/4] bpf: Propagate error from visit_tailcall_insn Daniel Borkmann
2026-04-08 19:12 ` [PATCH bpf-next v2 2/4] bpf: Fix ld_{abs,ind} failure path analysis in subprogs Daniel Borkmann
@ 2026-04-08 19:12 ` Daniel Borkmann
2026-04-08 19:12 ` [PATCH bpf-next v2 4/4] selftests/bpf: Add tests for ld_{abs,ind} failure path in subprogs Daniel Borkmann
2026-04-09 1:50 ` [PATCH bpf-next v2 1/4] bpf: Propagate error from visit_tailcall_insn patchwork-bot+netdevbpf
3 siblings, 0 replies; 5+ messages in thread
From: Daniel Borkmann @ 2026-04-08 19:12 UTC (permalink / raw)
To: bpf; +Cc: ast, eddyz87, info, Anton Protopopov
The local subprog pointer in create_jt() and visit_abnormal_return_insn()
was declared static.
It is unconditionally assigned via bpf_find_containing_subprog() before
every use. Thus, the static qualifier serves no purpose and rather creates
confusion. Just remove it.
Fixes: e40f5a6bf88a ("bpf: correct stack liveness for tail calls")
Fixes: 493d9e0d6083 ("bpf, x86: add support for indirect jumps")
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Acked-by: Anton Protopopov <a.s.protopopov@gmail.com>
---
kernel/bpf/verifier.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index fffb38a441e0..1227b168bb07 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -19225,7 +19225,7 @@ static struct bpf_iarray *jt_from_subprog(struct bpf_verifier_env *env,
static struct bpf_iarray *
create_jt(int t, struct bpf_verifier_env *env)
{
- static struct bpf_subprog_info *subprog;
+ struct bpf_subprog_info *subprog;
int subprog_start, subprog_end;
struct bpf_iarray *jt;
int i;
@@ -19300,7 +19300,7 @@ static int visit_gotox_insn(int t, struct bpf_verifier_env *env)
*/
static int visit_abnormal_return_insn(struct bpf_verifier_env *env, int t)
{
- static struct bpf_subprog_info *subprog;
+ struct bpf_subprog_info *subprog;
struct bpf_iarray *jt;
if (env->insn_aux_data[t].jt)
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH bpf-next v2 4/4] selftests/bpf: Add tests for ld_{abs,ind} failure path in subprogs
2026-04-08 19:12 [PATCH bpf-next v2 1/4] bpf: Propagate error from visit_tailcall_insn Daniel Borkmann
2026-04-08 19:12 ` [PATCH bpf-next v2 2/4] bpf: Fix ld_{abs,ind} failure path analysis in subprogs Daniel Borkmann
2026-04-08 19:12 ` [PATCH bpf-next v2 3/4] bpf: Remove static qualifier from local subprog pointer Daniel Borkmann
@ 2026-04-08 19:12 ` Daniel Borkmann
2026-04-09 1:50 ` [PATCH bpf-next v2 1/4] bpf: Propagate error from visit_tailcall_insn patchwork-bot+netdevbpf
3 siblings, 0 replies; 5+ messages in thread
From: Daniel Borkmann @ 2026-04-08 19:12 UTC (permalink / raw)
To: bpf; +Cc: ast, eddyz87, info
Extend the verifier_ld_ind BPF selftests with subprogs containing
ld_{abs,ind} and craft the test in a way where the invalid register
read is rejected in the fixed case. Also add a success case each,
and add additional coverage related to the BTF return type enforcement.
# LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t verifier_ld_ind
[...]
#611/1 verifier_ld_ind/ld_ind: check calling conv, r1:OK
#611/2 verifier_ld_ind/ld_ind: check calling conv, r1 @unpriv:OK
#611/3 verifier_ld_ind/ld_ind: check calling conv, r2:OK
#611/4 verifier_ld_ind/ld_ind: check calling conv, r2 @unpriv:OK
#611/5 verifier_ld_ind/ld_ind: check calling conv, r3:OK
#611/6 verifier_ld_ind/ld_ind: check calling conv, r3 @unpriv:OK
#611/7 verifier_ld_ind/ld_ind: check calling conv, r4:OK
#611/8 verifier_ld_ind/ld_ind: check calling conv, r4 @unpriv:OK
#611/9 verifier_ld_ind/ld_ind: check calling conv, r5:OK
#611/10 verifier_ld_ind/ld_ind: check calling conv, r5 @unpriv:OK
#611/11 verifier_ld_ind/ld_ind: check calling conv, r7:OK
#611/12 verifier_ld_ind/ld_ind: check calling conv, r7 @unpriv:OK
#611/13 verifier_ld_ind/ld_abs: subprog early exit on ld_abs failure:OK
#611/14 verifier_ld_ind/ld_ind: subprog early exit on ld_ind failure:OK
#611/15 verifier_ld_ind/ld_abs: subprog with both paths safe:OK
#611/16 verifier_ld_ind/ld_ind: subprog with both paths safe:OK
#611/17 verifier_ld_ind/ld_abs: reject void return subprog:OK
#611/18 verifier_ld_ind/ld_ind: reject void return subprog:OK
#611 verifier_ld_ind:OK
Summary: 1/18 PASSED, 0 SKIPPED, 0 FAILED
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
.../selftests/bpf/progs/verifier_ld_ind.c | 142 ++++++++++++++++++
1 file changed, 142 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/verifier_ld_ind.c b/tools/testing/selftests/bpf/progs/verifier_ld_ind.c
index c925ba9a2e74..09e81b99eecb 100644
--- a/tools/testing/selftests/bpf/progs/verifier_ld_ind.c
+++ b/tools/testing/selftests/bpf/progs/verifier_ld_ind.c
@@ -107,4 +107,146 @@ __naked void ind_check_calling_conv_r7(void)
: __clobber_all);
}
+/*
+ * ld_{abs,ind} subprog that always sets r0=1 on the success path.
+ * bpf_gen_ld_abs() emits a hidden exit with r0=0 when the load helper
+ * fails. The verifier must model this failure return so that callers
+ * account for r0=0 as a possible return value.
+ */
+__naked __noinline __used
+static int ldabs_subprog(void)
+{
+ asm volatile (
+ "r6 = r1;"
+ ".8byte %[ld_abs];"
+ "r0 = 1;"
+ "exit;"
+ :
+ : __imm_insn(ld_abs, BPF_LD_ABS(BPF_W, 0))
+ : __clobber_all);
+}
+
+__naked __noinline __used
+static int ldind_subprog(void)
+{
+ asm volatile (
+ "r6 = r1;"
+ "r7 = 0;"
+ ".8byte %[ld_ind];"
+ "r0 = 1;"
+ "exit;"
+ :
+ : __imm_insn(ld_ind, BPF_LD_IND(BPF_W, BPF_REG_7, 0))
+ : __clobber_all);
+}
+
+SEC("socket")
+__description("ld_abs: subprog early exit on ld_abs failure")
+__failure __msg("R9 !read_ok")
+__naked void ld_abs_subprog_early_exit(void)
+{
+ asm volatile (
+ "call ldabs_subprog;"
+ "if r0 != 0 goto l_exit_%=;"
+ "r0 = r9;"
+ "l_exit_%=:"
+ "r0 = 0;"
+ "exit;"
+ ::: __clobber_all);
+}
+
+SEC("socket")
+__description("ld_ind: subprog early exit on ld_ind failure")
+__failure __msg("R9 !read_ok")
+__naked void ld_ind_subprog_early_exit(void)
+{
+ asm volatile (
+ "call ldind_subprog;"
+ "if r0 != 0 goto l_exit_%=;"
+ "r0 = r9;"
+ "l_exit_%=:"
+ "r0 = 0;"
+ "exit;"
+ ::: __clobber_all);
+}
+
+SEC("socket")
+__description("ld_abs: subprog with both paths safe")
+__success
+__naked void ld_abs_subprog_both_paths_safe(void)
+{
+ asm volatile (
+ "call ldabs_subprog;"
+ "r0 = 0;"
+ "exit;"
+ ::: __clobber_all);
+}
+
+SEC("socket")
+__description("ld_ind: subprog with both paths safe")
+__success
+__naked void ld_ind_subprog_both_paths_safe(void)
+{
+ asm volatile (
+ "call ldind_subprog;"
+ "r0 = 0;"
+ "exit;"
+ ::: __clobber_all);
+}
+
+/*
+ * ld_{abs,ind} in subprogs require scalar (int) return type in BTF.
+ * A test with void return must be rejected.
+ */
+__naked __noinline __used
+static void ldabs_void_subprog(void)
+{
+ asm volatile (
+ "r6 = r1;"
+ ".8byte %[ld_abs];"
+ "r0 = 1;"
+ "exit;"
+ :
+ : __imm_insn(ld_abs, BPF_LD_ABS(BPF_W, 0))
+ : __clobber_all);
+}
+
+SEC("socket")
+__description("ld_abs: reject void return subprog")
+__failure __msg("LD_ABS is only allowed in functions that return 'int'")
+__naked void ld_abs_void_subprog_reject(void)
+{
+ asm volatile (
+ "call ldabs_void_subprog;"
+ "r0 = 0;"
+ "exit;"
+ ::: __clobber_all);
+}
+
+__naked __noinline __used
+static void ldind_void_subprog(void)
+{
+ asm volatile (
+ "r6 = r1;"
+ "r7 = 0;"
+ ".8byte %[ld_ind];"
+ "r0 = 1;"
+ "exit;"
+ :
+ : __imm_insn(ld_ind, BPF_LD_IND(BPF_W, BPF_REG_7, 0))
+ : __clobber_all);
+}
+
+SEC("socket")
+__description("ld_ind: reject void return subprog")
+__failure __msg("LD_ABS is only allowed in functions that return 'int'")
+__naked void ld_ind_void_subprog_reject(void)
+{
+ asm volatile (
+ "call ldind_void_subprog;"
+ "r0 = 0;"
+ "exit;"
+ ::: __clobber_all);
+}
+
char _license[] SEC("license") = "GPL";
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* Re: [PATCH bpf-next v2 1/4] bpf: Propagate error from visit_tailcall_insn
2026-04-08 19:12 [PATCH bpf-next v2 1/4] bpf: Propagate error from visit_tailcall_insn Daniel Borkmann
` (2 preceding siblings ...)
2026-04-08 19:12 ` [PATCH bpf-next v2 4/4] selftests/bpf: Add tests for ld_{abs,ind} failure path in subprogs Daniel Borkmann
@ 2026-04-09 1:50 ` patchwork-bot+netdevbpf
3 siblings, 0 replies; 5+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-04-09 1:50 UTC (permalink / raw)
To: Daniel Borkmann; +Cc: bpf, ast, eddyz87, info
Hello:
This series was applied to bpf/bpf-next.git (master)
by Alexei Starovoitov <ast@kernel.org>:
On Wed, 8 Apr 2026 21:12:39 +0200 you wrote:
> Commit e40f5a6bf88a ("bpf: correct stack liveness for tail calls") added
> visit_tailcall_insn() but did not check its return value.
>
> Fixes: e40f5a6bf88a ("bpf: correct stack liveness for tail calls")
> Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
> ---
> kernel/bpf/verifier.c | 7 +++++--
> 1 file changed, 5 insertions(+), 2 deletions(-)
Here is the summary with links:
- [bpf-next,v2,1/4] bpf: Propagate error from visit_tailcall_insn
https://git.kernel.org/bpf/bpf-next/c/6bd96e40f31d
- [bpf-next,v2,2/4] bpf: Fix ld_{abs,ind} failure path analysis in subprogs
https://git.kernel.org/bpf/bpf-next/c/ee861486e377
- [bpf-next,v2,3/4] bpf: Remove static qualifier from local subprog pointer
https://git.kernel.org/bpf/bpf-next/c/9dba0ae973e7
- [bpf-next,v2,4/4] selftests/bpf: Add tests for ld_{abs,ind} failure path in subprogs
https://git.kernel.org/bpf/bpf-next/c/e0fcb42bc6f4
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply [flat|nested] 5+ messages in thread