lima-vm / lima-vm/lima

vz: startVM goroutine hot-loops on for { <-ctx.Done() } and deadlocks on unbuffered errCh after stop

Open
#4,921 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.