Can my eBPF tool actually run here, and if not, exactly what needs to change?
kfeatures is a pure-Go library that answers this question.
It probes kernel capabilities at runtime and returns actionable diagnostics: not just unsupported, but why and how to fix it.
if err := kfeatures.Check(kfeatures.FeatureBPFLSM, kfeatures.FeatureBTF); err != nil {
var fe *kfeatures.FeatureError
if errors.As(err, &fe) {
log.Fatalf("%s - %s", fe.Feature, fe.Reason)
// Output: BPF LSM - CONFIG_BPF_LSM=y but 'bpf' not in active LSM list; add lsm=...,bpf to kernel boot params
}
}The same answers are available from the CLI for CI/CD gating (semantic exit codes), and from --mcp mode for AI agents (JSON-RPC over stdio, every subcommand exposed as an MCP tool with a discoverable input schema). See CLI.
cilium/ebpf/features answers: "Does this kernel support program type X?"
bpftool feature probe answers: "What BPF features does this kernel have?" (CLI only, not embeddable in Go)
Neither tells you whether your tool can actually run. For example, BPF LSM requires three things simultaneously: CONFIG_BPF_LSM=y in the kernel config, bpf in the active LSM boot parameter list, and the LSM program type supported by the running kernel. cilium/ebpf/features can only check the last one. bpftool can check the first and last, but not the second. Neither provides remediation guidance.
| Capability | cilium/ebpf/features |
bpftool feature probe |
kfeatures |
|---|---|---|---|
| BPF program type probes | ✓ | ✓ | ✓ |
| BPF map type / helper probes | ✓ | ✓ | ✓ † |
BTF availability (/sys/kernel/btf/vmlinux) |
✗ | ✗ * | ✓ |
Kernel config parsing (any CONFIG_*, =y/=m) |
✗ | ✓ | ✓ |
Active LSM list (/sys/kernel/security/lsm) |
✗ | ✗ | ✓ |
| BPF LSM enabled (config + boot params + program type) | ✗ | ✗ | ✓ |
| IMA detection (LSM list + securityfs directory) | ✗ | ✗ | ✓ |
| IMA any measurement active (runtime measurement activity) | ✗ | ✗ | ✓ |
| IMA runtime measurement count (raw count for before/after probes) | ✗ | ✗ | ✓ |
| IMA exec measurement active (fresh-inode exec probe) | ✗ | ✗ | ✓ |
| IMA file-check measurement active (fresh-inode file-open probe) | ✗ | ✗ | ✓ |
| Process capabilities (CAP_BPF, CAP_SYS_ADMIN, CAP_PERFMON) | ✗ | ✗ | ✓ |
| Unprivileged BPF status | ✗ | ✓ | ✓ |
| Mount-state gates (bpffs/tracefs/custom paths via superblock magic) | ✗ | ✗ | ✓ |
ELF requirement extraction (parse .o, derive requirements) |
✗ | ✗ | ✓ |
| ELF static analysis (superseded-helper warnings, CO-RE memory-access classification) | ✗ | ✗ | ✓ |
| Min-kernel-version derivation (helper / prog-type / map-type → minimum kernel) | ✗ | partial | ✓ |
| Composite feature validation | ✗ | ✗ | ✓ |
| Actionable diagnostics (remediation steps) | ✗ | ✗ | ✓ |
| Selective probing (minimize overhead) | ✓ ‡ | ✗ § | ✓ |
| Pure Go, no CGO | ✓ | ✗ | ✓ |
| Usable as a Go library | ✓ | ✗ | ✓ |
* bpftool checks CONFIG_DEBUG_INFO_BTF in the kernel config but does not verify /sys/kernel/btf/vmlinux exists.
† Exposed in kfeatures as parameterized requirements (RequireMapType, RequireProgramHelper) consumed by Check(...).
‡ cilium/ebpf/features is per-function: callers invoke individual probe functions on demand.
§ bpftool feature probe runs the full probe set on every invocation.
Other Go projects (libbpfgo, Tetragon, Falco libs) have some feature detection built in, but none is a standalone reusable library. They are either CGO-dependent, tightly coupled to their parent project, or written in C/C++.
Library:
go get github.com/leodido/kfeaturesCLI binary (Linux amd64 / arm64):
# Replace VERSION (e.g. 0.5.1) and ARCH (amd64 or arm64).
curl -sSLO "https://github.com/leodido/kfeatures/releases/download/v${VERSION}/kfeatures_${VERSION}_linux_${ARCH}.tar.gz"
tar xzf "kfeatures_${VERSION}_linux_${ARCH}.tar.gz"
./kfeatures versionFor supply-chain verification of the binary before extracting, see Verifying releases below.
Validate that required kernel features are available:
import (
"errors"
"log"
"github.com/leodido/kfeatures"
)
if err := kfeatures.Check(kfeatures.FeatureBPFLSM, kfeatures.FeatureBTF); err != nil {
var fe *kfeatures.FeatureError
if errors.As(err, &fe) {
log.Fatalf("kernel not ready: %s - %s", fe.Feature, fe.Reason)
}
}Combine Feature enums with parameterized workload requirements:
import (
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/asm"
"github.com/leodido/kfeatures"
)
err := kfeatures.Check(
kfeatures.FeatureBTF,
kfeatures.RequireProgramType(ebpf.XDP),
kfeatures.RequireMapType(ebpf.Hash),
kfeatures.RequireProgramHelper(ebpf.XDP, asm.FnMapLookupElem),
)Gate on a filesystem mounted at an arbitrary path with the expected superblock magic. Useful when bpffs lives somewhere other than /sys/fs/bpf:
import (
"github.com/leodido/kfeatures"
"golang.org/x/sys/unix"
)
err := kfeatures.Check(
kfeatures.RequireMount("/run/bpf", unix.BPF_FS_MAGIC),
)Magic constants come from golang.org/x/sys/unix (e.g. unix.BPF_FS_MAGIC, unix.TRACEFS_MAGIC, unix.CGROUP2_SUPER_MAGIC).
FeatureGroup packages a set of requirements as a single value you can pass anywhere a Requirement is accepted:
var TracingTool = kfeatures.FeatureGroup{
kfeatures.FeatureBTF,
kfeatures.FeatureKprobeMulti,
kfeatures.RequireProgramType(ebpf.Kprobe),
}
if err := kfeatures.Check(TracingTool); err != nil {
log.Fatal(err)
}Point FromELF at an eBPF .o and get back a FeatureGroup describing its program types, map types, and helper-per-program requirements (directly consumable by Check):
reqs, err := kfeatures.FromELF("./bpf/probe.o")
if err != nil {
log.Fatal(err)
}
if err := kfeatures.Check(reqs); err != nil {
log.Fatalf("kernel cannot run probe.o: %v", err)
}Output is deterministic (deduplicated, stably ordered). Unknown ELF kinds fail closed.
FromELF returns only what Check(...) consumes. ProbeELF is the strict superset: a *ELFProbes snapshot with per-program metadata, map declarations, helper-per-program requirements, advisory warnings (e.g. uses of helpers superseded by safer variants), and (with WithCOREChecks()) a per-program memory-access classification distinguishing context loads, map-value loads, CO-RE-protected loads, and unprotected kernel-direct loads.
| Signal | API / CLI | Notes |
|---|---|---|
| Program, map, and helper requirements | FromELF, ProbeELF.Requirements(), check --from-elf |
FromELF is the minimal Check(...)-compatible projection. |
| Derived minimum kernel version | ProbeELF.MinKernel, ProbeELF.Requirements() |
Computed from helper, program type, and map type introduction versions. |
| Object metadata | ProbeELF, probe bpf |
License, BTF presence, CO-RE relocation count. |
| Per-program / per-map details | ProbeELF, probe bpf --json |
Program type, section/name, instruction count, maps, sizes, max entries. |
| Transport hints | ProbeELF.Transport |
Detects RingBuf and PerfEventArray event-streaming paths. |
| Superseded helper warnings | ProbeELF.Warnings |
Flags helpers with safer replacements such as bpf_probe_read* and bpf_get_current_task. |
| CO-RE memory-access classification | ProbeELFWith(..., WithCOREChecks()), probe bpf --with-core |
Classifies context, map-value, CO-RE-protected, kernel-direct, and uncategorized loads. |
Use FromELF when you only need requirements for Check(...); use ProbeELF / probe bpf when you want a static analysis report. The CO-RE classifier is heuristic and source file/line reporting is best-effort; missing source info does not block requirement extraction.
probes, err := kfeatures.ProbeELFWith("./bpf/probe.o", kfeatures.WithCOREChecks())
if err != nil {
log.Fatal(err)
}
for _, w := range probes.Warnings {
fmt.Println("warn:", w.Message)
}
if err := kfeatures.Check(probes.Requirements()...); err != nil {
log.Fatalf("kernel cannot run probe.o: %v", err)
}Requirements() projects to the same FeatureGroup shape FromELF returns, with the addition of a RequireMinKernel(...) derived from the highest-version helper / program type / map type the object touches. FromELF stays the frozen helper for callers that only need the Check-compatible projection.
Gate on the running kernel's version (parsed from uname -r):
err := kfeatures.Check(
kfeatures.RequireMinKernel(5, 11), // bpf_get_current_task_btf
kfeatures.RequireProgramHelper(ebpf.Kprobe, asm.FnGetCurrentTaskBtf),
)Probes.Requirements() adds this automatically based on the kernel-version snapshot under internal/kernelversions (regenerated weekly from BCC and Linux UAPI; see CONTRIBUTING.md).
Check returns the diagnosis for the first failing feature. To inspect any feature against a single probe snapshot, call Diagnose directly:
sf, _ := kfeatures.Probe()
if !sf.BPFLSMEnabled.Supported {
fmt.Println(sf.Diagnose(kfeatures.FeatureBPFLSM))
// CONFIG_BPF_LSM=y but 'bpf' not in active LSM list; add lsm=...,bpf to kernel boot params
}Probe all features for diagnostics and reporting:
sf, err := kfeatures.Probe()
if err != nil {
log.Fatal(err)
}
fmt.Println(sf)Sample output (truncated):
Kernel: 6.1.0-generic
Program Types:
LSM: yes
kprobe: yes
kprobe.multi: yes
Core:
BTF: yes
Security Subsystems:
BPF LSM enabled: yes
IMA enabled: no
IMA directory: yes
IMA any measurement active: no
Active LSMs: lockdown, capability, yama, apparmor, bpf
Filesystems:
tracefs: yes
bpffs: yes
Individual fields are typed and inspectable programmatically (see SystemFeatures).
Probe only what you need:
sf, err := kfeatures.ProbeWith(
kfeatures.WithProgramTypes(ebpf.LSM, ebpf.Kprobe),
kfeatures.WithSecuritySubsystems(),
kfeatures.WithCapabilities(),
)WithX options select probe scope. They do not define requirements; use Check(...) for gating.
A CLI is included for operator diagnostics, CI/CD gating, and AI-agent integration:
go install github.com/leodido/kfeatures/cmd/kfeatures@latestkfeatures probe # probe live kernel (alias for `probe host`)
kfeatures probe host # probe live kernel
kfeatures probe bpf ./probe.o # probe a compiled eBPF object
kfeatures probe bpf ./probe.o --with-core # add CO-RE memory-access classification
kfeatures probe bpf ./probe.o --requirements # print only the Check-compatible requirement projection
kfeatures check --require bpf-lsm,btf,cap-bpf # exit 0 if met, 1 otherwise
kfeatures check --from-elf ./probe.o # gate on requirements derived from a compiled object
kfeatures check --from-elf ./probe.o --require btf # combine ELF-derived and explicit requirements
kfeatures probe --json # JSON output (any subcommand)
kfeatures config # display kernel configkfeatures check exits 0 when all requirements are met and 1 when any are missing. Drop it into a Helm chart pre-install hook, an init container, or a CI job. With --json the verdict is a parse-friendly object on stdout:
$ kfeatures check --require bpf-lsm,btf --json
{
"ok": false,
"feature": "bpf-lsm",
"reason": "CONFIG_BPF_LSM=y but 'bpf' not in active LSM list; add lsm=...,bpf to kernel boot params"
}
$ echo $?
1Invocation errors (missing required flag, unknown flag, invalid value, unknown subcommand) emit a structured JSON envelope on stderr and exit with a semantic code so wrappers can distinguish "the user invoked us wrong" from "the kernel is missing a feature":
| Exit code | Category | Example |
|---|---|---|
0 |
OK | check passed |
1 |
Runtime | FeatureError, probe failure, missing kernel config |
10 |
Input | missing_required_flag: required flag not provided |
11 |
Input | invalid_flag_value: wrong type or unknown enum |
12 |
Input | unknown_flag |
14 |
Input | unknown_command |
$ kfeatures check --require bogus
{"error":"invalid_flag_value","exit_code":11,"flag":"require","got":"bogus","expected":"feature","command":"kfeatures check","message":"invalid argument \"bogus\" for \"-r, --require\" flag: unknown feature: \"bogus\" (available: …)"}
$ echo $?
11Codes follow the structcli/exitcode categories: input errors 10–19 are agent-fixable, runtime errors 1–9 are operator-fixable.
kfeatures is built to be driven by LLM agents and code-generation tooling without --help scraping.
--jsonschema dumps a JSON Schema describing a command's flags. Use =tree to walk the entire subtree:
$ kfeatures check --jsonschema | jq '.title, (.properties | keys)'
"kfeatures check"
[
"from-elf",
"json",
"require"
]
$ kfeatures --jsonschema=tree | jq 'map(.title) | map(select(test("^kfeatures( probe| check| config| version)?$")))'
[
"kfeatures",
"kfeatures check",
"kfeatures config",
"kfeatures probe",
"kfeatures version"
](--jsonschema=tree walks every node, including cobra-generated help and completion leaves; filter with jq to the ones you care about.)
--mcp turns kfeatures into a Model Context Protocol server over stdio. Each runnable leaf command becomes an MCP tool whose input schema mirrors the cobra flag set; agents introspect via tools/list and invoke via tools/call:
Tools exposed: probe-host, probe-bpf, check, config. The server stays alive across business-outcome errors (a failing check does not terminate the session), and invocation errors flow through the same structured envelope as the CLI. Pure stdlib JSON-RPC inside structcli; no extra heavy SDK dependency.
| Category | Features |
|---|---|
| Program types | LSM, kprobe, kprobe.multi, tracepoint, fentry |
| Core | BTF availability (CO-RE) |
| Security | BPF LSM enabled, IMA enabled, IMA any measurement active, IMA exec measurement active, IMA file-check measurement active, active LSM list |
| Capabilities and runtime gates | CAP_BPF, CAP_SYS_ADMIN, CAP_PERFMON, unprivileged BPF disabled, BPF stats enabled |
| Syscalls | bpf(), perf_event_open() |
| JIT | enabled, hardened, kallsyms, memory limit, CONFIG_BPF_JIT_ALWAYS_ON |
| Filesystems | tracefs, debugfs, securityfs, bpffs (gated tracefs/bpffs checks verify the filesystem is mounted with the expected superblock magic) |
| Custom mount gates | any path + superblock magic via RequireMount |
| Namespaces | initial user namespace, initial PID namespace |
| Parameterized workload requirements | program type, map type, helper-per-program-type, minimum kernel version via requirement items |
| ELF-derived requirements | program/map types and helper-per-program requirements via FromELF; full snapshot (warnings, CO-RE memory-access classification, derived RequireMinKernel) via ProbeELF / ProbeELFWith(WithCOREChecks()) |
| Mitigation context | Spectre v1/v2 vulnerability status |
| Kernel config | CONFIG_BPF_LSM, CONFIG_IMA, CONFIG_DEBUG_INFO_BTF, CONFIG_FPROBE, any CONFIG_* |
Pre-1.0. The public API may change between minor versions; breaking changes are
called out explicitly in CHANGELOG.md. The FromELF contract
(signature, determinism, fail-closed semantics) is frozen; see
CONTRIBUTING.md.
- Linux for runtime probing/checking (uses Linux-specific syscalls and sysfs).
FromELFis parser-only and works on any platform.- Some probes require
CAP_BPForCAP_SYS_ADMIN.
Every release artifact (each platform tarball and the checksums.txt) is
signed with cosign keyless signing
backed by GitHub's OIDC token. Each artifact has a sibling
<artifact>.sigstore.json bundle containing the signature, the signing
certificate (with the workflow identity baked in), and the Rekor
transparency-log inclusion proof.
To verify before extracting (replace VERSION and ARCH):
curl -sSLO "https://github.com/leodido/kfeatures/releases/download/v${VERSION}/kfeatures_${VERSION}_linux_${ARCH}.tar.gz"
curl -sSLO "https://github.com/leodido/kfeatures/releases/download/v${VERSION}/kfeatures_${VERSION}_linux_${ARCH}.tar.gz.sigstore.json"
cosign verify-blob \
--bundle "kfeatures_${VERSION}_linux_${ARCH}.tar.gz.sigstore.json" \
--certificate-identity "https://github.com/leodido/kfeatures/.github/workflows/release.yaml@refs/tags/v${VERSION}" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"kfeatures_${VERSION}_linux_${ARCH}.tar.gz"A successful verification proves that the artifact was produced by the
release.yaml workflow at the tagged revision, signed by GitHub's OIDC
issuer, and is recorded on the public Rekor transparency log. Requires
cosign v2.0+.
See CONTRIBUTING.md for the API model, the feature-addition checklist, and the development workflow.
Apache License 2.0. Project attribution in NOTICE, per Apache 2.0 §4(d).
kfeatures is pure-Go userspace. No kernel source embedded, no cgo, no GPL/LGPL deps. Kernel interaction is uABI only: reads from /proc and /sys, syscalls and constants via golang.org/x/sys/unix (BSD-3-Clause), ELF parsing via github.com/cilium/ebpf (MIT; never calls BPF_PROG_LOAD). The kernel's COPYING carves "user programs that use kernel services by normal system calls" out of GPL: the carve-out ps, ls, and mount rely on.
Apache 2.0 over MIT:
- Patent grant (§3). Probing eBPF, LSM, IMA, namespaces, and Spectre mitigations is patent-adjacent. Apache 2.0 grants an irrevocable patent license with defensive termination. MIT has none.
- Adopter alignment. Cilium, Tetragon, Tracee, Falco, Pixie, and Inspektor Gadget are Apache 2.0. No compatibility review needed.