eBPF Fundamentals

On this page

The loader exits, yet the program remains attached; the verifier passes, but counters may still be lost. Object lifetime, memory safety proofs, and business correctness are three distinct concerns in eBPF.

This article discusses extended BPF. seccomp uses the classic BPF filter format and specialized system call semantics, and should not be confused with standard eBPF program types.

ObjectiveSection
Loading, attachment, and verifierSections 1–2
Runtime costs, maps, and event buffersSections 3–5
Compatibility and permissionsSections 6–7

1. Multiple Independent Objects in the Lifecycle

BPF Objects: Compilation, Loading, Attachment, and Persistent References Compile to BPF ELF Code, map descriptions, BTF, and relocation info 1 User-space loader prepares the object Perform CO-RE relocations based on target BTF, create maps 2 Kernel loads and verifies the program Check types, bounds, resources, and target program type constraints 3 Attach to supported execution points Use maps or emit events upon trigger 4 Detach and release references FDs, links, and pins collectively determine object lifetime 5 A successful load does not mean the program is attached; closing an FD does not necessarily destroy the object immediately.
Object or ActionRole in Lifecycle
ProgramParsed, relocated, and loaded by the kernel; holds references to maps, etc.
MapManages state independently; lifetime does not simply equal the loader process lifetime
Attach / BPF linkConnects the program to an event source; manages associations via the interface
PinPersists object references in bpffs for other processes to access

libbpf coordinates ELF parsing, relocations, and kernel interface calls; do not treat successful loading as successful attachment.

Closing the loader process does not necessarily revoke all programs: resources pinned or referenced by other objects may still exist; lifetime rules differ across attach APIs. When designing tools, clearly define who holds the link, whether pinning is used, and how to revoke upon exit. Do not rely solely on the assumption that "process termination will automatically clean up." See libbpf Overview.

2. The Verifier Checks Provable Safety Conditions

The verifier propagates abstract state along the control flow, tracking scalar ranges, pointer types, initialized regions, and resources requiring release in registers and stack slots. It handles paths through mechanisms like state merging and pruning, rather than executing every specific input individually.

CheckCommon Rejection Reasons
Types and OriginsTreating a plain integer as a kernel pointer; reading uninitialized registers
Memory BoundsFailing to prove packet bounds; exceeding map value or stack boundaries
References and ResourcesFailing to release tracked objects on all paths after acquisition
Helper/kfunc ContractsProgram type does not permit the call; parameter types or context do not satisfy requirements
Control Flow and ComplexityFailure to prove termination; state exploration or call/stack limits exceed budget

Bounded loops, bpf_loop, or iterators expand expressiveness, but not all loops are accepted. Program instruction count and verifier exploration complexity are distinct limits; do not treat "one million" as a unified upper bound for source code length, execution count, and all programs simultaneously.

BPF stacks have strict limits; the common maximum stack frame per subprogram is 512 bytes; call chains and tail call combinations add further constraints. Tail call chain limits differ from standard BPF-to-BPF call depth. Rely on the target kernel's verification logs rather than memorizing a universal constant table across versions.

Non-null Proof and Key Range Are Not the Same Thing

Below is a local pattern after a map lookup, assuming counts value type is __u64 and the key is an initialized __u32:

__u32 key = 7;
__u64 *value = bpf_map_lookup_elem(&counts, &key);
if (!value)
    return 0;
__sync_fetch_and_add(value, 1);

The lookup helper determines the result based on map type and key content; the verifier must confirm the key memory is readable and that the value is not NULL before dereferencing. Do not treat if (x < 10) as a proof that any map lookup will succeed, especially if x may be signed and the lower bound has not been checked.

Standard (*value)++ may lose counts on concurrent updates to shared values; the example uses atomic addition; atomic operations still must satisfy target instruction and access constraints. The verifier accepts that it does not prove business statistical correctness, nor does it guarantee that the kernel or verifier implementation is bug-free. See Verifier Documentation.

Compare one successful sample with the proof required for every reachable path. Removing the NULL guard changes the proof even when this particular key exists.

Preparing the visual
A successful lookup example does not prove every path safe

The verifier must prove types and bounds on every reachable path. One non-null, in-bounds sample cannot rule out a different NULL-dereference path.

3. JIT Reduces Interpretation Overhead, But Does Not Eliminate All Costs

When JIT is supported and enabled, BPF instructions are compiled to target machine code; other available execution paths depend on architecture and build configuration. You cannot specify a fixed "corresponds to 1 to 3 x86 instructions" for all BPF instructions: calls, atomic operations, checks, and JIT hardening all affect the generated code.

Runtime costs also include hooks, map lookups, locks, event copying, and user-space consumption. Verification occurs at load time, but boundary checks from the source program used for proof may still remain in the runtime code, so you cannot claim "zero runtime overhead for all boundary checks."

4. Maps Are State Objects with Different Semantics

MapSuitable ForUsage Boundaries
ARRAYFixed-index configuration or countersFixed index range; elements cannot be deleted like in a hash
HASHSparse key-value stateCapacity and update failures must be handled
PERCPU_ARRAY / HASHPer-CPU countersUser-space must sum per-CPU slots; summation is not an atomic snapshot
LRU_HASHCapacity-limited approximate cacheEviction loses state; unsuitable for lossless storage of all in-flight requests
RINGBUFEmitting events to consumersReservation/output may fail when space is insufficient
PROG_ARRAYTail call targetsNot a standard function pointer array

Per-CPU reduces cross-CPU shared write contention, but does not imply the entire map is "absolutely lock-free," nor does it automatically resolve consistency during user-space reads. Distinction is also needed between hash structure updates and atomic consistency of multiple fields within values.

BPF global variables can also retain state; libbpf often organizes related data sections as maps, so "programs are completely stateless" is an oversimplification. See BPF Maps for more semantics.

5. Choosing Between Ring Buffer and Perf Buffer

Perf event arrays are often paired with per-CPU perf buffers to output BPF events; BPF ringbuf provides a multi-producer, single-consumer shared ring buffer, and multiple ringbufs can also be used for sharding. Both support variable-length records; do not claim that perf buffers only support fixed-length records.

Early Reservations Unfinished May Cause Later Events to Wait

The ringbuf reserve/submit path allows direct filling of reserved records; if earlier reserved records are not yet completed, it affects the consumer's ability to continue reading subsequent records. Ordering corresponds to the reservation sequence within the same ringbuf, not to the absolute chronological order of events across CPUs, nor to a global order across multiple ringbufs.

Both solutions may lose events. Ringbuf reservation failures must be counted; perf buffer lost information must be handled; timestamps, CPU IDs, event identifiers, and loss counts should become part of the data protocol. Whether shared memory is used and whether copying occurs depend on the specific output API and consumption path; do not simply claim that perf always involves two copies and ringbuf never involves copying. See BPF Ring Buffer.

6. Compatibility Boundaries of BTF and CO-RE

BTF describes type information; CO-RE uses target BTF at load time to handle relocations for field offsets, sizes, presence, etc. BPF_CORE_READ(task, pid) allows correct access even if fields move, but does not turn the entire kernel internal ABI into a stable interface.

Compatibility ConditionCan CO-RE Solve It Alone?
Field offset changesCan be handled via target BTF relocations
Field absenceRequires appropriate presence branches or compatibility paths
Unsupported helpers, kfuncs, maps, or attachmentsCannot be fixed by field relocations alone
Changed field semantics, missing functions, insufficient permissionsStill requires program design and deployment handling

The program must still pass the verifier for the target version.

/sys/kernel/btf/vmlinux often provides the running kernel's BTF; module BTFs have their own availability conditions. CO-RE does not require installing a full compiler on every target machine, but build and release planning still requires defining minimum capabilities, compatibility paths, and verification matrices.

7. Permissions and Observation Tools

Loading tracing-related programs often involves CAP_PERFMON; network management operations may involve CAP_NET_ADMIN; CAP_BPF is not a pass for all attachments. Unprivileged BPF is subject to sysctls, program types, and other restrictions. LSM, lockdown, etc., may further constrain operations.

After installing bpftool, the following commands are used for read-only inspection of objects; what you can see depends on permissions:

bpftool prog show
bpftool map show
bpftool link show

bpftool feature probe probes capabilities by attempting relevant operations; do not treat it as purely reading a static version table. JIT instruction display also depends on permissions and kernel configuration, and does not guarantee that every program has exportable machine code.

Continue reading: Program Types, Tracing and Debugging.