ITADN

execution/cache, execution/commitment: epoch reuse after Clear can alias a later unwind and serve dead-fork state

#22474Openyperbasis 创建于 2026-07-14
Y
yperbasiscommented
`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.
12 条评论