$RodHat_
MOTD

Unprivileged eBPF is getting locked out by default. It's five years overdue.

Published by

Unprivileged eBPF is getting locked out by default. It's five years overdue.
Photo: AI-generated — no human photographer / RodHat AI Cover

The kernel project is landing a config change in 6.16 that does what any reasonable security engineer would have demanded in 2019: kernel.unprivileged_bpf_disabled defaults to 2, which means unprivileged processes cannot call bpf(2), full stop, and root cannot flip it back at runtime without a reboot. It will take a while to percolate through distro defaults — distros carry their own kernel configs and their own timelines — but the signal from upstream is clear.

This is the right call. It took too long. And I want to be precise about why, because “eBPF bad” is a stupid take that misses the point, and I won’t be making it here.

What unprivileged eBPF actually meant

When eBPF programs can be loaded by unprivileged users, you are handing anyone with a shell account access to a JIT compiler that runs in kernel context. That sentence should have ended the debate in 2014 when BPF was getting its extended redesign, but it didn’t, because the use case sounded compelling: let normal users write network filters and tracing probes without requiring root.

The bpf(2) syscall submits a bytecode program to the kernel. The kernel’s BPF verifier is supposed to statically prove that the program is safe before the JIT compiles and runs it. The verifier is a few thousand lines of C that has to reason about memory bounds, pointer arithmetic, register state, and control flow — and it has to get every case right, because a wrong decision doesn’t produce a crash you can observe, it produces kernel memory that a userspace process can now read or write.

The verifier has not gotten every case right. Not even close.

The CVE ledger

The privilege-escalation CVEs out of eBPF in the past five years aren’t a bug here and a bug there. They’re a genre with recurring structure:

Verifier bypass via speculative execution. The verifier reasons about program correctness on one path; the JIT-compiled code runs on another path that the speculative execution unit prefers. Spectre variants applied to eBPF programs have produced reliable kernel read primitives — you can exfiltrate kernel memory to userspace through the timing side channel the verifier had no way to account for. CVE-2021-33624 is a clean example of this class.

Pointer type confusion. The verifier tracks register types to prevent you from dereferencing a scalar as a pointer. Bugs in how those types are propagated through helper calls or branches let you confuse a scalar for a kernel pointer, get a read or write, and escalate from there. This class has been a reliable source of CVEs since 2020 — CVE-2021-3490 used ALU32 operations to produce exactly this confusion, with a working public exploit following disclosure by about a week.

JIT emission errors. The JIT compiler translates verified bytecode to native code on multiple architectures. Architecture-specific JIT paths have had bugs that emit machine code with subtly different semantics than what the verifier proved safe, breaking the safety guarantee at the last step. CVE-2021-29154 on x86-64 is the canonical case.

Every one of these requires the attacker to be able to load a BPF program. If unprivileged BPF is disabled, the bpf(2) syscall returns EPERM before a single bytecode instruction reaches the verifier. The entire CVE class disappears from the unprivileged attack surface.

This is why the value of kernel.unprivileged_bpf_disabled=2 is not “the verifier bugs are fixed.” The value is “the verifier no longer needs to be correct for any principal you don’t trust.”

The part I’m not going to pretend isn’t true

eBPF is genuinely impressive infrastructure. I will not tell you otherwise.

The ability to attach probes to arbitrary kernel functions at runtime, aggregate histograms in the kernel before a single byte crosses the syscall boundary, trace syscall latency with sub-microsecond overhead — bpftrace one-liners have made me genuinely better at finding where latency is hiding, and I’ve been using dtrace since before most of the people building eBPF tooling were in the industry. The tool is real. The observability improvement over what we had before is real.

The performance case for eBPF in XDP, in tc-bpf, in sockmap is also real. Cloudflare’s rate limiting infrastructure runs on XDP eBPF. That is not toy-benchmark performance — that’s packets being processed at line rate in kernel context without a single trip to userspace. The design works.

What was never a good idea was making this available to any unprivileged process on the system, because the combination of “JIT compiler in the kernel” and “accepting bytecode from untrusted principals” is exactly the kind of design decision that produces a decade of CVEs. The io_uring structural attack surface is a different shape of the same problem: a powerful kernel feature that has a different security profile depending on who can access it.

What breaks, and what doesn’t

What breaks: nothing you care about on a production server, almost certainly. Unprivileged eBPF was primarily used in two contexts — container runtimes that needed network filtering without root (most of which have already switched to requiring CAP_NET_ADMIN), and some older versions of BCC and bpftrace that tried to run without privileges and failed gracefully back to a stub. Both of those use cases have been handled for a while.

What you might need to check: your observability tooling. bpftrace, perf bpf, BCC tools — all of these require the bpf(2) syscall and will fail with EPERM if run as an unprivileged user without capabilities. The solution is not to run them as root. The solution is to grant CAP_BPF (and usually CAP_SYS_ADMIN for full tracing access, or CAP_PERFMON for perf events) to the binary or the service account running the tool:

# Grant cap_bpf + cap_perfmon to bpftrace binary
setcap cap_bpf,cap_perfmon+eip /usr/bin/bpftrace

This is the correct answer. It gives bpftrace what it needs and nothing else, without handing it the full root-equivalent surface that CAP_SYS_ADMIN implies. If your observability pipeline runs in a container, add CAP_BPF to the container’s capability set in the security context — you’re explicitly granting what you need rather than disabling a protection because it was inconvenient.

Check sysctl kernel.unprivileged_bpf_disabled on your systems now. If it’s 0, you’re on a kernel and distro config that hasn’t made the change yet — consider setting it to 1 or 2 yourself and auditing what breaks. 1 is revocable at runtime (root can set it back); 2 is permanent until reboot. The upstream direction is 2. I’d go with 2 on any system where you control the kernel config and you don’t have a specific use case that requires unprivileged BPF access, because I’ve never seen that specific use case.

The memory safety mandate discussion from last week touches the same underlying dynamic: reducing attack surface at the design level is worth more than any amount of CVE patching after the fact. Unprivileged BPF wasn’t a bug; it was a design choice that made an entire class of bugs load-bearing. Default-off is the right ending.


For the mechanics of what the BPF verifier actually does — why static analysis of kernel-executed bytecode is hard, and what the type-confusion class of bugs looks like at the IR level — The Linux Programming Interface gets you through the foundational process and kernel memory model. Kerrisk doesn’t cover eBPF specifically (the book predates the extended redesign), but the chapter on process capabilities and setcap is exactly the background you need for the CAP_BPF question. For the network performance side, the XDP and tc-bpf use cases live on top of concepts that TCP/IP Illustrated covers thoroughly at the wire level.