All skills
Skillintermediate
eBPF authoring guide (engine/ebpf)
This document explains how to add a new eBPF program to the Dagger engine and wire it so it can be loaded at engine start. It is written for future LLMs and humans to follow step-by-step.
Claude Code Knowledge Pack7/10/2026
Overview
eBPF authoring guide (engine/ebpf)
This document explains how to add a new eBPF program to the Dagger engine and wire it so it can be loaded at engine start. It is written for future LLMs and humans to follow step-by-step.
Scope and expectations
- Programs live under
engine/ebpf/<name>/with a BPF C file, Go tracer, and generatedbpf2gooutputs. - Each tracer implements the Go interface in
engine/ebpf/common.go:Run(ctx)blocks until context cancel; read events and log.Close()releases links, maps, ringbuf, etc.
- Programs are enabled via env vars
DAGGER_EBPF_PROG_=yand registered incmd/engine/main.go. - Defaults are only enabled when extra-debug/trace logging is on.
Directory layout (follow existing patterns)
Use either filetracer/ or ovltracer/ as a template.
engine/ebpf/bpf/
common.h # shared helpers + standard BPF includes
vmlinux.h # CO-RE BTF header (checked in once)
engine/ebpf/<prog>/
tracer.go # Go wrapper implementing ebpf.Tracer
bpf/<prog>.bpf.c # eBPF C source (//go:build ignore)
<prog>_x86_bpfel.go # generated by bpf2go
<prog>_arm64_bpfel.go # generated by bpf2go
<prog>_x86_bpfel.o # generated by bpf2go at build time (not committed)
<prog>_arm64_bpfel.o # generated by bpf2go at build time (not committed)
Step-by-step: adding a new tracer
- Create package skeleton under
engine/ebpf/<prog>/withtracer.goandbpf/<prog>.bpf.c. - Write the BPF C program (see “BPF C guidelines” below). Prefer
#include "common.h"for shared helpers. - Add
go:generatebpf2go lines intracer.go(see existing packages). - Generate bindings: run
go generatein the package (or at repo root). This creates*_bpfel.goand.ofiles. - Implement Go tracer to:
rlimit.RemoveMemlock()- check
/sys/kernel/btf/vmlinuxexists loadObjects()and handleebpf.VerifierError- attach links (tracepoints/kprobes) with clear required/optional logic
- open ringbuf reader
- return a
Tracerthat canRunandClose
- Wire into engine: add to
ebpfProgramsincmd/engine/main.goand optionally add todefaultEbpfPrograms.
Wiring into engine start
- Registration happens in
cmd/engine/main.go:- Add
{name: "<name>", new: <pkg>.New() }toebpfPrograms.
- Add
- Env var name is derived automatically:
- Name is uppercased and non‑alphanumeric chars become
_. - Example:
name: "ovl_inuse"→DAGGER_EBPF_PROG_OVL_INUSE.
- Name is uppercased and non‑alphanumeric chars become
BPF C guidelines (deep gotchas included)
- Stack limit is tiny (~512 bytes). Avoid large stack buffers:
- Use per‑CPU arrays for temporary scratch (see
ovltracer’stmp_mount_args). - Keep local structs small; move large data into maps.
- Use per‑CPU arrays for temporary scratch (see
- CO‑RE + BTF:
- Use
#include "vmlinux.h"andBPF_CORE_READfor kernel struct reads. - The runtime requires
/sys/kernel/btf/vmlinux(checked in Go before loading).
- Use
- Tracepoint vs kprobe:
- Tracepoints are more stable; kprobes may fail if symbols are missing or renamed.
- For kprobes, treat non‑essential probes as optional and log a debug message instead of failing (see
ovltracer). - Symbols may have suffixes like
.isra.0; resolve via/proc/kallsymsif needed.
- User vs kernel memory:
- Use
bpf_probe_read_user_strfor user pointers (syscall args). - Use
bpf_probe_read_kernel_str/BPF_CORE_READfor kernel memory.
- Use
- Event structs must match Go exactly:
- Use fixed-width integer types (
__u32,__u64, etc.). - Fixed-size arrays only; no pointers.
- Keep field order identical between C and Go.
- Use fixed-width integer types (
- Ring buffer limits:
bpf_ringbuf_reservecan return NULL; handle it gracefully.- Keep event sizes reasonable to reduce drops.
- Context maps and cleanup:
- If you store data at entry (e.g.,
sys_enter_*), delete it on exit to avoid leaks. - If a kretprobe is optional, also guard map cleanup for the missing side.
- If you store data at entry (e.g.,
- Loops must be bounded:
- Use
#pragma unrollfor short loops; avoid dynamic loops (verifier dislikes them).
- Use
- Strings:
- Always null‑terminate and bound copies; ringbuf events should not carry unterminated strings.
Go tracer guidelines
- Memlock: call
rlimit.RemoveMemlock()before loading. - BTF: check for
/sys/kernel/btf/vmlinuxand return a clear error if missing. - Verifier errors: wrap
*ebpf.VerifierErrorwith%+vfor verbose diagnostics. - Attachment patterns:
- Use
link.Tracepoint("syscalls", "sys_enter_*", ...)for tracepoints. - Use
link.Kprobe/link.Kretprobefor kernel functions. - Keep required vs optional probes explicit.
- Use
- Run loop:
- On
ctx.Done(), close the ringbuf reader to unblockRead(). - Decode event bytes with
binary.Read(..., binary.LittleEndian, &event).
- On
- Close():
- Close reader, then links, then objects.
- Aggregate errors (
errors.Join).
bpf2go generation
- Add
//go:generatelines intracer.go, e.g.:go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -Wall -Werror -D__TARGET_ARCH_x86 -I../bpf" -target amd64 <name> ./bpf/<name>.bpf.cgo run ... -D__TARGET_ARCH_arm64 -I../bpf -target arm64 ...
- The generated
.gofiles are checked in; the.ofiles are build artifacts and are not committed.
Quick checklist before shipping
- C structs and Go structs match exactly (sizes + field order).
- No large stack allocations in BPF program.
- All maps have bounded sizes; cleanup paths remove entries.
- BTF check is present and error messages are clear.
-
New()returns errors for hard failures, logs for optional probes. - Program registered in
cmd/engine/main.gowith correct name. - Env var name is documented (or easy to infer from name).
-
go generaterun and new.gofiles committed (no.ofiles in git).
Common failure modes and fixes
- Verifier error about stack size: move large buffers into a map or per‑CPU array.
- Missing BTF: kernel lacks
CONFIG_DEBUG_INFO_BTF; fail gracefully. - Kprobe attach failures: symbol not present; make probes optional or resolve symbol suffixes.
- Ringbuf read hangs on shutdown: make sure
Runcloses the reader onctx.Done(). - Garbage map entries: always delete per‑pid context maps on exit.