From bef9f9f07d1aaa9863b98e1b70a963d85d1a024f Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sat, 7 Mar 2026 22:26:16 +0900 Subject: [PATCH 1/6] add experimental vnc source overrides --- .../Sources/VPhoneObjC/VPhoneObjC.m | 330 ++++++++++++++++++ .../Sources/VPhoneObjC/include/VPhoneObjC.h | 96 +++++ .../Sources/vphone-cli/VPhoneCLI.swift | 154 ++++++++ .../Sources/vphone-cli/VPhoneVM.swift | 213 +++++++++++ .../Sources/vphone-cli/VPhoneVNC.swift | 45 +++ experimental-vnc/enable.sh | 48 +++ 6 files changed, 886 insertions(+) create mode 100644 experimental-vnc/Sources/VPhoneObjC/VPhoneObjC.m create mode 100644 experimental-vnc/Sources/VPhoneObjC/include/VPhoneObjC.h create mode 100644 experimental-vnc/Sources/vphone-cli/VPhoneCLI.swift create mode 100644 experimental-vnc/Sources/vphone-cli/VPhoneVM.swift create mode 100644 experimental-vnc/Sources/vphone-cli/VPhoneVNC.swift create mode 100644 experimental-vnc/enable.sh diff --git a/experimental-vnc/Sources/VPhoneObjC/VPhoneObjC.m b/experimental-vnc/Sources/VPhoneObjC/VPhoneObjC.m new file mode 100644 index 0000000..cc2bb9d --- /dev/null +++ b/experimental-vnc/Sources/VPhoneObjC/VPhoneObjC.m @@ -0,0 +1,330 @@ +// VPhoneObjC.m — ObjC wrappers for private Virtualization.framework APIs +#import "VPhoneObjC.h" +#import +#import + +// Private class forward declarations +@interface _VZMacHardwareModelDescriptor : NSObject +- (instancetype)init; +- (void)setPlatformVersion:(unsigned int)version; +- (void)setISA:(long long)isa; +- (void)setBoardID:(unsigned int)boardID; +@end + +@interface VZMacHardwareModel (Private) ++ (instancetype)_hardwareModelWithDescriptor:(id)descriptor; +@end + +@interface VZMacOSVirtualMachineStartOptions (Private) +- (void)_setForceDFU:(BOOL)force; +- (void)_setPanicAction:(BOOL)stop; +- (void)_setFatalErrorAction:(BOOL)stop; +- (void)_setStopInIBootStage1:(BOOL)stop; +- (void)_setStopInIBootStage2:(BOOL)stop; +@end + +@interface VZMacOSBootLoader (Private) +- (void)_setROMURL:(NSURL *)url; +@end + +@interface VZVirtualMachineConfiguration (Private) +- (void)_setDebugStub:(id)stub; +- (void)_setPanicDevice:(id)device; +- (void)_setCoprocessors:(NSArray *)coprocessors; +- (void)_setMultiTouchDevices:(NSArray *)devices; +@end + +@interface VZMacPlatformConfiguration (Private) +- (void)_setProductionModeEnabled:(BOOL)enabled; +@end + +// --- Implementation --- + +VZMacHardwareModel *VPhoneCreateHardwareModel(void) { + // Create descriptor with PV=3, ISA=2, boardID=0x90 (matches vrevm vresearch101) + _VZMacHardwareModelDescriptor *desc = [[_VZMacHardwareModelDescriptor alloc] init]; + [desc setPlatformVersion:3]; + [desc setBoardID:0x90]; + [desc setISA:2]; + + VZMacHardwareModel *model = [VZMacHardwareModel _hardwareModelWithDescriptor:desc]; + return model; +} + +void VPhoneSetBootLoaderROMURL(VZMacOSBootLoader *bootloader, NSURL *romURL) { + [bootloader _setROMURL:romURL]; +} + +void VPhoneConfigureStartOptions(VZMacOSVirtualMachineStartOptions *opts, + BOOL forceDFU, + BOOL stopOnPanic, + BOOL stopOnFatalError) { + [opts _setForceDFU:forceDFU]; + [opts _setStopInIBootStage1:NO]; + [opts _setStopInIBootStage2:NO]; + // Note: _setPanicAction: / _setFatalErrorAction: don't exist on + // VZMacOSVirtualMachineStartOptions. Panic handling is done via + // _VZPvPanicDeviceConfiguration set on VZVirtualMachineConfiguration. +} + +void VPhoneSetGDBDebugStub(VZVirtualMachineConfiguration *config, NSInteger port) { + Class stubClass = NSClassFromString(@"_VZGDBDebugStubConfiguration"); + if (!stubClass) { + NSLog(@"[vphone] WARNING: _VZGDBDebugStubConfiguration not found"); + return; + } + // Use objc_msgSend to call initWithPort: with an NSInteger argument + id (*initWithPort)(id, SEL, NSInteger) = (id (*)(id, SEL, NSInteger))objc_msgSend; + id stub = initWithPort([stubClass alloc], NSSelectorFromString(@"initWithPort:"), port); + [config _setDebugStub:stub]; +} + +void VPhoneSetPanicDevice(VZVirtualMachineConfiguration *config) { + Class panicClass = NSClassFromString(@"_VZPvPanicDeviceConfiguration"); + if (!panicClass) { + NSLog(@"[vphone] WARNING: _VZPvPanicDeviceConfiguration not found"); + return; + } + id device = [[panicClass alloc] init]; + [config _setPanicDevice:device]; +} + +void VPhoneSetCoprocessors(VZVirtualMachineConfiguration *config, NSArray *coprocessors) { + [config _setCoprocessors:coprocessors]; +} + +void VPhoneDisableProductionMode(VZMacPlatformConfiguration *platform) { + [platform _setProductionModeEnabled:NO]; +} + +// --- NVRAM --- + +@interface VZMacAuxiliaryStorage (Private) +- (BOOL)_setDataValue:(NSData *)value forNVRAMVariableNamed:(NSString *)name error:(NSError **)error; +@end + +BOOL VPhoneSetNVRAMVariable(VZMacAuxiliaryStorage *auxStorage, NSString *name, NSData *value) { + NSError *error = nil; + BOOL ok = [auxStorage _setDataValue:value forNVRAMVariableNamed:name error:&error]; + if (!ok) { + NSLog(@"[vphone] NVRAM set '%@' failed: %@", name, error); + } + return ok; +} + +// --- PL011 Serial Port --- + +@interface _VZPL011SerialPortConfiguration : VZSerialPortConfiguration +@end + +VZSerialPortConfiguration *VPhoneCreatePL011SerialPort(void) { + Class cls = NSClassFromString(@"_VZPL011SerialPortConfiguration"); + if (!cls) { + NSLog(@"[vphone] WARNING: _VZPL011SerialPortConfiguration not found"); + return nil; + } + return [[cls alloc] init]; +} + +// --- SEP Coprocessor --- + +@interface _VZSEPCoprocessorConfiguration : NSObject +- (instancetype)initWithStorageURL:(NSURL *)url; +- (void)setRomBinaryURL:(NSURL *)url; +- (void)setDebugStub:(id)stub; +@end + +id VPhoneCreateSEPCoprocessorConfig(NSURL *storageURL) { + Class cls = NSClassFromString(@"_VZSEPCoprocessorConfiguration"); + if (!cls) { + NSLog(@"[vphone] WARNING: _VZSEPCoprocessorConfiguration not found"); + return nil; + } + _VZSEPCoprocessorConfiguration *config = [[cls alloc] initWithStorageURL:storageURL]; + return config; +} + +void VPhoneSetSEPRomBinaryURL(id sepConfig, NSURL *romURL) { + if ([sepConfig respondsToSelector:@selector(setRomBinaryURL:)]) { + [sepConfig performSelector:@selector(setRomBinaryURL:) withObject:romURL]; + } +} + +void VPhoneConfigureSEP(VZVirtualMachineConfiguration *config, + NSURL *sepStorageURL, + NSURL *sepRomURL) { + id sepConfig = VPhoneCreateSEPCoprocessorConfig(sepStorageURL); + if (!sepConfig) { + NSLog(@"[vphone] Failed to create SEP coprocessor config"); + return; + } + if (sepRomURL) { + VPhoneSetSEPRomBinaryURL(sepConfig, sepRomURL); + } + // Set debug stub on SEP (same as vrevm) + Class stubClass = NSClassFromString(@"_VZGDBDebugStubConfiguration"); + if (stubClass) { + id sepDebugStub = [[stubClass alloc] init]; + [sepConfig performSelector:@selector(setDebugStub:) withObject:sepDebugStub]; + } + [config _setCoprocessors:@[sepConfig]]; + NSLog(@"[vphone] SEP coprocessor configured (storage: %@)", sepStorageURL.path); +} + +void VPhoneSetGDBDebugStubDefault(VZVirtualMachineConfiguration *config) { + Class stubClass = NSClassFromString(@"_VZGDBDebugStubConfiguration"); + if (!stubClass) { + NSLog(@"[vphone] WARNING: _VZGDBDebugStubConfiguration not found"); + return; + } + id stub = [[stubClass alloc] init]; // default init, no specific port (same as vrevm) + [config _setDebugStub:stub]; +} + +// --- Multi-Touch (VNC click fix) --- + +@interface _VZMultiTouchDeviceConfiguration : NSObject +@end + +@interface _VZUSBTouchScreenConfiguration : _VZMultiTouchDeviceConfiguration +- (instancetype)init; +@end + +void VPhoneConfigureMultiTouch(VZVirtualMachineConfiguration *config) { + Class cls = NSClassFromString(@"_VZUSBTouchScreenConfiguration"); + if (!cls) { + NSLog(@"[vphone] WARNING: _VZUSBTouchScreenConfiguration not found"); + return; + } + id touchConfig = [[cls alloc] init]; + [config _setMultiTouchDevices:@[touchConfig]]; + NSLog(@"[vphone] USB touch screen configured"); +} + +void VPhoneConfigureUSBKeyboard(VZVirtualMachineConfiguration *config) { + id kbConfig = [[VZUSBKeyboardConfiguration alloc] init]; + SEL setSel = NSSelectorFromString(@"setKeyboards:"); + if ([config respondsToSelector:setSel]) { + void (*setter)(id, SEL, NSArray *) = (void (*)(id, SEL, NSArray *))objc_msgSend; + setter(config, setSel, @[kbConfig]); + NSLog(@"[vphone] USB keyboard configured"); + } else { + NSLog(@"[vphone] WARNING: config does not respond to setKeyboards:"); + } +} + +// VZTouchHelper: create _VZTouch using KVC to avoid crash in initWithView:... +// The _VZTouch initializer does a struct copy (objc_copyStruct) that causes +// EXC_BAD_ACCESS (SIGBUS) when called from Swift Dynamic framework. +// Using alloc+init then KVC setValue:forKey: bypasses the problematic initializer. +id VPhoneCreateTouch(NSInteger index, + NSInteger phase, + CGPoint location, + NSInteger swipeAim, + NSTimeInterval timestamp) { + Class touchClass = NSClassFromString(@"_VZTouch"); + if (!touchClass) { + return nil; + } + + id touch = [[touchClass alloc] init]; + + [touch setValue:@((unsigned char)index) forKey:@"_index"]; + [touch setValue:@(phase) forKey:@"_phase"]; + [touch setValue:@(swipeAim) forKey:@"_swipeAim"]; + [touch setValue:@(timestamp) forKey:@"_timestamp"]; + [touch setValue:[NSValue valueWithPoint:location] forKey:@"_location"]; + + return touch; +} + +id VPhoneCreateMultiTouchEvent(NSArray *touches) { + Class cls = NSClassFromString(@"_VZMultiTouchEvent"); + if (!cls) { + return nil; + } + // _VZMultiTouchEvent initWithTouches: + SEL sel = NSSelectorFromString(@"initWithTouches:"); + id event = [cls alloc]; + id (*initWithTouches)(id, SEL, NSArray *) = (id (*)(id, SEL, NSArray *))objc_msgSend; + return initWithTouches(event, sel, touches); +} + +NSArray *VPhoneGetMultiTouchDevices(VZVirtualMachine *vm) { + SEL sel = NSSelectorFromString(@"_multiTouchDevices"); + if (![vm respondsToSelector:sel]) { + return nil; + } + NSArray * (*getter)(id, SEL) = (NSArray * (*)(id, SEL))objc_msgSend; + return getter(vm, sel); +} + +void VPhoneSendMultiTouchEvents(id multiTouchDevice, NSArray *events) { + SEL sel = NSSelectorFromString(@"sendMultiTouchEvents:"); + if (![multiTouchDevice respondsToSelector:sel]) { + return; + } + void (*send)(id, SEL, NSArray *) = (void (*)(id, SEL, NSArray *))objc_msgSend; + send(multiTouchDevice, sel, events); +} + +id VPhoneCreateVNCServer(VZVirtualMachine *virtualMachine, NSString *password) { + Class secClass = NSClassFromString(@"_VZVNCAuthenticationSecurityConfiguration"); + if (!secClass) { + NSLog(@"[vphone] WARNING: _VZVNCAuthenticationSecurityConfiguration not found"); + return nil; + } + + SEL initWithPasswordSel = NSSelectorFromString(@"initWithPassword:"); + id (*initWithPassword)(id, SEL, NSString *) = (id (*)(id, SEL, NSString *))objc_msgSend; + id secConfig = initWithPassword([secClass alloc], initWithPasswordSel, password); + + Class serverClass = NSClassFromString(@"_VZVNCServer"); + if (!serverClass) { + NSLog(@"[vphone] WARNING: _VZVNCServer not found"); + return nil; + } + + SEL initSel = NSSelectorFromString(@"initWithPort:queue:securityConfiguration:"); + if (![serverClass instancesRespondToSelector:initSel]) { + NSLog(@"[vphone] WARNING: _VZVNCServer does not support initWithPort:queue:securityConfiguration:"); + return nil; + } + + id (*initVNC)(id, SEL, NSInteger, dispatch_queue_t, id) = + (id (*)(id, SEL, NSInteger, dispatch_queue_t, id))objc_msgSend; + id server = initVNC([serverClass alloc], initSel, + (NSInteger)0, + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + secConfig); + if (!server) { + NSLog(@"[vphone] WARNING: failed to create _VZVNCServer"); + return nil; + } + + [server setValue:virtualMachine forKey:@"virtualMachine"]; + + SEL startSel = NSSelectorFromString(@"start"); + if ([server respondsToSelector:startSel]) { + void (*start)(id, SEL) = (void (*)(id, SEL))objc_msgSend; + start(server, startSel); + } else { + NSLog(@"[vphone] WARNING: _VZVNCServer does not respond to start"); + } + + return server; +} + +uint16_t VPhoneGetVNCPort(id vncServer) { + id portValue = [vncServer valueForKey:@"port"]; + if (!portValue) return 0; + return (uint16_t)[portValue unsignedShortValue]; +} + +void VPhoneStopVNCServer(id vncServer) { + SEL stopSel = NSSelectorFromString(@"stop"); + if ([vncServer respondsToSelector:stopSel]) { + void (*stop)(id, SEL) = (void (*)(id, SEL))objc_msgSend; + stop(vncServer, stopSel); + } +} diff --git a/experimental-vnc/Sources/VPhoneObjC/include/VPhoneObjC.h b/experimental-vnc/Sources/VPhoneObjC/include/VPhoneObjC.h new file mode 100644 index 0000000..85ef9f3 --- /dev/null +++ b/experimental-vnc/Sources/VPhoneObjC/include/VPhoneObjC.h @@ -0,0 +1,96 @@ +// VPhoneObjC.h — ObjC wrappers for private Virtualization.framework APIs +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Create a PV=3 (vphone) VZMacHardwareModel using private _VZMacHardwareModelDescriptor. +VZMacHardwareModel *VPhoneCreateHardwareModel(void); + +/// Set _setROMURL: on a VZMacOSBootLoader. +void VPhoneSetBootLoaderROMURL(VZMacOSBootLoader *bootloader, NSURL *romURL); + +/// Configure VZMacOSVirtualMachineStartOptions. +/// Sets _setForceDFU:, _setPanicAction:, _setFatalErrorAction: +void VPhoneConfigureStartOptions(VZMacOSVirtualMachineStartOptions *opts, + BOOL forceDFU, + BOOL stopOnPanic, + BOOL stopOnFatalError); + +/// Set _setDebugStub: with a _VZGDBDebugStubConfiguration on the VM config (specific port). +void VPhoneSetGDBDebugStub(VZVirtualMachineConfiguration *config, NSInteger port); + +/// Set _setDebugStub: with default _VZGDBDebugStubConfiguration (system-assigned port, same as vrevm). +void VPhoneSetGDBDebugStubDefault(VZVirtualMachineConfiguration *config); + +/// Set _VZPvPanicDeviceConfiguration on the VM config. +void VPhoneSetPanicDevice(VZVirtualMachineConfiguration *config); + +/// Set _setCoprocessors: on the VM config (empty array = no coprocessors). +void VPhoneSetCoprocessors(VZVirtualMachineConfiguration *config, NSArray *coprocessors); + +/// Set _setProductionModeEnabled:NO on VZMacPlatformConfiguration. +void VPhoneDisableProductionMode(VZMacPlatformConfiguration *platform); + +/// Create a _VZSEPCoprocessorConfiguration with the given storage URL. +/// Returns the config object, or nil on failure. +id _Nullable VPhoneCreateSEPCoprocessorConfig(NSURL *storageURL); + +/// Set romBinaryURL on a _VZSEPCoprocessorConfiguration. +void VPhoneSetSEPRomBinaryURL(id sepConfig, NSURL *romURL); + +/// Configure SEP coprocessor on the VM config. +/// Creates storage at sepStorageURL, optionally sets sepRomURL, and calls _setCoprocessors:. +void VPhoneConfigureSEP(VZVirtualMachineConfiguration *config, + NSURL *sepStorageURL, + NSURL *_Nullable sepRomURL); + +/// Set an NVRAM variable on VZMacAuxiliaryStorage using the private _setDataValue API. +/// Returns YES on success. +BOOL VPhoneSetNVRAMVariable(VZMacAuxiliaryStorage *auxStorage, NSString *name, NSData *value); + +/// Create a _VZPL011SerialPortConfiguration (ARM PL011 UART serial port). +/// Returns nil if the private class is unavailable. +VZSerialPortConfiguration *_Nullable VPhoneCreatePL011SerialPort(void); + +// --- USB Input Devices --- + +/// Configure _VZUSBTouchScreenConfiguration on the VM config. +/// Must be called before VM starts to enable touch input. +void VPhoneConfigureMultiTouch(VZVirtualMachineConfiguration *config); + +/// Configure _VZUSBKeyboardConfiguration on the VM config. +/// Enables physical keyboard input in the native GUI window. +void VPhoneConfigureUSBKeyboard(VZVirtualMachineConfiguration *config); + +/// Create a _VZTouch object using KVC (avoids crash in _VZTouch initWithView:...). +/// Returns nil if the _VZTouch class is unavailable. +id _Nullable VPhoneCreateTouch(NSInteger index, + NSInteger phase, + CGPoint location, + NSInteger swipeAim, + NSTimeInterval timestamp); + +/// Create a _VZMultiTouchEvent from an array of _VZTouch objects. +id _Nullable VPhoneCreateMultiTouchEvent(NSArray *touches); + +/// Get the _multiTouchDevices array from a running VZVirtualMachine. +NSArray *_Nullable VPhoneGetMultiTouchDevices(VZVirtualMachine *vm); + +/// Send multi-touch events to a multi-touch device. +void VPhoneSendMultiTouchEvents(id multiTouchDevice, NSArray *events); + +// --- VNC (experimental, Virtualization.framework built-in) --- + +/// Create and start a _VZVNCServer bound to a random port (port:0). +/// Returns the server object (opaque AnyObject for Swift), or nil on failure. +id _Nullable VPhoneCreateVNCServer(VZVirtualMachine *virtualMachine, NSString *password); + +/// Return the current port the VNC server is listening on. +/// Returns 0 if not yet assigned. +uint16_t VPhoneGetVNCPort(id vncServer); + +/// Stop the VNC server. +void VPhoneStopVNCServer(id vncServer); + +NS_ASSUME_NONNULL_END diff --git a/experimental-vnc/Sources/vphone-cli/VPhoneCLI.swift b/experimental-vnc/Sources/vphone-cli/VPhoneCLI.swift new file mode 100644 index 0000000..a7681fa --- /dev/null +++ b/experimental-vnc/Sources/vphone-cli/VPhoneCLI.swift @@ -0,0 +1,154 @@ +import AppKit +import ArgumentParser +import Foundation +import Virtualization + +@main +struct VPhoneCLI: AsyncParsableCommand { + static var configuration = CommandConfiguration( + commandName: "vphone-cli", + abstract: "Boot a virtual iPhone (PV=3)", + discussion: """ + Creates a Virtualization.framework VM with platform version 3 (vphone) + and boots it into DFU mode for firmware loading via irecovery. + + Requires: + - macOS 15+ (Sequoia or later) + - SIP/AMFI disabled + - Signed with vphone entitlements (done automatically by wrapper script) + + Example: + vphone-cli --rom firmware/rom.bin --disk firmware/disk.img + """, + ) + + @Option(help: "Path to the AVPBooter / ROM binary") + var rom: String + + @Option(help: "Path to the disk image") + var disk: String + + @Option(help: "Path to NVRAM storage (created/overwritten)") + var nvram: String = "nvram.bin" + + @Option(help: "Number of CPU cores") + var cpu: Int = 4 + + @Option(help: "Memory size in MB") + var memory: Int = 4096 + + @Option(help: "Path to write serial console log file") + var serialLog: String? = nil + + @Flag(help: "Stop VM on guest panic") + var stopOnPanic: Bool = false + + @Flag(help: "Stop VM on fatal error") + var stopOnFatalError: Bool = false + + @Flag(help: "Skip SEP coprocessor setup") + var skipSep: Bool = false + + @Option(help: "Path to SEP storage file (created if missing)") + var sepStorage: String? = nil + + @Option(help: "Path to SEP ROM binary") + var sepRom: String? = nil + + @Flag(help: "Boot into DFU mode") + var dfu: Bool = false + + @Flag(help: "Run without GUI (headless)") + var noGraphics: Bool = false + + @Flag( + help: ArgumentHelp( + "Use Virtualization.framework built-in VNC server (experimental).", + discussion: """ + Starts a private _VZVNCServer on a random local port and prints the URL. + Useful when the default viewer path is slow or keyboard input is unreliable. + """)) + var vncExperimental: Bool = false + + mutating func validate() throws { + if vncExperimental && noGraphics { + throw ValidationError("--vnc-experimental and --no-graphics are mutually exclusive") + } + } + + @MainActor + mutating func run() async throws { + let romURL = URL(fileURLWithPath: rom) + guard FileManager.default.fileExists(atPath: romURL.path) else { + throw VPhoneError.romNotFound(rom) + } + + let diskURL = URL(fileURLWithPath: disk) + let nvramURL = URL(fileURLWithPath: nvram) + + print("=== vphone-cli ===") + print("ROM : \(rom)") + print("Disk : \(disk)") + print("NVRAM : \(nvram)") + print("CPU : \(cpu)") + print("Memory: \(memory) MB") + let sepStorageURL = sepStorage.map { URL(fileURLWithPath: $0) } + let sepRomURL = sepRom.map { URL(fileURLWithPath: $0) } + + print("SEP : \(skipSep ? "skipped" : "enabled")") + if !skipSep { + print(" storage: \(sepStorage ?? "(auto)")") + if let r = sepRom { print(" rom : \(r)") } + } + print("") + + let options = VPhoneVM.Options( + romURL: romURL, + nvramURL: nvramURL, + diskURL: diskURL, + cpuCount: cpu, + memorySize: UInt64(memory) * 1024 * 1024, + skipSEP: skipSep, + sepStorageURL: sepStorageURL, + sepRomURL: sepRomURL, + serialLogPath: serialLog, + stopOnPanic: stopOnPanic, + stopOnFatalError: stopOnFatalError, + ) + + let vm = try VPhoneVM(options: options) + + // Handle Ctrl+C + signal(SIGINT, SIG_IGN) + let sigintSrc = DispatchSource.makeSignalSource(signal: SIGINT) + sigintSrc.setEventHandler { + print("\n[vphone] SIGINT — shutting down") + vm.stopConsoleCapture() + Foundation.exit(0) + } + sigintSrc.activate() + + // Start VM + try await vm.start(forceDFU: dfu, stopOnPanic: stopOnPanic, stopOnFatalError: stopOnFatalError) + + if vncExperimental { + NSApplication.shared.setActivationPolicy(.prohibited) + let vnc = try VPhoneVNC(virtualMachine: vm.virtualMachine) + let vncURL = try await vnc.waitForURL() + print("[vphone] VNC password : \(vnc.password)") + print("[vphone] VNC URL : \(vncURL)") + NSWorkspace.shared.open(vncURL) + await vm.waitUntilStopped() + vnc.stop() + } else if noGraphics { + // Headless: just wait + NSApplication.shared.setActivationPolicy(.prohibited) + await vm.waitUntilStopped() + } else { + // GUI: show VM window with touch support + let windowController = VPhoneWindowController() + windowController.showWindow(for: vm.virtualMachine) + await vm.waitUntilStopped() + } + } +} diff --git a/experimental-vnc/Sources/vphone-cli/VPhoneVM.swift b/experimental-vnc/Sources/vphone-cli/VPhoneVM.swift new file mode 100644 index 0000000..995a4f5 --- /dev/null +++ b/experimental-vnc/Sources/vphone-cli/VPhoneVM.swift @@ -0,0 +1,213 @@ +import Foundation +import Virtualization +import VPhoneObjC + +/// Minimal VM for booting a vphone (virtual iPhone) in DFU mode. +class VPhoneVM: NSObject, VZVirtualMachineDelegate { + let virtualMachine: VZVirtualMachine + private var done = false + + struct Options { + var romURL: URL + var nvramURL: URL + var diskURL: URL + var cpuCount: Int = 4 + var memorySize: UInt64 = 4 * 1024 * 1024 * 1024 + var skipSEP: Bool = true + var sepStorageURL: URL? + var sepRomURL: URL? + var serialLogPath: String? = nil + var stopOnPanic: Bool = false + var stopOnFatalError: Bool = false + } + + private var consoleLogFileHandle: FileHandle? + + init(options: Options) throws { + // --- Hardware model (PV=3) --- + let hwModel = try VPhoneHardware.createModel() + print("[vphone] PV=3 hardware model: isSupported = true") + + // --- Platform --- + let platform = VZMacPlatformConfiguration() + + // Persist machineIdentifier for stable ECID (same as vrevm) + let machineIDPath = options.nvramURL.deletingLastPathComponent() + .appendingPathComponent("machineIdentifier.bin") + if let savedData = try? Data(contentsOf: machineIDPath), + let savedID = VZMacMachineIdentifier(dataRepresentation: savedData) { + platform.machineIdentifier = savedID + print("[vphone] Loaded machineIdentifier (ECID stable)") + } else { + let newID = VZMacMachineIdentifier() + platform.machineIdentifier = newID + try newID.dataRepresentation.write(to: machineIDPath) + print("[vphone] Created new machineIdentifier -> \(machineIDPath.lastPathComponent)") + } + + let auxStorage = try VZMacAuxiliaryStorage( + creatingStorageAt: options.nvramURL, + hardwareModel: hwModel, + options: .allowOverwrite, + ) + platform.auxiliaryStorage = auxStorage + platform.hardwareModel = hwModel + // platformFusing = prod (same as vrevm config) + + // Set NVRAM boot-args to enable serial output (same as vrevm restore) + let bootArgs = "serial=00 debug=0x104c04 cs_enforcement_disable=1 amfi_allow_any_signature=1 txm_cs_disable=1 amfi=0x8f" + if let bootArgsData = bootArgs.data(using: .utf8) { + if VPhoneSetNVRAMVariable(auxStorage, "boot-args", bootArgsData) { + print("[vphone] NVRAM boot-args: \(bootArgs)") + } + } + + // --- Boot loader with custom ROM --- + let bootloader = VZMacOSBootLoader() + VPhoneSetBootLoaderROMURL(bootloader, options.romURL) + + // --- VM Configuration --- + let config = VZVirtualMachineConfiguration() + config.bootLoader = bootloader + config.platform = platform + config.cpuCount = max(options.cpuCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount) + config.memorySize = max(options.memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize) + + // Display (vresearch101: 1290x2796 @ 460 PPI — matches vrevm) + let gfx = VZMacGraphicsDeviceConfiguration() + gfx.displays = [ + VZMacGraphicsDisplayConfiguration(widthInPixels: 1290, heightInPixels: 2796, pixelsPerInch: 460), + ] + config.graphicsDevices = [gfx] + + // Storage + if FileManager.default.fileExists(atPath: options.diskURL.path) { + let attachment = try VZDiskImageStorageDeviceAttachment(url: options.diskURL, readOnly: false) + config.storageDevices = [VZVirtioBlockDeviceConfiguration(attachment: attachment)] + } + + // Network (shared NAT) + let net = VZVirtioNetworkDeviceConfiguration() + net.attachment = VZNATNetworkDeviceAttachment() + config.networkDevices = [net] + + // Serial port (PL011 UART — always configured) + // Connect host stdin/stdout directly for interactive serial console + do { + if let serialPort = VPhoneCreatePL011SerialPort() { + serialPort.attachment = VZFileHandleSerialPortAttachment( + fileHandleForReading: FileHandle.standardInput, + fileHandleForWriting: FileHandle.standardOutput + ) + config.serialPorts = [serialPort] + print("[vphone] PL011 serial port attached (interactive)") + } + + // Set up log file if requested + if let logPath = options.serialLogPath { + let logURL = URL(fileURLWithPath: logPath) + FileManager.default.createFile(atPath: logURL.path, contents: nil) + self.consoleLogFileHandle = FileHandle(forWritingAtPath: logURL.path) + print("[vphone] Serial log: \(logPath)") + } + } + + // Multi-touch (USB touch screen for VNC click support) + VPhoneConfigureMultiTouch(config) + VPhoneConfigureUSBKeyboard(config) + + // GDB debug stub (default init, system-assigned port — same as vrevm) + VPhoneSetGDBDebugStubDefault(config) + + // Coprocessors + if options.skipSEP { + print("[vphone] SKIP_SEP=1 — no coprocessor") + } else if let sepStorageURL = options.sepStorageURL { + VPhoneConfigureSEP(config, sepStorageURL, options.sepRomURL) + print("[vphone] SEP coprocessor enabled (storage: \(sepStorageURL.path))") + } else { + // Create default SEP storage next to NVRAM + let defaultSEPURL = options.nvramURL.deletingLastPathComponent() + .appendingPathComponent("sep_storage.bin") + VPhoneConfigureSEP(config, defaultSEPURL, options.sepRomURL) + print("[vphone] SEP coprocessor enabled (storage: \(defaultSEPURL.path))") + } + + // Validate + try config.validate() + print("[vphone] Configuration validated") + + virtualMachine = VZVirtualMachine(configuration: config) + super.init() + virtualMachine.delegate = self + } + + // MARK: - DFU start + + @MainActor + func start(forceDFU: Bool, stopOnPanic: Bool, stopOnFatalError: Bool) async throws { + let opts = VZMacOSVirtualMachineStartOptions() + VPhoneConfigureStartOptions(opts, forceDFU, stopOnPanic, stopOnFatalError) + print("[vphone] Starting\(forceDFU ? " DFU" : "")...") + try await virtualMachine.start(options: opts) + if forceDFU { + print("[vphone] VM started in DFU mode — connect with irecovery") + } else { + print("[vphone] VM started — booting normally") + } + } + + // MARK: - Wait + + func waitUntilStopped() async { + while !done { + try? await Task.sleep(nanoseconds: 500_000_000) + } + } + + // MARK: - Delegate + + func guestDidStop(_: VZVirtualMachine) { + print("[vphone] Guest stopped") + done = true + } + + func virtualMachine(_: VZVirtualMachine, didStopWithError error: Error) { + print("[vphone] Stopped with error: \(error)") + done = true + } + + func virtualMachine(_: VZVirtualMachine, networkDevice _: VZNetworkDevice, + attachmentWasDisconnectedWithError error: Error) + { + print("[vphone] Network error: \(error)") + } + + // MARK: - Cleanup + + func stopConsoleCapture() { + consoleLogFileHandle?.closeFile() + } +} + +// MARK: - Errors + +enum VPhoneError: Error, CustomStringConvertible { + case hardwareModelNotSupported + case romNotFound(String) + + var description: String { + switch self { + case .hardwareModelNotSupported: + """ + PV=3 hardware model not supported. Check: + 1. macOS >= 15.0 (Sequoia) + 2. Signed with com.apple.private.virtualization + \ + com.apple.private.virtualization.security-research + 3. SIP/AMFI disabled + """ + case let .romNotFound(p): + "ROM not found: \(p)" + } + } +} diff --git a/experimental-vnc/Sources/vphone-cli/VPhoneVNC.swift b/experimental-vnc/Sources/vphone-cli/VPhoneVNC.swift new file mode 100644 index 0000000..c4b0abe --- /dev/null +++ b/experimental-vnc/Sources/vphone-cli/VPhoneVNC.swift @@ -0,0 +1,45 @@ +import AppKit +import Foundation +import VPhoneObjC +import Virtualization + +class VPhoneVNC { + private let vncServer: AnyObject + let password: String + + init(virtualMachine: VZVirtualMachine) throws { + let words = ["apple", "swift", "phone", "vnc", "boot", "tart", "mac", "arm"] + password = (0..<4).map { _ in words[Int.random(in: 0.. URL { + while true { + let port = VPhoneGetVNCPort(vncServer) + if port != 0 { + return URL(string: "vnc://:\(password)@127.0.0.1:\(port)")! + } + try await Task.sleep(nanoseconds: 50_000_000) + } + } + + func stop() { + VPhoneStopVNCServer(vncServer) + } + + deinit { + stop() + } +} + +enum VPhoneVNCError: Error, CustomStringConvertible { + case serverCreationFailed + + var description: String { + "Failed to create _VZVNCServer" + } +} diff --git a/experimental-vnc/enable.sh b/experimental-vnc/enable.sh new file mode 100644 index 0000000..569075f --- /dev/null +++ b/experimental-vnc/enable.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="${1:-}" + +if [ -z "$PROJECT_DIR" ]; then + echo "Usage: $0 " + exit 1 +fi + +if [ ! -d "$PROJECT_DIR/Sources" ]; then + echo "ERROR: Sources directory not found: $PROJECT_DIR/Sources" + exit 1 +fi + +copy_file() { + local rel="$1" + local src="$SCRIPT_DIR/$rel" + local dst="$PROJECT_DIR/$rel" + mkdir -p "$(dirname "$dst")" + cp "$src" "$dst" + echo " updated: $rel" +} + +echo "[vphone-aio] Applying experimental VNC source overrides..." +copy_file "Sources/vphone-cli/VPhoneCLI.swift" +copy_file "Sources/vphone-cli/VPhoneVNC.swift" +copy_file "Sources/vphone-cli/VPhoneVM.swift" +copy_file "Sources/VPhoneObjC/include/VPhoneObjC.h" +copy_file "Sources/VPhoneObjC/VPhoneObjC.m" + +BOOT_SH="$PROJECT_DIR/boot.sh" +if [ -f "$BOOT_SH" ] && ! grep -q -- '--vnc-experimental' "$BOOT_SH"; then + awk ' + { + print $0 + if ($0 ~ /--no-graphics/) { + print " --vnc-experimental \\" + } + } + ' "$BOOT_SH" > "$BOOT_SH.tmp" + mv "$BOOT_SH.tmp" "$BOOT_SH" + chmod +x "$BOOT_SH" + echo " updated: boot.sh (added --vnc-experimental)" +fi + +echo "[vphone-aio] Experimental VNC mode enabled." From fb431077be2fc0218dba27a5553df768750316f7 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sat, 7 Mar 2026 22:26:36 +0900 Subject: [PATCH 2/6] update launcher for experimental vnc mode --- experimental-vnc/enable.sh | 0 vphone-aio.sh | 7 +++++++ 2 files changed, 7 insertions(+) mode change 100644 => 100755 experimental-vnc/enable.sh diff --git a/experimental-vnc/enable.sh b/experimental-vnc/enable.sh old mode 100644 new mode 100755 diff --git a/vphone-aio.sh b/vphone-aio.sh index 32fd948..6020261 100755 --- a/vphone-aio.sh +++ b/vphone-aio.sh @@ -18,6 +18,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ARCHIVE="$SCRIPT_DIR/vphone-cli.tar.zst" PROJECT="$SCRIPT_DIR/vphone-cli" +VNC_EXPERIMENTAL="${VPHONE_VNC_EXPERIMENTAL:-0}" BOOT_PID="" IPROXY_SSH_PID="" @@ -147,6 +148,12 @@ else echo "[1/4] vphone-cli/ already exists, skipping merge & extraction." fi +if [ "$VNC_EXPERIMENTAL" = "1" ]; then + echo "[2/4] Enabling experimental VNC mode ..." + "$SCRIPT_DIR/experimental-vnc/enable.sh" "$PROJECT" + echo "" +fi + echo "" # ── Start iproxy tunnels ───────────────────────────────────────── From 15d50e529d5846eae9e686575358457a159c538e Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sat, 7 Mar 2026 22:26:59 +0900 Subject: [PATCH 3/6] update readme for experimental vnc mode --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index e153641..3010f52 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,21 @@ Do this step by step: 9. Connect VNC (using RealVNC or Screen Sharing): `vnc://127.0.0.1:5901` 10. Enjoy! +### Experimental VNC mode (faster input / hotkeys) + +If default VNC feels laggy, enable the experimental Virtualization.framework VNC path: + +```bash +VPHONE_VNC_EXPERIMENTAL=1 ./vphone-aio.sh +``` + +When enabled, startup logs will print a random VNC URL and password, for example: + +```text +[vphone] VNC password : swift-mac-arm-vnc +[vphone] VNC URL : vnc://:swift-mac-arm-vnc@127.0.0.1:57739 +``` + ## SHA-256 Checksums To verify your downloaded files are not corrupted: From c98fa78c3e59ddc8a98d9a1b03264e23ed4366d8 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sat, 7 Mar 2026 22:32:39 +0900 Subject: [PATCH 4/6] fix experimental vnc boot option conflict --- experimental-vnc/enable.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/experimental-vnc/enable.sh b/experimental-vnc/enable.sh index 569075f..d80e215 100755 --- a/experimental-vnc/enable.sh +++ b/experimental-vnc/enable.sh @@ -34,9 +34,10 @@ BOOT_SH="$PROJECT_DIR/boot.sh" if [ -f "$BOOT_SH" ] && ! grep -q -- '--vnc-experimental' "$BOOT_SH"; then awk ' { - print $0 if ($0 ~ /--no-graphics/) { print " --vnc-experimental \\" + } else { + print $0 } } ' "$BOOT_SH" > "$BOOT_SH.tmp" From dee58e8eee946d49c5d0880e1a00cc4a5bc7ecba Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sat, 7 Mar 2026 22:36:19 +0900 Subject: [PATCH 5/6] set experimental vnc defaults to 5901/alpine --- README.md | 20 ++++++++++++++++--- .../Sources/VPhoneObjC/VPhoneObjC.m | 4 ++-- .../Sources/VPhoneObjC/include/VPhoneObjC.h | 4 ++-- .../Sources/vphone-cli/VPhoneVNC.swift | 8 +++++--- vphone-aio.sh | 3 +++ 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3010f52..8d2e887 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,25 @@ If default VNC feels laggy, enable the experimental Virtualization.framework VNC VPHONE_VNC_EXPERIMENTAL=1 ./vphone-aio.sh ``` -When enabled, startup logs will print a random VNC URL and password, for example: +When enabled, startup logs will print VNC URL/password. +Default values in experimental mode: +- Port: `5901` +- Password: `alpine` + +You can override defaults: + +```bash +VPHONE_VNC_EXPERIMENTAL=1 \ +VPHONE_VNC_PORT=5901 \ +VPHONE_VNC_PASSWORD=alpine \ +./vphone-aio.sh +``` + +Example output: ```text -[vphone] VNC password : swift-mac-arm-vnc -[vphone] VNC URL : vnc://:swift-mac-arm-vnc@127.0.0.1:57739 +[vphone] VNC password : alpine +[vphone] VNC URL : vnc://:alpine@127.0.0.1:5901 ``` ## SHA-256 Checksums diff --git a/experimental-vnc/Sources/VPhoneObjC/VPhoneObjC.m b/experimental-vnc/Sources/VPhoneObjC/VPhoneObjC.m index cc2bb9d..7addcdd 100644 --- a/experimental-vnc/Sources/VPhoneObjC/VPhoneObjC.m +++ b/experimental-vnc/Sources/VPhoneObjC/VPhoneObjC.m @@ -268,7 +268,7 @@ void VPhoneSendMultiTouchEvents(id multiTouchDevice, NSArray *events) { send(multiTouchDevice, sel, events); } -id VPhoneCreateVNCServer(VZVirtualMachine *virtualMachine, NSString *password) { +id VPhoneCreateVNCServer(VZVirtualMachine *virtualMachine, NSString *password, uint16_t port) { Class secClass = NSClassFromString(@"_VZVNCAuthenticationSecurityConfiguration"); if (!secClass) { NSLog(@"[vphone] WARNING: _VZVNCAuthenticationSecurityConfiguration not found"); @@ -294,7 +294,7 @@ id VPhoneCreateVNCServer(VZVirtualMachine *virtualMachine, NSString *password) { id (*initVNC)(id, SEL, NSInteger, dispatch_queue_t, id) = (id (*)(id, SEL, NSInteger, dispatch_queue_t, id))objc_msgSend; id server = initVNC([serverClass alloc], initSel, - (NSInteger)0, + (NSInteger)port, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), secConfig); if (!server) { diff --git a/experimental-vnc/Sources/VPhoneObjC/include/VPhoneObjC.h b/experimental-vnc/Sources/VPhoneObjC/include/VPhoneObjC.h index 85ef9f3..0d338c9 100644 --- a/experimental-vnc/Sources/VPhoneObjC/include/VPhoneObjC.h +++ b/experimental-vnc/Sources/VPhoneObjC/include/VPhoneObjC.h @@ -82,9 +82,9 @@ void VPhoneSendMultiTouchEvents(id multiTouchDevice, NSArray *events); // --- VNC (experimental, Virtualization.framework built-in) --- -/// Create and start a _VZVNCServer bound to a random port (port:0). +/// Create and start a _VZVNCServer bound to the given port. /// Returns the server object (opaque AnyObject for Swift), or nil on failure. -id _Nullable VPhoneCreateVNCServer(VZVirtualMachine *virtualMachine, NSString *password); +id _Nullable VPhoneCreateVNCServer(VZVirtualMachine *virtualMachine, NSString *password, uint16_t port); /// Return the current port the VNC server is listening on. /// Returns 0 if not yet assigned. diff --git a/experimental-vnc/Sources/vphone-cli/VPhoneVNC.swift b/experimental-vnc/Sources/vphone-cli/VPhoneVNC.swift index c4b0abe..f391a52 100644 --- a/experimental-vnc/Sources/vphone-cli/VPhoneVNC.swift +++ b/experimental-vnc/Sources/vphone-cli/VPhoneVNC.swift @@ -6,12 +6,14 @@ import Virtualization class VPhoneVNC { private let vncServer: AnyObject let password: String + let port: UInt16 init(virtualMachine: VZVirtualMachine) throws { - let words = ["apple", "swift", "phone", "vnc", "boot", "tart", "mac", "arm"] - password = (0..<4).map { _ in words[Int.random(in: 0.. Date: Sat, 7 Mar 2026 22:53:34 +0900 Subject: [PATCH 6/6] skip iproxy 5901 in experimental vnc mode --- vphone-aio.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/vphone-aio.sh b/vphone-aio.sh index e6314b4..6ce39c4 100755 --- a/vphone-aio.sh +++ b/vphone-aio.sh @@ -166,9 +166,13 @@ iproxy 22222 22222 >/dev/null 2>&1 & IPROXY_SSH_PID=$! echo " SSH : localhost:22222 -> device:22222" -iproxy 5901 5901 >/dev/null 2>&1 & -IPROXY_VNC_PID=$! -echo " VNC : localhost:5901 -> device:5901" +if [ "$VNC_EXPERIMENTAL" = "1" ]; then + echo " VNC : experimental mode enabled (skip iproxy 5901)" +else + iproxy 5901 5901 >/dev/null 2>&1 & + IPROXY_VNC_PID=$! + echo " VNC : localhost:5901 -> device:5901" +fi # ── Build & Boot VM ────────────────────────────────────────────── echo ""