eBPF Explained: Safe, Programmable Observability in the Linux Kernel
How eBPF lets sandboxed, verified programs run inside the kernel, and why that changed what's possible for tracing, networking, and security tooling.
Before eBPF, extending what the kernel could observe or enforce meant writing a kernel module — with all the risk that implies, since a bug there can panic the entire machine. eBPF (extended Berkeley Packet Filter) changed that calculus: it lets small, verified programs run inside the kernel, in a sandboxed virtual machine, without the caller writing a single line of kernel module code.
From packet filters to a general-purpose kernel VM
The original BPF (late 1990s) was purpose-built for one thing: efficiently filtering packets for tools like tcpdump, using a tiny, restricted instruction set evaluated by an in-kernel interpreter. eBPF generalized that same idea — a small, safely-executed program handed to the kernel — into something applicable far beyond networking: tracing function calls, aggregating performance metrics, enforcing security policy, and more.
# See what BPF programs are already loaded on a running system
bpftool prog list
The verifier: why eBPF programs can’t crash the kernel
The property that makes eBPF trustworthy enough to load from unprivileged (or semi-privileged) userspace is the verifier. Before any eBPF program is allowed to run, the kernel statically analyzes it — every possible execution path — to prove it terminates, never accesses memory out of bounds, and never dereferences an unchecked pointer. A program the verifier can’t prove safe is rejected outright, before ever executing a single instruction.
# A failed verification shows up immediately at load time
bpftool prog load myprogram.o /sys/fs/bpf/myprogram
# libbpf: permission denied ... verifier log: ...
This is the fundamental difference from a kernel module: a kernel module is trusted as ordinary kernel code, capable of anything (including crashing the whole system); an eBPF program is mathematically constrained by the verifier before it’s allowed anywhere near kernel execution.
Hook points: where eBPF programs attach
An eBPF program doesn’t run continuously — it’s attached to a specific hook, a point in the kernel where an event of interest occurs, and runs only when that hook fires. Common hook types include kprobes (arbitrary kernel function entry/exit), tracepoints (stable, purpose-built instrumentation points the kernel maintainers have committed to), XDP (eXpress Data Path, for packets at the earliest possible point in the network driver), and cgroup hooks (for per-cgroup policy enforcement).
SEC("kprobe/do_sys_open")
int trace_open(struct pt_regs *ctx) {
bpf_printk("file opened\n");
return 0;
}
Maps: how eBPF programs share state
Because eBPF programs are short-lived and stateless by design, persisting data (a counter, a table of recent events) across invocations — or exposing it to userspace — happens through eBPF maps, key-value stores the kernel manages on the program’s behalf:
bpftool map create /sys/fs/bpf/mymap type hash key 4 value 8 entries 1024
bpftool map dump name mymap
A tracing program might increment a counter in a hash map keyed by process ID every time a syscall fires; a userspace tool then reads that same map to render a live histogram — this map-as-shared-state pattern is the backbone of nearly every eBPF-based tool.
Where eBPF is already running on a typical system
Even without deliberately writing any eBPF code, a modern Linux system is likely already running quite a bit of it. bpftrace provides a high-level, awk-like scripting language compiled down to eBPF for ad hoc tracing:
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args.filename)); }'
Tools like Cilium implement Kubernetes networking and network policy almost entirely in eBPF/XDP rather than iptables, and bcc (BPF Compiler Collection) ships a library of ready-made tracing tools — execsnoop, opensnoop, biolatency — that are themselves thin wrappers generating and loading eBPF programs on demand.
opensnoop-bpfcc # trace every open() syscall system-wide, live
biolatency-bpfcc # histogram of block I/O latency
Why this matters beyond tracing
The same “safe, sandboxed, kernel-resident program” primitive that makes eBPF good for observability also makes it a serious platform for networking (XDP-based load balancing and DDoS mitigation, operating before packets even reach the normal networking stack) and security (seccomp-style syscall filtering with far richer logic than a static allow-list, and runtime security tools like Falco that alert on suspicious kernel-level behavior in real time). The unifying idea across all of it is the same one BPF started with decades ago, just generalized: let userspace hand the kernel a small, provably-safe program instead of asking the kernel maintainers to hard-code every possible use case in advance.