From 1c9125db37f1ece793dbbe75cb26fc11adbe56e3 Mon Sep 17 00:00:00 2001 From: Paul Hammant Date: Sat, 19 Sep 2026 11:44:13 +0100 Subject: [PATCH] [std.snapshot][skip actions] doc: warn cas() is ABA-unsafe for multi-writer reclaim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `snapshot.cas` is a plain pointer-value CAS with no generation tag. That is correct for the single-writer-actor + defer-one-generation pattern the header already documents, but it silently breaks a MULTI-writer reclaiming CAS-COW: once displaced values are actually freed, a recycled address lets a slow writer's stale cas() spuriously succeed and clobber a committed value (ABA). This is orthogonal to reader use-after-free — a reader grace epoch (std.sync, #2082) closes reader UAF but does nothing for writer ABA. Comment-only: adds an ABA WARNING to the reclamation contract pointing at the fix (serialize writers) and at #2112. No code change. Surfaced by selaenium's grid registry, which hit exactly this the instant it stopped leaking displaced tables; it works around it with a writer spinlock. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01N1Vjg3yn7uc7uMXNmyJz1e --- std/snapshot/module.ae | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/std/snapshot/module.ae b/std/snapshot/module.ae index 13bdbd27b..a8ea89924 100644 --- a/std/snapshot/module.ae +++ b/std/snapshot/module.ae @@ -35,6 +35,22 @@ // have completed). See docs/snapshot-cell.md for the full pattern and // the CAS retry loop. // +// ABA WARNING (multiple concurrent writers): `cas` is a PLAIN +// pointer-value CAS — it compares `expected` against the stored pointer +// by value, with no generation tag. That is safe for the single-writer +// pattern above, but RECLAIMING (actually freeing displaced values) +// under MORE THAN ONE concurrent writer is ABA-unsafe: once the +// allocator can hand a just-freed address back out, writer A frees P, +// the next allocation reuses P for a fresh value, and a slow writer B +// whose `expected` is still P sees the cell holding P again and its +// stale `cas(P -> ...)` SPURIOUSLY SUCCEEDS, clobbering the committed +// value. Note this is orthogonal to reader use-after-free: a reader +// grace period (e.g. a std.sync reader-count epoch) closes reader UAF +// but does NOTHING for writer-vs-writer ABA. If you must reclaim from +// many writers, SERIALIZE the writers (a lock or single owner) so no +// other writer holds a displaced pointer as its `expected` while it is +// being freed. See aether#2112. +// // USE / DON'T-USE: // * Use for read-mostly data: tiny rebuild rate, huge read rate. // * Do NOT use for write-heavy data or large values that change often