* [PATCH bpf-next v2] arm32, bpf: Reject BPF-to-BPF calls and callbacks in the JIT
@ 2026-04-17 14:33 Puranjay Mohan
2026-04-17 15:48 ` Emil Tsalapatis
2026-04-17 19:10 ` patchwork-bot+netdevbpf
0 siblings, 2 replies; 4+ messages in thread
From: Puranjay Mohan @ 2026-04-17 14:33 UTC (permalink / raw)
To: bpf, linux-arm-kernel
Cc: Puranjay Mohan, Jonas Rebmann, Alexei Starovoitov,
Daniel Borkmann, Andrii Nakryiko, Martin KaFai Lau,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Song Liu, Russell King,
kernel
The ARM32 BPF JIT does not support BPF-to-BPF function calls
(BPF_PSEUDO_CALL) or callbacks (BPF_PSEUDO_FUNC), but it does
not reject them either.
When a program with subprograms is loaded (e.g. libxdp's XDP
dispatcher uses __noinline__ subprograms, or any program using
callbacks like bpf_loop or bpf_for_each_map_elem), the verifier
invokes bpf_jit_subprogs() which calls bpf_int_jit_compile()
for each subprogram.
For BPF_PSEUDO_CALL, since ARM32 does not reject it, the JIT
silently emits code using the wrong address computation:
func = __bpf_call_base + imm
where imm is a pc-relative subprogram offset, producing a bogus
function pointer.
For BPF_PSEUDO_FUNC, the ldimm64 handler ignores src_reg and
loads the immediate as a normal 64-bit value without error.
In both cases, build_body() reports success and a JIT image is
allocated. ARM32 lacks the jit_data/extra_pass mechanism needed
for the second JIT pass in bpf_jit_subprogs(). On the second
pass, bpf_int_jit_compile() performs a full fresh compilation,
allocating a new JIT binary and overwriting prog->bpf_func. The
first allocation is never freed. bpf_jit_subprogs() then detects
the function pointer changed and aborts with -ENOTSUPP, but the
original JIT binary has already been leaked. Each program
load/unload cycle leaks one JIT binary allocation, as reported
by kmemleak:
unreferenced object 0xbf0a1000 (size 4096):
backtrace:
bpf_jit_binary_alloc+0x64/0xfc
bpf_int_jit_compile+0x14c/0x348
bpf_jit_subprogs+0x4fc/0xa60
Fix this by rejecting both BPF_PSEUDO_CALL in the BPF_CALL
handler and BPF_PSEUDO_FUNC in the BPF_LD_IMM64 handler, falling
through to the existing 'notyet' path. This causes build_body()
to fail before any JIT binary is allocated, so
bpf_int_jit_compile() returns the original program unjitted.
bpf_jit_subprogs() then sees !prog->jited and cleanly falls
back to the interpreter with no leak.
Acked-by: Daniel Borkmann <daniel@iogearbox.net>
Fixes: 1c2a088a6626 ("bpf: x64: add JIT support for multi-function programs")
Reported-by: Jonas Rebmann <jre@pengutronix.de>
Closes: https://lore.kernel.org/bpf/b63e9174-7a3d-4e22-8294-16df07a4af89@pengutronix.de
Tested-by: Jonas Rebmann <jre@pengutronix.de>
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
---
Changelog:
v1: https://lore.kernel.org/all/20260417103004.3552500-1-puranjay@kernel.org/
Changes in v2:
- Add Acked-by: Daniel Borkmann <daniel@iogearbox.net>
- Reject BPF_PSEUDO_FUNC in the BPF_LD | BPF_IMM | BPF_DW handler
- Move code below declarations
---
arch/arm/net/bpf_jit_32.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/arch/arm/net/bpf_jit_32.c b/arch/arm/net/bpf_jit_32.c
index deeb8f292454..a900aa973885 100644
--- a/arch/arm/net/bpf_jit_32.c
+++ b/arch/arm/net/bpf_jit_32.c
@@ -1852,6 +1852,9 @@ static int build_insn(const struct bpf_insn *insn, struct jit_ctx *ctx)
{
u64 val = (u32)imm | (u64)insn[1].imm << 32;
+ if (insn->src_reg == BPF_PSEUDO_FUNC)
+ goto notyet;
+
emit_a32_mov_i64(dst, val, ctx);
return 1;
@@ -2055,6 +2058,9 @@ static int build_insn(const struct bpf_insn *insn, struct jit_ctx *ctx)
const s8 *r5 = bpf2a32[BPF_REG_5];
const u32 func = (u32)__bpf_call_base + (u32)imm;
+ if (insn->src_reg == BPF_PSEUDO_CALL)
+ goto notyet;
+
emit_a32_mov_r64(true, r0, r1, ctx);
emit_a32_mov_r64(true, r1, r2, ctx);
emit_push_r64(r5, ctx);
base-commit: 1f5ffc672165ff851063a5fd044b727ab2517ae3
--
2.52.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* Re: [PATCH bpf-next v2] arm32, bpf: Reject BPF-to-BPF calls and callbacks in the JIT
2026-04-17 14:33 [PATCH bpf-next v2] arm32, bpf: Reject BPF-to-BPF calls and callbacks in the JIT Puranjay Mohan
@ 2026-04-17 15:48 ` Emil Tsalapatis
2026-04-17 16:15 ` Puranjay Mohan
2026-04-17 19:10 ` patchwork-bot+netdevbpf
1 sibling, 1 reply; 4+ messages in thread
From: Emil Tsalapatis @ 2026-04-17 15:48 UTC (permalink / raw)
To: Puranjay Mohan, bpf, linux-arm-kernel
Cc: Jonas Rebmann, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman,
Kumar Kartikeya Dwivedi, Song Liu, Russell King, kernel
On Fri Apr 17, 2026 at 10:33 AM EDT, Puranjay Mohan wrote:
> The ARM32 BPF JIT does not support BPF-to-BPF function calls
> (BPF_PSEUDO_CALL) or callbacks (BPF_PSEUDO_FUNC), but it does
> not reject them either.
>
> When a program with subprograms is loaded (e.g. libxdp's XDP
> dispatcher uses __noinline__ subprograms, or any program using
> callbacks like bpf_loop or bpf_for_each_map_elem), the verifier
> invokes bpf_jit_subprogs() which calls bpf_int_jit_compile()
> for each subprogram.
>
> For BPF_PSEUDO_CALL, since ARM32 does not reject it, the JIT
> silently emits code using the wrong address computation:
>
> func = __bpf_call_base + imm
>
> where imm is a pc-relative subprogram offset, producing a bogus
> function pointer.
>
> For BPF_PSEUDO_FUNC, the ldimm64 handler ignores src_reg and
> loads the immediate as a normal 64-bit value without error.
>
> In both cases, build_body() reports success and a JIT image is
> allocated. ARM32 lacks the jit_data/extra_pass mechanism needed
> for the second JIT pass in bpf_jit_subprogs(). On the second
> pass, bpf_int_jit_compile() performs a full fresh compilation,
> allocating a new JIT binary and overwriting prog->bpf_func. The
> first allocation is never freed. bpf_jit_subprogs() then detects
> the function pointer changed and aborts with -ENOTSUPP, but the
> original JIT binary has already been leaked. Each program
> load/unload cycle leaks one JIT binary allocation, as reported
> by kmemleak:
>
> unreferenced object 0xbf0a1000 (size 4096):
> backtrace:
> bpf_jit_binary_alloc+0x64/0xfc
> bpf_int_jit_compile+0x14c/0x348
> bpf_jit_subprogs+0x4fc/0xa60
>
> Fix this by rejecting both BPF_PSEUDO_CALL in the BPF_CALL
> handler and BPF_PSEUDO_FUNC in the BPF_LD_IMM64 handler, falling
> through to the existing 'notyet' path. This causes build_body()
> to fail before any JIT binary is allocated, so
> bpf_int_jit_compile() returns the original program unjitted.
> bpf_jit_subprogs() then sees !prog->jited and cleanly falls
> back to the interpreter with no leak.
Reviewed-by: Emil Tsalapatis <emil@etsalapatis.com>
The Fixes tag is a bit unrelated since it's for x64 but the original
commit that adds the file (ddecdfcea0ae8 ?) is so far back it probably
doesn't matter.
>
> Acked-by: Daniel Borkmann <daniel@iogearbox.net>
> Fixes: 1c2a088a6626 ("bpf: x64: add JIT support for multi-function programs")
> Reported-by: Jonas Rebmann <jre@pengutronix.de>
> Closes: https://lore.kernel.org/bpf/b63e9174-7a3d-4e22-8294-16df07a4af89@pengutronix.de
> Tested-by: Jonas Rebmann <jre@pengutronix.de>
> Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
> ---
>
> Changelog:
> v1: https://lore.kernel.org/all/20260417103004.3552500-1-puranjay@kernel.org/
> Changes in v2:
> - Add Acked-by: Daniel Borkmann <daniel@iogearbox.net>
> - Reject BPF_PSEUDO_FUNC in the BPF_LD | BPF_IMM | BPF_DW handler
> - Move code below declarations
>
> ---
> arch/arm/net/bpf_jit_32.c | 6 ++++++
> 1 file changed, 6 insertions(+)
>
> diff --git a/arch/arm/net/bpf_jit_32.c b/arch/arm/net/bpf_jit_32.c
> index deeb8f292454..a900aa973885 100644
> --- a/arch/arm/net/bpf_jit_32.c
> +++ b/arch/arm/net/bpf_jit_32.c
> @@ -1852,6 +1852,9 @@ static int build_insn(const struct bpf_insn *insn, struct jit_ctx *ctx)
> {
> u64 val = (u32)imm | (u64)insn[1].imm << 32;
>
> + if (insn->src_reg == BPF_PSEUDO_FUNC)
> + goto notyet;
> +
> emit_a32_mov_i64(dst, val, ctx);
>
> return 1;
> @@ -2055,6 +2058,9 @@ static int build_insn(const struct bpf_insn *insn, struct jit_ctx *ctx)
> const s8 *r5 = bpf2a32[BPF_REG_5];
> const u32 func = (u32)__bpf_call_base + (u32)imm;
>
> + if (insn->src_reg == BPF_PSEUDO_CALL)
> + goto notyet;
> +
> emit_a32_mov_r64(true, r0, r1, ctx);
> emit_a32_mov_r64(true, r1, r2, ctx);
> emit_push_r64(r5, ctx);
>
> base-commit: 1f5ffc672165ff851063a5fd044b727ab2517ae3
^ permalink raw reply [flat|nested] 4+ messages in thread* Re: [PATCH bpf-next v2] arm32, bpf: Reject BPF-to-BPF calls and callbacks in the JIT
2026-04-17 15:48 ` Emil Tsalapatis
@ 2026-04-17 16:15 ` Puranjay Mohan
0 siblings, 0 replies; 4+ messages in thread
From: Puranjay Mohan @ 2026-04-17 16:15 UTC (permalink / raw)
To: Emil Tsalapatis
Cc: bpf, linux-arm-kernel, Jonas Rebmann, Alexei Starovoitov,
Daniel Borkmann, Andrii Nakryiko, Martin KaFai Lau,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Song Liu, Russell King,
kernel
On Fri, Apr 17, 2026 at 4:48 PM Emil Tsalapatis <emil@etsalapatis.com> wrote:
>
> On Fri Apr 17, 2026 at 10:33 AM EDT, Puranjay Mohan wrote:
> > The ARM32 BPF JIT does not support BPF-to-BPF function calls
> > (BPF_PSEUDO_CALL) or callbacks (BPF_PSEUDO_FUNC), but it does
> > not reject them either.
> >
> > When a program with subprograms is loaded (e.g. libxdp's XDP
> > dispatcher uses __noinline__ subprograms, or any program using
> > callbacks like bpf_loop or bpf_for_each_map_elem), the verifier
> > invokes bpf_jit_subprogs() which calls bpf_int_jit_compile()
> > for each subprogram.
> >
> > For BPF_PSEUDO_CALL, since ARM32 does not reject it, the JIT
> > silently emits code using the wrong address computation:
> >
> > func = __bpf_call_base + imm
> >
> > where imm is a pc-relative subprogram offset, producing a bogus
> > function pointer.
> >
> > For BPF_PSEUDO_FUNC, the ldimm64 handler ignores src_reg and
> > loads the immediate as a normal 64-bit value without error.
> >
> > In both cases, build_body() reports success and a JIT image is
> > allocated. ARM32 lacks the jit_data/extra_pass mechanism needed
> > for the second JIT pass in bpf_jit_subprogs(). On the second
> > pass, bpf_int_jit_compile() performs a full fresh compilation,
> > allocating a new JIT binary and overwriting prog->bpf_func. The
> > first allocation is never freed. bpf_jit_subprogs() then detects
> > the function pointer changed and aborts with -ENOTSUPP, but the
> > original JIT binary has already been leaked. Each program
> > load/unload cycle leaks one JIT binary allocation, as reported
> > by kmemleak:
> >
> > unreferenced object 0xbf0a1000 (size 4096):
> > backtrace:
> > bpf_jit_binary_alloc+0x64/0xfc
> > bpf_int_jit_compile+0x14c/0x348
> > bpf_jit_subprogs+0x4fc/0xa60
> >
> > Fix this by rejecting both BPF_PSEUDO_CALL in the BPF_CALL
> > handler and BPF_PSEUDO_FUNC in the BPF_LD_IMM64 handler, falling
> > through to the existing 'notyet' path. This causes build_body()
> > to fail before any JIT binary is allocated, so
> > bpf_int_jit_compile() returns the original program unjitted.
> > bpf_jit_subprogs() then sees !prog->jited and cleanly falls
> > back to the interpreter with no leak.
>
> Reviewed-by: Emil Tsalapatis <emil@etsalapatis.com>
>
> The Fixes tag is a bit unrelated since it's for x64 but the original
> commit that adds the file (ddecdfcea0ae8 ?) is so far back it probably
> doesn't matter.
That fixes tag commit has verifier changes too:
-- 8< --
+ }
+ }
+ for (i = 0; i <= env->subprog_cnt; i++) {
+ old_bpf_func = func[i]->bpf_func;
+ tmp = bpf_int_jit_compile(func[i]);
+ if (tmp != func[i] || func[i]->bpf_func != old_bpf_func) {
+ verbose(env, "JIT doesn't support bpf-to-bpf calls\n");
+ err = -EFAULT;
+ goto out_free;
+ }
+ cond_resched();
+ }
+
-- >8 --
This call to bpf_int_jit_compile() is where the memory leak was
introduced, before this commit, there was no memory leak.
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH bpf-next v2] arm32, bpf: Reject BPF-to-BPF calls and callbacks in the JIT
2026-04-17 14:33 [PATCH bpf-next v2] arm32, bpf: Reject BPF-to-BPF calls and callbacks in the JIT Puranjay Mohan
2026-04-17 15:48 ` Emil Tsalapatis
@ 2026-04-17 19:10 ` patchwork-bot+netdevbpf
1 sibling, 0 replies; 4+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-04-17 19:10 UTC (permalink / raw)
To: Puranjay Mohan
Cc: bpf, linux-arm-kernel, jre, ast, daniel, andrii, martin.lau,
eddyz87, memxor, song, linux, kernel
Hello:
This patch was applied to bpf/bpf.git (master)
by Alexei Starovoitov <ast@kernel.org>:
On Fri, 17 Apr 2026 07:33:52 -0700 you wrote:
> The ARM32 BPF JIT does not support BPF-to-BPF function calls
> (BPF_PSEUDO_CALL) or callbacks (BPF_PSEUDO_FUNC), but it does
> not reject them either.
>
> When a program with subprograms is loaded (e.g. libxdp's XDP
> dispatcher uses __noinline__ subprograms, or any program using
> callbacks like bpf_loop or bpf_for_each_map_elem), the verifier
> invokes bpf_jit_subprogs() which calls bpf_int_jit_compile()
> for each subprogram.
>
> [...]
Here is the summary with links:
- [bpf-next,v2] arm32, bpf: Reject BPF-to-BPF calls and callbacks in the JIT
https://git.kernel.org/bpf/bpf/c/e1d486445af3
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] 4+ messages in thread
end of thread, other threads:[~2026-04-17 19:10 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-17 14:33 [PATCH bpf-next v2] arm32, bpf: Reject BPF-to-BPF calls and callbacks in the JIT Puranjay Mohan
2026-04-17 15:48 ` Emil Tsalapatis
2026-04-17 16:15 ` Puranjay Mohan
2026-04-17 19:10 ` patchwork-bot+netdevbpf
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox