A Deep Dive into CVE-2023-2163: How Google Found and Fixed an eBPF Linux Kernel Vulnerability

Google's account of discovering and patching a CVSS 10.0 flaw in the Linux eBPF verifier where incorrect propagation of 32-bit register bounds to their 64-bit counterparts let a crafted program convince the kernel it was performing safe memory accesses when it was not.

A Deep Dive into CVE-2023-2163: How Google Found and Fixed an eBPF Linux Kernel Vulnerability

The eBPF verifier is the kernel’s last gate before user-controlled code runs with kernel privileges. When the verifier approves a program, the kernel trusts it completely — which means a single unsound analysis rule does not just create a bug, it creates a passkey that bypasses every security boundary that depends on the kernel’s own memory integrity.

Source

A deep dive into CVE-2023-2163: How We found and Fixed an eBPF Linux Kernel Vulnerability

By: Google Security Engineering
Published: Aug 08 2024
Type: Engineering article

Saved because the verifier’s register bounds tracking is the most important and most fragile part of eBPF’s security model — understanding precisely where it breaks is necessary for anyone writing eBPF programs or depending on eBPF-based security tooling.

Why a single wrong inference rule invalidates the entire proof

The eBPF verifier’s job sounds tractable: confirm that a program cannot do anything the kernel should not allow. No unbounded loops, no out-of-bounds memory access, no calls to arbitrary kernel functions. To do this, it simulates the program using abstract interpretation — running it with symbolic representations of what values could exist at each point in execution rather than with actual runtime data. For every register, the verifier maintains a set of bounds: unsigned minimum and maximum as 64-bit values, signed minimum and maximum, and separate 32-bit variants of each. Those bounds are updated at every instruction, narrowed by conditional branches, and consulted to decide whether pointer arithmetic stays inside safe memory regions.

The assumption built into this architecture is that the bounds are always sound — any actual runtime value a register holds at a given instruction is guaranteed to fall within the range the verifier believes it can. If that soundness property holds, the verifier’s approval is a proof, not an estimate. If it breaks anywhere in the analysis, the verifier’s stamp of approval becomes a promise the kernel has no way to enforce at execution time.

CVE-2023-2163 broke that soundness property through a gap between how the verifier tracked 32-bit sub-register operations and how it reflected those results into the full 64-bit register view. When a program compares or operates on the lower 32 bits of a register and then branches on the result, the verifier must narrow both the 32-bit and 64-bit bounds for that register to reflect what the branch has proved. The flaw was in that propagation step: under specific conditions, updating the 32-bit bounds after a conditional branch did not correctly tighten the 64-bit bounds. The 64-bit view remained wider than the truth. From that point forward in the analysis, the verifier believed a register’s value could span a range it actually could not — and, critically, a range larger than any allocation the program was legitimately authorized to access.

An attacker who understood this could construct a program that made the verifier’s widened 64-bit view plausible through legal-looking operations leading up to the exploitable branch, then used the resulting slack to perform pointer arithmetic the verifier considered in-bounds while the actual address landed anywhere in kernel memory. The verifier would approve the load or store, the kernel would execute it, and the attacker would have an arbitrary read or write primitive in kernel space. The CVSS score of 10.0 reflects what that primitive enables: full local privilege escalation, bypassing every Linux security mechanism that depends on the kernel’s own memory integrity.

The fix required ensuring that after any operation affecting 32-bit sub-registers, the 64-bit view was immediately derived from the updated 32-bit bounds rather than being left in its previous state. Because the 32-bit bounds after a branch are often tighter — the branch proved something about the low bits — deriving the 64-bit view from them properly tightens the combined analysis. The patch targeted the bounds propagation logic in the verifier’s scalar register handling, which is among the most performance-sensitive parts of the verifier since it runs on every instruction of every program the kernel loads.

This class of vulnerability is more dangerous than most kernel memory errors because of where it sits in the trust hierarchy. A typical kernel memory corruption bug requires a specific vulnerable code path to execute at the right time. A verifier flaw is exploitable by anyone with permission to load eBPF programs — and on modern systems with CAP_BPF available, that permission is often granted more broadly than kernel developers historically assumed. Container runtimes, Kubernetes environments with eBPF-based networking, and developer workstations running observability tooling can all be in scope.

The timing made the exposure especially significant. By 2023, eBPF had moved from a niche network packet filter to a central mechanism for production observability, security enforcement, and system tracing. Tools like Cilium for network policy, Falco for runtime threat detection, and various endpoint detection products attach eBPF programs to kernel hooks specifically because those hooks sit at a trusted boundary. A compromised verifier means those enforcement points can be bypassed from the same host they are supposed to protect — not just worked around, but actively defeated by a program that the security tool itself approved as safe.

The broader implication of Google’s disclosure is that “the verifier checked it” is not a complete security argument. The verifier has had critical soundness bugs before, and the correct response is to ensure kernels are patched, to audit who holds CAP_BPF or equivalent access, and to treat eBPF program loading from untrusted sources as a privilege boundary even on patched systems — because the patch history has not yet been long enough to establish confidence that no similar gap remains.

My take

What this shows is that the eBPF verifier is a proof system, not a filter, and proof systems fail categorically rather than probabilistically. A single wrong rule invalidates soundness for every program that exercises it, no matter how correctly the verifier handles everything else. That property makes verifier bugs uniquely dangerous — and means organizations that rely on eBPF-based security tooling should track verifier CVEs as carefully as they track privilege escalation vulnerabilities in user-space software.

Where this is useful

  • Evaluating the security posture of systems that grant CAP_BPF or unprivileged eBPF access to broad user populations or container workloads
  • Understanding the risk model for eBPF-based security tools — the tool’s own program loading can be the attack surface
  • Justifying kernel version requirements when eBPF is in the security-critical path
  • Not applicable as an argument against eBPF adoption — the verifier has been significantly hardened since 6.3, and the risk is manageable with patched kernels and controlled CAP_BPF grants; the lesson is precision, not avoidance

Apply this: Audit which users, containers, and service accounts on your systems have CAP_BPF or unprivileged BPF access, and confirm you are running a kernel version that includes the fix for CVE-2023-2163 (Linux 6.3+ or the relevant stable-series backport).

What I want to remember

Remember: The eBPF verifier is a proof system — one incorrect inference rule invalidates soundness for every program that exercises it, regardless of how correctly the verifier handles every other program it has ever seen.