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
18 changes: 18 additions & 0 deletions .github/workflows/arm64-build-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ jobs:
- name: Build C validator dynamically
run: make validator-dynamic

- name: Build + stage static validator for arm64 (hostload embed)
# uname -m is aarch64 here, so LIB_EMBED_ARCH resolves to arm64 and the
# binary lands at pkg/bpfcompat/validators/bpfcompat-validator-arm64.
run: make pkg-embed-validator

- name: Build embeddable library (hostload, arm64)
run: make lib-hostload

- name: Run hostload library tests (arm64)
run: go test -tags hostload ./pkg/bpfcompat/...

- name: Upload arm64 validator (for multi-arch embed / release)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: bpfcompat-validator-arm64
path: pkg/bpfcompat/validators/bpfcompat-validator-arm64
if-no-files-found: error

- name: Build ARM64 BPF examples
run: make examples-arm64

Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) once a
## [Unreleased]

### Added
- Embeddable library mode (`pkg/bpfcompat`). `ValidateBeforeLoad` /
`ValidateBytes` do a real load of a compiled eBPF object against the local
running kernel — no VM, no network — for use as a pre-load gate (e.g.
bpfman); `Validate` exposes the full VM matrix engine. Host-kernel loading is
gated behind the `hostload` build tag (default builds return
`ErrHostLoadNotEnabled`), the static validator is embedded via `go:embed`
(amd64/arm64; staged by `make pkg-embed-validator`, built by
`make lib-hostload`), and an internal provider seam keeps the public API
stable for a future in-process CGO validator. Pre-1.0 / experimental; see
`pkg/bpfcompat/README.md`.
- Auto-type programs libbpf can't classify by section name: when a program is
left `BPF_PROG_TYPE_UNSPEC` after open because its ELF section name isn't one
libbpf recognizes, the validator sets the type the artifact's own loader
Expand Down
20 changes: 19 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ LDFLAGS ?= -X $(VERSION_PKG).Version=$(VERSION) \
-X $(VERSION_PKG).BuildDate=$(BUILD_DATE)
GO_BUILD_FLAGS ?= -trimpath -ldflags '$(LDFLAGS)'

.PHONY: all deps vendor doctor doctor-virtme doctor-firecracker doctor-arm64-kvm firecracker-install firecracker-kernel-install firecracker-runnable firecracker-preflight arm64-kvm-preflight build test test-vendor tidy validator validator-dynamic validator-static examples examples-arm64 oss-examples oss-evidence compatibility-site clean vm-ubuntu-22 vm-ubuntu-22-arm64 vm-images vm-images-tier1 vm-images-extended vm-images-expanded-2026 vm-images-expanded-2026-dry-run vm-images-latest-kernel matrix-runnable matrix-runnable-strict matrix-runnable-keep-manual latest-kernel-runnable upstream-kernel-runnable manual-image-check manual-image-check-strict profile-catalog-audit matrix-readiness runtime-selector-proof runtime-delivery-proof production-runtime-drill beta-tech-check tech-stability production-tech-check acceptance-dev-one acceptance-functional-dev-one acceptance-suite-dev-one acceptance-arm64-smoke acceptance-latest-kernel acceptance-upstream-kernel acceptance-firecracker-dev-one acceptance acceptance-expanded-runnable acceptance-evidence serve azure-provision-vm azure-bootstrap-vm azure-provision-foundation azure-production-boundary-proof azure-configure-tls azure-rotate-registry-secret
.PHONY: all deps vendor doctor doctor-virtme doctor-firecracker doctor-arm64-kvm firecracker-install firecracker-kernel-install firecracker-runnable firecracker-preflight arm64-kvm-preflight build test test-vendor tidy validator validator-dynamic validator-static pkg-embed-validator lib-hostload examples examples-arm64 oss-examples oss-evidence compatibility-site clean vm-ubuntu-22 vm-ubuntu-22-arm64 vm-images vm-images-tier1 vm-images-extended vm-images-expanded-2026 vm-images-expanded-2026-dry-run vm-images-latest-kernel matrix-runnable matrix-runnable-strict matrix-runnable-keep-manual latest-kernel-runnable upstream-kernel-runnable manual-image-check manual-image-check-strict profile-catalog-audit matrix-readiness runtime-selector-proof runtime-delivery-proof production-runtime-drill beta-tech-check tech-stability production-tech-check acceptance-dev-one acceptance-functional-dev-one acceptance-suite-dev-one acceptance-arm64-smoke acceptance-latest-kernel acceptance-upstream-kernel acceptance-firecracker-dev-one acceptance acceptance-expanded-runnable acceptance-evidence serve azure-provision-vm azure-bootstrap-vm azure-provision-foundation azure-production-boundary-proof azure-configure-tls azure-rotate-registry-secret

all: build validator

Expand Down Expand Up @@ -110,6 +110,24 @@ validator-dynamic:
validator-static:
$(MAKE) -C validator/c-libbpf clean all LIBBPF_LINK_MODE=static

# pkg-embed-validator builds the static validator and stages it under
# pkg/bpfcompat/validators/ so the embeddable library (`-tags hostload`) can
# go:embed it. Defaults to the host arch; cross-stage with e.g.
# `make pkg-embed-validator CC=aarch64-linux-gnu-gcc LIB_EMBED_ARCH=arm64`.
LIB_EMBED_ARCH ?= $(shell uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/')
LIB_VALIDATOR_DIR := pkg/bpfcompat/validators

pkg-embed-validator: validator-static
@mkdir -p $(LIB_VALIDATOR_DIR)
cp validator/c-libbpf/bin/bpfcompat-validator \
$(LIB_VALIDATOR_DIR)/bpfcompat-validator-$(LIB_EMBED_ARCH)
@echo "staged static validator for $(LIB_EMBED_ARCH) -> $(LIB_VALIDATOR_DIR)/bpfcompat-validator-$(LIB_EMBED_ARCH)"

# lib-hostload builds the embeddable library with host-kernel loading enabled,
# staging the embedded validator first.
lib-hostload: pkg-embed-validator
$(GO) build -tags hostload ./pkg/bpfcompat/...

BPF_TARGET_ARCH ?= x86
BPF_ARCH_INCLUDE ?= /usr/include/x86_64-linux-gnu

Expand Down
54 changes: 54 additions & 0 deletions examples/libmode/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Command libmode is a tiny driver that exercises the pkg/bpfcompat library
// entry point (ValidateBeforeLoad) end-to-end. Build with -tags hostload and
// run with privilege:
//
// go build -tags hostload -o /tmp/libmode ./examples/libmode
// sudo /tmp/libmode <artifact.bpf.o>
package main

import (
"context"
"fmt"
"os"
"time"

"github.com/kernel-guard/bpfcompat/pkg/bpfcompat"
)

func main() {
// os.Exit only in main, with no pending defers (gocritic exitAfterDefer).
os.Exit(run())
}

func run() int {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: libmode <artifact.bpf.o>")
return 2
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

start := time.Now()
res, err := bpfcompat.ValidateBeforeLoad(ctx, os.Args[1])
elapsed := time.Since(start)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}

fmt.Printf("ValidateBeforeLoad -> OK=%v (%.0fms)\n", res.OK(), float64(elapsed.Microseconds())/1000)
fmt.Printf(" kernel : %s %s btf=%v\n", res.Kernel.Release, res.Kernel.Arch, res.Kernel.BTF)
fmt.Printf(" load : %s errno=%d\n", res.Load.Status, res.Load.Errno)
if !res.OK() {
fmt.Printf(" reason : [%s/%s] %s\n", res.Classification.Code, res.Classification.Confidence, res.Classification.Reason)
if res.Load.LogTail != "" {
fmt.Printf(" verifier-log-tail: %s\n", res.Load.LogTail)
}
}

// Exit code mirrors what a gate would do: 0 loadable, 1 not.
if !res.OK() {
return 1
}
return 0
}
92 changes: 92 additions & 0 deletions pkg/bpfcompat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# pkg/bpfcompat

Embeddable Go API for validating compiled eBPF objects against real Linux
kernels.

There are two entry points, sharing one result type:

| Function | What it does | Boots a VM? | Network? |
|---|---|---|---|
| `ValidateBeforeLoad` / `ValidateBytes` | Real load of an object against the **local running kernel** | No | No |
| `Validate` | Full **matrix** run across N kernel profiles in disposable VMs | Yes | Only for OCI artifacts |

`ValidateBeforeLoad` is the pre-load gate: the node it runs on is the node the
program will load on, so the running kernel *is* the target. It does a real
`bpf()` load (the verifier), not static ELF/BTF inference — strictly more
accurate, and fast enough to sit in front of every load.

## Quick start (pre-load gate)

```go
import "github.com/kernel-guard/bpfcompat/pkg/bpfcompat"

res, err := bpfcompat.ValidateBeforeLoad(ctx, "probe.bpf.o")
if err != nil {
return err
}
if !res.OK() {
return fmt.Errorf("won't load on %s: [%s] %s",
res.Kernel.Release, res.Classification.Code, res.Classification.Reason)
}
// safe to load
```

`ValidateBytes(ctx, obj []byte, ...)` is the same for an object already in
memory (e.g. one you pulled from an OCI store yourself). Nothing in this path
touches the network — suitable for air-gapped environments.

## Build requirements for host-load

Host-kernel loading is gated behind the **`hostload` build tag**. Binaries that
must never load BPF on their own kernel (e.g. a public-facing server) are built
*without* the tag, and `ValidateBeforeLoad` / `ValidateBytes` then return
`ErrHostLoadNotEnabled`.

To enable it:

```sh
make pkg-embed-validator # builds the static validator, stages it for embed
go build -tags hostload ./... # or: make lib-hostload
```

Runtime requirements:

- **Privilege** — loading BPF needs `CAP_BPF`/`CAP_SYS_ADMIN`. The library does
not escalate; the caller must already hold it (bpfman does).
- **No external assets** — the static validator is embedded via `go:embed` and
extracted to a private temp dir per call. `amd64` and `arm64` are supported;
other arches return a clear error unless a validator is supplied via
`WithValidator`.

## Matrix mode

`Validate(ctx, Config)` drives the same engine as the `bpfcompat` CLI and GitHub
Action — one or more kernel profiles, each booted in a disposable VM. It does
**not** load on the host (use `ValidateBeforeLoad` for that). Returns a `Report`
with one `Result` per profile plus the raw on-disk report.

## Options

- `WithMode(LoadOnly | LoadAttach)` — `LoadOnly` (default) runs the verifier
only; `LoadAttach` also attaches to hooks (more invasive).
- `WithFeatureProbe(bool)` — off by default; enables the full kernel-capability
census (slower).
- `WithValidator(path)` — pin a validator binary instead of the embedded one.

## Stability

**Pre-1.0 / experimental.** While the module is `v0.x`, the surface of this
package may change between minor versions. Once it stabilises it will follow
[semantic versioning](https://semver.org/): no breaking changes to exported
identifiers within a major version. Pin a tag if you depend on it.

The internal seam (`validatorProvider`) lets the embed-and-exec implementation
be replaced by an in-process CGO validator later **without** changing this
public API or the `Result` shape — that change would not be breaking.

## Result

`Result` carries `Loadable` (the gate), `Kernel` (release/arch/BTF), `Load`
(status/errno/verifier-log tail), `Attach`, `Capabilities`, a machine-readable
`Classification` (branch on `Code`, don't parse `Reason`), and `RawJSON` for the
full underlying document. `Result.OK()` == `Loadable`.
Loading
Loading