Does deleting a key from a BPF hash map zero out the value memory, and can a reused slot still hold the old value bytes?
Short answer: No. bpf_map_delete_elem unlinks the element from the hash buckets and hands its memory back to the per-CPU freelist (preallocated hash) or to the RCU-deferred BPF allocator (a BPF_F_NO_PREALLOC hash); it does not clear the value bytes. You cannot read a deleted value back through the map API — bpf_map_lookup_elem returns NULL and bpf_map_get_next_key skips it — but the physical bytes stay in the reused slot until a later update overwrites them, and a normal full update overwrites the whole value region on every CPU — with one exception: on the BPF_F_CPU create path for per-CPU hash maps, only the named CPU's slot is written and the other CPUs keep the previous occupant's bytes, so a lookup of the new key (with BPF_F_CPU targeting a non-named CPU) can still read a deleted key's per-CPU values there. Until a zero-fill patch posted to the bpf mailing list on 2026-09-23 is merged (not merged as of the 2026-09-24 mainline fetch), that case is the only cross-key value leak reachable through the map API: on ordinary HASH maps a full update rewrites the whole value region on every CPU before the key becomes visible, and BPF-program creation of a per-CPU value goes through pcpu_init_value, which zeros the non-current CPU slots. The rule: never rely on delete (or LRU eviction) to erase value contents; zero the value yourself, and do not assume embedded kernel object fields (timers, workqueues, task work queues) survive, because the kernel cancels those fields when the element is freed.
What delete actually does
htab_map_delete_elem computes the bucket for the key, takes the bucket lock, and — if the element is present — calls hlist_nulls_del_rcu(&l->hash_node) to unlink it, then free_htab_elem. There is no memset or equivalent on the value region anywhere in that path. What happens to the memory next depends on how the map was created:
- Preallocated hash (the default). At map creation,
prealloc_initallocates allmax_entrieselements in one contiguoushtab->elemspool and fills a per-CPU freelist. On delete the element goes back onto that per-CPU freelist (pcpu_freelist_push). It is immediately reusable — the next same-CPU update pops it back (__pcpu_freelist_pop, LIFO) — and the value bytes were never touched at delete time. The pool itself was zeroed once at map creation (__GFP_ZEROin__bpf_map_area_alloc,kernel/bpf/syscall.c); for per-CPU variants the separate per-CPU value area allocated bybpf_map_alloc_percpuinsideprealloc_initis zeroed at creation by the kernel'salloc_percpuzero-fill guarantee (the BPF code passes no__GFP_ZEROflag — the zeroing is a property ofalloc_percpuitself). Neither area is re-zeroed on delete; on element reuse, the only post-creation zeroing anywhere ispcpu_init_valuezeroing the non-current CPU slots when a BPF program creates a per-CPU value. - Lazy hash (
BPF_F_NO_PREALLOC). Elements are allocated on demand from the per-CPU BPF memory cache, and on deletehtab_elem_freereturns them to that allocator. The physical free is RCU-deferred (the allocator'sfree_by_rcu/call_rcu_tasks_tracepath), so the page is not recycled until a grace period completes; the value bytes persist even longer.
The element count is decremented synchronously in both paths (bpf_map_dec_elem_count / dec_elem_count), which is why a delete immediately makes room for a new element even though the underlying memory may be recycled a little later.
Why a full update cannot leak one key's value into another
When a new key is inserted into a recycled element, alloc_htab_elem copies the full key_size bytes and then copies the entire value region (copy_map_value, or a rounded-value_size memcpy). A well-formed, fully-written value therefore completely replaces the previous occupant's bytes. There is no path where you bpf_map_lookup_elem key B and read key A's value through the map API — with one documented exception: on the BPF_F_CPU create path for per-CPU maps, pcpu_copy_value writes only the named CPU's slot and returns early, leaving the other CPUs with the recycled element's previous bytes, so a lookup of the new key returns a deleted key's per-CPU values on those CPUs. A zero-fill patch for that arm (Fixes: c6936161fd55) was posted to the bpf mailing list on 2026-09-23 and is not merged as of the 2026-09-24 mainline fetch. The residual from an ordinary full update or a BPF-program write is a physical-memory fact (the old bytes sit in the slot until overwritten or scrubbed), not a logical leak: on ordinary HASH maps the value region is fully rewritten on every CPU before the key is visible, and BPF-program creation of a per-CPU value zeros the non-current CPU slots via pcpu_init_value. The BPF_F_CPU create-path exception above is the one logical, API-visible leak: until the patch lands, bpf_map_lookup_elem_flags with BPF_F_CPU|cpu<<32 targeting a non-named CPU returns the previous occupant's bytes.
The corners that are not safe
- The value bytes are not erased. Delete contains no scrub step, and the lazy allocator recycles its objects without a scrub a BPF program can rely on. If a value ever held secret data,
deletedoes not wipe it — the bytes linger in the preallocated pool or the allocator's freelist until reuse. For an explicitdelete, the safe pattern — and the only reliable erasure — is to zero the value explicitly before releasing the entry; automatic LRU eviction gives no user-space hook before the element leaves, so an evicted value's bytes may survive in the pool until a later allocation overwrites them. (Creation-time zeroing —__GFP_ZEROfor the non-per-CPU pool, thealloc_percpuguarantee for per-CPU value areas — is a one-time event; it does not apply on delete or reuse.) - Embedded kernel object fields are cancelled, not preserved. On free,
check_and_cancel_fields/bpf_obj_cancel_fieldscancel the value's BPF object fields (timers, workqueues, task work queues). For per-CPU hash maps these field types are rejected at map creation bymap_check_btf(), so the cancellation is a no-op there; for non-per-CPU maps it is a real guarantee. A program that reads the value after the element is freed or recycled cannot assume an embedded object is still valid. - LRU maps evict on their own.
BPF_MAP_TYPE_LRU_HASH(andLRU_PERCPU_HASH) evict the least-recently-used element when the map is full, through the same unlink-and-free path (htab_lru_map_delete_node:hlist_nulls_del_rcu+ element-count decrement + cancel fields, no zeroing). A key you never calleddeleteon can read backNULLafter eviction. WithBPF_F_NO_COMMON_LRUeach CPU keeps its own LRU list (per-CPU LRU + per-CPU map forLRU_PERCPU_HASH; per-CPU LRU + global map forLRU_HASH), so which element evicts is per-CPU rather than global.
How to verify it
- Confirm the unlink.
bpf_map_lookup_elem(key)returnsNULLright afterbpf_map_delete_elem(key), andbpf_map_get_next_key/bpf_map_get_next_key_batchnever return the deleted key. - Mind the iteration restart. The docs note that if you pass a
cur_keythat has just been deleted,bpf_map_get_next_keyrestarts from the first key in the table; use the batched lookup for a stable sweep. - Contrast with arrays.
BPF_MAP_TYPE_ARRAYvalues are zero-initialized at creation time (documented). Non-per-CPU preallocated hash values are likewise zeroed at map creation (__GFP_ZEROin__bpf_map_area_alloc); per-CPU value areas are zeroed at creation by thealloc_percpuguarantee. What differs: arrays are not recycled element-by-element, whereas hash elements are — and no path re-zeros a hash value on delete (the only post-creation zeroing ispcpu_init_valuezeroing the non-current CPU slots when a BPF program creates a per-CPU value). That recycling-without-rezeroing is the whole boundary. - Guarantee clean values. For an explicit
delete, the only reliable erasure is to write zeros beforedelete; automatic LRU eviction has no guaranteed user-space point before the element leaves, so scrub-on-eviction cannot be guaranteed; there is no scrub flag onbpf_map_delete_elem.
Where the answer stops
- This is about value memory, not the map API's contract: a deleted or evicted key still reads as
NULLand is not enumerated, so the logical view is clean even when the physical bytes are not — with the one exception above: on the unfixedBPF_F_CPUcreate path, a lookup of the new key on a non-named CPU is an API-visible cross-key logical leak until the zero-fill patch lands. - Whether the slab later scrubs a recycled page is not a BPF-visible guarantee; a program must not depend on it.
- The per-CPU LRU flag (
BPF_F_NO_COMMON_LRU) gives each CPU its own LRU list (per-CPU LRU + per-CPU map forLRU_PERCPU_HASH; per-CPU LRU + global map forLRU_HASH), changing which element evicts per CPU, but not the unlink-and-free-without-zeroing mechanism.
References
- BPF_MAP_TYPE_HASH, with PERCPU and LRU Variants (kernel docs) — default preallocation,
BPF_F_NO_PREALLOC, delete semantics, thebpf_map_get_next_keyrestart caveat, per-CPU slots. - BPF_MAP_TYPE_ARRAY (kernel docs) — "all array elements are pre-allocated and zero initialized when created."
- kernel/bpf/hashtab.c —
htab_map_delete_elem→free_htab_elem→pcpu_freelist_push/htab_elem_free;prealloc_init; the full-value overwrite inalloc_htab_elem;pcpu_init_value(per-CPU create-path zeroing);check_and_cancel_fields. - kernel/bpf/memalloc.c — the lazy BPF memory cache and its RCU-deferred free path.
- kernel/bpf/syscall.c —
__GFP_ZEROin__bpf_map_area_alloc(the prealloc element pool, line 384);bpf_map_alloc_percpu→__alloc_percpu_gfp(the per-CPU value area, line 583);__bpf_alloc_page(line 606);bpf_map_value_sizereturning the single value size underBPF_F_CPU/BPF_F_ALL_CPUS(line 137). - bpf mailing list, 2026-09-23: zero-fill patch for the
BPF_F_CPUper-CPU hash create path — thepcpu_init_valuediff (Fixes: c6936161fd55); the element-reuse selftest is at message 18. Not merged as of the 2026-09-24 mainline fetch. Cited by URL and subject only.
Community discussion today
Honest coverage note: the two watchlist-opted Slack archives returned zero messages in the rolling window today, and the allowlisted Discord channels plus the public bpf mailing list and r/eBPF remain visible-browser-only, with no visible-browser session available in this run — so no private community material was available for 2026-09-23. The question above is the fallback selection: a genuine, recurring practitioner boundary of the monitored eBPF development community, grounded entirely in the public primary sources above (kernel BPF docs and upstream source) rather than in any thread.
The recurring practitioner theme in that space — "my hash-map values go stale, a deleted key still seems to show old data" — resolves to exactly this boundary: deletion is an unlink, not a zeroing, and the element's memory is recycled through a per-CPU freelist (prealloc) or an RCU-deferred allocator (lazy). Two public-doc-anchored follow-ups recur here. First, the LRU surprise: a key that was never deleted can read back NULL once the map is full and the least-recently-used element evicts, which people misread as a delete bug when it is actually capacity eviction. Second, the secret-handling implication: because delete does not scrub the value bytes, a map that once held sensitive data still holds them physically until reuse, so the safe pattern is to zero the value (and drop embedded kernel objects explicitly) before releasing the entry. The boundary the docs leave open is that there is no scrub-on-delete API; keeping value memory clean is a program responsibility, not a kernel one. One public primary-source signal from the same day corroborates that this boundary is live in upstream development: the bpf mailing-list zero-fill patch for the BPF_F_CPU per-CPU create path described above (the patch, with the element-reuse selftest at message 18) — a same-day public signal, not a private-community message; the honest coverage note above is unchanged.
Continue exploring
Back to index
eBPF Q&A
Daily, source-grounded answers to recurring questions about eBPF, Linux observability, profiling, runtime extension, and security.
Previous
Why do TLS-encrypted service-to-service traces not stitch together in zero-code eBPF tracing, and which component actually bridges the encrypted hop?
Short answer: because the trace context travels in the HTTP headers, and TLS encrypts exactly that payload.
Next
Why does a BPF LPM-trie lookup return a less specific stored prefix, and how does the key prefixlen field decide which entry wins?
Short answer: Set the lookup key prefixlen to the map's maximum prefix length (32 for a 4-byte IPv4 data field, 128 for a 16-byte IPv6 field).
- Last updated
- Sep 24, 2026
- First published
- Sep 24, 2026
- Contributors
- Yunwei, Yunwei 123
Was this page helpful?