How to Configure `VZVirtualMachineConfiguration` for iOS VMs: A Complete Guide
VZVirtualMachineConfiguration for iOS VMs requires assembling a VZMacPlatformConfiguration with a PV=3 hardware model, persistent machine identifier, auxiliary NVRAM storage, and specialized devices including multi-touch, Secure Enclave, and synthetic battery support.
Configuring Apple's Virtualization framework to run iOS requires precise setup that mirrors physical iPhone hardware. The open-source vphone-cli tool by Lakr233 demonstrates production-ready implementation in Swift, building a fully-featured iOS virtual machine through systematic VZVirtualMachineConfiguration assembly. This guide extracts the exact methodology from the source code, covering both minimal and complete configurations.
Core Configuration Steps
The vphone-cli project constructs iOS VM configurations through 19 logical steps in VPhoneVirtualMachine.swift. Each step maps to specific framework APIs and private extensions.
Step 1: Create the Hardware Model
All iOS VMs require a PV=3 hardware model generated through custom logic:
// VPhoneVirtualMachine.swift#L50-L52
let hwModel = try VPhoneHardware.createModel()
The VPhoneHardware.createModel() function in VPhoneHardware.swift produces a VZMacHardwareModel compatible with iPhone virtualization. This hardware model defines the virtual device's capabilities and must match your target iOS version.
Step 2: Configure Platform and NVRAM
The platform configuration binds hardware to persistent state:
// VPhoneVirtualMachine.swift#L106-L144
let platform = VZMacPlatformConfiguration()
platform.hardwareModel = hwModel
platform.machineIdentifier = resolvedIdentifier // Persistent ECID/UDID
platform.auxiliaryStorage = try VZMacAuxiliaryStorage(
creatingStorageAt: nvramURL,
hardwareModel: hwModel,
options: .allowOverwrite
)
VZMacAuxiliaryStorage maintains NVRAM variables across boots. The machineIdentifier must be persisted to config.plist via VPhoneVirtualMachineManifest.swift to ensure stable device identity.
Step 3: Set Up the Boot Loader
// VPhoneVirtualMachine.swift#L146-L151
let bootLoader = VZMacOSBootLoader()
if let romURL = options.romURL {
Dynamic(bootLoader)._setROMURL(romURL) // Private API for custom ROM
}
config.bootLoader = bootLoader
The VZMacOSBootLoader initiates the iOS kernel. Custom ROM injection requires private API access through the Dynamic wrapper.
Step 4: Configure CPU and Memory
Framework minimums must be respected with explicit clamping:
// VPhoneVirtualMachine.swift#L155-L159
config.cpuCount = max(requestedCPUs, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
config.memorySize = max(requestedMemory, VZVirtualMachineConfiguration.minimumAllowedMemorySize)
For iOS, typical production values are 4+ CPU cores and 4GB+ RAM, though these scale with host capabilities.
Essential Device Configurations
Beyond core settings, iOS VMs require specialized device configurations unavailable in standard macOS virtualization.
Graphics Display Configuration
Match iPhone screen parameters precisely:
// VPhoneVirtualMachine.swift#L61-L69
let graphics = VZMacGraphicsDeviceConfiguration()
graphics.displays = [
VZMacGraphicsDisplayConfiguration(
widthInPixels: 1290, // iPhone 15 Pro width
heightInPixels: 2796, // iPhone 15 Pro height
pixelsPerInch: 460 // Retina density
)
]
config.graphicsDevices = [graphics]
Audio Input/Output Streams
// VPhoneVirtualMachine.swift#L71-L78
let audio = VZVirtioSoundDeviceConfiguration()
let inputStream = VZVirtioSoundDeviceInputStreamConfiguration()
inputStream.source = VZHostAudioInputStreamSource()
let outputStream = VZVirtioSoundDeviceOutputStreamConfiguration()
outputStream.sink = VZHostAudioOutputStreamSink()
audio.streams = [inputStream, outputStream]
config.audioDevices = [audio]
Storage Attachment
// VPhoneVirtualMachine.swift#L80-L86
let attachment = try VZDiskImageStorageDeviceAttachment(
url: diskImageURL,
readOnly: false
)
let storage = VZVirtioBlockDeviceConfiguration(attachment: attachment)
config.storageDevices = [storage]
Network Device
Network configuration delegates to VPhoneNetworking.swift:
// VPhoneVirtualMachine.swift#L88-L92
if let networkDevice = try VPhoneNetworking.makeNetworkDevice(networkConfig) {
config.networkDevices = [networkDevice]
}
This supports NAT, bridged, or none modes based on manifest settings.
Serial Console (PL011 UART)
Interactive debugging requires serial port wiring to host pipes:
// VPhoneVirtualMachine.swift#L94-L122
guard let serialPort = Dynamic._VZPL011SerialPortConfiguration().asObject
as? VZSerialPortConfiguration else {
throw VPhoneError.serialPortUnavailable
}
let inputPipe = Pipe()
let outputPipe = Pipe()
serialPort.attachment = VZFileHandleSerialPortAttachment(
fileHandleForReading: inputPipe.fileHandleForReading,
fileHandleForWriting: outputPipe.fileHandleForWriting
)
// Host stdin → VM input
// VM output → host stdout (read from outputPipe elsewhere)
config.serialPorts = [serialPort]
Private API Extensions for iOS Support
Standard Virtualization framework APIs lack several iOS-critical features. vphone-cli accesses these through private API bridging.
Hardware Accelerators
// VPhoneVirtualMachine.swift#L124-L130
if let videoToolbox = Dynamic._VZMacVideoToolboxDeviceConfiguration().asObject,
let neuralEngine = Dynamic._VZMacNeuralEngineDeviceConfiguration().asObject,
let scaler = Dynamic._VZMacScalerAcceleratorDeviceConfiguration().asObject {
Dynamic(config)._setAcceleratorDevices([videoToolbox, neuralEngine, scaler])
}
VZMacVideoToolboxDeviceConfiguration, VZMacNeuralEngineDeviceConfiguration, and VZMacScalerAcceleratorDeviceConfiguration enable Media Engine, Neural Engine, and display scaler passthrough respectively.
Multi-Touch Input
Critical for iOS UI interaction:
// VPhoneVirtualMachine.swift#L132-L136
if let touchScreen = Dynamic._VZUSBTouchScreenConfiguration().asObject {
Dynamic(config)._setMultiTouchDevices([touchScreen])
}
VZUSBTouchScreenConfiguration exposes touch events to the guest system.
Synthetic Battery
iOS requires valid battery state for proper operation:
// VPhoneVirtualMachine.swift#L150-L158
let batterySource = Dynamic._VZMacSyntheticBatterySource()
batterySource.setCharge(100.0) // Full charge
batterySource.setConnectivity(1) // Charging state
let batteryConfig = Dynamic._VZMacBatteryPowerSourceDeviceConfiguration()
batteryConfig.setSource(batterySource.asObject)
if let battery = batteryConfig.asObject {
Dynamic(config)._setPowerSourceDevices([battery])
}
GDB Debug Stub
Kernel debugging support:
// VPhoneVirtualMachine.swift#L160-L176
let debugPort = options.kernelDebugPort ?? 0 // 0 = auto-assign
guard debugPort == 0 || (6000...65535).contains(debugPort) else {
throw VPhoneError.invalidKernelDebugPort(debugPort)
}
let debugStub = Dynamic._VZGDBDebugStubConfiguration(port: debugPort)
Dynamic(config)._setDebugStub(debugStub.asObject)
Secure Enclave Processor (SEP)
iOS security features require SEP emulation:
// VPhoneVirtualMachine.swift#L178-L188
let sep = Dynamic._VZSEPCoprocessorConfiguration(storageURL: sepStorageURL)
if let sepRom = options.sepRomURL {
sep.setRomBinaryURL(sepRom)
}
sep.setDebugStub(Dynamic._VZGDBDebugStubConfiguration().asObject)
if let sepConfig = sep.asObject {
Dynamic(config)._setCoprocessors([sepConfig])
}
Complete Implementation Examples
Minimal Working Configuration
This stripped-down example boots iOS with essential components only:
import Virtualization
func createMinimaliOSConfig(
diskURL: URL,
nvramURL: URL,
hardwareModel: VZMacHardwareModel
) throws -> VZVirtualMachineConfiguration {
// Platform with persistent storage
let platform = VZMacPlatformConfiguration()
platform.hardwareModel = hardwareModel
platform.auxiliaryStorage = try VZMacAuxiliaryStorage(
creatingStorageAt: nvramURL,
hardwareModel: hardwareModel,
options: .allowOverwrite
)
// Core configuration
let config = VZVirtualMachineConfiguration()
config.bootLoader = VZMacOSBootLoader()
config.platform = platform
config.cpuCount = max(4, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
config.memorySize = max(4_294_967_296, VZVirtualMachineConfiguration.minimumAllowedMemorySize)
// Display
let graphics = VZMacGraphicsDeviceConfiguration()
graphics.displays = [
VZMacGraphicsDisplayConfiguration(
widthInPixels: 1290,
heightInPixels: 2796,
pixelsPerInch: 460
)
]
config.graphicsDevices = [graphics]
// Storage
let storage = try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false)
config.storageDevices = [
VZVirtioBlockDeviceConfiguration(attachment: storage)
]
// Validation
try config.validate()
return config
}
Production-Grade Configuration (Full vphone-cli Implementation)
import Dynamic
import Virtualization
struct VMOptions {
let cpuCount: Int
let memorySize: UInt64
let screenWidth: Int
let screenHeight: Int
let screenPPI: Int
let diskURL: URL
let nvramURL: URL
let romURL: URL?
let sepStorageURL: URL
let sepRomURL: URL?
let networkConfig: NetworkConfiguration?
let kernelDebugPort: UInt16?
let noVphoned: Bool
}
func createProductioniOSConfig(options: VMOptions) throws -> VZVirtualMachineConfiguration {
// 1. Hardware model
let hwModel = try VPhoneHardware.createModel()
// 2. Platform setup
let platform = VZMacPlatformConfiguration()
platform.hardwareModel = hwModel
platform.auxiliaryStorage = try VZMacAuxiliaryStorage(
creatingStorageAt: options.nvramURL,
hardwareModel: hwModel,
options: .allowOverwrite
)
// 3. Boot loader with optional ROM
let bootLoader = VZMacOSBootLoader()
if let rom = options.romURL {
Dynamic(bootLoader)._setROMURL(rom)
}
// 4. Base configuration
let config = VZVirtualMachineConfiguration()
config.bootLoader = bootLoader
config.platform = platform
config.cpuCount = max(options.cpuCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
config.memorySize = max(options.memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize)
// 5. Graphics
let gfx = VZMacGraphicsDeviceConfiguration()
gfx.displays = [
VZMacGraphicsDisplayConfiguration(
widthInPixels: options.screenWidth,
heightInPixels: options.screenHeight,
pixelsPerInch: options.screenPPI
)
]
config.graphicsDevices = [gfx]
// 6. Audio
let audio = VZVirtioSoundDeviceConfiguration()
audio.streams = [
VZVirtioSoundDeviceInputStreamConfiguration().with { $0.source = VZHostAudioInputStreamSource() },
VZVirtioSoundDeviceOutputStreamConfiguration().with { $0.sink = VZHostAudioOutputStreamSink() }
]
config.audioDevices = [audio]
// 7. Storage
let disk = try VZDiskImageStorageDeviceAttachment(url: options.diskURL, readOnly: false)
config.storageDevices = [VZVirtioBlockDeviceConfiguration(attachment: disk)]
// 8. Network
if let net = try VPhoneNetworking.makeNetworkDevice(options.networkConfig) {
config.networkDevices = [net]
}
// 9. Serial console
if let serial = Dynamic._VZPL011SerialPortConfiguration().asObject as? VZSerialPortConfiguration {
let (inPipe, outPipe) = (Pipe(), Pipe())
serial.attachment = VZFileHandleSerialPortAttachment(
fileHandleForReading: inPipe.fileHandleForReading,
fileHandleForWriting: outPipe.fileHandleForWriting
)
config.serialPorts = [serial]
}
// 10. Accelerators (Video Toolbox, Neural Engine, Scaler)
if let vt = Dynamic._VZMacVideoToolboxDeviceConfiguration().asObject,
let ne = Dynamic._VZMacNeuralEngineDeviceConfiguration().asObject,
let sc = Dynamic._VZMacScalerAcceleratorDeviceConfiguration().asObject {
Dynamic(config)._setAcceleratorDevices([vt, ne, sc])
}
// 11. Touch screen
if let touch = Dynamic._VZUSBTouchScreenConfiguration().asObject {
Dynamic(config)._setMultiTouchDevices([touch])
}
// 12. Entropy
config.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
// 13. Keyboard
config.keyboards = [VZUSBKeyboardConfiguration()]
// 14. Socket device (vsock)
if !options.noVphoned {
config.socketDevices = [VZVirtioSocketDeviceConfiguration()]
}
// 15. Battery
let battery = Dynamic._VZMacSyntheticBatterySource()
battery.setCharge(100.0)
battery.setConnectivity(1)
let batteryDev = Dynamic._VZMacBatteryPowerSourceDeviceConfiguration()
batteryDev.setSource(battery.asObject)
if let b = batteryDev.asObject {
Dynamic(config)._setPowerSourceDevices([b])
}
// 16. Debug stub
let debugPort = options.kernelDebugPort.map(Int.init) ?? 0
let debugStub = Dynamic._VZGDBDebugStubConfiguration(port: debugPort)
Dynamic(config)._setDebugStub(debugStub.asObject)
// 17. SEP coprocessor
let sep = Dynamic._VZSEPCoprocessorConfiguration(storageURL: options.sepStorageURL)
if let sepRom = options.sepRomURL { sep.setRomBinaryURL(sepRom) }
sep.setDebugStub(Dynamic._VZGDBDebugStubConfiguration().asObject)
if let s = sep.asObject {
Dynamic(config)._setCoprocessors([s])
}
// 18. Validation
try config.validate()
return config
}
Validation and Instantiation
Always validate before creating the VM instance:
// VPhoneVirtualMachine.swift#L190-L193
try config.validate()
let virtualMachine = VZVirtualMachine(configuration: config)
virtualMachine.delegate = self
config.validate() throws VZError if any configuration violates framework constraints—common failures include incompatible hardware models, insufficient resources, or missing required devices.
Key Source Files Reference
| File | Purpose | Lines of Interest |
|---|---|---|
VPhoneVirtualMachine.swift |
Central VM construction | 50–193 (full configuration pipeline) |
VPhoneHardware.swift |
PV=3 hardware model generation | Entire file |
VPhoneVirtualMachineManifest.swift |
config.plist persistence |
Machine identifier storage |
VPhoneNetworking.swift |
Virtual NIC construction | Network device factory |
VPhoneControl.swift |
vsock host-guest channel | Socket device client |
Summary
- Hardware model: Use
VPhoneHardware.createModel()for PV=3 iPhone compatibility - Platform setup: Combine
VZMacPlatformConfigurationwithVZMacAuxiliaryStoragefor persistent NVRAM - Core resources: Clamp CPU and memory to
VZVirtualMachineConfigurationminimums - iOS-specific devices: Require private API access for touch screen, accelerators, battery, and SEP
- Validation: Always call
config.validate()beforeVZVirtualMachineinstantiation - Serial debugging: Wire
VZPL011SerialPortConfigurationto host pipes for interactive console access
Frequently Asked Questions
What is PV=3 in iOS virtualization?
PV=3 refers to Platform Version 3, the hardware model identifier Apple uses for modern iPhone SoCs (A15 and later). vphone-cli generates this through VPhoneHardware.createModel() in VPhoneHardware.swift. The hardware model determines which iOS kernels will boot and what device features are exposed to the guest.
Why does iOS VM configuration require private APIs?
Apple's public Virtualization framework only exposes macOS virtualization APIs. iOS-specific features—multi-touch input (VZUSBTouchScreenConfiguration), Secure Enclave (VZSEPCoprocessorConfiguration), battery simulation (VZMacSyntheticBatterySource), and media accelerators—are implemented in framework binaries but not public headers. vphone-cli accesses these through runtime message sending via the Dynamic wrapper.
How do I persist VM state across reboots?
Three components require persistence: (1) the disk image (standard file), (2) auxiliary storage (VZMacAuxiliaryStorage pointing to an NVRAM file), and (3) machine identifier (ECID/UDID stored in config.plist via VPhoneVirtualMachineManifest.swift). The manifest file must be read at launch and updated after any identifier changes.
What network modes does vphone-cli support?
According to VPhoneNetworking.swift, the tool supports NAT (default, isolated with outbound access), bridged (direct interface attachment), and none (disconnected). The mode is determined by parsing config.plist and constructing the appropriate VZNetworkDeviceConfiguration subclass—typically VZNATNetworkDeviceConfiguration or VZBridgedNetworkDeviceConfiguration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →