From 7e14fde0f3e727eba5e2d32e94629d6fc9f59e94 Mon Sep 17 00:00:00 2001 From: Vincent Hengel Date: Fri, 18 Sep 2026 23:34:09 +0200 Subject: [PATCH] fix(base): announce the EBR epoch before begin()'s first node dereference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit begin() walked the bucket chains looking for the first live node before the iterator's ebr::Guard announced — exactly the nodes GC sweeps, retires and frees. Under concurrent insert/remove churn the preamble could step into freed memory and hand the traversal a garbage start node; callers that range-for the map (zetanet's packet-ack retry scan) then fault on a near-null node. Announce in begin()/order_begin() before the first load and hand the live guard to the iterator through a new IteratorBase constructor, so the pin is continuous from before the first dereference until the iterator dies. No behavior change for single-threaded use. --- base/containers/lock_free_ordered_map.h | 40 ++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/base/containers/lock_free_ordered_map.h b/base/containers/lock_free_ordered_map.h index 164ac1bc..155a3e31 100644 --- a/base/containers/lock_free_ordered_map.h +++ b/base/containers/lock_free_ordered_map.h @@ -297,6 +297,19 @@ class LockFreeOrderedHashMap { skip_deleted(); } + // Active iterator taking an already-announced guard. Factories that must + // dereference nodes to *find* the traversal's start (begin() walks bucket + // chains skipping deleted nodes) announce first and hand the live guard + // here, so no node is ever touched outside an EBR critical section. The + // guard's pin is continuous across the handoff: it stays announced from + // before the first load until the iterator dies. + IteratorBase(const LockFreeOrderedHashMap* m, + Node* start_node, + ebr::Guard&& announced) + : map(m), currentNode(start_node), guard_(static_cast(announced)) { + skip_deleted(); + } + // End sentinel: no epoch pin needed. IteratorBase(const LockFreeOrderedHashMap* m) : map(m), currentNode(nullptr), guard_(ebr::Guard::inactive) {} @@ -330,6 +343,10 @@ class LockFreeOrderedHashMap { public: OrderIterator(const LockFreeOrderedHashMap* m, Node* start_node) : IteratorBase(m, start_node) {} + OrderIterator(const LockFreeOrderedHashMap* m, + Node* start_node, ebr::Guard&& announced) + : IteratorBase(m, start_node, + static_cast(announced)) {} OrderIterator(const LockFreeOrderedHashMap* m) : IteratorBase(m) {} }; @@ -359,6 +376,11 @@ class LockFreeOrderedHashMap { mem_size b_idx, Node* start_node) : IteratorBase(m, start_node), bucketIndex(b_idx) {} + BucketIterator(const LockFreeOrderedHashMap* m, + mem_size b_idx, Node* start_node, ebr::Guard&& announced) + : IteratorBase(m, start_node, + static_cast(announced)), + bucketIndex(b_idx) {} BucketIterator(const LockFreeOrderedHashMap* m) : IteratorBase(m), bucketIndex(m->bucketCount) {} @@ -372,17 +394,27 @@ class LockFreeOrderedHashMap { // ---- begin / end ---- OrderIterator order_begin() const { - return OrderIterator(this, orderHead.load(base::memory_order_acquire)); + // Announce before the first load: orderHead may be CAS-swapped the moment + // we read it, retiring the node we hold, and a later pass may free it. The + // guard is handed to the iterator, keeping the pin continuous. + ebr::Guard guard; + return OrderIterator(this, orderHead.load(base::memory_order_acquire), + static_cast(guard)); } OrderIterator order_end() const { return OrderIterator(this); } + // The preamble that finds the first live node walks deleted nodes — exactly + // the ones GC sweeps, retires and frees — so it must run inside an EBR + // critical section. Without the guard, a concurrent collect_garbage() can + // free the chain between the bucket-head load and the next hop, and the + // iterator starts life on freed memory (observed as crashes in callers that + // range-for the map under sustained insert/remove churn). BucketIterator begin() const { + ebr::Guard guard; for (mem_size i = 0; i < bucketCount; ++i) { Node* node = buckets[i].load(base::memory_order_acquire); - while (node && node->is_deleted.load(base::memory_order_acquire)) - node = node->bucketNext.load(base::memory_order_acquire); if (node) - return BucketIterator(this, i, node); + return BucketIterator(this, i, node, static_cast(guard)); } return end(); }