Summary
ResetState mutates a.initialSnapshot outside any lock. snapshotInitOnce only guards the first write — the steady-state reassignment after each revert is unsynchronized, and is read concurrently by other ResetState calls.
Detail
anvil.go (ResetState):
- Line ~641:
a.initialSnapshot, initErr = a.Snapshot(ctx) — guarded by snapshotInitOnce.Do(...) (first call only).
- Line ~652:
success, err := a.Revert(ctx, a.initialSnapshot) — reads the field.
- Line ~663:
a.initialSnapshot, err = a.Snapshot(ctx) — writes the field, with no lock.
The shared-anvil test pattern documented in CLAUDE.md reuses a single *Anvil across subtests via setupSharedAnvil. Two concurrent ResetState calls — or a ResetState racing any reader of initialSnapshot — is an unsynchronized write/read of a string field. This undermines the very pattern the test suite is built on.
Confidence
Provable by inspection. Should be confirmed with go test -race against a subtest that calls ResetState from multiple goroutines. (I could not run -race on my host — the local Go toolchain is broken — so this is filed on static analysis; please confirm with -race before the fix lands so the fix has a red→green guard.)
Suggested fix
Guard initialSnapshot with a sync.Mutex (or atomic.Pointer[string]), or document ResetState as single-goroutine-only and assert it in the shared-anvil helper.
Found during an external read-through of go-anvil@3701910. Not covered by any open v0.2.0 issue.
Summary
ResetStatemutatesa.initialSnapshotoutside any lock.snapshotInitOnceonly guards the first write — the steady-state reassignment after each revert is unsynchronized, and is read concurrently by otherResetStatecalls.Detail
anvil.go(ResetState):a.initialSnapshot, initErr = a.Snapshot(ctx)— guarded bysnapshotInitOnce.Do(...)(first call only).success, err := a.Revert(ctx, a.initialSnapshot)— reads the field.a.initialSnapshot, err = a.Snapshot(ctx)— writes the field, with no lock.The shared-anvil test pattern documented in
CLAUDE.mdreuses a single*Anvilacross subtests viasetupSharedAnvil. Two concurrentResetStatecalls — or aResetStateracing any reader ofinitialSnapshot— is an unsynchronized write/read of astringfield. This undermines the very pattern the test suite is built on.Confidence
Provable by inspection. Should be confirmed with
go test -raceagainst a subtest that callsResetStatefrom multiple goroutines. (I could not run-raceon my host — the local Go toolchain is broken — so this is filed on static analysis; please confirm with-racebefore the fix lands so the fix has a red→green guard.)Suggested fix
Guard
initialSnapshotwith async.Mutex(oratomic.Pointer[string]), or documentResetStateas single-goroutine-only and assert it in the shared-anvil helper.Found during an external read-through of
go-anvil@3701910. Not covered by any open v0.2.0 issue.