From 50a1c58de4f0d23ff37ec81b64b0c34312c2f2e7 Mon Sep 17 00:00:00 2001 From: ErenAri Date: Thu, 2 Jul 2026 02:39:56 +0300 Subject: [PATCH] =?UTF-8?q?feat(examples):=20ebpf-go=20validation=20recipe?= =?UTF-8?q?=20=E2=80=94=20loader=20example=20+=20cookbook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ebpf-go (cilium/ebpf) is a separate loader implementation that trails libbpf's feature set, so a libbpf load-pass does not guarantee an ebpf-go load-pass on the same kernel. Projects that ship with ebpf-go should validate through ebpf-go — command mode makes that a one-binary recipe. - examples/ebpf-go-loader: ~50-line static loader (CGO_ENABLED=0) that loads a .bpf.o via ebpf.NewCollection, prints the verifier log on rejection, exit code = verdict. Standalone Go module so cilium/ebpf stays out of the main module's dependency surface. - docs/ebpf-go-validation.md: cookbook (build static, test-command CLI + GitHub Action snippets, real-run matrix, extension notes). - README: recipe linked from the command-mode section + doc map. - ci.yml: compile the standalone example module (root go build ./... does not see it). VERIFIED with a real run (3 VMs, test-command, ringbuf_modern.bpf.o): ubuntu-20.04-5.4 exit 1 ("map events: map create: invalid argument"), almalinux-8-4.18 exit 0, ubuntu-22.04-5.15 exit 0 — the version-lies contrast through the ebpf-go loader path, libbpf phase skipped. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 3 ++ README.md | 6 +++ docs/ebpf-go-validation.md | 87 +++++++++++++++++++++++++++++++++ examples/ebpf-go-loader/go.mod | 10 ++++ examples/ebpf-go-loader/go.sum | 4 ++ examples/ebpf-go-loader/main.go | 74 ++++++++++++++++++++++++++++ 6 files changed, 184 insertions(+) create mode 100644 docs/ebpf-go-validation.md create mode 100644 examples/ebpf-go-loader/go.mod create mode 100644 examples/ebpf-go-loader/go.sum create mode 100644 examples/ebpf-go-loader/main.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20d47d4..17282d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,3 +137,6 @@ jobs: # exercise the Go side so the CI gate stays portable. - name: go build run: go build ./... + # Standalone example module (own go.mod, so `go build ./...` skips it). + - name: go build (examples/ebpf-go-loader) + run: cd examples/ebpf-go-loader && CGO_ENABLED=0 go build ./... diff --git a/README.md b/README.md index cf1fa5c..dc508a0 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,11 @@ shipped binary), `$BPFCOMPAT_ARTIFACT` (a staged `.bpf.o`, if given), and `$BPFCOMPAT_REMOTE_ROOT` exported. See [docs/command-validation.md](docs/command-validation.md). +If your project loads with **ebpf-go** (cilium/ebpf) rather than libbpf, ship an +ebpf-go loader the same way — ebpf-go is a separate loader implementation, so a +libbpf pass does not guarantee an ebpf-go pass. A complete static-loader recipe +lives in [docs/ebpf-go-validation.md](docs/ebpf-go-validation.md). + Point either flow at the **library of known-tricky vendor kernels** — the ones where "version ≠ feature support" bites (ring-buffer boundary, enterprise backports, no-BTF, vendor rebases, variant bands): @@ -645,6 +650,7 @@ User guide — start here: - [`docs/project-compatibility-suite.md`](docs/project-compatibility-suite.md) — suites and collection matrices - [`docs/validator.md`](docs/validator.md) — what the in-guest validator checks - [`docs/command-validation.md`](docs/command-validation.md) — validate via your own loader binary/command (exit-code verdict) +- [`docs/ebpf-go-validation.md`](docs/ebpf-go-validation.md) — validate through ebpf-go (cilium/ebpf): a libbpf pass ≠ an ebpf-go pass - [`docs/kernel-quirk-library.md`](docs/kernel-quirk-library.md) — curated library of known-tricky vendor kernels (version ≠ feature support) - [`docs/profile-catalog.md`](docs/profile-catalog.md) — kernel/distro profiles and image maintenance - [`docs/image-pipeline.md`](docs/image-pipeline.md) — where images come from, integrity, adding profiles diff --git a/docs/ebpf-go-validation.md b/docs/ebpf-go-validation.md new file mode 100644 index 0000000..b1f96db --- /dev/null +++ b/docs/ebpf-go-validation.md @@ -0,0 +1,87 @@ +# Validating with ebpf-go (cilium/ebpf) instead of libbpf + +bpfcompat's bundled validator is built on **libbpf** — the kernel's reference +loader. But if your project loads its objects with +[ebpf-go](https://github.com/cilium/ebpf) (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`](../examples/ebpf-go-loader/main.go) 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: + +```bash +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: + +```yaml +- run: cd examples/ebpf-go-loader && CGO_ENABLED=0 go build -o ebpf-go-loader . +- uses: Kernel-Guard/bpfcompat@v0.2.0 + 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: + +| Kernel | ebpf-go loader verdict | Why | +|---|---|---| +| `ubuntu-20.04-5.4` | ❌ exit 1 — `map events: map create: invalid argument` | ring buffer lands upstream in 5.8 | +| `almalinux-8-4.18` | ✅ exit 0 — loaded 1 program, 1 map | RHEL backports ring buffer onto 4.18 | +| `ubuntu-22.04-5.15` | ✅ exit 0 — loaded 1 program, 1 map | comfortably 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. diff --git a/examples/ebpf-go-loader/go.mod b/examples/ebpf-go-loader/go.mod new file mode 100644 index 0000000..14cecbd --- /dev/null +++ b/examples/ebpf-go-loader/go.mod @@ -0,0 +1,10 @@ +// Standalone module: keeps the example's cilium/ebpf dependency out of the +// main bpfcompat module (and its vulnerability/license scanning surface). +module github.com/kernel-guard/bpfcompat/examples/ebpf-go-loader + +go 1.25.0 + +require ( + github.com/cilium/ebpf v0.22.0 // indirect + golang.org/x/sys v0.43.0 // indirect +) diff --git a/examples/ebpf-go-loader/go.sum b/examples/ebpf-go-loader/go.sum new file mode 100644 index 0000000..02d7cd8 --- /dev/null +++ b/examples/ebpf-go-loader/go.sum @@ -0,0 +1,4 @@ +github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY= +github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/examples/ebpf-go-loader/main.go b/examples/ebpf-go-loader/main.go new file mode 100644 index 0000000..5f7d59b --- /dev/null +++ b/examples/ebpf-go-loader/main.go @@ -0,0 +1,74 @@ +// Command ebpf-go-loader loads a compiled .bpf.o with ebpf-go (cilium/ebpf) +// and exits 0 iff every map and program in the object loads into the kernel. +// +// Why this exists: ebpf-go is libbpf-compatible for the features it supports, +// but it is a separate loader implementation — so a libbpf load-pass does not +// guarantee an ebpf-go load-pass on the same kernel. If your project ships +// with ebpf-go, validate through ebpf-go. Ship this binary into each matrix +// kernel with bpfcompat command mode: +// +// CGO_ENABLED=0 go build -o ebpf-go-loader . +// 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 +// +// This is a standalone Go module so the example's cilium/ebpf dependency does +// not enter the main bpfcompat module. See docs/ebpf-go-validation.md. +package main + +import ( + "errors" + "fmt" + "os" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/rlimit" +) + +func main() { + // os.Exit only in main, with no pending defers. + os.Exit(run()) +} + +func run() int { + path := os.Getenv("BPFCOMPAT_ARTIFACT") + if len(os.Args) > 1 { + path = os.Args[1] + } + if path == "" { + fmt.Fprintln(os.Stderr, "usage: ebpf-go-loader (or set $BPFCOMPAT_ARTIFACT)") + return 2 + } + + // Kernels < 5.11 account BPF map memory against RLIMIT_MEMLOCK. + if err := rlimit.RemoveMemlock(); err != nil { + fmt.Fprintf(os.Stderr, "ebpf-go-loader: remove memlock: %v\n", err) + return 1 + } + + spec, err := ebpf.LoadCollectionSpec(path) + if err != nil { + fmt.Fprintf(os.Stderr, "ebpf-go-loader: parse %s: %v\n", path, err) + return 1 + } + + coll, err := ebpf.NewCollection(spec) + if err != nil { + var verr *ebpf.VerifierError + if errors.As(err, &verr) { + // %+v prints the full verifier log, which lands in the bounded + // stderr tail of the bpfcompat report. + fmt.Fprintf(os.Stderr, "ebpf-go-loader: verifier rejected %s:\n%+v\n", path, verr) + } else { + fmt.Fprintf(os.Stderr, "ebpf-go-loader: load %s: %v\n", path, err) + } + return 1 + } + defer coll.Close() + + fmt.Printf("ebpf-go-loader: loaded %d program(s) and %d map(s) from %s\n", + len(coll.Programs), len(coll.Maps), path) + return 0 +}