* [PATCH bpf-next] bpf, arm32: build constants inline instead of using a literal pool
@ 2026-11-11 12:16 Ramin Moussavi
2026-11-11 12:23 ` sashiko-bot
0 siblings, 1 reply; 2+ messages in thread
From: Ramin Moussavi @ 2026-11-11 12:16 UTC (permalink / raw)
To: Russell King, Puranjay Mohan
Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, bpf,
linux-arm-kernel, linux-kernel
ARMv5 and ARMv6 have no MOVW/MOVT, so emit_mov_i_no8m() put every
constant that is not an imm8m into a literal pool behind the epilogue and
loaded it with LDR rX, [pc, #off]. That offset field is 12 bits and the
pool sits at the end of the image, so from roughly 4 KB of JITed code on,
the load no longer reaches it.
Commit 0b59d8806a31 ("ARM: net: delegate filter to kernel interpreter
when imm_offset() return value can't fit into 12bits.") handles that by
setting FLAG_IMM_OVERFLOW and dropping the whole program to the
interpreter, and already noted that multiple pools "would better be done
as a feature instead of fix".
Build the constant in the register instead: a single MVN when the
inverted value is an imm8m, otherwise one MOV/ORR per non-zero byte,
since a byte at a byte-aligned position is always an imm8m. The arm64
JIT does the same in emit_a64_mov_i64() with MOVZ/MOVN and MOVK. Without
a pool there is no offset to overflow, so programs of any size are
translated, and FLAG_IMM_OVERFLOW, ctx->imms, ctx->imm_count and
ctx->epilogue_bytes go away with it.
It costs up to four instructions per hard constant instead of one. The
pool was not much cheaper: imm_offset() counts duplicates on the sizing
pass, so every use already took 4 bytes of LDR plus 4 bytes of slot.
The 4 KB ceiling is low enough to be hit by the kernel itself. An ARMv5
kernel fails to boot when CONFIG_BPF_JIT_ALWAYS_ON=y and
CONFIG_NET_PTP_CLASSIFY=y are set together: the first leaves no
interpreter to fall back to, the second makes sock_init() build the PTP
classifier, which needs 5124 bytes of ARM code. bpf_prog_create() then
returns -ENOTSUPP and ptp_classifier_init() turns that into a BUG_ON.
Neither option is unusual - NET_PTP_CLASSIFY is selected by
PTP_1588_CLOCK, which defaults to ETHERNET, which defaults to y:
kernel BUG at net/core/ptp_classifier.c:223!
PC is at ptp_classifier_init+0x3c/0x70
ptp_classifier_init from sock_init+0xac/0xf0
seccomp reaches the JIT the same way, so filters of that size are likely
rejected there as well; that was not measured.
ARMv7 and later are unaffected, the MOVW/MOVT path is unchanged.
Tested on versatile_defconfig (ARM926EJ-S) under qemu with
CONFIG_BPF_JIT_ALWAYS_ON=y: the kernel boots again and SO_ATTACH_FILTER
accepts classic filters up to BPF_MAXINSNS, all of which the JIT now
translates. That is the classic limit of 4096 instructions; eBPF
programs loaded through bpf() can be far larger and were not part of the
test. The emitted sequences were verified separately by executing them
for 6.2 million values.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Ramin Moussavi <ramin.moussavi@yacoub.de>
---
arch/arm/net/bpf_jit_32.c | 114 +++++++++-----------------------------
arch/arm/net/bpf_jit_32.h | 3 +
2 files changed, 29 insertions(+), 88 deletions(-)
diff --git a/arch/arm/net/bpf_jit_32.c b/arch/arm/net/bpf_jit_32.c
index 9ede81afbc50..d3b5d7745068 100644
--- a/arch/arm/net/bpf_jit_32.c
+++ b/arch/arm/net/bpf_jit_32.c
@@ -130,8 +130,6 @@ enum {
#define TMP_REG_2 (MAX_BPF_JIT_REG + 1) /* TEMP Register 2 */
#define TCALL_CNT (MAX_BPF_JIT_REG + 2) /* Tail Call Count */
-#define FLAG_IMM_OVERFLOW (1 << 0)
-
/*
* Map eBPF registers to ARM 32bit registers or stack scratch space.
*
@@ -193,10 +191,6 @@ static const s8 bpf2a32[][2] = {
* offsets : array of eBPF instruction offsets in
* JITed code.
* target : final JITed code.
- * epilogue_bytes : no of bytes used in epilogue.
- * imm_count : no of immediate counts used for global
- * variables.
- * imms : array of global variable addresses.
*/
struct jit_ctx {
@@ -209,11 +203,6 @@ struct jit_ctx {
u32 *offsets;
u32 *target;
u32 stack_size;
-#if __LINUX_ARM_ARCH__ < 7
- u16 epilogue_bytes;
- u16 imm_count;
- u32 *imms;
-#endif
};
/*
@@ -404,53 +393,6 @@ static void jit_fill_hole(void *area, unsigned int size)
#define _STACK_SIZE (ctx->prog->aux->stack_depth + SCRATCH_SIZE)
#define STACK_SIZE ALIGN(_STACK_SIZE, STACK_ALIGNMENT)
-#if __LINUX_ARM_ARCH__ < 7
-
-static u16 imm_offset(u32 k, struct jit_ctx *ctx)
-{
- unsigned int i = 0, offset;
- u16 imm;
-
- /* on the "fake" run we just count them (duplicates included) */
- if (ctx->target == NULL) {
- ctx->imm_count++;
- return 0;
- }
-
- while ((i < ctx->imm_count) && ctx->imms[i]) {
- if (ctx->imms[i] == k)
- break;
- i++;
- }
-
- if (ctx->imms[i] == 0)
- ctx->imms[i] = k;
-
- /* constants go just after the epilogue */
- offset = ctx->offsets[ctx->prog->len - 1] * 4;
- offset += ctx->prologue_bytes;
- offset += ctx->epilogue_bytes;
- offset += i * 4;
-
- ctx->target[offset / 4] = k;
-
- /* PC in ARM mode == address of the instruction + 8 */
- imm = offset - (8 + ctx->idx * 4);
-
- if (imm & ~0xfff) {
- /*
- * literal pool is too far, signal it into flags. we
- * can only detect it on the second pass unfortunately.
- */
- ctx->flags |= FLAG_IMM_OVERFLOW;
- return 0;
- }
-
- return imm;
-}
-
-#endif /* __LINUX_ARM_ARCH__ */
-
static inline int bpf2a32_offset(int bpf_to, int bpf_from,
const struct jit_ctx *ctx) {
int to, from;
@@ -469,7 +411,30 @@ static inline int bpf2a32_offset(int bpf_to, int bpf_from,
static inline void emit_mov_i_no8m(const u8 rd, u32 val, struct jit_ctx *ctx)
{
#if __LINUX_ARM_ARCH__ < 7
- emit(ARM_LDR_I(rd, ARM_PC, imm_offset(val, ctx)), ctx);
+ /* A single MVN if the inverted value is an imm8m, else one
+ * MOV/ORR per non-zero byte -- a byte is always an imm8m.
+ */
+ int imm12 = imm8m(~val);
+ unsigned int shift;
+ bool first = true;
+
+ if (imm12 >= 0) {
+ emit(ARM_MVN_I(rd, imm12), ctx);
+ return;
+ }
+ for (shift = 0; shift < 32; shift += 8) {
+ u32 part = val & (0xffU << shift);
+
+ if (!part)
+ continue;
+ if (first)
+ emit(ARM_MOV_I(rd, imm8m(part)), ctx);
+ else
+ emit(ARM_ORR_I(rd, rd, imm8m(part)), ctx);
+ first = false;
+ }
+ if (first)
+ emit(ARM_MOV_I(rd, 0), ctx);
#else
emit(ARM_MOVW(rd, val & 0xffff), ctx);
if (val > 0xffff)
@@ -2091,14 +2056,6 @@ static int build_insn(const struct bpf_insn *insn, struct jit_ctx *ctx)
pr_err_once("unknown opcode %02x\n", code);
return -EINVAL;
}
-
- if (ctx->flags & FLAG_IMM_OVERFLOW)
- /*
- * this instruction generated an overflow when
- * trying to access the literal pool, so
- * delegate this filter to the kernel interpreter.
- */
- return -1;
return 0;
}
@@ -2192,21 +2149,7 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
ctx.epilogue_offset = ctx.idx;
-#if __LINUX_ARM_ARCH__ < 7
- tmp_idx = ctx.idx;
build_epilogue(&ctx);
- ctx.epilogue_bytes = (ctx.idx - tmp_idx) * 4;
-
- ctx.idx += ctx.imm_count;
- if (ctx.imm_count) {
- ctx.imms = kcalloc(ctx.imm_count, sizeof(u32), GFP_KERNEL);
- if (ctx.imms == NULL)
- goto out_off;
- }
-#else
- /* there's nothing about the epilogue on ARMv7 */
- build_epilogue(&ctx);
-#endif
/* Now we can get the actual image size of the JITed arm code.
* Currently, we are not considering the THUMB-2 instructions
* for jit, although it can decrease the size of the image.
@@ -2224,7 +2167,7 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
* we must fall back to the interpretation
*/
if (header == NULL)
- goto out_imms;
+ goto out_off;
/* 2.) Actual pass to generate final JIT code */
ctx.target = (u32 *) image_ptr;
@@ -2254,11 +2197,6 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
prog->jited = 1;
prog->jited_len = image_size;
-out_imms:
-#if __LINUX_ARM_ARCH__ < 7
- if (ctx.imm_count)
- kfree(ctx.imms);
-#endif
out_off:
kfree(ctx.offsets);
@@ -2267,6 +2205,6 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
out_free:
image_ptr = NULL;
bpf_jit_binary_free(header);
- goto out_imms;
+ goto out_off;
}
diff --git a/arch/arm/net/bpf_jit_32.h b/arch/arm/net/bpf_jit_32.h
index 438f0e1f91a0..76d83b486ded 100644
--- a/arch/arm/net/bpf_jit_32.h
+++ b/arch/arm/net/bpf_jit_32.h
@@ -104,6 +104,7 @@
#define ARM_INST_MOV_I 0x03a00000
#define ARM_INST_MOVW 0x03000000
#define ARM_INST_MOVT 0x03400000
+#define ARM_INST_MVN_I 0x03e00000
#define ARM_INST_MUL 0x00000090
@@ -231,6 +232,8 @@
#define ARM_MOVT(rd, imm) \
(ARM_INST_MOVT | ((imm) >> 12) << 16 | (rd) << 12 | ((imm) & 0x0fff))
+#define ARM_MVN_I(rd, imm) _AL3_I(ARM_INST_MVN, rd, 0, imm)
+
#define ARM_MUL(rd, rm, rn) (ARM_INST_MUL | (rd) << 16 | (rm) << 8 | (rn))
#define ARM_POP(regs) (ARM_INST_POP | (regs))
--
2.53.0
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH bpf-next] bpf, arm32: build constants inline instead of using a literal pool
2026-11-11 12:16 [PATCH bpf-next] bpf, arm32: build constants inline instead of using a literal pool Ramin Moussavi
@ 2026-11-11 12:23 ` sashiko-bot
0 siblings, 0 replies; 2+ messages in thread
From: sashiko-bot @ 2026-11-11 12:23 UTC (permalink / raw)
To: Ramin Moussavi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Low] bpf, arm32: non-standard multi-line comment style in
emit_mov_i_no8m()
- [Low] bpf, arm32: unused flags field left in struct jit_ctx
--
commit 04b7597139a2756fa5ffbad42747333b65860a8c
Author: Ramin Moussavi <ramin.moussavi@yacoub.de>
bpf, arm32: build constants inline instead of using a literal pool
This commit replaces the literal pool used for non-imm8m constants on
ARMv5 and ARMv6 with inline MVN/MOV/ORR instruction sequences and removes
the literal pool tracking fields and overflow fallback.
> diff --git a/arch/arm/net/bpf_jit_32.c b/arch/arm/net/bpf_jit_32.c
> index 9ede81afbc50e..d3b5d77450687 100644
> --- a/arch/arm/net/bpf_jit_32.c
> +++ b/arch/arm/net/bpf_jit_32.c
> @@ -130,8 +130,6 @@ enum {
> #define TMP_REG_2 (MAX_BPF_JIT_REG + 1) /* TEMP Register 2 */
> #define TCALL_CNT (MAX_BPF_JIT_REG + 2) /* Tail Call Count */
>
> -#define FLAG_IMM_OVERFLOW (1 << 0)
> -
[ ... ]
> @@ -209,11 +203,6 @@ struct jit_ctx {
> u32 *offsets;
> u32 *target;
> u32 stack_size;
> -#if __LINUX_ARM_ARCH__ < 7
> - u16 epilogue_bytes;
> - u16 imm_count;
> - u32 *imms;
> -#endif
> };
[Severity: Low]
With FLAG_IMM_OVERFLOW removed, is struct jit_ctx's flags member now
unused as well? Nothing else in arch/arm/net/bpf_jit_32.c reads or writes
ctx->flags, so it can be dropped alongside epilogue_bytes, imm_count, and
imms.
[ ... ]
> @@ -469,7 +411,30 @@ static inline int bpf2a32_offset(int bpf_to, int bpf_from,
> static inline void emit_mov_i_no8m(const u8 rd, u32 val, struct jit_ctx *ctx)
> {
> #if __LINUX_ARM_ARCH__ < 7
> - emit(ARM_LDR_I(rd, ARM_PC, imm_offset(val, ctx)), ctx);
> + /* A single MVN if the inverted value is an imm8m, else one
> + * MOV/ORR per non-zero byte -- a byte is always an imm8m.
> + */
[Severity: Low]
This isn't a bug, but BPF code uses the multi-line comment style where
the opening /* sits on its own line and the comment text starts on the
next line.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261111121621.665533-1-ramin.moussavi@yacoub.de?part=1
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-11-11 12:23 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-11 12:16 [PATCH bpf-next] bpf, arm32: build constants inline instead of using a literal pool Ramin Moussavi
2026-11-11 12:23 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox