BPF Helper Functions

A BPF helper function is a kernel function that an eBPF program is allowed to call by a fixed integer identifier, with a frozen, never-broken signature that lives in the kernel’s user-facing API (the UAPI). A BPF program cannot call arbitrary kernel code — the verifier rejects any call it does not recognise. Helpers are the original, stable answer to “what may a BPF program call”: each one gets a number in enum bpf_func_id (e.g. BPF_FUNC_map_lookup_elem = 1), a struct bpf_func_proto that tells the verifier exactly what argument types and return type to enforce, and a place on the per-program-type allow-list that decides which program types may use it (uapi/linux/bpf.h, v6.12; kernel/bpf/helpers.c, v6.12). Because the identifiers and signatures are UAPI, they are append-only: a helper’s number is never recycled and its calling contract is never changed in a way that would break an old program. That permanence is the helper system’s defining virtue and its defining cost — and it is exactly why the kernel community now steers most new functionality to kfuncs instead.

This note is the stable, UAPI-frozen half of a deliberately contrasting pair. Read it alongside BPF Kernel Functions (kfuncs), which covers the modern, intentionally-unstable mechanism; the spine running through both notes is the stability trade-off — a frozen ABI you can rely on forever versus a flexible kernel↔kernel interface that can change between releases.

All version-specific claims below are pinned to Linux 6.12 LTS (released 2024-11-17), verified against the raw source tree at tag v6.12. Where 6.12 and 6.18 LTS differ on a load-bearing fact it is called out; otherwise treat statements as “as of 6.12 LTS.”


Mental Model

Think of helpers as the system call table of the BPF virtual machine. Just as a userspace process cannot jump into arbitrary kernel code and instead invokes a numbered syscall whose ABI is frozen, a BPF program cannot call arbitrary kernel functions and instead emits a BPF_CALL instruction whose immediate field carries a helper number. The number selects one entry in a table the verifier knows about; the verifier looks up that entry’s bpf_func_proto, checks every argument against the declared types, and only then lets the JIT wire the call to the real C function. The analogy is tight enough that the kernel documentation literally points at bpf-helpers(7) — a man page in the same family as syscalls(2) (helpers.rst, v6.12).

The single most important consequence of “helpers are UAPI” is append-only evolution. A helper id, once assigned, is permanent: id 1 will always be bpf_map_lookup_elem, id 113 will always be bpf_probe_read_kernel. Numbers are never reused, and a helper’s argument/return contract is never altered in a breaking way. New helpers are added by appending the next integer. This is what lets a BPF object compiled years ago still load on a current kernel: the call BPF_CALL imm=1 means the same thing it always did.

flowchart LR
  PROG["BPF program<br/>BPF_CALL imm=1"] -->|"helper id 1"| TBL["enum bpf_func_id<br/>(UAPI, frozen)"]
  TBL --> PROTO["struct bpf_func_proto<br/>ret_type + arg1..arg5 types"]
  PROTO --> VCHK["Verifier<br/>checks each arg type,<br/>computes return type"]
  GATE["per-prog-type<br/>get_func_proto() switch"] -->|"is helper N allowed<br/>for THIS program type?"| PROTO
  VCHK --> JIT["JIT patches imm -> address<br/>of the real C function"]

The helper call pipeline. What it shows: a BPF_CALL instruction carries a helper id in its 32-bit immediate; the id indexes the UAPI-frozen enum bpf_func_id; the kernel resolves it to a bpf_func_proto, but only after the program type’s get_func_proto() callback confirms this helper is permitted for this kind of program; the verifier enforces the proto’s argument and return types; finally the JIT rewrites the immediate to the function’s real address. The insight to take: two independent gates protect every helper call — a per-program-type allow-list (“may you call this at all?”) and a per-helper type contract (“are your arguments shaped correctly?”). Both are static, both run before the program ever executes, and both are mirror images of how kfuncs are gated (by BTF-set registration and BTF type matching).


Mechanical Walk-through

The helper id table is UAPI

Every helper has an entry in a single macro, ___BPF_FUNC_MAPPER, in include/uapi/linux/bpf.h. Each line is FN(name, id, ...):

