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 VZMacPlatformConfiguration with VZMacAuxiliaryStorage for persistent NVRAM
  • Core resources: Clamp CPU and memory to VZVirtualMachineConfiguration minimums
  • iOS-specific devices: Require private API access for touch screen, accelerators, battery, and SEP
  • Validation: Always call config.validate() before VZVirtualMachine instantiation
  • Serial debugging: Wire VZPL011SerialPortConfiguration to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →