Karya Semi
HomeBlogSearchCategoriesAboutContact
Karya Semi

Less noise. More notes.

HomeBlogAboutContactPrivacy PolicyDisclaimer

© 2026 Karya Semi. All rights reserved.

XGitHubLinkedIn
  1. Home
  2. /Categories
  3. /Software Engineering

Boot Virtual iOS Instances Using Apple Virtualization Framework CLI

Boot virtual iphone virtualization framework instances on Apple silicon. Control iOS environments via CLI. Speed up development.

Dian Rijal Asyrof/August 31, 2026/8 min read
Illustration for Boot Virtual iOS Instances Using Apple Virtualization Framework CLI

Apple Silicon changed how we develop and test software. While the iOS Simulator works for basic UI testing, it runs on the host macOS kernel. It doesn't run actual iOS binaries compiled for the ARM64 iOS target; it runs binaries compiled for the macOS target. When you need to test kernel-level behavior, sandbox escape vectors, or low-level security mitigations (similar to the isolation guarantees discussed in sel4 security proofs completed on aarch64), you need a real virtualized iOS environment.

Apple Virtualization.framework provides the native APIs to run virtual machines on Apple Silicon. While most documentation focuses on macOS or Linux guests, you can boot iPadOS and iOS restore images directly. Let's look at how to build a Swift command-line utility to provision, install, and boot virtual iOS instances.

Why the Simulator Isn't Enough

The iOS Simulator is a simulator, not a virtual machine. It shares the host's system libraries, memory manager, and kernel. If you write code that behaves differently on iOS than macOS, the Simulator might hide the bug. This highlights why developers choose tools that encode trust and predictability over convenience.

A true virtual machine runs a guest kernel. It boots from an IPSW (iPhone Software Update) file. The guest kernel manages its own virtual memory, page tables, and sandbox rules. This is critical for security researchers, infrastructure engineers building CI/CD pipelines (who might also be interested in ai infrastructure engineering patterns for scaling workloads), and developers debugging low-level framework integrations.

Virtualizing iOS on Apple Silicon relies on the fact that the host hardware shares the same microarchitecture as the target guest. The Virtualization framework exposes this capability by allowing you to restore and run iPadOS images (which share the iOS core) as guest operating systems.

Prerequisites and Setup

Before writing the CLI tool, you need to prepare your development environment.

  • An Apple Silicon Mac (M1, M2, M3, or M4 family).
  • macOS 13 or later.
  • Xcode Command Line Tools.
  • A valid iPadOS IPSW file matching your host architecture. You can download these directly from Apple's developer portal or public IPSW trackers.

Because the Virtualization framework requires specific security permissions, your compiled binary must be signed with the virtualization entitlement. Create a file named entitlements.plist with the following content:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.virtualization</key>
    <true/>
</dict>
</plist>

You will use this file to sign the compiled binary before running it.

The Swift CLI Implementation

We will write a Swift command-line tool that handles two main tasks: installing the guest OS from an IPSW file onto a virtual disk, and booting an existing installation.

Here is the complete implementation of the CLI utility. Save this code as main.swift.

import Foundation
import Virtualization
 
@main
struct IOSVirtualMachineCLI {
    static func main() async {
        let arguments = CommandLine.arguments
        guard arguments.count >= 3 else {
            printUsage()
            exit(1)
        }
 
        let command = arguments[1]
        let vmDirectoryPath = arguments[2]
        let vmDirectoryURL = URL(fileURLWithPath: vmDirectoryPath)
 
        do {
            switch command {
            case "install":
                guard arguments.count == 4 else {
                    print("Error: Missing IPSW path for install command.")
                    exit(1)
                }
                let ipswURL = URL(fileURLWithPath: arguments[3])
                try await installVM(directory: vmDirectoryURL, ipsw: ipswURL)
            case "boot":
                try await bootVM(directory: vmDirectoryURL)
            default:
                print("Unknown command: \(command)")
                printUsage()
                exit(1)
            }
        } catch {
            print("Error: \(error.localizedDescription)")
            exit(1)
        }
    }
 
    static func printUsage() {
        print("""
        Usage:
          ios-vm-cli install <vm-directory-path> <ipsw-path>
          ios-vm-cli boot <vm-directory-path>
        """)
    }
}

Configuring the Virtual Machine Hardware

To boot the virtual machine, we need to define its hardware configuration. This includes the CPU count, memory size, storage devices, network interfaces, and display settings.

The Virtualization framework uses VZVirtualMachineConfiguration to represent these settings. Add the following helper function to your Swift file to construct this configuration:

func createVMConfiguration(directory: URL, diskSizeInBytes: Int64 = 64 * 1024 * 1024 * 1024) throws -> VZVirtualMachineConfiguration {
    let configuration = VZVirtualMachineConfiguration()
 
    // Configure CPU and Memory
    configuration.cpuCount = computeCPUCount()
    configuration.memorySize = computeMemorySize()
 
    // Configure Platform Support
    let platform = VZMacPlatformConfiguration()
    
    // Load or create hardware model and machine identifier
    let hardwareModelURL = directory.appendingPathComponent("HardwareModel.bin")
    let machineIdentifierURL = directory.appendingPathComponent("MachineIdentifier.bin")
    
    let hardwareModel: VZMacHardwareModel
    let machineIdentifier: VZMacMachineIdentifier
 
    if FileManager.default.fileExists(atPath: hardwareModelURL.path) {
        guard let hardwareModelData = try? Data(contentsOf: hardwareModelURL),
              let savedModel = VZMacHardwareModel(dataRepresentation: hardwareModelData) else {
            throw NSError(domain: "VMConfig", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to load hardware model."])
        }
        hardwareModel = savedModel
    } else {
        // Fallback or initialization during install phase
        throw NSError(domain: "VMConfig", code: 2, userInfo: [NSLocalizedDescriptionKey: "Hardware model not found. Run install first."])
    }
 
    if FileManager.default.fileExists(atPath: machineIdentifierURL.path) {
        guard let machineIdentifierData = try? Data(contentsOf: machineIdentifierURL),
              let savedIdentifier = VZMacMachineIdentifier(dataRepresentation: machineIdentifierData) else {
            throw NSError(domain: "VMConfig", code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to load machine identifier."])
        }
        machineIdentifier = savedIdentifier
    } else {
        throw NSError(domain: "VMConfig", code: 4, userInfo: [NSLocalizedDescriptionKey: "Machine identifier not found. Run install first."])
    }
 
    platform.hardwareModel = hardwareModel
    platform.machineIdentifier = machineIdentifier
    configuration.platform = platform
 
    // Configure Bootloader
    configuration.bootLoader = VZMacBootLoader()
 
    // Configure Virtual Storage
    let diskImageURL = directory.appendingPathComponent("Disk.img")
    let diskAttachment = try VZDiskImageStorageDeviceAttachment(url: diskImageURL, readOnly: false)
    let storageDevice = VZVirtioBlockDeviceConfiguration(attachment: diskAttachment)
    configuration.storageDevices = [storageDevice]
 
    // Configure Network (NAT mode by default)
    let networkDevice = VZVirtioNetworkDeviceConfiguration()
    networkDevice.attachment = VZNATNetworkDeviceAttachment()
    configuration.networkDevices = [networkDevice]
 
    // Configure Display
    let display = VZMacGraphicsDisplayConfiguration(widthInPixels: 1920, heightInPixels: 1080, pixelsPerInch: 220)
    configuration.graphicsDevices = [VZMacGraphicsDeviceConfiguration(displays: [display])]
 
    // Configure Input Devices
    configuration.keyboards = [VZUSBKeyboardConfiguration()]
    configuration.pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()]
 
    try configuration.validate()
    return configuration
}
 
private func computeCPUCount() -> Int {
    let hostPhysicalCPUs = ProcessInfo.processInfo.activeProcessorCount
    return max(2, min(hostPhysicalCPUs - 2, 4))
}
 
private func computeMemorySize() -> UInt64 {
    let hostMemory = ProcessInfo.processInfo.physicalMemory
    // Use 4GB if host has enough headroom, otherwise 2GB
    return hostMemory >= 8 * 1024 * 1024 * 1024 ? 4 * 1024 * 1024 * 1024 : 2 * 1024 * 1024 * 1024
}

The Installation Phase

The installation process takes an IPSW restore image, extracts the system files, configures the virtual storage, and saves the hardware metadata.

Add the installVM implementation:

func installVM(directory: URL, ipsw: URL) async throws {
    let fileManager = FileManager.default
    if !fileManager.fileExists(atPath: directory.path) {
        try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
    }
 
    print("Loading restore image metadata from \(ipsw.lastPathComponent)...")
    
    let image = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<VZMacOSRestoreImage, Error>) in
        VZMacOSRestoreImage.load(from: ipsw) { result in
            continuation.resume(with: result)
        }
    }
 
    guard let mostFeaturefulSupportedConfiguration = image.mostFeaturefulSupportedConfiguration else {
        throw NSError(domain: "VMInstall", code: 5, userInfo: [NSLocalizedDescriptionKey: "The restore image is not supported on this host."])
    }
 
    // Save platform configuration details
    let hardwareModel = mostFeaturefulSupportedConfiguration.hardwareModel
    let machineIdentifier = VZMacMachineIdentifier()
 
    try hardwareModel.dataRepresentation.write(to: directory.appendingPathComponent("HardwareModel.bin"))
    try machineIdentifier.dataRepresentation.write(to: directory.appendingPathComponent("MachineIdentifier.bin"))
 
    // Create empty disk image
    let diskImageURL = directory.appendingPathComponent("Disk.img")
    let diskSize: Int64 = 64 * 1024 * 1024 * 1024 // 64 GB
    try createEmptyDiskImage(at: diskImageURL, size: diskSize)
 
    // Construct configuration specifically for installation
    let configuration = VZVirtualMachineConfiguration()
    configuration.cpuCount = computeCPUCount()
    configuration.memorySize = computeMemorySize()
    
    let platform = VZMacPlatformConfiguration()
    platform.hardwareModel = hardwareModel
    platform.machineIdentifier = machineIdentifier
    configuration.platform = platform
    configuration.bootLoader = VZMacBootLoader()
 
    let diskAttachment = try VZDiskImageStorageDeviceAttachment(url: diskImageURL, readOnly: false)
    let storageDevice = VZVirtioBlockDeviceConfiguration(attachment: diskAttachment)
    configuration.storageDevices = [storageDevice]
 
    let networkDevice = VZVirtioNetworkDeviceConfiguration()
    networkDevice.attachment = VZNATNetworkDeviceAttachment()
    configuration.networkDevices = [networkDevice]
 
    try configuration.validate()
 
    let vm = VZVirtualMachine(configuration: configuration)
    let installer = VZMacOSInstaller(virtualMachine: vm, restoringFromImageAt: ipsw)
 
    print("Starting installation. This will take several minutes...")
    
    let progressObservation = installer.progress.observe(\.fractionCompleted) { progress, _ in
        let percent = String(format: "%.2f%%", progress.fractionCompleted * 100)
        print("Installation progress: \(percent)", terminator: "\r")
        fflush(stdout)
    }
 
    try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
        installer.install { error in
            progressObservation.invalidate()
            if let error = error {
                continuation.resume(throwing: error)
            } else {
                continuation.resume()
            }
        }
    }
 
    print("\nInstallation successful.")
}
 
func createEmptyDiskImage(at url: URL, size: Int64) throws {
    let fileManager = FileManager.default
    guard !fileManager.fileExists(atPath: url.path) else { return }
    
    // Create a sparse file to save disk space on the host
    let fd = open(url.path, O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR)
    guard fd >= 0 else {
        throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: nil)
    }
    defer { close(fd) }
 
    let result = ftruncate(fd, off_t(size))
    guard result == 0 else {
        throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: nil)
    }
}

The Booting Phase

Once the VM is installed, you can boot it using the saved hardware configuration. The CLI tool will start the virtual machine and wait for it to terminate.

Add the bootVM implementation:

func bootVM(directory: URL) async throws {
    let configuration = try createVMConfiguration(directory: directory)
    let vm = VZVirtualMachine(configuration: configuration)
 
    print("Starting virtual machine...")
    try await vm.start()
    print("Virtual machine running.")
 
    // Keep the CLI running while the VM executes
    let delegate = VMDelegate()
    vm.delegate = delegate
 
    try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
        NotificationCenter.default.addObserver(forName: .VZVirtualMachineReceiverStopped, object: vm, queue: nil) { _ in
            continuation.resume()
        }
        
        // Block main thread until VM stops
        RunLoop.main.run(until: Date.distantFuture)
    }
}
 
class VMDelegate: NSObject, VZVirtualMachineDelegate {
    func guestDidStop(_ virtualMachine: VZVirtualMachine) {
        print("Guest OS stopped.")
        exit(0)
    }
 
    func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
        print("Virtual machine stopped with error: \(error.localizedDescription)")
        exit(1)
    }
}
 
extension Notification.Name {
    static let VZVirtualMachineReceiverStopped = Notification.Name("VZVirtualMachineReceiverStopped")
}

Compilation and Signing

To compile the Swift file into an executable binary, run the following commands in your terminal:

xcrun -sdk macosx swiftc -O main.swift -o ios-vm-cli

Once compiled, apply the entitlements to the binary:

codesign -force -sign - -entitlements entitlements.plist ios-vm-cli

If you do not sign the binary with the correct entitlements, the Virtualization framework will throw a runtime error when you try to initialize VZVirtualMachineConfiguration.

Running the CLI Tool

Create a directory to store your virtual machine files:

mkdir ~/iOSVM

Download a compatible iPadOS IPSW file, then run the installation command:

./ios-vm-cli install ~/iOSVM /path/to/ipsw/ipsw-file.ipsw

Once the installation completes, boot the VM:

./ios-vm-cli boot ~/iOSVM

When running in a standard CLI environment, the virtual machine starts, but you will not see a graphical interface unless you bind the virtual machine instance to a window. Let's look at how to handle graphics and remote access.

Interacting with the Virtual Machine

Unlike Linux guests where you can redirect the console to a serial port, iOS/iPadOS guest environments expect a graphical display and user input devices.

If you run the CLI tool directly from a standard terminal, the VM runs in the background. To view the screen, you have two primary options:

  1. Build a lightweight AppKit/SwiftUI wrapper: Replace the command-line entry point with a simple desktop app that uses VZVirtualMachineView to render the display and capture keyboard/mouse events.
  2. Network access: Configure the guest OS to enable remote access tools (like SSH or remote management) during the initial setup phase.

For local debugging, wrapping the VZVirtualMachine in a basic AppKit window is the most reliable approach. Here is a minimal implementation pattern using AppKit:

import Cocoa
import Virtualization
 
class VMWindowController: NSWindowController {
    var virtualMachine: VZVirtualMachine?
    var virtualMachineView: VZVirtualMachineView?
 
    override func loadWindow() {
        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 1024, height: 768),
            styleMask: [.titled, .closable, .miniaturizable, .resizable],
            backing: .buffered,
            defer: false
        )
        window.title = "iOS Virtual Machine"
        self.window = window
 
        let vmView = VZVirtualMachineView()
        vmView.virtualMachine = virtualMachine
        vmView.capturesSystemKeys = true
        
        window.contentView = vmView
        self.virtualMachineView = vmView
    }
}

Network and Storage Configuration

NAT vs. Bridged Networking

By default, the CLI tool uses VZNATNetworkDeviceAttachment. This sets up a private network behind the host's IP address. The guest VM can access the internet, but external devices on your local network cannot connect directly to the VM.

If you need to connect to the VM from other devices on your physical network, switch to a bridged network attachment. Replace the network device configuration code with this:

let networkDevice = VZVirtioNetworkDeviceConfiguration()
let networkInterfaces = VZBridgedNetworkInterface.networkInterfaces
if let primaryInterface = networkInterfaces.first {
    networkDevice.attachment = VZBridgedNetworkDeviceAttachment(interface: primaryInterface)
} else {
    // Fall back to NAT if no physical interface is available
    networkDevice.attachment = VZNATNetworkDeviceAttachment()
}

Bridged networking requires the com.apple.security.networking.custom-protocol entitlement. Make sure to update your entitlements.plist if you switch to bridged mode.

Disk Provisioning and Storage Limits

The virtual disk is created as a sparse file using ftruncate. A sparse file allocates physical blocks on the host disk only when data is written to them. Even if you allocate a 64GB disk, the file size on your host machine will initially be only a few megabytes.

Be careful not to over-provision. If the host running the VM runs out of actual physical disk space, the virtual disk write operations will fail silently or crash the guest kernel, leading to filesystem corruption.

Automation and Headless CI/CD Integration

To run these VMs in a continuous integration environment (like GitHub Actions, GitLab CI, or Jenkins), you must address several platform limitations.

Apple ID and iCloud Restrictions

Apple restricts iCloud sign-in inside virtualized environments. Features like App Store downloads, iCloud Drive, and Xcode provisioning profile syncing that require an Apple ID will not function inside the guest OS.

To deploy test applications to the VM in a CI/CD pipeline, you cannot rely on the App Store. Instead, you must install applications using:

  • Native development tools like devicectl (part of Xcode 15+).
  • Custom MDM (Mobile Device Management) profiles.
  • Direct installation of ad-hoc signed .ipa packages via local network servers.

Managing State with Snapshots

To ensure tests are repeatable, you need to return the VM to a clean state before every test run. The easiest way to do this with Virtualization.framework is to copy the virtual disk image and configuration files before booting, then restore them when the run completes.

A basic shell script can manage this lifecycle:

#!/bin/bash
# Reset VM state
cp ~/iOSVM/Disk.img.clean ~/iOSVM/Disk.img
 
# Run the VM in the background
./ios-vm-cli boot ~/iOSVM &
VM_PID=$!
 
# Run your test suite
npm run test-ios
 
# Clean up
kill $VM_PID

By separating the installation phase from the boot phase, you can generate a single golden image (Disk.img.clean), distribute it to your build agents, and boot fresh instances in seconds.

DR

Dian Rijal Asyrof

Writes about useful AI tools, programming practice, and the craft of building reliable software.

Previous articleAnthropic Demonstrates Automated AI Alignment Improvement SystemNext articleTypebase Delivers File-Based TypeScript Backend Architecture
IosCliVirtualization FrameworkApple SiliconDeveloper Tools
On this page↓
  1. Why the Simulator Isn't Enough
  2. Prerequisites and Setup
  3. The Swift CLI Implementation
  4. Configuring the Virtual Machine Hardware
  5. The Installation Phase
  6. The Booting Phase
  7. Compilation and Signing
  8. Running the CLI Tool
  9. Interacting with the Virtual Machine
  10. Network and Storage Configuration
  11. NAT vs. Bridged Networking
  12. Disk Provisioning and Storage Limits
  13. Automation and Headless CI/CD Integration
  14. Apple ID and iCloud Restrictions
  15. Managing State with Snapshots

On this page

  1. Why the Simulator Isn't Enough
  2. Prerequisites and Setup
  3. The Swift CLI Implementation
  4. Configuring the Virtual Machine Hardware
  5. The Installation Phase
  6. The Booting Phase
  7. Compilation and Signing
  8. Running the CLI Tool
  9. Interacting with the Virtual Machine
  10. Network and Storage Configuration
  11. NAT vs. Bridged Networking
  12. Disk Provisioning and Storage Limits
  13. Automation and Headless CI/CD Integration
  14. Apple ID and iCloud Restrictions
  15. Managing State with Snapshots

See also

Illustration for Open Source Experiential Router Uses Request Data to Fine-Tune Models
Programming/Aug 28, 2026

Open Source Experiential Router Uses Request Data to Fine-Tune Models

New API router uses request data for openrouter model fine tuning. Turn inference routing patterns into training feedback for better LLMs.

6 min read
LLMLLMs
Illustration for Streamline Parallel Feature Work with Git Worktree
Software Engineering/Aug 28, 2026

Streamline Parallel Feature Work with Git Worktree

Avoid stash conflicts. Leverage git worktree parallel development to manage multiple active branches at once. Eliminate context switching and deploy faster.

7 min read
Git WorktreeVersion Control
Illustration for Long-Term Technical Impact of AI Coding Assistants on Senior Software Engineering
Programming/Aug 28, 2026

Long-Term Technical Impact of AI Coding Assistants on Senior Software Engineering

Measure ai coding impact expertise. Automated generation risks senior system design skills. Learn to balance speed with deep technical mastery.

9 min read
AI CodingSenior