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 @@ -35,6 +35,9 @@ jobs:
- name: Test
run: make test TEST_FLAGS="-v -race -count=1"

- name: Coverage gate
run: make cover-check

- name: Setup Bats
id: setup-bats
uses: bats-core/bats-action@5b1e60c2ee94cb1b44a616ea4b1f466f9d6e38ef # v4.0.0
Expand Down
120 changes: 120 additions & 0 deletions .github/workflows/refresh-kernel-versions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: refresh-kernel-versions

# Re-runs kvgen against the latest iovisor/bcc and torvalds/linux HEAD
# commits and opens a PR if the snapshot under internal/kernelversions
# drifted. Kept on its own schedule so the noise of upstream churn does
# not pollute the regular CI signal on main.

on:
schedule:
# Weekly, Monday 06:00 UTC. Cheap enough; UAPI helpers and BCC docs
# change rarely, so most runs will be a no-op.
- cron: "0 6 * * 1"
workflow_dispatch:
inputs:
bcc-commit:
description: "Override iovisor/bcc commit SHA (defaults to HEAD)"
required: false
default: ""
kernel-commit:
description: "Override torvalds/linux commit SHA (defaults to HEAD)"
required: false
default: ""

permissions:
contents: write
pull-requests: write

jobs:
refresh:
name: regenerate kernel-version snapshot
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version: "1.24"

- name: Resolve upstream HEAD commits
id: pins
run: |
set -eu
bcc="${{ inputs.bcc-commit }}"
if [ -z "$bcc" ]; then
bcc=$(git ls-remote https://github.com/iovisor/bcc HEAD | awk '{print $1}')
fi
kernel="${{ inputs.kernel-commit }}"
if [ -z "$kernel" ]; then
kernel=$(git ls-remote https://github.com/torvalds/linux HEAD | awk '{print $1}')
fi
echo "bcc=$bcc" >> "$GITHUB_OUTPUT"
echo "kernel=$kernel" >> "$GITHUB_OUTPUT"
echo "Resolved BCC -> $bcc"
echo "Resolved kernel -> $kernel"

- name: Bump pinned default commits in kvgen
run: |
set -eu
file=internal/kernelversions/cmd/kvgen/main.go
sed -i \
-e 's|^\(\s*defaultBCCCommit\s*=\s*\)"[0-9a-f]\{40\}"|\1"${{ steps.pins.outputs.bcc }}"|' \
-e 's|^\(\s*defaultKernelCommit\s*=\s*\)"[0-9a-f]\{40\}"|\1"${{ steps.pins.outputs.kernel }}"|' \
"$file"
grep -E 'defaultBCCCommit|defaultKernelCommit' "$file"

- name: Regenerate snapshot
run: |
go generate ./internal/kernelversions/...

- name: Verify build still passes
run: |
go vet ./...
go test ./internal/kernelversions/...

- name: Detect drift
id: drift
run: |
if git diff --quiet -- internal/kernelversions; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No snapshot drift; nothing to do."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
git --no-pager diff --stat -- internal/kernelversions
fi

- name: Open PR
if: steps.drift.outputs.changed == 'true'
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
with:
branch: chore/refresh-kernel-versions
delete-branch: true
# Conventional-commit prefix `deps(kernelversions):` lets
# .github/workflows/labeler.yml route the PR into the
# `dependencies` bucket of release notes.
title: "deps(kernelversions): refresh BCC + UAPI snapshot"
commit-message: |
deps(kernelversions): refresh BCC + UAPI snapshot

BCC pin -> ${{ steps.pins.outputs.bcc }}
kernel pin -> ${{ steps.pins.outputs.kernel }}
body: |
Automated refresh of `internal/kernelversions` against the
latest upstream sources.

- **iovisor/bcc**: `${{ steps.pins.outputs.bcc }}`
- **torvalds/linux**: `${{ steps.pins.outputs.kernel }}`

Regenerated via `go generate ./internal/kernelversions/...`.
Verified with `go vet ./...` and the kernelversions test
package; full CI runs on this PR.

Review the diff in `internal/kernelversions/tables.go`.
New helpers / program types / map types appearing in
upstream UAPI but not yet in BCC's table will surface as
cross-validation failures here; in that case, decide
between waiting for BCC to catch up and adding the symbol
to the audited allow-list in
`internal/kernelversions/cmd/kvgen/known_gaps.go`.
labels: |
dependencies
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,26 @@ When you change CLI behaviour, the corresponding bats file must be updated in th

Run locally with `bats test/`. The full suite must stay green before committing.

## Coverage gate (`make cover-check`)

`make cover-check` runs the test suite, captures a profile, and refuses to pass if any file listed in `COVER_FILES` (in the makefile) falls below 90%. Wire any new public/library file into `COVER_FILES` in the same PR that introduces it.

To skip a function from the gate's denominator, put a single line `// coverage:ignore` in its doc comment (works for both `func` declarations and `var foo = func(...)`). Skip only code that genuinely cannot be reached from the unit-test environment; never paper over a real coverage gap with a marker. Always pair the marker with a one-line justification on the line above it.

The checker source lives in `internal/tools/covercheck/main.go`. Do not bypass it by editing `COVER_THRESHOLD` per-file; raise it through tests instead.

## Kernel-version snapshot (`internal/kernelversions`)

The tables under `internal/kernelversions` (helpers, program types, map types → minimum kernel version) are **generated**. Do not hand-edit `source.json` or `tables.go`.

When you need to update them:

- Routine refresh: `.github/workflows/refresh-kernel-versions.yml` runs weekly and opens a PR if upstream BCC / Linux drift produces a different snapshot. Let the bot do it.
- Manual refresh during development: `go generate ./internal/kernelversions/...` (network-bound; fetches at the pinned commits in `internal/kernelversions/cmd/kvgen/main.go`). Override the pins with `--bcc-commit=...` / `--kernel-commit=...` only when reproducing a specific drift.
- Cross-validation failures (a UAPI symbol without a BCC row, or vice versa) are intentional: the generator refuses to emit a partial snapshot. Resolve by either (a) waiting for BCC to catch up, or (b) adding the symbol to `internal/kernelversions/cmd/kvgen/known_gaps.go` with a one-line rationale. Never widen the validator.

The library reads the snapshot through `internal/kernelversions.HelperKernelVersion`, `MapTypeKernelVersion`, `ProgramTypeKernelVersion`. New consumers (e.g. follow-up `Require*` constructors) should go through these helpers; do not import `tables.go` directly.

## Commits and PRs

- Conventional Commits (`feat:`, `fix:`, `docs:`, `test:`, `chore:`,
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `ProbeELF(path)` and `ProbeELFWith(path, opts...)`: parse a compiled eBPF object and return an `*ELFProbes` snapshot describing the program/map types, helper-per-program requirements, and (with `WithCOREChecks()`) a per-program memory-access classification distinguishing context loads, map-value loads, CO-RE-protected loads, and unprotected kernel-direct loads. Useful for verifying an object will load on a target kernel before shipping. `Requirements()` projects the snapshot to a `FeatureGroup` consumable by `Check(...)` (superset of what `FromELF` returns; `FromELF` remains the frozen helper).
- `RequireMinKernel(major, minor)`: parameterized requirement that gates on `uname -r` reporting at least the given kernel version. Composes with the snapshot of helper / program-type / map-type kernel-version metadata derived from BCC's `kernel-versions.md`, so `kfeatures.Check(reqs.Requirements()...)` automatically rejects an object whose helpers were introduced after the running kernel.
- Kernel-version snapshot under `internal/kernelversions`: pure-Go tables of the minimum kernel version that introduced each BPF helper, program type, and map type. Generated by `internal/kernelversions/cmd/kvgen` against pinned `iovisor/bcc` and `torvalds/linux` commits, regenerated automatically on a weekly schedule (`.github/workflows/refresh-kernel-versions.yml`) and re-validated against UAPI on every CI run.
- Superseded-helper warnings: `ELFProbes.Warnings` flags calls to `bpf_probe_read` / `bpf_probe_read_str` (split into `bpf_probe_read_kernel*` / `bpf_probe_read_user*` since 5.5) and `bpf_get_current_task` (subsumed by `bpf_get_current_task_btf` since 5.11). Advisory only; no effect on `Check(...)` verdicts.
- CLI: `kfeatures probe host` is the new canonical name for the live-kernel probe (`kfeatures probe` continues to work as an alias). `kfeatures probe bpf <path.o>` runs the ELF probe on a compiled object and prints the snapshot; `--with-core` enables CO-RE memory-access classification, `--requirements` prints only the requirement projection, `--json` switches both to JSON. Exposed as MCP tools `probe-host` and `probe-bpf`.
- CLI: `kfeatures check --from-elf <path.o>` accepts an ELF object as a requirement source (composes with `--require`); both flags are now optional and at least one must be supplied.
- Releases: every artifact (per-platform tarballs and `checksums.txt`) is now signed with [cosign](https://github.com/sigstore/cosign) keyless signing backed by GitHub's OIDC token. Each artifact has a sibling `<artifact>.sigstore.json` bundle containing the signature, certificate (with the workflow identity baked in), and Rekor transparency-log inclusion proof. Verifying a download is a single `cosign verify-blob --bundle ...` invocation; see the new [Verifying releases](README.md#verifying-releases) section in the README for the exact commands. Requires cosign v2.0+ on the verifier side.
- `NOTICE` file at repo root carrying the `Copyright 2026 Leonardo Di Donato` attribution. Apache 2.0 distinguishes the license text (canonical, verbatim, in `LICENSE`) from project-level attribution (in a `NOTICE` file that downstream consumers must propagate). The previous setup folded the copyright line into `LICENSE` itself; that conflated the two and is one of the deviations that caused licensecheck to mis-classify the file (see corresponding `### Fixed` entry).
- README License section: "Why Apache 2.0" paragraph. Documents the kernel-uABI posture (no kernel source, no cgo, no GPL deps; `/proc` and `/sys` reads fall under the kernel `COPYING` "normal syscalls" carve-out) and the Apache-2.0-over-MIT rationale (patent grant for security-adjacent probing; same-license adopter base of Cilium, Tetragon, Falco, etc.).
Expand Down
35 changes: 30 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ Requirement items consumed by `Check(...)`:
- `FeatureGroup`: reusable preset of requirements (also returned by `FromELF`)
- `RequireProgramType(...)`, `RequireMapType(...)`, `RequireProgramHelper(...)`: parameterized workload requirements
- `RequireMount(path, magic)`: parameterized filesystem-mount gate; magic comes from `golang.org/x/sys/unix` (e.g. `unix.BPF_FS_MAGIC`)
- `RequireMinKernel(major, minor)`: parameterized minimum-kernel-version gate; composes with the helper/prog-type/map-type kernel-version snapshot maintained under `internal/kernelversions`
- `FromELF(path)`: producer of requirement items in the same model (program/map types + helper-per-program requirements)
- `ProbeELF(path)` / `ProbeELFWith(path, opts...)`: full ELF snapshot (`*ELFProbes`) with optional CO-RE classification; `Requirements()` projects to a `FeatureGroup`

`FromELF` is parser-only and available cross-platform; runtime probing/checking
remains Linux-specific.
`FromELF`, `ProbeELF`, and `ProbeELFWith` are parser-only and available cross-platform; runtime probing/checking remains Linux-specific.

## Feature-addition review checklist

Expand Down Expand Up @@ -54,6 +55,20 @@ The `FromELF` API is frozen against the following contract:

Changes to any of these points require explicit discussion in the PR and a CHANGELOG entry under a new minor version.

`ProbeELF` is the strict superset: it returns a richer `*ELFProbes` snapshot (warnings, memory-access summaries, CO-RE classification when opted in) and lets callers project to the same `FeatureGroup` shape via `Requirements()`. New extraction surface (additional warning rules, additional CO-RE classifications) belongs on `ProbeELF`; `FromELF` stays frozen against the four contract points above.

## Kernel-version snapshot (`internal/kernelversions`)

The helper / program-type / map-type minimum-kernel-version tables are generated, not hand-edited. The generator (`internal/kernelversions/cmd/kvgen`) parses BCC's `docs/kernel-versions.md` and Linux UAPI `include/uapi/linux/bpf.h` at pinned commits, cross-validates that every `BPF_FUNC_*` / `BPF_PROG_TYPE_*` / `BPF_MAP_TYPE_*` enum value in UAPI has a corresponding row in the BCC table, and emits `tables.go`.

Workflow:

- **Routine refresh**: `.github/workflows/refresh-kernel-versions.yml` runs weekly, resolves upstream HEAD for both repos, rewrites the `defaultBCCCommit` / `defaultKernelCommit` constants in `cmd/kvgen/main.go`, regenerates the snapshot, and opens a PR labelled `dependencies` if the output drifted.
- **Manual refresh**: `go generate ./internal/kernelversions/...` from a clean checkout.
- **Cross-validation failure**: when UAPI ships a new symbol before BCC documents it (or vice versa), the generator returns an error. Decide between waiting for BCC to catch up and adding the symbol to the audited allow-list in `internal/kernelversions/cmd/kvgen/known_gaps.go` with a one-line rationale; never silence the validator wholesale.

Do not commit hand-edited changes to `tables.go`. The auto-refresh PR is the only sanctioned path.

## CLI conventions

The `cmd/kfeatures` binary is built on [structcli](https://github.com/leodido/structcli) (>= v0.17.0). The patterns below are invariants: PRs that break them need explicit discussion in the description.
Expand Down Expand Up @@ -107,11 +122,21 @@ When introducing a new subcommand, default to exposing it. Only exclude after th
## Development workflow

```bash
make test # unit tests
make lint # go vet + golangci-lint
make build # build the CLI
make test # unit tests
make lint # go vet + golangci-lint
make build # build the CLI
make cover # produce coverage.out
make cover-check # enforce per-file coverage threshold
```

### Coverage gate

`make cover-check` runs the test suite with `-coverprofile=coverage.out` and then runs `internal/tools/covercheck` against that profile, failing if any gated source file falls below `COVER_THRESHOLD` (default `90`). The list of gated files lives in the `COVER_FILES` makefile variable.

The checker honours a single-line `// coverage:ignore` marker placed in the doc comment of a function declaration (or of a `var foo = func(...)` declaration). The marker excludes every statement attributed to that function from both the numerator and the denominator. Use it sparingly and only for code that is genuinely impossible to cover from the unit-test environment (network-bound `main` entrypoints, disk-bound wrappers like `ProbeELFWith` whose branches are exercised through programmatic fixtures against the inner helper). Every marker should carry a one-line justification immediately above it.

When you add a new feature file, append it to `COVER_FILES` in the makefile and bring it up to threshold in the same PR. Internal tools (`internal/tools/covercheck`, `internal/kernelversions/cmd/kvgen`) are intentionally excluded from the gate: their happy paths are exercised by the scheduled refresh workflow against live network data.

Integration tests (real `unix.Statfs` / `unix.Mount`) are gated behind a build
tag and a dedicated CI job:

Expand Down
Loading
Loading