In eBPF, what is a helper function, and why can a BPF program call bpf_map_lookup_elem() but not an arbitrary kernel function such as kmalloc()?
answer
- a small, closed vocabulary the verifier understands
- calls carry an ID, not a kernel address
- which ones you get depends on program type
- return values are part of the contract
- kfuncs trade stability for reach
basics
~20 sBPF helpers are a fixed set of kernel functions, each with a numbered ID and a prototype the verifier knows, that BPF programs are allowed to call. Arbitrary kernel functions like kmalloc() have no such contract and cannot be proven safe.
solid answer
~60 sA helper is a kernel function exposed to BPF through a stable, enumerated ABI: it has a fixed ID, a fixed prototype, and rules the verifier understands about its arguments and its return value. When the verifier sees a call it checks that each argument matches — that a map pointer really is a map pointer, that a memory argument is paired with a length the program has already bounds-checked — and it records what the return value means, for example that `bpf_map_lookup_elem()` returns *either* a pointer into the map *or* NULL, so the program must test it before dereferencing. Arbitrary kernel symbols have none of that: no stable prototype, no argument semantics the verifier can reason about, no promise the function is safe in the context where the hook fires. Which helpers you may call also depends on program type — a tracing program and an XDP program have different allow-lists. Modern kernels add **kfuncs**, kernel functions annotated with BTF that BPF may call, but they are explicitly not a stable ABI.
code
c · 25 lines#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, __u64);
} last_seen SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_execve")
int on_execve(void *ctx)
{
__u32 pid = bpf_get_current_pid_tgid() >> 32;
__u64 now = bpf_ktime_get_ns();
__u64 *prev = bpf_map_lookup_elem(&last_seen, &pid);
if (!prev) /* NULL check is mandatory */
return bpf_map_update_elem(&last_seen, &pid, &now, BPF_ANY);
*prev = now;
return 0;
}
char LICENSE[] SEC("license") = "GPL";go deeper
Know that BPF programs work through named helper calls such as bpf_map_lookup_elem() and bpf_ktime_get_ns(), and that the list of callable functions is fixed rather than open.
Explain the contract: a helper has an ID and a prototype the verifier checks, arbitrary kernel symbols have neither, and the return value carries meaning the program must act on.
Bring the operational angle — per-type allow-lists, GPL-only helpers, and instrumenting failed map updates so a full map does not silently eat events.
Frame the ABI tradeoff: helpers are a permanent compatibility promise, kfuncs buy reach at the cost of pinning your tooling to kernel versions across a fleet.
## The problem helpers solve BPF bytecode runs in the kernel but is not trusted. The verifier has to prove, before the program ever runs, that it terminates and touches nothing it should not. That proof only works if the program's vocabulary is small and completely understood. Raw memory access is constrained to the program's stack, the context, and pointers the verifier itself handed out. Everything else — allocating, hashing, timing, reading a task's name, pushing an event to user space — happens through **helpers**. ## What a helper actually is A helper is a kernel function published to BPF through a fixed table. Each has a number (the `BPF_FUNC_*` ID that a `call` instruction carries), a fixed prototype of up to five arguments, and metadata describing what each argument is and what the return value means. Some familiar ones: - `bpf_map_lookup_elem()`, `bpf_map_update_elem()`, `bpf_map_delete_elem()` — the map operations. - `bpf_ktime_get_ns()` — a monotonic timestamp. - `bpf_get_current_pid_tgid()`, `bpf_get_current_uid_gid()`, `bpf_get_current_comm()` — who is running. - `bpf_perf_event_output()`, `bpf_ringbuf_reserve()`, `bpf_ringbuf_submit()` — pushing events out. - `bpf_printk()` (the `bpf_trace_printk` helper behind the macro) — debug output to the trace pipe. At load time the compiler has already emitted these as calls to the helper IDs, and the verifier type-checks each one. ## Why not just call kmalloc() Several reasons stack up, and a good answer names more than one. **There is no linkage.** BPF bytecode is not linked against the kernel image. A call instruction carries a helper ID, not a resolved kernel address, precisely so that programs are portable and so the set of callable things is a closed list. **There is no proof.** The verifier's whole job is to reason about what a call does to the machine state. For a helper it has a description: this argument must be a pointer to a map, this one a pointer to `value_size` bytes of readable stack, the return value is a possibly-NULL pointer valid for so many bytes. For an arbitrary symbol it has none of that, so it cannot certify anything about what happens afterwards. **There is no context guarantee.** A tracing program can fire inside an interrupt, holding kernel locks, in NMI context. A function that sleeps, takes a lock, or allocates with `GFP_KERNEL` would deadlock or crash the kernel there. Helpers are chosen and written to be callable from the contexts their program types run in. **There is no stable API.** Kernel-internal functions change signature and disappear between releases. Helper IDs are a compatibility contract: a program built against them keeps working. ## Per-program-type allow-lists Even within the helper set, availability depends on program type. Socket filters, XDP programs, tracing programs and cgroup programs each get a different subset, and some helpers are marked GPL-only, so a program whose `license` section is not GPL-compatible is rejected when it calls one. "That helper does not exist here" is a common load failure, and the fix is to check what your program type is allowed to call rather than to blame the toolchain. ## Return values are part of the contract The most common helper mistake is ignoring the return value. `bpf_map_lookup_elem()` returns a pointer *into the map* or NULL, and the verifier will refuse to load a program that dereferences it without a check: ```c __u64 *val = bpf_map_lookup_elem(&counts, &key); if (!val) return 0; /* the verifier requires this test */ __sync_fetch_and_add(val, 1); ``` Update and delete return a negative errno; a map that is full will start rejecting inserts, and a program that never inspects the return simply loses those events. `bpf_ringbuf_reserve()` returns NULL when the buffer is full, and the same discipline applies. ## kfuncs: the modern extension Helpers grew slowly because each one is a permanent ABI commitment. Newer kernels therefore support **kfuncs** — kernel functions annotated with BTF type information and registered into sets that particular program types may call. They give BPF access to far more of the kernel without inflating the helper table, but the kernel documents them as *not* a stable interface: a kfunc may change or vanish between releases, which is exactly the tradeoff helpers were designed to avoid. When you rely on one, you accept a kernel-version dependency. ## What people get wrong Saying "BPF runs in a sandbox so it cannot call kernel code" misses the point — it calls kernel code constantly, but only through a vetted interface. Assuming any helper is available in any program type is the second trap. And treating helper return values as decoration is the third, and the one that silently loses data in production.
- A program loads fine as a kprobe but is rejected when you attach the same logic as an XDP program, complaining about an unknown helper. What is going on?Helper availability is per program type. Each type has its own allow-list, chosen for the context it runs in — an XDP program runs in the driver receive path with no current task, so task-oriented helpers such as bpf_get_current_comm() are simply not offered to it. The fix is to get the data another way, not to force the call.
- What does it mean that some BPF helpers are GPL-only?Those helpers are exported as GPL symbols, so the verifier checks the program's license string, declared in its `license` section, and rejects the load unless it is GPL-compatible. It is a licensing boundary enforced at load time, not a capability or privilege check.
- Why does ignoring the return value of bpf_map_update_elem() cause problems in production rather than at load time?The verifier only cares that the call is well-formed, not that you inspect the result. At runtime a full or size-limited map starts returning a negative errno, and a program that discards it drops those events silently. Tools that care keep a separate counter of failed updates so the loss is visible.
saying these in an interview costs you the question
- Says BPF cannot call kernel code at all
- Assumes every helper is available to every program type
- Dereferences a lookup result without a NULL check
- Treats kfuncs as a stable, version-proof interface
- Ignores helper return values as unimportant