BPF syscalls, verifier, maps, JIT, and lifetime

Trace BPF loading, verification, maps and program lifetime.

13 min readChapter 82 of 168Advanced
In this chapter

Before you start#

Review system calls and user copies, RCU and references, and capabilities. This lesson describes upstream Linux v6.12. Program types, privilege checks, JIT availability, kernel configuration and distribution policy differ on running systems. The examples describe verifier reasoning, not a captured verifier log or a program loaded on your host.

Learning goals#

Trace program and map creation through validation and cleanup, explain the verifier's register and memory model, distinguish verifier acceptance from functional correctness, and account for BPF object lifetime beyond one fd.

Source walkthrough#

BPF syscalls, verifier, maps, JIT, and lifetime
  1. BPF syscall

    Validate attributes, authorization and object allocation

  2. Verifier

    Track pointer types, bounds, stack and references

  3. Runtime

    Use interpreter or architecture-specific JIT

  4. Lifetime

    FDs, links, maps and pins hold distinct references

Conceptual model. Source-based conceptual overview, not a captured execution; behavior depends on configuration, permissions and runtime state.

The bpf system call multiplexes commands through a command-specific union bpf_attr. Commands can create maps, load programs, obtain object information, create links, or work with BPF Type Format (BTF), among other operations. A program is verified instructions plus metadata and runtime state. A map is a kernel object with type-specific key, value, capacity and operations. A link can represent an attachment with its own lifetime; a BTF object describes types and metadata. These objects are not all interchangeable file descriptors, even though each may be referenced through an fd. The v6.12 UAPI defines the command and attribute interface.

A successful fd is one reference to an object. Programs can hold references to maps; links can hold program and attachment references; pins in a configured BPF filesystem can retain objects after the creating process closes its fd. Reference counting, RCU and deferred cleanup protect different paths. Closing one fd does not prove immediate destruction if a pin, link or other reference remains. Conversely, an integer fd reused after close can name a different object. Safe administration requires understanding who owns each reference and which unpin, detach or close action releases it. See the v6.12 syscall implementation’s object and fd paths.

Map allocation and unwind

In kernel/bpf/syscall.c, map_create validates attributes and map-type requirements, allocates the map, sets up metadata, invokes security checks and allocates an ID before creating an fd. This exact v6.12 GPL-2.0-only excerpt, copyright PLUMgrid and contributors, shows the fd failure path; surrounding lines are omitted:

