Update README.md

This commit is contained in:
Lakr
2026-03-01 02:47:52 +09:00
parent d9e66ae30d
commit 8200cd5a55
24 changed files with 1164 additions and 944 deletions

1
.gitignore vendored
View File

@@ -310,3 +310,4 @@ __marimo__/
*.resolved
/VM
.limd/
/.swiftpm

View File

@@ -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`.

View File

@@ -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`)

1
CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
AGENTS.md

View File

@@ -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

View File

@@ -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"),
]
)
),
]
)

View File

@@ -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 |

View File

@@ -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+** |

View File

@@ -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<uint64_t>
-> [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<const unsigned char>{data_ptr, length}
-> std::vector<std::span<...>>{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<uint64_t>)
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<std::span<const unsigned char>>**:
```
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<uint64_t> for sendKeyboardEvents
```swift
let data = UnsafeMutablePointer<UInt64>.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<span<unsigned char>> for _processHIDReports
```swift
let reportPtr = UnsafeMutablePointer<UInt8>.allocate(capacity: N)
// fill report bytes...
let spanPtr = UnsafeMutablePointer<Int>.allocate(capacity: 2)
spanPtr[0] = Int(bitPattern: reportPtr) // data pointer
spanPtr[1] = N // length
let vecPtr = UnsafeMutablePointer<Int>.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 |

View File

@@ -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)

View File

@@ -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

View File

@@ -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("<I", asm(s, addr))[0]
# ══════════════════════════════════════════════════════════════════
# Configuration
# ══════════════════════════════════════════════════════════════════
@@ -74,11 +51,6 @@ OUTPUT_DIR = "Ramdisk"
TEMP_DIR = "ramdisk_builder_temp"
INPUT_DIR = "ramdisk_input"
# Default location to copy resources from
CFW_DIR = os.path.expanduser(
"~/Documents/GitHub/super-tart-vphone-private/CFW"
)
# Ramdisk boot-args
RAMDISK_BOOT_ARGS = b"serial=3 rd=md0 debug=0x2014e -v wdt=-1 %s"
@@ -298,7 +270,9 @@ def build_ramdisk(restore_dir, im4m_path, vm_dir, input_dir, output_dir, temp_di
for pattern in SIGN_DIRS:
for path in glob.glob(os.path.join(mountpoint, pattern)):
if os.path.isfile(path) and not os.path.islink(path):
if "Mach-O" in subprocess.getoutput(f'file "{path}"'):
if "Mach-O" in subprocess.run(
["file", path], capture_output=True, text=True,
).stdout:
subprocess.run(
[ldid, "-S", "-M", f"-K{signcert}", path],
capture_output=True,

View File

@@ -0,0 +1,103 @@
import AppKit
import Foundation
import Virtualization
class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
private let cli: VPhoneCLI
private var vm: VPhoneVM?
private var windowController: VPhoneWindowController?
private var menuController: VPhoneMenuController?
private var sigintSource: DispatchSourceSignal?
init(cli: VPhoneCLI) {
self.cli = cli
super.init()
}
func applicationDidFinishLaunching(_: Notification) {
NSApp.setActivationPolicy(cli.noGraphics ? .prohibited : .regular)
signal(SIGINT, SIG_IGN)
let src = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
src.setEventHandler {
print("\n[vphone] SIGINT — shutting down")
NSApp.terminate(nil)
}
src.activate()
sigintSource = src
Task { @MainActor in
do {
try await self.startVM()
} catch {
print("[vphone] Fatal: \(error)")
NSApp.terminate(nil)
}
}
}
@MainActor
private func startVM() async throws {
let romURL = URL(fileURLWithPath: cli.rom)
guard FileManager.default.fileExists(atPath: romURL.path) else {
throw VPhoneError.romNotFound(cli.rom)
}
let diskURL = URL(fileURLWithPath: cli.disk)
let nvramURL = URL(fileURLWithPath: cli.nvram)
let machineIDURL = URL(fileURLWithPath: cli.machineId)
let sepStorageURL = URL(fileURLWithPath: cli.sepStorage)
let sepRomURL = URL(fileURLWithPath: cli.sepRom)
print("=== vphone-cli ===")
print("ROM : \(cli.rom)")
print("Disk : \(cli.disk)")
print("NVRAM : \(cli.nvram)")
print("MachID: \(cli.machineId)")
print("CPU : \(cli.cpu)")
print("Memory: \(cli.memory) MB")
print("Screen: \(cli.screenWidth)x\(cli.screenHeight) @ \(cli.screenPpi) PPI (scale \(cli.screenScale)x)")
print("SEP : enabled")
print(" storage: \(cli.sepStorage)")
print(" rom : \(cli.sepRom)")
print("")
let options = VPhoneVM.Options(
romURL: romURL,
nvramURL: nvramURL,
machineIDURL: machineIDURL,
diskURL: diskURL,
cpuCount: cli.cpu,
memorySize: UInt64(cli.memory) * 1024 * 1024,
sepStorageURL: sepStorageURL,
sepRomURL: sepRomURL,
screenWidth: cli.screenWidth,
screenHeight: cli.screenHeight,
screenPPI: cli.screenPpi,
screenScale: cli.screenScale
)
let vm = try VPhoneVM(options: options)
self.vm = vm
try await vm.start(forceDFU: cli.dfu)
if !cli.noGraphics {
let keyHelper = VPhoneKeyHelper(vm: vm.virtualMachine)
let wc = VPhoneWindowController()
wc.showWindow(
for: vm.virtualMachine,
screenWidth: cli.screenWidth,
screenHeight: cli.screenHeight,
screenScale: cli.screenScale,
keyHelper: keyHelper
)
windowController = wc
menuController = VPhoneMenuController(keyHelper: keyHelper)
}
}
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
!cli.noGraphics
}
}

View File

@@ -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 {}
}

View File

@@ -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)"
}
}
}

View File

@@ -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
}

View File

@@ -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<uint64_t>. 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<UInt64>.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
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)"
}
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -1,256 +0,0 @@
// VPhoneObjC.m ObjC wrappers for private Virtualization.framework APIs
#import "VPhoneObjC.h"
#import <objc/message.h>
// 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 <NSCopying>
@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);
}

View File

@@ -1,79 +0,0 @@
// VPhoneObjC.h — ObjC wrappers for private Virtualization.framework APIs
#import <Foundation/Foundation.h>
#import <Virtualization/Virtualization.h>
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