Skip to content

fix(ui): prevent crash when window size is zero#7656

Open
ty-yqs wants to merge 1 commit intoutmapp:mainfrom
ty-yqs:issue_7650
Open

fix(ui): prevent crash when window size is zero#7656
ty-yqs wants to merge 1 commit intoutmapp:mainfrom
ty-yqs:issue_7650

Conversation

@ty-yqs
Copy link
Contributor

@ty-yqs ty-yqs commented Mar 20, 2026

Description

This PR addresses a critical issue reported in #7650 where setting a VM window's bounds to {0, 0, 0, 0} (e.g., via AppleScript or automation) causes the application to crash. This crash would forcefully terminate all running virtual machines, potentially leading to data loss.

The root cause was that the standard NSWindow allows setting a zero frame size, which subsequently causes failures in the layout or rendering pipeline (e.g., coordinate calculations resulting in NaN or division by zero).

Changes

  • New File: Platform/macOS/Display/VMDisplayWindow.swift
    • Implemented a custom NSWindow subclass named VMDisplayWindow.
    • Added sanitizeFrame(_:) logic to enforce a minimum safe window size (400x300) and prevent invalid dimensions (width/height <= 0).
    • Overrode setFrame methods to intercept all resize attempts and apply sanitized bounds.
    • Added logging to warn developers/users when invalid window bounds are attempted.
  • Modified: Platform/macOS/Display/Base.lproj/VMDisplayWindow.xib
    • Updated the window class from NSWindow to the custom VMDisplayWindow to activate the safe resizing logic.

Testing Instructions

  1. Launch UTM and start a virtual machine.
  2. Open Script Editor.app and run the following AppleScript (replace "vmname" with your VM's window title):
    tell application "UTM"
        set bounds of window "vmname" to {0, 0, 0, 0}
    end tell
  3. Expected Result:
    • The app does not crash.
    • The VM window resizes to the minimum allowed size (400x300).
    • A warning is logged in the console: "Attempted to set invalid window width...".

Related Issue

Fixes #7650

Copilot AI review requested due to automatic review settings March 20, 2026 08:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a macOS NSWindow subclass to sanitize invalid VM display window frame sizes (notably {0,0,0,0} via AppleScript) to prevent UI crashes that can terminate running VMs.

Changes:

  • Introduces VMDisplayWindow (NSWindow subclass) that clamps window frames to a minimum size during setFrame calls and logs warnings for invalid dimensions.
  • Updates VMDisplayWindow.xib to use VMDisplayWindow as the window’s custom class so the sanitization applies at runtime.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
Platform/macOS/Display/VMDisplayWindow.swift Adds NSWindow subclass that clamps frame size and logs invalid resize attempts.
Platform/macOS/Display/Base.lproj/VMDisplayWindow.xib Switches the window in the VM display nib from NSWindow to VMDisplayWindow.

Comment on lines +59 to +66
override var minSize: NSSize {
get {
return NSSize(width: kMinWindowWidth, height: kMinWindowHeight)
}
set {
super.minSize = newValue
}
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overriding minSize to always return the hard-coded 400x300 breaks existing callers that intentionally set window.minSize at runtime (e.g. VMDisplayAppleDisplayWindowController.updateWindowFrame() sets window.minSize based on content/dynamic resolution). AppKit will consult this overridden getter, so the effective minimum size becomes fixed and those code paths can no longer raise the minimum when needed. Consider removing this override and instead (a) set super.minSize to the base minimum in awakeFromNib/init, and (b) when sanitizing frames, clamp to max(super.minSize, baseMinimum) so higher minimums set by controllers are still respected.

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +45
private let kMinWindowWidth: CGFloat = 400
private let kMinWindowHeight: CGFloat = 300

private func sanitizeFrame(_ frameRect: NSRect) -> NSRect {
var frame = frameRect

// Check for invalid or too small dimensions
if frame.size.width < kMinWindowWidth {
// Log warning only if it's significantly invalid (e.g. 0 or negative)
if frame.size.width <= 0 {
logger.warning("Attempted to set invalid window width: \(frame.size.width). Clamping to \(kMinWindowWidth).")
}
frame.size.width = kMinWindowWidth
}

if frame.size.height < kMinWindowHeight {
if frame.size.height <= 0 {
logger.warning("Attempted to set invalid window height: \(frame.size.height). Clamping to \(kMinWindowHeight).")
}
frame.size.height = kMinWindowHeight
}

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sanitizeFrame(_:) clamps against the hard-coded constants rather than the window’s current minSize. This can conflict with runtime minimum sizes that other parts of the app set on the window (for example to enforce an aspect ratio or content-driven minimum). Consider clamping against max(baseMinimum, super.minSize) so the crash fix still applies while preserving larger minimum sizes configured elsewhere.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +41
// Check for invalid or too small dimensions
if frame.size.width < kMinWindowWidth {
// Log warning only if it's significantly invalid (e.g. 0 or negative)
if frame.size.width <= 0 {
logger.warning("Attempted to set invalid window width: \(frame.size.width). Clamping to \(kMinWindowWidth).")
}
frame.size.width = kMinWindowWidth
}

if frame.size.height < kMinWindowHeight {
if frame.size.height <= 0 {
logger.warning("Attempted to set invalid window height: \(frame.size.height). Clamping to \(kMinWindowHeight).")
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sanitizeFrame(_:) doesn’t handle NaN/±infinity dimensions: comparisons like width < kMinWindowWidth are false for NaN, so an invalid incoming frame can bypass clamping and still propagate into AppKit. If the goal is to prevent crashes from invalid geometry, treat non-finite sizes as invalid and clamp them to the minimum as well (e.g. check isFinite before comparing).

Suggested change
// Check for invalid or too small dimensions
if frame.size.width < kMinWindowWidth {
// Log warning only if it's significantly invalid (e.g. 0 or negative)
if frame.size.width <= 0 {
logger.warning("Attempted to set invalid window width: \(frame.size.width). Clamping to \(kMinWindowWidth).")
}
frame.size.width = kMinWindowWidth
}
if frame.size.height < kMinWindowHeight {
if frame.size.height <= 0 {
logger.warning("Attempted to set invalid window height: \(frame.size.height). Clamping to \(kMinWindowHeight).")
// Check for invalid or too small dimensions, including non-finite values
let width = frame.size.width
if !width.isFinite || width < kMinWindowWidth {
// Log warning only if it's significantly invalid (e.g. non-finite, 0 or negative)
if !width.isFinite || width <= 0 {
logger.warning("Attempted to set invalid window width: \(width). Clamping to \(kMinWindowWidth).")
}
frame.size.width = kMinWindowWidth
}
let height = frame.size.height
if !height.isFinite || height < kMinWindowHeight {
if !height.isFinite || height <= 0 {
logger.warning("Attempted to set invalid window height: \(height). Clamping to \(kMinWindowHeight).")

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UTM crash if VM window resized to {0, 0, 0, 0}

2 participants