eBPF Fundamentals
Sandboxed programs in the kernel — the programmable data plane
What Is eBPF?
eBPF (extended Berkeley Packet Filter) allows you to run sandboxed, event-driven programs inside the Linux kernel without modifying kernel source code or loading kernel modules. Think of it as a safe, programmable virtual machine embedded in the kernel that lets you hook into virtually any kernel event — networking, tracing, security — and run custom logic at native speed.
Why This Matters for Infrastructure
- No kernel recompilation — deploy new kernel-level logic without rebooting or rebuilding the kernel
- No kernel modules — no risk of crashing the kernel with buggy
.komodules - Safety guaranteed — the kernel verifier statically analyzes every eBPF program before loading
- Near-native performance — JIT-compiled to native machine code, not interpreted
- Dynamic — load/unload programs at runtime, no downtime
Note
In practice: eBPF is the technology behind Cilium (K8s CNI), Falco (runtime security), bpftrace (dynamic tracing), Katran (Facebook's L4 load balancer), and Cloudflare's DDoS mitigation. If you're running a modern Linux infrastructure, you're almost certainly running eBPF programs already.
History: BPF to eBPF
BPF (1992)
Berkeley Packet Filter — tcpdump's engine for filtering packets in the kernel
cBPF in Linux
"Classic" BPF — limited instruction set, packet filtering only
eBPF (2014+)
Extended BPF — general-purpose kernel programmability, 64-bit registers, maps, helpers
Modern eBPF
CO-RE, BTF, bounded loops, BPF LSM, signed programs — production-grade
| Feature | Classic BPF (cBPF) | Extended BPF (eBPF) |
|---|---|---|
| Registers | 2 (32-bit) | 11 (64-bit, R0-R10) |
| Instruction set | ~30 instructions | ~100+ instructions |
| Use case | Packet filtering only | Networking, tracing, security, profiling |
| Data sharing | None | eBPF maps (kernel <-> userspace) |
| JIT compilation | Optional, limited | Default on modern kernels |
| Helper functions | None | Extensive kernel-provided API |
The Verifier: The Safety Guarantee
The eBPF verifier is a static analysis engine inside the kernel that inspects every eBPF program before it is loaded. If the verifier rejects a program, it never runs. This is the fundamental reason eBPF is safe to run in production kernels — it is mathematically impossible for a verified program to crash the kernel.
Write eBPF program
C or Rust, compiled to eBPF bytecode
bpf() syscall
Userspace requests program load
Verifier
Static analysis — reject or accept
JIT Compile
Bytecode -> native machine code
Attach to hook
XDP, tc, kprobe, tracepoint...
What the Verifier Checks
Safety Invariants
- No infinite loops — only bounded loops allowed (verifier must prove termination). Before kernel 5.3, loops were not allowed at all.
- No null pointer dereferences — every pointer must be checked before use
- Stack size limited to 512 bytes — prevents stack overflow in kernel context
- Memory access bounds checking — no out-of-bounds reads/writes
- All code paths must terminate — DAG analysis, no unreachable code
- Only approved helper functions — cannot call arbitrary kernel functions
How It Works
- Walks all possible execution paths (DAG traversal)
- Tracks register types and value ranges at each instruction
- Ensures map accesses use correct key/value sizes
- Verifies pointer arithmetic stays within bounds
- Checks that
bpf_probe_read()is used for kernel memory access - Complexity limit: max ~1M verified instructions (prevents verifier DoS)
Warning
Verifier complexity: The verifier is the biggest pain point in eBPF development. Programs that are logically correct may be rejected because the verifier can't prove they're safe. This is why toolchains like libbpf + CO-RE and higher-level frameworks like Cilium/bpftrace exist — they generate verifier-friendly code.
JIT Compilation
After verification, eBPF bytecode is JIT-compiled to native machine instructions (x86_64, ARM64, etc.). This means eBPF programs run at near-native speed — there is no interpreter overhead in the hot path.
- Each eBPF instruction decoded at runtime
- Switch-case dispatch per opcode
- Significant overhead per packet/event
- Still safe, just slower
- eBPF bytecode translated to native instructions once
- Direct CPU execution, no dispatch overhead
- Performance within 5-10% of hand-written kernel C
- Enabled by default:
net.core.bpf_jit_enable=1
$ sysctl net.core.bpf_jit_enable
net.core.bpf_jit_enable = 1
$ # Verify JIT is active for loaded programs:
$ bpftool prog list
6: xdp name xdp_drop tag abc123 gpl jited 96B loaded_at 2024-01-15
Hook Points: Where eBPF Programs Attach
eBPF programs are event-driven — they execute when a specific kernel event fires. The "hook point" determines when and where your code runs. This is what makes eBPF so versatile: the same technology powers networking, observability, and security.
Packet Path Through the Kernel (with eBPF Hook Points)
NIC (Hardware)
Packet arrives
XDP
Earliest hook. Before sk_buff allocation. Can DROP/PASS/TX/REDIRECT
tc ingress
Traffic control layer. Has full sk_buff metadata. Can classify/redirect
Netfilter / iptables
Traditional firewall chains (PREROUTING, INPUT, FORWARD...)
Socket Layer
cgroup/socket eBPF hooks
Application
Userspace receives data
| Hook Point | When It Fires | Use Cases | Performance |
|---|---|---|---|
XDP |
Packet arrives at NIC driver (pre-sk_buff) | DDoS mitigation, load balancing, packet filtering | Fastest — millions of pps |
tc (traffic control) |
Ingress/egress after initial processing | Packet mangling, policy enforcement, redirects | Very fast, full sk_buff access |
cgroup |
Socket operations for a cgroup | Per-container networking policy, connect/bind interception | Per-socket granularity |
socket_filter |
Per-socket packet filtering | Application-level packet inspection | Socket-scoped |
kprobes |
Any kernel function entry/return (dynamic) | Debugging, tracing, profiling kernel internals | Overhead per invocation |
tracepoints |
Static kernel instrumentation points | Stable tracing interface (survives kernel upgrades) | Lower overhead than kprobes |
fentry/fexit |
Function entry/exit (modern, BTF-based) | Safer, faster alternative to kprobes (5.5+) | Near-zero overhead |
LSM |
Linux Security Module hooks | Security policy enforcement (file access, process creation) | Security-critical path |
Tip
Key insight: The earlier in the packet path you attach, the faster you can act — but the less metadata you have. XDP sees raw packets (no sk_buff), so it's blazing fast but has limited context. tc sees full sk_buff metadata. Choose the right hook for your use case.
eBPF Maps: Shared Data Structures
eBPF maps are kernel-resident data structures that serve as the primary communication channel between eBPF programs (kernel space) and userspace applications. Maps persist across program invocations — they're how you maintain state, share configuration, and export telemetry.
Map Architecture
| Map Type | Structure | Lookup | Use Cases |
|---|---|---|---|
BPF_MAP_TYPE_HASH |
Key-value hash table | O(1) | Connection tracking, rate limiting, blocklists |
BPF_MAP_TYPE_ARRAY |
Fixed-size indexed array | O(1) | Configuration, counters, per-CPU stats |
BPF_MAP_TYPE_RINGBUF |
Lock-free ring buffer | FIFO | Stream events to userspace efficiently (preferred over perf_event) |
BPF_MAP_TYPE_LRU_HASH |
Hash table with LRU eviction | O(1) | Bounded-size caches, conntrack tables with auto-cleanup |
BPF_MAP_TYPE_PERCPU_HASH |
Per-CPU hash (no locks) | O(1) | High-throughput counters, stats without lock contention |
BPF_MAP_TYPE_PERCPU_ARRAY |
Per-CPU array (no locks) | O(1) | Per-CPU scratch space, histogram buckets |
BPF_MAP_TYPE_LPM_TRIE |
Longest prefix match trie | O(log n) | IP prefix/CIDR matching, routing tables |
BPF_MAP_TYPE_PROG_ARRAY |
Array of eBPF program fds | O(1) | Tail calls — chain eBPF programs together |
Note
Per-CPU maps are critical for performance: In a networking hot path processing millions of packets per second, lock contention on shared maps would destroy throughput. Per-CPU variants give each core its own copy — userspace aggregates when reading.
Map Lifecycle
- Create —
bpf(BPF_MAP_CREATE, ...)at program load time (usually via libbpf) - Pin — optionally pin to
/sys/fs/bpf/so maps persist beyond process lifetime - Use — eBPF programs call
bpf_map_lookup_elem(),bpf_map_update_elem(),bpf_map_delete_elem() - Read from userspace — control plane reads/writes maps via
bpf()syscall orbpftool map dump - Cleanup — maps are reference-counted; destroyed when no programs or file descriptors reference them
Helper Functions
eBPF programs cannot call arbitrary kernel functions. Instead, the kernel exposes a curated set of helper functions that eBPF programs can invoke. The verifier ensures only valid helpers are called for the given program type.
Map Operations
// Lookup a value by key
void *bpf_map_lookup_elem(&map, &key);
// Insert or update
int bpf_map_update_elem(&map, &key, &val, flags);
// Delete an entry
int bpf_map_delete_elem(&map, &key);Packet Operations
// Redirect packet to another interface
int bpf_redirect(ifindex, flags);
// Modify packet bytes
int bpf_skb_store_bytes(skb, off, &data, len, flags);
// Adjust packet headroom
int bpf_xdp_adjust_head(xdp_md, delta);Tracing & Context
// Get current PID and TID
u64 bpf_get_current_pid_tgid();
// Safely read kernel memory
int bpf_probe_read(&dst, size, &src);
// Get current timestamp (ns)
u64 bpf_ktime_get_ns();Output & Debug
// Send event to ring buffer
int bpf_ringbuf_output(&ringbuf, &data, size, flags);
// Print to trace pipe (debug only)
int bpf_trace_printk(fmt, fmt_size, ...);
// Get current cgroup id
u64 bpf_get_current_cgroup_id();eBPF Program Types
Each eBPF program has a type that determines which hook points it can attach to, which helpers it can call, and what context (input data) it receives. The verifier enforces these constraints.
| Program Type | Context Struct | Attach Point | Primary Use |
|---|---|---|---|
BPF_PROG_TYPE_XDP |
xdp_md |
NIC driver (pre-sk_buff) | Packet filtering, DDoS, load balancing |
BPF_PROG_TYPE_SCHED_CLS |
__sk_buff |
tc ingress/egress | Traffic shaping, policy, packet mangling |
BPF_PROG_TYPE_CGROUP_SKB |
__sk_buff |
cgroup ingress/egress | Per-container network policy |
BPF_PROG_TYPE_SOCKET_FILTER |
__sk_buff |
Socket | Per-socket packet filtering |
BPF_PROG_TYPE_KPROBE |
pt_regs |
Any kernel function | Dynamic tracing, debugging |
BPF_PROG_TYPE_TRACEPOINT |
Varies per tracepoint | Static kernel tracepoints | Stable tracing interface |
BPF_PROG_TYPE_SOCK_OPS |
bpf_sock_ops |
Socket operations | TCP tuning, socket-level decisions |
BPF_PROG_TYPE_LSM |
Varies per hook | LSM security hooks | Security policy enforcement |
eBPF Architecture Summary
The Complete Picture
Loads programs, reads/writes maps
Configures policy and routes
eBPF programs execute on events
Maps store shared state
Tip
The split architecture pattern: In production eBPF systems (Cilium, Falco, Katran), you always have a userspace control plane (Go, Rust, C++) that manages eBPF programs and maps, plus kernel data plane eBPF programs that do the fast-path work. The control plane configures policy via maps; the data plane enforces it at line rate.
eBPF Development Ecosystem
| Tool / Library | Language | Description |
|---|---|---|
libbpf |
C | Reference library for loading eBPF programs. CO-RE (Compile Once, Run Everywhere) |
cilium/ebpf |
Go | Pure-Go library for eBPF. Used by Cilium, Hubble, Tetragon |
aya |
Rust | Write eBPF programs and userspace in Rust. No C dependency |
bpftrace |
DSL | High-level tracing language (like awk for eBPF). One-liners for debugging |
bpftool |
CLI | Inspect loaded eBPF programs, maps, links. Essential debugging tool |
bcc |
Python/C | BPF Compiler Collection. Older toolchain, being superseded by libbpf + CO-RE |
$ # List all loaded eBPF programs
$ bpftool prog list
6: xdp name xdp_firewall tag a1b2c3d4 gpl jited 208B
12: sched_cls name tc_policy tag e5f6a7b8 gpl jited 384B
$ # Dump a map's contents
$ bpftool map dump id 4
key: 0a 00 01 01 value: 00 00 00 42
key: 0a 00 01 02 value: 00 00 01 a3
$ # One-liner: count syscalls by process
$ bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
Attaching 1 probe...
@[kubelet]: 4521
@[containerd]: 3102
@[cilium-agent]: 1847