mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-05 04:59:05 +08:00
Update README.md
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -310,3 +310,4 @@ __marimo__/
|
|||||||
*.resolved
|
*.resolved
|
||||||
/VM
|
/VM
|
||||||
.limd/
|
.limd/
|
||||||
|
/.swiftpm
|
||||||
|
|||||||
58
AGENTS.md
58
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
|
## 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`
|
Makefile # Single entry point — run `make help`
|
||||||
|
|
||||||
sources/
|
sources/
|
||||||
├── vphone-objc/ # ObjC bridge for private Virtualization.framework APIs
|
├── vphone.entitlements # Private API entitlements (5 keys)
|
||||||
│ ├── include/VPhoneObjC.h
|
└── vphone-cli/ # Swift 6.0 executable (pure Swift, no ObjC)
|
||||||
│ └── VPhoneObjC.m
|
├── main.swift # Entry point — NSApplication + AppDelegate
|
||||||
└── vphone-cli/ # Swift executable
|
├── VPhoneAppDelegate.swift # App lifecycle, SIGINT, VM start/stop
|
||||||
├── VPhoneCLI.swift
|
├── VPhoneCLI.swift # ArgumentParser options (no execution logic)
|
||||||
├── VPhoneVM.swift
|
├── VPhoneVM.swift # @MainActor VM configuration and lifecycle
|
||||||
├── VPhoneHardwareModel.swift
|
├── VPhoneHardwareModel.swift # PV=3 hardware model via Dynamic
|
||||||
└── VPhoneVMWindow.swift
|
├── VPhoneVMView.swift # Touch-enabled VZVirtualMachineView + helpers
|
||||||
|
├── VPhoneWindowController.swift # @MainActor window management
|
||||||
|
├── VPhoneError.swift # Error types
|
||||||
|
└── MainActor+Isolated.swift # MainActor.isolated helper
|
||||||
|
|
||||||
scripts/
|
scripts/
|
||||||
├── patchers/ # Python patcher package
|
├── patchers/ # Python patcher package
|
||||||
@@ -38,17 +54,16 @@ scripts/
|
|||||||
├── setup_venv.sh # Creates Python venv with native keystone dylib
|
├── setup_venv.sh # Creates Python venv with native keystone dylib
|
||||||
└── setup_libimobiledevice.sh # Builds libimobiledevice toolchain from source
|
└── setup_libimobiledevice.sh # Builds libimobiledevice toolchain from source
|
||||||
|
|
||||||
Research/ # Research notes and verification reports
|
|
||||||
researchs/ # Component analysis and architecture docs
|
researchs/ # Component analysis and architecture docs
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Patterns
|
### Key Patterns
|
||||||
|
|
||||||
- **Private API access:** All private Virtualization.framework calls go through the ObjC bridge (`VPhoneObjC`). Swift code never calls private APIs directly.
|
- **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.
|
||||||
- **Function naming:** ObjC bridge functions use the `VPhone` prefix (e.g., `VPhoneCreateHardwareModel`, `VPhoneConfigureSEP`).
|
- **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`.
|
- **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.
|
- **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
|
### Swift
|
||||||
|
|
||||||
|
- **Language:** Swift 6.0 (strict concurrency).
|
||||||
- **Style:** Pragmatic, minimal. No unnecessary abstractions.
|
- **Style:** Pragmatic, minimal. No unnecessary abstractions.
|
||||||
- **Sections:** Use `// MARK: -` to organize code within files.
|
- **Sections:** Use `// MARK: -` to organize code within files.
|
||||||
- **Access control:** Default (internal). Only mark `private` when needed for clarity.
|
- **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.
|
- **Naming:** Types are `VPhone`-prefixed (`VPhoneVM`, `VPhoneWindowController`). Match Apple framework conventions.
|
||||||
|
- **Private APIs:** Use `Dynamic()` for runtime method dispatch. Touch objects use `NSClassFromString` + KVC to avoid designated initializer crashes.
|
||||||
### 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.
|
|
||||||
|
|
||||||
### Shell Scripts
|
### Shell Scripts
|
||||||
|
|
||||||
@@ -228,7 +239,12 @@ Creates a VM directory with:
|
|||||||
- Sparse disk image (default 64 GB)
|
- Sparse disk image (default 64 GB)
|
||||||
- SEP storage (512 KB flat file)
|
- SEP storage (512 KB flat file)
|
||||||
- AVPBooter + AVPSEPBooter ROMs (copied from `/System/Library/Frameworks/Virtualization.framework/`)
|
- 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`.
|
Override defaults: `make vm_new VM_DIR=myvm DISK_SIZE=32`.
|
||||||
|
|
||||||
|
|||||||
16
CLAUDE.md
16
CLAUDE.md
@@ -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`)
|
|
||||||
17
Makefile
17
Makefile
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
# ─── Configuration (override with make VAR=value) ─────────────────
|
# ─── Configuration (override with make VAR=value) ─────────────────
|
||||||
VM_DIR ?= vm
|
VM_DIR ?= vm
|
||||||
CPU ?= 4
|
CPU ?= 8
|
||||||
MEMORY ?= 4096
|
MEMORY ?= 8192
|
||||||
DISK_SIZE ?= 64
|
DISK_SIZE ?= 64
|
||||||
|
|
||||||
# ─── Paths ────────────────────────────────────────────────────────
|
# ─── Paths ────────────────────────────────────────────────────────
|
||||||
@@ -18,7 +18,7 @@ IRECOVERY := $(LIMD_PREFIX)/bin/irecovery
|
|||||||
IDEVICERESTORE := $(LIMD_PREFIX)/bin/idevicerestore
|
IDEVICERESTORE := $(LIMD_PREFIX)/bin/idevicerestore
|
||||||
PYTHON := $(CURDIR)/$(VENV)/bin/python3
|
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 ────────────────
|
# ─── Environment — prefer project-local binaries ────────────────
|
||||||
export PATH := $(CURDIR)/$(LIMD_PREFIX)/bin:$(CURDIR)/$(VENV)/bin:$(CURDIR)/.build/release:$(PATH)
|
export PATH := $(CURDIR)/$(LIMD_PREFIX)/bin:$(CURDIR)/$(VENV)/bin:$(CURDIR)/.build/release:$(PATH)
|
||||||
@@ -39,7 +39,7 @@ help:
|
|||||||
@echo ""
|
@echo ""
|
||||||
@echo "VM management:"
|
@echo "VM management:"
|
||||||
@echo " make vm_new Create VM directory"
|
@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 " make boot_dfu Boot VM in DFU mode"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Firmware pipeline:"
|
@echo "Firmware pipeline:"
|
||||||
@@ -110,21 +110,18 @@ boot: build
|
|||||||
--rom ./AVPBooter.vresearch1.bin \
|
--rom ./AVPBooter.vresearch1.bin \
|
||||||
--disk ./Disk.img \
|
--disk ./Disk.img \
|
||||||
--nvram ./nvram.bin \
|
--nvram ./nvram.bin \
|
||||||
|
--machine-id ./machineIdentifier.bin \
|
||||||
--cpu $(CPU) --memory $(MEMORY) \
|
--cpu $(CPU) --memory $(MEMORY) \
|
||||||
--serial-log ./serial.log \
|
|
||||||
--stop-on-panic --stop-on-fatal-error \
|
|
||||||
--sep-rom ./AVPSEPBooter.vresearch1.bin \
|
--sep-rom ./AVPSEPBooter.vresearch1.bin \
|
||||||
--sep-storage ./SEPStorage \
|
--sep-storage ./SEPStorage
|
||||||
--no-graphics
|
|
||||||
|
|
||||||
boot_dfu: build
|
boot_dfu: build
|
||||||
cd $(VM_DIR) && "$(CURDIR)/$(BINARY)" \
|
cd $(VM_DIR) && "$(CURDIR)/$(BINARY)" \
|
||||||
--rom ./AVPBooter.vresearch1.bin \
|
--rom ./AVPBooter.vresearch1.bin \
|
||||||
--disk ./Disk.img \
|
--disk ./Disk.img \
|
||||||
--nvram ./nvram.bin \
|
--nvram ./nvram.bin \
|
||||||
|
--machine-id ./machineIdentifier.bin \
|
||||||
--cpu $(CPU) --memory $(MEMORY) \
|
--cpu $(CPU) --memory $(MEMORY) \
|
||||||
--serial-log ./serial.log \
|
|
||||||
--stop-on-panic --stop-on-fatal-error \
|
|
||||||
--sep-rom ./AVPSEPBooter.vresearch1.bin \
|
--sep-rom ./AVPSEPBooter.vresearch1.bin \
|
||||||
--sep-storage ./SEPStorage \
|
--sep-storage ./SEPStorage \
|
||||||
--no-graphics --dfu
|
--no-graphics --dfu
|
||||||
|
|||||||
@@ -1,40 +1,28 @@
|
|||||||
// swift-tools-version:5.10
|
// swift-tools-version:6.0
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "vphone-cli",
|
name: "vphone-cli",
|
||||||
platforms: [
|
platforms: [
|
||||||
.macOS(.v14),
|
.macOS(.v15),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.1"),
|
.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: [
|
targets: [
|
||||||
// ObjC module: wraps private Virtualization.framework APIs
|
|
||||||
.target(
|
|
||||||
name: "VPhoneObjC",
|
|
||||||
path: "sources/vphone-objc",
|
|
||||||
publicHeadersPath: "include",
|
|
||||||
linkerSettings: [
|
|
||||||
.linkedFramework("Virtualization"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
// Swift executable
|
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "vphone-cli",
|
name: "vphone-cli",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"VPhoneObjC",
|
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
|
.product(name: "Dynamic", package: "Dynamic"),
|
||||||
],
|
],
|
||||||
path: "sources/vphone-cli",
|
path: "sources/vphone-cli",
|
||||||
swiftSettings: [
|
|
||||||
.unsafeFlags(["-parse-as-library"]),
|
|
||||||
],
|
|
||||||
linkerSettings: [
|
linkerSettings: [
|
||||||
.linkedFramework("Virtualization"),
|
.linkedFramework("Virtualization"),
|
||||||
.linkedFramework("AppKit"),
|
.linkedFramework("AppKit"),
|
||||||
]
|
]
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -6,10 +6,11 @@ Boot a virtual iPhone (iOS 26) via Apple's Virtualization.framework using PCC re
|
|||||||
|
|
||||||
## Tested Environments
|
## Tested Environments
|
||||||
|
|
||||||
| Record | Host macOS | Host Hardware | iPhone IPSW | CloudOS IPSW |
|
| Host | iPhone | CloudOS |
|
||||||
|--------|------------|---------------|-------------|--------------|
|
|------|--------|---------|
|
||||||
| 1 | macOS 26.3 (Tahoe, Build 25D125) | MacBook Air, Apple M4 | `iPhone17,3_26.1_23B85_Restore.ipsw` | `PCC-CloudOS-26.1-23B85.ipsw` |
|
| Mac16,12 26.3 | `17,3_26.1_23B85` | `26.1-23B85` |
|
||||||
| 2 | macOS 26.3 (Tahoe, Build 25D125) | MacBook Air, Apple M4 | `iPhone17,3_26.3_23D127_Restore.ipsw` | `PCC-CloudOS-26.1-23B85.ipsw` |
|
| 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
|
## Prerequisites
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ Run `make help` for the full list. Key targets:
|
|||||||
| `vm_new` | Create VM directory |
|
| `vm_new` | Create VM directory |
|
||||||
| `fw_prepare` | Download/merge IPSWs |
|
| `fw_prepare` | Download/merge IPSWs |
|
||||||
| `fw_patch` | Patch boot chain |
|
| `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_get_shsh` | Fetch SHSH blob |
|
||||||
| `restore` | Flash firmware |
|
| `restore` | Flash firmware |
|
||||||
| `ramdisk_build` | Build SSH ramdisk |
|
| `ramdisk_build` | Build SSH ramdisk |
|
||||||
|
|||||||
119
researchs/jailbreak_patches.md
Normal file
119
researchs/jailbreak_patches.md
Normal 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+** |
|
||||||
251
researchs/keyboard_event_pipeline.md
Normal file
251
researchs/keyboard_event_pipeline.md
Normal 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 |
|
||||||
@@ -29,8 +29,8 @@ Dependencies:
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import plistlib
|
import plistlib
|
||||||
import re
|
|
||||||
import struct
|
import struct
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN
|
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):
|
def patch_launchd_cache_loader(filepath):
|
||||||
"""NOP the cache validation check in launchd_cache_loader.
|
"""NOP the cache validation check in launchd_cache_loader.
|
||||||
|
|
||||||
Anchor strategies (in order):
|
Anchor strategy:
|
||||||
1. Search for "unsecure_cache" substring, resolve to full null-terminated
|
Search for "unsecure_cache" substring, resolve to full null-terminated
|
||||||
string start, find ADRP+ADD xref to it, NOP the nearby cbz/cbnz branch
|
string start, find ADRP+ADD xref to it, NOP the nearby cbz/cbnz branch.
|
||||||
2. Verified known offset fallback
|
|
||||||
|
|
||||||
The binary checks boot-arg "launchd_unsecure_cache=" — if not found,
|
The binary checks boot-arg "launchd_unsecure_cache=" — if not found,
|
||||||
it skips the unsecure path via a conditional branch. NOPping that branch
|
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")
|
text_sec = find_section(sections, "__TEXT,__text")
|
||||||
if not text_sec:
|
if not text_sec:
|
||||||
print(" [-] __TEXT,__text not found")
|
print(" [-] __TEXT,__text not found")
|
||||||
return _launchd_cache_fallback(filepath, data)
|
return False
|
||||||
|
|
||||||
text_va, text_size, text_foff = text_sec
|
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}")
|
print(f" [+] NOPped at 0x{branch_foff:X}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Strategy 2: Fallback to verified known offset
|
print(" [-] Dynamic anchor not found — all strategies exhausted")
|
||||||
print(" Dynamic anchor not found, trying verified fallback...")
|
return False
|
||||||
return _launchd_cache_fallback(filepath, data)
|
|
||||||
|
|
||||||
|
|
||||||
def _find_cstring_start(data, match_off, section_foff):
|
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
|
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
|
# 3. mobileactivationd — Hackivation bypass
|
||||||
# ══════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════
|
||||||
@@ -461,7 +434,6 @@ def patch_mobileactivationd(filepath):
|
|||||||
Anchor strategies (in order):
|
Anchor strategies (in order):
|
||||||
1. Search LC_SYMTAB for symbol containing "should_hactivate"
|
1. Search LC_SYMTAB for symbol containing "should_hactivate"
|
||||||
2. Parse ObjC metadata: methnames -> selrefs -> method_list -> IMP
|
2. Parse ObjC metadata: methnames -> selrefs -> method_list -> IMP
|
||||||
3. Verified known offset fallback
|
|
||||||
|
|
||||||
The method determines if the device should self-activate (hackivation).
|
The method determines if the device should self-activate (hackivation).
|
||||||
Patching it to always return YES bypasses activation lock.
|
Patching it to always return YES bypasses activation lock.
|
||||||
@@ -481,15 +453,15 @@ def patch_mobileactivationd(filepath):
|
|||||||
if imp_foff < 0:
|
if imp_foff < 0:
|
||||||
imp_foff = _find_via_objc_metadata(data)
|
imp_foff = _find_via_objc_metadata(data)
|
||||||
|
|
||||||
# Strategy 3: Fallback
|
# All dynamic strategies exhausted
|
||||||
if imp_foff < 0:
|
if imp_foff < 0:
|
||||||
print(" Dynamic anchor not found, trying verified fallback...")
|
print(" [-] Dynamic anchor not found — all strategies exhausted")
|
||||||
return _mobileactivationd_fallback(filepath, data)
|
return False
|
||||||
|
|
||||||
# Verify the target looks like code
|
# Verify the target looks like code
|
||||||
if imp_foff + 8 > len(data):
|
if imp_foff + 8 > len(data):
|
||||||
print(f" [-] IMP offset 0x{imp_foff:X} out of bounds")
|
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)
|
insns = disasm_at(data, imp_foff, 4)
|
||||||
if insns:
|
if insns:
|
||||||
@@ -602,26 +574,6 @@ def _find_via_objc_metadata(data):
|
|||||||
return -1
|
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
|
# BuildManifest parsing
|
||||||
# ══════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════
|
||||||
@@ -660,7 +612,8 @@ def parse_cryptex_paths(manifest_path):
|
|||||||
def inject_daemons(plist_path, daemon_dir):
|
def inject_daemons(plist_path, daemon_dir):
|
||||||
"""Inject bash/dropbear/trollvnc entries into launchd.plist."""
|
"""Inject bash/dropbear/trollvnc entries into launchd.plist."""
|
||||||
# Convert to XML first (macOS binary plist -> XML)
|
# 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:
|
with open(plist_path, "rb") as f:
|
||||||
target = plistlib.load(f)
|
target = plistlib.load(f)
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ Dependencies: keystone-engine, capstone
|
|||||||
import struct, plistlib
|
import struct, plistlib
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE
|
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,
|
from capstone.arm64_const import (ARM64_OP_REG, ARM64_OP_IMM,
|
||||||
ARM64_REG_W0, ARM64_REG_X0, ARM64_REG_X8)
|
ARM64_REG_W0, ARM64_REG_X0, ARM64_REG_X8)
|
||||||
|
|
||||||
# ── Assembly / disassembly helpers ───────────────────────────────
|
# ── Assembly / disassembly helpers ───────────────────────────────
|
||||||
_ks = Ks(KS_ARCH_ARM64, KS_MODE_LE)
|
_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
|
_cs.detail = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,17 +24,14 @@ import glob
|
|||||||
import os
|
import os
|
||||||
import plistlib
|
import plistlib
|
||||||
import shutil
|
import shutil
|
||||||
import struct
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
|
|
||||||
# Ensure sibling modules (patch_firmware) are importable when run from any CWD
|
# Ensure sibling modules (patch_firmware) are importable when run from any CWD
|
||||||
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
if _SCRIPT_DIR not in sys.path:
|
if _SCRIPT_DIR not in sys.path:
|
||||||
sys.path.insert(0, _SCRIPT_DIR)
|
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 pyimg4 import IM4P
|
||||||
|
|
||||||
from fw_patch import (
|
from fw_patch import (
|
||||||
@@ -46,26 +43,6 @@ from fw_patch import (
|
|||||||
)
|
)
|
||||||
from patchers.iboot import IBootPatcher
|
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
|
# Configuration
|
||||||
# ══════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════
|
||||||
@@ -74,11 +51,6 @@ OUTPUT_DIR = "Ramdisk"
|
|||||||
TEMP_DIR = "ramdisk_builder_temp"
|
TEMP_DIR = "ramdisk_builder_temp"
|
||||||
INPUT_DIR = "ramdisk_input"
|
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
|
||||||
RAMDISK_BOOT_ARGS = b"serial=3 rd=md0 debug=0x2014e -v wdt=-1 %s"
|
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 pattern in SIGN_DIRS:
|
||||||
for path in glob.glob(os.path.join(mountpoint, pattern)):
|
for path in glob.glob(os.path.join(mountpoint, pattern)):
|
||||||
if os.path.isfile(path) and not os.path.islink(path):
|
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(
|
subprocess.run(
|
||||||
[ldid, "-S", "-M", f"-K{signcert}", path],
|
[ldid, "-S", "-M", f"-K{signcert}", path],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
|
|||||||
103
sources/vphone-cli/VPhoneAppDelegate.swift
Normal file
103
sources/vphone-cli/VPhoneAppDelegate.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
import AppKit
|
|
||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import Foundation
|
import Foundation
|
||||||
import Virtualization
|
|
||||||
|
|
||||||
@main
|
struct VPhoneCLI: ParsableCommand {
|
||||||
struct VPhoneCLI: AsyncParsableCommand {
|
static let configuration = CommandConfiguration(
|
||||||
static var configuration = CommandConfiguration(
|
|
||||||
commandName: "vphone-cli",
|
commandName: "vphone-cli",
|
||||||
abstract: "Boot a virtual iPhone (PV=3)",
|
abstract: "Boot a virtual iPhone (PV=3)",
|
||||||
discussion: """
|
discussion: """
|
||||||
@@ -31,100 +28,40 @@ struct VPhoneCLI: AsyncParsableCommand {
|
|||||||
@Option(help: "Path to NVRAM storage (created/overwritten)")
|
@Option(help: "Path to NVRAM storage (created/overwritten)")
|
||||||
var nvram: String = "nvram.bin"
|
var nvram: String = "nvram.bin"
|
||||||
|
|
||||||
|
@Option(help: "Path to machineIdentifier file (created if missing)")
|
||||||
|
var machineId: String
|
||||||
|
|
||||||
@Option(help: "Number of CPU cores")
|
@Option(help: "Number of CPU cores")
|
||||||
var cpu: Int = 4
|
var cpu: Int = 8
|
||||||
|
|
||||||
@Option(help: "Memory size in MB")
|
@Option(help: "Memory size in MB")
|
||||||
var memory: Int = 4096
|
var memory: Int = 8192
|
||||||
|
|
||||||
@Option(help: "Path to write serial console log file")
|
|
||||||
var serialLog: String? = nil
|
|
||||||
|
|
||||||
@Flag(help: "Stop VM on guest panic")
|
|
||||||
var stopOnPanic: Bool = false
|
|
||||||
|
|
||||||
@Flag(help: "Stop VM on fatal error")
|
|
||||||
var stopOnFatalError: Bool = false
|
|
||||||
|
|
||||||
@Flag(help: "Skip SEP coprocessor setup")
|
|
||||||
var skipSep: Bool = false
|
|
||||||
|
|
||||||
@Option(help: "Path to SEP storage file (created if missing)")
|
@Option(help: "Path to SEP storage file (created if missing)")
|
||||||
var sepStorage: String? = nil
|
var sepStorage: String
|
||||||
|
|
||||||
@Option(help: "Path to SEP ROM binary")
|
@Option(help: "Path to SEP ROM binary")
|
||||||
var sepRom: String? = nil
|
var sepRom: String
|
||||||
|
|
||||||
@Flag(help: "Boot into DFU mode")
|
@Flag(help: "Boot into DFU mode")
|
||||||
var dfu: Bool = false
|
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)")
|
@Flag(help: "Run without GUI (headless)")
|
||||||
var noGraphics: Bool = false
|
var noGraphics: Bool = false
|
||||||
|
|
||||||
@MainActor
|
/// Execution is driven by VPhoneAppDelegate; main.swift calls parseOrExit()
|
||||||
mutating func run() async throws {
|
/// and hands the parsed options to the delegate.
|
||||||
let romURL = URL(fileURLWithPath: rom)
|
mutating func run() throws {}
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
24
sources/vphone-cli/VPhoneError.swift
Normal file
24
sources/vphone-cli/VPhoneError.swift
Normal 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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import Dynamic
|
||||||
import Foundation
|
import Foundation
|
||||||
import Virtualization
|
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:
|
/// The Virtualization.framework checks:
|
||||||
/// default_configuration_for_platform_version(3) validity byte =
|
/// default_configuration_for_platform_version(3) validity byte =
|
||||||
@@ -13,9 +13,17 @@ import VPhoneObjC
|
|||||||
/// Minimum host OS for PV=3: macOS 15.0 (Sequoia)
|
/// Minimum host OS for PV=3: macOS 15.0 (Sequoia)
|
||||||
///
|
///
|
||||||
enum VPhoneHardware {
|
enum VPhoneHardware {
|
||||||
/// Create a PV=3 VZMacHardwareModel. Throws if isSupported is false.
|
|
||||||
static func createModel() throws -> VZMacHardwareModel {
|
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 {
|
guard model.isSupported else {
|
||||||
throw VPhoneError.hardwareModelNotSupported
|
throw VPhoneError.hardwareModelNotSupported
|
||||||
}
|
}
|
||||||
|
|||||||
273
sources/vphone-cli/VPhoneKeyHelper.swift
Normal file
273
sources/vphone-cli/VPhoneKeyHelper.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
141
sources/vphone-cli/VPhoneMenuController.swift
Normal file
141
sources/vphone-cli/VPhoneMenuController.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,27 @@
|
|||||||
|
import Dynamic
|
||||||
import Foundation
|
import Foundation
|
||||||
import Virtualization
|
import Virtualization
|
||||||
import VPhoneObjC
|
|
||||||
|
|
||||||
/// Minimal VM for booting a vphone (virtual iPhone) in DFU mode.
|
/// Minimal VM for booting a vphone (virtual iPhone) in DFU mode.
|
||||||
|
@MainActor
|
||||||
class VPhoneVM: NSObject, VZVirtualMachineDelegate {
|
class VPhoneVM: NSObject, VZVirtualMachineDelegate {
|
||||||
let virtualMachine: VZVirtualMachine
|
let virtualMachine: VZVirtualMachine
|
||||||
private var done = false
|
|
||||||
|
|
||||||
struct Options {
|
struct Options {
|
||||||
var romURL: URL
|
var romURL: URL
|
||||||
var nvramURL: URL
|
var nvramURL: URL
|
||||||
|
var machineIDURL: URL
|
||||||
var diskURL: URL
|
var diskURL: URL
|
||||||
var cpuCount: Int = 4
|
var cpuCount: Int = 8
|
||||||
var memorySize: UInt64 = 4 * 1024 * 1024 * 1024
|
var memorySize: UInt64 = 8 * 1024 * 1024 * 1024
|
||||||
var skipSEP: Bool = true
|
var sepStorageURL: URL
|
||||||
var sepStorageURL: URL?
|
var sepRomURL: URL
|
||||||
var sepRomURL: URL?
|
var screenWidth: Int = 1290
|
||||||
var serialLogPath: String? = nil
|
var screenHeight: Int = 2796
|
||||||
var stopOnPanic: Bool = false
|
var screenPPI: Int = 460
|
||||||
var stopOnFatalError: Bool = false
|
var screenScale: Double = 3.0
|
||||||
}
|
}
|
||||||
|
|
||||||
private var consoleLogFileHandle: FileHandle?
|
|
||||||
|
|
||||||
init(options: Options) throws {
|
init(options: Options) throws {
|
||||||
// --- Hardware model (PV=3) ---
|
// --- Hardware model (PV=3) ---
|
||||||
let hwModel = try VPhoneHardware.createModel()
|
let hwModel = try VPhoneHardware.createModel()
|
||||||
@@ -31,18 +30,17 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate {
|
|||||||
// --- Platform ---
|
// --- Platform ---
|
||||||
let platform = VZMacPlatformConfiguration()
|
let platform = VZMacPlatformConfiguration()
|
||||||
|
|
||||||
// Persist machineIdentifier for stable ECID (same as vrevm)
|
// Persist machineIdentifier for stable ECID
|
||||||
let machineIDPath = options.nvramURL.deletingLastPathComponent()
|
if let savedData = try? Data(contentsOf: options.machineIDURL),
|
||||||
.appendingPathComponent("machineIdentifier.bin")
|
let savedID = VZMacMachineIdentifier(dataRepresentation: savedData)
|
||||||
if let savedData = try? Data(contentsOf: machineIDPath),
|
{
|
||||||
let savedID = VZMacMachineIdentifier(dataRepresentation: savedData) {
|
|
||||||
platform.machineIdentifier = savedID
|
platform.machineIdentifier = savedID
|
||||||
print("[vphone] Loaded machineIdentifier (ECID stable)")
|
print("[vphone] Loaded machineIdentifier (ECID stable)")
|
||||||
} else {
|
} else {
|
||||||
let newID = VZMacMachineIdentifier()
|
let newID = VZMacMachineIdentifier()
|
||||||
platform.machineIdentifier = newID
|
platform.machineIdentifier = newID
|
||||||
try newID.dataRepresentation.write(to: machineIDPath)
|
try newID.dataRepresentation.write(to: options.machineIDURL)
|
||||||
print("[vphone] Created new machineIdentifier -> \(machineIDPath.lastPathComponent)")
|
print("[vphone] Created new machineIdentifier -> \(options.machineIDURL.lastPathComponent)")
|
||||||
}
|
}
|
||||||
|
|
||||||
let auxStorage = try VZMacAuxiliaryStorage(
|
let auxStorage = try VZMacAuxiliaryStorage(
|
||||||
@@ -52,19 +50,19 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate {
|
|||||||
)
|
)
|
||||||
platform.auxiliaryStorage = auxStorage
|
platform.auxiliaryStorage = auxStorage
|
||||||
platform.hardwareModel = hwModel
|
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"
|
let bootArgs = "serial=3 debug=0x104c04"
|
||||||
if let bootArgsData = bootArgs.data(using: .utf8) {
|
if let bootArgsData = bootArgs.data(using: .utf8) {
|
||||||
if VPhoneSetNVRAMVariable(auxStorage, "boot-args", bootArgsData) {
|
let ok = Dynamic(auxStorage)
|
||||||
print("[vphone] NVRAM boot-args: \(bootArgs)")
|
._setDataValue(bootArgsData, forNVRAMVariableNamed: "boot-args", error: nil)
|
||||||
}
|
.asBool ?? false
|
||||||
|
if ok { print("[vphone] NVRAM boot-args: \(bootArgs)") }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Boot loader with custom ROM ---
|
// --- Boot loader with custom ROM ---
|
||||||
let bootloader = VZMacOSBootLoader()
|
let bootloader = VZMacOSBootLoader()
|
||||||
VPhoneSetBootLoaderROMURL(bootloader, options.romURL)
|
Dynamic(bootloader)._setROMURL(options.romURL)
|
||||||
|
|
||||||
// --- VM Configuration ---
|
// --- VM Configuration ---
|
||||||
let config = VZVirtualMachineConfiguration()
|
let config = VZVirtualMachineConfiguration()
|
||||||
@@ -73,63 +71,65 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate {
|
|||||||
config.cpuCount = max(options.cpuCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
|
config.cpuCount = max(options.cpuCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
|
||||||
config.memorySize = max(options.memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize)
|
config.memorySize = max(options.memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize)
|
||||||
|
|
||||||
// Display (vresearch101: 1290x2796 @ 460 PPI — matches vrevm)
|
// Display
|
||||||
let gfx = VZMacGraphicsDeviceConfiguration()
|
let gfx = VZMacGraphicsDeviceConfiguration()
|
||||||
gfx.displays = [
|
gfx.displays = [
|
||||||
VZMacGraphicsDisplayConfiguration(widthInPixels: 1290, heightInPixels: 2796, pixelsPerInch: 460),
|
VZMacGraphicsDisplayConfiguration(
|
||||||
|
widthInPixels: options.screenWidth, heightInPixels: options.screenHeight,
|
||||||
|
pixelsPerInch: options.screenPPI
|
||||||
|
),
|
||||||
]
|
]
|
||||||
config.graphicsDevices = [gfx]
|
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
|
// Storage
|
||||||
if FileManager.default.fileExists(atPath: options.diskURL.path) {
|
guard FileManager.default.fileExists(atPath: options.diskURL.path) else {
|
||||||
let attachment = try VZDiskImageStorageDeviceAttachment(url: options.diskURL, readOnly: false)
|
throw VPhoneError.diskNotFound(options.diskURL.path)
|
||||||
config.storageDevices = [VZVirtioBlockDeviceConfiguration(attachment: attachment)]
|
|
||||||
}
|
}
|
||||||
|
let attachment = try VZDiskImageStorageDeviceAttachment(url: options.diskURL, readOnly: false)
|
||||||
|
config.storageDevices = [VZVirtioBlockDeviceConfiguration(attachment: attachment)]
|
||||||
|
|
||||||
// Network (shared NAT)
|
// Network (shared NAT)
|
||||||
let net = VZVirtioNetworkDeviceConfiguration()
|
let net = VZVirtioNetworkDeviceConfiguration()
|
||||||
net.attachment = VZNATNetworkDeviceAttachment()
|
net.attachment = VZNATNetworkDeviceAttachment()
|
||||||
config.networkDevices = [net]
|
config.networkDevices = [net]
|
||||||
|
|
||||||
// Serial port (PL011 UART — always configured)
|
// Serial port (PL011 UART — interactive stdin/stdout)
|
||||||
// Connect host stdin/stdout directly for interactive serial console
|
if let serialPort = Dynamic._VZPL011SerialPortConfiguration().asObject as? VZSerialPortConfiguration {
|
||||||
do {
|
serialPort.attachment = VZFileHandleSerialPortAttachment(
|
||||||
if let serialPort = VPhoneCreatePL011SerialPort() {
|
fileHandleForReading: FileHandle.standardInput,
|
||||||
serialPort.attachment = VZFileHandleSerialPortAttachment(
|
fileHandleForWriting: FileHandle.standardOutput
|
||||||
fileHandleForReading: FileHandle.standardInput,
|
)
|
||||||
fileHandleForWriting: FileHandle.standardOutput
|
config.serialPorts = [serialPort]
|
||||||
)
|
print("[vphone] PL011 serial port attached (interactive)")
|
||||||
config.serialPorts = [serialPort]
|
|
||||||
print("[vphone] PL011 serial port attached (interactive)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up log file if requested
|
|
||||||
if let logPath = options.serialLogPath {
|
|
||||||
let logURL = URL(fileURLWithPath: logPath)
|
|
||||||
FileManager.default.createFile(atPath: logURL.path, contents: nil)
|
|
||||||
self.consoleLogFileHandle = FileHandle(forWritingAtPath: logURL.path)
|
|
||||||
print("[vphone] Serial log: \(logPath)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi-touch (USB touch screen for VNC click support)
|
// Multi-touch (USB touch screen)
|
||||||
VPhoneConfigureMultiTouch(config)
|
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)
|
config.keyboards = [VZUSBKeyboardConfiguration()]
|
||||||
VPhoneSetGDBDebugStubDefault(config)
|
|
||||||
|
// GDB debug stub (default init, system-assigned port)
|
||||||
|
Dynamic(config)._setDebugStub(Dynamic._VZGDBDebugStubConfiguration().asObject)
|
||||||
|
|
||||||
// Coprocessors
|
// Coprocessors
|
||||||
if options.skipSEP {
|
let sepConfig = Dynamic._VZSEPCoprocessorConfiguration(storageURL: options.sepStorageURL)
|
||||||
print("[vphone] SKIP_SEP=1 — no coprocessor")
|
sepConfig.setRomBinaryURL(options.sepRomURL)
|
||||||
} else if let sepStorageURL = options.sepStorageURL {
|
sepConfig.setDebugStub(Dynamic._VZGDBDebugStubConfiguration().asObject)
|
||||||
VPhoneConfigureSEP(config, sepStorageURL, options.sepRomURL)
|
if let sepObj = sepConfig.asObject {
|
||||||
print("[vphone] SEP coprocessor enabled (storage: \(sepStorageURL.path))")
|
Dynamic(config)._setCoprocessors([sepObj])
|
||||||
} else {
|
print("[vphone] SEP coprocessor enabled (storage: \(options.sepStorageURL.path))")
|
||||||
// Create default SEP storage next to NVRAM
|
|
||||||
let defaultSEPURL = options.nvramURL.deletingLastPathComponent()
|
|
||||||
.appendingPathComponent("sep_storage.bin")
|
|
||||||
VPhoneConfigureSEP(config, defaultSEPURL, options.sepRomURL)
|
|
||||||
print("[vphone] SEP coprocessor enabled (storage: \(defaultSEPURL.path))")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate
|
// Validate
|
||||||
@@ -141,12 +141,14 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate {
|
|||||||
virtualMachine.delegate = self
|
virtualMachine.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - DFU start
|
// MARK: - Start
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func start(forceDFU: Bool, stopOnPanic: Bool, stopOnFatalError: Bool) async throws {
|
func start(forceDFU: Bool) async throws {
|
||||||
let opts = VZMacOSVirtualMachineStartOptions()
|
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" : "")...")
|
print("[vphone] Starting\(forceDFU ? " DFU" : "")...")
|
||||||
try await virtualMachine.start(options: opts)
|
try await virtualMachine.start(options: opts)
|
||||||
if forceDFU {
|
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
|
// MARK: - Delegate
|
||||||
|
|
||||||
func guestDidStop(_: VZVirtualMachine) {
|
// VZ delivers delegate callbacks via dispatch source on the main queue.
|
||||||
|
|
||||||
|
nonisolated func guestDidStop(_: VZVirtualMachine) {
|
||||||
print("[vphone] Guest stopped")
|
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)")
|
print("[vphone] Stopped with error: \(error)")
|
||||||
done = true
|
exit(EXIT_FAILURE)
|
||||||
}
|
}
|
||||||
|
|
||||||
func virtualMachine(_: VZVirtualMachine, networkDevice _: VZNetworkDevice,
|
nonisolated func virtualMachine(_: VZVirtualMachine, networkDevice _: VZNetworkDevice,
|
||||||
attachmentWasDisconnectedWithError error: Error)
|
attachmentWasDisconnectedWithError error: Error)
|
||||||
{
|
{
|
||||||
print("[vphone] Network 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)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
16
sources/vphone-cli/VPhoneVMView.swift
Normal file
16
sources/vphone-cli/VPhoneVMView.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
38
sources/vphone-cli/VPhoneWindowController.swift
Normal file
38
sources/vphone-cli/VPhoneWindowController.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
sources/vphone-cli/main.swift
Normal file
11
sources/vphone-cli/main.swift
Normal 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()
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user