[PATCH 0/3] libstdc++: implement std::hazard_pointer (P2530R3, C++26)
Thomas Rodgers
rodgert@twrodgers.com
Tue Jul 21 02:22:12 GMT 2026
Thanks for taking this on, the test matrix and the memory-ordering
write-up in your patch announcement made the review much easier.
1. Reclaim-side memory ordering - This is a real use-after-free on
weak-memory targets, and I confirmed the finding with Maged.
_M_synchronize() snapshots the slots with memory_order_acquire.
The reader side is correct (the hazard store and the validation
reload are both seq_cst) but an acquire load on the scan side does
not join the seq_cst total order, and the collect step's lock chain
only orders the *writer's* retirement against the collect, not the
independent reader's hazard store against the scan.
Your announcement cites P2530R3's seq_cst/acquire pairing, but the
acquire prescribed there is the reader's try_protect reload; the scan
is constrained by [saferecl.hp.general] p6, which wants the end of
the protection epoch to strongly happen before the reclamation, and
an acquire-only scan doesn't give you that.
Concretely: src == O; a reader does hazard.store(O, seq_cst) then
reloads src seq_cst and still sees O, so it believes O is protected;
a writer swaps O out and retires it; the reclaimer's acquire-load
reads a stale null, does not see the hazard, and frees O; the reader
then dereferences freed memory.
x86 as compiled hides it (the seq_cst store's fence and the locked
RMWs on the reclaim path), but the abstract machine permits it, so
weaker targets (POWER) and future compiler transformations are
entitled to it. Fix: a seq_cst fence between the collect step and the
snapshot loop (the snapshot loads can then stay acquire). Upgrading
the loads alone doesn't close it, the removal store on src is user
code and P2530R3 doesn't require it to be seq_cst.
I checked the shape with herd7: the bad outcome is allowed as written
(acquire scan), still allowed with seq_cst snapshot loads when the
removal store is only release, and forbidden with the fence. On
POWER9 and POWER10 (compile farm, litmus7 7.58) the acquire scan
reordering is observed on hardware, up to 1.4M of 959M runs, and the
fence stays at zero across ~10^9. Folly's implementation agrees,
do_reclamation() issues a seq_cst asymmetric_thread_fence_heavy
immediately before load_hazptr_vals() and the slot loads themselves
are only relaxed (acquire only under TSan). I can post the litmus
tests if that's useful.
2. retire() and noexcept - retire() is noexcept per [saferecl.hp.base]
and your noexcept.cc rightly asserts it, but the current path pushes
onto a std::vector and may call _M_synchronize(), which also
allocates, so OOM becomes terminate(). Strictly that's a QoI question
rather than a conformance one, but the clause's shape says what's
intended: make_hazard_pointer gets an explicit "Throws: May throw
bad_alloc" while retire is noexcept with even the deleter move-assign
covered by precondition, and P2530R3's ABI guidance (section 1.5)
already expects the obj_base to carry intrusive bookkeeping (it
reserves a counter and a cohort pointer there). An intrusive retire
link in the same spirit is how Folly's hazptr_obj keeps retire() from
allocating. The retire path shouldn't be able to fail, which comes
down to where the retire-list storage lives.
3. Slot acquire/release cost - Every make_hazard_pointer() and every
non-empty ~hazard_pointer() serializes on the one _M_slot_free_mutex;
taking that lock is the real cost, and the scans under it (std::find
over the free bitmap, std::find_if over the whole deque because the
handle caches a raw slot pointer, not an index) stretch the critical
section by O(pool) on top. The deque/std::find choices from the RFC
round are fine locally. The performance the paper holds up (section
3.1) is ~4ns construction/destruction, so this is perhaps worth
reconsidering.
4. concurrent.cc can't detect the failure in (1) - It flags
use-after-free only via p->value < 0, but the writers only ever
publish positive counters, so a reused Node won't trip it. A
poisoning/tagging scheme so (1) can actually fail a test on a
weak-memory target rather than pass green would be good to have. I
did run an end-to-end repro on POWER9 (trunk g++, ASan) against your
header and didn't observe a failure, but that's inconclusive, not
exculpatory: the model permits the outcome, so a clean run doesn't
clear it.
Tom.
More information about the Libstdc++
mailing list