eBPF is a kernel-resident virtual machine that lets you attach sandboxed programs to tracepoints, kprobes, uprobes, and a growing set of LSM hooks without loading a kernel module. It sits adjacent to the same machinery that enforces CET shadow stacks, PAC return-address signing, and MTE tag checks, but it does not magically inherit their guarantees. For anyone doing low-level vulnerability research on x86-64 or ARM64, eBPF is less a security product and more a way to inspect what the kernel, scheduler, and compiler-generated code actually do before an abstraction layer lies to you.

This guide is for people who already distrust vendor claims about runtime visibility. It covers what eBPF can observe, where its isolation model breaks down, and how to use it as an instrumentation tool rather than a replacement for hardware-enforced control-flow integrity.

Close-up of a server motherboard with CPU socket and memory slots

What eBPF Actually Is

eBPF is an in-kernel register-based VM with a verifier that attempts to prove memory safety and termination before a program is JIT-compiled to native x86-64 or ARM64 instructions. The verifier is the interesting part: it performs abstract interpretation over the eBPF instruction stream, tracks register types, and rejects programs that could read arbitrary kernel memory or loop indefinitely. In practice, the verifier is a large C program with a long history of privilege-escalation bugs, which should temper any claim that eBPF is inherently safe.

The execution model is event-driven. You attach a program to a hook, the kernel invokes it when the event fires, and the program can read a limited context structure, update maps, and in some cases modify the event’s outcome. The hooks that matter for security monitoring are:

  • Tracepoints — static markers in the kernel source, such as sys_enter_execve or sched_process_exec.
  • kprobes/kretprobes — dynamic probes on kernel function entry and return, useful for functions that lack tracepoints.
  • LSM hooks — security module callbacks that can deny operations, not just observe them.
  • Network hooks — XDP, TC, and socket filters that see packets before or after the network stack.

Each hook exposes a different slice of kernel state. A tracepoint gives you a stable ABI-ish structure. A kprobe gives you raw register state and a function name. An LSM hook gives you the ability to block. Conflating these is how vendor marketing produces nonsense like “full kernel visibility.”

Why Security Teams Adopt eBPF

The standard pitch is that eBPF replaces kernel modules, auditd, and ptrace-based monitoring with lower overhead and fewer stability risks. There is some truth to that. A well-written eBPF program attached to a tracepoint can run in microseconds, and the verifier prevents the most obvious crashes that plague out-of-tree kernel modules.

But the real reason security teams adopt eBPF is that it gives them a programmable filter in the kernel. Instead of shipping every syscall event to userspace and filtering there, you can filter in the kernel, aggregate in maps, and only wake userspace when something interesting happens. That is a genuine architectural improvement over auditd, which is essentially a syscall-level firehose with a userspace parser.

For low-level researchers, eBPF is also a cheap way to instrument kernel behavior without rebuilding the kernel or fighting with ftrace’s limited scripting. You can attach a kprobe to copy_from_user, record the size and destination, and correlate that with page-fault activity. You can trace do_mmap and see exactly what the dynamic linker is doing. You can watch flush_icache_range on ARM64 and catch JIT code generation in real time.

Rows of server racks in a dark data center

The Isolation Model Is Not a Security Boundary

Here is where vendor claims need scrutiny. eBPF programs run in kernel context, but they are not isolated from the kernel in any hardware-enforced sense. The verifier is a software gate. If the verifier has a bug, or if the JIT compiler emits incorrect native code, the eBPF program can read or write arbitrary kernel memory. This has happened repeatedly.

On x86-64, the JIT compiler emits native code that runs with the kernel’s normal privilege level. There is no separate page table, no ring transition, and no CET shadow stack for eBPF programs unless the kernel explicitly enables it. On ARM64, the situation is similar: eBPF JIT output runs at EL1, and PAC is not automatically applied to eBPF-generated code. If you are relying on eBPF as a security boundary, you are relying on a C program that has been wrong before.

The correct mental model is that eBPF is a constrained execution environment, not a sandbox. It reduces the attack surface of kernel instrumentation, but it does not eliminate it. Anyone who tells you otherwise is selling something.

What eBPF Can Actually See

Let’s be concrete. Here is what eBPF can observe at each major hook type, and what it cannot.

Syscall Tracepoints

Attaching to sys_enter_execve gives you the filename, argv, and envp pointers. You can read the filename string with bpf_probe_read_user or bpf_probe_read_kernel, depending on the kernel version. You can record the process’s PID, UID, and cgroup. You cannot see the file’s contents, the ELF headers, or what the process will do after execve returns. You also cannot see syscalls that bypass the tracepoint, such as direct int 0x80 invocations on x86-64, which still work and still hit the syscall table but may not trigger the same tracepoint path in all kernel versions.

kprobes on Memory Management

A kprobe on do_mmap or __vmalloc shows you the requested size, flags, and protection bits. This is useful for detecting JIT spraying, where an attacker allocates executable memory and fills it with shellcode. But a kprobe only sees the function entry and exit. It does not see the page-table walk, the TLB flush, or the actual physical page allocation. On ARM64 with MTE enabled, a kprobe does not see the tag assignment unless you also probe the MTE-specific functions, which are not always exported.

LSM Hooks

LSM hooks are the only eBPF attachment points that can deny an operation. A program attached to file_open can return -EPERM and block the open. This is powerful, but it is also a policy enforcement point, not an observation point. If you use LSM hooks for monitoring, you are changing kernel behavior, and that has consequences for stability and correctness. A buggy LSM program can prevent the system from booting or lock out legitimate processes.

Network Hooks

XDP programs see packets before the network stack allocates an skb. This is the lowest-overhead packet filtering point in the kernel. You can drop, redirect, or modify packets at line rate on many NICs. But XDP does not see TCP state, connection tracking, or application-layer data without additional parsing. TC hooks see packets after the stack has done some work, which means more context but also more overhead.

Where eBPF Breaks Down

eBPF’s limitations are not always obvious from the documentation. Here are the ones that matter for security monitoring.

Verifier Complexity

The verifier is a multi-thousand-line C program that attempts to prove properties about eBPF bytecode. It has known limitations: it cannot handle loops with variable bounds, it has a maximum instruction count, and it rejects some programs that are actually safe. This means you will spend time restructuring code to satisfy the verifier, and you will occasionally hit verifier bugs that cause false positives or false negatives. The verifier is also version-dependent: a program that passes on Linux 6.1 may fail on 6.6 because the verifier’s analysis changed.

Spectre Mitigations

On x86-64, the kernel applies Spectre mitigations to eBPF programs, including retpolines and, on some CPUs, eIBRS. This adds overhead and changes the native code that the JIT emits. If you are trying to measure exact instruction counts or cache behavior, eBPF is not a clean instrument. The JIT output is also affected by the kernel’s hardening options, such as CONFIG_BPF_JIT_ALWAYS_ON and CONFIG_RETPOLINE.

ARM64 PAC and BTI

On ARM64, the kernel can enable pointer authentication and branch target identification for eBPF JIT output, but this is not universal. Some kernels disable PAC for eBPF because the verifier does not model PAC correctly. If you are researching PAC bypasses, eBPF is not a reliable way to test them. The JIT output may or may not have PAC instructions, and the verifier may reject programs that manipulate pointers in ways that PAC would otherwise protect.

Map Semantics

eBPF maps are the shared memory between eBPF programs and userspace. They are not coherent with the CPU cache in the way you might expect. On x86-64, map updates use atomic operations, but the memory ordering is not always what you want. On ARM64, the kernel uses stlr and ldar for some map operations, but not all. If you are using eBPF to detect race conditions or memory-ordering bugs, you need to understand the exact instructions the JIT emits for map access.

Practical Examples for Low-Level Researchers

Here are three concrete use cases that fit this blog’s focus.

Detecting JIT Code Generation

Attach a kprobe to bpf_int_jit_compile on x86-64 or bpf_jit_compile on ARM64. Record the program’s instruction count and the address of the JIT output. Correlate that with do_mmap calls that request PROT_EXEC. This gives you a timeline of when executable memory is allocated and when code is written to it. It does not give you the code itself, but it narrows the search space for JIT spraying.

Tracing Page-Fault Handling

Attach a kprobe to do_page_fault and record the faulting address, the error code, and the current process. On x86-64, the error code tells you whether the fault was a protection violation or a not-present fault. On ARM64, the equivalent is do_mem_abort, which gives you the fault status register. This is useful for detecting attempts to probe kernel memory from userspace, which is a common first step in privilege-escalation exploits.

Monitoring Firmware Extraction Attempts

On embedded devices, firmware extraction often involves reading from /dev/mem or a baseband-specific device node. Attach an LSM hook to file_open and filter for paths that match /dev/mem or /dev/kmem. Record the PID, UID, and parent process. This does not prevent the read, but it gives you an audit trail. If you want to block the read, you can return -EPERM from the LSM hook, but be prepared for the device to misbehave if a legitimate process needs that access.