FN(unspec,              0, ...)   /* sentinel, not a real helper */
FN(map_lookup_elem,     1, ...)
FN(map_update_elem,     2, ...)
FN(map_delete_elem,     3, ...)
FN(probe_read,          4, ...)
FN(ktime_get_ns,        5, ...)
...
FN(cgrp_storage_get,    210, ...)
FN(cgrp_storage_delete, 211, ...)

The macro is expanded into the actual enumeration:

#define __BPF_ENUM_FN(x, y) BPF_FUNC_ ## x = y,
enum bpf_func_id {
        ___BPF_FUNC_MAPPER(__BPF_ENUM_FN)
        __BPF_FUNC_MAX_ID,
};

In 6.12 LTS the last assigned id is cgrp_storage_delete = 211, so enum bpf_func_id holds 212 named values (0 through 211) and __BPF_FUNC_MAX_ID evaluates to 212 (uapi/linux/bpf.h, v6.12). Id 0 (unspec) is a reserved sentinel, so there are 211 real helpers. Because this enum is in uapi/, it is part of the kernel’s user-facing contract: the numbers are stable forever, which is why the comment above __BPF_ENUM_FN reads “integer value in ‘imm’ field of BPF_CALL instruction selects which helper function eBPF program intends to call.” The compiler emits these numbers; the kernel resolves them.

When you write BPF C with libbpf, you do not type these numbers — you call functions like bpf_map_lookup_elem(...) declared in bpf_helper_defs.h, a header that libbpf generates from this same uapi enum. Each declaration is a function pointer at a fixed address-as-integer; Clang lowers the call into a BPF_CALL instruction with the right immediate. So bpf_helper_defs.h is just the userspace-facing face of enum bpf_func_id.

struct bpf_func_proto — the contract the verifier enforces

A bare id is not enough; the verifier needs to know how to type-check a call. That is what struct bpf_func_proto carries (include/linux/bpf.h, v6.12):

struct bpf_func_proto {
        u64 (*func)(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5);
        bool gpl_only;
        bool pkt_access;
        bool might_sleep;
        bool allow_fastcall;
        enum bpf_return_type ret_type;
        enum bpf_arg_type arg1_type;
        enum bpf_arg_type arg2_type;
        enum bpf_arg_type arg3_type;
        enum bpf_arg_type arg4_type;
        enum bpf_arg_type arg5_type;
        /* ... arg*_btf_id, arg*_size unions follow ... */
};

Walking the fields: func is the real C implementation the JIT will call — it takes five 64-bit args because BPF passes arguments in registers R1R5 and returns in R0. gpl_only means the helper may only be used by a program declaring a GPL-compatible license (many tracing helpers are GPL-only). pkt_access says the helper may touch packet data. might_sleep marks helpers that can block, restricting them to sleepable programs. ret_type and the five arg*_type fields are the heart of the contract: they are drawn from two enums the verifier understands.

The argument types (enum bpf_arg_type) are a small vocabulary of what the verifier must prove about each register before the call. For example ARG_CONST_MAP_PTR requires the argument be a pointer to a struct bpf_map known at verification time; ARG_PTR_TO_MAP_KEY requires a stack pointer sized to the map’s key; ARG_PTR_TO_MEM paired with ARG_CONST_SIZE describes a memory-and-length pair the verifier bounds-checks; ARG_ANYTHING accepts any initialised scalar (include/linux/bpf.h, v6.12). The return types (enum bpf_return_type) tell the verifier what kind of value comes back: RET_INTEGER, RET_PTR_TO_MAP_VALUE_OR_NULL (a map-value pointer that might be NULL, forcing a NULL-check before use), RET_PTR_TO_BTF_ID, and so on. The _OR_NULL variants are how the verifier makes you check the result of a lookup before dereferencing it — the single most common pattern in real BPF code.

A concrete proto, the canonical bpf_map_lookup_elem (helpers.c, v6.12):

const struct bpf_func_proto bpf_map_lookup_elem_proto = {
        .func     = bpf_map_lookup_elem,
        .gpl_only = false,
        .pkt_access = true,
        .ret_type = RET_PTR_TO_MAP_VALUE_OR_NULL,
        .arg1_type = ARG_CONST_MAP_PTR,
        .arg2_type = ARG_PTR_TO_MAP_KEY,
};

This single struct tells the verifier: argument 1 must be a constant map pointer, argument 2 must be a stack pointer sized to that map’s key, and the result is a map-value pointer that may be NULL (so the program must test it). bpf_ktime_get_ns is the minimal case — it takes nothing and returns an integer:

const struct bpf_func_proto bpf_ktime_get_ns_proto = {
        .func     = bpf_ktime_get_ns,
        .gpl_only = false,
        .ret_type = RET_INTEGER,
};

bpf_get_current_pid_tgid is similarly argument-free; its implementation packs the thread-group id and pid into one 64-bit value ((u64) task->tgid << 32 | task->pid), which is why userspace code does pid_tgid >> 32 to get the PID and pid_tgid & 0xFFFFFFFF to get the kernel thread id (helpers.c, v6.12).

Per-program-type gating: the get_func_proto switch

A helper existing in the enum does not mean every program may call it. Each program type provides a get_func_proto(func_id, prog) callback in its bpf_verifier_ops; the verifier calls it for each helper the program invokes, and a NULL return means “this helper is not available to this program type → reject.” This is the per-program-type allow-list.

The pattern is a switch that returns the proto for permitted helpers and delegates the rest. For kprobe programs (bpf_trace.c, v6.12):

kprobe_prog_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
        switch (func_id) {
        case BPF_FUNC_perf_event_output:
                return &bpf_perf_event_output_proto;
        case BPF_FUNC_get_stackid:
                return &bpf_get_stackid_proto;
        case BPF_FUNC_get_stack:
                return prog->sleepable ? &bpf_get_stack_sleepable_proto
                                       : &bpf_get_stack_proto;
        /* ... */
        default:
                return bpf_tracing_func_proto(func_id, prog);
        }
}
const struct bpf_verifier_ops kprobe_verifier_ops = {
        .get_func_proto  = kprobe_prog_func_proto,
        .is_valid_access = kprobe_prog_is_valid_access,
};

The kprobe handler returns tracing-specific helpers directly, then falls through to bpf_tracing_func_proto for the helpers shared by all tracing program types (kprobe, tracepoint, perf_event, raw tracepoint), which in turn falls through to bpf_base_func_proto for the universally-available set. This chain — type-specific → category-shared → base — is how the same helper id can be allowed for one program type and rejected for another. An XDP program calling bpf_probe_read_kernel is rejected because XDP’s get_func_proto never returns that proto; a kprobe program calling bpf_xdp_adjust_head is rejected for the mirror reason.

The base set and the privilege gate

bpf_base_func_proto defines the helpers available to (almost) every program type (helpers.c, v6.12). Its first switch returns unconditionally-available helpers — the map accessors (map_lookup_elem, map_update_elem, map_delete_elem, the push/pop/peek queue ops, map_lookup_percpu_elem), randomness (get_prandom_u32), CPU/NUMA identity (get_smp_processor_id, get_numa_node_id), the time helpers (ktime_get_ns, ktime_get_boot_ns, ktime_get_tai_ns), the ring-buffer family (ringbuf_output/reserve/submit/discard/query), string compare/parse (strncmp, strtol, strtoul), tail calls, and get_current_pid_tgid. After that first switch there is an explicit privilege check:

if (!bpf_token_capable(prog->aux->token, CAP_BPF))
        return NULL;
 
switch (func_id) {
case BPF_FUNC_spin_lock:   return &bpf_spin_lock_proto;
case BPF_FUNC_spin_unlock: return &bpf_spin_unlock_proto;
case BPF_FUNC_jiffies64:   return &bpf_jiffies64_proto;
case BPF_FUNC_per_cpu_ptr: return &bpf_per_cpu_ptr_proto;
/* timers, kptr_xchg, bpf_loop, for_each_map_elem, dynptr ringbuf ... */
}

The second batch — spinlocks, jiffies64, per-CPU pointer helpers, the timer family, kptr_xchg, bpf_loop, for_each_map_elem, the dynptr-based ringbuf helpers — is only returned if the program holds CAP_BPF (capability CAP_BPF, or an equivalent BPF token). This is how the kernel keeps the more powerful helpers out of reach of unprivileged programs while still exposing a minimal safe set. (See CAP_BPF and BPF Privilege Model for the capability split.)


Canonical Helpers Worth Knowing

A reader new to BPF will meet the same handful of helpers in almost every program. Each is identified here by its frozen id (uapi/linux/bpf.h, v6.12):

  • bpf_map_lookup_elem (1) / bpf_map_update_elem (2) / bpf_map_delete_elem (3) — the fundamental map accessors. lookup returns a RET_PTR_TO_MAP_VALUE_OR_NULL, so the verifier forces a NULL check before you touch the value. These are the basis of all persistent state (see BPF Maps).
  • bpf_probe_read_kernel (113) / bpf_probe_read_user (112) and their _str variants (115/114) — the safe way for a tracing program to read kernel or user memory by address; they fault-protect the access and return an error rather than crashing. They replace the older, address-space-ambiguous bpf_probe_read (4), whose own uapi documentation now says “Generally, use bpf_probe_read_user() or bpf_probe_read_kernel() instead” (uapi/linux/bpf.h, v6.12). This is the key nuance about “append-only”: the number 4 and its signature are frozen forever, but the helper is discouraged — frozen ABI does not mean recommended forever.
  • bpf_ktime_get_ns (5) — a monotonic nanosecond timestamp; the workhorse for latency measurement (timestamp on entry, again on exit, store the delta).
  • bpf_get_current_pid_tgid (14) — packs tgid (the userspace PID) in the high 32 bits and pid (the kernel thread id) in the low 32 bits; the canonical way to attribute an event to a process.
  • bpf_perf_event_output (25) — pushes an event record into a perf ring buffer keyed by CPU; the classic (pre-ringbuf) path for streaming variable-size events to userspace.
  • bpf_ringbuf_reserve (131) / bpf_ringbuf_submit (132) / bpf_ringbuf_discard (133) / bpf_ringbuf_output (130) — the modern BPF ring buffer API. reserve returns a RET_PTR_TO_RINGBUF_MEM_OR_NULL you fill in and then submit (or discard on error); output is the copy-based variant. This is the recommended event-streaming mechanism, superseding per-CPU perf buffers for most use cases (see BPF Ring Buffer).

Failure Modes and Common Misunderstandings

“The verifier says my helper isn’t allowed here.” The program type’s get_func_proto returned NULL for that helper id. The fix is to use a helper that is on that program type’s allow-list, or to choose a different program type. A tracing helper like bpf_probe_read_kernel simply does not exist for networking program types and vice-versa; this is by design, not a bug.

“My program won’t load — bpf_probe_read_kernel is rejected as GPL-only.” Many tracing helpers set .gpl_only = true. If your program’s SEC("license") (or char LICENSE[] SEC("license")) is not a GPL-compatible string, every GPL-only helper proto is refused. The fix is to declare a GPL-compatible license, which you must only do if your code genuinely is GPL-compatible.

“I forgot to NULL-check a map lookup.” Because bpf_map_lookup_elem returns RET_PTR_TO_MAP_VALUE_OR_NULL, the verifier tracks the result as a maybe-NULL pointer and refuses any dereference until you branch on != NULL. The error is a “R0 invalid mem access ‘map_value_or_null’” style message. This is the verifier doing its job; the fix is the if (!val) return 0; guard.

Assuming “stable ABI” means “you should always use the oldest helper.” Stability is about not breaking old programs, not about recommending old helpers. bpf_probe_read (4) is the canonical example: permanent, frozen, and discouraged in favour of the explicit-address-space variants. Read each helper’s uapi documentation for “use X instead” notes.

Confusing the helper number with the function address. In bytecode the call carries the number; the JIT patches it to the real address at load time. Disassembling a loaded program shows the resolved address, but the on-disk object carries the portable number — which is what makes the object loadable across kernels.


Helpers vs kfuncs — the Trade-off That Defines Both

