BPF Maps
A BPF map is a generic key/value store, living in the kernel, that a Berkeley Packet Filter (BPF) program and userspace — and other BPF programs — can all read and write. It is the only persistent memory a BPF program has: a program is invoked afresh for each event (a packet arriving, a function being entered, a tracepoint firing), it gets a small stack and its registers, and when it returns, everything local is gone. Anything that must survive between invocations, or be observed from userspace, or be shared with another program, must live in a map. Maps are created by the
bpf()system call with theBPF_MAP_CREATEcommand, which returns a file descriptor; that descriptor is how userspace later looks up, updates, and deletes entries, and it is the lifetime anchor that keeps the map alive (per the kernel’s maps documentation andkernel/bpf/syscall.c). The kernel ships a broad catalog of map types — 33 distinct types as of Linux 6.12 and 6.18 — from a plain hash table or array to per-CPU variants, longest-prefix-match tries, ring buffers, and maps that hold other maps. Behind that catalog sits a single C-style vtable,struct bpf_map_ops, so one syscall path and one set of helpers serve thirty-odd very different data structures. This note is the overview that the type-specific notes hang off: it explains the object model, the two access paths, memory accounting, the concurrency rules, and the lifetime model that are common to all map types.
Kernel version — this note is pinned to an LTS
Every struct field, constant, error code, and code path below was read from the Linux 6.12 LTS source tree (released 2024-11-17), fetched from
raw.githubusercontent.com/torvalds/linux/v6.12/…while writing this note. 6.12 is chosen deliberately: it is a maintained long-term-support branch (6.12.107 shipped as of 2026-08), so its behaviour is what a large fraction of production kernels actually run, and it is the pin used across this vault’s eBPF material. Where something is known to differ in a later kernel, it is called out with its version. Treat every claim here as “as of 6.12 LTS” and re-check anything load-bearing against your own tree withbpftool feature probe.
Mental Model: A Program Is Stateless; the Map Is Its Memory
The single most important idea is that BPF programs are event handlers with no durable state of their own. When you attach a BPF program to a hook, the kernel calls it once per event. The program receives a context pointer (a packet, a struct pt_regs, a tracepoint argument record — the shape depends on the program type) in register R1, it has at most 512 bytes of stack (MAX_BPF_STACK), and it must return. There is no global variable that quietly accumulates across calls inside the program’s own address space; there is no heap it allocates from at will. The verifier — eBPF’s static safety checker, covered in eBPF Verifier — would reject anything else. So the question “how does a BPF program remember the packet count, or share a blocklist with userspace, or hand an event to a logging daemon?” has exactly one answer: a map.
A map, then, is the BPF memory model. It is a typed container the kernel owns and the program borrows. The program does not allocate it; userspace (or the loader) creates it ahead of time, the verifier records which maps a program references, and at run time the program calls helpers like bpf_map_lookup_elem to reach into it. Because the map lives in the kernel independently of any single program invocation, it bridges three boundaries at once: across time (one invocation writes, a later one reads), across the kernel/user boundary (a BPF program updates a counter, a userspace agent polls it), and across programs (two BPF programs attached to different hooks can coordinate through a shared map).
This is not an accident of implementation; it is the deliberate design. The kernel’s own bpf_design_QA.rst records the rule that keeps it that way — asked whether a BPF program can call an arbitrary kernel function, it answers “NO. BPF programs can only call specific functions exposed as BPF helpers or kfuncs” — and the map helpers are among the very few of those that hand back memory the program may write. Everything else the program touches is either read-only context or its own ephemeral stack.
flowchart LR subgraph US["Userspace"] LD["Loader / agent<br/>(libbpf, bpftool, your daemon)"] FD["map fd<br/>from BPF_MAP_CREATE"] LD --- FD end subgraph K["Kernel"] M["struct bpf_map<br/>ops, map_type, key_size,<br/>value_size, max_entries,<br/>refcnt, usercnt"] P1["BPF prog A<br/>e.g. XDP, runs per packet"] P2["BPF prog B<br/>e.g. kprobe, runs per call"] end BPFFS["bpffs pin<br/>/sys/fs/bpf/my_map"] FD -->|"bpf() BPF_MAP_*_ELEM<br/>COPIES the value in/out"| M P1 -->|"bpf_map_lookup_elem()<br/>returns a LIVE POINTER"| M P2 -->|"bpf_map_update_elem()"| M FD -.->|"BPF_OBJ_PIN"| BPFFS BPFFS -.->|"BPF_OBJ_GET<br/>(any process, later)"| M
How a map sits between userspace and BPF programs. What it shows: one struct bpf_map in the kernel, reached three ways — userspace goes through the bpf() syscall’s BPF_MAP_*_ELEM commands (which copy the value across the kernel/user boundary), BPF programs call helper functions that operate on the live in-kernel object (and, for lookups, get a direct pointer into it), and a bpffs pin lets an unrelated process obtain a fresh fd later. The insight to take: the map is the shared rendezvous point; the program holds no state itself, so all coordination — between invocations, between programs, and with userspace — is just reads and writes against this one object. The copy-versus-pointer asymmetry on the two live edges is the single most common source of BPF data races, and it is examined in detail below.
The Object Itself: struct bpf_map
Every map, regardless of type, is represented in the kernel by a struct bpf_map, defined in include/linux/bpf.h. It is worth reading the real thing rather than a summary, because the field list is the feature list: each field corresponds to a capability the map layer has grown over the years.
classDiagram class bpf_map { +const bpf_map_ops* ops +bpf_map* inner_map_meta +void* security +enum bpf_map_type map_type +u32 key_size +u32 value_size +u32 max_entries +u64 map_extra +u32 map_flags +u32 id +btf_record* record +int numa_node +u32 btf_key_type_id +u32 btf_value_type_id +btf* btf +obj_cgroup* objcg +char name[BPF_OBJ_NAME_LEN] +mutex freeze_mutex +atomic64_t refcnt +atomic64_t usercnt +atomic64_t writecnt +bool frozen +bool bypass_spec_v1 +atomic64_t sleepable_refcnt +s64 __percpu* elem_count +struct owner } class bpf_map_ops { <<vtable>> +map_alloc() +map_free() +map_lookup_elem() +map_update_elem() +map_delete_elem() +map_mem_usage() ... 40 more slots } class btf_record { <<value layout>> +BPF_SPIN_LOCK offset +BPF_TIMER offset +BPF_WORKQUEUE offset +BPF_KPTR offset +BPF_LIST_HEAD offset +BPF_RB_ROOT offset } class obj_cgroup { <<memcg charge target>> +captured at BPF_MAP_CREATE } class bpf_htab { +bpf_map map +bucket* buckets +bpf_mem_alloc ma +u32 n_buckets } class bpf_array { +bpf_map map +u32 elem_size +u32 index_mask +char value[] } bpf_map --> bpf_map_ops : dispatches through bpf_map --> btf_record : describes special fields inside each value bpf_map --> obj_cgroup : charges allocations to bpf_htab --|> bpf_map : embeds as first member bpf_array --|> bpf_map : embeds as first member
struct bpf_map as the base class of a C-style object hierarchy, v6.12. What it shows: the generic map object, the vtable it dispatches through, the two satellite objects that carry newer features (btf_record, which records where special fields such as bpf_spin_lock and kernel pointers sit inside the value; obj_cgroup, the memory-cgroup charge target captured at creation), and two concrete types that embed struct bpf_map as their first member so container_of() can recover them. The insight to take: this is textbook kernel object orientation in C — a base struct plus a function-pointer table, with each implementation embedding the base. Everything generic (identity, sizing, permissions, refcounting, accounting) lives in the base and is written once; everything type-specific lives behind ops.
The fields that matter for understanding the model are few, and each one answers a real question:
const struct bpf_map_ops *ops— the per-type vtable of function pointers. This is how one syscall path dispatches to thirty-odd different implementations, and it is dissected in its own section below.enum bpf_map_type map_type— which type this is (BPF_MAP_TYPE_HASH,BPF_MAP_TYPE_ARRAY, …). Kept in the base struct because generic code (the verifier,bpftool, permission checks) needs it without calling intoops.u32 key_size,u32 value_size— the fixed sizes, in bytes, of every key and every value. Chosen at creation, never changed. A key or value can be a scalar or a struct; the kernel treats them as opaque blobs of these sizes.u32 max_entries— the capacity. For an array this is the exact element count; for a hash it is the upper bound on live key/value pairs before updates start failing (or, for a least-recently-used hash, before eviction kicks in).u32 map_flags— creation-time flags:BPF_F_NO_PREALLOC,BPF_F_NO_COMMON_LRU,BPF_F_NUMA_NODE,BPF_F_RDONLY_PROG,BPF_F_WRONLY_PROG,BPF_F_MMAPABLE,BPF_F_PRESERVE_ELEMS,BPF_F_INNER_MAP,BPF_F_TOKEN_FD, and (for arenas)BPF_F_SEGV_ON_FAULTandBPF_F_NO_USER_CONV, at bit positions 0, 1, 2, 7, 8, 10, 11, 12, 16, 17 and 18 respectively ininclude/uapi/linux/bpf.h.struct btf_record *record— the map layer’s answer to “the value is not always a flat blob.” A value may embed abpf_spin_lock, abpf_timer, abpf_wq(workqueue), a kernel pointer (kptr), or the head of a BPF linked list or red-black tree.btf_recordrecords the byte offset of each such field, learned from BTF, so generic code can initialize, copy, and destroy values correctly. This is whycopy_map_value()exists rather than a plainmemcpy.struct obj_cgroup *objcg— the memory cgroup that map allocations are charged to, captured at creation time (see the memory-accounting section).atomic64_t refcnt,atomic64_t usercnt— the two reference counts that govern lifetime.refcntcounts all holders (file descriptors, loaded programs, pins, inner-map slots);usercntcounts only userspace holders. The split exists so a map type can be told “no userspace holder is left” (map_release_uref) while the object itself stays alive for still-running programs. Real users of that hook in v6.12: a hash or array map cancels anybpf_timer/bpf_wqembedded in its values (htab_map_free_timers_and_wq,array_map_free_timers_wq), a prog-array clears its program slots (prog_array_map_clear), and a sockmap releases its attached programs (sock_map_release_progsinnet/core/sock_map.c). Without the split, a timer armed from a map value could keep firing after the last userspace handle was gone.atomic64_t writecnt,bool frozen,struct mutex freeze_mutex— the machinery behindBPF_MAP_FREEZE, described below.atomic64_t sleepable_refcnt— counts loaded sleepable programs referencing the map. Its only consumer is the map-of-maps code: when this map is an outer map and an inner map is removed from one of its slots, a non-zerosleepable_refcntforces the inner map’s free onto the heavier RCU-tasks-trace path (see Lifetime).bool bypass_spec_v1— whether this map may skip Spectre-v1 index masking because the loader was privileged enough; see BPF and Spectre Hardening.s64 __percpu *elem_count— an optional per-CPU live-element counter used bybpftooland themap_mem_usagereporting path.
This triple — key_size, value_size, max_entries — plus the type is the entire shape of a map. A BPF_MAP_TYPE_HASH with key_size=4, value_size=8, max_entries=1024 is “a hash table from u32 to u64 holding up to 1024 entries.” There is no schema beyond the byte sizes; if you want a struct value, you set value_size = sizeof(struct foo) and both sides agree on the layout. BTF can additionally record the type of keys and values for tooling and verification — see BTF (BPF Type Format) — and, via btf_record, is what makes spin locks and kernel pointers inside values possible at all.
Creating a Map: BPF_MAP_CREATE → an fd
All map operations go through the single multiplexed bpf() system call (see The bpf() Syscall). To create a map, userspace fills a union bpf_attr and calls bpf(BPF_MAP_CREATE, &attr, sizeof(attr)). The mandatory fields, per Documentation/bpf/maps.rst, are map_type, key_size, value_size, and max_entries; map_flags and a map_name are optional. The documentation’s own worked example:
int fd;
union bpf_attr attr = {
.map_type = BPF_MAP_TYPE_ARRAY, /* mandatory */
.key_size = sizeof(__u32), /* mandatory */
.value_size = sizeof(__u32), /* mandatory */
.max_entries = 256, /* mandatory */
.map_flags = BPF_F_MMAPABLE,
.map_name = "example_array",
};
fd = bpf(BPF_MAP_CREATE, &attr, sizeof(attr));Line by line: map_type selects the vtable; key_size must be exactly 4 for an array (an array is indexed by u32, and array_map_alloc_check() rejects anything else with -EINVAL); value_size is the per-element blob size; max_entries is the element count, which for an array is also the allocation size; BPF_F_MMAPABLE asks for the value region to be page-aligned and vmalloc-backed so userspace can mmap() it and read counters without a syscall per read; and map_name is a debugging label, limited to the characters A-Z a-z 0-9 _ . and BPF_OBJ_NAME_LEN bytes, that shows up in bpftool map show. The call returns a process-local file descriptor; close(fd) deletes the map unless something else holds a reference, and “maps held by open file descriptors will be deleted automatically when a process exits.”
sequenceDiagram autonumber participant U as Userspace (libbpf) participant S as bpf() syscall<br/>kernel/bpf/syscall.c participant T as bpf_map_types[]<br/>vtable table participant O as Type impl<br/>e.g. htab_map_ops participant MM as mm / memcg U->>S: bpf(BPF_MAP_CREATE, &attr, size) S->>S: CHECK_ATTR(BPF_MAP_CREATE)<br/>reject unknown trailing fields S->>S: bounds-check map_type,<br/>array_index_nospec() mask S->>T: bpf_map_types[map_type] T-->>S: ops (NULL => -EINVAL) S->>O: ops->map_alloc_check(attr) O-->>S: 0 or -EINVAL / -E2BIG / -ENOTSUPP S->>S: reject if !ops->map_mem_usage S->>S: token / capability gate<br/>(unpriv | CAP_BPF | CAP_NET_ADMIN) S->>O: ops->map_alloc(attr) O->>MM: bpf_map_area_alloc()<br/>kmalloc_node or __vmalloc_node_range MM-->>O: zeroed memory O-->>S: struct bpf_map * S->>S: map->ops = ops; map->map_type = map_type<br/>refcnt = usercnt = 1 S->>S: bpf_obj_name_cpy(map->name, ...) S->>S: attach BTF, build btf_record S->>MM: bpf_map_save_memcg(map)<br/>capture current obj_cgroup S->>S: bpf_map_alloc_id() -> map->id S->>S: bpf_map_new_fd(map, f_flags) S-->>U: file descriptor (or negative errno)
The full BPF_MAP_CREATE path in v6.12, from syscall entry to returned fd. What it shows: the ordering of the checks — attribute sanity first, then vtable lookup, then type-specific validation, then privilege, and only then allocation — and the two bookkeeping steps that are easy to overlook, capturing the memory cgroup and allocating the global map ID. The insight to take: a BPF_MAP_CREATE failure’s errno tells you which stage rejected you. -EINVAL from step 3–5 means “no such map type or bad attribute shape”; -E2BIG or -ENOTSUPP from step 6 means the type refused your sizing (e.g. an LRU hash with BPF_F_NO_PREALLOC); -EPERM from step 8 means capabilities; -ENOMEM from step 9 means the allocation itself failed. Reading the stage backwards from the errno is the fastest way to debug a map that will not create.
Inside the kernel, map_create() in kernel/bpf/syscall.c does the dispatch. The core of it is a lookup into a statically built table:
/* kernel/bpf/syscall.c (v6.12), abridged */
map_type = attr->map_type;
if (map_type >= ARRAY_SIZE(bpf_map_types))
return -EINVAL;
map_type = array_index_nospec(map_type, ARRAY_SIZE(bpf_map_types));
ops = bpf_map_types[map_type]; /* the per-type vtable */
if (!ops)
return -EINVAL;
if (ops->map_alloc_check) { /* type-specific validation */
err = ops->map_alloc_check(attr);
if (err)
return err;
}
if (attr->map_ifindex)
ops = &bpf_map_offload_ops; /* hardware-offloaded map */
if (!ops->map_mem_usage)
return -EINVAL;
...
map = ops->map_alloc(attr); /* type-specific allocation */
map->ops = ops;
map->map_type = map_type;Walking this: attr->map_type is bounds-checked against the size of the bpf_map_types[] table; array_index_nospec() masks the index so a mis-speculated out-of-range load cannot leak kernel memory (a Spectre-v1 hardening — see BPF and Spectre Hardening); bpf_map_types[map_type] yields the vtable ops for that type, and a NULL slot means “this type was compiled out of your kernel,” which is why -EINVAL on create can simply mean a missing CONFIG_ option. Each type may supply a map_alloc_check that validates attributes before any memory is touched, and a map_alloc that does the real allocation. Note the mandatory map_mem_usage check: since Linux 6.4 every map type must be able to report its own memory footprint, and a type that cannot is refused outright.
The bpf_map_types[] table itself is generated by the C preprocessor from include/linux/bpf_types.h:
/* kernel/bpf/syscall.c (v6.12) */
static const struct bpf_map_ops * const bpf_map_types[] = {
#define BPF_MAP_TYPE(_id, _ops) [_id] = &_ops,
#include <linux/bpf_types.h>
#undef BPF_MAP_TYPE
};and bpf_types.h contains one line per type — BPF_MAP_TYPE(BPF_MAP_TYPE_HASH, htab_map_ops), BPF_MAP_TYPE(BPF_MAP_TYPE_ARRAY, array_map_ops), and so on, many of them wrapped in #ifdef CONFIG_… guards. The result is a sparse array indexed by the map-type enum value, each populated slot pointing at that type’s bpf_map_ops. The same header is #included several times with different definitions of the BPF_MAP_TYPE macro to build several parallel tables — an idiom the kernel calls an X-macro.
Who is allowed to create which type
Map creation is not uniformly privileged. map_create() contains an explicit three-tier switch (v6.12), and it is worth memorizing because it explains a lot of otherwise-mystifying -EPERMs:
| Tier | Map types | Requirement |
|---|---|---|
| Unprivileged | ARRAY, PERCPU_ARRAY, PROG_ARRAY, PERF_EVENT_ARRAY, CGROUP_ARRAY, ARRAY_OF_MAPS, HASH, PERCPU_HASH, HASH_OF_MAPS, RINGBUF, USER_RINGBUF, CGROUP_STORAGE, PERCPU_CGROUP_STORAGE | none beyond sysctl_unprivileged_bpf_disabled == 0 |
CAP_BPF | SK_STORAGE, INODE_STORAGE, TASK_STORAGE, CGRP_STORAGE, BLOOM_FILTER, LPM_TRIE, REUSEPORT_SOCKARRAY, STACK_TRACE, QUEUE, STACK, LRU_HASH, LRU_PERCPU_HASH, STRUCT_OPS, CPUMAP, ARENA | bpf_token_capable(token, CAP_BPF) |
CAP_NET_ADMIN | SOCKMAP, SOCKHASH, DEVMAP, DEVMAP_HASH, XSKMAP | bpf_token_capable(token, CAP_NET_ADMIN) |
Map-creation privilege tiers, read from the switch (map_type) in map_create(), v6.12. What it shows: the exact partition of the 33 types into three privilege classes, plus the global override — if the kernel.unprivileged_bpf_disabled sysctl is set, every row additionally needs CAP_BPF. The insight to take: the tiers track blast radius, not complexity. A plain hash map can only hurt the process that made it, so it is unprivileged; anything that redirects packets or steals sockets needs CAP_NET_ADMIN; anything that attaches storage to kernel objects (sockets, inodes, tasks) or plugs into kernel operations (STRUCT_OPS) needs CAP_BPF. The token argument threaded through every check is the BPF token mechanism (Linux 6.9+), which lets a privileged supervisor hand a delegated, narrowly-scoped creation right to an unprivileged container.
The freshly built struct bpf_map gets its ops and map_type stamped in, refcnt and usercnt initialized to 1, a global ID allocated from map_idr (this is the id that bpftool map show prints and that BPF_MAP_GET_FD_BY_ID resolves), and finally bpf_map_new_fd() wraps it in a file descriptor. Closing that fd — and dropping any pins — is what eventually frees the map.
The Per-Type Vtable: struct bpf_map_ops
struct bpf_map_ops is the heart of how one map abstraction serves thirty-odd very different data structures. In v6.12 it carries 41 function-pointer slots (plus a BTF type id and an iterator descriptor), and the source groups them by who is allowed to call them. That grouping is the conceptual key to the whole model.
/* include/linux/bpf.h (v6.12), abridged and annotated */
struct bpf_map_ops {
/* funcs callable from userspace (via syscall) */
int (*map_alloc_check)(union bpf_attr *attr);
struct bpf_map *(*map_alloc)(union bpf_attr *attr);
void (*map_release)(struct bpf_map *map, struct file *map_file);
void (*map_free)(struct bpf_map *map);
int (*map_get_next_key)(struct bpf_map *map, void *key, void *next_key);
void (*map_release_uref)(struct bpf_map *map);
void *(*map_lookup_elem_sys_only)(struct bpf_map *map, void *key);
int (*map_lookup_batch)(struct bpf_map *map, const union bpf_attr *attr,
union bpf_attr __user *uattr);
int (*map_update_batch)(struct bpf_map *map, struct file *map_file,
const union bpf_attr *attr, union bpf_attr __user *uattr);
int (*map_delete_batch)(struct bpf_map *map, const union bpf_attr *attr,
union bpf_attr __user *uattr);
/* funcs callable from userspace and from eBPF programs */
void *(*map_lookup_elem)(struct bpf_map *map, void *key);
long (*map_update_elem)(struct bpf_map *map, void *key, void *value, u64 flags);
long (*map_delete_elem)(struct bpf_map *map, void *key);
long (*map_push_elem)(struct bpf_map *map, void *value, u64 flags);
long (*map_pop_elem)(struct bpf_map *map, void *value);
long (*map_peek_elem)(struct bpf_map *map, void *value);
void *(*map_lookup_percpu_elem)(struct bpf_map *map, void *key, u32 cpu);
/* funcs called by prog_array and perf_event_array map */
void *(*map_fd_get_ptr)(struct bpf_map *map, struct file *map_file, int fd);
void (*map_fd_put_ptr)(struct bpf_map *map, void *ptr, bool need_defer);
int (*map_gen_lookup)(struct bpf_map *map, struct bpf_insn *insn_buf);
...
/* Direct value access helpers. */
int (*map_direct_value_addr)(const struct bpf_map *map, u64 *imm, u32 off);
int (*map_mmap)(struct bpf_map *map, struct vm_area_struct *vma);
__poll_t (*map_poll)(struct bpf_map *map, struct file *filp,
struct poll_table_struct *pts);
...
bool (*map_meta_equal)(const struct bpf_map *meta0, const struct bpf_map *meta1);
long (*map_redirect)(struct bpf_map *map, u64 key, u64 flags);
u64 (*map_mem_usage)(const struct bpf_map *map);
int *map_btf_id;
const struct bpf_iter_seq_info *iter_seq_info;
};The first group — map_alloc, map_free, map_get_next_key, map_release_uref, map_lookup_elem_sys_only, and the batch operations — is reachable only from userspace through the syscall. The second group — map_lookup_elem, map_update_elem, map_delete_elem, the queue/stack push/pop/peek, and the per-CPU lookup — is the set of operations that both userspace and a running BPF program can perform. When a BPF program calls the helper bpf_map_lookup_elem(&my_map, &key), the kernel routes to map->ops->map_lookup_elem(map, key); when userspace issues bpf(BPF_MAP_LOOKUP_ELEM, …), it routes to the same slot, after copying the key in from userspace and the value back out. One vtable slot, two callers, different memory semantics on each side.
The remaining groups are where individual map types earn their keep. map_gen_lookup lets a type ask the verifier to inline its lookup as raw BPF instructions instead of emitting a call. map_mmap and map_poll are what make an mmap-able array and an epoll-able ring buffer possible through the ordinary map fd. map_redirect is the XDP fast path for DEVMAP/CPUMAP/XSKMAP. map_meta_equal is the runtime check that lets a map be inserted into a map-of-maps. map_fd_get_ptr/map_fd_put_ptr are how a PROG_ARRAY or PERF_EVENT_ARRAY converts a userspace fd into a refcounted kernel pointer at update time.
classDiagram direction LR class bpf_map_ops { <<interface / vtable>> +map_alloc_check(attr) int +map_alloc(attr) bpf_map* +map_free(map) void +map_get_next_key(map,k,nk) int +map_lookup_elem(map,key) void* +map_update_elem(map,k,v,flags) long +map_delete_elem(map,key) long +map_push_elem / pop / peek +map_lookup_percpu_elem(map,k,cpu) void* +map_gen_lookup(map,insn_buf) int +map_mmap(map,vma) int +map_poll(map,filp,pts) __poll_t +map_redirect(map,key,flags) long +map_meta_equal(m0,m1) bool +map_mem_usage(map) u64 } class htab_map_ops { lookup, update, delete get_next_key, batch ops map_gen_lookup = direct call NO mmap, NO poll } class array_map_ops { lookup, update map_gen_lookup = fully inlined map_mmap = yes map_direct_value_addr = yes delete stub returns -EINVAL } class ringbuf_map_ops { lookup/update/delete stubs all return -ENOTSUPP map_mmap = yes, map_poll = yes key_size and value_size MUST be 0 } class queue_map_ops { push / pop / peek only NO keyed lookup } class dev_map_ops { map_redirect = yes holds net_device pointers } class array_of_maps_map_ops { map_fd_get_ptr / put_ptr map_meta_equal enforced inner lookup only from BPF } bpf_map_ops <|.. htab_map_ops bpf_map_ops <|.. array_map_ops bpf_map_ops <|.. ringbuf_map_ops bpf_map_ops <|.. queue_map_ops bpf_map_ops <|.. dev_map_ops bpf_map_ops <|.. array_of_maps_map_ops
struct bpf_map_ops as an interface, with six of its ~33 implementations. What it shows: each concrete type fills in only the slots that make sense for it, and leaves the rest NULL. The insight to take: “which operations does this map type support?” is answered mechanically by the vtable — there is no capability bitmap anywhere. A type refuses an operation one of two ways: it leaves the slot NULL and generic code rejects the call (bpf_map_mmap() returns -ENOTSUPP when map->ops->map_mmap is NULL, which is why you cannot mmap() a hash map), or it installs a stub that returns an error — array_map_delete_elem() returns -EINVAL, and all four of the ring buffer’s key/value stubs return -ENOTSUPP. The vtable is the type system, and the errno you get tells you which of the two mechanisms said no.
The asymmetry that causes most bugs
There is one difference between the two callers that is worth internalizing early, because it is the most common source of confusion and of real data races.
From a BPF program, bpf_map_lookup_elem() returns a pointer directly into the live value in the map — no copy is made. The program reads and writes the value in place, which is why concurrent updates need atomic instructions (__sync_fetch_and_add()) or a bpf_spin_lock embedded in the value. The kernel array documentation states this outright: the helper “returns a pointer into the array element, so to avoid data races with userspace reading the value, the user must use primitives like __sync_fetch_and_add()” (map_array.rst).
From userspace, the BPF_MAP_LOOKUP_ELEM syscall copies the value out into a userspace buffer — it cannot hand a raw kernel pointer across the boundary. So the BPF side gets zero-copy in-place access; the userspace side always gets a snapshot that may be stale the instant it is returned. Keeping that distinction straight is what separates a working agent from one that races. (The one escape hatch is BPF_F_MMAPABLE on an array, which maps the value region into the process’s address space so userspace reads the live memory directly — at the cost of having to do its own atomics.)
Two Callers, Two Paths: the Lookup Walk-through
Because “the same vtable slot, reached two ways” is the whole model, it pays to trace both paths concretely for a single operation — a lookup on a BPF_MAP_TYPE_ARRAY and on a BPF_MAP_TYPE_HASH.
The userspace path. bpf(BPF_MAP_LOOKUP_ELEM, &attr, size) lands in map_lookup_elem(), which resolves attr->map_fd to a struct bpf_map *, checks that the fd was not opened write-only (map_get_sys_perms() against FMODE_CAN_READ), copies the key in from userspace with ___bpf_copy_key(), allocates a kernel bounce buffer of bpf_map_value_size(map) bytes, and then calls bpf_map_copy_value(). That function is where the interesting guards live:
/* kernel/bpf/syscall.c (v6.12), the tail of bpf_map_copy_value(), abridged */
bpf_disable_instrumentation();
rcu_read_lock();
ptr = map->ops->map_lookup_elem(map, key);
if (ptr) {
if (flags & BPF_F_LOCK)
/* lock 'ptr' and copy everything but lock */
copy_map_value_locked(map, value, ptr, true);
else
copy_map_value(map, value, ptr);
/* mask lock and timer, since value wasn't zero inited */
check_and_init_map_value(map, value);
}
rcu_read_unlock();
bpf_enable_instrumentation();Reading it symbol by symbol: bpf_disable_instrumentation() bumps a per-CPU counter that stops kprobes and tracepoints from firing inside this critical section — otherwise a BPF program attached to a function in the map code could re-enter the same bucket lock and deadlock. rcu_read_lock() is what makes the returned ptr safe to dereference at all: hash-map elements are freed via RCU (read-copy-update; see Read-Copy-Update Fundamentals), so holding the read-side lock guarantees a concurrent delete cannot free the element out from under the copy. copy_map_value() is not a memcpy — it consults map->record and skips over any embedded bpf_spin_lock, bpf_timer or bpf_wq field, because those are kernel-internal objects that must never be exported to userspace. BPF_F_LOCK takes the value’s embedded spin lock for a consistent multi-field read. Finally check_and_init_map_value() zeroes the skipped regions in the output buffer so no uninitialized kernel memory leaks out. Only after rcu_read_unlock() does the outer function copy_to_user() the bounce buffer.
The BPF-program path. A program’s bpf_map_lookup_elem(&map, &key) is BPF helper number 1 (see BPF Helper Functions). Nominally it becomes a BPF_CALL to bpf_map_lookup_elem(), which does map->ops->map_lookup_elem(map, key) — two indirect hops. That is far too expensive for a per-packet fast path, so the verifier performs an inlining rewrite using the map_gen_lookup vtable slot: at the end of verification, do_misc_fixups() asks the map type to emit replacement instructions, and the call disappears. For an array, array_map_gen_lookup() emits the whole lookup as straight-line code:
/* kernel/bpf/arraymap.c (v6.12) — emitted in place of the helper call */
*insn++ = BPF_ALU64_IMM(BPF_ADD, map_ptr, offsetof(struct bpf_array, value));
*insn++ = BPF_LDX_MEM(BPF_W, ret, index, 0); /* ret = *(u32 *)key */
if (!map->bypass_spec_v1) {
*insn++ = BPF_JMP_IMM(BPF_JGE, ret, map->max_entries, 4); /* OOB -> NULL */
*insn++ = BPF_ALU32_IMM(BPF_AND, ret, array->index_mask); /* Spectre mask */
} else {
*insn++ = BPF_JMP_IMM(BPF_JGE, ret, map->max_entries, 3);
}
if (is_power_of_2(elem_size))
*insn++ = BPF_ALU64_IMM(BPF_LSH, ret, ilog2(elem_size)); /* shift, not mul */
else
*insn++ = BPF_ALU64_IMM(BPF_MUL, ret, elem_size);
*insn++ = BPF_ALU64_REG(BPF_ADD, ret, map_ptr); /* ret = value + idx*size */
*insn++ = BPF_JMP_IMM(BPF_JA, 0, 0, 1);
*insn++ = BPF_MOV64_IMM(ret, 0); /* the NULL path */Seven or eight instructions, no call, no indirection — an array lookup compiles down to a bounds check, an optional Spectre mask, a shift and an add. That is why array maps are the right choice for the hottest counters. Note index_mask: when the loader is not privileged enough to bypass Spectre-v1 mitigation, array_map_alloc() rounds max_entries up to a power of two and stores index_mask = max_entries - 1, so that even a mis-speculated index past the bounds check wraps back inside the allocation instead of reading arbitrary kernel memory.
A hash map cannot be inlined that far — it still has to walk a bucket — but htab_map_gen_lookup() does the next best thing: it replaces the two-level indirect call with a direct call to __htab_map_lookup_elem() plus pointer arithmetic to skip past the element header to the value:
/* kernel/bpf/hashtab.c (v6.12) */
*insn++ = BPF_EMIT_CALL(__htab_map_lookup_elem);
*insn++ = BPF_JMP_IMM(BPF_JEQ, ret, 0, 1); /* NULL -> return NULL */
*insn++ = BPF_ALU64_IMM(BPF_ADD, ret,
offsetof(struct htab_elem, key) + round_up(map->key_size, 8));flowchart TD subgraph UP["Path A - userspace, via bpf() syscall"] U1["bpf(BPF_MAP_LOOKUP_ELEM)"] --> U2["resolve map_fd -> struct bpf_map*"] U2 --> U3["map_get_sys_perms: FMODE_CAN_READ?<br/>else -EPERM"] U3 --> U4["___bpf_copy_key: copy key IN<br/>kvmalloc bounce buffer"] U4 --> U5["bpf_disable_instrumentation()<br/>rcu_read_lock()"] U5 --> U6["map->ops->map_lookup_elem()"] U6 --> U7["copy_map_value: skip spin_lock,<br/>timer, wq fields"] U7 --> U8["check_and_init_map_value:<br/>zero the skipped bytes"] U8 --> U9["rcu_read_unlock()<br/>copy_to_user(value)"] U9 --> U10["SNAPSHOT - may be stale<br/>the instant it returns"] end subgraph BP["Path B - BPF program, via helper"] B1["bpf_map_lookup_elem(&map, &key)"] --> B2{"map type has<br/>map_gen_lookup?"} B2 -->|"ARRAY"| B3["verifier inlines:<br/>bounds check + index_mask<br/>+ shift + add<br/>NO CALL AT ALL"] B2 -->|"HASH"| B4["verifier emits direct call to<br/>__htab_map_lookup_elem<br/>+ offset add"] B2 -->|"other"| B5["indirect: bpf_map_lookup_elem<br/>-> ops->map_lookup_elem"] B3 --> B6["LIVE POINTER into the map value<br/>or NULL"] B4 --> B6 B5 --> B6 B6 --> B7["verifier forces a NULL check<br/>before any dereference"] B7 --> B8["read/write IN PLACE<br/>needs __sync_fetch_and_add<br/>or bpf_spin_lock"] end
The two lookup paths for one vtable slot. What it shows: the userspace side pays for permission checks, two copies, RCU protection and value sanitization, and ends with a snapshot; the BPF side is rewritten by the verifier at load time into either fully inline code (array) or a single direct call (hash), and ends with a live pointer. The insight to take: the performance gap between the two paths is not a constant factor, it is structural — one is a system call with copies, the other is a handful of arithmetic instructions. This is why the standard high-rate pattern is “aggregate in the kernel with a map, poll rarely from userspace,” and why polling a map in a tight loop from userspace is the classic way to make a BPF tool slower than the thing it is measuring. It is also why the verifier will not let you dereference a lookup result without a NULL check: on the BPF side there is no error return to hide behind.
Updates mirror this. From a program, bpf_map_update_elem(map, key, value, flags) takes BPF_ANY (insert or overwrite), BPF_NOEXIST (insert only if absent, else -EEXIST), or BPF_EXIST (update only if present, else -ENOENT). From userspace, BPF_MAP_UPDATE_ELEM copies the value in and calls the same slot, wrapped in the same instrumentation/RCU dance — plus, for a few types that must run in sleepable context (PROG_ARRAY, STRUCT_OPS, offloaded maps), a special case that skips the rcu_read_lock() entirely because those implementations may sleep.
The Type Catalog — Breadth Is the Point
The reason maps make eBPF expressive is the breadth of the type catalog: the type you pick encodes performance and semantics you would otherwise have to build by hand. As of Linux 6.12 and 6.18, enum bpf_map_type in include/uapi/linux/bpf.h ends at __MAX_BPF_MAP_TYPE = 34; excluding BPF_MAP_TYPE_UNSPEC (value 0, the “not a real type” sentinel) that is 33 distinct map-type values, the newest being BPF_MAP_TYPE_ARENA (value 33). Two of the 33 — BPF_MAP_TYPE_CGROUP_STORAGE and BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE — are now deprecated aliases (*_DEPRECATED in the enum), superseded by BPF_MAP_TYPE_CGRP_STORAGE plus local per-CPU kernel pointers, but they keep their slots for ABI stability. The enum is byte-identical between the v6.12 and v6.18 headers.
On counting map types
The headline number depends on the counting convention: the enum runs to
__MAX_BPF_MAP_TYPE = 34, so excludingUNSPECthere are 33 distinct values; secondary sources quote 32 or 34 depending on whether they exclude the two deprecated aliases or include__MAX. The authoritative number for your kernel is whatever itsinclude/uapi/linux/bpf.henum says, andbpftool feature probelists the types the running kernel actually supports (a type compiled out leaves aNULLslot inbpf_map_types[]and returns-EINVALon create).
mindmap root(("33 BPF<br/>map types")) ("Foundational stores") ("HASH") ("ARRAY") ("BLOOM_FILTER") ("Per-CPU") ("PERCPU_HASH") ("PERCPU_ARRAY") ("LRU_PERCPU_HASH") ("PERCPU_CGROUP_STORAGE (deprecated)") ("Self-managing / specialised lookup") ("LRU_HASH") ("LPM_TRIE") ("QUEUE") ("STACK") ("Event streaming") ("RINGBUF") ("USER_RINGBUF") ("PERF_EVENT_ARRAY (legacy)") ("STACK_TRACE") ("Object-lifetime storage") ("SK_STORAGE") ("INODE_STORAGE") ("TASK_STORAGE") ("CGRP_STORAGE") ("CGROUP_STORAGE (deprecated)") ("Redirect targets - networking") ("DEVMAP") ("DEVMAP_HASH") ("CPUMAP") ("XSKMAP") ("SOCKMAP") ("SOCKHASH") ("REUSEPORT_SOCKARRAY") ("CGROUP_ARRAY") ("Control flow / plumbing") ("PROG_ARRAY - tail calls") ("STRUCT_OPS - sched_ext, TCP CC") ("ARRAY_OF_MAPS") ("HASH_OF_MAPS") ("ARENA - shared address space")
The 33 map types grouped into seven families. What it shows: the catalog is not a flat list of data structures — most of it is plumbing for a specific kernel subsystem, and only the first three families are general-purpose containers. The insight to take: when you are choosing a map you are almost always choosing within the first four branches; the redirect and control-flow families are not alternatives you weigh, they are the mandatory interface to a particular hook (you use an XSKMAP because you are doing AF_XDP, not because you compared it to a hash map). This is also why “how many map types are there” is a slightly silly question: the number grows every time a subsystem gains a BPF interface.
The matrix
The table below is the practically load-bearing one: for each general-purpose type, what the key and value must look like, whether values are per-CPU, whether a sleepable program may touch it, and how BPF_F_NO_PREALLOC interacts. All four columns are read directly from v6.12 source — the sleepable column from the switch (map->map_type) under if (prog->sleepable) in check_map_prog_compatibility() in kernel/bpf/verifier.c, and the preallocation column from each type’s *_CREATE_FLAG_MASK and map_alloc_check.
| Map type | Key | Value | Per-CPU values | Sleepable-safe | BPF_F_NO_PREALLOC |
|---|---|---|---|---|---|
HASH | any size | any size | no | yes | optional — prealloc is the default |
PERCPU_HASH | any size | ≤ PCPU_MIN_UNIT_SIZE | yes | yes | optional |
LRU_HASH | any size | any size | no | yes | rejected — -ENOTSUPP |
LRU_PERCPU_HASH | any size | ≤ PCPU_MIN_UNIT_SIZE | yes | yes | rejected — -ENOTSUPP |
ARRAY | exactly 4 bytes | any size | no | yes | not accepted (always preallocated) |
PERCPU_ARRAY | exactly 4 bytes | ≤ PCPU_MIN_UNIT_SIZE | yes | yes | not accepted |
LPM_TRIE | 5–260 bytes ({u32 prefixlen; u8 data[1..256];}) | 1 byte – ~KMALLOC_MAX_SIZE | no | no | mandatory — -EINVAL without it |
QUEUE / STACK | must be 0 | ≤ KMALLOC_MAX_SIZE | no | yes | not accepted |
BLOOM_FILTER | must be 0 | any size | no | no | not accepted |
RINGBUF | must be 0 | must be 0 | no | yes | n/a |
USER_RINGBUF | must be 0 | must be 0 | no | yes | n/a |
SK_STORAGE | sizeof(int) (an fd) | any size | no | yes | mandatory |
INODE_STORAGE | sizeof(int) | any size | no | yes | mandatory |
TASK_STORAGE | sizeof(int) | any size | no | yes | mandatory |
CGRP_STORAGE | sizeof(int) | any size | no | yes | mandatory |
ARRAY_OF_MAPS | exactly 4 bytes | inner map fd | no | yes | n/a |
HASH_OF_MAPS | any size | inner map fd | no | yes | inherited from hash |
ARENA | n/a (raw address space) | n/a | no | yes | n/a |
PROG_ARRAY | exactly 4 bytes | program fd | no | no | n/a |
PERF_EVENT_ARRAY | exactly 4 bytes | perf event fd | per-CPU by construction | no | n/a |
STACK_TRACE | 4 bytes (stack id) | n * sizeof(u64) | no | no | n/a |
SOCKMAP / SOCKHASH | 4 bytes / any | socket fd | no | no | n/a |
DEVMAP / DEVMAP_HASH | 4 bytes / any | ifindex or bpf_devmap_val | no | no | n/a |
CPUMAP | exactly 4 bytes | bpf_cpumap_val | no | no | n/a |
XSKMAP | exactly 4 bytes | AF_XDP socket fd | no | no | n/a |
STRUCT_OPS | exactly 4 bytes | a kernel ops struct | no | no (rejected in programs entirely) | n/a |
The map-type matrix, v6.12. What it shows: the four properties that actually constrain a design decision, per type. PCPU_MIN_UNIT_SIZE is the kernel’s per-CPU allocator’s maximum single-allocation size (32 KiB on mainstream configurations), and it is the reason a per-CPU map cannot hold a large value. The insight to take: three of these columns are hard gates that will reject your program at load or create time, not performance hints. If your program is SEC("fentry.s/…") or an LSM hook marked sleepable, the verifier will reject it with Sleepable programs can only use array, hash, ringbuf and local storage maps the moment it touches, say, an LPM_TRIE or a STACK_TRACE — the fix is a redesign, not a flag. And BPF_F_NO_PREALLOC is not a uniform knob: it is optional on hash, forbidden on LRU, and mandatory on LPM-trie and every local-storage type.
The catalog falls into families, each owned by a sibling note:
- Foundational stores —
BPF_MAP_TYPE_HASH(a bucketed hash table) andBPF_MAP_TYPE_ARRAY(a fixed-size, index-keyed array). These two are the default tools and are covered in Hash and Array Maps. Array maps also underpin BPF global variables (.data/.bss/.rodatasections), which is how a BPF program appears to have ordinary globals at all.BPF_MAP_TYPE_BLOOM_FILTER(Linux 5.16) joins them as a probabilistic set: it has no keys at all, onlypush/peek, and takes its hash-function count from the low four bits ofmap_extra(defaulting to 5). - Per-CPU variants —
PERCPU_HASH,PERCPU_ARRAY,LRU_PERCPU_HASH: each CPU gets its own copy of the value, eliminating cross-core contention on hot counters. See Per-CPU Maps. - Self-managing and lookup-specialized —
LRU_HASH(evicts the least-recently-used entry when full) andLPM_TRIE(longest-prefix-match, the data structure behind IP routing-table lookups). See LRU and LPM-Trie Maps. - Event streaming —
RINGBUFandUSER_RINGBUF, the modern lossless channel for shipping events to (and from) userspace, replacing the older per-CPU perf buffer. See BPF Ring Buffer and the dedicated comparison below. - Object-lifetime storage — the
*_STORAGEfamily, whose values live attached to a kernel object (socket, inode, task, cgroup) rather than in the map. - Control-flow and subsystem-specific —
PROG_ARRAY(holds other BPF programs, the substrate for tail calls),SOCKMAP/SOCKHASH,DEVMAP/CPUMAP/XSKMAP(XDP redirect targets),ARRAY_OF_MAPS/HASH_OF_MAPS, andSTRUCT_OPS(the table behindsched_extand pluggable TCP congestion control). The notable ones are covered in Specialized Maps (prog-array, sockmap, cgroup-storage).
Preallocation, BPF_F_NO_PREALLOC, and the BPF Memory Allocator
The most consequential map-creation flag is BPF_F_NO_PREALLOC (bit 0), and understanding why it exists explains a lot about how the map layer works.
A hash map, unlike an array, does not know at creation time which keys will exist. The naive implementation would kmalloc() an element on each insert. But a BPF program can run in any context — inside a kprobe on a slab-allocator function, inside an NMI, with interrupts disabled — and calling the general allocator from there is either unsafe or a recursion hazard. The original answer was radical: allocate everything up front. By default a hash map calls prealloc_init(), which allocates one contiguous block of elem_size * max_entries bytes at create time, threads every element onto a lock-free per-CPU freelist (pcpu_freelist_populate()), and thereafter an insert is a freelist pop, not an allocation. Nothing in the update path can sleep or recurse.
Preallocation has two costs. The obvious one is memory: a hash map with max_entries = 1,000,000 and a 64-byte value consumes its full footprint the moment it is created, whether you insert one key or a million. The second is startup latency, because that whole block is zeroed on allocation. BPF_F_NO_PREALLOC says “allocate elements lazily instead” — and, before Linux 6.1, that meant kmalloc() on the update path with all the context hazards that implies.
Linux 6.1 changed this by adding kernel/bpf/memalloc.c — a BPF-specific memory allocator usable from any context, including NMI (the file first appears at tag v6.1; it is absent at v6.0). Its design comment states the problem plainly: “Tracing BPF programs can attach to kprobe and fentry. Hence they run in unknown context where calling plain kmalloc() might not be safe.” Jonathan Corbet’s LWN write-up of Alexei Starovoitov’s original patch set frames the same point from the memory-management side: most kernel code is written for a specific context and passes the matching GFP flags, but a function that “can be invoked in multiple contexts… generally must allocate memory as if it were always running in the most restrictive possible context” (Corbet, LWN, 30 June 2022) — and for a BPF program that most-restrictive context is NMI. The solution is a per-CPU, per-size-class cache of already-allocated objects, refilled asynchronously from irq_work:
/* kernel/bpf/memalloc.c (v6.12), from the header comment */
* CPU_0 buckets
* 16 32 64 96 128 196 256 512 1024 2048 4096
* ...
* CPU_N buckets
* 16 32 64 96 128 196 256 512 1024 2048 4096Eleven size classes (NUM_CACHES 11), capped at BPF_MEM_ALLOC_SIZE_MAX = 4096 bytes per object. Each per-CPU cache carries a low and a high watermark — 32 and 96 free objects for unit sizes up to 256 bytes, scaled down for larger units, and 1 and 3 for per-CPU allocations — and a batch size of three-quarters of the gap between them. When a program allocates and the free count drops below the low watermark, an irq_work is scheduled to refill from kmalloc() in a safe context; when it rises above the high watermark, the excess is trimmed with kfree(). Every object is padded with eight extra bytes for the struct llist_node that links it into the lock-free list. Freeing always goes to the current CPU’s bucket, and the irq_work trimming lets the global slab allocator sort out objects allocated on one CPU and freed on another.
flowchart TD CREATE["BPF_MAP_CREATE, hash map"] --> Q{"BPF_F_NO_PREALLOC<br/>set?"} Q -->|"no - the DEFAULT"| PRE["prealloc_init()"] PRE --> PRE1["bpf_map_area_alloc(<br/>elem_size * max_entries)<br/>one contiguous block, zeroed"] PRE1 --> PRE2{"LRU map?"} PRE2 -->|"yes"| LRU["bpf_lru_init + bpf_lru_populate<br/>elements owned by the LRU list"] PRE2 -->|"no"| FL["pcpu_freelist_init + populate<br/>elements on per-CPU freelists"] FL --> INS1["insert = __pcpu_freelist_pop()<br/>NO allocation, NO sleeping"] INS1 --> FULL1["freelist empty => -E2BIG"] Q -->|"yes"| NOP["bpf_mem_alloc_init(&htab->ma,<br/>elem_size, percpu)"] NOP --> NOP1["per-CPU caches, 11 size classes<br/>16..4096 bytes, prefilled"] NOP1 --> INS2["insert = bpf_mem_cache_alloc()<br/>pop from this CPU's cache"] INS2 --> WM{"free_cnt below<br/>low_watermark?"} WM -->|"yes"| IRQ["schedule irq_work:<br/>refill by batch via kmalloc<br/>in a SAFE context"] WM -->|"no"| OK["return object"] INS2 --> FULL2["is_map_full() => -E2BIG<br/>cache empty => -ENOMEM"] IRQ --> OK
How a hash-map insert gets its memory, v6.12. What it shows: the two allocation regimes and their distinct failure modes. The preallocated path can only ever fail with -E2BIG (the freelist is empty because the map is full); the lazy path can fail with -E2BIG or -ENOMEM (the per-CPU cache was empty and the refill had not landed yet). The insight to take: BPF_F_NO_PREALLOC is a memory-versus-determinism trade. Preallocation gives you a fixed, known footprint and an insert that cannot fail for allocator reasons — at the price of paying for max_entries you may never use. Lazy allocation gives you pay-as-you-go memory at the price of a rare, load-dependent -ENOMEM under burst, since the refill is asynchronous. For a sparsely-populated map with a large max_entries, lazy is almost always right; for a map that will fill up anyway, preallocation is strictly better.
Two corollaries follow from this design, and both show up as confusing errors:
- An LRU hash cannot use
BPF_F_NO_PREALLOC.htab_map_alloc_check()containsif (lru && !prealloc) return -ENOTSUPP;. The reason is structural: an LRU map’s whole contract is that it always has an element available to hand you, evicting the least-recently-used one if necessary. That only works if the element pool is fixed and fully materialized at creation, sobpf_lru_populate()can own every element from the start. - Local-storage and LPM-trie maps require it.
bpf_local_storage_map_alloc_check()rejects creation unlessBPF_F_NO_PREALLOCis set (along withmax_entries == 0andkey_size == sizeof(int)), andtrie_alloc()does the same. For local storage, preallocation is meaningless — values are allocated when a socket or task first asks for one, and there is nomax_entriesto preallocate against. For the LPM trie, nodes are variable-shaped and created during insertion.
There is a third, subtler consequence that bites in production. Because a preallocated hash map hands out elements from a shared pool, an update that replaces an existing key does not need a fresh element — but taking one from the freelist and putting the old one back is two lock-free operations. The kernel optimizes this with extra_elems, one spare element per CPU allocated by alloc_extra_elems(), used specifically for the replace case so the freelist is not touched at all. That is why htab_map_mem_usage() adds num_possible_cpus() extra entries to its accounting for non-LRU, non-per-CPU preallocated hash maps: on a 128-CPU machine, your “1000-entry” map really holds 1128 elements.
Memory Accounting: memcg, Not rlimit
Where does a map’s memory get charged? This has a history worth knowing, because it is the single most common source of “my BPF program used to load and now it doesn’t” between kernel versions.
Until Linux 5.11, BPF used the RLIMIT_MEMLOCK resource limit. Every map creation pre-charged an estimated page count against the calling user’s memlock limit, and exceeding it returned a bare -EPERM. Roman Gushchin’s cover letter for the replacement series enumerates why this was untenable: the limit is per-user, “but because most bpf operations are performed as root, the limit has a little value”; it is impossible to pick a sensible value because the counter is shared with genuine mlock() users, so “any specific value is either too low and creates false failures or too high and useless”; the charging is “not connected to the actual memory allocation,” requiring BPF code to hand-calculate an estimate and hand-uncharge on every error path, which “makes it easy to leak a charge”; there is no easy way to read the current value; and the resulting “cryptic -EPERM” was so confusing that “libbpf even had a function to ‘explain’ this case for users” (Gushchin, bpf: switch to memcg-based memory accounting, LWN, Nov 2020).
The replacement is memory-cgroup accounting. The advantages Gushchin lists are the mirror image of the complaints: the limit becomes “per-cgroup and hierarchical”; “the actual memory consumption is taken into account… automatically on the allocation time if __GFP_ACCOUNT flags is passed,” with uncharging equally automatic on free; and “there is a simple way to get the current value and statistics.” The charging rule he states is precise, and it is the part people get wrong: “if a process performs a bpf operation (e.g. creates or updates a map), its memory cgroup is charged. However map updates performed from an interrupt context are charged to the memory cgroup which contained the process which created the map.”
That last sentence is exactly what map->objcg implements. At create time, bpf_map_save_memcg() captures the creating process’s object cgroup:
/* kernel/bpf/syscall.c (v6.12) */
static void bpf_map_save_memcg(struct bpf_map *map)
{
/* Currently if a map is created by a process belonging to the root
* memory cgroup, get_obj_cgroup_from_current() will return NULL.
* So we have to check map->objcg for being NULL each time it's
* being used.
*/
if (memcg_bpf_enabled())
map->objcg = get_obj_cgroup_from_current();
}Later, every allocation made on behalf of the map — an element inserted from a kprobe on some unrelated CPU, say — is wrapped so that it lands in the right cgroup regardless of who happens to be running:
/* kernel/bpf/syscall.c (v6.12) */
void *bpf_map_kmalloc_node(const struct bpf_map *map, size_t size, gfp_t flags,
int node)
{
struct mem_cgroup *memcg, *old_memcg;
void *ptr;
memcg = bpf_map_get_memcg(map); /* map->objcg, or root_mem_cgroup */
old_memcg = set_active_memcg(memcg);
ptr = kmalloc_node(size, flags | __GFP_ACCOUNT, node);
set_active_memcg(old_memcg);
mem_cgroup_put(memcg);
return ptr;
}Walking it: set_active_memcg() temporarily overrides the “current” cgroup for accounting purposes on this CPU; __GFP_ACCOUNT tells the slab allocator to charge the allocation; and the old value is restored immediately after. There are four such wrappers in v6.12 — bpf_map_kmalloc_node(), bpf_map_kzalloc(), bpf_map_kvcalloc(), and bpf_map_alloc_percpu() — plus the same pattern inside bpf_mem_alloc, which stores its own objcg per cache. bpf_map_release_memcg() drops the reference in bpf_map_free_deferred().
sequenceDiagram autonumber participant P as Process in cgroup /sys/fs/cgroup/app participant S as bpf() syscall participant M as struct bpf_map participant K as kprobe BPF prog<br/>(arbitrary CPU, IRQ context) participant MC as memcg accounting P->>S: BPF_MAP_CREATE S->>MC: get_obj_cgroup_from_current() MC-->>S: objcg for /app S->>M: map->objcg = objcg Note over M: the charge target is now<br/>pinned to the CREATOR's cgroup S->>MC: bpf_map_area_alloc() with<br/>bpf_memcg_flags(__GFP_ACCOUNT) MC-->>M: buckets, prealloc block charged to /app Note over K: much later, unrelated process<br/>in cgroup /other triggers the kprobe K->>M: bpf_map_update_elem() - needs a new element M->>MC: bpf_map_kmalloc_node(map, ...) MC->>MC: set_active_memcg(map->objcg) MC->>MC: kmalloc_node(..., __GFP_ACCOUNT) MC->>MC: set_active_memcg(old) Note over MC: charged to /app, NOT /other
How a map’s memory is charged, v6.12. What it shows: the charge target is captured once, at BPF_MAP_CREATE, and every later allocation — including ones made from an interrupt on behalf of a completely different process — is redirected to it with a set_active_memcg() window. The insight to take: a BPF map’s memory belongs to whoever created it, forever, not to whoever triggers the event that grows it. That is the only sane answer (the triggering process is arbitrary and may be in the root cgroup), but it has two practical consequences: a monitoring agent that creates maps for a whole machine will see all of that memory in its own cgroup and can be OOM-killed for it; and if a map is created by a process in the root cgroup, get_obj_cgroup_from_current() returns NULL and the accounting silently falls back to root_mem_cgroup — i.e. effectively unlimited. Run your BPF agent in a real cgroup with a real limit, or you get no accounting at all. See The Memory Cgroup memcg for the charging machinery itself.
Since Linux 6.4 there is also a per-map self-report: every type must implement map_mem_usage(), and bpftool map show prints the result. The hash implementation is instructive because it shows exactly what you are paying for:
/* kernel/bpf/hashtab.c (v6.12), htab_map_mem_usage(), abridged */
u64 usage = sizeof(struct bpf_htab);
usage += sizeof(struct bucket) * htab->n_buckets;
usage += sizeof(int) * num_possible_cpus() * HASHTAB_MAP_LOCK_COUNT;
if (prealloc) {
num_entries = map->max_entries;
if (htab_has_extra_elems(htab))
num_entries += num_possible_cpus();
usage += htab->elem_size * num_entries;
if (percpu)
usage += value_size * num_possible_cpus() * num_entries;
...
} else {
num_entries = /* live element count */;
usage += (htab->elem_size + LLIST_NODE_SZ) * num_entries;
...
}Three things to read out of that. First, the bucket array is charged in full regardless of occupancy, and n_buckets = roundup_pow_of_two(max_entries) — so max_entries = 1025 silently allocates 2048 buckets. Second, a per-CPU map multiplies the value size by num_possible_cpus(), which on a large machine is a factor of 128 or 256, and num_possible_cpus() counts possible, not online, CPUs — a VM configured for hotplug can report far more than it has. Third, the non-preallocated branch adds LLIST_NODE_SZ (8 bytes) per element, the padding bpf_mem_alloc attaches to every object.
Concurrency: RCU, Bucket Locks, and the Lifetime of a Map Value
A BPF program can be invoked on every CPU at once, from any context, while userspace is simultaneously hammering the same map through the syscall. The map layer’s concurrency story has three layers, and confusing them is the source of most real BPF bugs.
Layer 1 — RCU protects the element, not its contents. Hash-map elements are freed under read-copy-update. A lookup runs inside rcu_read_lock(); a delete unlinks the element from its bucket and defers the actual free until a grace period has elapsed, so any reader that already obtained the pointer can finish safely. This is why bpf_map_lookup_elem() can hand a raw pointer to a BPF program at all. It is also why the guarantee is narrower than people assume: RCU guarantees the memory will not be freed while you hold it; it guarantees nothing about another CPU concurrently overwriting the value in place. Two programs that both lookup and then *val += 1 will lose updates, RCU or no RCU. See Read-Copy-Update Fundamentals and RCU Read-Side Critical Sections.
Layer 2 — a per-bucket raw spinlock serializes writers. Updates and deletes take htab_lock_bucket(). The v6.12 implementation is unusually defensive, and the source comment explains why: the bucket lock has “two protection scopes: 1) Serializing concurrent operations from BPF programs on different CPUs, 2) Serializing concurrent operations from BPF programs and sys_bpf().” Because “BPF programs can execute in any context including perf, kprobes and tracing,” the lock “needs to be protected against deadlocks… caused by recursion and by an invocation in the lock held section.”
/* kernel/bpf/hashtab.c (v6.12) */
static inline int htab_lock_bucket(const struct bpf_htab *htab,
struct bucket *b, u32 hash,
unsigned long *pflags)
{
unsigned long flags;
hash = hash & min_t(u32, HASHTAB_MAP_LOCK_MASK, htab->n_buckets - 1);
preempt_disable();
local_irq_save(flags);
if (unlikely(__this_cpu_inc_return(*(htab->map_locked[hash])) != 1)) {
__this_cpu_dec(*(htab->map_locked[hash]));
local_irq_restore(flags);
preempt_enable();
return -EBUSY; /* recursion detected: bail out */
}
raw_spin_lock(&b->raw_lock);
*pflags = flags;
return 0;
}Read it carefully, because the -EBUSY return is a real, observable failure mode. htab->map_locked[] is an array of HASHTAB_MAP_LOCK_COUNT = 8 per-CPU counters; the bucket hash is folded down to one of those eight. preempt_disable() plus local_irq_save() pins us to this CPU with interrupts off. Then __this_cpu_inc_return() — if the counter was already non-zero, this CPU is already inside a bucket lock in the same group, which means we got here by recursion (a kprobe fired on a function called from inside the map update path, and its BPF program is now trying to update the same map). Rather than deadlock, the kernel refuses the operation and returns -EBUSY. A bpf_map_update_elem() in a tracing program can therefore fail for reasons that have nothing to do with the map being full, and this is why probing deep into the allocator or the hash-map code itself produces mysteriously dropped updates. The lock is a raw_spinlock_t, not a spinlock_t, so it stays a true spinlock even on PREEMPT_RT — see Raw Spinlocks and PREEMPT_RT.
Layer 3 — you serialize your own value contents. The map layer will not do it for you. Three options, in increasing order of cost:
- Atomics.
__sync_fetch_and_add(&val->count, 1)compiles to a BPF atomic add instruction (BPF_ATOMIC), which the JIT turns into a nativelock xaddorldadd. Correct for a single scalar, free of locking. - A per-CPU map. Each CPU writes its own copy with no synchronization at all; userspace sums across CPUs on read. This is the standard answer for high-rate counters and it is why per-CPU types exist.
struct bpf_spin_lockembedded in the value. Declare astruct bpf_spin_lock lock;field inside the value struct; BTF records its offset inmap->record; the program brackets its critical section withbpf_spin_lock()/bpf_spin_unlock(). This is the only option that gives you a consistent multi-field update. It comes with real restrictions the verifier enforces incheck_map_prog_compatibility(): a socket-filter program may not usebpf_spin_lock, and neither may a tracing program (tracing progs cannot use bpf_spin_lock yet). The same section bansbpf_timer,bpf_wq, and BPF linked lists / red-black trees from tracing programs.
sequenceDiagram autonumber participant A as CPU 0: BPF prog (XDP) participant B as CPU 1: BPF prog (kprobe) participant H as Hash map bucket participant U as Userspace syscall participant R as RCU A->>H: lookup(key) under rcu_read_lock H-->>A: live pointer to value B->>H: htab_lock_bucket(hash) Note over B,H: per-CPU map_locked counter 0 -> 1,<br/>then raw_spin_lock B->>H: delete(key): hlist_nulls_del_rcu B->>R: free_htab_elem -> element leaves the bucket B->>H: htab_unlock_bucket Note over A: A's pointer is STILL VALID -<br/>RCU has not passed a grace period A->>A: __sync_fetch_and_add(&val->n, 1) Note over A,R: write lands in an element that is<br/>already unlinked - it is safe, but LOST A->>R: rcu_read_unlock R->>R: grace period elapses R->>H: element actually freed / returned to cache U->>H: BPF_MAP_LOOKUP_ELEM(key) H-->>U: -ENOENT
A realistic three-way race on one hash-map bucket. What it shows: RCU keeps CPU 0’s pointer safe to dereference right through a concurrent delete on CPU 1, but nothing keeps its increment meaningful — the element it updates is already unlinked and will be recycled. The insight to take: “the verifier accepted it and it doesn’t crash” is a much weaker statement than “it is correct.” RCU is a memory-lifetime mechanism, not a mutual-exclusion mechanism. If a lost update matters to you, the answer is a per-CPU map (no sharing to lose), an atomic (the read-modify-write is indivisible), or a bpf_spin_lock (you hold the element across the whole operation) — and if entries can be deleted underneath you, an LRU or a delete-free design.
One more subtlety, visible in free_htab_elem(): what “free” means depends on preallocation, and in neither case is it “wait for a grace period, then free.”
- A preallocated map pushes the element straight back onto the per-CPU freelist (
__pcpu_freelist_push()). It is immediately available to the next insert on this CPU. - A non-preallocated map calls
htab_elem_free()→bpf_mem_cache_free()→unit_free(), which pushes the object onto the current CPU’sfree_llist— again immediately reusable by the next allocation of the same size class.
The grace period exists one level down: only when free_bulk() trims a cache above its high watermark do objects move to free_by_rcu_ttrace and get released to kfree() after an RCU-tasks-trace grace period (do_call_rcu_ttrace() → __free_rcu_tasks_trace() → __free_rcu()). So the guarantee is type-stable memory, in the spirit of SLAB_TYPESAFE_BY_RCU: the address will remain a valid object of the same size for as long as you hold an RCU read lock, but its contents may have been recycled into a different key’s element. That is much weaker than “the value you read is the value you looked up,” and it is the deep reason the verifier is strict about map-value pointer lifetimes — and why holding a bpf_map_lookup_elem() result across a helper call that might delete is a mistake even though the compiler and the verifier will both let you.
Per-CPU Maps: What the Memory Actually Looks Like
Per-CPU map types deserve a concrete picture, because the “one value per CPU” idea has a surprising consequence at the syscall boundary that trips up almost everyone the first time.
For a BPF_MAP_TYPE_PERCPU_ARRAY, bpf_array_alloc_percpu() makes one per-CPU allocation of round_up(value_size, 8) bytes for every array index, storing the resulting void __percpu * in array->pptrs[i]. A BPF program’s bpf_map_lookup_elem() resolves to this_cpu_ptr(array->pptrs[index]) — the current CPU’s copy, reached with no locking, no atomics, and no cache-line sharing with any other core. That is the entire performance argument for per-CPU maps: a counter increment becomes a plain load-add-store on a cache line only this CPU touches.
BPF_MAP_TYPE_PERCPU_ARRAY, max_entries = 3, value_size = 12
elem_size = round_up(12, 8) = 16 bytes
struct bpf_array
+----------------------------------+
| map (struct bpf_map) |
| elem_size = 16 |
| index_mask |
| pptrs[0] ---------------------------+
| pptrs[1] ------------------------+ |
| pptrs[2] ---------------------+ | |
+------------------------------ |--|--|-----+
| | |
per-CPU areas | | |
(one chunk per CPU, allocated by __alloc_percpu_gfp)
| | |
CPU 0 area v v v
+----------------+ +----------------+ +----------------+
| idx2: 16 bytes | | idx1: 16 bytes | | idx0: 16 bytes |
+----------------+ +----------------+ +----------------+
CPU 1 area
+----------------+ +----------------+ +----------------+
| idx2: 16 bytes | | idx1: 16 bytes | | idx0: 16 bytes |
+----------------+ +----------------+ +----------------+
... (x num_possible_cpus)
BPF program on CPU 1: bpf_map_lookup_elem(&m, &idx0)
-> this_cpu_ptr(pptrs[0]) -> CPU 1's idx0 chunk only
Userspace: BPF_MAP_LOOKUP_ELEM with key = idx0 fills a buffer of
bpf_map_value_size(map) = round_up(12,8) * num_possible_cpus()
= 16 * N bytes:
buffer: [ CPU0 idx0 |16B| ][ CPU1 idx0 |16B| ][ CPU2 ... ] ... [ CPU N-1 ]
^ you must sum these yourself
Per-CPU array memory layout and the syscall-side value size. Drawn as an ASCII box diagram rather than mermaid packet-beta because the structure is a two-dimensional pointer indirection (index × CPU) rather than a bit-packed wire format, which packet-beta cannot express. What it shows: the value is not stored in the array; the array stores a per-CPU pointer per index, and the actual bytes live in each CPU’s per-CPU area. The insight to take: the value size a BPF program sees (value_size, here 12 bytes) and the value size userspace must allocate for (round_up(value_size, 8) * num_possible_cpus()) are different numbers. Passing a value_size-sized buffer to BPF_MAP_LOOKUP_ELEM on a per-CPU map is a buffer overflow waiting to happen, which is why libbpf provides libbpf_num_possible_cpus() and why every per-CPU-map example allocates sizeof(value) * nr_cpus. Note it is possible, not online, CPUs — a hotplug-capable VM inflates this.
Three details follow from the layout:
round_up(value_size, 8)is mandatory padding, not an optimization. The per-CPU allocator requires 8-byte alignment, so a 12-byte value occupies 16 bytes per CPU. Sizing a per-CPU map byvalue_size * nr_cpusunder-counts.PCPU_MIN_UNIT_SIZEcaps the value. Bothhtab_map_alloc_check()andarray_map_alloc_check()rejectround_up(attr->value_size, 8) > PCPU_MIN_UNIT_SIZEwith-E2BIG. That constant isPFN_ALIGN(32 << 10)— 32 KiB — ininclude/linux/percpu.h. A per-CPU map cannot hold a large struct.- A program can read another CPU’s copy, but only via
bpf_map_lookup_percpu_elem(map, key, cpu)(Linux 5.20/6.0 onward), which routes to themap_lookup_percpu_elemvtable slot and usesper_cpu_ptr()instead ofthis_cpu_ptr(). It is bounds-checked againstnr_cpu_idsand returnsNULLfor an invalid CPU. This is how an in-kernel aggregation can sum across CPUs without a syscall — see In-Kernel Aggregation with BPF Maps.
A per-CPU hash map is the same idea with one more indirection: struct htab_elem stores a void *ptr_to_pptr rather than the value inline, so htab->elem_size is sizeof(struct htab_elem) + round_up(key_size, 8) + sizeof(void *), and the per-CPU chunks are allocated separately (from htab->pcpu_ma, a second bpf_mem_alloc instance, when the map is not preallocated).
Lifetime: References, Freezing, and Pinning
A map is not owned by the program that uses it, nor even by the process that created it. It is a refcounted kernel object, and the rules for who keeps it alive — and what happens the instant the last holder lets go — produce more production surprises than any other part of the model: a map that vanishes when the loader exits, a map that outlives the agent that made it and quietly holds a gigabyte of kernel memory nobody can account for, or a BPF_MAP_FREEZE that returns -EBUSY for reasons that have nothing to do with contention.
Two counters, not one. struct bpf_map carries atomic64_t refcnt and atomic64_t usercnt, and they answer different questions. refcnt answers “may this object be freed?” — it counts every holder of any kind. usercnt answers “does any userspace handle still exist?” — it counts only the holders that userspace can act through. Both start at 1 when map_create() returns an fd. The manipulators are a matched pair in kernel/bpf/syscall.c: bpf_map_inc() takes a reference on refcnt alone, while bpf_map_inc_with_uref() takes both; symmetrically bpf_map_put() drops refcnt and bpf_map_put_with_uref() drops both.
| Holder | refcnt | usercnt | Taken by |
|---|---|---|---|
| An open map file descriptor | +1 | +1 | BPF_MAP_CREATE, BPF_OBJ_GET, fd duplication, SCM_RIGHTS passing |
A bpffs pin at /sys/fs/bpf/… | +1 | +1 | BPF_OBJ_PIN → bpf_any_get() → bpf_map_inc_with_uref() in kernel/bpf/inode.c |
BPF_MAP_GET_FD_BY_ID | +1 | +1 | __bpf_map_inc_not_zero(map, true) — and it requires CAP_SYS_ADMIN, not merely CAP_BPF |
A loaded program that references the map (prog->aux->used_maps[]) | +1 | — | bpf_map_inc() at verification time |
An inner-map slot in an ARRAY_OF_MAPS / HASH_OF_MAPS | +1 | — | bpf_map_fd_get_ptr() in kernel/bpf/map_in_map.c |
Who holds a reference to a map, v6.12. What it shows: the two right-hand columns are what make the split meaningful — a loaded program and an inner-map slot hold the object alive without being a “user”. The insight to take: this table explains the two classic lifetime outcomes. A loader that creates a map, loads a program against it, and then exits without pinning drops both counters by one, but the program’s used_maps reference keeps refcnt at 1, so the map survives exactly as long as the program does — invisible to userspace, unreachable except by ID. Conversely a pin holds both counters, so a pinned map survives everything, including the death of every process that ever touched it, until the bpffs entry is unlinked. That asymmetry is why “my map leaked” and “my map disappeared” are both routine complaints about the same mechanism.
Dropping the last user. When usercnt reaches zero, bpf_map_put_uref() fires the map_release_uref vtable slot if the type installed one:
/* kernel/bpf/syscall.c (v6.12) */
static void bpf_map_put_uref(struct bpf_map *map)
{
if (atomic64_dec_and_test(&map->usercnt)) {
if (map->ops->map_release_uref)
map->ops->map_release_uref(map);
}
}This is the hook described earlier in the field list, and it exists because some things a map holds must be torn down the moment no human-facing handle remains, even though the object itself is still alive for running programs. A hash or array map cancels every bpf_timer and bpf_wq embedded in its values; a PROG_ARRAY clears its program slots so tail-call targets can be freed; a sockmap detaches its programs. Without it, a timer armed from a map value would keep firing forever after the last file descriptor closed.
Dropping the last reference. When refcnt reaches zero, bpf_map_put() runs, and it is worth reading in full because the three-way branch at its heart is the “heavier free path” that sleepable_refcnt selects:
/* kernel/bpf/syscall.c (v6.12) */
void bpf_map_put(struct bpf_map *map)
{
if (atomic64_dec_and_test(&map->refcnt)) {
/* bpf_map_free_id() must be called first */
bpf_map_free_id(map);
WARN_ON_ONCE(atomic64_read(&map->sleepable_refcnt));
if (READ_ONCE(map->free_after_mult_rcu_gp))
call_rcu_tasks_trace(&map->rcu, bpf_map_free_mult_rcu_gp);
else if (READ_ONCE(map->free_after_rcu_gp))
call_rcu(&map->rcu, bpf_map_free_rcu_gp);
else
bpf_map_free_in_work(map);
}
}Step by step. bpf_map_free_id() removes the map from the global map_idr first, so no concurrent BPF_MAP_GET_FD_BY_ID can resurrect an object already committed to death — __bpf_map_inc_not_zero() is the matching guard on the lookup side, and it is why that path uses an inc_not_zero at all. Then one of three disposals is chosen:
- The common case,
bpf_map_free_in_work(). The map is handed to a workqueue:INIT_WORK(&map->work, bpf_map_free_deferred); queue_work(system_unbound_wq, &map->work). The reason is stated in the comment abovebpf_map_put()— “underlying map implementationops->map_free()might sleep” — so freeing can never happen inline from whatever context dropped the last reference (which may be an interrupt, or a BPF program’s context). Thesystem_unbound_wqchoice carries its own comment: “Avoid spawning kworkers, since they all might contend for the same mutex likeslab_mutex.” The deferred worker then runssecurity_bpf_map_free()(the LSM hook),bpf_map_release_memcg()(dropping theobjcgreference), andbpf_map_free(), which callsops->map_free()and only afterwards frees thebtf_recordand drops thebtfreference — deliberately in that order, because the type’s own free callback usually needs both. free_after_rcu_gp— one RCU grace period first. The map is freed viacall_rcu(), whose callback re-enters the workqueue path.free_after_mult_rcu_gp— an RCU-tasks-trace grace period and an ordinary RCU grace period.call_rcu_tasks_trace()runsbpf_map_free_mult_rcu_gp(), which chains a secondcall_rcu()unlessrcu_trace_implies_rcu_gp()says the stronger grace period already subsumed the weaker one.
The two free_after_* flags are set in exactly one place, and knowing where demystifies sleepable_refcnt completely:
/* kernel/bpf/map_in_map.c (v6.12) */
void bpf_map_fd_put_ptr(struct bpf_map *map, void *ptr, bool need_defer)
{
struct bpf_map *inner_map = ptr;
/* Defer the freeing of inner map according to the sleepable attribute
* of bpf program which owns the outer map, so unnecessary waiting for
* RCU tasks trace grace period can be avoided.
*/
if (need_defer) {
if (atomic64_read(&map->sleepable_refcnt))
WRITE_ONCE(inner_map->free_after_mult_rcu_gp, true);
else
WRITE_ONCE(inner_map->free_after_rcu_gp, true);
}
bpf_map_put(inner_map);
}When an inner map is removed or replaced in a map-of-maps, a BPF program on some other CPU may be right now holding the pointer it got from looking that slot up. It must not be freed until every such program has finished. If the outer map is used only by ordinary (non-sleepable) programs, those run inside a classic RCU read-side critical section, so one call_rcu() grace period is sufficient. But a sleepable program — an fentry.s/…, an LSM hook, a uprobe.s/… — can block, so it is protected by RCU-tasks-trace instead, which waits much longer. map->sleepable_refcnt is the outer map’s count of loaded sleepable programs referencing it: incremented in the verifier at program load (in add_used_map_from_fd(), which also enforces the MAX_USED_MAPS cap, and again in the BPF_PROG_BIND_MAP syscall command), decremented in __bpf_free_used_maps() in kernel/bpf/core.c. If it is zero, the kernel takes the cheap path; if not, it pays for the expensive one. This is a pure optimization for the common case, and it is why the field exists in the base struct at all.
flowchart TD START["last reference dropped:<br/>bpf_map_put -> refcnt hits 0"] --> ID["bpf_map_free_id()<br/>remove from map_idr FIRST<br/>so GET_FD_BY_ID cannot resurrect it"] ID --> Q1{"free_after_mult_rcu_gp?<br/>inner map of an outer map<br/>used by a SLEEPABLE program"} Q1 -->|"yes"| RT["call_rcu_tasks_trace()<br/>wait for sleepable progs"] RT --> Q2{"rcu_trace_implies_rcu_gp()?"} Q2 -->|"no"| RC1["chain call_rcu()<br/>wait for classic readers too"] Q2 -->|"yes"| WQ RC1 --> WQ Q1 -->|"no"| Q3{"free_after_rcu_gp?<br/>inner map, non-sleepable outer"} Q3 -->|"yes"| RC2["call_rcu()<br/>one classic grace period"] RC2 --> WQ Q3 -->|"no - the COMMON case"| WQ["bpf_map_free_in_work()<br/>queue_work(system_unbound_wq)"] WQ --> DEF["bpf_map_free_deferred() in process context:<br/>1. security_bpf_map_free() - LSM<br/>2. bpf_map_release_memcg() - drop objcg<br/>3. bpf_map_free() -> ops->map_free()<br/>4. btf_record_free(), btf_put()"] DEF --> GONE["memory returned;<br/>memcg charge released"]
The map free path in v6.12. What it shows: three entry points that all converge on the same deferred worker, and the reason for the deferral — ops->map_free() may sleep, so it can never run in the context that happened to drop the last reference. The insight to take: freeing a map is asynchronous and can be arbitrarily delayed. close(fd) does not release the memory; it schedules the release. A test that closes a map fd and immediately checks memory.current in the creating cgroup will see the old value, and a monitoring agent that recreates a large map in a restart loop can transiently hold two copies. If you need the memory back deterministically before doing something else, the only reliable signal is that the map’s ID has disappeared from bpftool map show — and even that only proves bpf_map_free_id() ran, not that the worker has finished.
Freezing: BPF_MAP_FREEZE and why the verifier cares
BPF_MAP_FREEZE makes a map’s contents permanently immutable from the syscall side. It is a one-way door — there is no unfreeze — and it exists for a single, very specific reason that is easy to miss: it is what turns a BPF program’s const global variables into actual compile-time constants inside the verifier.
/* kernel/bpf/syscall.c (v6.12), map_freeze(), abridged */
if (map->map_type == BPF_MAP_TYPE_STRUCT_OPS || !IS_ERR_OR_NULL(map->record))
return -ENOTSUPP;
if (!(map_get_sys_perms(map, f) & FMODE_CAN_WRITE))
return -EPERM;
mutex_lock(&map->freeze_mutex);
if (bpf_map_write_active(map)) {
err = -EBUSY;
goto err_put;
}
if (READ_ONCE(map->frozen)) {
err = -EBUSY;
goto err_put;
}
WRITE_ONCE(map->frozen, true);Four gates, four distinct errnos. A STRUCT_OPS map, or any map whose values contain special fields (map->record is non-empty — a spin lock, a timer, a kptr, a list head), cannot be frozen at all: -ENOTSUPP. The fd must be writable: -EPERM. And -EBUSY covers two cases that look identical from userspace but are not — the map is already frozen, or bpf_map_write_active(map) is true, meaning map->writecnt is non-zero.
That second -EBUSY is the one that bites. writecnt is incremented for the duration of any in-flight BPF_MAP_UPDATE_ELEM or BPF_MAP_DELETE_ELEM syscall, and — critically — for as long as a writable mmap() of the map exists, via bpf_map_mmap_open() / bpf_map_mmap_close(). So a hand-rolled loader that maps its .rodata array writable to populate it, and then tries to freeze without unmapping, gets -EBUSY from the freeze it needs in order to load the program.
tools/lib/bpf/libbpf.c sidesteps that trap entirely, and its sequence is worth knowing because it is the one you are actually running. At object-open time, bpf_object__init_internal_map() builds each global-data section into a BPF_MAP_TYPE_ARRAY with key_size = sizeof(int), value_size = data_sz, max_entries = 1, BPF_F_RDONLY_PROG for .rodata and .kconfig, and BPF_F_MMAPABLE only if map_is_mmapable() finds at least one non-static variable in the section. It then allocates the staging buffer as an anonymous mapping — mmap(NULL, mmap_sz, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0) — which is not backed by the map at all, so your writes to skel->rodata->cfg before load() never touch writecnt. At load, bpf_object__populate_internal_map() pushes the whole section in with a single bpf_map_update_elem(map->fd, &zero, map->mmaped, 0) and then, for .rodata/.kconfig only, calls bpf_map_freeze(map->fd). Finally the skeleton re-mmap()s the map fd over the same address with MAP_FIXED — PROT_READ if BPF_F_RDONLY_PROG, PROT_READ | PROT_WRITE otherwise — so that from userspace the pointer and its contents are unchanged while the pages underneath become the kernel’s. The comment in the source calls this “remap anonymous mmap()-ed ‘map initialization image’ as a BPF map-backed mmap()-ed memory, but preserving the same memory address.” The freeze therefore happens at the one moment when no mapping of the map exists, which is why you never see -EBUSY from libbpf and always see it from a loader that improvises.
The payoff is in the verifier. bpf_map_is_rdonly() in kernel/bpf/verifier.c states its three conditions in a comment: the map was created with BPF_F_RDONLY_PROG (so the program side can never write it), it has been frozen (so the syscall side can never write it), and no write is currently in flight. Only when all three hold does the verifier treat a load from the map as a known constant:
/* kernel/bpf/verifier.c (v6.12) */
static bool bpf_map_is_rdonly(const struct bpf_map *map)
{
return (map->map_flags & BPF_F_RDONLY_PROG) &&
READ_ONCE(map->frozen) &&
!bpf_map_write_active(map);
}and in check_mem_access(), a constant-offset read from such a map is folded via bpf_map_direct_read() into __mark_reg_known(®s[value_regno], val) — the register becomes a scalar with a known value, not an unknown. That single fact cascades: a branch on that scalar is now statically decidable, so the verifier prunes the dead arm instead of exploring it. This is the mechanism behind libbpf’s const volatile configuration idiom, and it is why a feature flag written into .rodata before load costs nothing at run time — the disabled half of the program is never verified and never JITed. It also underpins bpf_map_is_rdonly()’s other user, the constant-string check check_reg_const_str() used by bpf_snprintf-family helpers, which refuses a format string that does not live in a frozen read-only map (R%d does not point to a readonly map).
There is a matching guard on the mmap side. bpf_map_mmap() refuses a writable mapping of a frozen map with -EPERM, and refuses a writable mapping of a BPF_F_RDONLY_PROG map with -EACCES — the comment explains that otherwise “it’s possible to leak a writable page reference and allows user-space to still modify it after freezing, while verifier will assume contents do not change.” Note also that map_get_sys_perms() strips FMODE_CAN_WRITE from any frozen map’s fd, so every subsequent BPF_MAP_UPDATE_ELEM on it returns -EPERM regardless of how the fd was opened.
stateDiagram-v2 [*] --> Live: BPF_MAP_CREATE<br/>refcnt 1, usercnt 1<br/>id allocated in map_idr Live --> Live: update, delete, lookup<br/>writecnt raised during each write Live --> MappedRW: mmap PROT_WRITE MAP_SHARED<br/>writecnt raised for the mapping lifetime MappedRW --> Live: munmap<br/>writecnt decremented MappedRW --> MappedRW: freeze here returns -EBUSY Live --> Frozen: BPF_MAP_FREEZE<br/>only if writecnt is 0, not already frozen,<br/>and the value has no lock, timer or kptr Frozen --> Frozen: syscall writes now -EPERM<br/>writable mmap now -EPERM<br/>second freeze -EBUSY Live --> Pinned: BPF_OBJ_PIN<br/>refcnt and usercnt both raised Pinned --> Live: unlink the bpffs path<br/>refcnt and usercnt released Frozen --> VerifierConst: BPF_F_RDONLY_PROG and frozen<br/>and writecnt is 0 VerifierConst --> VerifierConst: loads folded to known scalars;<br/>dead branches pruned at verification Live --> Dying: last refcnt dropped Frozen --> Dying: last refcnt dropped Dying --> [*]: id freed, then deferred<br/>bpf_map_free_deferred()
The lifecycle of a map, v6.12. What it shows: freezing is a state, not an operation on the data, and it interacts with three other things — the write counter, memory mappings, and the verifier’s constant folding. The insight to take: the Frozen and VerifierConst states are not the same. Freezing alone only stops writes; it is the conjunction of BPF_F_RDONLY_PROG at creation and frozen afterwards that buys you constant folding. Setting one without the other is a very common mistake and silently costs you the optimization, with no error anywhere — the program just verifies as if the values were unknown, and both branches of every configuration test survive into the JITed code.
Pinning is the third lifetime mechanism, and it is the one that decouples a map from any process. BPF_OBJ_PIN creates an entry in the BPF filesystem (bpffs, typically mounted at /sys/fs/bpf) that holds a full reference — refcnt and usercnt — so the map survives the death of its creator, and BPF_OBJ_GET on that path yields a fresh fd to any process with permission to open it. Cilium’s reference guide gives the canonical motivation: file descriptors “are limited to a processes’ lifetime, which makes options like map sharing rather cumbersome,” which “brings a number of complications for certain use cases such as iproute2, where tc or XDP sets up and loads the program into the kernel and terminates itself eventually” (Cilium BPF and XDP Reference Guide). The full mechanics — mount options, path semantics, bpftool pin, and the ownership problems pins create on upgrade — belong to Map Pinning and bpffs and BPF Object Pinning and Lifetime; what matters here is only that a pin is a reference, and an unreferenced pin is a leak that no process is holding and no ps will show you.
Failure Modes and Gotchas
Map bugs cluster into a small number of recurring shapes. Almost all of them come from one of four mismatches: an errno that means something other than what it says, a size that userspace computes differently from the kernel, a lifetime assumption that RCU does not actually give you, or a capacity that is not the number you asked for. This section is organized by the symptom you will actually see.
The errno decoder
The single most useful thing to internalize is that the map layer reuses a handful of errnos across completely different causes, and the operation disambiguates them.
| Errno | On BPF_MAP_CREATE | On update / delete | On lookup | On freeze / mmap |
|---|---|---|---|---|
-EINVAL | unknown map_type, type compiled out (NULL slot in bpf_map_types[]), bad key_size/value_size/max_entries for the type, illegal flag combination | unknown map_flags above BPF_EXIST; array_map_delete_elem() always | key size mismatch | mapping not MAP_SHARED |
-E2BIG | value_size > INT_MAX; per-CPU round_up(value_size,8) > PCPU_MIN_UNIT_SIZE; more than MAX_USED_MAPS (64) maps in one program | the map is full — preallocated freelist empty, or is_map_full() for a lazily-allocated hash | — | — |
-ENOMEM | the allocation genuinely failed | bpf_mem_cache_alloc() found an empty per-CPU cache; or an LRU hash could not pop a victim (LRU maps never return -E2BIG) | — | — |
-EPERM | missing CAP_BPF / CAP_NET_ADMIN for the tier; kernel.unprivileged_bpf_disabled; BPF_F_ZERO_SEED without CAP_SYS_ADMIN | fd is write-only-denied, or the map is frozen | fd opened BPF_F_WRONLY | writable mmap of a frozen map |
-EBUSY | — | bucket-lock recursion in htab_lock_bucket() | — | writecnt != 0 (a live writable mapping or an in-flight write), or already frozen |
-ENOTSUPP | LRU hash with BPF_F_NO_PREALLOC | ring buffer key/value stubs | ring buffer key/value stubs | STRUCT_OPS map, or a map whose values contain a lock/timer/kptr |
-EACCES | — | — | — | writable mmap of a BPF_F_RDONLY_PROG map |
-ENOENT | — | BPF_EXIST on a missing key | key absent | — |
-EEXIST | — | BPF_NOEXIST on a present key | — | — |
Errno by operation, v6.12, read from the checks in kernel/bpf/syscall.c, hashtab.c, arraymap.c and ringbuf.c. What it shows: the same three or four numbers carry ten different meanings, disambiguated only by which command you issued. The insight to take: -E2BIG on an update almost never means “your value is too big” — by then the sizes were validated at create time. It means the map is full. Conversely -E2BIG at create time really is about size. And -ENOMEM on an update to a preallocated map is impossible by construction, so seeing it tells you immediately that the map was created with BPF_F_NO_PREALLOC (or is an LRU, which reports eviction failure the same way).
max_entries is not the number of entries you get
Three separate mechanisms make a map’s real capacity and real footprint differ from the number you passed.
Hash maps round the bucket count up to a power of two. htab_map_alloc() sets n_buckets = roundup_pow_of_two(max_entries), and htab_map_mem_usage() charges sizeof(struct bucket) * n_buckets in full regardless of occupancy. A max_entries of 1025 therefore allocates 2048 buckets; a max_entries of 65537 allocates 131072. Rounding your own capacity down to a power of two before creating the map is free memory.
Preallocated hash maps allocate one extra element per CPU. alloc_extra_elems() reserves num_possible_cpus() spare elements so that an update replacing an existing key can swap in a spare instead of doing a freelist pop-and-push. htab_map_mem_usage() accounts for them honestly (if (htab_has_extra_elems(htab)) num_entries += num_possible_cpus();), which is why a “1000-entry” map on a 128-CPU host really materializes 1128 elements. On a machine with a large num_possible_cpus() — and remember that is possible, not online, so a hotplug-capable VM can report far more CPUs than it has — this is not a rounding error.
A large hash map’s occupancy counter is approximate. htab_map_alloc() chooses between an atomic_t and a percpu_counter for the live-element count, with an explicit heuristic:
/* kernel/bpf/hashtab.c (v6.12) */
#define PERCPU_COUNTER_BATCH 32
if (attr->max_entries / 2 > num_online_cpus() * PERCPU_COUNTER_BATCH)
htab->use_percpu_counter = true;The comment above it reasons that a hash map more than half full “isn’t going to be O(1)” anyway, so contention on a shared atomic counter matters more than exactness. Once use_percpu_counter is on, is_map_full() calls __percpu_counter_compare(..., PERCPU_COUNTER_BATCH), which tolerates up to 32 * nr_cpus of drift. So a big map can accept somewhat more or somewhat fewer entries than max_entries before it starts returning -E2BIG. Do not build a correctness argument on the exact capacity of a large hash map.
LRU maps report eviction failure as -ENOMEM, and BPF_F_NO_COMMON_LRU silently rewrites your capacity. An LRU hash never returns -E2BIG: htab_lru_map_update_elem() calls prealloc_lru_pop() before taking the bucket lock (the comment explains why — getting a free node may itself have to delete an older element, which needs a bucket lock) and returns -ENOMEM if no node comes back. In the default common LRU that is genuinely rare, because kernel/bpf/bpf_lru_list.c tries hard: bpf_common_lru_pop_free() first pops this CPU’s local free list (LOCAL_FREE_TARGET = 128 nodes), then refills it from the global LRU list, and finally walks every other CPU’s local free and pending lists in round-robin, stealing a node from whichever has one. Only when that whole sweep comes up empty does the update fail.
The BPF_F_NO_COMMON_LRU variant is the dangerous one, and its trap is at creation time rather than update time. It gives each CPU an entirely separate LRU list with no stealing at all — bpf_percpu_lru_pop_free() touches only per_cpu_ptr(lru->percpu_lru, cpu) and gives up if that list is empty after a shrink. To make that partition even, htab_map_alloc() rewrites the capacity you asked for:
/* kernel/bpf/hashtab.c (v6.12) */
if (percpu_lru) {
/* ensure each CPU's lru list has >=1 elements.
* since we are at it, make each lru list has the same
* number of elements.
*/
htab->map.max_entries = roundup(attr->max_entries,
num_possible_cpus());
if (htab->map.max_entries < attr->max_entries)
htab->map.max_entries = rounddown(attr->max_entries,
num_possible_cpus());
}So max_entries in the kernel — and in bpftool map show — is not the number you passed, and each CPU gets only max_entries / num_possible_cpus() slots. Ask for 1000 entries on a 128-CPU host and every CPU is working with roughly eight: a workload whose keys are not evenly spread across CPUs will thrash catastrophically while the map looks 99% empty in aggregate. Use BPF_F_NO_COMMON_LRU only when you actually want per-CPU partitioning and have sized max_entries as per_cpu_capacity × num_possible_cpus().
Iteration is not a snapshot, and can loop forever
BPF_MAP_GET_NEXT_KEY walks a hash map’s buckets in whatever order they happen to be in at that moment, with no lock held across successive calls. Entries inserted during the walk may or may not be seen; entries deleted during the walk simply disappear. That much is expected. The trap is documented explicitly in the kernel’s own hash-map page:
Note that if
cur_keygets deleted thenbpf_map_get_next_key()will instead return the first key in the hash table which is undesirable. It is recommended to use batched lookup if there is going to be key deletion intermixed withbpf_map_get_next_key(). —Documentation/bpf/map_hash.rst
Read that carefully: the classic “iterate and delete as you go” loop — the natural way to write a garbage collector for a connection-tracking table — restarts from the beginning every time, and therefore never terminates on a map that keeps receiving inserts. This is the single most common way a userspace BPF agent burns 100% of a core. The fixes are BPF_MAP_LOOKUP_AND_DELETE_BATCH (which holds the bucket lock per batch and gives a coherent chunk), a BPF iterator program (bpf_iter, which walks in-kernel under proper locking), or collecting keys in one pass and deleting in a second.
The pointer you got may already belong to someone else
The concurrency section traced why this happens; the operational consequence deserves restating as a rule. bpf_map_lookup_elem() in a BPF program returns a live pointer whose memory is kept valid by RCU, but whose contents are not stable. Because both free paths return elements to a reusable pool immediately — the preallocated freelist or the bpf_mem_alloc per-CPU free_llist — the guarantee is type-stable memory in the spirit of SLAB_TYPESAFE_BY_RCU, not “nobody touched it”. Holding a lookup result across any helper call that can delete or evict, or across a bpf_loop() body that updates the same map, is a bug even though the verifier accepts it and nothing crashes. The symptom is a counter that occasionally jumps to a nonsensical value, or attributes traffic to the wrong key — never a crash, which is precisely what makes it expensive to find.
Per-CPU buffer sizing, restated because it keeps happening
For a per-CPU map, the buffer userspace must pass to BPF_MAP_LOOKUP_ELEM and BPF_MAP_UPDATE_ELEM is round_up(value_size, 8) * num_possible_cpus(), not value_size. Getting this wrong is a stack buffer overflow in your agent, written by the kernel, on the very first lookup. Use libbpf_num_possible_cpus() rather than sysconf(_SC_NPROCESSORS_ONLN) — the kernel indexes by possible CPUs, and on a cloud VM with hotplug enabled those numbers differ.
Memory accounting surprises
Two follow directly from the memcg model. First, the creator pays. A node-wide observability agent that creates maps on behalf of the whole machine accumulates every byte of them in its own cgroup, and is therefore the process the kernel OOM-killer selects when that cgroup hits its limit — even though the memory is being grown by events triggered by entirely unrelated workloads. Second, the root cgroup means no accounting at all: get_obj_cgroup_from_current() returns NULL for a process in the root memory cgroup, map->objcg stays NULL, and allocations fall back to root_mem_cgroup, i.e. effectively unlimited. An agent launched from a systemd unit lands in a real cgroup; one launched from an interactive root shell may not. If you are measuring BPF memory, verify you are measuring anything at all.
A third surprise is a version-skew artifact: a program that loaded fine on a pre-5.11 kernel with a generous RLIMIT_MEMLOCK, and now fails with an OOM or a cgroup charge failure instead of -EPERM, has not changed — the accounting scheme under it has. The reverse also happens: code that carefully raised RLIMIT_MEMLOCK to infinity at startup is now a no-op, and the real limit is memory.max on whatever cgroup the process is in.
Recursion, and probing the map layer itself
htab_lock_bucket() returns -EBUSY when this CPU is already inside a bucket lock in the same eight-way group. That is a deliberate refusal to deadlock, and it means a tracing program that probes anything reachable from the hash-map update path — the slab allocator, bpf_mem_cache_alloc(), the hash-map functions themselves — will silently drop a fraction of its own updates. If you attach a kprobe to kmalloc and count into a non-preallocated hash map, you have built exactly this recursion. Preallocation removes the allocator from the path but not the bucket lock; the robust answers are a per-CPU array (no bucket lock at all, and lookup is inlined arithmetic) or a ring buffer.
flowchart TD S["bpf_map_update_elem() failed"] --> E{"errno?"} E -->|"-E2BIG"| A1{"map type?"} A1 -->|"hash, prealloc"| A2["freelist empty:<br/>map is FULL at max_entries<br/>+ extra_elems per CPU"] A1 -->|"hash, no-prealloc"| A3["is_map_full() true -<br/>approximate above<br/>max_entries/2 > cpus*32"] A1 -->|"array"| A4["index >= max_entries<br/>(arrays never grow)"] E -->|"-ENOMEM"| B1{"LRU map?"} B1 -->|"yes"| B2["prealloc_lru_pop() found no victim.<br/>Common LRU: local list + global list<br/>+ round-robin steal all came up empty.<br/>NO_COMMON_LRU: this CPU's private list<br/>holds only max_entries/nr_cpus slots"] B1 -->|"no"| B3["bpf_mem_cache_alloc() cache empty;<br/>irq_work refill had not landed.<br/>Burst-dependent, will pass on retry"] E -->|"-EBUSY"| C1["htab_lock_bucket recursion:<br/>this CPU already holds a lock<br/>in the same 8-way group.<br/>You are probing the map path itself"] E -->|"-EPERM"| D1{"from syscall?"} D1 -->|"yes"| D2["map is FROZEN, or fd is<br/>BPF_F_WRONLY / RDONLY"] D1 -->|"no"| D3["not reachable from a BPF program"] E -->|"-EINVAL"| F1["flags above BPF_EXIST, or<br/>delete on an ARRAY (always -EINVAL)"] E -->|"-EEXIST / -ENOENT"| G1["BPF_NOEXIST hit / BPF_EXIST missed -<br/>these are ANSWERS, not errors"]
A diagnostic decision tree for a failed map update, v6.12. What it shows: the branch you need is almost always “which map type and which allocation regime”, because the same errno means different things in each. The insight to take: two of these are not really errors and should not be logged as such — -EEXIST and -ENOENT are the intended results of BPF_NOEXIST and BPF_EXIST, and treating them as failures is how a BPF agent ends up emitting millions of useless log lines. Of the genuine failures, only -E2BIG on a full map is a design problem you must fix by resizing; -ENOMEM on a lazy map is transient and worth retrying; and -EBUSY is telling you your probe placement is recursive, which no amount of resizing will fix.
Alternatives and When to Choose Them
There are two different questions hiding under “which map should I use?” The first is a choice within the map layer — hash or array, shared or per-CPU, ring buffer or perf buffer. The second is whether a map is the right mechanism at all. Both are worth answering explicitly, because the defaults people reach for are frequently wrong in the same predictable ways.
Choosing among the general-purpose types
flowchart TD Q0{"What is the shape<br/>of the thing you are storing?"} Q0 -->|"a stream of events<br/>going to userspace"| RB{"Do you need to know<br/>WHICH CPU produced it,<br/>at millions of events/sec?"} RB -->|"no - almost always"| RINGBUF["BPF_MAP_TYPE_RINGBUF<br/>5.8+, MPSC, ordered,<br/>one shared buffer, epoll-able"] RB -->|"yes"| PERCPURB["several RINGBUFs,<br/>one per CPU, in an<br/>ARRAY_OF_MAPS"] Q0 -->|"a value per dense<br/>small integer index"| ARR{"Contended across CPUs<br/>on the hot path?"} ARR -->|"yes"| PCA["BPF_MAP_TYPE_PERCPU_ARRAY<br/>no atomics, no false sharing;<br/>userspace sums across CPUs"] ARR -->|"no"| ARRAY["BPF_MAP_TYPE_ARRAY<br/>lookup inlines to<br/>bounds-check + shift + add"] Q0 -->|"a value per arbitrary key"| H{"Is the key set<br/>unbounded over time?"} H -->|"yes - flows, PIDs,<br/>connections"| LRU["BPF_MAP_TYPE_LRU_HASH<br/>self-evicting; sized by<br/>working set, not key space"] H -->|"no - bounded set"| HP{"Sparse relative<br/>to max_entries?"} HP -->|"yes"| NOPRE["HASH + BPF_F_NO_PREALLOC<br/>pay-as-you-go via bpf_mem_alloc"] HP -->|"no"| HASH["HASH, preallocated<br/>fixed footprint, insert<br/>cannot fail on allocation"] Q0 -->|"a value per PREFIX<br/>of a key"| LPM["BPF_MAP_TYPE_LPM_TRIE<br/>longest-prefix match;<br/>NO_PREALLOC mandatory"] Q0 -->|"'have I seen this before?'<br/>approximate is fine"| BLOOM["BPF_MAP_TYPE_BLOOM_FILTER<br/>5.16+, no keys, push/peek,<br/>hash count in map_extra"] Q0 -->|"state attached to a<br/>socket / task / inode / cgroup"| LOCAL["*_STORAGE family<br/>lifetime follows the object,<br/>freed automatically with it"] Q0 -->|"a work list<br/>with no keys"| QS["QUEUE / STACK<br/>push / pop / peek only"]
A decision tree for picking a general-purpose map type. What it shows: the choice is driven by the shape of the data and the boundedness of the key space, not by performance folklore. The insight to take: the two branches people most often get wrong are the top and the middle. Reaching for a PERF_EVENT_ARRAY for event streaming is now a legacy choice — the ring buffer is better on ordering, memory, and usually throughput. And reaching for a plain HASH when the key space is unbounded (one entry per TCP flow, one per PID) guarantees you will eventually hit -E2BIG and start silently dropping data; an LRU_HASH degrades gracefully instead, because you size it by the working set you can afford rather than by the key space you cannot bound.
The table version, for the recurring pairwise questions:
| If you are choosing between… | Choose the first when… | Choose the second when… |
|---|---|---|
ARRAY vs HASH | keys are dense small integers (CPU id, protocol number, an enum) — lookup inlines to ~8 instructions with no call | keys are sparse or arbitrary-width |
HASH vs PERCPU_HASH | values are read/written by one CPU at a time, or the value is large (per-CPU is capped at PCPU_MIN_UNIT_SIZE, 32 KiB) | the value is a hot counter — per-CPU removes the atomic and the cache-line ping-pong |
HASH vs LRU_HASH | the key set is genuinely bounded and you want an explicit -E2BIG when it is exceeded | the key set is unbounded and graceful degradation beats an error |
preallocated vs BPF_F_NO_PREALLOC | the map will fill up anyway, or you need an insert that cannot fail for allocator reasons | the map is sparse relative to max_entries and you can tolerate a rare burst-dependent -ENOMEM |
LRU_HASH vs LRU_HASH | BPF_F_NO_COMMON_LRU | keys are shared across CPUs (the default; there is cross-CPU stealing) | each CPU genuinely owns a disjoint key set and you have sized max_entries as a multiple of num_possible_cpus() |
RINGBUF vs PERF_EVENT_ARRAY | essentially always — see below | you are sustaining millions of events/sec and have measured that per-CPU buffers win for your workload |
a map vs an arena (BPF_MAP_TYPE_ARENA, 6.9+) | the data is key/value shaped | you want a shared address space with real pointers — linked structures written by the BPF program and read by userspace at the same addresses |
The dedicated comparison: ring buffer versus perf buffer
Because “how do I get events to userspace?” is the most common map question, and because the answer changed in Linux 5.8, it is worth spelling out. The perf buffer (BPF_MAP_TYPE_PERF_EVENT_ARRAY, fed by bpf_perf_event_output()) is a collection of per-CPU circular buffers built on the perf subsystem. The BPF ring buffer (BPF_MAP_TYPE_RINGBUF, fed by bpf_ringbuf_output() or the reserve/submit pair) is a single multi-producer/single-consumer queue shared across all CPUs. Andrii Nakryiko, who wrote the ring buffer, laid out the comparison in BPF ring buffer (2020-10-26), and it is worth reading his framing rather than a paraphrase:
- Memory. Perfbuf “allocates a separate buffer for each CPU,” which forces a trade-off between buffers large enough to absorb spikes and buffers small enough not to waste memory when idle — “it’s quite hard to find just the right balance, so BPF applications would typically either over-allocate perfbuf memory to be on the safe side, or will suffer inevitable data drops from time to time.” One shared ring buffer absorbs a spike wherever it lands, and “memory usage also scales better with increased amount of CPUs, because going from 16 to 32 CPUs doesn’t necessarily require twice as big a buffer.”
- Ordering. This is the decisive one for correlated events. With perfbuf, “if correlated events happen in rapid succession (within a few milliseconds) on different CPUs, they might get delivered out of order.” Nakryiko’s own example is a process-lifecycle tracer where
fork(),exec()andexit()for short-lived processes routinely arrived out of order because the scheduler migrated the task between CPUs, and handling that “required a significant increase of complexity in application’s handling logic.” The ring buffer “guarantees that if event A was submitted before event B, then it will be also consumed before event B.” - Wasted work. With perfbuf a program must build the sample in a scratch buffer and then copy it in, and all of that is thrown away if the buffer turns out to be full. The ring buffer’s
bpf_ringbuf_reserve()/bpf_ringbuf_submit()pair reserves space first, so the program formats the record directly in its final location and the submit “can’t possibly fail and doesn’t perform any extra memory copies at all” — and a failed reservation is discovered before the work is done, not after. - The one caveat. “BPF ringbuf internally uses a very lightweight spin-lock, which means that data reservation might fail, if lock is contended in NMI context,” so a program running from NMI (a
perf_eventprogram samplingcpu-cycles, typically) may drop records even when the buffer has room. That, and sustained multi-million-events-per-second throughput, are the only reasons Nakryiko gives to still consider perfbuf — and even then his measured advice is to use several ring buffers as per-CPU buffers rather than to fall back.
The ring buffer’s own constraints follow from its design and are worth knowing before you create one: key_size and value_size must both be zero (it is not a key/value store, and ringbuf_map_alloc() rejects anything else with -EINVAL), and max_entries is the buffer size in bytes, which must be a power of two and page-aligned. max_entries = 4096 is legal; max_entries = 1000 is -EINVAL. The mechanism itself — the producer/consumer position counters, the record header, BPF_RB_NO_WAKEUP / BPF_RB_FORCE_WAKEUP, and the USER_RINGBUF direction — belongs to BPF Ring Buffer.
When a map is not the answer
Three cases where reaching for a map is the wrong instinct.
Global variables are already maps — use them. A BPF program’s .data, .bss and .rodata sections are turned by libbpf into single-element BPF_MAP_TYPE_ARRAY maps — key_size = sizeof(int), max_entries = 1, the whole section as one value — with BPF_F_RDONLY_PROG on .rodata, BPF_F_MMAPABLE whenever the section holds a non-static variable, and the array type’s map_direct_value_addr support wired up. Writing static u64 packets; at file scope and incrementing it is not a hack around the map layer; it is the map layer, with the fd hidden by libbpf and userspace access provided by an mmap through the skeleton’s skel->bss->packets. Declaring an explicit one-element array map for a single counter is strictly more code for the same object. And const volatile globals plus BPF_MAP_FREEZE give you the constant folding described above, which no hand-rolled map can.
For state attached to a kernel object, use local storage rather than a hash keyed by the object’s identifier. A hash map keyed by PID is a classic beginner’s design, and it has three defects: PIDs are reused, so state leaks onto an unrelated process; nothing cleans up the entry when the task exits, so the map fills and starts returning -E2BIG; and lookup is a hash walk rather than a pointer dereference. BPF_MAP_TYPE_TASK_STORAGE fixes all three — the value hangs off the struct task_struct itself, is freed automatically when the task is, and is reached without hashing. The same argument applies to SK_STORAGE versus a hash keyed by a 5-tuple, and INODE_STORAGE versus a hash keyed by inode number.
For structures with internal pointers, consider an arena instead. BPF_MAP_TYPE_ARENA (value 33, the newest type as of 6.12) is not a key/value store at all; it is a shared region of address space that both the BPF program and userspace map, so a pointer written by one side is meaningful to the other. If what you actually want is a linked list, a tree, or a graph rather than a flat key/value table, a hash-of-indices simulating pointers is the wrong shape and an arena is the right one. It requires CAP_BPF and is the least mature of the families here; treat it as the modern escape hatch rather than a default.
And sometimes the answer is not BPF state at all. If what you want is a periodic scalar that the kernel already exports, a /proc or /sys file is cheaper than a BPF program plus a map plus a polling agent. If you want a one-off answer during debugging, bpf_printk() into the trace pipe skips the whole apparatus. Maps earn their cost when you need aggregation in the kernel at event rate — which is exactly the case where copying every event to userspace would be the bottleneck, and where the structural performance gap between the two lookup paths traced earlier becomes the entire point.
Production Notes
The map layer’s abstractions are cheap to describe and expensive to size. What follows is what actually goes wrong — and what people actually do about it — at scale.
Capacity planning is the whole job: Cilium’s numbers
Cilium is the most instructive published example, because it is a large BPF datapath whose entire scalability envelope is a list of map capacities, and it says so in as many words: “All BPF maps are created with upper capacity limits. Insertion beyond the limit will fail and thus limits the scalability of the datapath.” Its per-node defaults, as documented for Cilium 1.20 (verified 2026-09), are worth studying as a worked example of what “size your maps” means in practice:
| Cilium map | Default limit | What running out of it means |
|---|---|---|
| Connection tracking | 512k TCP / 256k UDP | max concurrent TCP connections per node |
| NAT | 512k | max NAT entries per node |
| Neighbor table | 512k | max neighbor entries |
| IP cache | 512k | max endpoints (IPv4+IPv6) across all clusters |
| Endpoints | 64k | max local endpoints + host IPs per node |
| Service load balancer | 64k | max ~3k clusterIP/nodePort services |
| Service backends | 64k | max cumulative unique backends |
| Policy (per endpoint, not per node) | 16k | max allowed identity + port + protocol pairs for one endpoint |
| IPv4 / IPv6 fragmentation | 8k each | max fragmented datagrams in flight simultaneously |
Cilium’s default BPF map capacities, from its eBPF Maps documentation. What it shows: every operational limit of a production BPF datapath is a max_entries somewhere. The insight to take: note the scope column in the original — the policy map is scoped per endpoint, everything else per node. That difference is the whole memory model of the product: one 16k map per pod, plus a fixed set of half-million-entry node maps. When you design a BPF system, the equivalent table is the first artifact you should be able to produce, and if you cannot, you have not sized your maps — you have guessed.
Two operational practices come out of this. First, Cilium exposes the big ones as flags (--bpf-ct-global-tcp-max, --bpf-ct-global-any-max, --bpf-nat-global-max, --bpf-neigh-global-max, --bpf-policy-map-max, --bpf-fragments-map-max, --bpf-lb-map-max) with a documented invariant between them: “the NAT table size must not exceed 2/3 of the combined CT table size (TCP + UDP).” Coupled map sizes need coupled configuration; sizing one in isolation produces a datapath that fails in a way the individual limits do not explain.
Second — and this is the interesting one given everything above about preallocation — Cilium sizes its largest maps as a fraction of host RAM rather than as a constant. The --bpf-map-dynamic-size-ratio flag “determines the upper capacity limits of several large BPF maps at agent startup based on the given ratio of the total system memory,” and the documentation’s worked example is “a ratio of 0.0025 leads to 0.25% of the total system memory to be used for these maps,” applied to the connection-tracking, neighbor, SNAT and reverse-service maps. This is the only sane answer when your maps are preallocated: a fixed max_entries is either a hard ceiling on a big machine or an OOM on a small one, so the capacity has to be derived from the memory you actually have. For comparison, the same page notes that kube-proxy sizes Linux’s conntrack table by core count — “a default of 32768 maximum entries per core with a minimum of 131072” — while Cilium sizes its BPF equivalents by memory, with the same 131072 floor. Cores or bytes: pick the resource the structure actually consumes.
Charge the memory to somebody who can survive it
The memcg model has one consequence that surprises every observability team on first contact, and it is worth planning for rather than discovering. Because map->objcg is captured at BPF_MAP_CREATE and every later allocation is redirected to it, all of a node-wide agent’s map memory is charged to the agent’s own cgroup, regardless of which workload’s events are growing it. A tracing agent whose hash maps swell because some unrelated pod started opening a million sockets is the process that gets OOM-killed, and the cgroup’s memory.current is the only place the growth shows up. The mitigations are all sizing decisions made before the fact: prefer bounded LRU_HASH over unbounded HASH for anything keyed by a workload-controlled identifier; set memory.max on the agent’s cgroup high enough to cover the preallocated footprint of every map it creates, which for preallocated maps is knowable exactly; and remember that an agent running in the root cgroup gets no accounting at all, so a systemd-managed unit and a manually-started binary have genuinely different failure modes.
The corollary for capacity review is that bpftool map show is the audit tool, not the cgroup. Since Linux 6.4 every map type must implement map_mem_usage() and bpftool prints it, so the per-map footprint is directly readable — including the parts you did not ask for, like the power-of-two bucket array, the per-CPU extra_elems, and the num_possible_cpus() multiplier on a per-CPU value. Note that reading it requires CAP_SYS_ADMIN, because BPF_MAP_GET_FD_BY_ID does; an unprivileged operator cannot audit the maps on their own machine.
Pin deliberately, or not at all
Pinning is what makes a map outlive the process that made it, and in production that is a two-edged property. Cilium’s reference guide gives the legitimate motivation — file descriptors “are limited to a processes’ lifetime, which makes options like map sharing rather cumbersome,” a real problem for the tc/iproute2 model where the loader “sets up and loads the program into the kernel and terminates itself eventually” (Cilium BPF and XDP Reference Guide). Datapath state that must survive an agent restart without dropping connections — a connection-tracking table, most obviously — has to be pinned; that is the feature working as designed.
The failure mode is the same mechanism without the intent. A pin holds both refcnt and usercnt, so a map pinned by a crashed or upgraded agent stays fully allocated with no process attached to it. Nothing in ps, top or a container’s memory view attributes it to anything; the memory is charged to a cgroup whose process is gone. The operational rule that follows is that pin paths must be versioned or reconciled: an upgrade either reuses the existing pin deliberately (keeping state across the restart) or unlinks it deliberately (accepting the state loss), and never leaves the decision to whether the new binary happens to pick the same path. libbpf makes the safe half of this automatic: when a map has a pin_path, bpf_object__reuse_map() opens the pin with BPF_OBJ_GET and runs map_is_reuse_compat(), which refuses to adopt it unless the existing map’s type, key_size, value_size, max_entries, map_flags and map_extra all match what the new object declares (tools/lib/bpf/libbpf.c). What it will not do is clean up the map it declined to reuse — the load fails, a new map is created under a new path or not at all, and the old pin is still there holding its memory. A hand-rolled agent that pins to a fixed path and never unlinks therefore accumulates one orphan per schema change, each one invisible to every process-oriented tool.
Design for the recursion and the drop
Two runtime behaviours that look like bugs are contractual, and production code has to handle them rather than log them.
The first is -EBUSY from htab_lock_bucket(). Any tracing program whose probe site is reachable from the map-update path will lose a fraction of its own writes to the recursion guard — this is a deliberate deadlock avoidance, not a transient. If you are instrumenting allocation, locking, or the BPF machinery itself, use a per-CPU array (no bucket lock) or a ring buffer, and treat a hash map as the wrong tool rather than a tool that needs a retry loop.
The second is that drops are normal and must be counted. Every mechanism here has a documented lossy path: a full hash map returns -E2BIG, a lazily-allocated map returns -ENOMEM under burst before its irq_work refill lands, an LRU evicts something you cared about, a ring buffer reservation fails under NMI contention, and a perf buffer drops per-CPU. The difference between a trustworthy BPF tool and an untrustworthy one is not that the first never drops — it is that the first has a per-CPU array counting its own drops and reports them alongside the data. Nakryiko’s advice about the ring buffer’s reserve-before-work API is the same principle at the API level: knowing you are about to drop before you spend the work is strictly better than discovering it after.
See Also
Parent map of content. Linux eBPF MOC — the hub for the whole eBPF subsystem, which places maps as one of ten areas alongside the VM, the verifier, the JIT, program types, helpers, BTF/CO-RE, the loader, control flow, and the security model.
The map types themselves. This note is the overview; each family has its own leaf. Hash and Array Maps covers the two foundational containers in detail — bucket structure, the inlined array lookup, and how array maps back BPF global variables. Per-CPU Maps goes deeper on the per-CPU family than the layout sketch above. LRU and LPM-Trie Maps covers the two self-managing/lookup-specialized types, including how the LRU’s active and inactive lists rotate. BPF Ring Buffer is the full treatment of RINGBUF and USER_RINGBUF, the reserve/submit protocol, and wakeup control. Specialized Maps (prog-array, sockmap, cgroup-storage) covers the subsystem-plumbing types. BPF Tail Calls explains what a PROG_ARRAY is actually for, and struct_ops and sched_ext does the same for STRUCT_OPS.
The interfaces a map sits behind. The bpf() Syscall is the multiplexed entry point that every command in this note goes through, including BPF_MAP_CREATE, the element commands, the batch commands, BPF_MAP_FREEZE and BPF_OBJ_PIN. BPF Helper Functions covers the program-side half — bpf_map_lookup_elem is helper number one — and BPF Kernel Functions (kfuncs) covers the newer, less stable extension mechanism. libbpf and the BPF Loader and BPF Skeletons and bpftool are how you actually create, populate, freeze and inspect maps in practice rather than by hand-filling union bpf_attr.
Why the verifier constrains what maps can do. eBPF Verifier is the long companion to this note: the NULL-check requirement on every lookup result, the map_gen_lookup inlining rewrite, the sleepable-program map restrictions, and the constant folding that BPF_F_RDONLY_PROG plus BPF_MAP_FREEZE unlocks all live there. Verifier Memory Safety and Pointer Types covers PTR_TO_MAP_VALUE and the pointer-lifetime rules that make “the pointer you got may already belong to someone else” a verifier concern as well as a correctness one. BTF (BPF Type Format) is what makes btf_record — and therefore spin locks, timers and kernel pointers inside map values — possible at all.
Memory and accounting. The Memory Cgroup memcg explains the charging machinery that map->objcg plugs into, and memcg Charging and Limits covers the limits that a BPF agent’s maps are counted against. Per-CPU Variables is the kernel-wide mechanism that per-CPU map values are built on, including the PCPU_MIN_UNIT_SIZE ceiling.
Concurrency. Read-Copy-Update Fundamentals and RCU Read-Side Critical Sections are the foundation for why a lookup can hand a BPF program a raw pointer; call_rcu and Deferred Reclamation covers the deferred-free machinery that bpf_mem_alloc uses for its trimmed objects. Raw Spinlocks and PREEMPT_RT explains why the hash bucket lock is a raw_spinlock_t rather than an ordinary one.
Security and privilege. CAP_BPF and BPF Privilege Model and Unprivileged BPF and Its Restrictions cover the tiering in the creation table above; BPF Token and Privilege Delegation covers the token argument threaded through every one of those checks; BPF and Spectre Hardening covers array_index_nospec(), the array index_mask, and bypass_spec_v1.
Lifetime and persistence. Map Pinning and bpffs and BPF Object Pinning and Lifetime pick up where the Lifetime section here leaves off — mount semantics, pin paths, and the operational conventions for surviving an agent upgrade. BPF Links and Attachment Lifecycle is the analogous story for programs rather than maps.
Applications. In-Kernel Aggregation with BPF Maps is the pattern this whole note exists to support: aggregate at event rate in the kernel, poll rarely from userspace. XDP Express Data Path and AF_XDP Zero-Copy Sockets are where the redirect-target map families (DEVMAP, CPUMAP, XSKMAP) are actually used, and sched_ext and BPF-Defined Schedulers is where STRUCT_OPS maps became load-bearing for something other than networking.