eBPF Basics
Mainack Mondal
Sandip Chakraborty
CS 60203
Autumn 2026
Outline
What is eBPF?
Name history
BPF = Berkeley Packet Filter (McCanne and Jacobson, 1993), built for tcpdump. Extended BPF arrived in Linux 3.18 (2014). Today "BPF" usually means eBPF.
Networking
XDP, tc, load balancing, Kubernetes networking (Cilium)
Observability
Tracing, profiling, latency histograms (bpftrace, BCC)
Security
Syscall filtering (seccomp), LSM policies, runtime detection
Scheduling and more
TCP congestion control, CPU schedulers (sched_ext)
Why eBPF? Changing Kernel Behaviour Was Hard
Suppose you want to count packets per flow, find slow disk I/O, or drop attack traffic early. Before eBPF, you had three options:
Change the kernel source
Write a kernel module
Do it in user space
eBPF: the fourth option
Classic BPF (cBPF) vs Extended BPF (eBPF)
Feature | cBPF (1993) | eBPF (2014 onwards) |
Registers | 2 x 32-bit (A, X) | 11 x 64-bit (R0 to R10) |
Memory | 16-word scratch area | 512-byte stack + maps |
Jumps | Forward only | Forward and backward (bounded loops) |
Kernel services | None | Helpers and kfuncs |
State across runs | None | Maps |
Attach points | Sockets (tcpdump), seccomp | Network, tracing, security, scheduler |
Loading | setsockopt(SO_ATTACH_FILTER) | bpf() system call |
Execution | Interpreter (JIT added later) | JIT on all major architectures |
See cBPF yourself
tcpdump compiles its filter to cBPF. Print it with:
$ tcpdump -d 'tcp port 80'
cBPF is still around
The Big Picture: Who Does What
1. Write
Restricted C program
2. Compile
clang → BPF bytecode
3. Load
Loader: libbpf, bpftool, bpftrace
User app
Reads maps, shows results
user space
kernel
4. Verify
Is it safe? Else reject
5. JIT
Bytecode → machine code
6. Attach
To a hook: XDP, kprobe, ...
bpf() syscall
7. Run
On every event; calls helpers
Maps
Shared key/value storage: counters, histograms, flow tables, event rings. Both the program and user space can read and write them.
blue: user space pink: kernel green: shared
How It Works: From C Code to a Running Program
1. Write
Restricted C (or Rust, Go via libraries)
2. Compile
clang -target bpf → ELF object with BPF bytecode
3. Load
bpf(BPF_PROG_LOAD) syscall, usually via libbpf
4. Verify
Verifier checks safety; reject if unsure
5. JIT
Bytecode → native machine code
6. Attach
Hook: XDP, tc, kprobe, tracepoint, ...
7. Run
Event fires → program runs in kernel context
8. Share
Program writes maps; user space reads them
blue: user space pink: kernel green: shared
$ clang -O2 -g -target bpf -c prog.bpf.c -o prog.bpf.o
$ bpftool prog load prog.bpf.o /sys/fs/bpf/prog # load and pin
$ bpftool prog show # list loaded programs
The eBPF Virtual Machine
Register | Role |
R0 | Return value of the program and of helper calls |
R1 to R5 | Arguments to helpers; R1 holds the context pointer at entry |
R6 to R9 | Callee-saved, keep values across helper calls |
R10 | Read-only frame pointer to the 512-byte stack |
Instruction encoding (64 bits)
opcode�8 bits
dst�4 bits
src�4 bits
offset�16 bits
immediate�32 bits
A 64-bit constant load uses two slots (16 bytes).
The Verifier: Safety Before Speed
Rejected: no bounds check
struct ethhdr *eth = data;
if (eth->h_proto == ...) /* may read
past data_end */
Accepted: bounds proved first
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto == ...) /* safe */
When the verifier says no, the error log shows the exact instruction and register state. Reading these logs is a key eBPF skill.
Verifier Limits and How to Work with Them
Limit | Value |
Instructions explored | 1 million per program (since 5.2); 4,096 for unprivileged users |
Stack size | 512 bytes per program |
Loops | Must be provably bounded (bounded loops since 5.3; bpf_loop() helper since 5.17) |
Tail calls | Chain of at most 33 programs |
Function arguments | At most 5, passed in R1 to R5 |
Memory access | Only inside checked packet, map value, stack or context bounds |
Tips to pass the verifier
Common error messages to recognise
"invalid access to packet" "R1 invalid mem access" "unbounded memory access" "back-edge from insn"
Read the log
The verifier log names the failing instruction and the register state
Where Programs Attach: Hooks and Program Types
Program type | Attach point | Typical use |
XDP | NIC driver RX, before sk_buff | DDoS drop, load balancing, fast forwarding |
tc (cls_bpf) | Traffic control ingress / egress, has sk_buff | Policy, NAT, container networking |
Socket filter / sockops | Sockets and TCP events | Filtering, tuning, redirect between sockets |
cgroup hooks | connect, bind, send per cgroup | Per-container network policy |
kprobe / kretprobe | Entry or return of almost any kernel function | Dynamic tracing (not stable across versions) |
Tracepoints | Static points placed by kernel developers | Stable tracing: syscalls, scheduler, block I/O |
fentry / fexit | Function entry / exit using BTF | Faster, typed tracing |
uprobes, USDT | Functions and probes in user programs | Tracing apps, libraries, runtimes |
perf_event | Timers and hardware counters | CPU profiling, flame graphs |
LSM, struct_ops | Security hooks; kernel ops tables | Security policy, TCP CC, sched_ext |
Context In, Verdict Out
Program type | Input (context in R1) | Return value (R0) means |
XDP | struct xdp_md: raw packet bytes | XDP_PASS, XDP_DROP, XDP_TX (send back out), XDP_REDIRECT, XDP_ABORTED |
tc | struct __sk_buff | TC_ACT_OK (continue), TC_ACT_SHOT (drop), TC_ACT_REDIRECT |
Socket filter | struct __sk_buff | Number of bytes to keep; 0 drops the packet for this socket |
cgroup (connect, bind) | struct bpf_sock_addr | 1 allows the call, 0 rejects it |
kprobe, tracepoint | Registers or tracepoint fields | Mostly ignored; results go to maps |
LSM | Arguments of the security hook | 0 allows; a negative error such as -EPERM denies |
Same pattern everywhere: the kernel passes a context pointer in R1, runs the program, and reads the verdict from R0. For tracing, the useful output is what the program writes into maps.
Where the Networking Hooks Sit
NIC
receives frame
XDP
in driver, before sk_buff
sk_buff
kernel allocates it
tc ingress
sees full sk_buff
IP stack
netfilter, routing
Socket
socket filter, cgroup
Application
reads data
earlier: cheaper to drop, less context
later: more context, more cost
green: eBPF hook
XDP native mode
XDP generic mode
XDP offload mode
Egress path: application → socket → IP stack → tc egress (eBPF) → driver → NIC. XDP is on the receive path only, but XDP_REDIRECT can send a frame out of another interface.
Maps: State Shared Between Kernel and User Space
eBPF program
(kernel)
Map
key → value
User-space app
(bpf() syscall)
Map type | Use |
HASH, ARRAY | General counters and tables |
PERCPU_HASH / ARRAY | One copy per CPU: no atomics |
LRU_HASH | Flow tables that evict old entries |
LPM_TRIE | Longest-prefix match (routing, ACLs) |
RINGBUF | Stream events to user space (5.8+) |
PROG_ARRAY | Table of programs for tail calls |
DEVMAP, CPUMAP, XSKMAP | XDP redirect targets (NIC, CPU, AF_XDP) |
struct {
__uint(type,
BPF_MAP_TYPE_HASH);
__uint(max_entries,
1024);
__type(key, __u32);
__type(value, __u64);
} cnt SEC(".maps");
Getting Data Out to User Space
1. Poll a map
2. Perf buffer
3. Ring buffer (5.8+)
Rule of thumb
Helpers, kfuncs and Tail Calls
Helper functions
kfuncs
Tail calls
BPF-to-BPF calls
Example: Drop UDP Packets to Port 9999 with XDP
SEC("xdp")
int xdp_drop_udp(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end) return XDP_PASS;
if (ip->protocol != IPPROTO_UDP) return XDP_PASS;
struct udphdr *udp = (void *)ip + ip->ihl * 4;
if ((void *)(udp + 1) > data_end) return XDP_PASS;
if (udp->dest != bpf_htons(9999)) return XDP_PASS;
__u32 k = 0;
__u64 *n = bpf_map_lookup_elem(&drops, &k);
if (n) (*n)++; /* per-CPU counter */
return XDP_DROP;
}
char LICENSE[] SEC("license") = "GPL";
$ clang -O2 -g -target bpf \
-c xdp_drop.c -o xdp_drop.o
$ ip link set dev eth0 \
xdp obj xdp_drop.o sec xdp
$ bpftool map dump name drops
$ ip link set dev eth0 xdp off
Tracing with bpftrace
probe /filter/ { action }
Which files are opened, and by whom?
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
How many system calls does each process make?
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
What sizes do read() calls return?
bpftrace -e 'kretprobe:vfs_read { @bytes = hist(retval); }'
bpftrace turns the script into eBPF, loads it, and prints all maps when you press Ctrl-C. Use it for quick questions; use libbpf for long-running tools.
Toolchain and CO-RE (Compile Once, Run Everywhere)
bpftrace: High-level tracing language; one-liners and short scripts
BCC: Python/Lua front end, compiles at run time; large tool collection
libbpf (C): Standard loader library; used with clang and bpftool
cilium/ebpf, Aya: Go and Rust libraries for production systems
bpftool: Inspect and manage programs, maps, BTF; generate skeletons
The problem CO-RE solves
# all kernel types in one header
$ bpftool btf dump file /sys/kernel/btf/vmlinux \
format c > vmlinux.h
/* in BPF code: portable field read */
pid = BPF_CORE_READ(task, real_parent, tgid);
Lifetime and Permissions
How long does a program live?
Who can load eBPF?
See what is loaded on a machine
$ bpftool prog list $ bpftool map list $ bpftool link list
eBPF in the Real World
Cilium
Katran (Meta)
DDoS mitigation
Observability
Runtime security
Beyond servers
blue: networking green: observability pink: security yellow: other
Limitations and Pitfalls
Programming limits
Operational concerns
Good practice
Prefer stable hooks (tracepoints, fentry with BTF), aggregate inside the kernel, and test on the kernels you will deploy to.
eBPF vs Kernel Modules vs Kernel Bypass
Aspect | Kernel module | eBPF | Kernel bypass (DPDK) |
Where code runs | Kernel | Kernel, sandboxed | User space |
Safety | None: a bug can crash the kernel | Verifier-checked before running | Process isolation + IOMMU |
Deployment | Build per kernel version | Load at run time, no reboot | NIC unbound from kernel |
Kernel features | Full access | Keeps stack, tools, policies | Lost on that NIC |
Flexibility | Anything | Restricted by verifier and helpers | Anything in user space |
Packet speed | Kernel stack speed | High with XDP | Highest (line rate) |
Typical use | Drivers, file systems | Tracing, XDP, security, policy | NFV, telco, appliances |
eBPF is the middle path: much of the speed of bypass, with the safety and tooling of the kernel.
Summary
References