What you'll learn
Quick Answer
eBPF lets you run small, sandboxed programs inside the Linux kernel itself, triggered by events like system calls, network packets, or function calls, without writing a kernel module or recompiling the kernel. A verifier statically checks every program is safe before it loads, then a JIT compiler turns it into native machine code so it runs at near-native speed. This combination is why tools like bpftrace, Cilium, and Falco can see everything happening on a machine with very little overhead.
What eBPF Actually Is
eBPF stands for extended Berkeley Packet Filter, which is a terrible name for what it's become. The original BPF, from 1992, was a tiny virtual machine inside the kernel that let tools like tcpdump filter packets without copying every single one up to userspace first. eBPF, added to the Linux kernel starting around version 3.18 and massively expanded since, kept the same basic idea — a small sandboxed VM inside the kernel — and turned it into a general-purpose way to run custom logic in response to almost anything the kernel sees: a system call being made, a function being entered or exited, a packet arriving on a network interface, a process forking.
Before eBPF, if you wanted this kind of visibility or control, you had two bad options. You could write a kernel module, which runs with full kernel privileges and, if it has a bug, can crash or corrupt the entire machine. Or you could use something like strace, which works by stopping the traced process at every system call and bouncing control back and forth to a userspace tracer — correctness over performance, and the context-switch overhead makes it unusable in production at any real scale. eBPF's pitch is that you get kernel-level visibility and kernel-level speed, but a crashing or malicious eBPF program is structurally prevented from taking the machine down, because nothing gets to run until it's been checked.
One honest caveat before we go further: actually running an eBPF program requires a Linux kernel built with BPF support, and loading most non-trivial programs requires root or specific capabilities. The sandbox this article was written in doesn't have that — no Linux kernel with BPF, no root. So instead of fabricating terminal output, the mechanics below are drawn from eBPF.io's own documentation and the Linux kernel's own tracing writeups, not from memory.
The eBPF Virtual Machine
An eBPF program isn't C code running directly in the kernel. You write it in a restricted subset of C, compile it with Clang/LLVM to eBPF bytecode, and that bytecode is what actually gets loaded. The eBPF VM that executes this bytecode has 11 general-purpose 64-bit registers, a small fixed-size stack, and an instruction set that looks a lot like a stripped-down RISC CPU — arithmetic, conditional jumps, memory loads and stores, and calls to a whitelisted set of kernel "helper functions" the program is allowed to invoke.
That helper-function whitelist matters more than it sounds like it should. An eBPF program cannot call arbitrary kernel functions. It can only call the specific helpers the kernel exposes for its program type — things like reading a value out of a map, getting the current timestamp, or looking up process metadata. This is one of the main things that makes the sandbox a sandbox: the attack surface isn't "the entire kernel," it's a deliberately curated set of safe entry points.
The other key piece is maps — key-value data structures that live in kernel memory but can be read and written from both the eBPF program and a regular userspace process. Maps are how an eBPF program hands data out to the world: a tracing program might count events into a hash map keyed by process ID, and a userspace agent (like bpftrace or a Cilium component) reads that map continuously to build dashboards, logs, or policy decisions. Without maps, an eBPF program would be able to see everything and report nothing.
The Verifier: Why It's Safe to Run Untrusted Code in the Kernel
Before any eBPF bytecode is allowed to load, it goes through the verifier, and this is the single most important piece of the whole system. The verifier performs static analysis — it walks every possible execution path the bytecode could take and checks a list of hard rules, without actually running the program yet.
- The program must provably terminate. Unbounded loops are rejected; the kernel needs a guarantee the program can't spin forever and hang whatever event triggered it.
- Every memory access must be bounds-checked at verification time. The verifier tracks the possible range of every register's value through every branch, and if it can't prove a memory access stays inside a valid buffer, it rejects the program outright.
- The program can only call the specific helper functions its program type is allowed to call — no arbitrary kernel function pointers.
- There's a hard cap on total instruction complexity the verifier will check (historically around a million instructions' worth of verified paths), so a program that's too large or branches combinatorially just fails to load rather than consuming unbounded verification time.
If any of these checks fail, the program is refused and never executes — not "executes and then gets killed," refused before a single instruction runs. This is why the common framing is accurate: eBPF doesn't trust the program, it trusts the proof the verifier constructs about the program.
JIT Compilation: From Bytecode to Native Instructions
Passing the verifier only gets you an interpreter-speed guarantee, which wouldn't be good enough for something like per-packet network processing. So once a program is verified, the kernel's Just-In-Time compiler translates the eBPF bytecode into native machine instructions for whatever CPU architecture the machine is actually running — x86-64, ARM64, and others each have their own eBPF JIT backend in the kernel.
This is the part that makes eBPF genuinely different from, say, a safe scripting sandbox. The program isn't interpreted at every invocation; it's compiled once, after verification, into real machine code that executes directly on the CPU. That's why an XDP program processing packets at line rate, or a tracing program firing on every syscall across a busy server, doesn't fall over — it's running about as fast as hand-written C would, just with the safety guarantees the verifier already proved before JIT even happens.
Hook Points: Where eBPF Programs Actually Attach
A verified, JIT-compiled eBPF program still needs something to trigger it. The kernel exposes several categories of attachment points, and which one you pick depends on what you're trying to observe or control:
- kprobes / uprobes — dynamic instrumentation points that can attach to almost any kernel function (kprobe) or userspace function (uprobe) by name, even ones that were never designed to be traced. Flexible, but tied to internal function signatures that can change between kernel versions.
- Tracepoints — stable hook points that kernel developers deliberately placed in the source and promise not to break, which makes them a safer long-term bet than kprobes for anything you plan to rely on across kernel upgrades.
- XDP (eXpress Data Path) — runs at the network driver level, before the kernel has even allocated its usual packet data structure for an incoming frame. This is as early as you can intercept a packet, which is why XDP programs are used for very high-throughput tasks like DDoS mitigation and load balancing, where every allocation avoided matters.
- LSM hooks and cgroup hooks — attachment points inside the kernel's security module framework and control-group subsystem, used for fine-grained security policy enforcement rather than pure observation.
The variety matters because it means eBPF isn't one feature, it's an attachment model. The same verifier-plus-JIT pipeline backs a packet filter, a syscall tracer, and a security policy engine — only the hook point and helper-function whitelist change.
Why It's Eating the Observability World
Once you have a safe way to run arbitrary logic at almost any point in the kernel with near-native performance, a whole category of tools stops needing to be a special case. bpftrace is a high-level tracing language (syntax borrowed loosely from awk and C, in the lineage of DTrace and SystemTap) that compiles down to eBPF bytecode via LLVM, so you can write a one-line script to answer "which process is opening this file" without writing a kernel module. Cilium uses eBPF to implement Kubernetes networking, load balancing, and network policy directly in the kernel data path, which is part of why it can outperform iptables-based approaches at scale — it was built from the ground up around eBPF rather than bolting eBPF onto an existing design. Falco uses eBPF (among other sources) to watch syscalls at the kernel layer and flag anomalous behavior for runtime security, the same category of job strace used to do badly and slowly.
The pattern across all three: instrumentation that used to require either kernel-module risk or userspace-tracer slowness now runs as a verified, compiled, sandboxed program with essentially no code changes required in the application being observed. That's the actual reason eBPF shows up in nearly every modern observability and Kubernetes security product's architecture diagram — it's not a trend, it's that the old tradeoff (safety vs. visibility vs. speed) mostly stopped existing.
