A minimal, runnable demo of the BPF verifier complexity limit (1,000,000 processed instructions). One source file, compiled two ways:
| Build | Static instructions | Processed insns | Result |
|---|---|---|---|
always_inline |
38 | 1,000,001 | REJECTED (E2BIG) |
__noinline |
43 | 6,742 | LOADED |
Same program, ~150× difference in verifier work — and only 38 static instructions, proving it is the complexity limit, not program size.
The program is an outer loop that, each iteration, calls a helper walk()
containing an inner bounded loop. The only thing that changes between builds is
whether walk() is always_inline or a real __noinline BPF-to-BPF subprogram
(-DINLINE_MODE=1 vs 0).
always_inline: the inner loop is stamped into the outer loop's body. The outer indexiis still live, so the verifier's state at the inner loop differs on every outer iteration → no pruning → the inner loop is re-walkedOUTERtimes. Cost ≈OUTER × INNER × k(k ≈ 13 instruction-visits per inner iteration on 5.10).__noinline:walk()is entered viaBPF_CALLwith a fresh frame whose only input is the (constant) map-value pointer.iis not part of the entry state, so the inner loop is verified once. Cost ≈(inner loop once) + OUTER × const.
- Linux with BTF (
/sys/kernel/btf/vmlinux); kernel >= 5.3 for BPF-to-BPF calls (tested on 5.10). clang,llvm(llvm-objdump),bpftool,libbpfheaders (libbpf-devel).- root (loading a program needs
CAP_BPF/CAP_SYS_ADMIN).
sudo ./run_explosion.shThe script generates vmlinux.h from kernel BTF, builds both variants, loads
each with bpftool prog load ... -d, and prints the verifier's
processed N insns line for each.
# 1. headers from your running kernel's BTF
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 2. build both variants (set ARCH to x86 / arm64 to match your machine)
clang -g -O2 -Wall -target bpf -D__TARGET_ARCH_x86 -DINLINE_MODE=1 \
-I. -c explosion_demo.bpf.c -o explosion.inline.o
clang -g -O2 -Wall -target bpf -D__TARGET_ARCH_x86 -DINLINE_MODE=0 \
-I. -c explosion_demo.bpf.c -o explosion.noinline.o
# 3. load each and read the verifier log (-d = verbose verifier output)
sudo bpftool prog load explosion.inline.o /sys/fs/bpf/expl_in type xdp -d 2>&1 | grep -E "processed|too large"
sudo bpftool prog load explosion.noinline.o /sys/fs/bpf/expl_no type xdp -d 2>&1 | grep -E "processed|too large"
sudo rm -f /sys/fs/bpf/expl_in /sys/fs/bpf/expl_no
# count static instructions in each object
llvm-objdump -d explosion.inline.o | grep -cE '^\s+[0-9a-f]+:'
llvm-objdump -d explosion.noinline.o | grep -cE '^\s+[0-9a-f]+:'The inline build fails with:
BPF program is too large. Processed 1000001 insn
The __noinline build loads (processed 6742 insns).
OUTER (default 280) and INNER (default 300) are #defined at the top of the
source and can be overridden at compile time (-DOUTER=... -DINNER=...):
- If both builds load, raise
OUTER/INNER. - If both fail, lower them (the inline build may be hitting the static
BPF_MAXINSNSsize limit instead of the complexity limit).
Measured crossover at INNER=300: OUTER=255 loads at 997,068 processed;
OUTER=256 caps at 1,000,001.