vz: startVM goroutine hot-loops on for { <-ctx.Done() } and deadlocks on unbuffered errCh after stop
- Dominant language
- Go
- Stars
- 21.9k
- Forks
- 957
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 53
Description
### Description
The state-change goroutine spawned by `startVM` in `pkg/driver/vz/vm_darwin.go` (lines 78–174) has the same shape of bug as #4891, plus an unbuffered-channel deadlock on the Stopped path:
```go
sendErrCh := make(chan error) // unbuffered
go func() {
defer func() { ... vmNetworkFiles ... }()
for {
select {
case <-ctx.Done():
logrus.Info("Context closed, stopping vm")
if machine.CanStop() {
_, err := machine.RequestStop()
logrus.Errorf("Error while stopping the VM %q", err) // unconditional
}
// no return — falls through to next iteration
case newState := <-machine.StateChangedNotify():
switch newState {
...
case vz.VirtualMachineStateStopped:
...
sendErrCh <- errors.New("vz driver state stopped") // unbuffered
...
}
}
}
}()
```
Three issues chain on every clean stop of a VZ instance (default driver on macOS):
1. **`<-ctx.Done()` does not return**, so once the parent (hostagent) cancels the driver context, the loop reruns the case body. `ctx.Done()` is closed at that point and `select` keeps picking it (or alternates with `StateChangedNotify`), spamming `level=info msg="Context closed, stopping vm"` plus `level=error msg="Error while stopping the VM \"\""` for the entire stop window.
2. **The `Errorf` is unconditional** — `RequestStop()` returning `nil` still produces a red `Error while stopping the VM ""` line, which is misleading on every clean stop.
3. **`sendErrCh` is unbuffered.** When the consumer in `(*HostAgent).startRoutinesAndWait` exits its outer `select` (because the user signalled, or the stop has already begun), nobody is draining `errCh`. The state-change handler reaches `VirtualMachineStateStopped` and then `sendErrCh <- errors.New("vz driver state stopped")` blocks forever — non-terminating goroutine outliving its context. The `defer` that closes `vmNetworkFiles` therefore never runs. The same blocking-send hazard exists on three other writers in the same goroutine (pidfile-collision, pidfile-write, and the inner `usernetClient.ConfigureDriver` failure path).
### Reproduction (deterministic)
```sh
limactl start default # VZ is the default macOS driver
tail -f ~/.lima/default/ha.stderr.log &
limactl stop default
```
In `ha.stderr.log` you observe, repeated for the duration of the stop window:
```
level=info msg="Context closed, stopping vm"
level=error msg="Error while stopping the VM \"\""
```
A `pprof goroutine?debug=2` snapshot taken just before the hostagent process exits shows one goroutine parked on `chan send` at the `sendErrCh <- errors.New("vz driver state stopped")` line.
This is structural, not timing-dependent — the loop has no exit condition and the sends are unbuffered with no `<-ctx.Done()` fallback.
### Fix
Mirror what was already done for the WSL2 driver in #4892:
1. `return` from the goroutine in the `<-ctx.Done()` arm and after the `Stopped` state is observed.
2. Buffer `sendErrCh` so simultaneous writers cannot deadlock.
3. Make every send context-aware via a `trySendErr` helper that drops the error once `ctx` is cancelled.
4. Guard `logrus.Errorf` for `RequestStop` with `if err != nil`.
PR follows.
### Environment
Affects every VZ instance (`vmType: vz`) on macOS — the default driver. Found by code inspection against `master`.
Contributor guide
Research direction
Read startVM in pkg/driver/vz/vm_darwin.go, then compare the corresponding WSL2 fix in #4892. Reproduce with limactl start default and limactl stop default while watching ha.stderr.log; done means clean stops no longer hot-loop or log a nil error, and the state-change goroutine cannot remain blocked on an error send.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- operating-systems
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100