From 8353ff9ef8b876b76ec8620cdd05faa2c0a3bbda Mon Sep 17 00:00:00 2001 From: ikshantshukla123 Date: Mon, 17 Aug 2026 01:24:16 +0530 Subject: [PATCH] fix(output): synchronize concurrent writes to the prefixed writer WrapWriter uses the same prefixWriter for both stdout and stderr, and os/exec can write to both streams concurrently. Since bytes.Buffer is not safe for concurrent use, this could corrupt its state and cause a "slice bounds out of range" panic. Add a mutex to prefixWriter and lock access in both Write and Close so only one goroutine uses the buffer at a time. Also add a regression test that writes many lines to stdout and stderr concurrently and verifies every line is correctly prefixed; it fails on the unfixed code and is reliably detected with -race. --- internal/output/output_test.go | 35 ++++++++++++++++++++++++++++++++++ internal/output/prefixed.go | 7 +++++++ 2 files changed, 42 insertions(+) diff --git a/internal/output/output_test.go b/internal/output/output_test.go index ba03c9adfa..401ff66c5a 100644 --- a/internal/output/output_test.go +++ b/internal/output/output_test.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "io" + "strings" + "sync" "testing" "github.com/fatih/color" @@ -190,3 +192,36 @@ func TestPrefixedWithColor(t *testing.T) { } }) } + +func TestPrefixedConcurrentWrites(t *testing.T) { + t.Parallel() + + var b bytes.Buffer + l := &logger.Logger{Color: false} + var o output.Output = output.NewPrefixed(l) + stdOut, stdErr, cleanup := o.WrapWriter(&b, &b, "prefix", nil) + + const lines = 1000 + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + for i := range lines { + fmt.Fprintf(stdOut, "stdout-%d\n", i) + } + }() + go func() { + defer wg.Done() + for i := range lines { + fmt.Fprintf(stdErr, "stderr-%d\n", i) + } + }() + wg.Wait() + require.NoError(t, cleanup(nil)) + + out := strings.Split(strings.TrimSuffix(b.String(), "\n"), "\n") + require.Len(t, out, lines*2) + for _, line := range out { + assert.True(t, strings.HasPrefix(line, "[prefix] ")) + } +} diff --git a/internal/output/prefixed.go b/internal/output/prefixed.go index fd2a230570..930008e3bf 100644 --- a/internal/output/prefixed.go +++ b/internal/output/prefixed.go @@ -38,9 +38,13 @@ type prefixWriter struct { prefixed *Prefixed prefix string buff bytes.Buffer + mutex sync.Mutex } func (pw *prefixWriter) Write(p []byte) (int, error) { + pw.mutex.Lock() + defer pw.mutex.Unlock() + n, err := pw.buff.Write(p) if err != nil { return n, err @@ -50,6 +54,9 @@ func (pw *prefixWriter) Write(p []byte) (int, error) { } func (pw *prefixWriter) close() error { + pw.mutex.Lock() + defer pw.mutex.Unlock() + return pw.writeOutputLines(true) }