Skip to content

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

Description

@yperbasis

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.

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions