From 79c0f40841f81d517fa4841dabe40a23599bdd09 Mon Sep 17 00:00:00 2001 From: Mostafa Moradian Date: Wed, 23 Sep 2026 10:49:40 +0200 Subject: [PATCH 1/4] refactor: Confine platform-only helpers to their build tags Several helpers in shared files are reachable only from Windows code, so a Linux build sees them as dead. Move containsEventTypes and containsFieldMatch next to their only caller in compiler_windows.go, and isNumber next to the field definitions that validate with it. Drop the Linux framePID and threadpool accessor stubs, which nothing calls now that callstack and threadpool fields are Windows-only. event.PID is an alias for the same integer the params accessor already returns on both platforms, so the conversions around MustGetPid never converted anything. This makes the packages the Linux port owns lint clean, which the next commit turns into a CI gate. --- pkg/filter/accessor_stubs_linux.go | 11 +++-- pkg/filter/fields/defs_windows.go | 11 +++++ pkg/filter/fields/fields.go | 12 ------ pkg/filter/util_linux.go | 28 ------------ pkg/rules/compiler.go | 67 ----------------------------- pkg/rules/compiler_windows.go | 69 ++++++++++++++++++++++++++++++ pkg/rules/engine_linux_test.go | 7 +++ pkg/rules/sequence.go | 8 ++-- 8 files changed, 96 insertions(+), 117 deletions(-) delete mode 100644 pkg/filter/util_linux.go diff --git a/pkg/filter/accessor_stubs_linux.go b/pkg/filter/accessor_stubs_linux.go index f9e2166e7..298f22b92 100644 --- a/pkg/filter/accessor_stubs_linux.go +++ b/pkg/filter/accessor_stubs_linux.go @@ -35,9 +35,8 @@ func (noopAccessor) SetFields([]Field) {} func (noopAccessor) SetSegments([]fields.Segment) {} func (noopAccessor) IsFieldAccessible(*event.Event) bool { return false } -func newModuleAccessor() Accessor { return noopAccessor{} } -func newRegistryAccessor() Accessor { return noopAccessor{} } -func newHandleAccessor() Accessor { return noopAccessor{} } -func newPEAccessor() Accessor { return noopAccessor{} } -func newDNSAccessor() Accessor { return noopAccessor{} } -func newThreadpoolAccessor() Accessor { return noopAccessor{} } +func newModuleAccessor() Accessor { return noopAccessor{} } +func newRegistryAccessor() Accessor { return noopAccessor{} } +func newHandleAccessor() Accessor { return noopAccessor{} } +func newPEAccessor() Accessor { return noopAccessor{} } +func newDNSAccessor() Accessor { return noopAccessor{} } diff --git a/pkg/filter/fields/defs_windows.go b/pkg/filter/fields/defs_windows.go index 3647ac5e7..1e48cc2d8 100644 --- a/pkg/filter/fields/defs_windows.go +++ b/pkg/filter/fields/defs_windows.go @@ -29,6 +29,17 @@ import ( func platformString() params.Type { return params.UnicodeString } func platformAnsiString() params.Type { return params.AnsiString } +// isNumber is the field argument validation function that +// returns true if all characters are digits. +var isNumber = func(s string) bool { + for _, c := range s { + if !unicode.IsNumber(c) { + return false + } + } + return true +} + var fields = platformFields(map[Field]FieldInfo{ EvtPID: {EvtPID, "process identifier generating the event", params.Uint32, []string{"evt.pid = 6"}, nil, nil}, EvtTID: {EvtTID, "thread identifier generating the event", params.Uint32, []string{"evt.tid = 1024"}, nil, nil}, diff --git a/pkg/filter/fields/fields.go b/pkg/filter/fields/fields.go index 56c920a89..272a32a12 100644 --- a/pkg/filter/fields/fields.go +++ b/pkg/filter/fields/fields.go @@ -21,7 +21,6 @@ package fields import ( "github.com/rabbitstack/fibratus/pkg/event/params" "sort" - "unicode" ) // FieldInfo is the field metadata descriptor. @@ -34,17 +33,6 @@ type FieldInfo struct { Argument *Argument } -// isNumber is the field argument validation function that -// returns true if all characters are digits. -var isNumber = func(s string) bool { - for _, c := range s { - if !unicode.IsNumber(c) { - return false - } - } - return true -} - // Argument defines field argument information. type Argument struct { // Optional indicates if the argument is optional. diff --git a/pkg/filter/util_linux.go b/pkg/filter/util_linux.go deleted file mode 100644 index 752ccdb36..000000000 --- a/pkg/filter/util_linux.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build linux - -/* - * Copyright 2026 by Mostafa Moradian - * https://www.fibratus.io - * All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package filter - -import "github.com/rabbitstack/fibratus/pkg/event" - -// framePID returns the process identifier associated with the event. -func framePID(e *event.Event) event.PID { - return e.PID -} diff --git a/pkg/rules/compiler.go b/pkg/rules/compiler.go index 151ec6000..c904db2fe 100644 --- a/pkg/rules/compiler.go +++ b/pkg/rules/compiler.go @@ -21,8 +21,6 @@ package rules import ( "expvar" "fmt" - "slices" - "strings" semver "github.com/hashicorp/go-version" "github.com/rabbitstack/fibratus/pkg/config" @@ -247,71 +245,6 @@ func (c *compiler) visitApproverPredicates(node ql.Node) { ql.WalkFunc(node, walk) } -func (c *compiler) containsEventTypes(root ql.Node, types ...event.Type) bool { - var contains bool - ql.WalkFunc(root, func(n ql.Node) { - expr, ok := n.(*ql.BinaryExpr) - if !ok { - return - } - lhs, ok := expr.LHS.(*ql.FieldLiteral) - if !ok || lhs.Field != fields.EvtName { - return - } - - vals, ok := rhsToStrings(expr.RHS) - if !ok { - return - } - - evts := make([]event.Type, 0, len(vals)) - for _, v := range vals { - evts = append(evts, event.NameToType(v)) - } - - for _, typ := range types { - if slices.Contains(evts, typ) { - contains = true - return - } - } - }) - return contains -} - -func (c *compiler) containsFieldMatch(root ql.Node, field fields.Field, op ql.Token, val string) bool { - var contains bool - ql.WalkFunc(root, func(n ql.Node) { - expr, ok := n.(*ql.BinaryExpr) - if !ok { - return - } - - lhs, ok := expr.LHS.(*ql.FieldLiteral) - if !ok || lhs.Field != field { - return - } - - if expr.Op != op { - return - } - - values, ok := rhsToStrings(expr.RHS) - if !ok { - return - } - for _, v := range values { - if strings.EqualFold(v, val) { - contains = true - return - } - } - }) - return contains -} - -// isNegated walks up the AST to check if the given node -// is a direct child of a NOT unary expression. func (c *compiler) isNegated(root ql.Node, node ql.Node) bool { negated := false ql.WalkFunc(root, func(n ql.Node) { diff --git a/pkg/rules/compiler_windows.go b/pkg/rules/compiler_windows.go index 8b8320cc2..6a0affa08 100644 --- a/pkg/rules/compiler_windows.go +++ b/pkg/rules/compiler_windows.go @@ -22,6 +22,9 @@ package rules import ( + "slices" + "strings" + "github.com/rabbitstack/fibratus/pkg/config" "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter/fields" @@ -61,3 +64,69 @@ func updatePlatformCompileResult(rs *config.RulesCompileResult, typ event.Type) rs.HasAuditAPIEvents = true } } + +func (c *compiler) containsEventTypes(root ql.Node, types ...event.Type) bool { + var contains bool + ql.WalkFunc(root, func(n ql.Node) { + expr, ok := n.(*ql.BinaryExpr) + if !ok { + return + } + lhs, ok := expr.LHS.(*ql.FieldLiteral) + if !ok || lhs.Field != fields.EvtName { + return + } + + vals, ok := rhsToStrings(expr.RHS) + if !ok { + return + } + + evts := make([]event.Type, 0, len(vals)) + for _, v := range vals { + evts = append(evts, event.NameToType(v)) + } + + for _, typ := range types { + if slices.Contains(evts, typ) { + contains = true + return + } + } + }) + return contains +} + +func (c *compiler) containsFieldMatch(root ql.Node, field fields.Field, op ql.Token, val string) bool { + var contains bool + ql.WalkFunc(root, func(n ql.Node) { + expr, ok := n.(*ql.BinaryExpr) + if !ok { + return + } + + lhs, ok := expr.LHS.(*ql.FieldLiteral) + if !ok || lhs.Field != field { + return + } + + if expr.Op != op { + return + } + + values, ok := rhsToStrings(expr.RHS) + if !ok { + return + } + for _, v := range values { + if strings.EqualFold(v, val) { + contains = true + return + } + } + }) + return contains +} + +// isNegated walks up the AST to check if the given node +// is a direct child of a NOT unary expression. diff --git a/pkg/rules/engine_linux_test.go b/pkg/rules/engine_linux_test.go index 932d75c12..d7ebb224f 100644 --- a/pkg/rules/engine_linux_test.go +++ b/pkg/rules/engine_linux_test.go @@ -237,8 +237,15 @@ func TestLinuxLifecycleSequence(t *testing.T) { Metadata: make(map[event.MetadataKey]any), } + require.Len(t, e.sequences, 1) + ss := e.sequences[0] + require.True(t, ss.isInitialState()) + require.False(t, processEvent(t, e, execve)) + require.False(t, ss.isInitialState(), "the execve stage should have advanced the machine") + require.True(t, processEvent(t, e, connect)) + require.True(t, ss.isInitialState(), "a fired sequence resets for the next match") } func TestSharedSemanticRuleFixtures(t *testing.T) { diff --git a/pkg/rules/sequence.go b/pkg/rules/sequence.go index 58d335696..8564da761 100644 --- a/pkg/rules/sequence.go +++ b/pkg/rules/sequence.go @@ -566,17 +566,17 @@ func (s *sequenceState) expire(e *event.Event) bool { // process spawned by CreateProcess, and it pertains // to the final sequence slot, it is safe to expire // the whole sequence - pid := event.PID(rhs.Params.MustGetPid()) + pid := rhs.Params.MustGetPid() if lhs.IsCreateProcess() && isFinalSlot { - return event.PID(lhs.Params.MustGetPid()) == pid + return lhs.Params.MustGetPid() == pid } if lhs.IsCreateThread() { // if the pids differ, the thread // is created in a remote process. // Sequence can be expired only if // the remote process terminates - if lhs.PID != event.PID(lhs.Params.MustGetPid()) { - return event.PID(lhs.Params.MustGetPid()) == pid + if lhs.PID != lhs.Params.MustGetPid() { + return lhs.Params.MustGetPid() == pid } } return lhs.PID == pid From 8d131bbea3fe3a521f07763729de28054ddf6ae5 Mon Sep 17 00:00:00 2001 From: Mostafa Moradian Date: Wed, 23 Sep 2026 11:07:52 +0200 Subject: [PATCH 2/4] feat(build): Add Linux build, lint, and generation-drift targets check-clang refuses to generate with the wrong clang major. The committed objects are byte-compared in CI and clang records its version in BTF, so generating with a different major rewrites every object and fails the drift check with a diff that looks like a source change. ebpf-drift turns that comparison into something runnable locally rather than a pair of CI steps, and build-linux cross-compiles from the committed objects to prove a checkout needs no clang. lint covers a narrower set than the tests. Packages outside it hold helpers only Windows reaches, so `unused` flags them on every Linux run with nothing to fix on this side. --- Makefile | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 3d82486be..67cc96b7f 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,15 @@ SHELL := /bin/bash .SHELLFLAGS := -euo pipefail -c BPF2GO_VERSION := v0.20.0 +GOLANGCI_LINT_VERSION := v2.9.0 + +# The committed objects are byte-compared in CI, and clang encodes its version +# into BTF, so generating with a different major produces a spurious diff. +CLANG_VERSION := 18 + +# The generated objects target x86-64 and the runtime refuses other +# architectures, so building the binary pins the same one. +TARGET_ARCH ?= amd64 # Specifies a list of build flags TAGS ?= @@ -42,19 +51,42 @@ BPF_TARGETS := $(foreach name,$(BPF_NAMES), \ titlecase = $(shell printf '%s' '$(1)' | awk '{print toupper(substr($$0,1,1)) substr($$0,2)}') .PHONY: ebpf -ebpf: $(BPF_TARGETS) +ebpf: check-clang $(BPF_TARGETS) $(BPF_OUT)/%_bpfel.go $(BPF_OUT)/%_bpfeb.go &: $(BPF_DIR)/c/%.bpf.c $(BPF2GO) $(BPF_FLAGS) -output-stem $* $(call titlecase,$*) $(BPF_DIR)/c/$*.bpf.c -- $(BPF_CFLAGS) +.PHONY: check-clang +check-clang: + @command -v $(CLANG) >/dev/null 2>&1 || { \ + echo "$(CLANG) not found. Generating the eBPF objects needs clang $(CLANG_VERSION)."; \ + exit 1; \ + } + @have=$$($(CLANG) --version | sed -n 's/.*clang version \([0-9]*\).*/\1/p' | head -1); \ + if [ "$$have" != "$(CLANG_VERSION)" ]; then \ + echo "clang $$have found but the committed objects were generated with $(CLANG_VERSION)."; \ + echo "Regenerating with another major rewrites every object and fails ebpf-drift."; \ + echo "Point CLANG at the right binary, e.g. make ebpf CLANG=clang-$(CLANG_VERSION)."; \ + exit 1; \ + fi + +# Regenerating must be reproducible: same sources and same clang, same bytes. +.PHONY: ebpf-drift +ebpf-drift: + $(MAKE) -B ebpf + git diff --exit-code -- $(BPF_OUT) + ifeq ($(strip $(TAGS)),) BUILD_TAGS := else BUILD_TAGS := -tags $(TAGS) endif +# Builds from the committed objects alone, so a fresh checkout needs no clang +# and no kernel headers. Naming the target pair keeps this buildable from a +# developer machine that is neither, which is the cheapest way to prove it. .PHONY: build build: - $(GO) build $(BUILD_TAGS) -o ./cmd/fibratus/fibratus ./cmd/fibratus/ + GOOS=linux GOARCH=$(TARGET_ARCH) $(GO) build $(BUILD_TAGS) -o ./cmd/fibratus/fibratus ./cmd/fibratus/ .PHONY: fmt fmt: @@ -76,6 +108,28 @@ TEST_PKGS := \ test: $(GO) test $(TEST_PKGS) +.PHONY: test-race +test-race: + $(GO) test -race $(TEST_PKGS) + +# Needs a 5.9+ kernel with runtime BTF, and root to load and attach. +.PHONY: test-integration +test-integration: + $(GO) test -tags ebpf_integration -count=1 ./internal/ebpf + +# Narrower than TEST_PKGS on purpose. Packages outside this set carry helpers +# that only Windows reaches, so `unused` reports them on every Linux run and +# there is nothing to fix without moving Windows code around. +LINT_PKGS := \ + ./internal/ebpf/... \ + ./internal/bootstrap/... \ + ./pkg/filter/... \ + ./pkg/rules/... + +.PHONY: lint +lint: + golangci-lint run $(LINT_PKGS) + .PHONY: clean clean: rm -f cmd/fibratus/fibratus From 5dc9c834be16c781f97552200e7e3d0958e20829 Mon Sep 17 00:00:00 2001 From: Mostafa Moradian Date: Wed, 23 Sep 2026 11:12:50 +0200 Subject: [PATCH 3/4] ci: Extend the Linux workflow to lint, race, and a prerequisite probe Build before clang is installed, so the step fails if anything starts depending on generation tools at build time rather than on the committed objects. Probe the runtime contract as its own step. Without it a runner that lost runtime BTF or fell below the kernel floor produces a green suite that never loaded a program, because every test would fail the same way for a reason buried in a wrapped error. Pin clang to the major the objects were generated with, otherwise the drift check reports a diff on every unrelated change. --- .github/workflows/linux-port.yml | 29 ++++++++--- Makefile | 8 +++ .../ebpf/prerequisites_integration_test.go | 49 +++++++++++++++++++ 3 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 internal/ebpf/prerequisites_integration_test.go diff --git a/.github/workflows/linux-port.yml b/.github/workflows/linux-port.yml index 667ef3e8a..adebae1fa 100644 --- a/.github/workflows/linux-port.yml +++ b/.github/workflows/linux-port.yml @@ -12,6 +12,8 @@ concurrency: env: GO_VERSION: 1.26.x + GOLANGCI_LINT_VERSION: v2.9.0 + CLANG_VERSION: 18 jobs: ebpf: @@ -28,22 +30,37 @@ jobs: uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} + # Deliberately before clang is installed: a checkout must build from the + # committed objects alone. + - name: Build without generation tools + run: make - name: Install clang run: | sudo apt-get update - sudo apt-get install -y clang llvm - clang --version + sudo apt-get install -y clang-${{ env.CLANG_VERSION }} llvm-${{ env.CLANG_VERSION }} + clang-${{ env.CLANG_VERSION }} --version + # The versioned packages ship no unversioned symlinks, so name both tools. - name: Check generation drift + run: make ebpf-drift CLANG=clang-${{ env.CLANG_VERSION }} LLVM_STRIP=llvm-strip-${{ env.CLANG_VERSION }} + - name: Install golangci-lint run: | - make -B ebpf - git diff --exit-code -- internal/ebpf/bpf + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${{ env.GOLANGCI_LINT_VERSION }} + - name: Lint + run: | + export PATH=$(go env GOPATH)/bin:$PATH + make lint - name: Unit tests run: make test + - name: Race tests + run: make test-race - name: Validate rules run: | - make build ./cmd/fibratus/fibratus rules validate \ --filters.rules.from-paths="rules/linux/*.yml" \ --filters.macros.from-paths="rules/linux/macros/*.yml" + # Fails, rather than skips, when the runner cannot host the backend, so a + # green integration run always means programs actually loaded. + - name: Probe eBPF prerequisites + run: sudo -E env "PATH=$PATH" go test -tags ebpf_integration -count=1 -run TestPrerequisitesAreMet -v ./internal/ebpf - name: Privileged process source - run: sudo -E env "PATH=$PATH" go test -tags ebpf_integration -count=1 ./internal/ebpf + run: sudo -E env "PATH=$PATH" make test-integration diff --git a/Makefile b/Makefile index 67cc96b7f..2abb04bdd 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ BPF_DIR := internal/ebpf BPF_OUT := $(BPF_DIR)/bpf CLANG ?= clang +LLVM_STRIP ?= llvm-strip GO ?= go GOFMT ?= gofmt BPF2GO ?= go run github.com/cilium/ebpf/cmd/bpf2go@$(BPF2GO_VERSION) @@ -29,6 +30,7 @@ BPF_FLAGS := \ -go-package bpf \ -output-dir $(BPF_OUT) \ -cc $(CLANG) \ + -strip $(LLVM_STRIP) \ -target bpfel,bpfeb \ -tags linux @@ -61,6 +63,12 @@ check-clang: echo "$(CLANG) not found. Generating the eBPF objects needs clang $(CLANG_VERSION)."; \ exit 1; \ } + @command -v $(LLVM_STRIP) >/dev/null 2>&1 || { \ + echo "$(LLVM_STRIP) not found. Versioned llvm packages ship llvm-strip-$(CLANG_VERSION)"; \ + echo "without the unversioned name bpf2go looks for."; \ + echo "Point LLVM_STRIP at it, e.g. make ebpf LLVM_STRIP=llvm-strip-$(CLANG_VERSION)."; \ + exit 1; \ + } @have=$$($(CLANG) --version | sed -n 's/.*clang version \([0-9]*\).*/\1/p' | head -1); \ if [ "$$have" != "$(CLANG_VERSION)" ]; then \ echo "clang $$have found but the committed objects were generated with $(CLANG_VERSION)."; \ diff --git a/internal/ebpf/prerequisites_integration_test.go b/internal/ebpf/prerequisites_integration_test.go new file mode 100644 index 000000000..3a7a8e755 --- /dev/null +++ b/internal/ebpf/prerequisites_integration_test.go @@ -0,0 +1,49 @@ +//go:build linux && ebpf_integration + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestPrerequisitesAreMet asserts the host satisfies the runtime contract +// instead of skipping when it does not. A runner that silently loses runtime +// BTF, or drops below the kernel floor, would otherwise report a green suite +// that never loaded a program. It runs the same gate Open does, so the arch +// check is covered too. +func TestPrerequisitesAreMet(t *testing.T) { + report, err := checkRuntimeSupport() + if report != nil { + t.Logf("kernel=%s (ok=%v, floor=%s) btf=%s (ok=%v) ringbuf=%v ringbuf_helper=%v tracing=%v iter=%v", + report.KernelRelease, report.KernelOK, MinKernelVersion, + report.BTFPath, report.BTFOK, + report.RingbufOK, report.RingbufHelper, report.TracingOK, report.IterOK) + } + require.NoError(t, err, "host does not meet the Linux eBPF runtime contract") + require.NotNil(t, report) + require.True(t, report.KernelOK) + require.True(t, report.BTFOK) + require.True(t, report.RingbufOK) + require.True(t, report.TracingOK) + require.True(t, report.IterOK) +} From c7e06f37ffe8d6ed8bee65d1a4e5627c7c2fb099 Mon Sep 17 00:00:00 2001 From: Mostafa Moradian Date: Wed, 23 Sep 2026 11:14:50 +0200 Subject: [PATCH 4/4] docs: Describe the Linux runtime contract and capabilities Covers what the backend refuses to start without, which capabilities replace running as root, and how to read the probe output. Calls out the two cases capabilities cannot fix, kernel lockdown and the pre-5.11 memlock accounting, because both surface as permission errors that look like a missing capability. Documents which attachment warnings are expected, since the optional legacy tracepoints log a permission denial on kernels that refuse a perf link on them and that is not a fault. --- docs/_sidebar.md | 1 + docs/setup/linux.md | 93 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 docs/setup/linux.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 7d893b36c..8e8df8af5 100755 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -2,6 +2,7 @@ * [Installation](setup/installation.md) * [Quick Start](setup/quick-start.md) * [Configuration](setup/configuration.md) + * [Linux](setup/linux.md) * --- * [Architecture](architecture.md) * --- diff --git a/docs/setup/linux.md b/docs/setup/linux.md new file mode 100644 index 000000000..0287c5a01 --- /dev/null +++ b/docs/setup/linux.md @@ -0,0 +1,93 @@ +# Linux + +On Linux, Fibratus collects telemetry through eBPF CO-RE programs attached to syscall and scheduler tracepoints, instead of ETW. The rule engine, filters, actions, and outputs are the same on both platforms. + +## Runtime prerequisites + +These are hard requirements. Fibratus probes each one at startup and refuses to run if any is missing, rather than degrading to partial telemetry. + +| Requirement | Why | +| --- | --- | +| Linux 5.9 or later | BPF task iterators, used to build the initial process tree, appear in 5.9 | +| Runtime kernel BTF at `/sys/kernel/btf/vmlinux` | CO-RE relocations and `iter/task` attachment resolve against the running kernel's own type information | +| `x86_64` | The shipped BPF objects are built for this architecture | +| Ring buffer map support | Events reach userspace through `BPF_MAP_TYPE_RINGBUF` | + +There is no BTFHub download and no embedded build-host BTF. A kernel compiled without `CONFIG_DEBUG_INFO_BTF` cannot run the backend, and no amount of configuration works around it. Most distribution kernels from 5.9 onwards enable it; if `/sys/kernel/btf/vmlinux` is absent, the kernel was built without it. + +Nothing needs to be compiled at install time. The BPF objects are generated ahead of time and committed, so neither clang nor kernel headers are required on the host. + +## Capabilities + +Running as root works. To run unprivileged, grant: + +| Capability | Needed for | +| --- | --- | +| `CAP_BPF` | Loading programs and creating maps | +| `CAP_PERFMON` | Attaching to tracepoints | +| `CAP_SYS_PTRACE` | Reading `/proc//exe` and `/proc//cmdline` of processes owned by other users | +| `CAP_KILL` | Only if a rule uses the `kill` action against processes you do not own | + +`CAP_BPF` and `CAP_PERFMON` were split out of `CAP_SYS_ADMIN` in 5.8. On a kernel that predates the split, or on a distribution that has not adopted it, `CAP_SYS_ADMIN` is the equivalent. + +Two environment-specific cases override all of the above. Under kernel lockdown in `confidentiality` mode, loading tracing programs is refused no matter which capabilities you hold; check with `cat /sys/kernel/security/lockdown`. Below 5.11, BPF memory is charged against `RLIMIT_MEMLOCK`, so a low limit causes load failures that read as permission errors; Fibratus raises the limit at startup, which itself needs privilege. + +## Reading a failed startup + +Fibratus prints the probe result before anything else: + +``` +eBPF prerequisites ok: kernel=6.8.0-40-generic btf=/sys/kernel/btf/vmlinux ringbuf=true iter=true +``` + +When a prerequisite is missing it refuses to start and names every failure at once, so one run tells you everything that needs fixing: + +``` +eBPF prerequisites: kernel 5.4.0-91-generic is below required 5.9; usable kernel BTF required at /sys/kernel/btf/vmlinux +``` + +Failures after that line are attachment problems rather than prerequisite problems. `cannot create bpf perf link: permission denied` on an optional tracepoint is expected and harmless: `fork`, `vfork`, and the legacy `open`, `unlink`, and `rename` syscalls are skipped when the kernel refuses a perf link on them, because libc routes through `clone`, `openat`, `unlinkat`, and `renameat` anyway. The same message on a required tracepoint is fatal and means capabilities or lockdown. + +## Process enrichment + +The initial process tree comes from a single `iter/task` pass over `task_struct`, which is race-free against processes starting during startup. Executable paths and command lines are then filled in best effort from `/proc//exe` and `/proc//cmdline`. + +That second step is allowed to fail. A process that exits between the iterator pass and the procfs read leaves those two fields empty, and Fibratus counts it rather than dropping the process. Enrichment never replaces what the iterator produced, so a failed read cannot corrupt process identity. + +## Metrics + +Counters are exported as expvars and available through `fibratus stats`. + +| Metric | Meaning | +| --- | --- | +| `ebpf.events.processed` | Events decoded from the ring buffer | +| `ebpf.events.excluded` | Events dropped in userspace by config or filters | +| `ebpf.events.unknown` | Records carrying a type this build does not know | +| `ebpf.events.parse.errors` | Malformed ring buffer records | +| `ebpf.ringbuf.drops` | Events the kernel could not queue because the ring buffer was full | +| `ebpf.approver.drops` | Events rejected in the kernel by an approver, before reaching the ring buffer | +| `ebpf.enrichment.miss` | Processes whose procfs enrichment failed | +| `ebpf.startup.pending.queued` | Live events buffered while the baseline scan ran | +| `ebpf.startup.pending.dropped` | Live events discarded because that buffer was full | +| `ebpf.startup.replay.applied` | Buffered events replayed once the baseline completed | +| `ebpf.startup.snapshot.upserts` | Processes recorded by the baseline scan | +| `ebpf.startup.snapshot.late` | Baseline records that arrived after the switch to live dispatch | + +`ebpf.ringbuf.drops` growing steadily is the signal that consumers are not keeping up; it is the one counter worth alerting on. `ebpf.approver.drops` growing is normal and desirable, since it counts work avoided. + +## Building from source + +Ordinary builds consume the committed objects: + +``` +make +``` + +Regenerating them needs clang, pinned to the major the committed objects were built with, because clang records its version in BTF and a different major rewrites every object: + +``` +make ebpf # regenerate +make ebpf-drift # regenerate and fail if the result differs from what is committed +``` + +Other targets: `make test`, `make test-race`, `make lint`, and `make test-integration`, which needs root and a host meeting the prerequisites above.