Home / Software y Cloud / Let your own code run inside the kernel without recompiling it

Let your own code run inside the kernel without recompiling it

When a server decides whether a network packet gets in or is dropped, that packet normally walks through a series of rules in user space or in the kernel firewall: the data is copied back and forth, compared against dozens of conditions, and only then is a decision made. The problem is the cost. At tens of millions of packets per second, that round trip between kernel and user programs becomes the bottleneck, not the network.

For a few years now there has been another way to do it, and it is holding up more and more critical infrastructure: eBPF (extended Berkeley Packet Filter, the kernel’s extended packet filter). The underlying idea is simple to state and hard to pull off: letting user-written code run inside the kernel, at a specific point along the path of a packet, a system call or a security event, without recompiling the kernel or maintaining modules outside the official tree.

A 1992 filter that ended up being a platform

The name goes back a long way. The original BPF appeared in 1992 as the mechanism tcpdump used to filter traffic: a small automaton that decided, packet by packet, whether it was worth copying up to user space. In 2014 the kernel rewrote it completely (eBPF) and gave it what it had been missing: 64-bit registers, calls to the kernel’s own helper functions, shared data maps, and the ability to attach to many more places than packet reception.

Today an eBPF program is written in a restricted subset of C, compiled to its own bytecode, and loaded with a single system call, bpf(). From there the kernel does two things. First it checks the program: a static verifier walks it instruction by instruction and demands very strict guarantees. No infinite loops (loops are only accepted if the verifier can prove they terminate), no arbitrary pointers (you can only read and write memory coming from a validated pointer, with its length checked), no access outside the declared maps, and a hard instruction limit, today around one million, which was 4,096 in early versions. If the verifier cannot prove the program terminates and stays within its bounds, it does not load. Then it translates it into native machine code with a JIT compiler: the filter ends up running at native instruction speed, with no interpreter.

Where the program attaches

The real power is in the number of anchor points, the hooks, the kernel offers. The most extreme is XDP (eXpress Data Path): the program runs in the network card driver itself, before the kernel even builds the structure that describes the packet. It can return three things: pass it through, drop the packet right there — the cheapest possible filtering — or redirect it to another interface. With XDP, mitigating a volumetric denial-of-service attack happens on the network card, at line rate, without the network stack ever noticing.

Other common points are the traffic subsystem (tc), kprobes and uprobes (which let you insert probes into any kernel function or user binary), tracepoints, the security hooks of the LSM module, control groups (cgroups), and the entry and exit points of specific functions. In all of them the program receives a context with the event data and decides what to do with it.

Maps, the shared state

A filter with no memory is of little use, so eBPF brings maps: data structures that live in the kernel and that both eBPF programs and user processes can read and write. There are hash tables, arrays, queues, Bloom filters, per-CPU arrays (one copy per core, so they do not fight over a lock) and ring buffers to ship events to user space without needless copies. A packet counter per IP address, a table of active connections or a log of security events is, literally, a map.

The detail that made all of this take off was CO-RE (Compile Once — Run Everywhere). Before, every eBPF program depended on the exact kernel version it had been compiled against, because internal structures change from one release to the next. The BTF type information the kernel has exposed since version 5.2 lets you compile once and have references to struct fields relocated when the program is loaded. No more recompiling for every distribution.

From toy to infrastructure

The jump to production came hand in hand with container networking. Cilium replaces much of Kubernetes’ iptables rules with eBPF programs and maps: instead of walking ever-longer rule chains, the kernel looks up a hash table to decide which service a connection goes to. Meta uses eBPF in its layer 4 load balancer, Katran, to spread traffic across servers; Cloudflare uses it against DDoS attacks; bpftrace and perf use it for live diagnostics; tools such as Falco or Tetragon build intrusion detection by watching system calls and process events in real time.

The limits are real

None of this comes free. The verifier is so strict that writing valid programs is sometimes harder than writing the filter; the error messages are cryptic and debugging is done blind until the program loads. Privileges are required: loading programs and creating maps demands elevated capabilities (CAP_BPF, CAP_SYS_ADMIN), and unprivileged mode is disabled by default on almost every distribution, precisely because the verifier is an attack surface in itself: a bug that let an improper memory access slip through would not be a bug in a program, it would be a kernel bug. That is why there are additional mitigations, such as constant blinding to make side-channel attacks harder, and a very fast pace of change between versions.

But the balance has changed how large systems are run. Before, changing the behaviour of the network or of observability meant waiting for the kernel cycle or accepting the risk of a proprietary module. Now you write a program of a few dozen instructions, load it hot and remove it when it is no longer needed. The Linux kernel has gone from being a closed component to being, in practice, a programmable platform.