coherence.Gen.Init() resets the epoch counter to 0, so caches that call it from Clear make epoch values reusable within a process: a writer that samples coh.Epoch() before a concurrent Clear and lands its entry after stamps a pre-Clear epoch onto a post-Clear entry. Once later unwinds re-reach that epoch value, IsStale sees a matching epoch and keeps the entry live despite txNum >= floor — the cache serves dead-fork state.
#22466 closed this for GenericCache (the epoch is sampled under the key's put stripe, and Clear's coherence re-init runs inside the stripe fence). Still open:
- CodeCache (
execution/cache/code_cache.go) — Clear purges the layers and calls coh.Init() with no writer exclusion; the content-layer and addr-map put paths stamp coh.Epoch() with nothing ordering them against Clear.
- BranchCache (
execution/commitment/branch_cache.go) — Clear likewise ends in coh.Init(); inserts stamp coh.Epoch() and need the same straddle audit.
Proposed fix
Add coherence.Gen.Reset() that bumps the epoch and lifts the floor back to MaxUint64, and call it from the Clear paths (keeping Init for construction). Monotonic epochs make aliasing impossible regardless of where the stamp is read, covering all coherence.Gen users without new fences. (uint32 wrap after 4B clears+unwinds is not a practical concern.)
Repro technique
The deterministic choreography in TestGenericCache_ClearRacingPut_EpochAlias (execution/cache/generic_cache_concurrency_test.go) — park Clear, then the put, on the write-serialization point, with one unwind before and one after — adapts to both caches.
coherence.Gen.Init()resets the epoch counter to 0, so caches that call it fromClearmake epoch values reusable within a process: a writer that samplescoh.Epoch()before a concurrentClearand lands its entry after stamps a pre-Clear epoch onto a post-Clear entry. Once later unwinds re-reach that epoch value,IsStalesees a matching epoch and keeps the entry live despitetxNum >= floor— the cache serves dead-fork state.#22466 closed this for
GenericCache(the epoch is sampled under the key's put stripe, andClear's coherence re-init runs inside the stripe fence). Still open:execution/cache/code_cache.go) —Clearpurges the layers and callscoh.Init()with no writer exclusion; the content-layer and addr-map put paths stampcoh.Epoch()with nothing ordering them againstClear.execution/commitment/branch_cache.go) —Clearlikewise ends incoh.Init(); inserts stampcoh.Epoch()and need the same straddle audit.Proposed fix
Add
coherence.Gen.Reset()that bumps the epoch and lifts the floor back toMaxUint64, and call it from theClearpaths (keepingInitfor construction). Monotonic epochs make aliasing impossible regardless of where the stamp is read, covering allcoherence.Genusers without new fences. (uint32 wrap after 4B clears+unwinds is not a practical concern.)Repro technique
The deterministic choreography in
TestGenericCache_ClearRacingPut_EpochAlias(execution/cache/generic_cache_concurrency_test.go) — parkClear, then the put, on the write-serialization point, with one unwind before and one after — adapts to both caches.