This is the spine connecting this note to BPF Kernel Functions (kfuncs). Helpers and kfuncs are two mechanisms for the same job — letting a BPF program call into the kernel — with opposite stability postures:

DimensionHelpers (this note)kfuncs (sibling)
Selected byinteger id in imm of BPF_CALL (enum bpf_func_id)BTF id of a BTF_KIND_FUNC, via BPF_PSEUDO_KFUNC_CALL
StabilityUAPI-frozen: id never recycled, signature never brokenexplicitly unstable: may change or be removed across releases
Type-checked viastruct bpf_func_proto (arg*_type/ret_type enums)BTF type matching + KF_* flags
Per-prog-type gateget_func_proto() switch returns the proto or NULLregister_btf_kfunc_id_set(prog_type, ...) registers the set
Added byappending the next integer to the uapi enum (a big deal)tagging a kernel function __bpf_kfunc and listing it (cheap)
Coupled tonothing — portable across kernel versionsthe running kernel’s BTF — version-coupled

The mechanical contrast is sharp and worth stating precisely. A helper call is BPF_CALL with src_reg == 0 and the helper id in the immediate. A kfunc call is BPF_CALL with src_reg == BPF_PSEUDO_KFUNC_CALL (value 2) and imm == btf_id of a BTF_KIND_FUNC in the running kernel (uapi/linux/bpf.h, v6.12). So a helper is named by a number assigned by the BPF maintainers and frozen forever; a kfunc is named by a BTF id resolved against whatever kernel you happen to be running on. That single difference — frozen number versus per-kernel BTF id — is the entire stability story in one line.

Why the kernel community is steering new functionality to kfuncs. Every helper is a permanent UAPI commitment. Minting a new helper means the BPF maintainers promise to support that exact signature forever, across every future kernel, for every program type that ever gained access to it. That is a heavy promise, so the bar for adding a helper is high and the process is slow. A kfunc carries no such handcuffs: it is treated like EXPORT_SYMBOL_GPL — a kernel↔kernel interface a subsystem maintainer can add, change, or remove as the subsystem evolves (kfuncs.rst, v6.12). The result is that helpers have become a largely closed set — the last id, cgrp_storage_delete (211 in 6.12), reflects a tail that grows slowly — while almost all genuinely new kernel surface now reaches BPF as kfuncs. The full argument for why unstable-by-design is the right call lives in BPF Kernel Functions (kfuncs); this note’s job is to explain what the frozen-ABI side buys you (portability and permanence) and what it costs (rigidity and a slow, conservative process).


Production Notes

In real deployments, the helper-vs-kfunc split shows up as a portability gradient. Tools that must run unmodified across a wide span of kernels — distributed observability agents, security sensors shipped as a single binary — lean on helpers precisely because the frozen ABI means bpf_map_lookup_elem and bpf_perf_event_output behave identically on a 5.x kernel and a 6.12 kernel. The CO-RE story handles data layout portability; the frozen helper ABI handles callable portability. Together they are why a libbpf-based tool ships as a small static binary rather than carrying a compiler.

The bpf-helpers(7) man page is the practical catalogue most engineers consult; it is regenerated from the uapi documentation comments that sit beside each FN(...) line, so it is authoritative for signatures and per-helper notes (man7.org bpf-helpers(7)). When a helper’s man-page description says “use X instead,” treat that as the kernel telling you the helper is frozen-but-superseded.

A recurring real-world surprise is the GPL-only gate biting a proprietary agent: a vendor’s BPF program declares a permissive license, then fails to load the moment it calls a GPL-only tracing helper. The diagnosis is always the same — check the program’s license section against the .gpl_only flag of the helpers it calls. The verifier log names the rejected helper.

Uncertain

Verify: the exact set of helpers gated behind the second (CAP_BPF) switch in bpf_base_func_proto, and the precise universally-available set, are stated from the 6.12 source. These switch contents shift between releases (helpers move in/out of the base set, new ones are appended). Reason: enumerated from kernel/bpf/helpers.c at tag v6.12 only; not re-diffed against 6.18. To resolve: re-read bpf_base_func_proto in kernel/bpf/helpers.c at the target kernel tag. uncertain


See Also