[PATCH bpf-next] bpf, arm32: Reject BPF_PSEUDO_CALL in the JIT

Puranjay Mohan
2026-04-17T10:30:39Z

The ARM32 BPF JIT does not support BPF-to-BPF function calls
(subprogram calls). When insn->src_reg == BPF_PSEUDO_CALL, the
imm field contains a pc-relative offset to another BPF function,
not a helper function index.

When a program containing BPF-to-BPF calls is loaded, the verifier
invokes bpf_jit_subprogs() which calls bpf_int_jit_compile() for each
subprogram. Since ARM32 does not reject BPF_PSEUDO_CALL, the JIT
silently emits code for the call using the wrong address computation:

func = __bpf_call_base + imm

where imm is actually a pc-relative subprogram offset, producing
a bogus function pointer. Because build_body() reports success,
bpf_jit_binary_alloc() is reached and a JIT image is allocated.

ARM32 also 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 BPF_PSEUDO_CALL early in build_insn(),
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.

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>
---
arch/arm/net/bpf_jit_32.c | 2 ++
1 file changed, 2 insertions(+)

diff --git a/arch/arm/net/bpf_jit_32.c b/arch/arm/net/bpf_jit_32.c
index deeb8f292454..91fef10e88bc 100644
--- a/arch/arm/net/bpf_jit_32.c
+++ b/arch/arm/net/bpf_jit_32.c
@@ -2047,6 +2047,8 @@ static int build_insn(const struct bpf_insn *insn, struct jit_ctx *ctx)
/* function call */
case BPF_JMP | BPF_CALL:
{
+ if (insn->src_reg == BPF_PSEUDO_CALL)
+ goto notyet;
const s8 *r0 = bpf2a32[BPF_REG_0];
const s8 *r1 = bpf2a32[BPF_REG_1];
const s8 *r2 = bpf2a32[BPF_REG_2];

base-commit: 1f5ffc672165ff851063a5fd044b727ab2517ae3
--
2.52.0