fix(base): announce the EBR epoch before begin()'s first node dereference - #17
Merged
Merged
Conversation
…ence 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.
Force67
force-pushed
the
fix/lfomap-unguarded-begin-preamble
branch
from
September 19, 2026 08:12
efb03b7 to
7e14fde
Compare
Force67
added a commit
to Force67/zetanet
that referenced
this pull request
Sep 19, 2026
…n begin (#4) Picks up Force67/equilibrium#17, which fixes the use-after-free behind the ProcessOutgoingPackets crash: LockFreeOrderedHashMap::begin() dereferenced deleted-but-unswept nodes before the iterator's EBR guard announced, so a concurrent collect_garbage() could free the chain under the ack-map traversal. The pin was 25 commits behind devel5, so this advances vendor/equilibrium by 26 commits rather than one. Beyond the fix, the notable ones are the STL and C runtime removal, the three security bug bash passes, and the container value-semantics work. Verified with -DZNET_USE_STL=OFF: the library and the samples build, and handshake_integration, mp_integration and the stressed chaos-soak profile (drop/reorder/jitter 5/5/5, seeds 1-3) pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
LockFreeOrderedHashMap::begin()walked the bucket chains looking for the firstlive node before any
ebr::Guardannounced — the guard is only acquiredinside the iterator's constructor, after the preamble:
The nodes that preamble steps over are deleted-but-not-yet-swept — exactly the
nodes a concurrent
collect_garbage()pass unlinks, retires and frees. Undersustained insert/remove churn the preamble can step into freed memory and hand
the traversal a garbage start node.
order_begin()had the same load-before-announce gap on a smaller window.The field crash this explains
zetanet's reliable-transport retry scan range-fors the ack map on the outgoing
thread (
ZPacketQueue::ProcessOutgoingPackets). Under minutes of gameplaytraffic (constant reliable packets + ack churn) it crashed with the traversal
dereferencing a near-null node:
Core dumps confirm the faulting deref reads a chain node at ~null.
The fix
Announce the epoch before the first node load in
begin()/order_begin(), and hand the live guard to the iterator through a newIteratorBaseconstructor, so the pin is continuous from before the firstdereference until the iterator dies. Verified:
lock_free_ordered_map_test.ccsuite passes under ASan;insert/remove churn) that ASan-crashes in the preamble on the parent commit
no longer crashes there post-fix.
Known follow-up (not addressed here)
Under duplicate-heavy insert churn the container still exposes a deeper
iterator-protocol flaw (readers can observe a null node past the
!=check,with or without the preamble fix, even with GC freeing disabled — so it is
structural, not lifetime-related). A reproducer and forensics are in the
follow-up issue; fixing it needs a proper analysis of the iterator protocol
and should not ride along with this strictly-correct change.
Rebased onto devel5 (2026-09-19)
The branch was cut from
e68c2b8and devel5 had moved 25 commits since, whichrewrote most of this header's formatting — hence the conflict. Rebased; both
conflicting hunks were the same lines reflowed, so the fix ported unchanged and
only its placement moved.
begin()still drops the unguarded skip-deletedpreamble and leaves that walk to the iterator's own
skip_deleted(), which nowruns inside the guard.
Re-verified on the rebased commit:
base_unittests— 972 tests, all pass;LockFreeOrderedHashMaptests pass again under-fsanitize=address,undefined;ZNET_USE_STL=OFF)and its handshake + mp integration tests pass, including the stressed
chaos-soak profile (drop/reorder/jitter 5/5/5, seeds 1-3).