Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./...
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
87 changes: 87 additions & 0 deletions docs/ebpf-go-validation.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions examples/ebpf-go-loader/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
4 changes: 4 additions & 0 deletions examples/ebpf-go-loader/go.sum
Original file line number Diff line number Diff line change
@@ -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=
74 changes: 74 additions & 0 deletions examples/ebpf-go-loader/main.go
Original file line number Diff line number Diff line change
@@ -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 <object.bpf.o> (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
}
Loading