The shape of it
A Virtualization.framework VM is built in one order. You declare a VZVirtualMachineConfiguration (CPU count, memory, a boot loader and a list of devices), validate it, and hand it to a VZVirtualMachine to run. Nothing starts until the whole configuration exists, and you can't add a device declared this way while the VM is running. The rest of this post is what goes into that device list, using Velo Workspaces' own configuration code as the example.
Boot loaders: two options, and why EFI wins for normal guests
Apple offers two ways to boot a Linux guest. VZLinuxBootLoader boots a kernel and initrd directly, with no boot menu. VZEFIBootLoader boots the way real hardware does, through EFI firmware, with a VZEFIVariableStore that keeps boot order and other NVRAM variables across reboots. Velo Workspaces uses EFI for every Linux guest: the variable store is created once per workspace and reloaded on every boot, like a real machine's NVRAM. That's what lets a distribution's own installer ISO or cloud image boot normally, GRUB and all, without you extracting a kernel first. The startup performance post covers what this costs over a workspace's lifetime.
Storage: three roles, not one device type
A guest's disks aren't interchangeable; the device type says what each one is for:
- The main disk is a
VZVirtioBlockDeviceConfigurationwrapping a read-writeVZDiskImageStorageDeviceAttachment. The framework only accepts raw images, so a cloud image downloaded as qcow2 is converted to a sparse raw disk when the workspace is created. - Installer media is attached read-only as
VZUSBMassStorageDeviceConfiguration, a virtual USB stick. There's no CD drive, which is why an ISO that can only boot from a CD needs a partition table added before the firmware will start it; Velo Workspaces writes one into its own copy of the ISO. - The cloud-init seed is a second, small read-only virtio block device: a FAT volume labelled
CIDATAcarrying theuser-dataandmeta-datathat set up your account on first boot. Virtio rather than USB, because some cloud kernels leave out USB storage drivers and every one of them has virtio.
The disk attachment has two settings worth knowing: cachingMode and synchronizationMode. Velo Workspaces sets the main disk to .cached on purpose. The default, .automatic, behaves as uncached in practice, which suits a long-running server but hurts what a VM app spends much of its time doing: an installer unpacking gigabytes of small files and rewriting the same metadata blocks. Caching absorbs that. synchronizationMode stays at .full, also on purpose, so the guest's own flushes stay durable and a sudden power loss costs no more than it would without caching. These are trade-offs against durability, not just speed knobs. The disk performance post compares this block device with a VirtioFS shared folder.
Networking: NAT, and a MAC address that isn't random
VZVirtioNetworkDeviceConfiguration with a VZNATNetworkDeviceAttachment gives the guest NAT networking: its own address behind the Mac's, with no presence on your LAN. Bridged networking is a different matter. It needs the com.apple.vm.networking entitlement, which is an approval from Apple, not a configuration flag.
Velo Workspaces derives each guest's MAC address from the workspace's UUID instead of letting the framework pick a random one. VZNetworkDevice doesn't report its live MAC address back to the app, so with a random address there'd be no reliable way to find a given guest's IP after a restart. A stable MAC means a stable DHCP lease. How Linux VM networking works covers the paths this produces, including reaching a guest from another computer.
Graphics: two device types, and only one does multiple monitors
This goes a layer deeper than the "no GPU for Linux" point in the fundamentals. It's about what each guest gets for a display:
| Guest | Device | What it gets |
|---|---|---|
| macOS | VZMacGraphicsDeviceConfiguration | A paravirtualized Metal pipeline with near-native GPU performance, up to four independent displays |
| Linux | VZVirtioGraphicsDeviceConfiguration | virtio-gpu: accelerated 2D and basic 3D composition for the desktop, and one scanout however many displays you declare |
The multi-display detail is easy to miss until you hit it: the framework only honors multiple displays for macOS guests. Ask a Linux guest for four and you get one.
Shared folders: one tag per folder, except on macOS
VZVirtioFileSystemDeviceConfiguration is the shared-folder device, backed by VirtioFS, and Linux and macOS guests use it differently. A Linux guest gets one device per shared folder, each with its own tag, mounted in the guest with mount -t virtiofs <tag> <mountpoint>. A macOS guest gets a single device with Apple's reserved automount tag and every folder packed into one VZMultipleDirectoryShare; adding a second, separately tagged device fails validation. The payoff on macOS 13 and later: folders appear under /Volumes/My Shared Files/ with no mount command.
Rosetta for Linux uses the same device type. VZLinuxRosettaDirectoryShare is a filesystem share with its own tag, offered only when VZLinuxRosettaDirectoryShare.availability is .installed on the Mac.
The rest of the device list
A full configuration declares several more virtio devices, each with one narrow job:
- Vsock (
VZVirtioSocketDeviceConfiguration): a socket channel between host and guest with no network stack in between. AI Bridge is built on it; see the architecture. - Entropy (
VZVirtioEntropyDeviceConfiguration): feeds the Mac's randomness into the guest, so a freshly booted guest isn't making keys from a cold random number generator. - Memory balloon (
VZVirtioTraditionalMemoryBalloonDeviceConfiguration): lets the host reclaim memory the guest isn't using. - Serial console (
VZVirtioConsoleDeviceSerialPortConfiguration): a raw text console that works independently of the graphical display. - Clipboard: another
VZVirtioConsoleDeviceConfiguration, carrying Apple's SPICE agent port (VZSpiceAgentPortAttachment), the supported way to sync the Mac's pasteboard with a guest's clipboard.
The gotcha that silently breaks saved state
This one doesn't announce itself. If nothing sets VZVirtualMachineConfiguration.platform, it defaults to a fresh VZGenericPlatformConfiguration(), and that default carries a new, random machineIdentifier every time the configuration is built. Restoring saved state checks that identifier against the one the state was saved with, just as it checks a macOS guest's hardware model. So an app that never sets .platform gives every run a new identity, and every saved state becomes unrestorable the moment it's written. Not sometimes: every time. The fix is to save the generated identifier on first creation and read it back on every later build, the same as the EFI variable store.
The lifecycle, and what's fast
VZVirtualMachine(configuration:) builds the VM and start() boots it. pause() and resume() are in-memory and take milliseconds, which makes them the right response to the Mac itself sleeping and waking. Saving a workspace's state to free its memory entirely is heavier: saveMachineStateTo(url:) writes the whole VM state to disk and refuses to run on a running VM, so it's pause, save, and later restoreMachineStateFrom(url:). Pause, resume, save and restore look alike from outside, but they're two mechanisms with very different costs.
Related reading: what a VM on Apple Silicon is, what determines VM performance on ARM, the architecture behind AI Bridge, and what's different about a macOS guest. For how these devices perform, see the deep dives on CPU, disk and network. Or download Velo Workspaces and try it yourself.