eBPF is a sandboxed execution environment inside the Linux kernel that lets you run custom programs in response to events—syscalls, tracepoints, network packets, and more—without loading a kernel module. It gets sold as a universal observability and security layer, but on x86-64 and ARM64, the gap between the eBPF abstraction and what the silicon actually does is where the interesting failures live. If you’ve spent time staring at perf counters, page-table walks, or the output of objdump -d on a BPF object file, the promise of “safe, zero-overhead introspection” deserves a hard look. This article maps what eBPF can and cannot see at the microarchitectural boundary, how the verifier’s model diverges from physical reality, and why that matters when you’re trying to catch a rootkit that understands the difference.

What eBPF Actually Hooks Into

eBPF programs attach to predefined hook points: kprobes, tracepoints, raw tracepoints, LSM hooks, network sockets, and more. Each hook exposes a specific set of arguments and a return context. A kprobe on __x64_sys_execve gives you the pt_regs struct, which is a software-constructed view of the register file at entry—not the actual hardware register state. On x86-64, the kernel builds that struct from the stack frame the CPU pushed during the syscall transition. If an attacker has tampered with the stack pointer or is using a syscall proxy via sysenter with a modified MSR, the eBPF program sees the sanitized version, not the raw state. This isn’t a bug; it’s the design. But it means your eBPF-based intrusion detection is only as good as the kernel’s own sanitization routines.

Close-up of a CPU socket on a motherboard, illustrating the physical hardware layer beneath eBPF abstractions

Tracepoints vs. Raw Tracepoints: A Microarchitectural Distinction

Tracepoints are stable API points defined in /sys/kernel/debug/tracing/events. They sit on top of the kernel’s trace event infrastructure, so you pay a small overhead from argument packing and format string processing. Raw tracepoints bypass that layer and hand you direct access to the tracepoint’s arguments, but they’re still software constructs. On ARM64, the difference hits harder because the exception level transition from EL0 to EL1 involves a different set of saved registers than what the tracepoint eventually exposes. If you’re monitoring for a rootkit that hooks the vector table directly, neither tracepoint type will see it—the hook happens before the kernel’s event machinery runs.

The Verifier’s Model of the Machine

The eBPF verifier simulates execution of your program to ensure it terminates, doesn’t access out-of-bounds memory, and doesn’t leak kernel pointers. It models the BPF virtual machine, not the physical CPU. On x86-64, the JIT compiler translates BPF instructions to native x86-64 instructions, and the verifier’s safety guarantees depend on the correctness of that translation. The verifier assumes a 64-bit load from a map value is atomic with respect to other eBPF programs, but on x86-64, a 64-bit load is only atomic if the address is 8-byte aligned and the compiler emits a single mov instruction. The JIT does guarantee alignment for map values, but if you’re using a BPF ring buffer and reading from it in userspace, the alignment story changes. I’ve seen a case where a userspace consumer read a partially updated event because the kernel and userspace had different ideas about the ring buffer’s alignment on an ARM64 board with a non-coherent DMA cache. The eBPF program was correct; the hardware was not.

Abstract visualization of data flow and memory access patterns, representing the eBPF verifier's logical model

Spectre Mitigations and the Verifier’s Blind Spot

The verifier inserts lfence instructions on x86-64 to mitigate Spectre v1 when a BPF program performs a bounds check followed by a memory access. But the verifier’s Spectre mitigations are coarse. It doesn’t model the branch predictor state, the BTB, or the RSB. If an attacker can train the branch predictor from userspace before the eBPF program runs, the lfence might serialize the pipeline too late. This is a known class of attacks—Spectre v1 against eBPF programs was demonstrated in 2021—and the kernel’s response has been to add more lfence instructions and to unmap BPF programs from userspace. But on ARM64, the equivalent mitigation is a sb (speculation barrier) instruction, and its semantics are different. The verifier treats them as interchangeable; the microarchitecture does not.

What eBPF Cannot See: Below the Kernel’s Horizon

eBPF programs run in kernel context, which means they cannot observe anything that happens before the kernel takes control. On x86-64, System Management Mode (SMM) code runs entirely outside the kernel’s view. A rootkit installed in SMM can intercept syscalls, modify MSRs, and tamper with the page tables without any eBPF hook firing. The same applies to ARM64’s EL3 secure monitor. If your threat model includes firmware-level implants, eBPF is a convenience tool, not a security boundary. You need to be reading SPI flash directly or using a PCIe analyzer.

Even within the kernel, eBPF cannot hook into arbitrary code paths. Kprobes work by replacing the first instruction of a function with an int3 (x86-64) or a breakpoint instruction (ARM64). If the function is very short—say, a single ret—the kprobe infrastructure will refuse to instrument it because there’s no room to place the breakpoint. More importantly, if an attacker has patched the kernel’s text section directly, the kprobe might be placed on the original instruction but the attacker’s modified code runs instead. eBPF sees nothing because the hook was never triggered.

Rows of server hardware in a data center, highlighting the physical infrastructure where firmware-level threats reside

Page-Table Manipulation and the TLB

eBPF programs can read user memory via bpf_probe_read_user, which walks the page tables in software. If an attacker has installed a shadow page table by modifying the CR3 register (x86-64) or TTBR0_EL1 (ARM64), the eBPF helper will walk the attacker’s page table and return the attacker’s data. The kernel’s own page-table walker is used, so any hardware-assisted virtualization (EPT/NPT) tricks that redirect the walker will also fool eBPF. The only way to detect this is to compare the eBPF-observed memory with a direct physical memory dump, which eBPF cannot do because it doesn’t have access to the physical address space.

Practical eBPF Security Monitoring: What Works

Despite these limitations, eBPF is useful for a specific class of monitoring tasks. File integrity monitoring via LSM hooks works well because the LSM framework is called after the kernel has resolved the file path and performed permission checks. An eBPF program attached to security_file_open can log every file open with the process context, and it’s difficult for a userspace rootkit to bypass this without also disabling the LSM entirely—which requires a kernel exploit. Similarly, network monitoring via BPF_PROG_TYPE_SOCKET_FILTER or XDP can catch exfiltration attempts at the socket layer, though a kernel module could bypass this by injecting packets below the socket layer.

For syscall monitoring, raw tracepoints on sys_enter and sys_exit are the most reliable because they fire on every syscall, even those from kernel threads. But they’re also noisy. A typical production system generates hundreds of thousands of syscalls per second, and filtering them in eBPF requires careful use of maps and bounded loops. The verifier limits loops to a maximum number of iterations, so you cannot iterate over a variable-length array. You have to unroll or use a bounded loop with a known maximum, which wastes instructions and bloats the program size. On x86-64, the JIT has a limit of 1 million instructions per program; on ARM64, the limit is lower due to instruction cache constraints. I’ve seen security monitoring programs hit this limit when trying to filter on multiple syscall arguments.

Detecting Kernel Module Hiding

One concrete use case: detecting hidden kernel modules. A rootkit often removes itself from the module list but keeps its code in memory. You can write an eBPF program that periodically reads the kernel’s module list and compares it to a known-good baseline. But the module list is a doubly linked list, and a rootkit can unlink itself without freeing the memory. eBPF cannot scan physical memory for orphaned code regions. A better approach is to use eBPF to monitor kprobe registration itself—if a rootkit tries to hide by unhooking your probes, you can detect the unhook attempt. But a sophisticated rootkit will just patch the kernel’s kprobe infrastructure to return success without actually removing the probe, and your eBPF program will never know.

eBPF on ARM64: Embedded Device Constraints

On ARM64 embedded devices—think IoT gateways, automotive ECUs, or industrial controllers—eBPF faces additional constraints. Many of these devices run kernels compiled without CONFIG_DEBUG_INFO_BTF, which means you cannot use CO-RE (Compile Once, Run Everywhere) eBPF programs. You have to compile your eBPF program against the exact kernel headers of the target device, which is often impossible because the vendor doesn’t release them. Even if you have the headers, the device’s kernel might be built with a different toolchain that changes struct layouts. I’ve spent days reverse-engineering the task_struct layout on a custom ARM64 kernel just to get a simple process-monitoring eBPF program to run.

Another ARM64-specific issue: the memory model. eBPF assumes a strongly ordered memory model similar to x86-64, but ARM64 is weakly ordered. The JIT inserts memory barriers where necessary, but the verifier doesn’t model the ARM64 memory ordering rules. If your eBPF program uses a map to communicate between CPUs, you need to understand the underlying barrier semantics. A BPF_STX | BPF_XADD instruction is translated to an atomic add with acquire-release semantics on ARM64, which is correct. But if you use a plain store followed by a load on a different CPU, the verifier won’t warn you about the lack of ordering, and you might read stale data. This is a classic bug that manifests only under heavy load on specific ARM64 implementations.

Firmware Extraction: When eBPF Is the Wrong Tool