Developer examining code on a monitor in a dimly lit lab

Tooling That Does Not Suck

The eBPF tooling ecosystem is fragmented, but a few tools are worth using.

  • bpftrace — a high-level tracing language that compiles to eBPF. Good for quick experiments, but the abstraction leaks when you need precise control over the generated code.
  • libbpf — the C library for loading eBPF programs. This is the lowest-level stable interface, and it is what you should use for production monitoring.
  • cilium/ebpf — a Go library that generates eBPF programs from Go code. Useful if you are already in a Go environment, but the generated code is not always what you expect.
  • bpftool — the Swiss Army knife for inspecting loaded programs, maps, and JIT output. Use bpftool prog dump jited to see the native instructions.

For low-level work, bpftool prog dump jited is essential. It shows you the exact x86-64 or ARM64 instructions that the JIT emitted, which is the only way to verify that the verifier’s model matches reality.

Vendor Claims vs. Reality

Several commercial products claim to use eBPF for “runtime security” or “zero-trust workload protection.” The marketing usually omits the following:

  • eBPF programs run with kernel privileges. A verifier bug is a kernel bug.
  • eBPF does not see everything. It sees what the hooks expose, and the hooks are not comprehensive.
  • eBPF overhead is not zero. The verifier adds compile-time overhead, and the JIT output adds runtime overhead. On some workloads, the overhead is measurable.
  • eBPF is not a replacement for hardware-enforced mitigations. CET, PAC, and MTE operate at a different level of the stack. eBPF can observe some of their effects, but it cannot enforce them.

If a vendor claims their eBPF agent provides “complete visibility,” ask them to show the kprobe that sees a PAC authentication failure on ARM64. There is not one, because PAC failures are handled by the hardware before the kernel’s exception handler runs, and the exception handler does not expose the failed authentication context to eBPF.

What to Build Next

If you are setting up eBPF monitoring on your own systems, start with syscall tracepoints and a small set of kprobes on memory-management functions. Use bpftool prog dump jited to verify the JIT output. Do not trust the verifier blindly; read the generated native code and look for places where the verifier’s assumptions might not hold.

For this blog, the natural next step is a deep dive into the eBPF verifier’s abstract interpretation algorithm, with a focus on the register-state tracking that prevents out-of-bounds reads. That is a topic that deserves its own article, and it connects directly to the microarchitectural focus of this site.

FAQ

Can eBPF detect Spectre or Meltdown attacks?

Not directly. Spectre and Meltdown are microarchitectural attacks that exploit speculative execution. eBPF programs run after speculation has been resolved, so they cannot see the transient execution window. What eBPF can do is detect the effects of a Spectre attack, such as unusual cache-access patterns or attempts to read kernel memory from userspace. But that is indirect evidence, not direct observation.

Is eBPF safe to run on production systems?

It depends on what you mean by safe. The verifier prevents most memory-safety bugs, but it is not a formal proof. The JIT compiler has had bugs that produced incorrect native code. The kernel’s eBPF subsystem has had privilege-escalation vulnerabilities. If you are running eBPF on a production system, you are accepting a small but nonzero risk of kernel compromise. For most security-monitoring use cases, that risk is acceptable. For high-assurance systems, it may not be.

How does eBPF interact with CET, PAC, and MTE?

On x86-64, eBPF JIT output is subject to the same CET shadow-stack and IBT enforcement as other kernel code, but only if the kernel is built with those options. On ARM64, PAC and BTI for eBPF JIT output are optional and not always enabled. MTE operates at the memory-tag level and is orthogonal to eBPF; eBPF programs do not see MTE tags unless they explicitly probe the MTE-specific kernel functions. In general, eBPF does not weaken these mitigations, but it does not strengthen them either.

What is the difference between eBPF and kernel modules for security monitoring?

A kernel module has full access to kernel memory and can do anything the kernel can do. eBPF is constrained by the verifier and the hook definitions. A kernel module can crash the kernel with a single bad pointer dereference. eBPF is supposed to prevent that, but the verifier is not perfect. The practical difference is that eBPF programs are easier to load and unload, do not require kernel headers, and are less likely to cause a kernel panic. But they are also less capable. If you need to hook a function that has no tracepoint or kprobe-accessible path, a kernel module may be your only option.