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), astruct bpf_func_protothat 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 R1–R5 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.lookupreturns aRET_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_strvariants (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-ambiguousbpf_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) — packstgid(the userspace PID) in the high 32 bits andpid(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.reservereturns aRET_PTR_TO_RINGBUF_MEM_OR_NULLyou fill in and thensubmit(ordiscardon error);outputis 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:
| Dimension | Helpers (this note) | kfuncs (sibling) |
|---|---|---|
| Selected by | integer id in imm of BPF_CALL (enum bpf_func_id) | BTF id of a BTF_KIND_FUNC, via BPF_PSEUDO_KFUNC_CALL |
| Stability | UAPI-frozen: id never recycled, signature never broken | explicitly unstable: may change or be removed across releases |
| Type-checked via | struct bpf_func_proto (arg*_type/ret_type enums) | BTF type matching + KF_* flags |
| Per-prog-type gate | get_func_proto() switch returns the proto or NULL | register_btf_kfunc_id_set(prog_type, ...) registers the set |
| Added by | appending the next integer to the uapi enum (a big deal) | tagging a kernel function __bpf_kfunc and listing it (cheap) |
| Coupled to | nothing — portable across kernel versions | the 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 inbpf_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 fromkernel/bpf/helpers.cat tagv6.12only; not re-diffed against 6.18. To resolve: re-readbpf_base_func_protoinkernel/bpf/helpers.cat the target kernel tag. uncertain
See Also
- BPF Kernel Functions (kfuncs) — the contrasting, intentionally-unstable mechanism; read together with this note for the full stability trade-off
- eBPF Verifier — the engine that consumes
bpf_func_protoand enforces every argument/return type at load time - BTF (BPF Type Format) — the type information that helpers do not depend on but kfuncs do; the mechanical root of the stability difference
- BPF Program Types — each type’s
get_func_protocallback defines its helper allow-list - BPF Maps — the persistent state the map-accessor helpers operate on
- BPF Ring Buffer — the modern event-streaming helpers (
bpf_ringbuf_*) and how they supersedebpf_perf_event_output - CAP_BPF and BPF Privilege Model — the capability gate that splits the base helper set
- CO-RE (Compile Once Run Everywhere) — the data-layout portability that complements the frozen helper ABI
- Linux eBPF MOC — parent map of content