Skip to content

Crash (index out of range) starting a VM after its displays are removed: stale Metal display window is reused #7882

Description

@taggie313

Describe the issue

UTM crashes (EXC_BREAKPOINT, Swift "Index out of range") about a second after starting a QEMU VM whose display list has been emptied with AppleScript update configuration, if that VM was started with a display earlier in the same UTM session. The VM being started goes down with it.

What happens, from the crash report and the source (the crash is from 4.7.5; the code paths involved are unchanged on main at 5534ffa):

  1. When a VM stops, its primary display window is kept. VMDisplayWindowController.virtualMachine(_:didTransitionToState:) only calls enterSuspended for .stopped (VMDisplayWindowController.swift:385).
  2. UTMData.run(vm:) reuses vmWindows[vm] when it exists, and only picks the window type (Metal / terminal / headless) when it creates a new one (UTMDataExtension.swift:21-23). So a VM that is now headless gets its old VMDisplayQemuMetalWindowController back.
  3. On start, enterLive() sets ioServiceDelegate. -[UTMSpiceIO setDelegate:] then replays spiceDynamicResolutionSupportDidChange: on the main queue. That method runs guard displayConfig!.isDynamicResolution (VMDisplayQemuMetalWindowController.swift:253), and displayConfig is vmQemuConfig?.displays[id] (lines 49-51). With id == 0 and an empty displays, the subscript traps. The optional chaining doesn't guard an out-of-range index.

Steps to reproduce (no guest OS or disk needed)

tell application "UTM"
  set vm to make new virtual machine with properties {backend:qemu, configuration:{name:"display-repro", architecture:"aarch64"}}
  set c to configuration of vm
  set displays of c to {{hardware:"virtio-ramfb-gl"}}
  update configuration of vm with c
end tell
utmctl start display-repro          # display window opens
utmctl stop display-repro --force
osascript -e 'tell application "UTM"' -e 'set vm to virtual machine named "display-repro"' \
  -e 'set c to configuration of vm' -e 'set displays of c to {}' -e 'update configuration of vm with c' -e 'end tell'
utmctl start display-repro          # UTM crashes ~1 s later

Expected: the VM starts headless.
Actual: UTM crashes.

I've reproduced this three times with an identical crash signature (same UTM offsets, index 0, count 0): once on a Windows 11 VM, and twice on the throwaway VM above, the second time running the steps exactly as written here. In my tests, quitting UTM before the second start avoided it: a freshly launched UTM creates a headless session for the same VM, and it runs normally.

Possible fixes

  • Bounds-check the lookup, e.g. vmQemuConfig.flatMap { $0.displays.indices.contains(id) ? $0.displays[id] : nil }, and replace the force unwrap in spiceDynamicResolutionSupportDidChange(_:) with a guard let.
  • In run(vm:), drop a cached window that no longer matches the VM's config (for example a Metal display window for a VM that now has no displays), or close the primary window when a stopped VM's display list changes.

Configuration

  • UTM Version: 4.7.5 (the code paths above are unchanged on main at 5534ffa)
  • macOS Version: 27.0 (26A428)
  • Mac Chip (Intel, M1, ...): Apple M5 Max

Crash log

Crashing thread. The shipped binary is stripped, so I matched these offsets to functions by disassembly plus the ObjC metadata.

Exception Type:  EXC_BREAKPOINT (SIGTRAP)  brk #1
Thread 0 (main thread):
0  UTM +0x207fe8   VMDisplayQemuMetalWindowController.displayConfig.getter   <- array bounds check
1  UTM +0x20bb3c   VMDisplayQemuMetalWindowController.spiceDynamicResolutionSupportDidChange(_:)
2  UTM +0x20bdd0   @objc thunk for the above
3  UTM +0xa478     block in -[UTMSpiceIO setDelegate:]
4  libdispatch.dylib  _dispatch_call_block_and_release
...
x23 (index) = 0, x8 (displays.count) = 0

I'm happy to attach the full .ips if that's useful.

Debug log

Not attached: the steps above reproduce it deterministically.


Disclosure: this was investigated and written with an AI assistant (Claude Code), which ran the reproduction steps above on my Mac.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions