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 within M[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 integer N (keep N bytes of the packet, or 0 to 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:

  1. seccomp-bpf Syscall Sandboxing: Container runtimes (Docker, containerd), browsers (Chromium), and sandboxes use cBPF programs attached via seccomp() to inspect struct seccomp_data (syscall number, instruction pointer, and up to 6 syscall args) because loading an unprivileged cBPF filter does not require CAP_BPF or CAP_SYS_ADMIN (only PR_SET_NO_NEW_PRIVS).
  2. libpcap / tcpdump: Expressions like tcpdump port 443 compile down to cBPF bytecode via pcap_compile() and attach to raw sockets via SO_ATTACH_FILTER. Inside the kernel, Linux immediately converts that cBPF filter into eBPF via bpf_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.