From d097519c05174ed2dd98251e9aafc4d38c6074f3 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 8 Jul 2026 19:13:33 +0200 Subject: [PATCH] Serialize concurrent writes to the streamed command's output writer runAndStreamAux funnels a command's stdout and stderr into a single cmdWriter (the command-log panel, or a buffer when output is suppressed) from two separate goroutines: stderr through the MultiWriter set on cmd.Stderr, and stdout through the onRun callback. Those goroutines wrote the shared writer without any synchronization, racing on the prefixWriter's prefixWritten flag and interleaving the two streams. Wrap the writer so its writes are serialized. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/commands/oscommands/cmd_obj_runner.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/commands/oscommands/cmd_obj_runner.go b/pkg/commands/oscommands/cmd_obj_runner.go index b70668431..74b7e721d 100644 --- a/pkg/commands/oscommands/cmd_obj_runner.go +++ b/pkg/commands/oscommands/cmd_obj_runner.go @@ -244,6 +244,10 @@ func (self *cmdObjRunner) runAndStreamAux( } else { cmdWriter = self.guiIO.newCmdWriterFn() } + // The command's stdout and stderr are streamed to cmdWriter concurrently + // from separate goroutines (stderr via the MultiWriter below, stdout via + // onRun), so it must be safe for concurrent writes. + cmdWriter = &synchronizedWriter{writer: cmdWriter} if cmdObj.ShouldLog() { self.logCmdObj(cmdObj) @@ -451,6 +455,20 @@ func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (Crede } } +// synchronizedWriter serializes writes to its underlying writer so that it can +// be written from multiple goroutines at once (see runAndStreamAux, which +// streams a command's stdout and stderr to one writer from two goroutines). +type synchronizedWriter struct { + mutex deadlock.Mutex + writer io.Writer +} + +func (self *synchronizedWriter) Write(p []byte) (int, error) { + self.mutex.Lock() + defer self.mutex.Unlock() + return self.writer.Write(p) +} + type Buffer struct { b bytes.Buffer m deadlock.Mutex