Introduction: The Promise and the Illusion
eBPF gets pitched as a cure-all for security monitoring—a way to peer into the kernel without kernel modules, trace syscalls, inspect network packets, and enforce access control, all from the safety of a sandboxed virtual machine. The marketing suggests a transparent, unbypassable observability layer. The reality, as anyone who has spent time staring at x86-64 or ARM64 disassembly knows, is that eBPF sees what the kernel’s tracing infrastructure allows it to see. That infrastructure is built on a set of abstractions that leak, hide, and sometimes outright lie about what’s happening at the microarchitectural level. This article is for those who need to understand the gap between the eBPF sales pitch and the silicon truth—specifically, how speculative execution, memory ordering, and hardware design decisions create blind spots that no BPF program can illuminate.
What eBPF Actually Sees: The Tracepoint and Kprobe Model
eBPF programs attach to predefined hooks: tracepoints, kprobes, uprobes, and USDT probes. These hooks fire when the kernel’s instrumented code paths execute. For security monitoring, the most common attachment points are syscall entry/exit tracepoints and kprobes on security-sensitive functions like cap_capable or selinux_file_permission. The eBPF program receives a fixed set of arguments—register values, stack pointers, or tracepoint-specific fields—and can make a decision based on that data.
The problem is that these hooks are placed at the software-visible boundary. They capture the architectural state: the values in rax or x0, the contents of the sockaddr struct, the pathname string. They do not capture what the CPU did speculatively before the hook fired, nor do they reveal what the memory controller reordered behind the scenes. If you’re monitoring for unauthorized memory access, you’re relying on the kernel’s page fault handler to trigger a tracepoint. But speculative execution doesn’t generate page faults; it generates microarchitectural side effects that are invisible to eBPF unless you’re also instrumenting performance counters—and even then, the correlation is noisy.

Speculative Execution: The Elephant in the Trace Buffer
Consider a simple security use case: detecting when a process attempts to read from a protected memory region. You attach an eBPF program to the security_file_open LSM hook or the openat syscall tracepoint. Your program checks the filename against a denylist and drops the event if it matches. This works for overt attempts. It fails completely for Spectre-v1 gadgets that probe the protected file’s contents without ever reaching the hook. The CPU speculatively executes the load, fills the cache, and the attacker recovers the data via a timing side channel. Your eBPF monitor sees nothing because the architectural path—the one that triggers the hook—was never taken.
This isn’t a bug in eBPF; it’s a fundamental limitation of attaching probes to the architectural instruction stream. The verifier ensures your BPF program won’t crash the kernel, but it can’t ensure the kernel’s own code is free of Spectre gadgets. On ARM64, where Pointer Authentication (PAC) adds a cryptographic signature to return addresses, the situation is more layered. A PAC miss triggers an exception, which does create an architectural event. But the speculative window before the exception is still wide enough to leak data, and the exception handler itself may not be instrumented by your eBPF program. You’re monitoring the cleanup, not the crime.
Case Study: Bypassing eBPF-Based File Integrity Monitoring
Imagine an eBPF program attached to the security_file_permission LSM hook, designed to log any attempt to open /etc/shadow for writing. A user-space attacker with knowledge of the CPU’s branch predictor can train the predictor to mispredict the conditional branch that checks the file path. The attacker then triggers a speculative load of the shadow file’s contents into the cache, using a covert channel to exfiltrate the data. The eBPF program never fires because the speculative execution never reaches the LSM hook—it’s squashed by the branch resolution. The only trace is a cache timing perturbation, which eBPF can’t natively capture without custom tracepoint instrumentation that most kernels don’t ship.
This isn’t theoretical. The PACMAN attack against Apple’s M1 demonstrated that PAC-protected return addresses can be brute-forced speculatively, bypassing the architectural exception. An eBPF monitor on the arm64_insn_abort tracepoint would see the PAC miss, but only after the speculative window closed. The damage—a leaked kernel pointer or worse—is already done.
Memory Ordering and the Visibility Problem
Even without speculative execution, eBPF’s view of memory is constrained by the kernel’s memory model. On x86-64, the strong total-store order (TSO) model means that stores appear in program order to all observers. But eBPF programs running on different cores can still see stale data if the kernel code they’re tracing uses WRITE_ONCE without an accompanying memory barrier. The eBPF program reads the value that was visible at the time the tracepoint fired, which may not be the value that another core has already written.
On ARM64, the situation is worse. The weaker memory model allows more aggressive reordering, and eBPF programs inherit the memory ordering of the kernel context they’re attached to. If you’re monitoring a network socket buffer via a kprobe on tcp_sendmsg, the data you read from the sk_buff might not reflect the most recent writes from the DMA engine. The kernel’s dma_wmb() barrier ensures the NIC driver sees the correct data, but that barrier doesn’t guarantee visibility to an eBPF program running on a different core. You’re monitoring a snapshot that’s already out of date.

