[PATCH bpf-next 04/38] bpf: don't hand out a linked element as extra element
flat view
COOLING10d
SUPERSEDED
From: Alexei Starovoitov <hidden>
Date: 2026-10-17 01:21:14
Also in:
bpf
Subsystem:
bpf [general] (safe dynamic programs and tools), the rest · Maintainers:
Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi, Linus Torvalds
Status: Superseded by v2 (posted 2026-10-17). [go to v2]
Revision v1 (2 revisions archived).
Revisions (2)
- v1 current
- v2 [diff against previous]
From: Josef Bacik <josef@toxicpanda.com>
When htab_map_update_elem() replaces an element of a preallocated map,
alloc_htab_elem() takes the spare element of this cpu and puts the old
element into the per-cpu slot right away, long before the old element is
unlinked from its bucket. The bucket lock disables interrupts, but it
does not keep out an NMI, or a tracing program on the return of
alloc_htab_elem(). bpf_prog_active only helps if both programs are
kprobe, tracepoint or perf_event programs. A program that updates an
existing key in another bucket of the same map gets to use the slot:
htab_map_update_elem(k1) tc, xdp, fentry, raw_tp, ...
alloc_htab_elem()
l_new = *pl_new
*pl_new = l_old still linked in bucket 1
<NMI>
htab_map_update_elem(k2) perf_event program
l_new = *pl_new the l_old of the outer update
hlist_nulls_add_head_rcu(l_new, bucket 2)
hlist_nulls_del_rcu(k2's old element)
</NMI>
hlist_nulls_add_head_rcu(l_new, bucket 1)
hlist_nulls_del_rcu(l_old)
If l_old was the first element of bucket 1, the outer add points its pprev
at l_new, and the delete takes it out of bucket 1 only. l_new->next now
leads into bucket 2, where l_old stays with a poisoned pprev, on which a
delete of k2 crashes. If it was not the first, the delete takes it out of
bucket 2: k2 is gone from the map, and the chain of bucket 1 still contains
l_old, whose next pointer leads into bucket 2.
Either way a walk of bucket 1 ends on the nulls value of bucket 2.
lookup_nulls_elem_raw() restarts whenever that happens, so every lookup in
bucket 1 which does not find its key spins forever:
rcu: INFO: rcu_preempt detected stalls on CPUs/tasks:
__htab_map_lookup_elem+0x354/0x4d0
BUG: KASAN: wild-memory-access in htab_map_delete_elem+0x30c/0x670
Write of size 8 at addr dead000000000122 by task extra/102
An NMI between the load and the store of the slot makes both updates use
the same element, with much the same result.
Empty the slot when the spare element is taken, in a way that is atomic
on this cpu, and put the old element in only after it has been unlinked.
A nested update that finds the slot empty, which is the case that
corrupted the map, fails with -EBUSY.
Fixes: 8c290e60fa2a ("bpf: fix hashmap extra_elems logic")
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/hashtab.c | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/kernel/bpf/hashtab.c b/kernel/bpf/hashtab.c
index 9d4791ee1741..923fec694747 100644
--- a/kernel/bpf/hashtab.c
+++ b/kernel/bpf/hashtab.c@@ -1279,11 +1279,17 @@ static struct htab_elem *alloc_htab_elem(struct bpf_htab *htab, void *key, if (prealloc) { if (old_elem) { /* if we're updating the existing element, - * use per-cpu extra elems to avoid freelist_pop/push + * use per-cpu extra elems to avoid freelist_pop/push. + * The caller puts old_elem into the slot once it is + * unlinked. Until then the slot is empty for an NMI + * or a kprobe that updates this map on this cpu. */ pl_new = this_cpu_ptr(htab->extra_elems); - l_new = *pl_new; - *pl_new = old_elem; + l_new = READ_ONCE(*pl_new); + do { + if (!l_new) + return ERR_PTR(-EBUSY); + } while (!try_cmpxchg_local(pl_new, &l_new, NULL)); } else { struct pcpu_freelist_node *l;
@@ -1459,11 +1465,14 @@ static long htab_map_update_elem(struct bpf_map *map, void *key, void *value, if (l_old) { hlist_nulls_del_rcu(&l_old->hash_node); - /* l_old has already been stashed in htab->extra_elems, cancel - * its reusable special fields before it is available for reuse. + /* l_old replaces the extra element that alloc_htab_elem() took, + * cancel its reusable special fields before the next update on + * this cpu reuses it. */ - if (htab_is_prealloc(htab)) + if (htab_is_prealloc(htab)) { + WRITE_ONCE(*this_cpu_ptr(htab->extra_elems), l_old); check_and_cancel_fields(htab, l_old); + } } htab_unlock_bucket(b, flags); if (l_old && !htab_is_prealloc(htab))
--
2.55.0