buildAndStartGuest registered its listener close between the deferred
cleanups for the process and the binary. The close is unconditional, so
a successful launch whose ln.Close failed set err after the process and
connection cleanups had read err as nil and skipped, but before the
binary removal read it. The launch returned a live handle alongside an
error; both callers check the error first and drop the handle, leaking
the guest process, its connection and its session, with the binary
already unlinked underneath it.
Release the process and the connection from a single deferred cleanup
registered before either is acquired, so it runs after the listener
close and sees the final error, and clear the returned handle so an
error never comes back with one. Split the part that owns the listener,
the process and the connection into startGuest, leaving
buildAndStartGuest to own only the binary: the removal now runs in a
different frame from the listener close, so the two can no longer be
misordered.
Nothing closed the current guest at exit, so run removed the guests'
directory while a guest still held its binary open. That is harmless
where a running executable can be unlinked, but on Windows it fails and
the error surfaces from main. Close the guest and wait for every reaping
goroutine before the directory goes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>