c
	err = bpf_map_new_fd(map, f_flags);
	if (err < 0) {

The call tries to create the userspace fd, and the condition detects failure. The following source comment explains that the map ID has already been published and another reference might have been acquired through an ID lookup; it calls bpf_map_put_with_uref rather than blindly freeing the object. Earlier failures use labels that release security state, the map and any token as appropriate. The returned error does not provide a usable fd. A successful map allocation still does not imply a program was loaded or attached.

The syscall checks attributes, credentials, capabilities, tokens where supported, LSM policy and configuration. CAP_BPF, CAP_PERFMON, CAP_NET_ADMIN, CAP_SYS_ADMIN and other requirements depend on the operation, program type and kernel version. An unprivileged BPF policy can disable operations even for a process that can call the syscall. Do not infer that a map creation failure is a verifier rejection; the verifier checks programs, while map creation has its own validation and authorization paths.

What the verifier models#

A BPF program type determines its context layout, allowed attach points, return conventions and permitted helpers or kernel functions. A verifier tracks abstract state for registers, stack slots and references while exploring possible instruction paths. A register can be a scalar, a context pointer, a map value pointer, a packet pointer or another typed value. Pointer type, fixed and variable offsets, provenance and bounds determine whether a memory access can be proved safe. A number that happens to equal a valid address at runtime is not automatically a verifier-approved pointer. The kernel verifier documentation describes register types, stack initialization and program-specific helper prototypes.

For scalars, the verifier tracks ranges and a tristate number or tnum representation of known and unknown bits. A conditional comparison can refine the range on each branch. Stack bytes must be initialized before reads under the applicable rules; storing only four bytes does not initialize an eight-byte read. State equivalence and pruning reduce repeated exploration, but precision, complexity limits and privilege/configuration can affect whether a program is accepted. Bounded loops and calls are supported under rules that let the verifier establish safe termination and state bounds; “loops are allowed” is not a guarantee that an arbitrary loop verifies.

Reference-tracked objects returned by helpers must be released or transferred on all paths according to their contract. A BPF spin lock has restrictions on nesting, allowed operations and control flow; sleepable programs have different context constraints from atomic or interrupt paths. Tail calls transfer control to another program through a program array and are bounded; a program must respect type and attachment compatibility and cannot assume an unbounded chain. Helpers and kfuncs have program-type, version, privilege and context constraints; their names in a newer tutorial do not guarantee availability on v6.12.

In kernel/bpf/verifier.c, bpf_check allocates a verifier environment and per-instruction auxiliary data, selects operations for the program type and later invokes do_check_main. The instruction walk in do_check routes memory instructions through check_mem_access; packet pointers can reach check_packet_access. This exact v6.12 GPL-2.0-only excerpt, copyright PLUMgrid, Facebook and contributors, is from the latter function; surrounding bounds checks and diagnostic text are omitted:

c
	if (reg->smin_value < 0) {
		verbose(env, "R%d min value is negative, either use unsigned index or do a if (index >=0) check.\n",
			regno);
		return -EACCES;
	}

The condition tests the tracked signed minimum of a register. If a possible negative offset remains, this branch reports the reason and rejects it with -EACCES; it does not assume that typical runtime values are nonnegative. Subsequent checks compare the access's fixed offset and size with the packet range the verifier has established. A different pointer type or failed earlier check can produce a different error; this excerpt is a specific packet-access branch, not a complete verifier specification.

Worked packet-state comparison#

Consider a conceptual packet program that receives a packet data pointer and an end pointer in a context permitted for that program type. It wants to read one byte at offset 13. A safe control-flow shape first computes a pointer to the end of the intended access, tests that it does not exceed data_end, and only reads on the branch where that relation is established. The verifier can associate range information with the packet pointer on the safe branch, provided the exact instructions, pointer types and program context meet its rules. A one-byte read at offset 13 requires at least 14 accessible bytes; a packet with fewer bytes follows the failure branch and must not be read there.

Contrast a program that derives an index with a tracked signed range of -1 through 13, adds it to a packet pointer and reads without proving nonnegativity. The quoted smin_value check can reject the access because -1 remains possible. Another program checks only that an address is below data_end but fails to establish the complete access size or pointer provenance; it is not automatically safe. These are source-based abstract-state scenarios, not literal verifier output or a promise that arbitrary pseudocode will load. Exact bytecode, compiler output, privilege, state precision and kernel version affect the result.

What the verifier can prove
Safe branch: 14 bytes proven
The packet pointer range covers a one-byte read at offset 13.
Two-byte read at offset 13
Needs 15 bytes; the 14-byte proof does not cover it.
Index range -1 to 13
A possible negative offset; the quoted smin_value check rejects it.
Pointer lost to scalar math
A numeric comparison alone may not establish provenance.
Conceptual model. Abstract-state scenarios from the chapter, not verifier logs; the target kernel decides acceptance.

For a stack example, assume a four-byte key is required by a map lookup. If the program writes all four bytes to an aligned permitted stack region before the helper call, the verifier can inspect that initialization under the helper's argument rules. If one byte remains uninitialized, rejection is possible even if the missing byte usually happened to be zero in an experiment. After a helper returns a nullable map value pointer, a null check is needed before dereferencing it on the non-null branch. The map value's lifetime and mutation rules are distinct from the lifetime of the program's stack.

Verification is a boundary, not correctness#

The verifier aims to establish properties such as permitted memory access and control-flow constraints under its model. Acceptance is not a proof that a program computes the right answer, counts every event, has no information leak through its intended outputs, or is correct for a changed kernel semantic. A program can safely return the wrong policy decision or use a valid but semantically inappropriate field. The verifier itself is complex kernel code; its assumptions and bugs matter to the security boundary.

After verification, the kernel can use an interpreter or a just-in-time compiler depending on architecture, configuration and policy. JIT code generation, hardening and availability are architecture- and version-dependent. A program that verifies on one kernel may fail on another due to different verifier behavior, helpers, types or attach points. JIT compilation does not make an unsafe program acceptable; it is a later runtime implementation of an accepted program. Conversely, successful JIT selection does not establish that the attached event occurred.

A safe debugging workflow separates three failures: syscall or permission rejection before verification, verifier rejection of a particular instruction path, and successful loading with incorrect runtime behavior. Capture exact errno, program type, kernel build and verifier output only where authorized; logs can contain sensitive program details. Do not enable unprivileged BPF or weaken host security just to reproduce a tutorial. A disposable guest with a pinned kernel and tools is appropriate for program loading, but no such guest test is asserted here.

Following verifier states through a branch#

The verifier starts from an abstract initial state for the selected program type, including a context pointer whose allowed accesses are defined by that type. The instruction walk applies transfer rules to registers and stack slots. If an instruction branches, the verifier explores the reachable successor states with constraints refined for each path. A safe load on one branch does not make the same load safe on another branch whose bounds are unknown. If the verifier can establish that a previous state subsumes a later state, pruning avoids re-exploring equivalent work; an overly broad state or exhausted complexity budget can still make verification fail. These are implementation strategies, not a guarantee of identical acceptance across releases.

Consider a packet pointer whose proven accessible range is 14 bytes. A one-byte read at fixed offset 13 is within that range, assuming the tracked pointer and other access constraints are valid. A two-byte read at the same offset requires 15 bytes and cannot rely on the 14-byte proof. Moving a pointer through a scalar operation can lose the pointer type or precision, so a numeric comparison alone may not establish provenance. A variable offset with a negative signed minimum is rejected by the quoted packet check before a successful bounds result. This is an abstract-state example: the exact generated instructions must still be verified by the target kernel.

On loading, the kernel allocates a program object and verifier resources, copies or validates user-provided attributes and instructions, checks access and invokes bpf_check. Rejection must release temporary verifier state and program resources through the corresponding labels; the caller does not receive a usable verified program fd merely because allocation succeeded. If verification succeeds, later runtime selection, security checks or attachment can still fail. A link and a pin are separately referenced objects, so cleanup after a partially successful loader must close or release each successful stage. The map failure excerpt illustrates why publication of an ID changes the appropriate cleanup operation.

The verifier's per-load environment is set up in bpf_check, v6.12, SPDX GPL-2.0-only (surrounding code omitted):

c
	env = kvzalloc(sizeof(struct bpf_verifier_env), GFP_KERNEL);
	if (!env)
		return -ENOMEM;

	env->bt.env = env;

	len = (*prog)->len;
	env->insn_aux_data =
		vzalloc(array_size(sizeof(struct bpf_insn_aux_data), len));
	ret = -ENOMEM;
	if (!env->insn_aux_data)
		goto err_free_env;
	for (i = 0; i < len; i++)
		env->insn_aux_data[i].orig_idx = i;
	env->prog = *prog;
	env->ops = bpf_verifier_ops[env->prog->type];

The environment is allocated with kvzalloc, and per-instruction auxiliary data with vzalloc, sized by the program length. If the second allocation fails, the code jumps to err_free_env, releasing the first. The excerpt does not show any abstract state, so it says nothing about whether a particular program will be accepted.

From load request to retained object
  1. bpf(BPF_PROG_LOAD)

    Copy the attributes and check the caller for the program type.

  2. bpf_check

    Allocate the verifier environment and per-instruction data.

  3. Verifier walk

    Track registers, stack slots and references on each path.

  4. Rejected

    Release verifier state; no usable program fd is returned.

  5. Accepted

    The verified program is returned as an fd.

  6. Runtime selection

    Interpreter or JIT, subject to architecture and policy.

  7. Attach and retain

    A link or pin holds a reference after the fd closes.

Conceptual model. Simplified teaching model of the BPF program load path in Linux v6.12; no program was loaded or verified for this chapter.

Verifier privilege choices can alter which operations or pointer disclosures are permitted, but a capability check is not a general promise that all program types and helpers are supported. A sleepable attachment cannot be substituted for an atomic-context attachment without checking its program and helper constraints. Similarly, a valid tail-call target must meet the kernel's compatibility checks; the tail-call limit means a chain is not ordinary recursion. These details distinguish an authorization failure, a verification failure and an actual runtime bug in the program's logic.

Practice#

Read-only configuration inventory

On an authorized host or disposable guest, inspect the running kernel and available policy indicators without loading a program or attaching anything:

sh
uname -r
if [ -r /proc/sys/kernel/unprivileged_bpf_disabled ]; then cat /proc/sys/kernel/unprivileged_bpf_disabled; else echo 'unprivileged BPF policy unavailable'; fi
if [ -r /sys/kernel/btf/vmlinux ]; then ls -l /sys/kernel/btf/vmlinux; else echo 'kernel BTF unavailable or restricted'; fi
if command -v bpftool >/dev/null 2>&1; then bpftool version; else echo 'bpftool unavailable'; fi

The sysctl is a policy indicator, not a complete authorization decision. A readable BTF file does not prove a particular program type, helper or attachment is available. bpftool is a separate userspace project; record its actual version and pin a specific release or commit when reproducing an experiment. Missing files, EPERM, EACCES, ENOSYS, unavailable tools or hidden procfs are legitimate observations. Do not write the sysctl or use sudo to enable a test.

Source-only alternative: draw two verifier states for the packet example: a safe branch where 14 bytes have been established and a branch with possible index -1. Record pointer type, minimum offset, required size and the reason the second case cannot pass the quoted check. Add a map-create timeline showing the published ID, failed fd creation and bpf_map_put_with_uref cleanup. Deliver those states or your actual inventory, marking that no program was loaded or verified at runtime.

Knowledge check#

  1. Why does a possible negative packet offset matter even when observed packets use nonnegative indices?
  2. Why does fd creation failure after map ID publication require reference-aware cleanup?
  3. Does a successful verifier result prove that a tracing program counts every event?
  4. Why must every path release a reference-tracked helper object?
  5. What does closing a program's original fd fail to prove about attached links or pins?

Answers#

  1. The verifier reasons over all values represented by the abstract state; the quoted packet-access branch rejects a negative signed minimum. Typical runtime input does not eliminate the unproved path.
  2. Another reference may already exist through the published ID. The source uses the map put operation so final destruction follows reference lifetime instead of immediately freeing a potentially referenced object.
  3. No. Verification addresses safety properties under its model; event selection, drops, program logic and runtime behavior remain separate questions.
  4. Otherwise an object reference could leak or violate the helper's lifetime contract. The verifier tracks such resources on relevant control-flow paths.
  5. A link, pin or other reference may keep the program alive and possibly attached. Object lifetime is not identical to one fd lifetime.

Checkpoint#

Submit the two packet-state cases and map-create cleanup timeline. Ready means pointer provenance, bounds, map references and verifier limits are explicit, and no invented runtime log is treated as evidence. Retry the pinned verifier and syscall branches if the negative path or reference owner is missing. This is not certification.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.