I’ve been asked whether eBPF can help extract firmware from locked-down embedded devices. The short answer is no. eBPF runs inside the kernel and has no direct access to physical memory, MMIO, or SPI controllers. On x86-64, you could theoretically use eBPF to trigger a kernel function that reads from the SPI flash, but that function must already exist and be accessible via a kprobe or tracepoint. If the firmware is locked down, the kernel likely doesn’t expose such a function. On ARM64, many embedded devices use secure boot with a chain of trust that starts in ROM. eBPF cannot see anything that happens before the kernel boots, so it’s useless for extracting boot ROM code. For that, you need fault injection, power glitching, or a debug interface that the vendor forgot to disable.

There’s one edge case: if the kernel has a driver that maps the firmware region into kernel memory, you might be able to read it via bpf_probe_read_kernel. But this requires that the driver actually maps the region and that the mapping is accessible from eBPF context. Most firmware drivers unmap the region after initialization. And if the device uses an IOMMU, the mapping might be restricted to the driver’s own address space, which eBPF cannot access. I’ve tried this on an Intel NUC with a locked SPI flash; the eBPF program returned zeros because the mapping wasn’t present in the kernel’s direct map.

FAQ

Can eBPF detect a rootkit that hooks syscalls by modifying the syscall table?

Yes, if the rootkit modifies the syscall table in a way that’s visible to the kernel’s own syscall dispatch. An eBPF program attached to a raw tracepoint on sys_enter will see the arguments that the kernel passes to the syscall handler. If the rootkit has replaced the handler, the eBPF program will see the arguments that the rootkit’s handler receives. However, if the rootkit hooks the syscall at a lower level—for example, by modifying the MSR that points to the syscall handler on x86-64—the kernel’s syscall dispatch code might never run, and the eBPF program won’t fire. Similarly, on ARM64, if the rootkit modifies the vector table entry for sync exceptions, the kernel’s syscall handler is bypassed entirely.

How reliable is eBPF for detecting kernel-level exploits in real time?

It depends on the exploit. For exploits that use standard kernel interfaces—such as triggering a use-after-free via a syscall—eBPF can be very effective if you have the right hooks in place. But for exploits that operate below the kernel’s abstraction layer—such as a Rowhammer attack that flips page-table entries directly in DRAM—eBPF is blind. The kernel’s memory management code might eventually notice the corruption, but by then the attacker has already escalated privileges. eBPF is a software tool; it cannot see hardware-level attacks.

Does eBPF introduce its own attack surface?

Yes. The eBPF verifier is a large, complex piece of code that has had its own vulnerabilities. A bug in the verifier could allow an attacker to load a malicious eBPF program that escapes the sandbox and gains arbitrary kernel read/write. The JIT compiler is another attack surface; a bug in the JIT could translate a safe BPF instruction into an unsafe native instruction sequence. Additionally, eBPF programs can consume kernel resources—memory for maps, CPU time for execution—and a poorly written or malicious program could cause a denial of service. The kernel has mitigations for this, such as memory limits and a configurable instruction count limit, but these aren’t foolproof.

What is the performance overhead of eBPF security monitoring?

The overhead depends on the hook point and the complexity of your eBPF program. A simple kprobe that logs a string might add a few microseconds per event. A raw tracepoint on sys_enter that filters on multiple arguments and updates a map can add tens of microseconds. On a busy system, this can add up to a significant percentage of CPU time. The JIT compilation reduces overhead compared to the interpreter, but the verifier’s safety checks—such as Spectre mitigations—add their own cost. On x86-64, the lfence instructions inserted by the verifier can stall the pipeline. On ARM64, the memory barriers inserted for weak ordering can be expensive. You should always benchmark your eBPF programs under realistic load before deploying them in production.

Where eBPF Fits in a Defense-in-Depth Strategy

eBPF is a powerful tool for observing the kernel’s behavior from within the kernel. It’s not a silver bullet, and it’s not a replacement for hardware-level security mechanisms like IOMMU, secure boot, or physical memory encryption. For the audience of this blog—people who care about what happens at the boundary between software and silicon—eBPF is best understood as a software sensor with a well-defined but limited field of view. Use it to catch the low-hanging fruit: process anomalies, file system changes, network connections. But don’t trust it to catch a rootkit that understands the microarchitecture better than the kernel does. For that, you need to be looking at the hardware directly.

In a future article, I’ll walk through building an eBPF-based syscall monitor that compares the kernel’s view of a process with the hardware’s view, using performance counters to detect discrepancies. That’s where the real fun begins.