From 8200cd5a55881c1888322849baaef51a64efb733 Mon Sep 17 00:00:00 2001 From: Lakr Date: Sun, 1 Mar 2026 02:47:52 +0900 Subject: [PATCH] Update README.md --- .gitignore | 1 + AGENTS.md | 58 ++-- CLAUDE.md | 17 +- Makefile | 17 +- Package.swift | 22 +- README.md | 11 +- researchs/jailbreak_patches.md | 119 ++++++++ researchs/keyboard_event_pipeline.md | 251 ++++++++++++++++ scripts/patchers/cfw.py | 73 +---- scripts/patchers/kernel.py | 4 +- scripts/ramdisk_build.py | 32 +- sources/vphone-cli/VPhoneAppDelegate.swift | 103 +++++++ sources/vphone-cli/VPhoneCLI.swift | 111 ++----- sources/vphone-cli/VPhoneError.swift | 24 ++ sources/vphone-cli/VPhoneHardwareModel.swift | 16 +- sources/vphone-cli/VPhoneKeyHelper.swift | 273 ++++++++++++++++++ sources/vphone-cli/VPhoneMenuController.swift | 141 +++++++++ sources/vphone-cli/VPhoneVM.swift | 186 +++++------- sources/vphone-cli/VPhoneVMView.swift | 16 + sources/vphone-cli/VPhoneVMWindow.swift | 249 ---------------- .../vphone-cli/VPhoneWindowController.swift | 38 +++ sources/vphone-cli/main.swift | 11 + sources/vphone-objc/VPhoneObjC.m | 256 ---------------- sources/vphone-objc/include/VPhoneObjC.h | 79 ----- 24 files changed, 1164 insertions(+), 944 deletions(-) mode change 100644 => 120000 CLAUDE.md create mode 100644 researchs/jailbreak_patches.md create mode 100644 researchs/keyboard_event_pipeline.md create mode 100644 sources/vphone-cli/VPhoneAppDelegate.swift create mode 100644 sources/vphone-cli/VPhoneError.swift create mode 100644 sources/vphone-cli/VPhoneKeyHelper.swift create mode 100644 sources/vphone-cli/VPhoneMenuController.swift create mode 100644 sources/vphone-cli/VPhoneVMView.swift delete mode 100644 sources/vphone-cli/VPhoneVMWindow.swift create mode 100644 sources/vphone-cli/VPhoneWindowController.swift create mode 100644 sources/vphone-cli/main.swift delete mode 100644 sources/vphone-objc/VPhoneObjC.m delete mode 100644 sources/vphone-objc/include/VPhoneObjC.h diff --git a/.gitignore b/.gitignore index e6c04f2..0262b49 100644 --- a/.gitignore +++ b/.gitignore @@ -310,3 +310,4 @@ __marimo__/ *.resolved /VM .limd/ +/.swiftpm diff --git a/AGENTS.md b/AGENTS.md index fbd4d66..959dcef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,17 @@ -# AGENTS — vphone-cli +# vphone-cli + +Virtual iPhone boot tool using Apple's Virtualization.framework with PCC research VMs. + +## Quick Reference + +- **Build:** `make build` +- **Boot (GUI):** `make boot` +- **Boot (DFU):** `make boot_dfu` +- **All targets:** `make help` +- **Python venv:** `make setup_venv` (installs to `.venv/`, activate with `source .venv/bin/activate`) +- **Platform:** macOS 14+ (Sequoia), SIP/AMFI disabled +- **Language:** Swift 6.0 (SwiftPM), private APIs via [Dynamic](https://github.com/mhdhejazi/Dynamic) +- **Python deps:** `capstone`, `keystone-engine`, `pyimg4` (see `requirements.txt`) ## Project Overview @@ -10,14 +23,17 @@ CLI tool that boots virtual iPhones (PV=3) via Apple's Virtualization.framework, Makefile # Single entry point — run `make help` sources/ -├── vphone-objc/ # ObjC bridge for private Virtualization.framework APIs -│ ├── include/VPhoneObjC.h -│ └── VPhoneObjC.m -└── vphone-cli/ # Swift executable - ├── VPhoneCLI.swift - ├── VPhoneVM.swift - ├── VPhoneHardwareModel.swift - └── VPhoneVMWindow.swift +├── vphone.entitlements # Private API entitlements (5 keys) +└── vphone-cli/ # Swift 6.0 executable (pure Swift, no ObjC) + ├── main.swift # Entry point — NSApplication + AppDelegate + ├── VPhoneAppDelegate.swift # App lifecycle, SIGINT, VM start/stop + ├── VPhoneCLI.swift # ArgumentParser options (no execution logic) + ├── VPhoneVM.swift # @MainActor VM configuration and lifecycle + ├── VPhoneHardwareModel.swift # PV=3 hardware model via Dynamic + ├── VPhoneVMView.swift # Touch-enabled VZVirtualMachineView + helpers + ├── VPhoneWindowController.swift # @MainActor window management + ├── VPhoneError.swift # Error types + └── MainActor+Isolated.swift # MainActor.isolated helper scripts/ ├── patchers/ # Python patcher package @@ -38,17 +54,16 @@ scripts/ ├── setup_venv.sh # Creates Python venv with native keystone dylib └── setup_libimobiledevice.sh # Builds libimobiledevice toolchain from source -Research/ # Research notes and verification reports researchs/ # Component analysis and architecture docs ``` ### Key Patterns -- **Private API access:** All private Virtualization.framework calls go through the ObjC bridge (`VPhoneObjC`). Swift code never calls private APIs directly. -- **Function naming:** ObjC bridge functions use the `VPhone` prefix (e.g., `VPhoneCreateHardwareModel`, `VPhoneConfigureSEP`). +- **Private API access:** Private Virtualization.framework APIs are called via the [Dynamic](https://github.com/mhdhejazi/Dynamic) library (runtime method dispatch from pure Swift). No ObjC bridge needed. +- **App lifecycle:** Explicit `main.swift` creates `NSApplication` + `VPhoneAppDelegate`. CLI args parsed before the run loop starts. AppDelegate drives VM start, window, and shutdown. - **Configuration:** CLI options parsed via `ArgumentParser`, converted to `VPhoneVM.Options` struct, then used to build `VZVirtualMachineConfiguration`. - **Error handling:** `VPhoneError` enum with `CustomStringConvertible` for user-facing messages. -- **Window management:** `VPhoneWindowController` wraps `NSWindow` + `VZVirtualMachineView`. Touch input translated from mouse events to multi-touch via `VPhoneVMView`. +- **Window management:** `VPhoneWindowController` wraps `NSWindow` + `VZVirtualMachineView`. Window size derived from configurable screen dimensions and scale factor. Touch input translated from mouse events to multi-touch via `VPhoneVMView`. --- @@ -184,17 +199,13 @@ AVPBooter (ROM, PCC) ### Swift +- **Language:** Swift 6.0 (strict concurrency). - **Style:** Pragmatic, minimal. No unnecessary abstractions. - **Sections:** Use `// MARK: -` to organize code within files. - **Access control:** Default (internal). Only mark `private` when needed for clarity. -- **Async:** Use `async/await` for VM lifecycle. `@MainActor` for UI and VM start operations. +- **Concurrency:** `@MainActor` for VM and UI classes. `nonisolated` delegate methods use `MainActor.isolated {}` to hop back safely. - **Naming:** Types are `VPhone`-prefixed (`VPhoneVM`, `VPhoneWindowController`). Match Apple framework conventions. - -### ObjC Bridge - -- All functions are C-style (no ObjC classes exposed to Swift). -- Return `nil`/`NULL` on failure — caller handles gracefully. -- Header documents the private API being wrapped in each function's doc comment. +- **Private APIs:** Use `Dynamic()` for runtime method dispatch. Touch objects use `NSClassFromString` + KVC to avoid designated initializer crashes. ### Shell Scripts @@ -228,7 +239,12 @@ Creates a VM directory with: - Sparse disk image (default 64 GB) - SEP storage (512 KB flat file) - AVPBooter + AVPSEPBooter ROMs (copied from `/System/Library/Frameworks/Virtualization.framework/`) -- NVRAM and machineIdentifier auto-created on first boot +- machineIdentifier (created on first boot if missing, persisted for stable ECID) +- NVRAM (created/overwritten each boot) + +All paths are passed explicitly via CLI (`--rom`, `--disk`, `--nvram`, `--machine-id`, `--sep-storage`, `--sep-rom`). SEP coprocessor is always enabled. + +Display is configurable via `--screen-width`, `--screen-height`, `--screen-ppi`, `--screen-scale` (defaults: 1290x2796 @ 460 PPI, scale 3.0). Override defaults: `make vm_new VM_DIR=myvm DISK_SIZE=32`. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index d112d49..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,16 +0,0 @@ -# vphone-cli - -Virtual iPhone boot tool using Apple's Virtualization.framework with PCC research VMs. - -See [AGENTS.md](./AGENTS.md) for project conventions, architecture, and design system. - -## Quick Reference - -- **Build:** `make build` -- **Boot (headless):** `make boot` -- **Boot (DFU):** `make boot_dfu` -- **All targets:** `make help` -- **Python venv:** `make setup_venv` (installs to `.venv/`, activate with `source .venv/bin/activate`) -- **Platform:** macOS 14+ (Sequoia), SIP/AMFI disabled -- **Language:** Swift 5.10 (SwiftPM), ObjC bridge for private APIs -- **Python deps:** `capstone`, `keystone-engine`, `pyimg4` (see `requirements.txt`) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Makefile b/Makefile index b3f0d80..a1aa60b 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,8 @@ # ─── Configuration (override with make VAR=value) ───────────────── VM_DIR ?= vm -CPU ?= 4 -MEMORY ?= 4096 +CPU ?= 8 +MEMORY ?= 8192 DISK_SIZE ?= 64 # ─── Paths ──────────────────────────────────────────────────────── @@ -18,7 +18,7 @@ IRECOVERY := $(LIMD_PREFIX)/bin/irecovery IDEVICERESTORE := $(LIMD_PREFIX)/bin/idevicerestore PYTHON := $(CURDIR)/$(VENV)/bin/python3 -SWIFT_SOURCES := $(shell find sources -name '*.swift' -o -name '*.m' -o -name '*.h') +SWIFT_SOURCES := $(shell find sources -name '*.swift') # ─── Environment — prefer project-local binaries ──────────────── export PATH := $(CURDIR)/$(LIMD_PREFIX)/bin:$(CURDIR)/$(VENV)/bin:$(CURDIR)/.build/release:$(PATH) @@ -39,7 +39,7 @@ help: @echo "" @echo "VM management:" @echo " make vm_new Create VM directory" - @echo " make boot Boot VM (headless)" + @echo " make boot Boot VM (GUI)" @echo " make boot_dfu Boot VM in DFU mode" @echo "" @echo "Firmware pipeline:" @@ -110,21 +110,18 @@ boot: build --rom ./AVPBooter.vresearch1.bin \ --disk ./Disk.img \ --nvram ./nvram.bin \ + --machine-id ./machineIdentifier.bin \ --cpu $(CPU) --memory $(MEMORY) \ - --serial-log ./serial.log \ - --stop-on-panic --stop-on-fatal-error \ --sep-rom ./AVPSEPBooter.vresearch1.bin \ - --sep-storage ./SEPStorage \ - --no-graphics + --sep-storage ./SEPStorage boot_dfu: build cd $(VM_DIR) && "$(CURDIR)/$(BINARY)" \ --rom ./AVPBooter.vresearch1.bin \ --disk ./Disk.img \ --nvram ./nvram.bin \ + --machine-id ./machineIdentifier.bin \ --cpu $(CPU) --memory $(MEMORY) \ - --serial-log ./serial.log \ - --stop-on-panic --stop-on-fatal-error \ --sep-rom ./AVPSEPBooter.vresearch1.bin \ --sep-storage ./SEPStorage \ --no-graphics --dfu diff --git a/Package.swift b/Package.swift index 963fec4..9ab30e8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,40 +1,28 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 import PackageDescription let package = Package( name: "vphone-cli", platforms: [ - .macOS(.v14), + .macOS(.v15), ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.1"), + .package(url: "https://github.com/mhdhejazi/Dynamic", from: "1.2.0"), ], targets: [ - // ObjC module: wraps private Virtualization.framework APIs - .target( - name: "VPhoneObjC", - path: "sources/vphone-objc", - publicHeadersPath: "include", - linkerSettings: [ - .linkedFramework("Virtualization"), - ] - ), - // Swift executable .executableTarget( name: "vphone-cli", dependencies: [ - "VPhoneObjC", .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Dynamic", package: "Dynamic"), ], path: "sources/vphone-cli", - swiftSettings: [ - .unsafeFlags(["-parse-as-library"]), - ], linkerSettings: [ .linkedFramework("Virtualization"), .linkedFramework("AppKit"), ] - ) + ), ] ) diff --git a/README.md b/README.md index bed021a..91c0ba8 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@ Boot a virtual iPhone (iOS 26) via Apple's Virtualization.framework using PCC re ## Tested Environments -| Record | Host macOS | Host Hardware | iPhone IPSW | CloudOS IPSW | -|--------|------------|---------------|-------------|--------------| -| 1 | macOS 26.3 (Tahoe, Build 25D125) | MacBook Air, Apple M4 | `iPhone17,3_26.1_23B85_Restore.ipsw` | `PCC-CloudOS-26.1-23B85.ipsw` | -| 2 | macOS 26.3 (Tahoe, Build 25D125) | MacBook Air, Apple M4 | `iPhone17,3_26.3_23D127_Restore.ipsw` | `PCC-CloudOS-26.1-23B85.ipsw` | +| Host | iPhone | CloudOS | +|------|--------|---------| +| Mac16,12 26.3 | `17,3_26.1_23B85` | `26.1-23B85` | +| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.1-23B85` | +| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.3-23D128` | ## Prerequisites @@ -103,7 +104,7 @@ Run `make help` for the full list. Key targets: | `vm_new` | Create VM directory | | `fw_prepare` | Download/merge IPSWs | | `fw_patch` | Patch boot chain | -| `boot` / `boot_dfu` | Boot VM (normal / DFU) | +| `boot` / `boot_dfu` | Boot VM (GUI / DFU headless) | | `restore_get_shsh` | Fetch SHSH blob | | `restore` | Flash firmware | | `ramdisk_build` | Build SSH ramdisk | diff --git a/researchs/jailbreak_patches.md b/researchs/jailbreak_patches.md new file mode 100644 index 0000000..5380550 --- /dev/null +++ b/researchs/jailbreak_patches.md @@ -0,0 +1,119 @@ +# Jailbreak Patches vs Base Patches + +Comparison of base boot-chain patches (`make fw_patch`) vs jailbreak-extended patches (`make fw_patch_jb`). + +Base patches enable VM boot with signature bypass and SSV override. +Jailbreak patches add code signing bypass, entitlement spoofing, task/VM security bypass, +sandbox hook neutralization, and kernel arbitrary call (kcall10). + +## iBSS + +| # | Patch | Purpose | Base | JB | +| --- | --------------------------------- | --------------------------------------- | :--: | :-: | +| 1 | Serial labels (2x) | "Loaded iBSS" in serial log | Y | Y | +| 2 | image4_validate_property_callback | Signature bypass (nop b.ne + mov x0,#0) | Y | Y | +| 3 | Skip generate_nonce | Keep apnonce stable for SHSH | — | Y | + +## iBEC + +| # | Patch | Purpose | Base | JB | +| --- | --------------------------------- | ------------------------------ | :--: | :-: | +| 1 | Serial labels (2x) | "Loaded iBEC" in serial log | Y | Y | +| 2 | image4_validate_property_callback | Signature bypass | Y | Y | +| 3 | Boot-args redirect | `serial=3 -v debug=0x2014e %s` | Y | Y | + +No additional JB patches for iBEC. + +## LLB + +| # | Patch | Purpose | Base | JB | +| --- | --------------------------------- | ---------------------------------- | :--: | :-: | +| 1 | Serial labels (2x) | "Loaded LLB" in serial log | Y | Y | +| 2 | image4_validate_property_callback | Signature bypass | Y | Y | +| 3 | Boot-args redirect | `serial=3 -v debug=0x2014e %s` | Y | Y | +| 4 | Rootfs bypass (5 patches) | Allow edited rootfs loading | Y | Y | +| 5 | Panic bypass | NOP cbnz after mov w8,#0x328 check | Y | Y | + +No additional JB patches for LLB. + +## TXM + +| # | Patch | Purpose | Base | JB | +| --- | ------------------------------------------ | --------------------------------- | :------------------------------------------: | :-: | --- | +| 1 | Trustcache binary search bypass | `bl hash_cmp → mov x0,#0` | Y | Y | +| 2 | CodeSignature selector 24 (3x mov x0,#0) | Bypass CS validation return paths | — | Y | +| 3 | CodeSignature selector 24 | 0xA1 (2x nop) | Bypass CS error path | — | Y | +| 4 | get-task-allow (selector 41 | 29) | `mov x0,#1` — allow get-task-allow | — | Y | +| 5 | Selector 42 | 29 + shellcode | Branch to shellcode that sets flag + returns | — | Y | +| 6 | com.apple.private.cs.debugger (selector 42 | 37) | `mov w0,#1` — allow debugger entitlement | — | Y | +| 7 | Developer mode bypass | NOP developer mode enforcement | — | Y | + +## Kernelcache + +### Base patches (SSV + basic AMFI + sandbox) + +| # | Patch | Function | Purpose | Base | JB | +| ----- | ------------------------ | -------------------------------- | ------------------------------------- | :--: | :-: | +| 1 | NOP panic | `_apfs_vfsop_mount` | Skip "root snapshot" panic | Y | Y | +| 2 | NOP panic | `_authapfs_seal_is_broken` | Skip "root volume seal" panic | Y | Y | +| 3 | NOP panic | `_bsd_init` | Skip "rootvp not authenticated" panic | Y | Y | +| 4-5 | mov w0,#0; ret | `_proc_check_launch_constraints` | Bypass launch constraints | Y | Y | +| 6-7 | mov x0,#1 (2x) | `PE_i_can_has_debugger` | Enable kernel debugger | Y | Y | +| 8 | NOP | `_postValidation` | Skip AMFI post-validation | Y | Y | +| 9 | cmp w0,w0 | `_postValidation` | Force comparison true | Y | Y | +| 10-11 | mov w0,#1 (2x) | `_check_dyld_policy_internal` | Allow dyld loading | Y | Y | +| 12 | mov w0,#0 | `_apfs_graft` | Allow APFS graft | Y | Y | +| 13 | cmp x0,x0 | `_apfs_vfsop_mount` | Skip mount check | Y | Y | +| 14 | mov w0,#0 | `_apfs_mount_upgrade_checks` | Allow mount upgrade | Y | Y | +| 15 | mov w0,#0 | `_handle_fsioc_graft` | Allow fsioc graft | Y | Y | +| 16-25 | mov x0,#0; ret (5 hooks) | Sandbox MACF ops table | Stub 5 sandbox hooks | Y | Y | + +### Jailbreak-only kernel patches + +| # | Patch | Function | Purpose | Base | JB | +| --- | -------------------------- | ------------------------------------ | ------------------------------------------ | :--: | :-: | +| 26 | Rewrite function | `AMFIIsCDHashInTrustCache` | Always return true + store hash | — | Y | +| 27 | Shellcode + branch | `_cred_label_update_execve` | Set cs_flags (platform+entitlements) | — | Y | +| 28 | cmp w0,w0 | `_postValidation` (additional) | Force validation pass | — | Y | +| 29 | Shellcode + branch | `_syscallmask_apply_to_proc` | Patch zalloc_ro_mut for syscall mask | — | Y | +| 30 | Shellcode + ops redirect | `_hook_cred_label_update_execve` | vnode_getattr ownership propagation + suid | — | Y | +| 31 | mov x0,#0; ret (20+ hooks) | Sandbox MACF ops table (extended) | Stub remaining 20+ sandbox hooks | — | Y | +| 32 | cmp xzr,xzr | `_task_conversion_eval_internal` | Allow task conversion | — | Y | +| 33 | mov x0,#0; ret | `_proc_security_policy` | Bypass security policy | — | Y | +| 34 | NOP (2x) | `_proc_pidinfo` | Allow pid 0 info | — | Y | +| 35 | b (skip panic) | `_convert_port_to_map_with_flavor` | Skip kernel map panic | — | Y | +| 36 | NOP | `_vm_fault_enter_prepare` | Skip fault check | — | Y | +| 37 | b (skip check) | `_vm_map_protect` | Allow VM protect | — | Y | +| 38 | NOP + mov x8,xzr | `___mac_mount` | Bypass MAC mount check | — | Y | +| 39 | NOP | `_dounmount` | Allow unmount | — | Y | +| 40 | mov x0,#0 | `_bsd_init` (2nd) | Skip auth at @%s:%d | — | Y | +| 41 | NOP (2x) | `_spawn_validate_persona` | Skip persona validation | — | Y | +| 42 | NOP | `_task_for_pid` | Allow task_for_pid | — | Y | +| 43 | b (skip check) | `_load_dylinker` | Allow dylinker loading | — | Y | +| 44 | cmp x0,x0 | `_shared_region_map_and_slide_setup` | Force shared region | — | Y | +| 45 | NOP | `_verifyPermission` (NVRAM) | Allow NVRAM writes | — | Y | +| 46 | b (skip check) | `_IOSecureBSDRoot` | Skip secure root check | — | Y | +| 47 | Syscall 439 + shellcode | kcall10 (SYS_kas_info replacement) | Kernel arbitrary call from userspace | — | Y | +| 48 | Zero out | `_thid_should_crash` | Prevent GUARD_TYPE_MACH_PORT crash | — | Y | + +## CFW (cfw_install) + +| # | Patch | Binary | Purpose | Base | JB | +| --- | -------------------- | -------------------- | ------------------------------ | :--: | :-: | +| 1 | /%s.gl → /AA.gl | seputil | Gigalocker UUID fix | Y | Y | +| 2 | NOP cache validation | launchd_cache_loader | Allow modified launchd.plist | Y | Y | +| 3 | mov x0,#1; ret | mobileactivationd | Activation bypass | Y | Y | +| 4 | Plist injection | launchd.plist | bash/dropbear/trollvnc daemons | Y | Y | +| 5 | b (skip jetsam) | launchd | Prevent jetsam panic on boot | — | Y | + +## Summary + +| Binary | Base | JB-only | Total | +| ----------- | :----: | :------: | :------: | +| iBSS | 2 | 1 | 3 | +| iBEC | 3 | 0 | 3 | +| LLB | 6 | 0 | 6 | +| TXM | 1 | ~13 | ~14 | +| Kernelcache | 25 | ~23+ | ~48+ | +| CFW | 4 | 1 | 5 | +| **Total** | **41** | **~38+** | **~79+** | diff --git a/researchs/keyboard_event_pipeline.md b/researchs/keyboard_event_pipeline.md new file mode 100644 index 0000000..cce9422 --- /dev/null +++ b/researchs/keyboard_event_pipeline.md @@ -0,0 +1,251 @@ +# Virtualization.framework Keyboard Event Pipeline + +Reverse engineering findings for the keyboard event pipeline in Apple's +Virtualization.framework (macOS 26.2, version 259.3.3.0.0). Documents how +keyboard events flow from the macOS host to the virtual iPhone guest. + +--- + +## Event Flow Architecture + +There are two pipelines for sending keyboard events to the VM: + +### Pipeline 1: _VZKeyEvent -> sendKeyEvents: (Standard Keys) + +``` +_VZKeyEvent(type, keyCode) + -> _VZKeyboard.sendKeyEvents:(NSArray<_VZKeyEvent>) + -> table lookup: keyCode -> intermediate index + -> pack: uint64_t = (index << 32) | is_key_down + -> std::vector + -> [if type==2] sendKeyboardEventsHIDReport:keyboardID: (switch -> IOHIDEvent -> HID reports) + -> [fallback] eventSender.sendKeyboardEvents:keyboardID: (VzCore C++ layer) +``` + +### Pipeline 2: _processHIDReports (Raw HID Reports) + +``` +Raw HID report bytes + -> std::span{data_ptr, length} + -> std::vector>{begin, end, cap} + -> VZVirtualMachine._processHIDReports:forDevice:deviceType: + -> XpcEncoder::encode_data(span) -> xpc_data_create + -> XPC to VMM process +``` + +--- + +## _VZKeyEvent Structure + +From IDA + LLDB inspection: + +```c +struct _VZKeyEvent { // sizeof = 0x18 + uint8_t isa[8]; // offset 0x00 -- ObjC isa pointer + uint16_t _keyCode; // offset 0x08 -- Apple VK code (0x00-0xB2) + uint8_t _pad[6]; // offset 0x0A -- padding + int64_t _type; // offset 0x10 -- 0 = keyDown, 1 = keyUp +}; +``` + +Initializer: `_VZKeyEvent(type: Int64, keyCode: UInt16)` + +--- + +## _VZKeyboard Object Layout + +From LLDB memory dump: + +``` ++0x00: isa ++0x08: _eventSender (weak, id<_VZHIDAdditions, _VZKeyboardEventSender>) ++0x10: _deviceIdentifier (uint32_t) -- value 1 for first keyboard ++0x18: type (int64_t) -- 0 for USB keyboard, 2 for type that tries HIDReport first +``` + +--- + +## Lookup Tables in sendKeyEvents: + +Two tables indexed by Apple VK keyCode (0x00-0xB2, 179 entries x 8 bytes each): + +**Table 1** (validity flags): All valid entries = `0x0000000100000000` (bit 32 set). +Invalid entries = 0. + +**Table 2** (intermediate indices): Maps Apple VK codes to internal indices (0x00-0x72). + +The tables are OR'd: `combined = table1[vk] | table2[vk]`. Bit 32 check validates +the entry. The lower 32 bits of combined become the intermediate index. + +### Sample Table 2 Entries + +| Apple VK | Key | Table2 (Index) | HID Page | HID Usage | +|----------|---------|----------------|----------|-----------| +| 0x00 | A | 0x00 | 7 | 0x04 | +| 0x01 | S | 0x12 | 7 | 0x16 | +| 0x24 | Return | 0x24 | 7 | 0x28 | +| 0x31 | Space | 0x29 | 7 | 0x2C | +| 0x35 | Escape | 0x25 | 7 | 0x29 | +| 0x38 | Shift | 0x51 | 7 | 0xE1 | +| 0x37 | Command | 0x53 | 7 | 0xE3 | + +### Invalid VK Codes (both tables = 0, silently dropped) + +0x48 (Volume Up), 0x49 (Volume Down), 0x4A (Mute), and many others. + +--- + +## Packed Event Format (std::vector) + +Each element in the vector sent to `sendKeyboardEvents:keyboardID:`: + +``` +bits 63:32 = intermediate_index (from table2, lower 32 bits of combined) +bits 31:1 = 0 +bit 0 = is_key_down (1 = down, 0 = up) +``` + +--- + +## sendKeyboardEventsHIDReport Switch Statement + +For type-2 keyboards, the intermediate index is mapped to +`IOHIDEventCreateKeyboardEvent(page, usage)` via a large switch. + +### Standard Keyboard Entries (HID Page 7) + +| Index | HID Page | HID Usage | Meaning | +|-----------|----------|-----------|------------------| +| 0x00-0x19 | 7 | 4-29 | Letters a-z | +| 0x1A-0x23 | 7 | 30-39 | Digits 1-0 | +| 0x24 | 7 | 40 | Return | +| 0x25 | 7 | 41 | Escape | +| 0x29 | 7 | 44 | Space | +| 0x48-0x4B | 7 | 79-82 | Arrow keys | +| 0x50-0x53 | 7 | 224-227 | L-Ctrl/Shift/Alt/Cmd | + +### Consumer / System Entries (Non-Standard Pages) + +| Index | HID Page | HID Usage | Meaning | +|-------|----------|-----------|---------------------------| +| 0x6E | **12** | 671 | **Consumer Volume Down** | +| 0x6F | **12** | 674 | **Consumer Volume Up** | +| 0x70 | **12** | 207 | **Consumer Play/Pause** | +| 0x71 | **12** | 545 | **Consumer Snapshot** | +| 0x72 | **1** | 155 | **Generic Desktop Wake** | + +**Home/Menu (Consumer page 0x0C, usage 0x40) has NO intermediate index.** It cannot +be sent through Pipeline 1 at all. + +--- + +## _processHIDReports Parameter Format + +From IDA decompilation of +`VZVirtualMachine._processHIDReports:forDevice:deviceType:` at 0x2301b2310. + +The `void *` parameter is a **pointer to std::vector>**: + +``` +Level 3 (outermost): std::vector (24 bytes, passed by pointer) + +0x00: __begin_ (pointer to span array) + +0x08: __end_ (pointer past last span) + +0x10: __end_cap_ (capacity pointer) + +Level 2: std::span (16 bytes per element in the array) + +0x00: data_ptr (const unsigned char *) + +0x08: length (size_t) + +Level 1 (innermost): raw HID report bytes +``` + +The function iterates spans in the vector: + +```c +begin = *vec; // vec->__begin_ +end = *(vec + 1); // vec->__end_ +for (span = begin; span != end; span += 16) { + data_ptr = *(uint64_t*)span; + length = *(uint64_t*)(span + 8); + encoder.encode_data(data_ptr, length); // -> xpc_data_create +} +``` + +**deviceType**: 0 = keyboard, 1 = pointing device + +**device**: device identifier (uint32_t, matches `_VZKeyboard._deviceIdentifier`) + +--- + +## Crash Analysis: Why Raw Bytes Crashed + +Passing raw `[0x40, 0x00]` as the `void*` parameter: + +1. Function reads bytes as vector struct: begin = 0x0040 (first 8 bytes), end = garbage +2. Dereferences begin as span pointer -> reads from address ~0x0040 +3. Gets garbage data_ptr (0x700420e) and garbage length (0x300000020 = 12GB) +4. `xpc_data_create(0x700420e, 0x300000020)` -> EXC_BAD_ACCESS in memcpy + +The three-level indirection (vector -> span -> bytes) must be constructed correctly +or the framework will dereference invalid pointers. + +--- + +## Swift Implementation Notes + +### Accessing _VZKeyboard + +```swift +// Get keyboards array +let arr = Dynamic(vm)._keyboards.asObject as? NSArray +let keyboard = arr?.object(at: 0) as AnyObject + +// _deviceIdentifier is an ivar, not a property -- use KVC +(keyboard as? NSObject)?.value(forKey: "_deviceIdentifier") as? UInt32 +``` + +### Constructing std::vector for sendKeyboardEvents + +```swift +let data = UnsafeMutablePointer.allocate(capacity: 1) +data.pointee = (index << 32) | (isKeyDown ? 1 : 0) +var vec = (data, data.advanced(by: 1), data.advanced(by: 1)) +withUnsafeMutablePointer(to: &vec) { vecPtr in + Dynamic(vm).sendKeyboardEvents(UnsafeMutableRawPointer(vecPtr), keyboardID: deviceId) +} +``` + +### Constructing vector> for _processHIDReports + +```swift +let reportPtr = UnsafeMutablePointer.allocate(capacity: N) +// fill report bytes... + +let spanPtr = UnsafeMutablePointer.allocate(capacity: 2) +spanPtr[0] = Int(bitPattern: reportPtr) // data pointer +spanPtr[1] = N // length + +let vecPtr = UnsafeMutablePointer.allocate(capacity: 3) +vecPtr[0] = Int(bitPattern: UnsafeRawPointer(spanPtr)) // begin +vecPtr[1] = Int(bitPattern: UnsafeRawPointer(spanPtr).advanced(by: 16)) // end +vecPtr[2] = vecPtr[1] // cap + +Dynamic(vm)._processHIDReports(UnsafeRawPointer(vecPtr), forDevice: deviceId, deviceType: 0) +``` + +--- + +## Source Files + +- Class dumps: `/Users/qaq/Documents/GitHub/super-tart-vphone-private/Virtualization_26.2-class-dump/` +- IDA database: dyld_shared_cache_arm64e with Virtualization.framework + +### Key Functions Analyzed + +| Function | Address | +|----------|---------| +| `-[_VZKeyboard sendKeyEvents:]` | 0x2301b2f54 | +| `-[_VZKeyboard sendKeyboardEventsHIDReport:keyboardID:]` | 0x2301b3230 | +| `-[VZVirtualMachine(_VZHIDAdditions) _processHIDReports:forDevice:deviceType:]` | 0x2301b2310 | +| `-[VZVirtualMachineView _sendKeyEventsToVirtualMachine:]` | -- | +| `-[_VZHIDEventMonitor getHIDReportsFromHIDEvent:]` | 0x2301b2af0 | diff --git a/scripts/patchers/cfw.py b/scripts/patchers/cfw.py index 8227085..59e8eee 100755 --- a/scripts/patchers/cfw.py +++ b/scripts/patchers/cfw.py @@ -29,8 +29,8 @@ Dependencies: import os import plistlib -import re import struct +import subprocess import sys from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN @@ -228,10 +228,9 @@ def patch_seputil(filepath): def patch_launchd_cache_loader(filepath): """NOP the cache validation check in launchd_cache_loader. - Anchor strategies (in order): - 1. Search for "unsecure_cache" substring, resolve to full null-terminated - string start, find ADRP+ADD xref to it, NOP the nearby cbz/cbnz branch - 2. Verified known offset fallback + Anchor strategy: + Search for "unsecure_cache" substring, resolve to full null-terminated + string start, find ADRP+ADD xref to it, NOP the nearby cbz/cbnz branch. The binary checks boot-arg "launchd_unsecure_cache=" — if not found, it skips the unsecure path via a conditional branch. NOPping that branch @@ -243,7 +242,7 @@ def patch_launchd_cache_loader(filepath): text_sec = find_section(sections, "__TEXT,__text") if not text_sec: print(" [-] __TEXT,__text not found") - return _launchd_cache_fallback(filepath, data) + return False text_va, text_size, text_foff = text_sec @@ -318,9 +317,8 @@ def patch_launchd_cache_loader(filepath): print(f" [+] NOPped at 0x{branch_foff:X}") return True - # Strategy 2: Fallback to verified known offset - print(" Dynamic anchor not found, trying verified fallback...") - return _launchd_cache_fallback(filepath, data) + print(" [-] Dynamic anchor not found — all strategies exhausted") + return False def _find_cstring_start(data, match_off, section_foff): @@ -425,31 +423,6 @@ def _find_nearby_branch(data, ref_foff, text_foff, text_size): return -1 -def _launchd_cache_fallback(filepath, data): - """Fallback: verify known offset and NOP.""" - KNOWN_OFF = 0xB58 - - if KNOWN_OFF + 4 > len(data): - print(f" [-] Known offset 0x{KNOWN_OFF:X} out of bounds") - return False - - insns = disasm_at(data, KNOWN_OFF, 1) - if insns: - mn = insns[0].mnemonic - print(f" Fallback: {mn} {insns[0].op_str} at 0x{KNOWN_OFF:X}") - - # Verify it's a branch-type instruction (expected for this patch) - branch_types = {"cbz", "cbnz", "tbz", "tbnz", "b"} - if mn not in branch_types and not mn.startswith("b."): - print(f" [!] Warning: unexpected instruction type '{mn}' at known offset") - print(f" Expected a conditional branch. Proceeding anyway.") - - data[KNOWN_OFF : KNOWN_OFF + 4] = NOP - open(filepath, "wb").write(data) - print(f" [+] NOPped at 0x{KNOWN_OFF:X} (fallback)") - return True - - # ══════════════════════════════════════════════════════════════════ # 3. mobileactivationd — Hackivation bypass # ══════════════════════════════════════════════════════════════════ @@ -461,7 +434,6 @@ def patch_mobileactivationd(filepath): Anchor strategies (in order): 1. Search LC_SYMTAB for symbol containing "should_hactivate" 2. Parse ObjC metadata: methnames -> selrefs -> method_list -> IMP - 3. Verified known offset fallback The method determines if the device should self-activate (hackivation). Patching it to always return YES bypasses activation lock. @@ -481,15 +453,15 @@ def patch_mobileactivationd(filepath): if imp_foff < 0: imp_foff = _find_via_objc_metadata(data) - # Strategy 3: Fallback + # All dynamic strategies exhausted if imp_foff < 0: - print(" Dynamic anchor not found, trying verified fallback...") - return _mobileactivationd_fallback(filepath, data) + print(" [-] Dynamic anchor not found — all strategies exhausted") + return False # Verify the target looks like code if imp_foff + 8 > len(data): print(f" [-] IMP offset 0x{imp_foff:X} out of bounds") - return _mobileactivationd_fallback(filepath, data) + return False insns = disasm_at(data, imp_foff, 4) if insns: @@ -602,26 +574,6 @@ def _find_via_objc_metadata(data): return -1 -def _mobileactivationd_fallback(filepath, data): - """Fallback: verify known offset and patch.""" - KNOWN_OFF = 0x2F5F84 - - if KNOWN_OFF + 8 > len(data): - print(f" [-] Known offset 0x{KNOWN_OFF:X} out of bounds (size: {len(data)})") - return False - - insns = disasm_at(data, KNOWN_OFF, 4) - if insns: - print(f" Fallback: {insns[0].mnemonic} {insns[0].op_str} at 0x{KNOWN_OFF:X}") - - data[KNOWN_OFF : KNOWN_OFF + 4] = MOV_X0_1 - data[KNOWN_OFF + 4 : KNOWN_OFF + 8] = RET - - open(filepath, "wb").write(data) - print(f" [+] Patched at 0x{KNOWN_OFF:X} (fallback): mov x0, #1; ret") - return True - - # ══════════════════════════════════════════════════════════════════ # BuildManifest parsing # ══════════════════════════════════════════════════════════════════ @@ -660,7 +612,8 @@ def parse_cryptex_paths(manifest_path): def inject_daemons(plist_path, daemon_dir): """Inject bash/dropbear/trollvnc entries into launchd.plist.""" # Convert to XML first (macOS binary plist -> XML) - os.system(f'plutil -convert xml1 "{plist_path}" 2>/dev/null') + subprocess.run(["plutil", "-convert", "xml1", plist_path], + capture_output=True) with open(plist_path, "rb") as f: target = plistlib.load(f) diff --git a/scripts/patchers/kernel.py b/scripts/patchers/kernel.py index b0487b2..8354e37 100755 --- a/scripts/patchers/kernel.py +++ b/scripts/patchers/kernel.py @@ -12,13 +12,13 @@ Dependencies: keystone-engine, capstone import struct, plistlib from collections import defaultdict from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE -from capstone import Cs, CS_ARCH_ARM64, CS_MODE_ARM +from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN from capstone.arm64_const import (ARM64_OP_REG, ARM64_OP_IMM, ARM64_REG_W0, ARM64_REG_X0, ARM64_REG_X8) # ── Assembly / disassembly helpers ─────────────────────────────── _ks = Ks(KS_ARCH_ARM64, KS_MODE_LE) -_cs = Cs(CS_ARCH_ARM64, CS_MODE_ARM) +_cs = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN) _cs.detail = True diff --git a/scripts/ramdisk_build.py b/scripts/ramdisk_build.py index 385239c..932a15a 100755 --- a/scripts/ramdisk_build.py +++ b/scripts/ramdisk_build.py @@ -24,17 +24,14 @@ import glob import os import plistlib import shutil -import struct import subprocess import sys -import tempfile # Ensure sibling modules (patch_firmware) are importable when run from any CWD _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) if _SCRIPT_DIR not in sys.path: sys.path.insert(0, _SCRIPT_DIR) -from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE from pyimg4 import IM4P from fw_patch import ( @@ -46,26 +43,6 @@ from fw_patch import ( ) from patchers.iboot import IBootPatcher -# ══════════════════════════════════════════════════════════════════ -# ARM64 assembler -# ══════════════════════════════════════════════════════════════════ - -_ks = Ks(KS_ARCH_ARM64, KS_MODE_LE) - - -def asm(s, addr=0): - """Assemble an ARM64 instruction string to bytes.""" - enc, _ = _ks.asm(s, addr) - if not enc: - raise RuntimeError(f"asm failed: {s}") - return bytes(enc) - - -def asm_u32(s, addr=0): - """Assemble an ARM64 instruction and return as little-endian u32.""" - return struct.unpack(" Bool { + !cli.noGraphics + } +} diff --git a/sources/vphone-cli/VPhoneCLI.swift b/sources/vphone-cli/VPhoneCLI.swift index 5a2c6f5..06899d8 100644 --- a/sources/vphone-cli/VPhoneCLI.swift +++ b/sources/vphone-cli/VPhoneCLI.swift @@ -1,11 +1,8 @@ -import AppKit import ArgumentParser import Foundation -import Virtualization -@main -struct VPhoneCLI: AsyncParsableCommand { - static var configuration = CommandConfiguration( +struct VPhoneCLI: ParsableCommand { + static let configuration = CommandConfiguration( commandName: "vphone-cli", abstract: "Boot a virtual iPhone (PV=3)", discussion: """ @@ -31,100 +28,40 @@ struct VPhoneCLI: AsyncParsableCommand { @Option(help: "Path to NVRAM storage (created/overwritten)") var nvram: String = "nvram.bin" + @Option(help: "Path to machineIdentifier file (created if missing)") + var machineId: String + @Option(help: "Number of CPU cores") - var cpu: Int = 4 + var cpu: Int = 8 @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 + var memory: Int = 8192 @Option(help: "Path to SEP storage file (created if missing)") - var sepStorage: String? = nil + var sepStorage: String @Option(help: "Path to SEP ROM binary") - var sepRom: String? = nil + var sepRom: String @Flag(help: "Boot into DFU mode") var dfu: Bool = false + @Option(help: "Display width in pixels (default: 1290)") + var screenWidth: Int = 1290 + + @Option(help: "Display height in pixels (default: 2796)") + var screenHeight: Int = 2796 + + @Option(help: "Display pixels per inch (default: 460)") + var screenPpi: Int = 460 + + @Option(help: "Window scale divisor (default: 3.0)") + var screenScale: Double = 3.0 + @Flag(help: "Run without GUI (headless)") var noGraphics: Bool = false - @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 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() - } - } + /// Execution is driven by VPhoneAppDelegate; main.swift calls parseOrExit() + /// and hands the parsed options to the delegate. + mutating func run() throws {} } diff --git a/sources/vphone-cli/VPhoneError.swift b/sources/vphone-cli/VPhoneError.swift new file mode 100644 index 0000000..56b313c --- /dev/null +++ b/sources/vphone-cli/VPhoneError.swift @@ -0,0 +1,24 @@ +import Foundation + +enum VPhoneError: Error, CustomStringConvertible { + case hardwareModelNotSupported + case romNotFound(String) + case diskNotFound(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)" + case let .diskNotFound(p): + "Disk image not found: \(p)" + } + } +} diff --git a/sources/vphone-cli/VPhoneHardwareModel.swift b/sources/vphone-cli/VPhoneHardwareModel.swift index 2f33935..a3595c1 100644 --- a/sources/vphone-cli/VPhoneHardwareModel.swift +++ b/sources/vphone-cli/VPhoneHardwareModel.swift @@ -1,8 +1,8 @@ +import Dynamic import Foundation import Virtualization -import VPhoneObjC -/// Wrapper around the ObjC private API call to create a PV=3 hardware model. +/// Creates a PV=3 hardware model via private _VZMacHardwareModelDescriptor. /// /// The Virtualization.framework checks: /// default_configuration_for_platform_version(3) validity byte = @@ -13,9 +13,17 @@ import VPhoneObjC /// Minimum host OS for PV=3: macOS 15.0 (Sequoia) /// enum VPhoneHardware { - /// Create a PV=3 VZMacHardwareModel. Throws if isSupported is false. static func createModel() throws -> VZMacHardwareModel { - let model = VPhoneCreateHardwareModel() + // platformVersion=3, boardID=0x90, ISA=2 matches vresearch101 + let desc = Dynamic._VZMacHardwareModelDescriptor() + desc.setPlatformVersion(NSNumber(value: UInt32(3))) + desc.setBoardID(NSNumber(value: UInt32(0x90))) + desc.setISA(NSNumber(value: Int64(2))) + + let model = Dynamic.VZMacHardwareModel + ._hardwareModelWithDescriptor(desc.asObject) + .asObject as! VZMacHardwareModel + guard model.isSupported else { throw VPhoneError.hardwareModelNotSupported } diff --git a/sources/vphone-cli/VPhoneKeyHelper.swift b/sources/vphone-cli/VPhoneKeyHelper.swift new file mode 100644 index 0000000..d631e79 --- /dev/null +++ b/sources/vphone-cli/VPhoneKeyHelper.swift @@ -0,0 +1,273 @@ +import AppKit +import Dynamic +import Foundation +import Virtualization + +// MARK: - Key Helper + +@MainActor +class VPhoneKeyHelper { + private let vm: VZVirtualMachine + + /// First _VZKeyboard from the VM's internal keyboard array. + private var firstKeyboard: AnyObject? { + guard let arr = Dynamic(vm)._keyboards.asObject as? NSArray, arr.count > 0 else { return nil } + return arr.object(at: 0) as AnyObject + } + + /// Get _deviceIdentifier from _VZKeyboard via KVC (it's an ivar, not a property). + private func keyboardDeviceId(_ keyboard: AnyObject) -> UInt32 { + if let obj = keyboard as? NSObject, + let val = obj.value(forKey: "_deviceIdentifier") as? UInt32 + { + return val + } + print("[keys] WARNING: Could not read _deviceIdentifier, defaulting to 1") + return 1 + } + + init(vm: VZVirtualMachine) { + self.vm = vm + } + + // MARK: - Send Key via _VZKeyEvent + + /// Send key down + up through _VZKeyEvent → _VZKeyboard.sendKeyEvents: pipeline. + private func sendKeyPress(keyCode: UInt16) { + guard let keyboard = firstKeyboard else { + print("[keys] No keyboard found") + return + } + + let down = Dynamic._VZKeyEvent(type: 0, keyCode: keyCode) + let up = Dynamic._VZKeyEvent(type: 1, keyCode: keyCode) + + guard let downObj = down.asAnyObject, let upObj = up.asAnyObject else { + print("[keys] Failed to create _VZKeyEvent") + return + } + + Dynamic(keyboard).sendKeyEvents([downObj, upObj] as NSArray) + print("[keys] Sent VK 0x\(String(keyCode, radix: 16)) (down+up)") + } + + // MARK: - Fn+Key Combos (iOS Full Keyboard Access) + + /// Send modifier+key combo via _VZKeyEvent (mod down → key down → key up → mod up). + private func sendVKCombo(modifierVK: UInt16, keyVK: UInt16) { + guard let keyboard = firstKeyboard else { + print("[keys] No keyboard found") + return + } + + var events: [AnyObject] = [] + if let obj = Dynamic._VZKeyEvent(type: 0, keyCode: modifierVK).asAnyObject { events.append(obj) } + if let obj = Dynamic._VZKeyEvent(type: 0, keyCode: keyVK).asAnyObject { events.append(obj) } + if let obj = Dynamic._VZKeyEvent(type: 1, keyCode: keyVK).asAnyObject { events.append(obj) } + if let obj = Dynamic._VZKeyEvent(type: 1, keyCode: modifierVK).asAnyObject { events.append(obj) } + + print("[keys] events: \(events)") + Dynamic(keyboard).sendKeyEvents(events as NSArray) + print("[keys] VK combo: 0x\(String(modifierVK, radix: 16))+0x\(String(keyVK, radix: 16))") + } + + // MARK: - Vector Injection (for keys with no VK code) + + /// Bypass _VZKeyEvent by calling sendKeyboardEvents:keyboardID: directly + /// with a crafted std::vector. Packed: (intermediate_index << 32) | is_key_down. + private func sendRawKeyPress(index: UInt64) { + guard let keyboard = firstKeyboard else { + print("[keys] No keyboard found") + return + } + let deviceId = keyboardDeviceId(keyboard) + + sendRawKeyEvent(index: index, isKeyDown: true, deviceId: deviceId) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [self] in + sendRawKeyEvent(index: index, isKeyDown: false, deviceId: deviceId) + } + } + + private func sendRawKeyEvent(index: UInt64, isKeyDown: Bool, deviceId: UInt32) { + let packed = (index << 32) | (isKeyDown ? 1 : 0) + + let data = UnsafeMutablePointer.allocate(capacity: 1) + defer { data.deallocate() } + data.pointee = packed + + var vec = (data, data.advanced(by: 1), data.advanced(by: 1)) + withUnsafeMutablePointer(to: &vec) { vecPtr in + _ = Dynamic(vm).sendKeyboardEvents(UnsafeMutableRawPointer(vecPtr), keyboardID: deviceId) + } + } + + // MARK: - Named Key Actions + + /// iOS hardware keyboard shortcuts (Cmd-based, Fn has no table entry in _VZKeyEvent) + func sendHome() { + sendVKCombo(modifierVK: 0x37, keyVK: 0x04) + } // Cmd+H → Home Screen + func sendSpotlight() { + sendVKCombo(modifierVK: 0x37, keyVK: 0x31) + } // Cmd+Space → Spotlight + + /// Standard keyboard keys + func sendReturn() { + sendKeyPress(keyCode: 0x24) + } + + func sendEscape() { + sendKeyPress(keyCode: 0x35) + } + + func sendSpace() { + sendKeyPress(keyCode: 0x31) + } + + func sendTab() { + sendKeyPress(keyCode: 0x30) + } + + func sendDeleteKey() { + sendKeyPress(keyCode: 0x33) + } + + func sendArrowUp() { + sendKeyPress(keyCode: 0x7E) + } + + func sendArrowDown() { + sendKeyPress(keyCode: 0x7D) + } + + func sendArrowLeft() { + sendKeyPress(keyCode: 0x7B) + } + + func sendArrowRight() { + sendKeyPress(keyCode: 0x7C) + } + + func sendShift() { + sendKeyPress(keyCode: 0x38) + } + + func sendCommand() { + sendKeyPress(keyCode: 0x37) + } + + /// Volume (Apple VK codes) + func sendVolumeUp() { + sendKeyPress(keyCode: 0x48) + } + + func sendVolumeDown() { + sendKeyPress(keyCode: 0x49) + } + + /// Power — no VK code, use vector injection (intermediate index 0x72 = System Wake) + func sendPower() { + sendRawKeyPress(index: 0x72) + } + + // MARK: - Type ASCII from Clipboard + + func typeFromClipboard() { + guard let string = NSPasteboard.general.string(forType: .string) else { + print("[keys] Clipboard has no string") + return + } + print("[keys] Typing \(string.count) characters from clipboard") + typeString(string) + } + + func typeString(_ string: String) { + guard let keyboard = firstKeyboard else { + print("[keys] No keyboard found") + return + } + + var delay: TimeInterval = 0 + let interval: TimeInterval = 0.02 + + for char in string { + guard let (keyCode, needsShift) = asciiToVK(char) else { + print("[keys] Skipping unsupported char: '\(char)'") + continue + } + + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + var events: [AnyObject] = [] + if needsShift { + if let obj = Dynamic._VZKeyEvent(type: 0, keyCode: UInt16(0x38)).asAnyObject { events.append(obj) } + } + if let obj = Dynamic._VZKeyEvent(type: 0, keyCode: keyCode).asAnyObject { events.append(obj) } + if let obj = Dynamic._VZKeyEvent(type: 1, keyCode: keyCode).asAnyObject { events.append(obj) } + if needsShift { + if let obj = Dynamic._VZKeyEvent(type: 1, keyCode: UInt16(0x38)).asAnyObject { events.append(obj) } + } + Dynamic(keyboard).sendKeyEvents(events as NSArray) + } + + delay += interval + } + } + + // MARK: - ASCII → Apple VK Code (US Layout) + + private func asciiToVK(_ char: Character) -> (UInt16, Bool)? { + switch char { + case "a": (0x00, false) case "b": (0x0B, false) + case "c": (0x08, false) case "d": (0x02, false) + case "e": (0x0E, false) case "f": (0x03, false) + case "g": (0x05, false) case "h": (0x04, false) + case "i": (0x22, false) case "j": (0x26, false) + case "k": (0x28, false) case "l": (0x25, false) + case "m": (0x2E, false) case "n": (0x2D, false) + case "o": (0x1F, false) case "p": (0x23, false) + case "q": (0x0C, false) case "r": (0x0F, false) + case "s": (0x01, false) case "t": (0x11, false) + case "u": (0x20, false) case "v": (0x09, false) + case "w": (0x0D, false) case "x": (0x07, false) + case "y": (0x10, false) case "z": (0x06, false) + case "A": (0x00, true) case "B": (0x0B, true) + case "C": (0x08, true) case "D": (0x02, true) + case "E": (0x0E, true) case "F": (0x03, true) + case "G": (0x05, true) case "H": (0x04, true) + case "I": (0x22, true) case "J": (0x26, true) + case "K": (0x28, true) case "L": (0x25, true) + case "M": (0x2E, true) case "N": (0x2D, true) + case "O": (0x1F, true) case "P": (0x23, true) + case "Q": (0x0C, true) case "R": (0x0F, true) + case "S": (0x01, true) case "T": (0x11, true) + case "U": (0x20, true) case "V": (0x09, true) + case "W": (0x0D, true) case "X": (0x07, true) + case "Y": (0x10, true) case "Z": (0x06, true) + case "0": (0x1D, false) case "1": (0x12, false) + case "2": (0x13, false) case "3": (0x14, false) + case "4": (0x15, false) case "5": (0x17, false) + case "6": (0x16, false) case "7": (0x1A, false) + case "8": (0x1C, false) case "9": (0x19, false) + case "-": (0x1B, false) case "=": (0x18, false) + case "[": (0x21, false) case "]": (0x1E, false) + case "\\": (0x2A, false) case ";": (0x29, false) + case "'": (0x27, false) case ",": (0x2B, false) + case ".": (0x2F, false) case "/": (0x2C, false) + case "`": (0x32, false) + case "!": (0x12, true) case "@": (0x13, true) + case "#": (0x14, true) case "$": (0x15, true) + case "%": (0x17, true) case "^": (0x16, true) + case "&": (0x1A, true) case "*": (0x1C, true) + case "(": (0x19, true) case ")": (0x1D, true) + case "_": (0x1B, true) case "+": (0x18, true) + case "{": (0x21, true) case "}": (0x1E, true) + case "|": (0x2A, true) case ":": (0x29, true) + case "\"": (0x27, true) case "<": (0x2B, true) + case ">": (0x2F, true) case "?": (0x2C, true) + case "~": (0x32, true) + case " ": (0x31, false) case "\t": (0x30, false) + case "\n": (0x24, false) case "\r": (0x24, false) + default: nil + } + } +} diff --git a/sources/vphone-cli/VPhoneMenuController.swift b/sources/vphone-cli/VPhoneMenuController.swift new file mode 100644 index 0000000..1cd5cae --- /dev/null +++ b/sources/vphone-cli/VPhoneMenuController.swift @@ -0,0 +1,141 @@ +import AppKit +import Foundation + +// MARK: - Menu Controller + +@MainActor +class VPhoneMenuController { + private let keyHelper: VPhoneKeyHelper + + init(keyHelper: VPhoneKeyHelper) { + self.keyHelper = keyHelper + setupMenuBar() + } + + // MARK: - Menu Bar Setup + + private func setupMenuBar() { + let mainMenu = NSMenu() + + // App menu + let appMenuItem = NSMenuItem() + let appMenu = NSMenu(title: "vphone") + appMenu.addItem(withTitle: "Quit vphone", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") + appMenuItem.submenu = appMenu + mainMenu.addItem(appMenuItem) + + // Keys menu — NO key equivalents to avoid intercepting VM keyboard input + let keysMenuItem = NSMenuItem() + let keysMenu = NSMenu(title: "Keys") + + // iOS hardware keyboard shortcuts + keysMenu.addItem(makeItem("Home Screen (Cmd+H)", action: #selector(sendHome))) + keysMenu.addItem(makeItem("Spotlight (Cmd+Space)", action: #selector(sendSpotlight))) + keysMenu.addItem(NSMenuItem.separator()) + keysMenu.addItem(makeItem("Return", action: #selector(sendReturn))) + keysMenu.addItem(makeItem("Escape", action: #selector(sendEscape))) + keysMenu.addItem(makeItem("Space", action: #selector(sendSpace))) + keysMenu.addItem(makeItem("Tab", action: #selector(sendTab))) + keysMenu.addItem(makeItem("Delete", action: #selector(sendDeleteKey))) + keysMenu.addItem(NSMenuItem.separator()) + keysMenu.addItem(makeItem("Arrow Up", action: #selector(sendArrowUp))) + keysMenu.addItem(makeItem("Arrow Down", action: #selector(sendArrowDown))) + keysMenu.addItem(makeItem("Arrow Left", action: #selector(sendArrowLeft))) + keysMenu.addItem(makeItem("Arrow Right", action: #selector(sendArrowRight))) + keysMenu.addItem(NSMenuItem.separator()) + keysMenu.addItem(makeItem("Power", action: #selector(sendPower))) + keysMenu.addItem(makeItem("Volume Up", action: #selector(sendVolumeUp))) + keysMenu.addItem(makeItem("Volume Down", action: #selector(sendVolumeDown))) + keysMenu.addItem(NSMenuItem.separator()) + keysMenu.addItem(makeItem("Shift (tap)", action: #selector(sendShift))) + keysMenu.addItem(makeItem("Command (tap)", action: #selector(sendCommand))) + + keysMenuItem.submenu = keysMenu + mainMenu.addItem(keysMenuItem) + + // Type menu + let typeMenuItem = NSMenuItem() + let typeMenu = NSMenu(title: "Type") + typeMenu.addItem(makeItem("Type ASCII from Clipboard", action: #selector(typeFromClipboard))) + typeMenuItem.submenu = typeMenu + mainMenu.addItem(typeMenuItem) + + NSApp.mainMenu = mainMenu + } + + private func makeItem(_ title: String, action: Selector) -> NSMenuItem { + let item = NSMenuItem(title: title, action: action, keyEquivalent: "") + item.target = self + return item + } + + // MARK: - Menu Actions (delegate to helper) + + @objc private func sendHome() { + keyHelper.sendHome() + } + + @objc private func sendSpotlight() { + keyHelper.sendSpotlight() + } + + @objc private func sendReturn() { + keyHelper.sendReturn() + } + + @objc private func sendEscape() { + keyHelper.sendEscape() + } + + @objc private func sendSpace() { + keyHelper.sendSpace() + } + + @objc private func sendTab() { + keyHelper.sendTab() + } + + @objc private func sendDeleteKey() { + keyHelper.sendDeleteKey() + } + + @objc private func sendArrowUp() { + keyHelper.sendArrowUp() + } + + @objc private func sendArrowDown() { + keyHelper.sendArrowDown() + } + + @objc private func sendArrowLeft() { + keyHelper.sendArrowLeft() + } + + @objc private func sendArrowRight() { + keyHelper.sendArrowRight() + } + + @objc private func sendPower() { + keyHelper.sendPower() + } + + @objc private func sendVolumeUp() { + keyHelper.sendVolumeUp() + } + + @objc private func sendVolumeDown() { + keyHelper.sendVolumeDown() + } + + @objc private func sendShift() { + keyHelper.sendShift() + } + + @objc private func sendCommand() { + keyHelper.sendCommand() + } + + @objc private func typeFromClipboard() { + keyHelper.typeFromClipboard() + } +} diff --git a/sources/vphone-cli/VPhoneVM.swift b/sources/vphone-cli/VPhoneVM.swift index 7d706b1..26d1c98 100644 --- a/sources/vphone-cli/VPhoneVM.swift +++ b/sources/vphone-cli/VPhoneVM.swift @@ -1,28 +1,27 @@ +import Dynamic import Foundation import Virtualization -import VPhoneObjC /// Minimal VM for booting a vphone (virtual iPhone) in DFU mode. +@MainActor class VPhoneVM: NSObject, VZVirtualMachineDelegate { let virtualMachine: VZVirtualMachine - private var done = false struct Options { var romURL: URL var nvramURL: URL + var machineIDURL: 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 + var cpuCount: Int = 8 + var memorySize: UInt64 = 8 * 1024 * 1024 * 1024 + var sepStorageURL: URL + var sepRomURL: URL + var screenWidth: Int = 1290 + var screenHeight: Int = 2796 + var screenPPI: Int = 460 + var screenScale: Double = 3.0 } - private var consoleLogFileHandle: FileHandle? - init(options: Options) throws { // --- Hardware model (PV=3) --- let hwModel = try VPhoneHardware.createModel() @@ -31,18 +30,17 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate { // --- 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) { + // Persist machineIdentifier for stable ECID + if let savedData = try? Data(contentsOf: options.machineIDURL), + 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)") + try newID.dataRepresentation.write(to: options.machineIDURL) + print("[vphone] Created new machineIdentifier -> \(options.machineIDURL.lastPathComponent)") } let auxStorage = try VZMacAuxiliaryStorage( @@ -52,19 +50,19 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate { ) platform.auxiliaryStorage = auxStorage platform.hardwareModel = hwModel - // platformFusing = prod (same as vrevm config) - // Set NVRAM boot-args to enable serial output (same as vrevm restore) + // Set NVRAM boot-args to enable serial output let bootArgs = "serial=3 debug=0x104c04" if let bootArgsData = bootArgs.data(using: .utf8) { - if VPhoneSetNVRAMVariable(auxStorage, "boot-args", bootArgsData) { - print("[vphone] NVRAM boot-args: \(bootArgs)") - } + let ok = Dynamic(auxStorage) + ._setDataValue(bootArgsData, forNVRAMVariableNamed: "boot-args", error: nil) + .asBool ?? false + if ok { print("[vphone] NVRAM boot-args: \(bootArgs)") } } // --- Boot loader with custom ROM --- let bootloader = VZMacOSBootLoader() - VPhoneSetBootLoaderROMURL(bootloader, options.romURL) + Dynamic(bootloader)._setROMURL(options.romURL) // --- VM Configuration --- let config = VZVirtualMachineConfiguration() @@ -73,63 +71,65 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate { config.cpuCount = max(options.cpuCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount) config.memorySize = max(options.memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize) - // Display (vresearch101: 1290x2796 @ 460 PPI — matches vrevm) + // Display let gfx = VZMacGraphicsDeviceConfiguration() gfx.displays = [ - VZMacGraphicsDisplayConfiguration(widthInPixels: 1290, heightInPixels: 2796, pixelsPerInch: 460), + VZMacGraphicsDisplayConfiguration( + widthInPixels: options.screenWidth, heightInPixels: options.screenHeight, + pixelsPerInch: options.screenPPI + ), ] config.graphicsDevices = [gfx] + // Audio + let afg = VZVirtioSoundDeviceConfiguration() + let inputAudioStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration() + let outputAudioStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration() + inputAudioStreamConfiguration.source = VZHostAudioInputStreamSource() + outputAudioStreamConfiguration.sink = VZHostAudioOutputStreamSink() + afg.streams = [inputAudioStreamConfiguration, outputAudioStreamConfiguration] + config.audioDevices = [afg] + // Storage - if FileManager.default.fileExists(atPath: options.diskURL.path) { - let attachment = try VZDiskImageStorageDeviceAttachment(url: options.diskURL, readOnly: false) - config.storageDevices = [VZVirtioBlockDeviceConfiguration(attachment: attachment)] + guard FileManager.default.fileExists(atPath: options.diskURL.path) else { + throw VPhoneError.diskNotFound(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)") - } + // Serial port (PL011 UART — interactive stdin/stdout) + if let serialPort = Dynamic._VZPL011SerialPortConfiguration().asObject as? VZSerialPortConfiguration { + serialPort.attachment = VZFileHandleSerialPortAttachment( + fileHandleForReading: FileHandle.standardInput, + fileHandleForWriting: FileHandle.standardOutput + ) + config.serialPorts = [serialPort] + print("[vphone] PL011 serial port attached (interactive)") } - // Multi-touch (USB touch screen for VNC click support) - VPhoneConfigureMultiTouch(config) + // Multi-touch (USB touch screen) + if let obj = Dynamic._VZUSBTouchScreenConfiguration().asObject { + Dynamic(config)._setMultiTouchDevices([obj]) + print("[vphone] USB touch screen configured") + } - // GDB debug stub (default init, system-assigned port — same as vrevm) - VPhoneSetGDBDebugStubDefault(config) + config.keyboards = [VZUSBKeyboardConfiguration()] + + // GDB debug stub (default init, system-assigned port) + Dynamic(config)._setDebugStub(Dynamic._VZGDBDebugStubConfiguration().asObject) // 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))") + let sepConfig = Dynamic._VZSEPCoprocessorConfiguration(storageURL: options.sepStorageURL) + sepConfig.setRomBinaryURL(options.sepRomURL) + sepConfig.setDebugStub(Dynamic._VZGDBDebugStubConfiguration().asObject) + if let sepObj = sepConfig.asObject { + Dynamic(config)._setCoprocessors([sepObj]) + print("[vphone] SEP coprocessor enabled (storage: \(options.sepStorageURL.path))") } // Validate @@ -141,12 +141,14 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate { virtualMachine.delegate = self } - // MARK: - DFU start + // MARK: - Start @MainActor - func start(forceDFU: Bool, stopOnPanic: Bool, stopOnFatalError: Bool) async throws { + func start(forceDFU: Bool) async throws { let opts = VZMacOSVirtualMachineStartOptions() - VPhoneConfigureStartOptions(opts, forceDFU, stopOnPanic, stopOnFatalError) + Dynamic(opts)._setForceDFU(forceDFU) + Dynamic(opts)._setStopInIBootStage1(false) + Dynamic(opts)._setStopInIBootStage2(false) print("[vphone] Starting\(forceDFU ? " DFU" : "")...") try await virtualMachine.start(options: opts) if forceDFU { @@ -156,57 +158,23 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate { } } - // MARK: - Wait - - func waitUntilStopped() async { - while !done { - try? await Task.sleep(nanoseconds: 500_000_000) - } - } - // MARK: - Delegate - func guestDidStop(_: VZVirtualMachine) { + // VZ delivers delegate callbacks via dispatch source on the main queue. + + nonisolated func guestDidStop(_: VZVirtualMachine) { print("[vphone] Guest stopped") - done = true + exit(EXIT_SUCCESS) } - func virtualMachine(_: VZVirtualMachine, didStopWithError error: Error) { + nonisolated func virtualMachine(_: VZVirtualMachine, didStopWithError error: Error) { print("[vphone] Stopped with error: \(error)") - done = true + exit(EXIT_FAILURE) } - func virtualMachine(_: VZVirtualMachine, networkDevice _: VZNetworkDevice, - attachmentWasDisconnectedWithError error: Error) + nonisolated 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/sources/vphone-cli/VPhoneVMView.swift b/sources/vphone-cli/VPhoneVMView.swift new file mode 100644 index 0000000..9f65241 --- /dev/null +++ b/sources/vphone-cli/VPhoneVMView.swift @@ -0,0 +1,16 @@ +import AppKit +import Dynamic +import Foundation +import Virtualization + +class VPhoneVMView: VZVirtualMachineView { + var keyHelper: VPhoneKeyHelper? + + override func rightMouseDown(with _: NSEvent) { + guard let keyHelper else { + print("[keys] keyHelper was not set, no way home!") + return + } + keyHelper.sendHome() + } +} diff --git a/sources/vphone-cli/VPhoneVMWindow.swift b/sources/vphone-cli/VPhoneVMWindow.swift deleted file mode 100644 index 16b812d..0000000 --- a/sources/vphone-cli/VPhoneVMWindow.swift +++ /dev/null @@ -1,249 +0,0 @@ -import AppKit -import Foundation -import Virtualization -import VPhoneObjC - -// MARK: - Touch-enabled VZVirtualMachineView - -struct NormalizedResult { - var point: CGPoint - var isInvalid: Bool -} - -class VPhoneVMView: VZVirtualMachineView { - var currentTouchSwipeAim: Int64 = 0 - - // 1. Mouse dragged -> touch phase 1 (moving) - override func mouseDragged(with event: NSEvent) { - handleMouseDragged(event) - super.mouseDragged(with: event) - } - - private func handleMouseDragged(_ event: NSEvent) { - guard let vm = self.virtualMachine, - let devices = VPhoneGetMultiTouchDevices(vm), - devices.count > 0 else { return } - - let normalized = normalizeCoordinate(event.locationInWindow) - let swipeAim = self.currentTouchSwipeAim - - guard let touch = VPhoneCreateTouch(0, 1, normalized.point, Int(swipeAim), event.timestamp) else { return } - guard let touchEvent = VPhoneCreateMultiTouchEvent([touch]) else { return } - - let device = devices[0] - VPhoneSendMultiTouchEvents(device, [touchEvent]) - } - - // 2. Mouse down -> touch phase 0 (began) - override func mouseDown(with event: NSEvent) { - handleMouseDown(event) - super.mouseDown(with: event) - } - - private func handleMouseDown(_ event: NSEvent) { - guard let vm = self.virtualMachine, - let devices = VPhoneGetMultiTouchDevices(vm), - devices.count > 0 else { return } - - let normalized = normalizeCoordinate(event.locationInWindow) - let localPoint = self.convert(event.locationInWindow, from: nil) - let edgeResult = hitTestEdge(at: localPoint) - self.currentTouchSwipeAim = Int64(edgeResult) - - guard let touch = VPhoneCreateTouch(0, 0, normalized.point, edgeResult, event.timestamp) else { return } - guard let touchEvent = VPhoneCreateMultiTouchEvent([touch]) else { return } - - let device = devices[0] - VPhoneSendMultiTouchEvents(device, [touchEvent]) - } - - // 3. Right mouse down -> two-finger touch began - override func rightMouseDown(with event: NSEvent) { - handleRightMouseDown(event) - super.rightMouseDown(with: event) - } - - private func handleRightMouseDown(_ event: NSEvent) { - guard let vm = self.virtualMachine, - let devices = VPhoneGetMultiTouchDevices(vm), - devices.count > 0 else { return } - - let normalized = normalizeCoordinate(event.locationInWindow) - guard !normalized.isInvalid else { return } - - let localPoint = self.convert(event.locationInWindow, from: nil) - let edgeResult = hitTestEdge(at: localPoint) - self.currentTouchSwipeAim = Int64(edgeResult) - - guard let touch = VPhoneCreateTouch(0, 0, normalized.point, edgeResult, event.timestamp), - let touch2 = VPhoneCreateTouch(1, 0, normalized.point, edgeResult, event.timestamp) else { return } - guard let touchEvent = VPhoneCreateMultiTouchEvent([touch, touch2]) else { return } - - let device = devices[0] - VPhoneSendMultiTouchEvents(device, [touchEvent]) - } - - // 4. Mouse up -> touch phase 3 (ended) - override func mouseUp(with event: NSEvent) { - handleMouseUp(event) - super.mouseUp(with: event) - } - - private func handleMouseUp(_ event: NSEvent) { - guard let vm = self.virtualMachine, - let devices = VPhoneGetMultiTouchDevices(vm), - devices.count > 0 else { return } - - let normalized = normalizeCoordinate(event.locationInWindow) - let swipeAim = self.currentTouchSwipeAim - - guard let touch = VPhoneCreateTouch(0, 3, normalized.point, Int(swipeAim), event.timestamp) else { return } - guard let touchEvent = VPhoneCreateMultiTouchEvent([touch]) else { return } - - let device = devices[0] - VPhoneSendMultiTouchEvents(device, [touchEvent]) - } - - // 5. Right mouse up -> two-finger touch ended - override func rightMouseUp(with event: NSEvent) { - handleRightMouseUp(event) - super.rightMouseUp(with: event) - } - - private func handleRightMouseUp(_ event: NSEvent) { - guard let vm = self.virtualMachine, - let devices = VPhoneGetMultiTouchDevices(vm), - devices.count > 0 else { return } - - let normalized = normalizeCoordinate(event.locationInWindow) - guard !normalized.isInvalid else { return } - - let swipeAim = self.currentTouchSwipeAim - - guard let touch = VPhoneCreateTouch(0, 3, normalized.point, Int(swipeAim), event.timestamp), - let touch2 = VPhoneCreateTouch(1, 3, normalized.point, Int(swipeAim), event.timestamp) else { return } - guard let touchEvent = VPhoneCreateMultiTouchEvent([touch, touch2]) else { return } - - let device = devices[0] - VPhoneSendMultiTouchEvents(device, [touchEvent]) - } - - // MARK: - Coordinate normalization - - func normalizeCoordinate(_ point: CGPoint) -> NormalizedResult { - let bounds = self.bounds - - if bounds.size.width <= 0 || bounds.size.height <= 0 { - return NormalizedResult(point: .zero, isInvalid: true) - } - - let localPoint = self.convert(point, from: nil) - - var nx = Double(localPoint.x / bounds.size.width) - var ny = Double(localPoint.y / bounds.size.height) - - nx = max(0.0, min(1.0, nx)) - ny = max(0.0, min(1.0, ny)) - - if !self.isFlipped { - ny = 1.0 - ny - } - - return NormalizedResult(point: CGPoint(x: nx, y: ny), isInvalid: false) - } - - // MARK: - Edge detection for swipe aim - - func hitTestEdge(at point: CGPoint) -> Int { - let bounds = self.bounds - let width = bounds.size.width - let height = bounds.size.height - - let distLeft = point.x - let distRight = width - point.x - - var minDist: Double - var edgeCode: Int - - if distRight < distLeft { - minDist = distRight - edgeCode = 4 // Right - } else { - minDist = distLeft - edgeCode = 8 // Left - } - - let topCode = self.isFlipped ? 2 : 1 - let bottomCode = self.isFlipped ? 1 : 2 - - let distTop = point.y - if distTop < minDist { - minDist = distTop - edgeCode = topCode - } - - let distBottom = height - point.y - if distBottom < minDist { - minDist = distBottom - edgeCode = bottomCode - } - - return minDist < 32.0 ? edgeCode : 0 - } -} - -// MARK: - Window management - -class VPhoneWindowController { - private var windowController: NSWindowController? - - @MainActor - func showWindow(for vm: VZVirtualMachine) { - let vmView: NSView - if #available(macOS 16.0, *) { - let view = VZVirtualMachineView() - view.virtualMachine = vm - view.capturesSystemKeys = true - vmView = view - } else { - let view = VPhoneVMView() - view.virtualMachine = vm - view.capturesSystemKeys = true - vmView = view - } - - let pixelWidth: CGFloat = 1179 - let pixelHeight: CGFloat = 2556 - let windowSize = NSSize(width: pixelWidth, height: pixelHeight) - - let window = NSWindow( - contentRect: NSRect(origin: .zero, size: windowSize), - styleMask: [.titled, .closable, .resizable, .miniaturizable], - backing: .buffered, - defer: false - ) - - window.contentAspectRatio = windowSize - window.title = "vphone" - window.contentView = vmView - window.center() - - let controller = NSWindowController(window: window) - controller.showWindow(nil) - self.windowController = controller - - if NSApp == nil { - _ = NSApplication.shared - } - NSApp.setActivationPolicy(.regular) - window.makeKeyAndOrderFront(nil) - NSApp.activate(ignoringOtherApps: true) - } - - func close() { - DispatchQueue.main.async { - self.windowController?.close() - self.windowController = nil - } - } -} diff --git a/sources/vphone-cli/VPhoneWindowController.swift b/sources/vphone-cli/VPhoneWindowController.swift new file mode 100644 index 0000000..fc92305 --- /dev/null +++ b/sources/vphone-cli/VPhoneWindowController.swift @@ -0,0 +1,38 @@ +import AppKit +import Foundation +import Virtualization + +@MainActor +class VPhoneWindowController { + private var windowController: NSWindowController? + + func showWindow(for vm: VZVirtualMachine, screenWidth: Int, screenHeight: Int, screenScale: Double, keyHelper: VPhoneKeyHelper) { + let view = VPhoneVMView() + view.virtualMachine = vm + view.capturesSystemKeys = true + view.keyHelper = keyHelper + let vmView: NSView = view + + let scale = CGFloat(screenScale) + let windowSize = NSSize(width: CGFloat(screenWidth) / scale, height: CGFloat(screenHeight) / scale) + + let window = NSWindow( + contentRect: NSRect(origin: .zero, size: windowSize), + styleMask: [.titled, .closable, .resizable, .miniaturizable], + backing: .buffered, + defer: false + ) + + window.contentAspectRatio = windowSize + window.title = "vphone" + window.contentView = vmView + window.center() + + let controller = NSWindowController(window: window) + controller.showWindow(nil) + windowController = controller + + window.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + } +} diff --git a/sources/vphone-cli/main.swift b/sources/vphone-cli/main.swift new file mode 100644 index 0000000..0b05c47 --- /dev/null +++ b/sources/vphone-cli/main.swift @@ -0,0 +1,11 @@ +import AppKit +import Foundation + +/// Parse CLI arguments before NSApplication starts so that bad-arg errors +/// print cleanly to the terminal without a run loop ever starting. +let cli = VPhoneCLI.parseOrExit() + +let app = NSApplication.shared +let delegate = VPhoneAppDelegate(cli: cli) +app.delegate = delegate +app.run() diff --git a/sources/vphone-objc/VPhoneObjC.m b/sources/vphone-objc/VPhoneObjC.m deleted file mode 100644 index 560b5b0..0000000 --- a/sources/vphone-objc/VPhoneObjC.m +++ /dev/null @@ -1,256 +0,0 @@ -// VPhoneObjC.m — ObjC wrappers for private Virtualization.framework APIs -#import "VPhoneObjC.h" -#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"); -} - -// 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); -} diff --git a/sources/vphone-objc/include/VPhoneObjC.h b/sources/vphone-objc/include/VPhoneObjC.h deleted file mode 100644 index 9e125d6..0000000 --- a/sources/vphone-objc/include/VPhoneObjC.h +++ /dev/null @@ -1,79 +0,0 @@ -// 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); - -// --- Multi-Touch (VNC click fix) --- - -/// Configure _VZUSBTouchScreenConfiguration on the VM config. -/// Must be called before VM starts to enable touch input. -void VPhoneConfigureMultiTouch(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); - -NS_ASSUME_NONNULL_END