Skip to content

fix: guard the shared log writers and mask list against concurrent access - #6153

Open
McNultyyy wants to merge 1 commit into
nektos:masterfrom
McNultyyy:act-split/1-log-writer-races
Open

fix: guard the shared log writers and mask list against concurrent access#6153
McNultyyy wants to merge 1 commit into
nektos:masterfrom
McNultyyy:act-split/1-log-writer-races

Conversation

@McNultyyy

Copy link
Copy Markdown

Three data races that exist on master today, fixed with no behaviour change.

1. ptyWriter.AutoStop

HostEnvironment.exec starts copyPtyOutput in a goroutine, which calls ptyWriter.Write for every chunk of output and reads AutoStop each time. Once the command returns, the executing goroutine sets writer.AutoStop = true while that reader is still running. It becomes an atomic.Bool.

This one is unconditional on --self-hosted. The new TestPtyWriterAutoStopRace reproduces it reliably: it reports WARNING: DATA RACE under -race on master and passes with this change.

2. The log writer slots

containerReference.ReplaceLogWriter swaps input.Stdout / input.Stderr, which waitForCommand and the attach path read while copying container output. HostEnvironment.ReplaceLogWriter swaps StdOut while a command may be running. Both swaps are now guarded, and the readers take a consistent snapshot through an accessor rather than reading the fields directly.

3. RunContext.Masks

AddMask appends from a running step while the log formatter iterates the same slice for every emitted line (valueMasker). Composite run contexts made this worse by aliasing the parent's slice header and appending to it. Appends and reads now take a lock, and composite contexts copy instead of aliasing.

This is the same family as #2199, which d8506bf only partially addressed — but note it does not fix that issue's most recently reported stack, which is map iteration over shared model structs in NewExpressionEvaluatorWithEnv. Referencing, not closing.

Testing

go test -race ./pkg/container/... ./pkg/runner/...

Only the ptyWriter race has a deterministic reproduction, which is included. The other two need a specific interleaving of a step's output with a writer swap and I could not make them fail reliably, so they are argued from the code rather than from a red test.

Trade-offs worth flagging

  • masksMutex is a package-level global guarding per-RunContext slices, which is coarser than ideal. Happy to move it onto RunContext if you prefer.
  • valueMasker now holds a read lock for the duration of the mask loop, which is on the path of every emitted log line. The alternative is copying the slice per line, which allocates instead. Either is defensible; say which you would rather have.

🤖 Generated with Claude Code

…cess

Three data races that are reachable today, with no behaviour change:

* ptyWriter.AutoStop is written by the goroutine running the command
  once it finished, while the goroutine copying the pty output reads it
  on every write. It becomes an atomic.Bool. The new test reproduces
  this reliably under -race.
* containerReference.ReplaceLogWriter swaps two fields that the
  goroutines copying container output read concurrently, and
  HostEnvironment.ReplaceLogWriter swaps StdOut while a command is
  running. Both are now guarded, and the readers take a consistent
  snapshot through an accessor.
* RunContext.Masks is appended to by a running step while the log
  formatter iterates it for every emitted line, and composite run
  contexts aliased the parent's slice and appended to it. Appends and
  reads now take a lock, and composite contexts copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant