eBPF vs cBPF
Classic BPF (cBPF) is a legacy, 32-bit in-kernel packet-filtering bytecode machine, whereas Extended BPF (eBPF) is a general-purpose, 64-bit RISC virtual machine that turns the Linux kernel into a programmable event-driven environment.
In modern Linux kernels (since 3.18+), the cBPF execution engine no longer exists independently—any cBPF program loaded into the kernel (via setsockopt(SO_ATTACH_FILTER) or seccomp) is transparently translated in-kernel into eBPF instructions before verification and JIT compilation.
Architectural & Feature Comparison
| Feature / Dimension | cBPF (Classic BPF) | eBPF (Extended BPF) |
|---|---|---|
| Register Count & Width | 2 registers (A accumulator, X index), 32-bit wide |
11 registers (R0–R10), 64-bit wide (maps 1:1 to hardware registers on x86_64, ARM64, RISC-V) |
| Scratch Stack Space | M[0..15] (16 words = 64 bytes total scratch memory) |
512-byte stack per program (R10 is a read-only frame pointer) |
| Persistent State / IPC | None. Stateless across events; communicates only via return code (pass/drop/truncate bytes). | eBPF Maps (hash tables, arrays, ring buffers, LPM tries) shared across kernel invocations and user space via file descriptors. |
| Kernel Helper Calls | None. Can only read packet data and fixed packet metadata (skb->len, protocol, etc.). |
Extensive (bpf_ call instruction + kfuncs). Can read kernel memory, manipulate sockets, get timestamps, walk stacks, or send signals. |
| Control Flow & Loops | Strictly acyclic (DAG only); forward jumps only, no backward jumps. | Bounded loops (Linux 5.3+), bpf_loop() helper, open-coded iterators, and function-to-function calls. |
| Instruction Encoding | 8 bytes (u16 opcode, u8 jt, u8 jf, u32 k) |
8 bytes (u8 opcode, u4 dst_reg, u4 src_reg, s16 offset, s32 imm), plus 16-byte wide instructions for 64-bit immediates. |
| Attach Points | Raw sockets (tcpdump/libpcap) and seccomp-bpf syscall filters. |
kprobes/uprobes, tracepoints, fentry/fexit, XDP, TC, cgroups, LSM hooks, socket operations, and schedulers (sched_ext). |
| Entry Syscall | setsockopt() or prctl(PR_SET_SECCOMP) / seccomp() |
Dedicated bpf() syscall (BPF_PROG_LOAD, BPF_MAP_CREATE, etc.) |
Core Technical Differences
Calling Convention & JIT Efficiency
cBPF was designed around an accumulator model (A and X registers) dating back to 1992. Because modern 64-bit CPUs are register-rich, mapping a 2-register 32-bit accumulator model onto x86_64 or ARM64 required awkward register spilling and extra instructions.
eBPF redesigned the instruction set to mirror modern 64-bit ABIs (System V AMD64 / AAPCS64):
R0: Return value from helper functions and exit value of the eBPF program.R1–R5: Function arguments passed to in-kernel helper functions (caller-saved).R6–R9: Callee-saved registers preserved across helper calls.R10: Read-only frame pointer to access the 512-byte stack.
Because eBPF registers map directly to physical CPU registers (e.g., R0 $\rightarrow$ rax, R1 $\rightarrow$ rdi on x86_64), the in-kernel JIT compiler translates eBPF bytecode to native machine instructions nearly 1-to-1 with minimal overhead.
Verification: Simple DAG Walk vs. Abstract Interpretation
- cBPF Verifier (
bpf_check_classic): Performs a basic single-pass check. It ensures there are no backward jumps (guaranteeing termination), checks that array indices stay withinM[0..15], and prevents division by zero. - eBPF Verifier: Performs full abstract interpretation over all possible execution paths. It tracks the exact type, bounds, and liveness of every register and stack slot (e.g.,
SCALAR_VALUE,PTR_TO_CTX,PTR_TO_MAP_VALUE_OR_NULL), enforces null-pointer checks before dereferencing, prevents speculative execution leaks (Spectre mitigations), and tracks reference counts on kernel objects.
Statefulness and User-Space Interaction
- cBPF cannot store data between invocations. In
tcpdump, the cBPF filter runs on a packet and returns an integerN(keepNbytes of the packet, or0to drop it). The matching packet still has to be copied to user space for aggregation. - eBPF can aggregate data directly in-kernel using eBPF Maps and atomic instructions. Instead of dumping every event to user space, an eBPF program can update an in-kernel histogram or per-CPU hash map and let user space poll only the summary—or stream structured events zero-copy via
BPF_MAP_TYPE_RINGBUF.
Where cBPF Still Lives Today
Even though eBPF dominates observability, networking (Cilium/XDP), and security (Falco/Tetragon), you will still encounter cBPF directly in two primary places because it works unprivileged:
seccomp-bpfSyscall Sandboxing: Container runtimes (Docker, containerd), browsers (Chromium), and sandboxes use cBPF programs attached viaseccomp()to inspectstruct seccomp_data(syscall number, instruction pointer, and up to 6 syscall args) because loading an unprivileged cBPF filter does not requireCAP_BPForCAP_SYS_ADMIN(onlyPR_SET_NO_NEW_PRIVS).libpcap/tcpdump: Expressions liketcpdump port 443compile down to cBPF bytecode viapcap_compile()and attach to raw sockets viaSO_ATTACH_FILTER. Inside the kernel, Linux immediately converts that cBPF filter into eBPF viabpf_migrate_filter()and runs it through the eBPF JIT.
FAQ
If the Linux kernel converts cBPF to eBPF anyway, why doesn't seccomp-bpf support eBPF directly?
seccomp-bpf only accepts cBPF for two security reasons: unprivileged attack surface and TOCTOU memory races. First, any unprivileged process can load a seccomp filter (PR_SET_NO_NEW_PRIVS); exposing the complex 20,000-line eBPF verifier to unprivileged users opens a massive surface for kernel privilege-escalation bugs, whereas cBPF's ~150-line verifier is trivial to audit. Second, seccomp runs at the raw syscall entry gate before user-space pointers are copied into kernel memory. Even if eBPF could dereference syscall pointer arguments (like file paths), another user-space thread could mutate that memory right after the check passes—a classic Time-of-Check to Time-of-Use (TOCTOU) vulnerability.
Is struct seccomp_data used by all cBPF programs?
No, struct seccomp_data is exclusive to the seccomp subsystem, not general cBPF. While network cBPF filters (like tcpdump via SO_ATTACH_FILTER) operate on raw network packets (struct sk_buff), seccomp populates a 64-byte struct seccomp_data containing the syscall number, CPU architecture, instruction pointer, and six raw scalar register arguments (args[0..5]). Beyond cBPF filters, struct seccomp_data is also passed to user-space supervisors via SECCOMP_RET_USER_NOTIF and ptrace tracers.
Does BPF LSM replace seccomp-bpf?
No, BPF LSM (BPF_PROG_TYPE_LSM) complements seccomp-bpf rather than replacing it, and production security stacks often run both. seccomp-bpf can be loaded by unprivileged processes to block unused syscalls right at the kernel entry gate—stopping exploits before buggy kernel argument-parsing code ever runs. In contrast, BPF LSM requires root privileges (CAP_BPF and CAP_MAC_ADMIN) and hooks deeper inside the kernel (security_* hooks) after user pointers have been safely copied and resolved into kernel objects (like struct file * or struct inode *), enabling TOCTOU-free auditing and Mandatory Access Control.
