Program Types and Hooks: Where You Attach, What You See
The previous posts ran two very different kinds of program: an XDP program that receives a packet (Article 2), and a tracepoint that receives an exec event (Article 3). Both are eBPF, yet they see completely different things. The reason is the program type — and this is the concept that decides what an eBPF program can do.
A program type decides three things
When you load a program, you declare which program type it is. That type decides:
- Hook — where the program attaches, i.e. when it runs: a packet arriving at the NIC, a syscall being called, a kernel function running, a security operation happening, and so on.
- Context — what structure the
r1argument points to at entry (Article 1): a packet? syscall parameters? CPU registers? - Allowed helpers — each type can only call a set of helpers that make sense for it, and the verifier (Article 2) applies type-specific constraints.
The kernel supports dozens of types. Inspect the list on the node:
sudo bpftool feature probe | grep 'program_type .* is available'
program_type socket_filter is available
program_type kprobe is available
program_type sched_cls is available # tc — the datapath Cilium uses (Article 0)
program_type tracepoint is available
program_type xdp is available
program_type perf_event is available
program_type cgroup_skb is available
program_type sock_ops is available
program_type sk_skb is available
... (and many more)
Grouped by category: networking (xdp, sched_cls/tc, socket_filter, sk_skb, sock_ops), tracing (kprobe, tracepoint, perf_event, uprobe), cgroup (cgroup_skb, cgroup_sock...), and security (lsm). On this cluster, Cilium mainly uses sched_cls (tc) for its datapath and cgroup_* (Article 0).
A different hook: a tracepoint on a syscall
Article 2 used XDP — the context was a packet. Now try a type from the other end: tracepoint, attached to the openat syscall (opening a file). The context is entirely different, and we get the event information through a helper instead of reading a packet:
SEC("tracepoint/syscalls/sys_enter_openat")
int on_openat(void *ctx)
{
char comm[16];
bpf_get_current_comm(comm, sizeof(comm)); // helper: process name
__u32 pid = bpf_get_current_pid_tgid() >> 32; // helper: pid
bpf_printk("openat by %s (pid %d)", comm, pid); // helper: print to trace_pipe
return 0;
}
SEC("tracepoint/syscalls/sys_enter_openat") declares both the type (tracepoint) and the specific hook. Load and auto-attach:
clang -O2 -g -target bpf -I/usr/include/x86_64-linux-gnu -c openat_trace.bpf.c -o openat_trace.bpf.o
sudo bpftool prog loadall openat_trace.bpf.o /sys/fs/bpf/openat autoattach
bpf_printk writes to the kernel's trace_pipe. Read it for a few seconds while the system opens files:
sudo timeout 2 cat /sys/kernel/debug/tracing/trace_pipe | grep 'openat by'
basename-355997 [000] ....1 47718.657120: bpf_trace_printk: openat by basename (pid 355997)
basename-355997 [000] ....1 47718.657129: bpf_trace_printk: openat by basename (pid 355997)
basename-355997 [000] ....1 47718.657133: bpf_trace_printk: openat by basename (pid 355997)
The program runs every time any process on the machine calls openat — this is the observability power of a tracepoint: attach in one place, see the whole system. Its context isn't a packet but a syscall event; we get the process name and pid through helpers.
Both are eBPF, but each type sees a different world
Put side by side the three types we've met (or will meet):
type hook (runs when) context (r1 points to)
───────── ────────────────────── ─────────────────────────
xdp a packet arrives at the NIC struct xdp_md (data, data_end)
tracepoint a kernel tracepoint fires event struct (e.g. syscall args)
kprobe a kernel function is called struct pt_regs (CPU registers then)
The same virtual machine (Article 1), the same verifier (Article 2), the same maps mechanism (Article 3) — but XDP "sees" a packet and can change where it goes, while a tracepoint "sees" an event and only observes. The verifier tightens differently too: an XDP program that reads a packet must check data_end (Article 2), whereas a tracepoint has no such notion. Choosing a program type is choosing where to stand inside the kernel to look and to act. The rest of the series exploits exactly these types: Part IV networking (xdp, tc), Part V security (lsm), Part VI observability (kprobe, tracepoint, perf_event).
🧹 Cleanup
sudo rm -rf /sys/fs/bpf/openat
rm -f /tmp/openat_trace.bpf.*
Removing the pin detaches the program from the tracepoint; the node goes back to 140 programs. Source is at github.com/nghiadaulau/ebpf-from-scratch, directory 04-program-types.
Wrap-up
The program type is what decides which hook an eBPF program attaches to (when it runs), what context it receives (which structure r1 points to), and which helpers it may call — with verifier constraints specific to the type. The kernel supports dozens of types grouped into networking (xdp, tc/sched_cls, socket), tracing (kprobe, tracepoint, perf_event), cgroup, lsm. We attached a tracepoint to sys_enter_openat and saw it run for real on every file open (trace_pipe prints openat by basename), getting comm/pid through helpers — a context entirely different from the XDP packet receiver in Article 2. Same virtual machine, same verifier, same maps, but each type stands at a different point in the kernel and sees a different world.
One foundational piece remains before we write real tools ourselves: how does a program read data structures inside the kernel (like task_struct) when their layout changes from one kernel version to the next? Article 5 — BTF and CO-RE — answers that, and opens the way to Part III.