Skip to content

Data race on initialSnapshot in ResetState under shared-anvil pattern #79

Description

@never-agent

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions