The helper's hand-written Info.plist declared its extension point the way
an App Store upload was rejected for in Apple forums thread 803896 (ITMS-90349,
invalid EXExtensionPointIdentifier); Apple's fix was the documented helper
extension setup, which binds in code and lets Xcode generate the Info.plist
instead. Build settings in the binding are expanded, so UTM and UTM SE
still bind to their own host with any bundle prefix.
Assisted-by: Claude:claude-opus-5-5
visionOS installs extensions only for Apple's extension points, so an app
carrying the helper for our own qemu-helper extension point fails to install.
Build and embed the helper only for iOS and report it unsupported on
visionOS, which hides the qemu-img features as on iOS before 26.
Assisted-by: Claude:claude-opus-5-5
The iOS-SE scheme has run with Address Sanitizer since the schemes were
first committed to set the architecture override, which was incidental.
An ExtensionKit extension linked with the sanitizer does not start on a
device, so every qemu-img operation of UTM SE failed when run from
Xcode. Turn it off, add the schemes Xcode created for the helper targets
so both can be built and debugged on their own, and take the Metal API
validation setting Xcode 27 writes into the macOS scheme.
Assisted-by: Claude:claude-fable-5-1
QEMU tools cannot run in the app process on iOS: a process may load QEMU
only once and qemu-img calls exit(). So disk snapshots of a stopped VM,
reclaiming space, compression, resizing and conversion on import were
macOS-only.
iOS 26 brings ExtensionKit, so qemu-img now runs in an ExtensionKit
extension (iOSHelper, and iOSHelper-SE for UTM SE since an extension's
bundle identifier must be prefixed by its host's). The helper speaks the
same QEMUHelperProtocol as the macOS XPC helper but loads the tool into
its own process, one tool per process. UTMProcess accepts an injected
connection so the remote path is shared with macOS, including file
access through bookmarks, whose implicit security scope grants the
resolving process access. UTMHelperProcess launches the helper and,
because the system hands a process that is still exiting to a launch
that comes too soon, tears it down with a handshake on the interruption
callback and runs helpers one at a time.
UTMQemuImage now reads the tool's output until it ends instead of
racing the exit message, resumes its continuations off the XPC reply
queue (a call made from that queue blocks in NSXPC), and stops the
process explicitly since the connection's exported object kept it from
ever being deallocated on macOS. The last stderr line becomes the error
text when the tool gives none.
Assisted-by: Claude:claude-fable-5-1
The macOS notes box was a TextEditor with a hand-drawn half-point
border, which did not match the bezel of the name field above it and
had no focus ring. A TextField with a vertical axis has the right look
but submits on Return instead of inserting a newline, and a drawn
overlay cannot follow the bezel style of each macOS release.
Wrap an NSTextField configured for wrapping instead, so AppKit draws
the same bezel, highlight and focus ring as the name field on every
supported macOS version. Return is routed through the field editor
to insert a newline, the field grows with its contents from a 200pt
minimum, and the page's disableAutocorrection setting is applied to
the field editor as the TextEditor did. iOS keeps the TextEditor.
Assisted-by: Claude:claude-fable-5-1
The QEMU settings page listed UTM's generated arguments next to the
editable custom ones in a single list, which made users think the
generated arguments could be changed and invited pasting options found
online that do not apply to UTM's build of QEMU.
Move custom arguments to their own page, edited through a popover on
macOS and a pushed page on iOS like port forwarding, instead of inline
text fields that needed SwiftUI workarounds. On macOS the page is hidden
behind a checkbox on the QEMU page that is only on while the settings
sheet is open unless the VM already has custom arguments; a short
disclaimer sits under the checkbox and under the iOS row. The complete
command line is shown as read-only selectable text, so the export
command feature is removed.
Assisted-by: Claude:claude-fable-5-1
Keystrokes typed into a QEMU VM's built-in terminal never reached the
guest. SPICE sends port writes by reference after CSPort returns, and
since the CocoaSpice update in cebbab9d drains an autorelease pool on
every SPICE loop iteration, the bytes were freed before they were sent.
Pull in the CocoaSpice fix that keeps the data alive until the write
completes.
Fixes#7814
Assisted-by: Claude:claude-opus-5-5
SwiftTerm 2 no longer exposes the underlying Terminal from TerminalView, so
move the terminal views to the copied view APIs: terminalDimensions,
resize(cols:rows:), setCursorStyle, softReset and resetToInitialState. The
view's resize also soft resets the terminal now, so only call it when the
saved size differs, otherwise every resume would reset the guest's modes.
Track SwiftTerm from the osy/SwiftTerm fork's fix/visionos-build branch
for now: upstream main reads UIWindow.screen, which does not exist on
visionOS, and the fork reads the display scale from the trait collection
instead so the visionOS build works again.
SwiftTerm now ships a build tool plugin, which needs two changes to
build_utm.sh for CI. Xcode refuses to run a package plugin the machine has
not trusted, so pass -skipPackagePluginValidation. And -sdk also applies
to host tools, so the plugin's generator was built for the target platform
instead of the Mac and the build could not find it; pick the platform
with a generic destination and the architectures with ARCHS instead.
Also match the SDK name macosx when picking the default macOS scheme.
Take advantage of what the update brings:
- Render with Metal on macOS and iOS, falling back to Core Graphics if the
GPU pipeline cannot be created. visionOS stays on Core Graphics because
SwiftTerm's Metal path draws at the wrong scale there.
- Release the renderer with updateUiClosed() when the terminal window
closes or the iOS view is dismantled, as SwiftTerm now requires.
- Feed guest output straight from the serial port thread; feed() is
thread-safe now, so the hop to the main queue is no longer needed.
- Add the standard Edit > Find menu on macOS, which the terminal's new
find bar is driven from.
- Open only http(s) links from the terminal. SwiftTerm now also turns host
file paths printed by the guest into links, and its default handler on
macOS opens any URL or existing host path, so clicking one could launch
a host app. On iOS, let the bell use SwiftTerm's default haptic instead
of an empty override.
Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-opus-5-5
The snapshot commands could only name a snapshot and could not say anything
about it. A script could not tell when a snapshot was taken, how much it
holds, whether restoring it resumes the guest or starts it up, or whether
its data has gone missing, and there was no way to rename one or to throw
away a suspended state, both of which the window offers.
Make a snapshot an object instead. A virtual machine now has snapshots as
elements, each carrying those details, so a script can pick one out and
read it:
tell application "UTM"
set vm to virtual machine named "Debian"
set candidates to (snapshots of vm whose includes running state)
restore (item 1 of candidates)
end tell
Renaming is setting the name, and the commands that act on a snapshot take
the snapshot itself. Creating one returns it. Two more commands cover what
the window can do and scripting could not: scanning the images for
snapshots made by other tools, and discarding a saved state so the guest
starts up instead of resuming.
This replaces the snapshot commands of the 5.0.5 beta, which addressed a
snapshot by name through its virtual machine, so scripts and utmctl calls
written against them have to be updated.
Snapshots are addressed by name, so names are now kept apart the way
virtual machine names are, by appending a number.
Deleting a snapshot is its own command rather than the standard delete,
which coerces a specifier for anything inside a virtual machine into the
virtual machine itself; deleting that would throw away far more than was
asked for. The delete command refuses such a specifier for the same reason.
UTMScripting.swift is regenerated from the sdef; only the hunks that
changed are taken.
Assisted-by: Claude:claude-opus-5
Apple virtualization reported snapshots as unsupported. It has no notion of
them, but the pieces are there: the disks can be layered and the saved
state and firmware storage are ordinary files.
Add a backend that freezes a layer per drive and keeps copies of the saved
state, the EFI variables and the macOS auxiliary storage beside them. The
layers under a disk that is attached cannot be changed, so a running VM may
only delete; everything else waits until it stops.
A layer sits beside the image it belongs to and is named after it, and the saved
state and firmware of a snapshot are copies named the same way. A bundle only
keeps the files its configuration names, so saving the configuration would take
all of them with it. Work out what the snapshots still need instead — the layer
being written to, the layers of the snapshots in the bundle, and whatever lies
underneath those — and keep exactly that, so anything left over is still cleaned
up. When that cannot be worked out, because the manifest or a layer cannot be
read, everything named after the file is kept instead.
Assisted-by: Claude:claude-fable-5-1
Assisted-by: Claude:claude-opus-5
Snapshots were read straight out of the QEMU images by the scripting
commands, which only worked while the VM was off and knew nothing beyond
the tags in the image.
Put a backend behind the operations instead. Which one is chosen depends on
whether the VM is running, and each says what it can do in that state:
- A running VM goes through the QEMU monitor, which saves and loads the
whole machine along with the disks.
- A VM that is off goes through qemu-img, which can only save and revert
disk contents. Nothing may touch the images while QEMU has them open, so
a snapshot of disks alone is left alone until the VM stops.
Either way a snapshot spans every writable image, the UEFI variable store
included, because QEMU refuses to load one that any of them is missing. A
snapshot that some of them lack, such as one taken before a drive was added,
is kept but cannot be restored, which is not the same as its state being
gone: reverting only some of the drives would leave them at different points
in time. Reclaiming space rebuilds an image without its snapshots, so it is
refused while there are any.
UTMSnapshotService sits above the backend and keeps the manifest and the
stored state consistent: it marks entries whose state has disappeared when
an operation fails, and the first time a VM is opened it adopts whatever
snapshots are already in the images so that snapshots taken by other tools
are not lost. Both wait until the VM is stopped, because a running QEMU
lists the snapshots of every drive together, removable ones included.
Suspending is folded into the same mechanism. A VM now suspends to a
snapshot of its own rather than always the same name, so a suspended state
can be kept as a snapshot without copying it, and restoring a snapshot
makes the VM resume from it. The state is deleted once nothing refers to
it, where it used to be left behind in the image. One that cannot be deleted
right away stays listed in the manifest and is tried again the next time.
Assisted-by: Claude:claude-fable-5-1
Assisted-by: Claude:claude-opus-5
A snapshot has a title, a date, a size, a screenshot and the snapshot it
was based on. None of that fits in the image formats the backends use, so
keep it in a new `snapshots.plist` in the VM bundle where each entry links
to the state in the backend by identifier.
The file also remembers which snapshot the current state came from and
which state the VM resumes from while it is suspended, along with the
states that are no longer needed but could not be deleted yet.
Assisted-by: Claude:claude-fable-5-1
Assisted-by: Claude:claude-opus-5
Snapshots could only be managed through scripting. Split the details view
into tabs (a tab view on macOS and a segmented control elsewhere, following
the HIG) and add a Snapshots tab next to the existing details.
The tab lists the current state followed by every snapshot, newest first,
along a timeline. A snapshot never changes unless it is overwritten, and
restoring one replaces the current state with a copy, so the actions say as
much: the current state can be saved as a new snapshot or overwrite the one
it is based on, and a snapshot can be restored or deleted. Anything that
loses data asks first.
A snapshot is based on whatever the current state was based on when it was
created. One that follows what it is based on is joined to it so a lineage
without branches reads as a single line. A base that is elsewhere in the
list is named by a link instead, which keeps branches of any depth from
widening the list. The lineage of the focused row is highlighted and only
that row shows its actions. Rows choose their layout from the measured
width because a regular size class can still be narrow next to the sidebar.
The operations only modify an in-memory list for now and no snapshots are
read from the VM yet.
Whether a snapshot holds the running machine decides whether restoring it
resumes the guest or starts it up, so the mark on the timeline is filled for one
that does and hollow for one that holds only the disks.
Assisted-by: Claude:claude-fable-5-1
On iOS 26 the system no longer draws a backdrop behind the input
accessory and the keyboard became a rounded glass panel with flat keys,
which left the old shadowed keys floating over the VM display with
nearly invisible paste and hide buttons.
Replace the XIB with a programmatic view that keeps the previous look on
iOS 18 and earlier and draws keyboard-style keys on a glass bar on
iOS 26 and later. Standard glass takes its brightness from the guest
display behind it and its tint renders differently on iOS 26 and 27, so
clear glass filled with the keyboard's color is used to follow the
keyboard's appearance on both. Keys with word labels now size to their
label instead of shrinking the font.
The localized accessibility labels move from the XIB strings to
Localizable.strings. The forward delete key was mislabeled "Right" and
the toggled state of modifiers was not reported to VoiceOver. visionOS
has its own keyboard window and no longer builds the accessory.
Assisted-by: Claude:claude-fable-5-1
A full screen VM on a Mac with a camera housing loses the strip beside
it, which the guest could otherwise use. Two things keep a native full
screen window out of that strip: AppKit insets the full screen frame and
the window server covers the strip with an opaque menu bar. There is no
public way around either, and a borderless window is not an option
because macOS would no longer treat the VM as a full screen app.
Add an opt-in preference (macOS 27+) and a window subclass that returns
the whole screen as the full screen frame and makes the menu bar of its
own full screen space transparent while it is not revealed. When the menu
bar is revealed it looks and behaves as it always did. It works within
the App Sandbox.
All of it relies on private AppKit and SkyLight symbols. None of them are
used or hooked until the option is enabled and a window enters full
screen, so the window is unaffected by changes to them otherwise. They
are looked up at runtime by name and checked against the signatures we
expect, so no private symbol is linked or compiled into the binary and
if any are missing or different the window behaves like any other.
The window server reveals the menu bar as soon as the pointer is over the
strip, which would make that area of the guest display unusable. It is
instead only revealed when the pointer reaches the top edge of the
screen: until then the space uses presentation options that hide the menu
bar, which also hides the Dock. AppKit applies the options of the space
again when it changes spaces, so they are kept up to date.
The window server only lets a window cover the strip if it does so from
the moment it enters the full screen space, which shapes the rest:
- Every frame query in the transition is answered with the whole screen,
and later requests to resize the window are ignored, because asking
for any frame again, even the current one, moves it out of the strip.
Our window controllers do so to fit the guest display, and AppKit does
when the space becomes active again.
- Once the strip has been left, such as when tiled in Split View, it is
not asked for again until full screen is next entered.
- The QEMU window is no longer resized to fit a display the guest just
changed while the transition to full screen is still running. The flag
the controller kept for that is only set once the transition is over,
so the window ended up taller than the screen and lost the strip.
- AppKit parks the auto hidden toolbar behind the menu bar, or over our
content while the menu bar is hidden, so it is hidden along with it.
The window stands down when tiled, on a screen without a camera housing,
and when the menu bar is set to always be visible in full screen.
Terminal windows do not opt in.
Resolves#6183
Assisted-by: Claude:claude-fable-5-1
Assisted-by: Claude:claude-opus-5
ASIF drives for Apple Virtualization are created, inspected and resized
through the private DiskImages2 framework, loaded by path and called via
NSInvocation. macOS 27 ships DiskImageKit, a public Swift-only framework
that covers the same operations and is what Virtualization.framework now
consumes directly.
Add UTMAppleDiskImage, a Swift wrapper that uses DiskImageKit on macOS 27
and later and falls back to the existing UTMASIFImage helper on macOS 13
through 26. The legacy path is confined to one private extension so it
can be deleted together with UTMASIFImage once the minimum host is
macOS 27. DiskImageKit is weak linked because the deployment target is
below macOS 27.
Callers switch to the wrapper; the resize path no longer force-unwraps
the helper.
Assisted-by: Claude:claude-fable-5-1
Apple Virtualization's existing Shared Network uses a separate NAT
attachment per VM, so guests cannot use it to reach each other. macOS 26
adds VZVmnetNetworkDeviceAttachment, which lets multiple VMs attach to the
same process-owned vmnet network.
Add two macOS 26+ modes:
- "Shared Network (VM to VM)": guests share a NAT-backed vmnet network and
can reach each other, the host, and external networks.
- "Host Only": guests share a host-only vmnet network and can reach each
other and the host without external routing.
UTMAppleVmnetNetworkManager keeps a weak attachment cache per mode while
Virtualization configuration and runtime objects own the attachment and its
native network. The network therefore follows the actual VZ object lifetime
rather than UTM's presentation state, including failure paths where a VZ VM
can remain alive after UTM reports it stopped. Configuration validation uses
a side-effect-free placeholder so saving a VM does not create a live vmnet
network.
Hide both modes before macOS 26 and add matching scripting enumerators and
round-trip parsing.
Resolves#6975.
Related to #7418 and #7380.
Assisted-by: Claude:claude-fable-5-1
Assisted-by: ChatGPT:GPT-6-Pro
Assisted-by: ChatGPT:GPT-5.6-Sol
Apple Virtualization VMs could not share host USB devices. macOS 27 adds
public APIs for this: AccessoryAccess.framework reports devices the user
assigned to UTM in the system "Virtual Machine Accessories" menu bar item and
VZUSBPassthroughDevice captures them, so the private API approach from the
abandoned branch is no longer needed.
Devices are reported once per process to UTMAppleUSBManager. A newly assigned
device is connected to the only running VM, or to the VM whose window is in
front when several are running, so the user does not have to select it twice.
The USB menu of the Apple VM window lists assigned devices and can move a
device from another VM after confirmation. A device the user disconnects from
the menu stays assigned to UTM but is not connected again until asked, since
the system reports it again right after it is detached. The VM label is set so
the system menu names the VM.
Connected devices are recorded with their UUID when the VM state is saved and
added to the configuration before the state is restored, so the guest keeps
the device across a suspend. A device that is missing at restore is skipped
and the guest sees it unplugged; the restore itself still succeeds.
Requires the com.apple.developer.accessory-access.usb entitlement.
Assisted-by: Claude:claude-fable-5-1
Since Xcode 26.1, actool ignores an .appiconset that shares its name with
an Icon Composer .icon and renders the icons for older OS versions from the
.icon instead. The undocumented --enable-icon-stack-fallback-generation flag
that used to turn this off is now parsed and discarded, so building with
Xcode 27 replaced our flat icons with generated ones on macOS 12-15, where we
prefer our own low resolution art.
Replacing AppIcon.icns is not enough because macOS reads the icon from
Assets.car before Tahoe as well. Instead compile AppIcon.appiconset into its
own catalog and merge its renditions into the built Assets.car with CoreUI,
leaving the icon stack renditions that macOS 26 uses in place.
The rendition format is undocumented, so the merge fails the build if the
catalog does not look the way we expect, and checks that the icons still
decode after writing.
Assisted-by: Claude:claude-opus-5[1m]
Xcode 27 no longer supports deploying to iOS 14 or macOS 11, and the
macOS 27 SDK breaks several dependency builds, so raise the minimum
versions and fix the dependency build:
- Bump IOS_SDKMINVER/MAC_SDKMINVER and the project deployment targets.
- Pass our deployment targets to the Hypervisor build, whose project
still targets iOS 14.
- Build ANGLE without -Werror; new clang warnings break it.
- Update MoltenVK to pick up the upstream minimum target bump and set
a visionOS minimum so it no longer inherits the SDK version.
- Patch QEMU apple-gfx to use PGCreateDeviceWithDescriptor, since
PGNewDeviceWithDescriptor is no longer exported by the macOS 27 SDK.
- Skip copying the IOKit headers on macOS where they are already in the
SDK and a second copy conflicts with the IOKit module.
- Export MACOSX_DEPLOYMENT_TARGET since libtool drops -target when
linking C++ libraries, which built libasprintf for macOS 27.
- Default CI to the installed Xcode.
Assisted-by: Claude:claude-opus-5
macOS treats Caps Lock as a locking modifier and never delivers its
physical release, so once "Caps Lock is treated as a key" is enabled a
guest that uses it as a held modifier (e.g. a screen reader's NVDA key)
never sees it let go.
While VM input is captured, remap Caps Lock to F20 on each attached
keyboard through IOHIDEventSystemClient (Apple TN2450) so macOS delivers
a normal key down/up pair, which VMMetalView translates back to the Caps
Lock scan code. The keyboard's previous mapping is put back when capture
ends, and a remap left behind by a crash is removed at the next launch.
F20 is the highest function key macOS delivers and is on no Apple
keyboard, so no physical key is shadowed.
Fixes#7838
Assisted-by: Codex:gpt-5
Assisted-by: Claude:claude-fable-5-1
A downloaded VM archive is attacker-controlled, so an entry named
`some.utm/../../elsewhere` must not be able to write outside the package
it claims to belong to. The extraction path was built by substituting the
archive's .utm prefix out of the entry path and appending the remainder,
and `appendingPathComponent()` does not resolve `..` -- the traversal was
only resolved by the filesystem at write time, landing anywhere in the
app container the depth reached.
Derive the relative path by dropping the prefix, and resolve every
destination through a containment check before extracting. Separators are
collapsed in a loop because `fopen()` and `URL.standardized` disagree on
a path such as `/../elsewhere`, and a single collapse pass still lets
`///../elsewhere` through.
Also bump ZIPFoundation to 0.9.20 for its accumulated fixes. This file is
the only call site in the tree and the deprecated failable initializer it
uses is still present, so the bump is source compatible.
Reported-by: Vo Duc Thang (ugvxb)
Assisted-by: Claude:claude-fable-5-1
Expose named create, list, restore, and delete operations through AppleScript and utmctl so automation can manage QEMU full-VM snapshots. QEMU image metadata remains the source of truth, the internal suspend snapshot stays hidden and reserved, and Apple virtualization reports the feature as unsupported until it can preserve equivalent VM state reliably.
Assisted-by: Codex:gpt-5
The SPICE main loop thread never drained an autorelease pool, so every VM
session leaked its autoreleased Objective-C objects — one
MTLBlitCommandEncoder per gl_draw plus a CSCursor per session — pinning
~12 MB of IOAccelerator memory per session until UTM was quit. Pull in the
CocoaSpice fix that drains a pool on every main loop iteration; measured
flat at ~2.3 MB dirty IOAccelerator across repeated sessions, with zero
leftover encoder or cursor objects.
Assisted-by: Claude:claude-fable-5
- virglrenderer uses shmem for texture import/export so we need App Group for both iOS and macOS
- Add QEMURenderServer for new process model rendering (both Venus and Neptune)
- macOS: add DXMT and D3DMetal-Native backends
- We require 8G hostmem for Neptune but iOS currently fails > 256M
This should reduce the latency of scanout updates. This also moves
the logic of viewport scale/origin from CSDisplay to CSRenderer
meaning that if you have multiple mirrored displays, they can have
independent scales.