Description
In pkg/backend/build/builder.go:413-430, splitReader() spawns a goroutine that copies from the original reader to a MultiWriter backed by two PipeWriters. If either pipe reader is not fully consumed (e.g., the interceptor fails or abandons the stream early), the goroutine blocks indefinitely on the write operation.
Code
go func() {
defer w1.Close()
defer w2.Close()
_, err := io.Copy(multiWriter, original) // blocks if either reader abandoned
if err \!= nil {
w1.CloseWithError(err)
w2.CloseWithError(err)
}
}()
Impact
- Goroutine leak: blocked goroutine is never reclaimed
- The second reader (
itReader) is passed to an interceptor that may fail
- If the interceptor abandons the reader, the goroutine hangs, also blocking the first reader
Fix suggestion
Add context-based cancellation so the goroutine can exit when a reader is abandoned:
go func() {
defer w1.Close()
defer w2.Close()
buf := make([]byte, 32*1024)
for {
select {
case <-ctx.Done():
w1.CloseWithError(ctx.Err())
w2.CloseWithError(ctx.Err())
return
default:
}
n, err := original.Read(buf)
if n > 0 {
if _, wErr := multiWriter.Write(buf[:n]); wErr \!= nil {
w1.CloseWithError(wErr)
w2.CloseWithError(wErr)
return
}
}
if err \!= nil {
if err \!= io.EOF {
w1.CloseWithError(err)
w2.CloseWithError(err)
}
return
}
}
}()
Severity
Major — goroutine leak, potential deadlock in build pipeline.
Description
In
pkg/backend/build/builder.go:413-430,splitReader()spawns a goroutine that copies from the original reader to aMultiWriterbacked by twoPipeWriters. If either pipe reader is not fully consumed (e.g., the interceptor fails or abandons the stream early), the goroutine blocks indefinitely on the write operation.Code
Impact
itReader) is passed to an interceptor that may failFix suggestion
Add context-based cancellation so the goroutine can exit when a reader is abandoned:
Severity
Major — goroutine leak, potential deadlock in build pipeline.