Kernel Memory Management: The Page Table Blind Spot
eBPF programs can read kernel memory via helper functions like bpf_probe_read_kernel, but they’re still subject to the kernel’s page table mappings. If a page is unmapped from the kernel’s direct map—a common technique for mitigating ret2dir attacks—the eBPF program will trigger a fault and be terminated. This is by design, but it means eBPF can’t monitor the very memory regions that are most interesting from a security perspective: the unmapped pages containing sensitive kernel data.
More insidiously, eBPF programs can’t detect when a page table entry is modified to redirect a virtual address to a malicious physical page. The modification happens via a store to the page table, which is just another memory write from the kernel’s perspective. Unless you’re tracing every set_pte call—which would be prohibitively expensive—you’re blind to page table manipulation. This is a fundamental limitation of monitoring at the software-visible level; the hardware’s memory management unit (MMU) operates below the eBPF instrumentation layer.
Firmware Extraction: When eBPF Can’t Even See the Target
On embedded devices—baseband processors, UEFI firmware, automotive ECUs—eBPF is often touted as a lightweight alternative to kernel modules for extracting firmware. The reality is messier. eBPF requires a running Linux kernel with CONFIG_DEBUG_INFO_BTF and a reasonably modern BPF subsystem. Most embedded devices run stripped, ancient kernels without BTF, or they use proprietary RTOSes where eBPF simply doesn’t exist. Even on Linux-based devices, the firmware is often mapped into memory regions that are explicitly excluded from the kernel’s direct map, making them inaccessible to bpf_probe_read_kernel.
I’ve spent weeks trying to dump baseband firmware from a Qualcomm modem using eBPF, only to discover that the relevant memory regions were behind an SMMU (System Memory Management Unit) that the kernel never mapped. The eBPF program could see the kernel’s view of the world, but the firmware lived in a separate address space, accessible only via a proprietary RPC mechanism. eBPF is a tool for monitoring the kernel’s internal state, not for breaking out of the kernel’s sandbox. If you need to extract firmware from a locked-down device, you’re better off with JTAG, fault injection, or DMA attacks—none of which are observable via eBPF.
Control Flow Integrity: The eBPF Verifier’s Own Blind Spots
eBPF itself is subject to control flow integrity (CFI) enforcement. The verifier ensures that BPF programs don’t contain indirect jumps to arbitrary addresses, and the JIT compiler emits code with hardware CFI support (e.g., Intel CET endbranch instructions). But the verifier’s analysis is static; it can’t account for speculative execution attacks against the BPF program itself. A Spectre-BTB attack could cause a BPF program to speculatively execute a gadget that leaks kernel memory, even though the architectural path is safe. The verifier’s safety guarantees are architectural, not microarchitectural.
On ARM64, the situation is complicated by PAC and BTI (Branch Target Identification). The kernel’s eBPF JIT can emit BTI instructions, but if the CPU speculatively executes a branch before the BTI check, the attacker can still redirect control flow. The hardware mitigations are probabilistic, not absolute. Relying on eBPF for security monitoring means relying on a subsystem that itself is vulnerable to the same microarchitectural attacks you’re trying to detect.

Practical Limitations: Performance and Probe Overhead
Attaching eBPF programs to high-frequency events—every syscall, every network packet—incurs measurable overhead. The verifier’s complexity limits (currently 1 million instructions) prevent deep analysis in a single program. Tail calls and BPF-to-BPF functions help, but they introduce latency and complicate the control flow. On ARM64, the situation is worse: the eBPF JIT is less mature, and some helper functions are not available, forcing fallback to the slower interpreter.
For security monitoring, this means you can’t simply attach a comprehensive eBPF program to every kernel function. You must choose a subset, which creates coverage gaps. An attacker who understands your eBPF policy can route their activity through unmonitored code paths. This is not a theoretical concern; it’s the same cat-and-mouse game that has plagued syscall monitoring since the days of strace evasion.
What eBPF Gets Right (and Why It Still Matters)
Despite these limitations, eBPF is a powerful tool when used with a clear understanding of its boundaries. It excels at monitoring the kernel’s architectural state: syscall arguments, network packet headers, file operations. It can enforce security policies at LSM hooks with minimal overhead. It can capture performance events and correlate them with system activity. The key is to treat eBPF as a supplementary mechanism, not a silver bullet. Pair it with hardware performance counters, firmware integrity checks, and microarchitectural side-channel detection to build a more complete picture.
For example, you can use eBPF to monitor perf_event_open calls and detect when a process is setting up a cache timing attack. Combine that with PEBS (Precise Event-Based Sampling) on Intel or SPE (Statistical Profiling Extension) on ARM to catch the speculative execution patterns that eBPF misses. This hybrid approach acknowledges that the kernel’s view is incomplete and compensates with hardware-level telemetry.
FAQ
Can eBPF detect Spectre or Meltdown attacks?
Not directly. eBPF operates on architectural events—syscalls, tracepoints, kprobes—while Spectre and Meltdown exploit microarchitectural side channels that leave no architectural trace. You can use eBPF to monitor for suspicious cache timing activity (e.g., frequent perf_event_open calls with precise event selection), but this is a heuristic, not a detection mechanism. The actual speculative execution happens below eBPF’s visibility threshold.
Is eBPF a viable replacement for kernel modules in security monitoring?
In many cases, yes—but with caveats. eBPF avoids the stability and security risks of loading custom kernel modules, and the verifier provides strong safety guarantees. However, eBPF programs are limited in what they can access: no arbitrary kernel memory, no direct hardware access, and no modification of kernel data structures. If your monitoring requires deep kernel introspection or hardware interaction, a kernel module may still be necessary. The tradeoff is safety versus capability.
How does eBPF handle ARM64 Pointer Authentication (PAC)?
eBPF itself doesn’t interact with PAC directly. The kernel’s eBPF JIT can emit PAC instructions to protect the BPF program’s control flow, but this is transparent to the eBPF developer. From a monitoring perspective, PAC adds complexity: a PAC miss generates an exception that eBPF can trace, but the speculative execution before the exception is still a blind spot. If you’re monitoring for control flow attacks, you need to correlate PAC miss exceptions with other signals—cache misses, branch mispredictions—to catch the speculative window.
Can eBPF be used to extract firmware from locked-down devices?
Rarely. eBPF requires a running Linux kernel with BTF support, which is absent from most embedded devices. Even when present, firmware is often mapped outside the kernel’s direct map or behind an IOMMU/SMMU, making it inaccessible to bpf_probe_read_kernel. For firmware extraction, you’re better off with hardware-level techniques: JTAG, SPI flash dumping, or fault injection. eBPF is a kernel observability tool, not a hardware hacking tool.
Conclusion: Trust, but Verify at the Microarchitectural Level
eBPF is a remarkable piece of engineering that has transformed kernel observability. But it’s not magic. It sees what the kernel’s instrumentation points expose, and those points are designed for the common case, not for the adversarial microarchitectural edge cases that define modern security research. If you’re building a security monitoring system on eBPF, understand its limitations: speculative execution, memory ordering, page table manipulation, and firmware isolation all create blind spots that no amount of BPF code can eliminate. Use eBPF as one layer in a defense-in-depth strategy, and always ask yourself: what is the hardware actually doing right now, and how much of it can I really see?