You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docker buildx build --load can crash the client with panic: send on closed channel in util/progress.(*Printer).Write when the local image load (LoadImage / "importing to docker") is still streaming progress after the build's top-level progress Printer has been closed. It is non-deterministic and only shows up on large images whose local load is slow enough to outlive the solve.
This is the same underlying defect as #3875, but on a different writer path. #3875 was closed by #3886, which reordered cleanup so the remote policy-resolver progress writer no longer outlives the printer. The LoadImage / --load writer (util/dockerutil) is not covered by that fix, and Printer.Write itself is still an unguarded channel send on master.
Version
Observed on a self-hosted CI runner using Docker's docker-buildx plugin. The racing code is unchanged on the latest release v0.35.0 and current master (verified by source inspection — see below), so this is not specific to an old version.
waitingWriter.Close() waits on its own w.done, so the load pipe is synchronized — but the goroutine writes into the build's top-level Printer, whose Wait() is called elsewhere in the build pipeline. When the local load is slow (large image), that Wait()/close(p.status) can happen while the "importing to docker" goroutine is still calling Printer.Write, and the send lands on a closed channel → panic.
This is exactly the "progress writer outlives the printer" class that #3886 fixed for the policy-resolver writer; the --load/LoadImage writer needs the same guarantee (or Printer.Write needs to stop panicking on a post-close write).
Suggested fix
Either (or both):
Make Printer.Write non-fatal after close, e.g.
func (p*Printer) Write(s*client.SolveStatus) {
select {
casep.status<-s:
case<-p.done:
return// printer already closed; drop late status
}
...
}
This makes any late writer (LoadImage, and future callers) safe rather than fixing one caller at a time.
Ensure the --load "importing to docker" goroutine is joined before the build's Printer.Wait() closes the channel, mirroring the cleanup reordering in build: fix policy resolver cleanup on errors #3886.
Impact
Non-deterministic red builds on --load of large images (multi-GB), even though the image built and every layer exported — the crash is purely in the client's progress plumbing. Happy to test a patch.
Summary
docker buildx build --loadcan crash the client withpanic: send on closed channelinutil/progress.(*Printer).Writewhen the local image load (LoadImage/ "importing to docker") is still streaming progress after the build's top-level progressPrinterhas been closed. It is non-deterministic and only shows up on large images whose local load is slow enough to outlive the solve.This is the same underlying defect as #3875, but on a different writer path. #3875 was closed by #3886, which reordered cleanup so the remote policy-resolver progress writer no longer outlives the printer. The
LoadImage/--loadwriter (util/dockerutil) is not covered by that fix, andPrinter.Writeitself is still an unguarded channel send onmaster.Version
Observed on a self-hosted CI runner using Docker's
docker-buildxplugin. The racing code is unchanged on the latest release v0.35.0 and current master (verified by source inspection — see below), so this is not specific to an old version.Stack trace
The log immediately before the panic shows the image load in progress (two multi-GB layers), which is what gives the race a wide enough window:
Root cause
Printer.Writesends onp.statuswith no guard (master,util/progress/printer.go):closeOnceonly makes the close idempotent; it does nothing to prevent aWritethat races the close.On
--load,LoadImagestreams "importing to docker" progress into that samePrinterfrom a goroutine (util/dockerutil/client.go):waitingWriter.Close()waits on its ownw.done, so the load pipe is synchronized — but the goroutine writes into the build's top-levelPrinter, whoseWait()is called elsewhere in the build pipeline. When the local load is slow (large image), thatWait()/close(p.status)can happen while the "importing to docker" goroutine is still callingPrinter.Write, and the send lands on a closed channel → panic.This is exactly the "progress writer outlives the printer" class that #3886 fixed for the policy-resolver writer; the
--load/LoadImagewriter needs the same guarantee (orPrinter.Writeneeds to stop panicking on a post-close write).Suggested fix
Either (or both):
Make
Printer.Writenon-fatal after close, e.g.This makes any late writer (LoadImage, and future callers) safe rather than fixing one caller at a time.
Ensure the
--load"importing to docker" goroutine is joined before the build'sPrinter.Wait()closes the channel, mirroring the cleanup reordering in build: fix policy resolver cleanup on errors #3886.Impact
Non-deterministic red builds on
--loadof large images (multi-GB), even though the image built and every layer exported — the crash is purely in the client's progress plumbing. Happy to test a patch.