Skip to content

feat(linux): Initial Linux support - #739

Merged
rabbitstack merged 65 commits into
masterfrom
linux-port
Sep 28, 2026
Merged

rabbitstack merged 65 commits into
masterfrom
linux-port

Conversation

@rabbitstack

@rabbitstack rabbitstack commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

What is the purpose of this PR / why it is needed?

This PR brings the initial Linux runtime detection capabilities, with support for file system, process, memory and network telemetry.

Kudos to @mostafa for the hard work.

What type of change does this PR introduce?


Uncomment one or more /kind <> lines:

/kind feature (non-breaking change which adds functionality)

/kind bug-fix (non-breaking change which fixes an issue)

/kind refactor (non-breaking change that restructures the code, while not changing the original functionality)

/kind breaking (fix or feature that would cause existing functionality to not work as expected

/kind cleanup

/kind improvement

/kind design

/kind documentation

/kind other (change that doesn't pertain to any of the above categories)

Any specific area of the project related to this PR?


Uncomment one or more /area <> lines:

/area instrumentation

/area telemetry

/area rule-engine

/area filters

/area yara

/area event

/area captures

/area alertsenders

/area outputs

/area rules

/area filaments

/area config

/area cli

/area tests

/area ci

/area build

/area docs

/area deps

/area evasion

/area other

Special notes for the reviewer


Does this PR introduce a user-facing change?


This is still in experimental stage, so the public announcement will follow after extensive testing and rule coverage.

Comment on lines +20 to +72
runs-on: ubuntu-latest
steps:
- name: Validate PR title
if: github.event_name == 'pull_request'
uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v4
- name: Install Go
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-${{ 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: |
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: |
./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" make test-integration
- name: Package
run: make pkg
- uses: actions/upload-artifact@v4
with:
name: fibratus-linux-packages
path: build/pkg/*
@rabbitstack
rabbitstack force-pushed the linux-port branch 4 times, most recently from 48e8bd1 to ab368f5 Compare September 27, 2026 18:38
Pull in cilium/ebpf v0.20.0 for the Linux CO-RE instrumentation spike and future event source work.
Encode the hard Linux runtime contract (kernel >=5.9, runtime BTF, ringbuf, tracing/iter support) and prototype ProcessKey-based startup reconciliation with bounded pending-queue and drop metrics.
Prove sched_process_exec ringbuf capture, iter/task baseline without bpf_d_path, shared-map replacement across separately generated objects, best-effort /proc enrichment, and race-safe startup on a real kernel.
Record validated runtime/tooling prerequisites, spike proofs, Windows coupling hotspots, and the ps.Snapshotter consumer migration needed for a Linux build.
Ignore late iterator snapshots after live switch, avoid double-closing MapReplacement-owned maps, and back off on persistent ringbuf read errors.
Keep github.com/cilium/ebpf as a direct go.mod require so Linux packages resolve in CI, and use the libebpf import alias requested in review.
Move shared catalogs out of GOOS-auto-gated filenames, tag remaining Windows packages and tests, and split colorizer/ntstatus so Linux builds no longer pull golang.org/x/sys/windows.
Drop the old _windows.go param and field catalogs now that the shared names live in platform-neutral files.
Provide Linux implementations for event helpers, parameter construction/formatting, callstack basics, and PE section stubs so shared packages compile without the Windows ETW path.
Add the in-memory Linux process snapshotter, ProcessKey identity, and PS fields needed by shared formatters and filter foreach helpers.
Switch the rule engine, compiler, sequence state, and filter options to the narrow Resolver interface, and add Linux filter/action stubs so rules compile and evaluate on Linux.
Credit newly authored Linux and platform-split files correctly. Keep Nedim's copyright on adapted splits and add a second line for the Linux port work.
Align copyright headers on the Linux eBPF feasibility spike with the rest of the Linux port work.
Restore the platform-agnostic CLI entrypoints and keep only the Stats struct Linux/Windows specific. Add Linux DialPipe and hostname helpers so the shared commands compile.
Those sections are Windows-specific and should not appear in the Linux capture version constants.
Drop the shared Resolver composition. Linux Snapshotter uses uint64 PIDs, Windows keeps uint32, and event.PID becomes a platform type alias so shared rule/filter call sites stay typed correctly.
Keep mail and Slack loading shared, and move systray/eventlog loading into the Windows-specific path. Linux Config now stores Alertsenders for the shared loader.
Move common config, event-source, output, schema, transformer, and validation behavior into shared files while retaining platform-specific hooks. Add Linux schema coverage and preserve Windows callers through explicit platform field assignment.
Separate Linux/Windows event types, parameters, queues, formatting, and marshalling while retaining shared behavior. Remove Windows-only Linux stubs, distinguish process/thread clones via clone flags, and add Linux coverage.
Split field/accessor/function registries and compiler behavior by platform, remove Linux-only Windows stubs, and preserve Windows-specific tests behind file suffixes. Use platform PID types and add Linux compiler coverage.
mostafa and others added 26 commits September 28, 2026 17:46
Add one CO-RE program per family for openat/unlink/rename, connect/accept, mmap/process_vm_readv/writev, and kill/ptrace/prctl. Optional families follow the existing enable-fileio, enable-net, and enable-mem switches, process-control events stay on, and accept reads the peer address on syscall exit because the kernel fills it then.
Add a checked matrix that every Linux event type decodes with its documented parameters, plus mmap process-state and live open/rename/unlink/kill coverage on a real kernel.
tp_btf/sys_exit supplies pt_regs, so kill/ptrace/prctl no longer need an enter probe or scratch slot. Rename filename2 to aux, document the truncation bits and padding, and keep only file-backed mmaps in process state until munmap is hooked.
Keep deprecated kevt aliases on Windows so they never enter the Linux catalog, and register process, file, network, memory, and thread fields for the captured syscalls.
…ad fields

Wire accessors for the syscall event matrix, gate file/net/mem on the existing enable flags, and expose syscall return, number, and truncation on evt fields.
…uncation

Prove shared-name semantics, missing-value defaults, truncation bits, and that Windows-only and kevt fields cannot compile.
Drop first-class truncated fields so callers inspect the truncated parameter through evt.arg.
The Makefile script automates the process of compiling the eBPF
programs, Fibratus binary, running tests, formatting, etc.
The bpf2go now drops all generated Go files + ebpf bytecode inside a
separate bpf package making it easier to navigate and distinguish the
generated code.
Make looked for bindings next to the C sources, so every CI run rebuilt and failed the clang 18 drift check.
Revalidate PID plus start boot time from /proc before SIGKILL so a reused PID cannot be terminated.
… connect

Ship an initial Linux detection set in a dedicated tree so Windows packaging and rule validation stay on the Windows catalog.
Prove Linux types are indexed, shipped rules fire, sequence lifecycle matches, and shared field fixtures compile on both platforms.
The kill, ptrace, and process_vm target was truncated through uint32 and widened as unsigned, so kill(-1) surfaced as 4294967295. Carry the argument as a signed value and expose ps.target.pid and mem.target.pid as signed, so a process group or broadcast target stays distinguishable from a process identifier.
Let the matches operator narrow the in-kernel prefilter instead of
forcing default-allow, by rewriting the patterns that have an exact
kernel equivalent rather than matching globs in BPF.

A pattern with no wildcard becomes an exact lookup, and one ending in a
single star becomes an LPM prefix. Both rewrites are exact, not merely
conservative, because a trailing star spans every remaining byte
including a path separator, exactly as wildcard.matchCaseSensitive does.
Runs of stars collapse first, so /tmp/** arrives here as /tmp/*. A star
anywhere else, or any '?', leaves the event type default-allow.

Matching globs in the kernel was the obvious approach and does not
survive the verifier. A faithful port of matchCaseSensitive backtracks,
so every byte comparison forks a path the verifier has to walk: at eight
patterns it exceeded the 8192 jump-sequence limit, and cutting it to two
patterns over 64 steps still burned the full 1M instruction budget
across 29668 states. Directory-aware globbing would verify more cheaply
but reject paths the operator accepts in userspace, which is the silent
false negative this prefilter exists to avoid.

imatches stays unsupported: folding in the kernel would have to agree
with unicode.ToLower on every rune.
Pin that a list of literal paths resolves entirely in the kernel's
exact-match hash, since nothing about it needs the wildcard machinery.

Run the privileged prefilter test twice, once with no approver and once
with one. The first pass establishes that the capture path reports the
open at all, so a failure in the second pass is about the prefilter and
not about an enter/exit correlation that lost its scratch entry.
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.
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.
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.
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.
The shared default bound the API to localhost:8080, so a Linux install
listened on the network before anyone asked it to. Defaulting to a
socket under /var/run leaves reachability to filesystem permissions, and
a TCP address still works when it is set explicitly.

Windows keeps its existing default through the same platform hook the
config file and rule paths already use.
Ships the binary, a Linux configuration, the rules, and a service unit.
The BPF objects need no separate packaging because bpf2go embeds them in
the binary.

The unit starts as root, since attaching a syscall tracepoint reads its
id from /sys/kernel/tracing and that is root-only on stock kernels. What
constrains it is the bounding set, which drops everything except loading
programs, attaching to tracepoints, reading other users' procfs entries,
and signalling a target for the kill action. ProtectSystem is full
rather than strict because strict also mounts /run read-only and the API
socket lives there, and ProtectProc stays permissive because hiding
other processes would silently empty ps.exe and ps.cmdline.

The shipped configuration omits the Windows-only sections instead of
carrying them inert, so what is in the file is what the platform reads.
The Linux Stats struct was an empty placeholder, so the command rendered
a table with no rows against a sensor that publishes a dozen counters.
It now carries the capture, drop, startup handover, and rule engine
expvars, grouped in the order they are useful to read.

Covers the endpoints the config and stats commands call over a socket
and over TCP, including a socket left behind by an unclean shutdown,
which would otherwise need manual cleanup before a restart. The
privileged test drives the same path against a live capture, since unit
tests can only prove the counters exist, not that a running sensor moves
them.
Covers the package layout, running in the foreground against a filter,
and the service. Notes that an idle host with rules enabled is expected
to be quiet, since only matching events reach the outputs and silence
otherwise reads as a broken install.

Records why the unit runs as root and what the bounding set leaves it.
Apply conflict resolutions and other activities to foment a stricter, cleaner, and more idiomatic code for multi platform support.
@rabbitstack
rabbitstack merged commit d5f14f1 into master Sep 28, 2026
10 of 11 checks passed
@rabbitstack
rabbitstack deleted the linux-port branch September 28, 2026 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants