The bpf() Syscall
Everything eBPF does to enter, query, and wire up the kernel flows through a single, multiplexed system call:
int bpf(int cmd, union bpf_attr *attr, unsigned int size). There is nobpf_prog_load()syscall and nobpf_map_create()syscall — there is one number in the syscall table, and thecmdargument selects which of ~three dozen operations to run, whileattrcarries a giantunionwhose active arm is chosen bycmd(per the man page and the kernel’sinclude/uapi/linux/bpf.h). The two properties that make this design coherent are: (1) every kernel-side BPF object — a loaded program, a map, a link, a BTF blob — is referred to from userspace by a file descriptor, reference-counted and torn down on the lastclose()unless pinned; and (2) thesizeargument plus a tail-zero check gives the union forward/backward compatibility as the kernel grows new fields. Almost nobody callsbpf()by hand — libbpf wraps it — but understanding the raw ABI is what makes the wrappers legible.
Mental Model: One Door, Many Rooms
Think of bpf() as a single front door into a building with many rooms. The cmd integer is the room number; attr is a parcel you hand through the door whose contents only make sense for the room you named; size tells the doorman how big your parcel is so they know which version of the building you were built against. The doorman (__sys_bpf in kernel/bpf/syscall.c) reads the room number, validates the parcel, and routes it to the right handler — map_create, bpf_prog_load, link_create, and so on.
This is the same pattern Linux uses for ioctl(), keyctl(), seccomp(), and prctl(): rather than burn dozens of syscall numbers (a scarce, ABI-frozen resource that must be wired into every architecture’s syscall table), a single number multiplexes a whole subsystem behind a command selector. The cost is that the ABI surface is opaque to tools like strace unless they special-case the command (they do), and that every command must independently validate the union arm it cares about.
flowchart TB U["userspace: syscall(SYS_bpf, cmd, &attr, size)"] U --> ENTRY["SYSCALL_DEFINE3(bpf, ...)<br/>-> __sys_bpf(cmd, uattr, size)"] ENTRY --> TAIL["bpf_check_uarg_tail_zero(uattr, sizeof attr, size)<br/>(reject if tail beyond 'size' is non-zero)"] TAIL --> COPY["size = min(size, sizeof attr)<br/>memset(&attr,0); copy_from_bpfptr(&attr, uattr, size)"] COPY --> SEC["security_bpf(cmd, &attr, size) (LSM hook)"] SEC --> SW{"switch (cmd)"} SW -->|BPF_MAP_CREATE| MC["map_create() -> new fd"] SW -->|BPF_PROG_LOAD| PL["bpf_prog_load() -> verifier -> JIT -> new fd"] SW -->|"BPF_MAP_*_ELEM"| ME["map_lookup/update/delete_elem()"] SW -->|BPF_OBJ_PIN/GET| OP["bpf_obj_pin/get() (bpffs)"] SW -->|BPF_PROG_ATTACH| PA["bpf_prog_attach()"] SW -->|BPF_LINK_CREATE| LC["link_create() -> new fd"] SW -->|BPF_BTF_LOAD| BL["bpf_btf_load() -> new fd"] SW -->|"BPF_*_GET_FD_BY_ID"| GF["bpf_*_get_fd_by_id() -> new fd"]
The single bpf() entry point and its dispatch, as it actually appears in kernel/bpf/syscall.c at v6.12. What it shows: one syscall validates the argument’s tail, copies a bounded prefix of the union, runs an LSM hook, then switches on cmd to a per-command handler — several of which (map_create, bpf_prog_load, link_create, the *_GET_FD_BY_ID family, bpf_btf_load) mint a new file descriptor as their return value, while the map-element and attach commands return 0/-errno. The insight to take: the fd-returning commands are the ones that create kernel objects; the fd is the only handle userspace ever gets, and the object lives exactly as long as some fd (or pin) references it.
The Command Set (enum bpf_cmd)
The complete command catalog is the enum bpf_cmd in include/uapi/linux/bpf.h. Counting distinct command values (ignoring BPF_PROG_RUN, which is just an alias for BPF_PROG_TEST_RUN, and the terminator __MAX_BPF_CMD), Linux 6.12 LTS defines 37 commands — BPF_MAP_CREATE (value 0) through BPF_TOKEN_CREATE (value 36) (verified against v6.12 bpf.h). Linux 6.18 LTS defines 38, adding BPF_PROG_STREAM_READ_BY_FD after BPF_TOKEN_CREATE (verified against v6.18 bpf.h). The set only grows — values are never reused once assigned, because they are a frozen ABI contract.
The commands group naturally by what they operate on:
Map data-plane — the create-and-CRUD over the generic key/value store (see BPF Maps): BPF_MAP_CREATE (make a map, returns an fd), BPF_MAP_LOOKUP_ELEM, BPF_MAP_UPDATE_ELEM, BPF_MAP_DELETE_ELEM, BPF_MAP_GET_NEXT_KEY (iterate by asking “what comes after this key”), BPF_MAP_LOOKUP_AND_DELETE_ELEM, and BPF_MAP_FREEZE (make a map read-only from userspace, used for .rodata). The batch variants BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH amortize syscall overhead by moving many elements per call.
Program lifecycle: BPF_PROG_LOAD (the big one — submit bytecode, run the verifier, JIT-compile, return a program fd), BPF_PROG_ATTACH / BPF_PROG_DETACH (the older attach mechanism, still used for cgroup and some networking hooks), BPF_PROG_TEST_RUN / BPF_PROG_RUN (execute a program once with synthetic input — the backbone of BPF unit tests), BPF_PROG_BIND_MAP (associate a map with a program after load), and BPF_RAW_TRACEPOINT_OPEN (the pre-link way to attach a raw tracepoint).
Object pinning (see BPF Object Pinning and Lifetime and Map Pinning and bpffs): BPF_OBJ_PIN writes an fd into the bpffs filesystem under a pathname so the object outlives its creating process; BPF_OBJ_GET re-opens a pinned object by pathname, yielding a fresh fd.
Links (the modern attachment primitive, see BPF Links and Attachment Lifecycle): BPF_LINK_CREATE (attach a program and get a link fd that owns the attachment), BPF_LINK_UPDATE (atomically swap the program behind a link), BPF_LINK_DETACH (force-detach while keeping the link object), and BPF_ITER_CREATE (instantiate a BPF iterator from a link).
Introspection / IDs: every BPF object has both an fd (process-local) and a stable global ID (system-wide, visible to bpftool). BPF_PROG_GET_NEXT_ID, BPF_MAP_GET_NEXT_ID, BPF_BTF_GET_NEXT_ID, BPF_LINK_GET_NEXT_ID walk the ID space; BPF_PROG_GET_FD_BY_ID, BPF_MAP_GET_FD_BY_ID, BPF_BTF_GET_FD_BY_ID, BPF_LINK_GET_FD_BY_ID turn an ID back into a usable fd (privilege permitting); BPF_OBJ_GET_INFO_BY_FD fills a bpf_*_info struct describing the object; BPF_PROG_QUERY lists which programs are attached at a hook; BPF_TASK_FD_QUERY asks “what BPF is behind this perf-event fd”; BPF_ENABLE_STATS turns on run-count/run-time accounting.
Type info and tokens: BPF_BTF_LOAD uploads a BTF blob (the type metadata that powers CO-RE and verifier-side struct knowledge), returning a BTF fd; BPF_TOKEN_CREATE (merged in 6.9) mints a BPF token from a bpffs mount to delegate scoped BPF privileges to an otherwise-unprivileged process (see BPF Token and Privilege Delegation).
The union bpf_attr — One Struct Per Command
The second argument, attr, is a union bpf_attr: a single union whose member structs each correspond to a command (or family of commands). Because it is a union, all arms overlap in memory; the active arm is implied by cmd. Userspace zero-fills the whole thing, populates only the arm it needs, and passes sizeof(union bpf_attr) (or the offset of the last field it uses) as size.
The BPF_MAP_CREATE arm is representative of how much an arm can carry:
struct { /* anonymous struct used by BPF_MAP_CREATE command */
__u32 map_type; /* one of enum bpf_map_type */
__u32 key_size; /* size of key in bytes */
__u32 value_size; /* size of value in bytes */
__u32 max_entries; /* max number of entries in a map */
__u32 map_flags; /* BPF_F_* creation flags */
__u32 inner_map_fd; /* fd of template map (for maps-of-maps) */
__u32 numa_node; /* NUMA node, if BPF_F_NUMA_NODE set */
char map_name[BPF_OBJ_NAME_LEN]; /* 16-byte name for introspection */
__u32 map_ifindex; /* netdev for offloaded maps */
__u32 btf_fd; /* fd of a loaded BTF blob */
__u32 btf_key_type_id; /* BTF type id of the key */
__u32 btf_value_type_id; /* BTF type id of the value */
__u32 btf_vmlinux_value_type_id; /* kernel struct as value (struct_ops) */
__u64 map_extra; /* per-type extra (bloom hashes, arena addr) */
__s32 value_type_btf_obj_fd;
__s32 map_token_fd; /* BPF token fd, if BPF_F_TOKEN_FD set */
};Reading it line by line: map_type selects the implementation (hash, array, ring buffer, LPM-trie, …); key_size/value_size/max_entries are the geometry; map_flags carries creation options (BPF_F_NO_PREALLOC, BPF_F_MMAPABLE, BPF_F_NUMA_NODE, BPF_F_TOKEN_FD, …); inner_map_fd makes maps-of-maps possible by giving a template; the btf_* fields attach type information so bpftool map dump can pretty-print keys and values; map_name is the 16-byte (BPF_OBJ_NAME_LEN) label you see in bpftool map list; and map_token_fd (with the BPF_F_TOKEN_FD flag) is how a delegated, unprivileged process proves it is allowed to create the map. The trailing map_token_fd and value_type_btf_obj_fd fields are newer additions to the end of the struct — which is exactly why the size mechanism matters.
The BPF_PROG_LOAD arm is the largest and most consequential. Its key fields: prog_type (selects the program type and thus the available helpers and context); insns + insn_cnt (a userspace pointer to the bytecode array and its length); license (a GPL-compatibility string — gpl_only helpers are rejected unless the program declares a GPL-compatible license); log_buf + log_size + log_level (where the verifier writes its multi-line reasoning, the text every BPF developer learns to read); prog_name; expected_attach_type; attach_btf_id / attach_btf_obj_fd (for fentry/fexit/LSM, the in-kernel BTF id of the function being hooked); func_info / line_info (BTF-keyed debug info); core_relos + core_relo_cnt (CO-RE relocation records, so the kernel can apply field relocations for some paths); fd_array (an array of fds — maps and BTF objects referenced by the program); log_true_size (output: the real log length, even if truncated); and prog_token_fd. The map-element arm, by contrast, is tiny — map_fd, a pointer to key, a union of value/next_key, and flags — yet it is the single most frequently invoked arm at runtime, because every userspace read of a map’s contents is one of these calls.
Other arms worth naming: the BPF_OBJ_* arm carries pathname + bpf_fd + file_flags + path_fd (the path_fd/BPF_F_PATH_FD pair lets pinning use an openat()-style directory fd); the BPF_PROG_ATTACH/DETACH arm carries target_fd/target_ifindex, attach_bpf_fd, attach_type, attach_flags, and replace_bpf_fd (for atomic replace at cgroup hooks); the link_create arm carries the program fd, the target, and per-type attach parameters; the BPF_BTF_LOAD arm carries the raw BTF blob plus its own log buffer; the BPF_*_GET_*_ID arm carries a start_id/prog_id/map_id/… union and a next_id output; and the compact token_create arm is just flags + bpffs_fd.
How Dispatch Actually Works (the v6.12 source)
The real entry point is SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size), which immediately forwards to __sys_bpf(cmd, USER_BPFPTR(uattr), size) (the bpfptr_t wrapper lets the same code path serve both userspace callers and the in-kernel bpf_sys_bpf helper). The first thing __sys_bpf does is the compatibility check (v6.12 syscall.c):
err = bpf_check_uarg_tail_zero(uattr, sizeof(attr), size);
if (err)
return err;
size = min_t(u32, size, sizeof(attr));
memset(&attr, 0, sizeof(attr));
if (copy_from_bpfptr(&attr, uattr, size) != 0)
return -EFAULT;This four-line sequence is the forward/backward-compatibility mechanism, and it is worth dwelling on because it is what lets one libbpf binary run across kernel generations:
bpf_check_uarg_tail_zero(uattr, sizeof(attr), size)handles the case where userspace is newer than the kernel. If the caller passes asizelarger than the kernel’s ownsizeof(union bpf_attr), the kernel inspects the bytes beyond its known struct and requires them to be all zero. If they are zero, the caller simply isn’t using any field the kernel doesn’t understand, and the call proceeds. If any tail byte is non-zero, the caller is relying on a feature this kernel lacks, and the call is rejected with-E2BIG. This is why you can compile against new headers and still run on an old kernel as long as you don’t set new fields.size = min_t(u32, size, sizeof(attr))then clamps to the kernel’s struct size, andmemset+copy_from_bpfptrcopies only that bounded prefix into a zero-initialized local. This handles the case where userspace is older than the kernel: a shortsizecopies fewer bytes, and the kernel’s newer trailing fields stay zero (their documented “unset” default). New fields are therefore always designed so that zero means “old behavior.”
After the copy, __sys_bpf calls the LSM hook security_bpf(cmd, &attr, size) — this is where SELinux, AppArmor, or a BPF-LSM policy can veto a command before any work happens — and then enters the switch (cmd). Each case is a thin shim: case BPF_MAP_CREATE: err = map_create(&attr);, case BPF_PROG_LOAD: err = bpf_prog_load(...);, case BPF_LINK_CREATE: ..., and so on. The per-command handlers do their own argument validation (typically via the CHECK_ATTR macro, which memchr_invs the bytes after each command’s declared last-used field to confirm the caller zeroed everything that command doesn’t read) and their own permission checks.
A subtle but important detail surfaced in the same file: bpf_sys_bpf (exposed to BPF programs as the bpf_sys_bpf kfunc, and as kern_sys_bpf for the kernel) re-enters __sys_bpf with a kernel bpfptr_t, but only whitelists a handful of commands (BPF_MAP_CREATE, BPF_PROG_LOAD, BPF_BTF_LOAD, BPF_LINK_CREATE, …). This is the machinery the light skeleton uses to load BPF from inside the kernel during boot — the same dispatch, called from a different side of the privilege boundary.
The File-Descriptor Object Model
The single most important semantic fact about bpf() is that every kernel BPF object is owned through file descriptors. BPF_MAP_CREATE, BPF_PROG_LOAD, BPF_BTF_LOAD, BPF_LINK_CREATE, and the *_GET_FD_BY_ID family all return a new fd; that fd is an anon_inode with O_CLOEXEC set automatically. The kernel keeps a reference count on each object, and the object is freed only when the last reference drops. References come from: open fds, pins in bpffs, and internal links from one object to another (a loaded program holds references to every map it uses, so the maps cannot disappear while the program is loaded).
The practical consequences:
- Programs and maps vanish when their loader exits — unless held. The man page states it plainly: “Generally, eBPF programs are loaded by the user process and automatically unloaded when the process exits” (bpf(2)). When your process dies, the kernel closes its fds, drops the refcounts, and reaps any object whose count hits zero. This is the canonical confusion for newcomers: a program “disappears” the moment the loader process ends. To outlive the process you must take another reference — by pinning to bpffs, or by handing the fd to a longer-lived holder, or (for attachments) by relying on the attachment itself to hold a reference.
- fds are inheritable and shareable. After
fork(2), the child shares the parent’s fds and therefore the same objects; the object survives as long as either holds it. fds can also be passed acrossSCM_RIGHTSUnix sockets, which is how privileged “BPF manager” daemons hand pre-loaded program fds to unprivileged consumers. - fd vs. ID. The fd is process-local and meaningless in another process; the ID is the system-wide stable handle (
bpftool prog show id 42). Turning an ID back into an fd (BPF_PROG_GET_FD_BY_ID) is itself a privileged operation — this is the boundary that stops an unprivileged process from grabbing another’s programs by guessing IDs.
This fd-centric model is why BPF links were introduced: the older BPF_PROG_ATTACH left an attachment that wasn’t owned by any fd (it lived in, say, a cgroup), so cleanup was ad hoc and racy. A link is an fd that owns an attachment and detaches it on close(), folding attachment into the same refcounted-fd discipline as everything else.
Failure Modes and Error Semantics
bpf() returns a new fd or 0 on success and -1 with errno set on failure. The errors are dense but legible once you know the model:
EACCESfromBPF_PROG_LOADis the verifier rejection — the program could not be proven safe. The why is in yourlog_buf; if you forgot to pass a log buffer you getEACCESwith no explanation, which is the single most common “the verifier hates me and won’t say why” trap. Always pass a log buffer (libbpf does, and re-loads with a bigger one on truncation).EPERMis a privilege failure — you lackCAP_BPF(orCAP_NET_ADMIN/CAP_PERFMONfor the relevant program/map type), or unprivileged BPF is disabled. See CAP_BPF and BPF Privilege Model and Unprivileged BPF and Its Restrictions; note that the man page’s “CAP_SYS_ADMIN required pre-4.4” framing is historical — the current 6.12 model is the split-capability one (bpf_capable(),is_net_admin_prog_type(),is_perfmon_prog_type()gate distinct subsets).E2BIGmeans either the program exceeds the instruction limit or — at the syscall boundary — yourattrtail-zero check failed because you set a field this kernel doesn’t know (you compiled against a newer header than the running kernel supports for that feature). Confirmed against the v6.12 source:bpf_check_uarg_tail_zeroreturns-E2BIGboth whenactual_size > PAGE_SIZEand when the bytes past the kernel’sexpected_sizeare not all zero (v6.12syscall.c).EINVALis the catch-all for a malformedattr: an unknowncmd, an out-of-rangemap_type, a non-zeroed reserved field thatCHECK_ATTRcaught, or asizethat doesn’t make sense.ENOENTfrom the map-element commands means the key wasn’t found (or, forGET_NEXT_KEY, you were already at the last key).EEXIST/ENOENTfromUPDATE_ELEMreflect theBPF_NOEXIST/BPF_EXISTflags.E2BIGfromUPDATE_ELEMmeans a fixed-capacity map is full.EBADFis a stale or wrong fd;EFAULTis a bad userspace pointer (e.g. akey/valuepointer outside your address space).
Why Nobody Calls It Directly
In principle you can drive bpf() with syscall(SYS_bpf, BPF_PROG_LOAD, &attr, sizeof(attr)) and hand-assembled bytecode — and the kernel selftests do exactly that to exercise edge cases. In practice this is almost never done, for three reasons. First, the union is a moving target: getting every field, flag, and size right by hand, across kernel versions, is exactly the toil that the tail-zero mechanism exists to paper over for a library, not for you. Second, bytecode authorship is miserable: real programs are written in C and compiled by Clang, and the compiled ELF needs map creation, relocation, and BTF handling before the BPF_PROG_LOAD arm can even be filled in. Third, the wrappers are excellent: libbpf provides both thin one-call wrappers (bpf_map_create(), bpf_prog_load(), bpf_link_create() in bpf.h, each a direct shim over one cmd) and the full high-level object/skeleton API. So the realistic stack is: your code calls libbpf → libbpf fills bpf_attr and calls bpf(). The raw syscall is the foundation everyone stands on and almost no one touches.
Uncertain
Verify: that no
bpf_cmdwas renamed or semantically changed (not merely added) between 6.12 and 6.18 beyond the singleBPF_PROG_STREAM_READ_BY_FDaddition. Reason: the count was checked precisely at both versions, but only the enum tail was diffed, not every command’sbpf_attrarm. To resolve: diffinclude/uapi/linux/bpf.hbetweenv6.12andv6.18in full.#uncertain
See Also
- libbpf and the BPF Loader — the userspace library that fills
bpf_attrand callsbpf()for you; the practical reason this raw ABI is rarely touched - BPF Object Pinning and Lifetime — how
BPF_OBJ_PIN/BPF_OBJ_GETand bpffs let fd-owned objects outlive their creating process - BPF Links and Attachment Lifecycle —
BPF_LINK_CREATE/UPDATE/DETACHand why links fold attachment into the refcounted-fd model - BPF Maps — the objects created by
BPF_MAP_CREATEand driven by theBPF_MAP_*_ELEMcommands - eBPF Verifier — what
BPF_PROG_LOADtriggers before a program is accepted - BTF (BPF Type Format) — uploaded via
BPF_BTF_LOAD; referenced by map and program arms - CAP_BPF and BPF Privilege Model · BPF Token and Privilege Delegation — who may issue these commands;
BPF_TOKEN_CREATE - Linux eBPF MOC — parent map of content