browsercheck: guard the reply table, which two goroutines were writing - #28
Merged
Conversation
CI on main dies partway through a check:
the tab handed back a 934 byte PDF
fatal error: concurrent map writes
main.(*conn).call browsercheck/main.go:252
`pump` runs in its own goroutine from the moment the connection opens. It
reads `c.replies[msg.ID]` and deletes the entry; `call` writes one and
increments `c.id`. Neither is guarded, so both the map and the counter are
raced -- and a map raced this way does not corrupt quietly, it kills the
process.
A mutex now covers the counter and the table in both places, held only
across the map work and never across a channel send or a websocket write.
`call` also deletes its own entry on the way out. Before, a failed write
or an ended context returned without removing it, leaving pump holding a
channel nobody would ever read.
The package has no tests -- what exercises this is the browsercheck run
itself, so the CI on this pull request is the verification.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
CI on
maindies partway through a check — not a failed assertion, a killed process:The race
pumpruns in its own goroutine from the moment the connection opens (go c.pump(ctx)), and both goroutines write the same map with nothing between them:pump, line 230/236c.replies[msg.ID], thendelete(c.replies, msg.ID)call, line 251/252c.id++, thenc.replies[id] = chSo
c.repliesand thec.idcounter are both raced. A map raced this way does not corrupt quietly — the runtime kills the process, which is exactly what the log shows.The change
A mutex covers the counter and the table in both places. It is held only across the map work, never across a channel send or a websocket write, so
pumpcannot block on a slow reader while holding it.callalso removes its own entry on the way out:Before, a failed
ws.Writeor an ended context returned without removing the entry, leavingpumpholding a channel nobody would ever read.Verification
go vetandgofmtare clean and it builds. The package has no tests — what exercises this code is the browsercheck run itself, so the CI on this pull request is the verification, against the same lane that crashed.Found while sweeping for red default branches: this was one of seven red out of 1877, and one of only four that were not a stale Renovate lane.