This document describes where every kernel/distro image used by bpfcompat
comes from, how it is cached and integrity-checked, and how to add a new
profile. The goal is that anyone can reproduce a compatibility matrix from
scratch — or verify exactly which image bytes produced a past result.
bpfcompat does not build custom OS images. Profiles reference
unmodified upstream vendor cloud images (Ubuntu cloud images, Debian cloud
images, AlmaLinux/Rocky GenericCloud, Amazon Linux KVM images, Oracle UEK
images, openSUSE Leap, and so on). This is deliberate:
- Distro kernels carry vendor patches and backports that diverge from upstream — testing the vendor's own published image tests the kernel your users actually run, which custom-built images cannot do.
- Every image is independently obtainable from the vendor: there is no private build step between "what the vendor ships" and "what was tested".
The trade-off is that vendor current/ URLs mutate over time. The
mechanisms below exist to make that explicit and controllable.
Each target is one YAML file under vm/profiles/:
id: ubuntu-22.04-5.15
distro: ubuntu
version: "22.04"
kernel_family: "5.15"
arch: x86_64
image:
source_url: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
local_path: "vm/cache/ubuntu-22.04.qcow2"
# Optional: pin the exact image. Requires a release-versioned source_url;
# "current" URLs mutate and will eventually fail the pin (by design).
# sha256: "<digest>"
boot:
memory_mb: 1024
cpus: 1Matrices (matrices/*.yaml) select sets of profile ids; the tiering
rationale is in profile-catalog.md.
- Recording (always): an image is rehashed before every run and its sha256
is recorded in a
<image>.sha256sidecar next to the file invm/cache/. The sidecar is evidence, not a trust source; bpfcompat never skips hashing based on its contents. Every VM target's report notes includebase image sha256: …, so any past matrix result is attributable to exact image bytes even when the vendor URL has since changed. - Pinning (opt-in): setting
image.sha256in a profile makes a mismatching download or cached file fail the run instead of silently testing different bytes. Use pinning with release-versioned URLs (for examplereleases/22.04/release-20260601/...instead ofcurrent/).
Images download on first use into vm/cache/ (never committed). Downloads
honor run cancellation, use a 45-minute HTTP timeout, reject responses larger
than 20 GiB, and remove partial output after any failure. Bulk prefetch targets:
make vm-images # MVP matrix images
make vm-images-extended # extended catalog
make vm-images-tier1 # enterprise/cloud tier
make vm-images-expanded-2026 # full expanded campaign
make vm-images-expanded-2026-dry-runManual/licensed images (SLES, Bottlerocket, Talos, mainline archives) have no public URL; import them explicitly:
make import-required-images SLES156_IMG=/abs/path/sles15.6.qcow2
make manual-image-checkmake profile-catalog-audit— validates every profile's source URL is still reachable and records audit evidence.make matrix-readiness— reports which matrix profiles are runnable right now (image cached + executor transport supported)..github/workflows/profile-catalog-maintenance.yml— runs the audit on a schedule so dead vendor URLs surface as CI signal, not as a failed run.go test ./internal/vm -run TestAllProfileYAMLLoadAndValidate— validates profile YAML integrity.
A cached image keeps producing matrix evidence for the kernel it shipped
with, even after the distro has moved on. The freshness oracle makes that
drift visible by comparing each profile's last-validated kernel release
against the per-distro kernel inventory that
falcosecurity/kernel-crawler
publishes weekly (per-arch list.json):
./bin/bpfcompat kernel-freshness # download inventory, print table
./bin/bpfcompat kernel-freshness --fail-on-stale # exit 2 when evidence is behindvm/kernel-baselines.yaml(committed) records the last-validated kernel per profile plus its crawler mapping: distro key, target flavor (ubuntu-genericvsubuntu-kvm), arelease_prefixpinning the series, and an optionalrelease_containsfor distros that mix major releases under one key (el9vsel10).- After a matrix run, refresh the baselines from the report:
bpfcompat kernel-freshness --update-from-report reports/<run>.json. Profiles the file doesn't know yet are appended without a mapping and show up asuncovereduntil one is added. .github/workflows/kernel-freshness.ymlruns the comparison weekly (after kernel-crawler's own Monday refresh) as a non-blocking signal lane. Stale profiles keep the run green and file (or refresh) a single "Stale kernel baselines" tracking issue that closes itself once everything is fresh again; a red run is reserved for the oracle itself breaking (crawler unreachable, invalid baselines file). Never a blocked merge either way.- Statuses are honest about coverage limits:
uncovered(kernel-crawler publishes no Debian entries, for example),no-entries(EOL series the archive dropped), andno-kernel(profile never validated) are reported distinctly fromstale. coveredis the family-aware status. A stock cloud image can never ship a kernel newer than itself, while the crawler tracks the newest package, so an image-based profile would otherwise be flaggedstaleforever through no fault of ours. When a kernel-sweep profile in the same family (<base>-k<release>) has already installed and validated the newest kernel, the base profile reportscovered— naming the profile that proves it — and is not counted as stale. Sibling variants stay independent:ubuntu-22.04-5.15-lockdownis only covered by its own…-lockdown-k…derivatives, never by plainubuntu-22.04-5.15ones.
The crawler indexes header packages, not bootable images, so it serves as a freshness oracle only — the boot substrate stays unmodified vendor cloud images.
Two lanes construct their environment at run time instead of downloading a distro image:
- virtme-ng upstream lane:
make upstream-kernel-runnablequeries kernel.org for current mainline/RC/LTS releases and generates profiles (matrices/upstream-kernel-runnable.yaml); virtme-ng builds/boots the kernel directly. Reproducibility comes from the recorded kernel release context, not an image digest. - Firecracker lane:
make firecracker-runnablegenerates profiles around a host-local uncompressed kernel plus a busybox initramfs that bpfcompat assembles on the fly (seefirecracker-backend.md). The validator and artifact are injected into the generated initramfs; nothing opaque is downloaded.
A cloud image boots exactly one kernel release, so image-per-profile
matrices sample a kernel series at one point. The sweep lane densifies
that: a generated profile sets install_kernel plus kernel_packages
(direct archive-pool .deb URLs), and the executor downloads and installs
that exact release inside the guest (dpkg), pins it as the grub default,
reboots into it (QEMU runs with -no-reboot, so the reboot exits the
first QEMU process and the executor relaunches it on the same overlay),
verifies uname -r, and only then validates. The overlay is grown +4G
before boot — cloud images ship near-full rootfs, and cloud-init's
growpart expands the partition to fit the kernel install.
Direct pool URLs matter: package indexes only carry the current ABI, so
superseded kernel releases remain downloadable in the archive pool but are
invisible to apt-get install / dnf install <name>. The kernel-crawler
inventory records pool URLs per release, which is exactly what makes dense
sweeps possible — the generator derives the real package URLs from the
crawler's headers URLs. The base image stays an unmodified vendor image;
the kernel packages come from the distro's own archive.
The derivation differs per family. Ubuntu's headers .deb sits in the same
pool directory as the image and modules .debs, so only the file name
changes. RHEL-family crawler entries publish kernel-devel from
AppStream, while the packages that actually boot (kernel-core plus
modules) live in BaseOS, so the component is swapped too — the mirror,
repository version, architecture and any per-letter subdirectory
(Rocky's Packages/k/) are inherited from the headers URL.
Generate the profiles and matrix from the crawler inventory:
./bin/bpfcompat kernel-sweep --profile ubuntu-22.04-5.15 --count 4
./bin/bpfcompat test --artifact app.bpf.o \
--matrix matrices/kernel-sweep-ubuntu-22.04-5.15.yaml \
--out reports/sweep.json --timeout 20mThis writes vm/profiles/<base>-k<release>.yaml per release (newest
first) plus matrices/kernel-sweep-<base>.yaml. Targets need generous
timeouts: the package download and reboot add minutes per kernel.
The crawler mapping (distro key, target flavor, release prefix, and the
release_contains that separates el9 from el10) is read from
vm/kernel-baselines.yaml for the base profile, so a sweep of
ubuntu-22.04-minimal-5.15 correctly uses the ubuntu-kvm flavor rather
than ubuntu-generic. --target and --series override it.
Supported families: Debian (apt/dpkg, boot selected via grub menu
titles) and RHEL — AlmaLinux, Rocky, CentOS Stream — (dnf, boot
selected with grubby --set-default, which matches on the vmlinuz path
rather than a menu string). Deliberately unsupported: Oracle Linux (UEK
ships from its own repositories, not BaseOS), Amazon Linux (kernels are
not published in a browsable pool the crawler maps to), and the
immutable/image-based systems (RHCOS, Fedora CoreOS, Flatcar, Bottlerocket,
Talos), whose kernels are part of the image rather than packages.
- Find the vendor's cloud image URL — prefer a release-versioned URL over
current/. - Create
vm/profiles/<id>.yamlwithid,distro,version,kernel_family,arch,image.source_url,image.local_path(undervm/cache/), and boot resources. Addimage.sha256if the URL is release-versioned. - Run
go test ./internal/vm -run TestAllProfileYAMLLoadAndValidate. - Add the profile id to the appropriate matrix in
matrices/and tier inprofile-catalog.md. - Run
make profile-catalog-auditand a single-profile smoke:./bin/bpfcompat test --artifact examples/simple-pass/simple_pass.bpf.o --matrix <your-matrix> ....
Cloud-init differences are handled by profile fields (SSH user candidates, NoCloud seed delivery mode); see existing RHEL/Amazon Linux profiles for non-default examples.
- No multi-version pinned catalog yet: profiles track one URL each, so
pinning everything to release-versioned URLs needs a refresh routine
(candidate: extend
profile-catalog-maintenance.ymlto propose pin bumps as PRs). - The freshness oracle flags profiles whose evidence is behind, but
refreshing is still manual (re-download image, re-run matrix,
--update-from-report). A follow-up could automate that as a proposed PR. - Some cataloged profiles (Talos, Bottlerocket, Flatcar, Amazon Linux 2 with 4.14) are not runnable on the current SSH/cloud-init executor and are marked non-blocking in matrices.
- ARM64 image entries exist but need a native ARM64 KVM runner.