Validating with ebpf-go (cilium/ebpf) instead of libbpf

July 16, 2026 · View on GitHub

bpfcompat's bundled validator is built on libbpf — the kernel's reference loader. But if your project loads its objects with ebpf-go (most Go eBPF projects do), that verdict has a gap: ebpf-go is libbpf-compatible for the features it supports, uses libbpf as its reference implementation, and by its own documentation trails libbpf's feature set. It is a separate loader implementation, so:

A libbpf load-pass does not guarantee an ebpf-go load-pass on the same kernel — and vice versa.

If you ship with ebpf-go, validate through ebpf-go. Command mode makes this a one-binary recipe: build a tiny static Go loader, ship it into each matrix kernel, and the per-kernel verdict is your loader's exit code.

The loader

examples/ebpf-go-loader is a complete, copyable implementation (~50 lines): parse the object, load every map and program via ebpf.NewCollection, print the verifier log on rejection, exit 0/1. It reads the object path from $BPFCOMPAT_ARTIFACT (set by bpfcompat inside the guest) or argv[1].

It is a standalone Go module, so its cilium/ebpf dependency stays out of the main bpfcompat module. Extend it with your project's real invariants — attach the programs, poke a map, run your feature probes — the exit code is the contract.

Build it static, run it everywhere

Go with CGO_ENABLED=0 produces a fully static binary — exactly what command mode wants, since the disposable guests have no Go toolchain and varying libc versions:

cd examples/ebpf-go-loader
CGO_ENABLED=0 go build -o ebpf-go-loader .

# Run it across the library of known-tricky vendor kernels:
bpfcompat test-command \
  --cmd '$BPFCOMPAT_BIN $BPFCOMPAT_ARTIFACT' \
  --bin ./ebpf-go-loader \
  --artifact ./your_object.bpf.o \
  --matrix matrices/quirk-library.yaml \
  --out report.json

Or in CI with the GitHub Action:

- run: cd examples/ebpf-go-loader && CGO_ENABLED=0 go build -o ebpf-go-loader .
- uses: Kernel-Guard/bpfcompat@v0.3.2
  with:
    command: $BPFCOMPAT_BIN $BPFCOMPAT_ARTIFACT
    command-binary: examples/ebpf-go-loader/ebpf-go-loader
    artifact: your_object.bpf.o
    matrix: quirk-library
    out: reports/bpfcompat.json

What a real run looks like

Shipping this loader with a ring-buffer object (examples/ringbuf-modern/ringbuf_modern.bpf.o) across the version-lies contrast trio:

Kernelebpf-go loader verdictWhy
ubuntu-20.04-5.4❌ exit 1 — map events: map create: invalid argumentring buffer lands upstream in 5.8
almalinux-8-4.18✅ exit 0 — loaded 1 program, 1 mapRHEL backports ring buffer onto 4.18
ubuntu-22.04-5.15✅ exit 0 — loaded 1 program, 1 mapcomfortably past the boundary

Same object, three kernels, and the lower-numbered enterprise kernel passes while the higher-numbered upstream one fails — through the loader your users actually run. The libbpf load/attach phase reports skipped; the verdict is entirely ebpf-go's.

Notes

  • Keep the loader in your repo, built from your go.mod — the point is to validate your ebpf-go version and load options, not ours.
  • ebpf-go needs kernels ≥ 4.9 (and the example calls rlimit.RemoveMemlock() for pre-5.11 map accounting).
  • For richer checks (attach, map round-trip, feature probes à la features.HaveMapType), grow the loader; command mode only cares about the exit code.