BPFix: making eBPF verifier errors more friendly
BPFix makes eBPF verifier errors feel closer to Rust compiler errors.
The Linux eBPF verifier is powerful, but its failure logs are hard to read. BPFix is a userspace diagnostic tool for that problem. It reads verifier logs from your existing workflow and turns them into source-level explanations that point to where the verifier lost the required proof.
Quick Start
Install bpfix and load the prebuilt verifier failure:
cargo install bpfix
sudo bpftool -d prog load examples/bpftool/quick-start.bpf.o /sys/fs/bpf/bpfix-demo 2>&1 | tee verifier.log
The command loads a real verifier failure from bpfix-empirical
(stackoverflow-53136145). The source parses either IPv4 or IPv6, derives a
UDP header pointer on each branch, checks the UDP header against data_end, and
then reads the destination port:
if (ethertype == ETH_P_IP) {
ipv4_hdr = (void *)eth + ETH_HLEN;
if ((void *)(ipv4_hdr + 1) > data_end)
return 1;
} else if (ethertype == ETH_P_IPV6) {
ipv6_hdr = (void *)eth + ETH_HLEN;
if ((void *)(ipv6_hdr + 1) > data_end)
return 1;
} else {
return 2;
}
if (ipv4_hdr)
udph = (void *)ipv4_hdr + sizeof(*ipv4_hdr);
else
udph = (void *)ipv6_hdr + sizeof(*ipv6_hdr);
if (udph + sizeof(struct udphdr) > data_end)
return 1;
dst_port = __constant_ntohs(((struct udphdr *)udph)->dest);
That source shape is normal BPF C: the developer made the packet proof explicit.
The failure is in the verifier-visible proof lifecycle. One replay path reaches
the shared udph->dest load with r5 as a scalar instead of a packet pointer:
from 31 to 34: ... R5_w=40 ...
; if (udph + sizeof(struct udphdr) > data_end) @ prog.c:267
34: (bf) r3 = r5 ; R3_w=40 R5_w=40
35: (07) r3 += 8 ; R3=48
36: (2d) if r3 > r2 goto pc+4 ; R2=pkt_end() R3=48
; dst_port = __constant_ntohs(((struct udphdr *)udph)->dest); @ prog.c:270
37: (69) r2 = *(u16 *)(r5 +2)
R5 invalid mem access 'scalar'
bpfix verifier.log
The raw log says where the verifier stopped, but not the source-level proof story. BPFix turns the trace into a Rust-style multi-span diagnostic:
error[BPFIX-E006]: verifier-visible compiler lowering hides the required proof
= class: lowering_artifact
= confidence: medium
= diagnostic: supported, help: repair_hint, span: exact_pc
= next action: provenance
--> prog.c:270
|
263 | if (ipv4_hdr)
| ------------- nearby source context for pointer provenance
267 | if (udph + sizeof(struct udphdr) > data_end)
| -------------------------------------------- verifier state changes from pkt to scalar before the rejected access
270 | dst_port = __constant_ntohs(((struct udphdr *)udph)->dest);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ rejected here: verifier sees a scalar where a pointer is required
|
= verifier[229]: R5 invalid mem access 'scalar'
= note: nearest BPF instruction pc 37
= note: parsed 60 verifier state snapshots
= note[lowering]: compiler-lowered control flow hides an established packet-pointer proof
= required proof: preserve a verifier-recognized pointer type at the operation that requires a pointer
help: Reacquire a verifier-tracked pointer before the rejected dereference.
help: Use the packet pointer that received the data_end proof, or rederive and recheck it before the load.
help: Keep the final access on a verifier-tracked pointer; rederive it from a checked base after scalar work.
This is the kind of failure that motivates the project: the program is not missing a generic "add a bounds check" hint. The useful answer is the proof lifecycle: where a verifier-recognized pointer proof exists, where branch-local provenance can be merged away, and where the rejected instruction finally needs that proof.

