vt: make Emulator.closed atomic to fix the Close/Read data race - #931
Closed
cameronsjo wants to merge 1 commit into
Closed
vt: make Emulator.closed atomic to fix the Close/Read data race#931cameronsjo wants to merge 1 commit into
cameronsjo wants to merge 1 commit into
Conversation
Emulator.closed is a plain bool that Close writes and Read reads with no synchronization between them, so -race flags every shutdown of an Emulator whose output is being consumed. Read blocks on an empty pipe and Close is the only thing that unblocks it, so no usage pattern avoids the pair running concurrently. SafeEmulator does not cover it either: its Read takes no lock and it does not override Close. The guard is an early-out rather than the mechanism — the actual unblocking is pw.CloseWithError(io.EOF) on an io.Pipe, which is internally synchronized and idempotent — so an atomic.Bool is enough and Read stays lock-free. Close uses CompareAndSwap rather than a Load then a Store, which also makes it single-winner: two concurrent Close calls could previously both pass the guard. Harmless given CloseWithError is idempotent, but the CAS form costs nothing. Refs charmbracelet#930 Session-Name: cedar-fugue Session-Id: a516020b-4fa8-4479-ab35-fc781fb6025a Model: claude-opus-5 Harness: claude-code 2.1.220 Machine: cf6e768835c7 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #930
Emulator.closedis a plainboolthatClosewrites andReadreads with no synchronization between them, so-raceflags every shutdown of anEmulatorwhose output is being consumed.Readblocks on an empty pipe andCloseis the only thing that unblocks it, so there is no usage pattern that avoids the pair running concurrently.SafeEmulatordoes not cover it either: itsReadtakes no lock (deliberately, so a parked reader can't holdmuagainst every writer) and it does not overrideClose, so the embedded*Emulator.Closeis promoted unchanged.The change
closedbecomes anatomic.Bool. The guard is an early-out rather than the mechanism — the actual unblocking ispw.CloseWithError(io.EOF)on anio.Pipe, which is internally synchronized and idempotent — so no mutex is needed andReadstays lock-free.CloseusesCompareAndSwaprather than aLoadthen aStore, which also makes it single-winner: two concurrentClosecalls previously could both pass the guard and both callCloseWithError. Harmless, since that call is idempotent, but the CAS form costs nothing and says what it means.Verification
go test -race ./...invt/passes.mainpasses with this change:On
mainthis reports the race atemulator.go:252(Read) againstemulator.go:265(Close). With this change it passes.vtthat had to exclude every shutdown-touching test from its-racejob runs its full suite green, with no exclusions, against a patchedvt.I did not add the reproducer as a test in this PR since it needs a sleep to set up the condition; happy to include it in whatever form suits the package if you'd like it covered.