
Preverify eBPF Programs in CI Before Kernel Load
A failed "preverify" is not a mystery box to route around. It is the eBPF verifier rejecting your bytecode before it loads, and the fix is to read the exact instruction and register state the log points to, not to bolt on workarounds until the error goes quiet. I built a workflow to preverify eBPF embedded code before it ever reached a device, so teams caught rejects in CI instead of in the field. That phrase means one thing: run the verifier logic in user space and catch failures early.
Last updated: 2026-07-21
You'll get verifier basics, portability notes, harness design, CI gates, and troubleshooting here, all tied to real kernel behavior: instruction limits, bounded loops, helper sets, and map rules. The rest of this is method you can run.
What does "preverify" actually mean for eBPF?
If you searched "preverify" and landed on a login portal, a dictionary entry, or tax software docs, this is not that. Here it means running the kernel's verifier checks against your eBPF program before the real load, so a reject shows up on a build machine instead of on a shipped board at 3 a.m.
The verifier's job is to prove your program terminates and touches only memory it is allowed to touch. To preverify is to replay that same static analysis off-device, during CI, so failures are reproducible and cheap.
That is the whole idea. It does not replace the real load. It moves the loud failures earlier, where you can actually read them.
The eBPF verifier and why it rejects your program
The verifier is a static analyzer inside the kernel that walks every possible instruction path in your bytecode before it lets a single instruction run. It does not execute your code. It simulates it, tracking the type and range of every register and stack slot at each step.
Reject any path it cannot prove safe, and the whole program is refused. That is by design. eBPF runs in kernel context, so a program that reads past a buffer or loops forever would take the machine with it.
Three checks fail people most often. First, control flow: the verifier must prove the program halts. Second, memory: every pointer dereference must be inside known bounds. Third, complexity: it gives up if there are too many paths to explore in the budget it allows.
The official BPF verifier documentation spells out the rules, but reading them cold rarely tells you why your program failed. For that, you read the log.
Reading a verifier rejection log line by line
Do not pattern-match the error text against the top Stack Overflow answer. Read the log the verifier hands you, because it prints the exact instruction and register state where it gave up. That shows the real problem instead of a guess.
A rejection looks like this near the bottom:
invalid access to packet, off=14 size=4, R3(id=0,off=0,r=0)
R3 pointer arithmetic on pkt_end prohibited
Read it right to left. R3 is the register that went bad. off=14 size=4 is the read it refused. The line above the error shows the register state going in, so you can see whether R3 was a validated packet pointer or an unbounded value the verifier lost track of.
Every log line is numbered by instruction offset. Map that offset back to your source by compiling with debug info and reading it with bpftool prog dump xlated. Now the reject points at a line of C instead of an abstract complaint.
When the log is too dense, cut the program down. Strip helpers and branches until it passes, then add one thing back at a time. The instruction that flips it back to failing is your culprit. This is slow and it is reliable.
Why bpf_sock_ops field access order and BTF layout break verification
Context field access is where "valid" C quietly becomes an invalid program. For a bpf_sock_ops program, you do not read the real kernel struct. The verifier rewrites each field access into an offset into the actual socket structure, and it only allows the offsets it knows about, in the widths it expects.
Access a field the running kernel does not expose, or read it at the wrong width, and you get invalid bpf_context access off=... size=.... The order you write the fields does not matter to the compiler, but the offset the verifier computes does. A struct laid out one way in your headers and another way in the target kernel produces a legal-looking read that lands on the wrong bytes.
This is a BTF problem, not a C problem. BTF, the type information compiled into your object, is what lets the loader relocate those offsets for the kernel actually in front of it. Mismatched BTF means the relocation points somewhere the verifier rejects.
The fix is CO-RE (Compile Once, Run Everywhere) with BPF_CORE_READ, so offsets are resolved against the target's BTF at load, not baked in at compile time. Read bpf_sock_ops fields through the vmlinux BTF headers, not a hand-copied struct, and access only the fields your minimum kernel supports.
From C source to a passing verifier check
Here is the pipeline, because a preverify failure is introduced at a specific point in it. Your C goes through clang and LLVM, which emit eBPF bytecode in an ELF object along with a BTF section. A user-space loader, usually libbpf, reads that object, applies CO-RE relocations, and hands the bytecode to the kernel through the bpf() syscall.
The kernel verifier runs during that syscall. Pass, and the bytecode is JIT-compiled to native instructions for your CPU. Fail, and the syscall returns an error with the log attached.
So a reject can be born in three places: your C, the clang output, or the relocation the loader applied. Most "the verifier is wrong" complaints are actually a relocation that moved a field access, or an optimization that produced a branch the verifier cannot follow. If you cross-compile for other boards, my proven method for building an ARM eBPF kernel covers where these mismatches creep in.
Diagnose a failed load with strace and bpftool, not guesses
Run this first: strace -e bpf ./your_loader. It shows you the exact bpf() syscall that failed, its arguments, and the errno that came back. That tells you whether the kernel refused the program (EACCES, verifier reject), refused the map, or refused you on privilege (EPERM).
Once you know which call failed, inspect the pieces:
bpftool prog showlists loaded programs, their types, and instruction counts. Confirm the type matches the hook you meant.bpftool prog dump xlated id <ID>prints the post-verifier bytecode so you can line log offsets up with instructions.bpftool btf dump file ./prog.oshows the BTF the loader is working from. Compare it against the target kernel's BTF when a context access fails.bpftool map showchecks key and value sizes, because a map mismatch fails the load just as hard as a bad instruction.
Only after strace names the failing syscall do I change any code. A guess costs you the night; the errno costs you thirty seconds. For a deeper tour of the tool, see my guide to inspecting programs with bpftool.
Source-level fixes that actually clear common rejections
Match the fix to what the log actually told you.
Out-of-bounds packet reads. The verifier will not trust a packet access until you compare against data_end first. Add the check right before the read: if (data + off + size > data_end) return 0;. This is the single most common networking reject, and it is not optional.
Stack overflow. The verifier caps eBPF stack at 512 bytes. A large local array or a deep struct on the stack blows it. Move the buffer into a BPF_MAP_TYPE_PERCPU_ARRAY and index it instead. The log says invalid indirect access to stack when you hit this.
Loops the verifier cannot reason about. Bounded loop support arrived in Linux 5.3. On kernels at or after that, a loop with a compile-time upper bound the verifier can prove is fine. Before it, unroll with #pragma unroll. "Bounded" means the verifier can prove the trip count. A small literal loop bound is not enough on its own.
Helper misuse and context assumptions. Call a helper the program type does not allow and you get unknown func or helper not allowed. Check the helper's allowed program types before you reach for it, because the verifier gates helpers per type.
Each of these comes straight from what the verifier prints. Read the line, apply the matching fix, reload, and confirm with strace that the syscall now succeeds.
How do you keep preverify failures out of CI?
Gate merges on a real verifier run across a kernel matrix, so the same failure class cannot come back. Verifier behavior drifts between versions, so testing against one kernel proves almost nothing about the boards you ship.
A practical setup:
- Compile once and keep the exact object, BTF, and clang version as build artifacts. A reject you cannot reproduce is a reject you cannot fix.
- Load the object against every kernel version in your fleet, in a VM or container per version. Boot the vendor config rather than a generic one, because config switches change which helpers exist.
- Fail the build on any nonzero load exit code, and archive the verifier log with the run so the reviewer reads the actual reason instead of a bare red X.
This catches the cross-kernel surprises that a single dev box hides. It does not prove the program works on live traffic, and it will not surface a vendor-only helper you do not have the kernel for. Pair it with on-device smoke tests and you have covered both ends. If your programs enforce policy at runtime, my notes on adding runtime enforcement with eBPF cover what the CI gate cannot.
FAQ
Does a passing preverify mean the program will load on the target device?
No, and treating it that way burns you. Preverify proves control-flow termination, pointer safety, and most map rules. It cannot prove a vendor-only helper exists, that the kernel config enables your program type, or that the attach point is present. Keep an on-device check for those.
What privilege does loading an eBPF program need?
Historically it took CAP_SYS_ADMIN, which is close to root. Linux 5.8 split out CAP_BPF so a loader can get program-loading rights without full admin. If strace shows the bpf() syscall returning EPERM, the verifier never ran; the kernel refused you on privilege. Check the process capabilities before you touch a line of code.
Why does the same object fail on an ARM board but pass on my x86 dev machine?
Different kernel, different BTF, different config. The verifier on the board may lack a helper, expose a context field at a different offset, or run an older instruction budget. Build against the board's kernel headers and BTF, and put that kernel in your CI matrix. A minimal kernel built for eBPF makes these differences easy to pin down.
Is the verifier a security boundary?
It proves memory and termination safety and says nothing about policy. A program can pass the verifier and still read data you did not intend it to. Verifier safety keeps the kernel from crashing; deciding who may load what is a separate privilege and policy question you have to answer yourself.
Can I run the verifier without loading into the kernel at all?
You can replay verifier.c in a user-space harness that reuses libbpf and stubs the bpf() syscall, which is what a real preverify workflow does. That buys you fast, reproducible rejects in CI. It stays an approximation, though: the true attach and config checks only fire in the kernel during the real syscall, so keep the on-device load as the final word.
