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.
Objective
Section
Loading, attachment, and verifier
Sections 1–2
Runtime costs, maps, and event buffers
Sections 3–5
Compatibility and permissions
Sections 6–7
1. Multiple Independent Objects in the Lifecycle
Object or Action
Role in Lifecycle
Program
Parsed, relocated, and loaded by the kernel; holds references to maps, etc.
Map
Manages state independently; lifetime does not simply equal the loader process lifetime
Attach / BPF link
Connects the program to an event source; manages associations via the interface
Pin
Persists 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.
Check
Common Rejection Reasons
Types and Origins
Treating a plain integer as a kernel pointer; reading uninitialized registers
Memory Bounds
Failing to prove packet bounds; exceeding map value or stack boundaries
References and Resources
Failing to release tracked objects on all paths after acquisition
Helper/kfunc Contracts
Program type does not permit the call; parameter types or context do not satisfy requirements
Control Flow and Complexity
Failure 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:
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.
{"id":"verifier-path-proof","title":"A successful lookup example does not prove every path safe","summary":"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.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"guard\" checked><span data-i18n=\"guard\"></span></label><div class=\"control\"><label for=\"offset\" data-i18n=\"offset\"></label><output for=\"offset\" id=\"offset-value\"></output><input id=\"offset\" type=\"range\" min=\"-4\" max=\"20\" value=\"0\" step=\"4\"></div><label class=\"check\"><input type=\"checkbox\" id=\"found\" checked><span data-i18n=\"found\"></span></label><div class=\"two\"><div class=\"card\"><header data-i18n=\"nullPath\"></header><div id=\"null-path\" class=\"readout\"></div></div><div class=\"card\"><header data-i18n=\"valuePath\"></header><div id=\"value-path\" class=\"readout\"></div></div></div><div class=\"track-row\"><span data-i18n=\"map\"></span><div id=\"map\" class=\"track\"></div></div><div class=\"formula\" id=\"bounds\"></div><div class=\"card\"><header data-i18n=\"verdict\"></header><div id=\"verdict\" class=\"readout\"></div></div><div class=\"card sample\"><header data-i18n=\"sample\"></header><div id=\"sample\" class=\"readout\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.readout{min-height:6.4em}.sample{margin-top:12px}.formula{min-height:4.8em}#map{display:grid;grid-template-columns:repeat(7,minmax(0,1fr))}#map .cell{min-height:40px;font-size:10px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}function draw(){const guard=$('#guard').checked,o=+$('#offset').value,found=$('#found').checked,safe=o>=0&&o+4<=16;set('offset-value',o+' B');set('null-path',t(guard?'returned':'unsafe'));set('value-path',t(safe?'safe':'unsafe'));cells('map',[-4,0,4,8,12,16,20].map(n=>({text:String(n),cls:o===n?'gold':n>=0&&n<16?'on':'empty'})));set('bounds','0 ≤ '+o+' ∧ '+(o+4)+' ≤ 16\\naccess ['+o+','+(o+4)+')');set('verdict',t(guard&&safe?'accepted':'rejected'));set('sample',t(found?safe?'safe':'unsafe':guard?'returned':'unsafe'));set('explanation',t('note'));}$('#guard').onchange=$('#found').onchange=$('#offset').oninput=draw;draw();","audio":false,"strings":{"accepted":"These two checks pass","found":"This run: key exists","guard":"Guard with if (!p) return","heading":"A successful lookup example does not prove every path safe","intro":"Toggle the NULL guard and move a four-byte access, then choose whether one sample lookup finds its key. Compare the sample with the load-time verdict.","map":"Four 4 B value cells; outer cells are out of bounds","note":"Keep offset 0, remove the NULL guard, and let this key exist. The sample is safe, yet loading is rejected: verification covers possible paths, not just this input.","nullPath":"Path one: p = NULL","offset":"Start offset of four-byte access","rejected":"Reject: a path remains unproved","reset":"Reset","returned":"Safe return, no dereference","safe":"Access bounds hold on this path","sample":"This sample run","scope":"A nullable map lookup returns a 16 B value; accesses are aligned 4 B, the key is initialized, and other verifier conditions hold. Only NULL and lower/upper access bounds are modeled. This is not a real verifier run or proof of business/concurrent-counter correctness.","scopeLabel":"Model scope and assumptions","unsafe":"Potential invalid access","valuePath":"Path two: p points to value","verdict":"Two load-time proof obligations"}}
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
Map
Suitable For
Usage Boundaries
ARRAY
Fixed-index configuration or counters
Fixed index range; elements cannot be deleted like in a hash
HASH
Sparse key-value state
Capacity and update failures must be handled
PERCPU_ARRAY / HASH
Per-CPU counters
User-space must sum per-CPU slots; summation is not an atomic snapshot
LRU_HASH
Capacity-limited approximate cache
Eviction loses state; unsuitable for lossless storage of all in-flight requests
RINGBUF
Emitting events to consumers
Reservation/output may fail when space is insufficient
PROG_ARRAY
Tail call targets
Not 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 Condition
Can CO-RE Solve It Alone?
Field offset changes
Can be handled via target BTF relocations
Field absence
Requires appropriate presence branches or compatibility paths
Unsupported helpers, kfuncs, maps, or attachments
Cannot be fixed by field relocations alone
Changed field semantics, missing functions, insufficient permissions
Still 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 showbpftool map showbpftool 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.