The eBPF Verifier - How Linux Safely Runs User Code in Kernel Space

Running arbitrary code in kernel space. If you've been in operations long enough, that sentence probably just triggered your fight-or-flight response. And for good reason—kernel modules have been causing production outages since before containers were cool.
But eBPF is different. Not "marketing different" but architecturally, fundamentally, mathematically different. Today we're going to dig deep into how the Linux kernel can safely run user-supplied programs without risking system stability. This isn't about any specific tool or vendor—it's about understanding why eBPF has become the kernel's preferred extension mechanism.
The Challenge: Extending the Kernel Without Breaking It
The Linux kernel needs to be extensible. Whether it's for observability, networking, or security, we need ways to add functionality without recompiling the kernel or loading risky modules. Historically, we had three options:
- Kernel modules: 😱 Full access, full risk
- Userspace polling: 😴 Safe but slow
- Kernel patches: 😰 Safe-ish but requires recompilation
Each approach involved trading off between safety, performance, and flexibility. Then eBPF came along and changed the game entirely.
The eBPF Architecture: Verified Code in a Sandbox
eBPF (extended Berkeley Packet Filter) allows you to run custom programs in kernel space, but with a twist: every program must pass through a verifier that proves it's safe before it can run. Let's break down how this works.
The Verifier: Your Mathematical Proof of Safety
The eBPF verifier is the heart of what makes eBPF safe. It's not just a linter or static analyzer—it's an abstract interpreter that simulates all program paths to ensure safety. Here's what it checks:
// Example: The verifier tracks EVERY possible code path
void* ptr = map_lookup_elem(&my_map, &key);
// The verifier knows ptr might be NULL
if (condition) {
// Path 1: verifier tracks that we checked ptr
if (ptr != NULL) {
value = *ptr; // Safe: verifier knows ptr is valid here
}
} else {
// Path 2: different code path
value = *ptr; // REJECTED: ptr might be NULL on this path
}
The verifier performs several types of analysis:
1. Control Flow Analysis
// The verifier ensures no infinite loops
for (int i = 0; i < 100; i++) { // OK: bounded loop
// do work
}
while (condition) { // REJECTED: unbounded loop
// this will never load
}
The verifier builds a complete control flow graph and ensures:
- Every path through the program terminates
- No backward jumps that could create infinite loops (unless bounded)
- Maximum instruction count isn't exceeded (1 million for privileged programs)
Note: The instruction count limit is higher for privileged programs—up to 1 million instructions. For unprivileged programs, it is typically limited to 10,000.
2. Memory Safety Verification
// The verifier tracks the size and validity of every memory region
struct sk_buff *skb = ctx; // ctx is validated by program type
// Verifier knows skb->data and skb->data_end boundaries
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
// Must check bounds before access
if (data + sizeof(struct ethhdr) > data_end)
return DROP; // Verifier requires this check
struct ethhdr *eth = data; // Now safe to access
For every memory access, the verifier ensures:
- Pointers are valid (not NULL unless explicitly checked)
- Array bounds are respected
- Stack doesn't overflow (512 bytes limit)
- No out-of-bounds reads or writes
3. Type and Context Tracking
The verifier maintains a state machine that tracks the type and range of every register and stack slot at every program point:
// Verifier tracks types through the program
int key = 5;
struct value *val;
// After this call, verifier knows val might be NULL
val = bpf_map_lookup_elem(&my_map, &key);
// Type confusion is impossible - verifier tracks that val
// is either NULL or a pointer to struct value
if (val) {
// Verifier knows val is non-NULL here
val->counter++; // Safe
}
// Verifier knows val might be NULL here
// val->counter++; // Would be REJECTED
The JIT Compiler: From Bytecode to Native Code
Once verified, eBPF bytecode is compiled to native machine code by the Just-In-Time (JIT) compiler. This happens after verification, so even JIT bugs can't introduce unsafe behavior—the bytecode is already proven safe.
The JIT compiler also adds runtime protections:
- Speculative execution barriers (Spectre mitigation)
- Constant blinding (JIT spraying mitigation)
- Tail call limits (stack overflow prevention)
Runtime Boundaries: The Sandbox in Action
Even after verification, eBPF programs run in a restricted environment:
Helper Functions: The Only Syscalls You Get
eBPF programs can't make arbitrary system calls. Instead, they can only call pre-approved helper functions:
// You can't do this:
open("/etc/passwd", O_RDONLY); // No arbitrary syscalls
// You can only do this:
bpf_map_lookup_elem(&my_map, &key); // Pre-approved helper
bpf_probe_read_user(&data, sizeof(data), user_ptr); // Another helper
Each helper function is carefully implemented to be safe even if called with invalid arguments. The verifier also ensures you're calling helpers correctly based on your program type. Helper functions are whitelisted per program type. You can't call a tracing helper from an XDP program, for instance.
Maps: Structured Data Sharing
eBPF programs communicate with userspace through maps—fixed-size data structures that both sides can access:
// Kernel side (eBPF program)
struct bpf_map_def SEC("maps") events = {
.type = BPF_MAP_TYPE_RINGBUF,
.max_entries = 256 * 1024,
};
// Safe, bounded write to ring buffer
bpf_ringbuf_output(&events, &event, sizeof(event), 0);
// Userspace side
ring := bpf.OpenRingBuffer(eventsMap)
for {
record, err := ring.Read()
// Process events safely in userspace
}
Maps provide:
- Bounded memory usage (size defined at creation)
- Atomic operations (no data races)
- Type safety (verifier ensures correct usage)
This means that map access must be bounded and deterministic, otherwise the verifier will reject the program.
Failure Modes: What Happens When Things Go Wrong
Let's examine specific failure scenarios and how eBPF handles them:
Scenario 1: Buggy Program Logic
// BUG: Forgot to check if ptr is NULL
void *ptr = bpf_map_lookup_elem(&my_map, &key);
int value = *(int*)ptr; // Potential NULL dereference
What happens: Verifier rejects the program at load time
System impact: Zero—program never runs
Error message: "invalid mem access 'map_value_or_null'"
Scenario 2: Resource Exhaustion Attempt
// Attempting to consume excessive CPU
for (int i = 0; i < 1000000; i++) {
expensive_operation();
}
What happens: Verifier rejects due to instruction limit
System impact: Zero—program never loads
Error message: "BPF program is too large. Processed 1000001 insn"
Scenario 3: Helper Function Abuse
// Trying to send signals to arbitrary processes
for (int pid = 1; pid < 65536; pid++) {
bpf_send_signal(pid, SIGKILL); // Trying to kill everything
}
What happens:
- Verifier checks if program has CAP_SYS_ADMIN
- Even if allowed, helper has built-in rate limiting
- Operations are logged for audit
System impact: Limited by helper implementation safeguards
Note: Even if allowed by the verifier, helpers like bpf_send_signal()
are gated by program type, capabilities (CAP_SYS_ADMIN
), and may include runtime limits to prevent abuse.
Scenario 4: Memory Corruption Attempt
// Trying to write beyond buffer bounds
char buffer[64];
for (int i = 0; i < 128; i++) {
buffer[i] = 'A'; // Buffer overflow attempt
}
What happens: Verifier rejects at load time
System impact: Zero
Error message: "invalid stack off=-65 size=1"
Performance Characteristics: The Cost of Safety
The verifier and runtime checks do have a cost, but it's surprisingly small:
Verification Time
- Simple programs: < 1ms
- Complex programs: 10-100ms
- Happens once at load time
Runtime Overhead
Native C code: 1.0x (baseline)
eBPF code: 1.1-1.2x
Kernel module: 1.0x (but no safety)
Userspace polling: 10-100x
Memory Overhead
- Program size: Limited to ~1MB of bytecode
- Stack: 512 bytes per program
- Maps: Explicitly bounded at creation
The Mathematics Behind Safety
The verifier uses abstract interpretation to prove safety properties. For each instruction, it maintains abstract state:
State = {
R0: {type: SCALAR, range: [0, 100]},
R1: {type: PTR_TO_MAP_VALUE, offset: 0, size: 64},
R2: {type: PTR_TO_CTX},
...
Stack: [{slot: -8, type: SCALAR}, ...]
}
For each possible path through the program, the verifier:
- Propagates abstract state forward
- Checks safety at each instruction
- Ensures all paths converge to safe states
This is similar to how Rust's borrow checker proves memory safety at compile time, except the eBPF verifier works on bytecode at load time.
eBPF in Production: Real-World Applications
This mathematical approach to safety is why major projects are betting on eBPF:
- Cilium uses eBPF for container networking and security
- Falco leverages eBPF for runtime security monitoring
- systemd employs eBPF for system observability
- Cloud providers run customer eBPF programs with confidence
At Qpoint, we use eBPF to provide deep visibility into external API traffic without compromising system stability. Our Qtap platform leverages eBPF's safety guarantees to monitor outbound connections at the kernel level, giving teams unprecedented insight into their external dependencies.
The Bottom Line
eBPF represents a fundamental shift in how we think about kernel extensibility. Instead of choosing between safety and performance, eBPF gives us both through static verification and runtime sandboxing. The verifier isn't just a safety check—it's a proof system that ensures your program cannot violate kernel integrity.
This is why major projects like Qpoint, Cilium, Falco, and systemd are betting on eBPF. It's why cloud providers are comfortable running customer eBPF programs. And it's why the Linux kernel community has embraced eBPF as the preferred way to extend kernel functionality.
The next time someone asks you about running code in kernel space, you can explain that eBPF programs are closer to mathematical proofs than traditional code. They're not just "probably safe"—they're "prove"-ably safe.
Want to go deeper? Check out the kernel's verifier documentation, browse the verifier source, or read the ✨amazing✨ eBPF Verifier Code Review by the NCC Group. Hopefully, this article alongside these resources help making the decision for you to start taking advantage of the immense power of eBPF enabled programs in production!
Inspired? Share the eBPF gospel with your kids! The Illustrated Children's Guide to eBPF 🐝
Ready to see eBPF in action? Schedule a demo with a Qpoint solution engineer and discover how eBPF-powered observability can transform your understanding of external API traffic.