The example object is copied from
bpfix-empirical/cases/stackoverflow-53136145/prog.o; replace it with the
object you are debugging in your own project.
Stable CLI Contract
The 0.1.x CLI contract is intentionally small:
bpfix [OPTIONS] [LOG]
LOG can be a verifier, build, bpftool, libbpf, Aya, or BCC log. When LOG
is omitted or -, BPFix reads stdin. Positional input and stdin are always log
text. Supported public options are --object <OBJECT>, --doctor,
--fail-on-unsupported, --help, and --version. The output is plain text;
there is no stable JSON output mode in 0.1.x.
BPFix does not execute loader commands in the default path, and there is no
default command-execution workflow. Empirical corpus YAML and Docker-based
execution are explicit non-default modes; if Docker support is added, it should
be selected with an option such as --docker, not inferred from LOG.
Best Workflow
The best user experience is to keep your current BPF workflow and let BPFix explain the verifier log it already produces:
make load 2>&1 | tee verifier.log
bpfix verifier.log
or:
sudo bpftool -d prog load examples/bpftool/quick-start.bpf.o /sys/fs/bpf/bpfix-demo 2>&1 | tee verifier.log
bpfix verifier.log
with feature-gated object metadata, after installing with --features object-analysis:
bpfix --object examples/bpftool/quick-start.bpf.o verifier.log
BPFix does not need case.yaml for normal use. Empirical corpus YAML records are
evaluation fixtures; use the evaluation scripts when measuring the bundled
corpus.
Check the local rebuild/load environment without reading a log:
bpfix --doctor
Agent Skill
skills/ebpf-verifier-repair/ is a portable
agent skill for end-to-end eBPF verifier repair. It works directly from BPF
source plus a verbose verifier log; BPFix diagnostics are optional structured
evidence, not a runtime dependency. The skill includes raw-log triage,
semantic-preservation and retry workflows, repair patterns, and a script for
compacting oversized verifier traces without modifying the original log.
Install the Agent Skill for Codex, GitHub Copilot, or another supported agent with GitHub CLI 2.90 or newer:
gh skill install eunomia-bpf/bpfix ebpf-verifier-repair --agent codex --scope user
For Claude Code, add this repository's marketplace and install the same skill as a namespaced plugin:
/plugin marketplace add eunomia-bpf/bpfix
/plugin install ebpf-verifier-repair@eunomia-bpf
The Agent Skill, Codex plugin, and Claude plugin share the same SKILL.md,
scripts, and references. The plugin manifests do not duplicate the repair
instructions.
Validate the skill and its frozen 75-case prompt surface with:
python3 "${CODEX_HOME:-$HOME/.codex}/skills/.system/skill-creator/scripts/quick_validate.py" skills/ebpf-verifier-repair
python3 -m unittest bpfix-bench/tools/test_run_suite.py -v
python3 bpfix-bench/tools/run_suite.py \
--split bpfix-bench/splits/main.txt \
--expected-count 75 \
--mode skills \
--prompt-only
The full user workflow is documented in docs/user-guide.md. More copyable
integration snippets live in examples/.
Project Status
The maintained implementation is the Rust workspace:
crates/
bpfix/ command-line diagnostic tool
bpfanalysis/ verifier-log and BPF bytecode analysis primitives
The earlier Python version has been removed from the maintained tree. Python
scripts that remain under bpfix-empirical/tools/ are empirical replay and corpus
maintenance helpers, not a second BPFix implementation and not part of the
public CLI.
The public CLI design lives in docs/open-source-tool-design.md, and the full
user workflow is in docs/user-guide.md. Evaluation methodology and
project planning notes live under docs/evaluation/ and
docs/project-status.md; they are supporting material, not required reading for
normal use.
bpfix-bench/ contains the frozen BPFix-Bench main75 LLM repair benchmark. It
is a source-first benchmark: a repair succeeds only when the candidate BPF C
file compiles, loads through the kernel verifier, and passes the case oracle.
Start with bpfix-bench/README.md for the benchmark contract and
docs/evaluation/bpfix-bench-llm-repair-eval.md for the reported raw-log versus
BPFix-diagnostic results.
The default bpfix CLI uses the verifier-log parser from bpfanalysis.
Object/CFG analysis is behind the object-analysis Cargo feature; that path
uses libbpf-sys and the vendor/libbpf submodule for BPF instruction and
program-type constants.
The current user-facing pipeline is log-first: bpfix parses verifier state
snapshots, instantiates the required verifier proof from the terminal error
and verifier state, extracts proof lifecycle events, and maps them back to
source comments when the log contains BTF/source annotations. Packet bounds,
scalar range, nullable pointer, stack readability, reference lifecycle, helper
capability, and pointer-provenance failures now produce parameterized proof
requirements instead of only terminal-error categories. When built with
--features object-analysis, the CLI accepts --object prog.o, builds
ProgramCFG summaries, and correlates verifier-state PCs with CFG sites when
the object section layout matches the loaded verifier program. Full BTF source
correlation is the next analysis layer, not a runtime requirement for the basic
CLI.
What BPFix Handles
Current diagnostics focus on common verifier failures:
- packet bounds checks
- nullable map value pointers
- uninitialized stack reads
- reference lifetime leaks
- scalar range and variable-offset problems
- pointer type/provenance loss
- verifier-visible compiler lowering artifacts
- verifier precision limits and likely false positives
- verifier complexity and loop limits
- missing kernel/helper/program-type support
- dynptr lifetime and bounds issues
Non-Goals
BPFix is not:
- a kernel patch
- a verifier replacement
- an automatic source-code repair tool
- a semantic correctness checker for accepted BPF programs
It explains why the verifier rejected a program and what proof the developer probably needs to make explicit.
Development
Run tests:
cargo test --workspace
Check the workspace:
cargo check --workspace
Run the same lint gate used by CI:
cargo clippy --workspace --all-targets --all-features -- -D warnings
Format code:
cargo fmt --all
Run a smoke test:
cargo run -p bpfix -- bpfix-empirical/cases/stackoverflow-60053570/replay-verifier.log
Check release packaging:
make release-check
This runs packaging checks, example consistency checks, the empirical corpus
--reject-fallback gate, and the feature-gated object-analysis CLI tests.
Release automation and the manual fallback are documented in
docs/release.md.
bpfix depends on the sibling bpfanalysis crate; the publish workflow selects
the next unpublished patch version, publishes bpfanalysis first, waits for it
to appear in the crates.io index, and then publishes bpfix.
Repository Layout
bpfix-empirical/ replayable verifier-failure corpus and raw examples
crates/bpfanalysis Rust analysis library
crates/bpfix user-facing CLI
docs/project-status.md project planning and evaluation notes
docs/user-guide.md install, getting logs, output, and CI usage
docs/open-source-tool-design.md public CLI contract
docs/release.md release gates and crates.io publishing process
examples/ bpftool, libbpf, Aya, BCC, CI, and editor integration snippets
docs/evaluation/ evaluation and metric notes
bpfix-empirical/tools/ empirical replay and corpus-maintenance tools
vendor/libbpf/ libbpf submodule
No comments yet
Be the first to share your take.