Build distributable VPhone bundle and remove OpenAPI document

This commit is contained in:
Lakr
2026-09-24 21:42:30 +09:00
parent 7d42b1542c
commit 456511f8e8
75 changed files with 411 additions and 480 deletions
+29 -177
View File
@@ -4,16 +4,16 @@ Virtual iPhone boot tool using Apple's Virtualization.framework with PCC researc
## Quick Reference
- **Build:** `zsh Scripts/build.sh` (builds, signs, and bundles the app)
- **Test:** `swift test`; then `zsh Scripts/check_aux.sh` for bundle admission gates
- **Build:** `xcodebuild -workspace VPhone.xcworkspace -scheme VPhone -configuration Debug -destination 'platform=macOS,arch=arm64' -derivedDataPath .build/XcodeBundle build`
- **Test:** Run the Xcode test schemes in their owning projects; then `zsh Scripts/check_aux.sh`
- **Boot (GUI):** `vphone-cli vm launch <name>`
- **Boot (DFU):** `vphone-cli vm launch <name> --dfu`
- **AMFI refuses `vphone-vm`?** Use the bundled `vphone-amfi-allow` helper as shown in `Documents/Guides/host-setup.md`. Repeat after every build because it allowlists cdhashes. See Key Patterns.
- **Restore:** `vphone-cli restore`, in process. Vendored libirecovery + idevicerestore (`Sources/MobileRecoveryCore`, `Sources/MobileRestoreCore`) over the `AppleMobileDeviceLibrary` xcframeworks. No interpreter, no environment to provision, no setup step. See `Research/Restore/native_restore_architecture.md`.
- **AMFI refuses `vphone-vm`?** Use the bundled `VPhoneEscalator` allowlist tool as shown in `Documents/Guides/host-setup.md`. Repeat after the VM cdhash changes.
- **Restore:** `vphone-cli restore` runs in process over the vendored recovery and restore C modules in `VPhoneExecutable/VPhoneCommand/VPhoneRestore`.
- **Platform:** macOS 15+ (Sequoia). `vphone-vm` needs amfid to accept its private entitlements: either SIP off with `amfi_get_out_of_my_way=1`, or SIP on (`--without debug`) plus an allowlist bypass the user runs. Both are in `Documents/Guides/host-setup.md`; neither is installed by this project.
- **Language:** Swift 6.0 (SwiftPM), private APIs via [Dynamic](https://github.com/mhdhejazi/Dynamic). This package's own manifest is `swift-tools-version:6.0`, but the **toolchain floor is Swift 6.2**: `libcapstone-spm` declares 6.2 so that it can reach `CSetting.disableWarning` instead of `.unsafeFlags`, which is what lets it be depended on by version at all.
- **Language:** Swift 6.0 in handwritten Xcode projects, private APIs via [Dynamic](https://github.com/mhdhejazi/Dynamic).
- **Dependencies:** Host and guest SwiftPM packages resolve dependencies by URL and version; `Package.resolved` pins the full graphs. There are no git submodules. **No Python anywhere, and no Homebrew package at runtime** — see Tiers below.
- **Tiers.** Three environments run code here and the rules differ. **build** (the machine that builds the `.app`) may use Xcode, `xcrun`, clang, swift, git and Homebrew. **dist** (the shipped `.app`, on a clean macOS) may use `/usr/lib`, `/System` and the bundle — nothing else, no `PATH` lookup. **guest** (inside the VM) is out of host self-containment scope. Every script declares its tier on **line 2** (`# vphone-tier: dist`); `Scripts/dist_manifest.sh` reads those and is what `Scripts/build.sh` stages from, so a script that declares nothing ships nowhere. `Scripts/check_aux.sh` runs the admission gates. The dist tier's registered-exception list is **empty** and a release requires it to stay that way.
- **Tiers.** The build machine may use Xcode and build tools. The shipped `VPhone.bundle` must use only system libraries and its own contents at runtime. Guest components run inside the VM. `Scripts/check_aux.sh` checks bundle admission.
## Workflow Rules
@@ -39,173 +39,26 @@ See `Research/` for detailed firmware pipeline, component origins, patch breakdo
## Architecture
```
Package.swift # SwiftPM targets and tests
`VPhone.xcworkspace` contains handwritten Xcode projects. Do not introduce project generators, SwiftPM app targets, recovered references, or linked source folders. Source folders should carry the owning project or target name.
Sources/
├── vphone.entitlements # Private API entitlements (7 keys) — signed ONTO vphone-vm ONLY
│
├── VPhoneCLI/ # Entry point. NO entitlements, so it always launches.
│ │ # Argument parsing + orchestration; spawns the others.
│ ├── main.swift # Parses, and forwards `boot` to vphone-vm
│ ├── VPhoneCLI.swift # Root command, patch-firmware/patch-component
│ ├── VPhoneFirmwareCLI.swift # Firmware subcommands
│ ├── VPhoneSetupCLI.swift # Setup subcommands
│ ├── VPhoneRestoreCLI.swift # Restore and recovery commands
│ ├── VPhoneCustomFirmwarePatchCLI.swift # `cfw` verbs: cryptex-paths, inject-*, patch-*
│ ├── VPhoneCustomFirmwareMachOVerbsCLI.swift # The six Mach-O `cfw patch-*` verbs
│ ├── VPhoneCustomFirmwareDyldSharedCacheVerbsCLI.swift # Dyld shared cache verbs
│ ├── VPhoneSignCLI.swift # `sign` — VPhoneSign from the command line
│ ├── VPhoneVirtualMachineCLI.swift # VM subcommand group
│ ├── VPhoneVirtualMachineCreateCLI.swift # VM create
│ ├── VPhoneVirtualMachineLaunchCLI.swift # VM launch
│ ├── VPhoneVirtualMachineTransferCLI.swift # VM transfer
│ ├── VPhoneVirtualMachineCreateOptions.swift # Create-flow option set
│ ├── VPhoneVirtualMachineCreator.swift # Native `vm create` pipeline driver
│ ├── VPhoneCustomFirmwareInstaller.swift # Native host-mount JB install
│ ├── VPhoneHostPreflight.swift # Native host launch check
│ ├── VPhoneFirmwareSourceSelection.swift # Firmware source resolver
│ ├── VPhoneVirtualMachineSelection.swift # Interactive VM picker
│ └── VPhoneProgressBar.swift # Terminal progress rendering
│
├── VPhoneVM/ # The ONLY entitled binary — a parse and a run loop
│ └── main.swift # VPhoneBootCLI.parseOrExit() → VPhoneGuestApp.run()
│
├── VPhoneArchiveCLI/ # Thin shell over VPhoneArchive
│ └── main.swift # extract / create / decompress / list / cat / fingerprint
│
├── VPhoneCore/ # No UI, no guest — what both entry points share
│ ├── VPhoneBootCLI.swift # Boot flags, parsed by both binaries; renders argv
│ ├── VPhoneGuestLauncher.swift # Spawns vphone-vm; explains an amfid refusal
│ ├── VPhoneBundle*.swift # VM bundle layout, ops, reporting
│ ├── VPhoneVirtualMachineManifest.swift # config.plist (replaced scripts/vm_manifest.py)
│ ├── VPhoneAPFSSnapshot.swift # Offline APFS boot-snapshot flip
│ └── … # networking, resources, process running, pickers
│
├── VPhoneArchive/ # libarchive: replaces gtar, bsdtar, unzip and zstd
│ ├── VPhoneArchiveExtractor.swift # Unpack, incl. the hand-written --no-overwrite-dir
│ ├── VPhoneArchiveWriter.swift # Pack + single-stream decompress
│ ├── VPhoneArchivePaths.swift # realpath(3) — NOT the Foundation equivalents
│ └── VPhoneTreeFingerprint.swift # Compare two extracted trees, field by field
│
├── VPhoneAPIKit/ # Public unentitled HTTP/WebSocket API client for vphone-ui
│ └── VPhoneAPIClient.swift # Typed JSON values, RPC, events, streaming file transfer
│
├── VPhoneSign/ # Mach-O code signing — replaces ldid, byte for byte
│ ├── VPhoneSigner.swift # Ad-hoc and PKCS#12 signing
│ ├── VPhoneCodeSignature.swift # SuperBlob / CodeDirectory construction
│ ├── VPhoneSignEntitlements.swift # Entitlements plist blob (+ …Reader for reading one back)
│ ├── VPhoneSignDER.swift # The DER entitlements blob
│ └── VPhoneMachOImage.swift # Slice parsing. ARM only — an x86 slice is refused
│
├── MobileRecoveryCore/ # libirecovery master, vendored C. IOKit USB, not libusb
│ ├── libirecovery.c # Upstream's bytes, unmodified. master, NOT 1.3.1 — the
│ │ # release predates the iPhone99,11 / vresearch101ap entry
│ │ # and without it a restore cannot identify the vphone VM
│ ├── Include/libirecovery.h # Upstream's public header
│ └── config.h # Ours — what ./configure concludes on macOS
│
├── MobileRestoreCore/ # idevicerestore, vendored C, built IDEVICERESTORE_NOMAIN
│ ├── restore.c asr.c fdr.c img4.c …# Upstream's bytes, unmodified (~19.5k lines)
│ ├── vphone_restore_bridge.c # Ours — the library entry point upstream's main() was
│ ├── vphone_zip_stub.c + zip.h # Ours — libzip has no counterpart here; see zip.h's header
│ ├── config.h # Ours
│ └── Include/vphone_restore_bridge.h # The only header a dependent sees
│
├── VPhoneRestore/ # Swift over those two C targets — replaced the Python bridge
│ ├── VPhoneRecoveryProbe.swift # irecv_open_with_ecid_and_attempts + timeout polling
│ ├── VPhoneRestoreService.swift # The three ported commands: probe, get-shsh, restore
│ ├── VPhoneRestoreTicket.swift # Undoes idevicerestore's -t: gzipped binary plist → plain
│ ├── VPhoneRestoreRunner.swift # Drives vphone_restore_run
│ └── … # options, identity, restore-tree layout, events, errors
│
├── FirmwarePatcher/ # The Swift firmware pipeline (largest module)
│ ├── IBoot/ Kernel/ TXM/ # Boot-chain patches; Kernel/JailbreakPatches/ is the JB set
│ ├── DeviceTree/ CryptexFilesystem/ # DT edits, cryptex/rootfs work
│ └── ARM64/ Binary/ PatchInfrastructure/ Pipeline/ # Disassembly, Mach-O, driver
│
└── VPhoneVirtualMachineKit/ # Everything that touches a running guest
├── VPhoneGuestApp.swift # NSApplication wiring (keeps the entry point logic-free)
├── VPhoneVirtualMachineAppDelegate.swift # App lifecycle, SIGINT, VM start/stop
├── VPhoneHostAutomationServer.swift # Unix-socket automation server (one JSON line in/out)
├── VPhoneBootCLI+VirtualMachine.swift # resolveOptions() — the half that needs Virtualization
│
├── VirtualMachine/ # VM core
│ ├── VPhoneVirtualMachine.swift # @MainActor VM configuration and lifecycle
│ ├── VPhoneVirtualMachineView.swift # Touch-enabled VZVirtualMachineView + helpers
│ ├── VPhoneVirtualMachineHardwareModel.swift # PV=3 hardware model via Dynamic
│ └── VPhoneVirtualMachineError.swift # Error types
│
├── GuestCommunication/ # Guest daemon client (vsock)
│ ├── VPhoneGuestControl.swift # Host-side HTTP client over direct VSOCK 1339
│ ├── VPhoneAPIProxy.swift # Opt-in TCP to guest VSOCK 1339 transparent proxy
│ ├── VPhoneGuestControlApps.swift # Installed apps — list and launch
│ ├── VPhoneGuestControlKeychain.swift # Keychain dump
│ └── VPhoneGuestControlSystem.swift # Device, battery, location, devmode
│
├── UserInterface/ # Window & UI
│ ├── VPhoneVirtualMachineWindowController.swift # VM window + toolbar
│ ├── VPhoneVirtualMachineKeySender.swift # Keyboard/hardware keys to VM
│ │
│ ├── Menu/ # Menu bar (extensions on VPhoneMenuController)
│ │ ├── VPhoneMenuController.swift # Menu bar controller
│ │ ├── VPhoneMenuApps.swift # Apps menu — installed app browser
│ │ ├── VPhoneMenuBattery.swift # Battery menu — battery status display
│ │ ├── VPhoneMenuCamera.swift # Camera menu — virtual camera source
│ │ ├── VPhoneMenuConnect.swift # Connect menu — devmode, ping, guest hash, file browser
│ │ ├── VPhoneMenuKeys.swift # Keys menu — home, power, volume, spotlight
│ │ ├── VPhoneMenuLocation.swift # Location menu — host location sync toggle
│ │ └── VPhoneMenuRecord.swift # Record menu — screen recording controls
│ │
│ └── Browsers/ # SwiftUI browsers in NSHostingController windows
│ ├── VPhoneFileBrowserModel.swift # @Observable file browser state + transfers
│ ├── VPhoneFileBrowserView.swift # SwiftUI file browser with search + drag-drop
│ ├── VPhoneFileWindowController.swift # File browser window
│ ├── VPhoneRemoteFile.swift # Remote file data model
│ ├── VPhoneAppBrowserModel.swift # App browser state
│ ├── VPhoneAppBrowserView.swift # SwiftUI app browser
│ ├── VPhoneAppWindowController.swift # App browser window
│ ├── VPhoneKeychainBrowserModel.swift # Keychain browser state
│ ├── VPhoneKeychainBrowserView.swift # SwiftUI keychain browser
│ ├── VPhoneKeychainWindowController.swift # Keychain browser window
│ ├── VPhoneKeychainItem.swift # Keychain item data model
│ └── VPhoneQuickLookController.swift # Quick Look preview panel
│
└── HostDevices/ # Host capability bridges into the running VM
├── VPhoneCameraServer.swift # Virtual-camera server (vsock port 1338)
├── VPhoneFrameProducer.swift # BGRA frame sources for the camera server
├── VPhoneLocationProvider.swift # CoreLocation → guest forwarding over vsock
├── VPhoneTouchIDMonitor.swift # BiometricKit delegate sink
└── VPhoneScreenRecorder.swift # VM screen recording to file
- `VPhoneExecutable/VPhoneVirtualization`: `VPhone.bundle`, `vphone-vm`, and `VPhoneVirtualMachineKit`. The bundle is the distributable output and has no app launcher.
- `VPhoneExecutable/VPhoneCommand`: `vphone-cli`, firmware patcher, signer, and command tests.
- `VPhoneExecutable/VPhoneCommand/VPhoneRestore`: native restore code and tests.
- `VPhoneExecutable/VPhoneEscalator`: the AMFI allowlist program, built for arm64e. It is not a general privilege service.
- `VPhoneKit`: shared core, archive, and external access kits with their tests.
- `VPhoneDaemon`: guest `vphoned`, native code including `vpregister`, and daemon configuration.
- `VPhoneGuestComponents`: guest dylibs built by Makefile from the bundle build phase.
Scripts/ # Build scripts and payloads only; no runtime shell
├── build.sh [b] # Compile, sign and bundle
├── dist_manifest.sh [b] # Payload allowlist staged by build.sh
├── check_aux.sh [b] # The self-containment admission gates
└── VPhoned/ # Guest SwiftNIO/IcliKit package plus native installer/keychain/camera code
Siblings/ # Guest component sources/provenance; separate package
├── CamFix/ VCamCaptured/ # Camera hooks and filter plists
├── TweakLoader/ VPRegister/ # Loader and iOS 27 app registrar
└── GraphicLoader/ # PCC firmware provenance; no Apple binary
Research/ # Detailed firmware/patch documentation
```
The `VPhone` scheme puts all shipped Mach-O files in `VPhone.bundle/Contents/MacOS`. Guest configuration is in `Contents/Resources`. Xcode targets have `CODE_SIGNING_ALLOWED=NO`; the bundle build phase signs each binary ad hoc with only its own entitlements, then seals the outer bundle. `VPhoneVirtualization.entitlements` belongs to `vphone-vm`; `VPhoneDaemon.entitlements` belongs to `vphoned`. The bundle and CLI have no private entitlements.
### Key Patterns
- **Three host binaries, one of them entitled.** `vphone-cli` carries no entitlements, so it launches on any host and is always there to explain what is wrong. `vphone-vm` holds all 7 private keys and is the only thing amfid can refuse. `vphone-archive` does the unpacking. **Do not sign `vphone-cli` with entitlements** — that is how it used to be, and it is why the entry point could not start without a bypass already running.
- **The AMFI bypass is ours, and it writes heap, not code.** `vphone-amfi-allow` (`Sources/VPhoneAMFIAllow/`, this project's copy of [Lakr233/amfi-allow](https://github.com/Lakr233/amfi-allow)) puts the cdhashes of both `vphone-vm` copies into `/Library/Preferences/com.apple.security.coderequirements.plist` — a file AMFI already reads — and flips one byte of `_isRunningInternalBuild` in amfid's `AMFIRequirementsManager` singleton so it consults that file. It is an **allowlist**, scoped to the cdhashes you name; do not describe it as a global switch. The helper's `allow`, `status`, and `off` commands manage it; see `Documents/Guides/host-setup.md`. It is a **per-build** step, because a cdhash changes with every signature. It must be **arm64e** to match amfid's slice, and `Scripts/build.sh` builds it with clang. The heap write is the load-bearing detail: `vphone-letmein` and LLDB-based tools dirty an executable page, and on a host with `vm.cs_system_enforcement = 1` the kernel kills amfid for that and takes the guest with it.
- **Guest launches go through `VPhoneGuestLaunchPlanner`** (`VPhoneCore`). It resolves `vphone-vm` as a sibling of the running image — never through `PATH` — checks its two PV=3 entitlements, then probes with `vphone-vm --help` for SIGKILL. A refusal is reported with the exact command the user has to run; the planner never arranges a bypass itself. Never spawn the guest directly.
- **Restore runs in `vphone-cli`'s own process.** `VPhoneRestore` calls `vphone_restore_run()` in `MobileRestoreCore`; there is no subprocess, no bridge script and no environment to resolve first. The three commands the old Python bridge exposed became `restore --get-shsh`, `restore` and `restore --offline`; its fourth, `usbmux-list`, had no call site and was not ported. `Research/Restore/native_restore_architecture.md` has the decision and the behaviour table.
- **Private API access:** Via [Dynamic](https://github.com/mhdhejazi/Dynamic) library (runtime method dispatch from pure Swift). No ObjC bridge.
- **App lifecycle:** `VPhoneVM/main.swift` → `VPhoneGuestApp.run()` → `NSApplication` + `VPhoneVirtualMachineAppDelegate`. Entry points hold no logic.
- **Configuration:** `ArgumentParser` → `VPhoneBootCLI` (in `VPhoneCore`, parsed by both binaries) → `VPhoneVirtualMachine.Options` → `VZVirtualMachineConfiguration`.
- **Guest daemon (vphoned):** SwiftNIO HTTP/WebSocket API on VSOCK 1339, using IcliKit for common device operations. The complete pinned icli CLI is installed inside the guest and available through `icli.execute` with an argv array. `VPhoneGuestControl` reaches HTTP directly over VSOCK; the former length-prefixed service on 1337 is removed. Camera data remains on 1338. The host exposes 1339 only when boot is given `--api-listen`.
- **Menu system:** `VPhoneMenuController` + per-menu extensions (Keys, Type, Location, Connect, Install, Record).
- **File browser:** SwiftUI (`VPhoneFileBrowserView` + `VPhoneFileBrowserModel`) in `NSHostingController`. Search, sort, upload/download, drag-drop via `VPhoneGuestControl`.
- **IPA installation:** `VPhoneIPAInstaller` extracts + re-signs via `VPhoneSigner` + installs over vsock.
- **Screen recording:** `VPhoneScreenRecorder` captures VM display. Controls via Record menu.
- `vphone-cli` is the unentitled entry point. `VPhoneGuestLaunchPlanner` resolves `vphone-vm` beside the running executable, checks its entitlements, probes AMFI, and reports the exact allowlist command on refusal. It never obtains root itself.
- `VPhoneEscalator` manages AMFI cdhash admission only. It writes amfid heap state, not executable code. Root authorization and installation are owned by the user or a future workstation application; this repository has no SMJobBless or sudo password flow.
- Host VM artifacts, caches, and archives created by vphone use mode `0777` for workstation access. Symlinks are not followed when changing permissions. Guest filesystem modes inside `Disk.img` remain unchanged.
- `VPhoneRestore` runs in the CLI process over vendored libirecovery and idevicerestore. No Python or runtime Homebrew dependency is allowed.
- The VM process owns AppKit windows and the guest control connection. `vphoned` serves HTTP and WebSocket over VSOCK 1339, with camera data on 1338.
- The guest HTTP client, menus, file browser, IPA installation, and screen recorder live in `VPhoneVirtualMachineKit`.
---
@@ -229,14 +82,13 @@ Research/ # Detailed firmware/patch documentation
### Patchers
Every patcher is Swift, in `Sources/FirmwarePatcher`. The boot chain and kernel
run through `patch-firmware`; the CFW/DSC patchers are `vphone-cli cfw <verb>`,
one verb per patch, driven by `scripts/cfw_install*.sh` only.
Every patcher is Swift, in `VPhoneExecutable/VPhoneCommand/FirmwarePatcher`.
The public firmware mode is JB; the CFW/DSC patchers are `vphone-cli cfw <verb>`.
- Disassembly is Capstone via `ARM64Disassembler` (the `libcapstone-spm` package). Assembly is `ARM64Encoder` plus the pre-encoded constants in `ARM64` (`ARM64Constants.swift`) — together they replace keystone's `asm()` / `asm_at()`, and `ARM64.nop` / `ARM64.movW0_0` are the old `NOP` / `MOV_W0_0`. IM4P containers go through `IM4PHandler` (the `libimg4-spm` package), which replaces pyimg4. Both resolve by URL; there is no `vendor/` directory to check out first.
- Dynamic pattern finding (string anchors, ADRP+ADD xrefs, BL frequency) — no hardcoded offsets.
- Each patch logged with offset and before/after state.
- No interpreter, no Python environment, no native-library repair: `Scripts/build.sh` builds the complete app.
- No interpreter or runtime native-library repair: the `VPhone` Xcode scheme builds the complete bundle.
### Python
@@ -246,7 +98,7 @@ There is none, and adding any is a regression.
heredoc, and nothing resolves a `python3` at runtime. `git ls-files '*.py'`
returns nothing; that is the standing check.
- There is no environment to activate and no dependency list to install. The
restore backend was the last holdout and is now `Sources/VPhoneRestore` over
restore backend was the last holdout and is now `VPhoneExecutable/VPhoneCommand/VPhoneRestore` over
two vendored C targets — see `Research/Restore/native_restore_architecture.md`.
- A patch, a probe, a format reader or a device protocol belongs in Swift,
where it is built, signed, gated by `Scripts/check_aux.sh` and tested with
@@ -255,21 +107,21 @@ There is none, and adding any is a regression.
`Scripts/check_aux.sh` cannot see.
- There is no counter-example left. `amfidont` used to be cited as one — a
third-party tool the user installed into their own Python — and it is gone
too: the AMFI bypass is `Sources/VPhoneAMFIAllow`, one C file built by
clang, and it needs no interpreter, no LLDB and no Xcode.
too: the AMFI allowlist tool is `VPhoneExecutable/VPhoneEscalator`, one C
file built by Xcode for arm64e, with no runtime interpreter or LLDB.
### Kernel patcher guardrails
- For kernel patchers, never hardcode file offsets, virtual addresses, or preassembled instruction bytes inside patch logic.
- All instruction matching must be derived from Capstone decode results (mnemonic / operands / control-flow), not exact operand-string text when a semantic operand check is possible. `ARM64Disassembler` is the only decoder — match on the decoded mnemonic and operand detail, never on a formatted operand string.
- All replacement instruction bytes must come from Keystone-backed helpers already used by the project: `ARM64Encoder.encode*` and the `ARM64` constants, which were generated by keystone-engine, verified by Capstone round-trip, and are asserted word for word against keystone in `Tests/FirmwarePatcherTests/ARM64EncoderTests.swift`. Never write a literal instruction word at a patch site. A new instruction means a new encoder plus its keystone-checked test case, not a raw `Data`. Keystone is deliberately **not** a project dependency any more — nothing at runtime or in the test suite calls it, and the expected words are frozen constants. To derive a new one, stand keystone up in a throwaway environment **outside this repository** (`brew install keystone`, plus `keystone-engine` in a scratch interpreter somewhere under `/tmp`) and run the one-liner in that test file's header against it. There is no dependency list here to add it to and no environment here to install it into; creating either is the regression the "Python" section above forbids. Do not invent an expected word without checking it.
- All replacement instruction bytes must come from Keystone-backed helpers already used by the project: `ARM64Encoder.encode*` and the `ARM64` constants, which were generated by keystone-engine, verified by Capstone round-trip, and are asserted word for word against keystone in `VPhoneExecutable/VPhoneCommand/FirmwarePatcherTests/Core/ARM64EncoderTests.swift`. Never write a literal instruction word at a patch site. A new instruction means a new encoder plus its keystone-checked test case, not a raw `Data`. Keystone is deliberately **not** a project dependency any more — nothing at runtime or in the test suite calls it, and the expected words are frozen constants. To derive a new one, stand keystone up in a throwaway environment **outside this repository** (`brew install keystone`, plus `keystone-engine` in a scratch interpreter somewhere under `/tmp`) and run the one-liner in that test file's header against it. There is no dependency list here to add it to and no environment here to install it into; creating either is the regression the "Python" section above forbids. Do not invent an expected word without checking it.
- Prefer source-backed semantic anchors: in-image symbol lookup, string xrefs, local call-flow, and XNU correlation. Do not depend on repo-exported per-kernel symbol dumps at runtime.
- When retargeting a patch, write the reveal procedure and validation steps into the relevant research doc or commit notes before handing off for testing. Do not create `TODO.md`.
- For `patchBsdInitAuth` (`Kernel/JailbreakPatches/Storage/KernelJailbreakPatchBsdInitAuth.swift`, named `patch_bsd_init_auth` in the research docs) specifically, the allowed reveal flow is: recover `bsd_init` -> locate rootvp panic block -> find the unique in-function `call` -> `cbnz w0/x0, panic` -> `bl imageboot_needed` site -> patch the branch gate only.
## Build & Sign
The binary requires private entitlements for PV=3 virtualization. Always use `zsh Scripts/build.sh` — never `swift build` alone, as the unsigned binary will fail at runtime.
The VM process requires private entitlements for PV=3 virtualization. Build the `VPhone` scheme in Xcode; its final build phase ad hoc signs each child and seals `VPhone.bundle`. `swift build` alone does not produce the distributable bundle.
## Design System
+40
View File
@@ -0,0 +1,40 @@
# VPhone bundle integration
The `VPhone` Xcode scheme produces `VPhone.bundle`. It is a container for
executables and resources, not a macOS app or a dynamically loaded plug-in.
`vphone-workstation` should keep the bundle intact and launch
`Contents/MacOS/vphone-cli` as a process. The CLI locates its companion
`vphone-vm` by resolving its own executable path.
| Path | Role |
| --- | --- |
| `Contents/MacOS/vphone-cli` | Unentitled command entry point |
| `Contents/MacOS/vphone-vm` | VM and window process; private virtualization entitlements |
| `Contents/MacOS/VPhoneEscalator` | AMFI allowlist tool for the current VM cdhash |
| `Contents/MacOS/vphoned.signed` | Guest daemon payload with its own entitlements |
| `Contents/MacOS/vpregister` and guest dylibs | Guest installation payloads |
| `Contents/Resources` | Guest configuration and nonexecutable resources |
All executable payloads use ad hoc code signatures. Only the required child
processes carry private entitlements. The bundle has no `CFBundleExecutable`,
app launcher, SMJobBless helper, installer, password prompt, or automatic root
acquisition. The integrating application owns download, release verification,
host authorization, installation, and update policy. Developer Tools access
and AMFI authorization are separate host decisions.
Install and update the whole bundle as one versioned unit. Do not rewrite a
signed binary in place. When `vphone-vm` changes, its cdhash changes; the
integrating application must arrange AMFI admission for the new build before
launching it. The VM library and caches stay outside `VPhone.bundle`, under
`~/.vphone` by default. Host-side VM outputs use mode `0777` so a separate
workstation process can access them, including after a root-run create.
Guest filesystem permissions inside `Disk.img` retain their own semantics.
Build and inspect:
```sh
xcodebuild -workspace VPhone.xcworkspace -scheme VPhone \
-configuration Debug -destination 'platform=macOS,arch=arm64' \
-derivedDataPath .build/XcodeBundle build
zsh Scripts/check_aux.sh .build/XcodeBundle/Build/Products/Debug/VPhone.bundle
```
+2 -2
View File
@@ -15,11 +15,11 @@ vphone-cli vm create myphone \
--cloudos-source /path/to/cloudOS.ipsw
```
Creation runs prepare → JB patch → online DFU restore → host-mounted CFW installation → first GUI boot. It needs network access for the restore ticket. CFW installation needs administrator authentication; on a host without an interactive terminal, `--root-popup` uses a macOS authentication dialog. The default virtual disk is 64 GB. `--keep-artifacts` retains the large prepared restore tree; omit it when disk space matters.
Creation runs prepare → JB patch → online DFU restore → host-mounted CFW installation → first GUI boot. It needs network access for the restore ticket. The caller must provide root privileges for CFW installation; this bundle has no authorization dialog or privilege helper. The default virtual disk is 64 GB. `--keep-artifacts` retains the large prepared restore tree; omit it when disk space matters.
`fw prepare` creates a temporary vphone VM, starts it in DFU, and restores the selected cloudOS IPSW using this project's restore backend. It mounts the restored System volume read-only, extracts the GPU bundle, then removes the temporary VM. This first restore supplies the GPU driver for the second, hybrid iPhone restore. `vm create` and `fw prepare` also accept `--gpu-driver-bundle /path/to/AppleParavirtGPUMetalIOGPUFamily.bundle` to reuse a previously extracted bundle without the temporary restore. The CLI checks its iPhoneOS platform version against the selected cloudOS version. Each restore obtains its own ticket online.
The build also ships an arm64e GPU compiler plugin as a compressed app resource. `fw prepare` extracts it and merges it into the staged GPU bundle before firmware patching and installation.
The build also ships an arm64e GPU compiler plugin in the bundle. `fw prepare` copies it into the staged GPU bundle before firmware patching and installation.
Success ends with `First boot: vphoned ping succeeded` and `JB VM created; vphoned connected`. **The verification VM is then stopped.** The ping proves the daemon answered over the host control socket during that boot; it does not leave a running VM behind.
+12 -12
View File
@@ -6,16 +6,16 @@ The VM needs an Apple Silicon Mac running macOS 15 or newer. PV=3 research guest
## Build and preflight
A distributed `.app` needs no Homebrew, Python or Xcode **at runtime**. A source build needs Xcode and its iPhoneOS SDK to compile vphoned. From a source checkout:
A distributed `.bundle` needs no Homebrew, Python or Xcode **at runtime**. A source build needs Xcode and its iPhoneOS SDK to compile vphoned. From a source checkout:
```sh
xcodebuild -workspace VPhone.xcworkspace -scheme vphone-app \
xcodebuild -workspace VPhone.xcworkspace -scheme VPhone \
-configuration Debug -destination 'platform=macOS,arch=arm64' \
-derivedDataPath .build/XcodeApp build
.build/XcodeApp/Build/Products/Debug/vphone-app.app/Contents/MacOS/vphone-cli host preflight
-derivedDataPath .build/XcodeBundle build
.build/XcodeBundle/Build/Products/Debug/VPhone.bundle/Contents/MacOS/vphone-cli host preflight
```
Build the `vphone-app` scheme in Xcode to produce the signed app with all companion binaries. `host preflight` checks the entitled companion before any VM is started. If AMFI refuses it, the error prints the bundled allowlist helper command.
Build the `VPhone` scheme in Xcode to produce the ad hoc signed bundle with all companion binaries. `host preflight` checks the entitled companion before any VM is started. If AMFI refuses it, the error prints the bundled allowlist helper command.
## Permit the entitled VM binary
@@ -50,16 +50,16 @@ csrutil allow-research-guests enable
After rebooting, allowlist the **current signed build**. In a source checkout:
```sh
app=.build/XcodeApp/Build/Products/Debug/vphone-app.app
sudo "$app/Contents/MacOS/VPhoneEscalator" allow "$app/Contents/MacOS/vphone-vm"
"$app/Contents/MacOS/VPhoneEscalator" status
"$app/Contents/MacOS/vphone-cli" host preflight
bundle=.build/XcodeBundle/Build/Products/Debug/VPhone.bundle
sudo "$bundle/Contents/MacOS/VPhoneEscalator" allow "$bundle/Contents/MacOS/vphone-vm"
"$bundle/Contents/MacOS/VPhoneEscalator" status
"$bundle/Contents/MacOS/vphone-cli" host preflight
```
The helper records the current `vphone-vm` cdhash in the AMFI code-requirements preference and enables amfid to consult it by changing one byte in its heap. It is scoped to that signed binary. **Repeat the `allow` command after every build**, including a rebuild that only changes the signature. Run `sudo "$app/Contents/MacOS/VPhoneEscalator" off` to remove the allowlist and restart amfid.
The helper records the current `vphone-vm` cdhash in the AMFI code-requirements preference and enables amfid to consult it by changing one byte in its heap. It is scoped to that signed binary. **Repeat the `allow` command after every build**, including a rebuild that only changes the signature. Run `sudo "$bundle/Contents/MacOS/VPhoneEscalator" off` to remove the allowlist and restart amfid.
For a distributed app without a source checkout, run `vphone-cli host preflight` first. If AMFI refuses the guest, its error gives the full `sudo .../VPhoneEscalator allow .../vphone-vm` command for that installed app.
For a distributed bundle without a source checkout, run `vphone-cli host preflight` first. If AMFI refuses the guest, its error gives the full `sudo .../VPhoneEscalator allow .../vphone-vm` command for that bundle.
## What the build contains
`vphone-cli` orchestrates the work without private entitlements and handles archives through `vphone-cli archive`. `vphone-vm` is the signed, entitled GUI/VM process. The app also bundles `vphoned`, compiled for iOS at build time; it is installed into each created guest. The [research notes on the binary split](../../Research/Host/host_binary_split.md) record the implementation history, including superseded approaches.
`vphone-cli` orchestrates the work without private entitlements and handles archives through `vphone-cli archive`. `vphone-vm` is the signed, entitled GUI/VM process. The bundle also contains `vphoned`, compiled for iOS at build time; it is installed into each created guest. The [research notes on the binary split](../../Research/Host/host_binary_split.md) record the implementation history, including superseded approaches.
+1 -1
View File
@@ -21,5 +21,5 @@ These pages give a translated overview and quick start. The English guides above
- [Research index](../Research/README.md) groups the patch and implementation records by subject.
- [Patch inventory](../Research/0_binary_patch_comparison.md) is the canonical per-component comparison.
- `zsh Scripts/build.sh` builds and bundles the application; `swift test` and `zsh Scripts/check_aux.sh` run the tests and bundle checks.
- `xcodebuild -workspace VPhone.xcworkspace -scheme VPhone build` produces `VPhone.bundle`; run each project's test scheme and `zsh Scripts/check_aux.sh` for bundle checks.
- `vphone-cli <group> --help` shows the CLI command surface.
+6 -5
View File
@@ -28,11 +28,10 @@ cloudOS 26.4(`23E5207q`)との組み合わせで、iPhone17,3 の iOS 26.6.2
## インストールとビルド
配布 `.app` の実行に Homebrew、Python、Xcode や別の実行環境は不要です。[GitHub Releases](https://github.com/Lakr233/vphone-cli/releases) から `vphone-cli-2.0.0.zip` をダウンロードして展開し、アプリ内の CLI を直接実行します。
Xcode は、将来 `vphone-workstation` に組み込む `VPhone.bundle` を生成します。実行時に Homebrew、Python、Xcode は不要です。bundle 内の CLI を直接実行できます。
```sh
ditto -x -k vphone-cli-2.0.0.zip .
./vphone-cli.app/Contents/MacOS/vphone-cli host preflight
.build/XcodeBundle/Build/Products/Debug/VPhone.bundle/Contents/MacOS/vphone-cli host preflight
```
上の例では、`vphone-cli` をこのアプリ内のパスに置き換えられます。ソースからのビルドには、vphoned をコンパイルするための iPhoneOS SDK を含む Xcode が必要です。
@@ -40,8 +39,10 @@ ditto -x -k vphone-cli-2.0.0.zip .
```sh
git clone https://github.com/Lakr233/vphone-cli.git
cd vphone-cli
zsh Scripts/build.sh
.build/release/vphone-cli host preflight
xcodebuild -workspace VPhone.xcworkspace -scheme VPhone \
-configuration Debug -destination 'platform=macOS,arch=arm64' \
-derivedDataPath .build/XcodeBundle build
zsh Scripts/check_aux.sh
```
AMFI の許可リストを使う場合、ビルドのたびに[ホストの設定手順](Guides/host-setup.md)に従って署名済み VM バイナリを再登録してください。[ドキュメント一覧](README.md)から現在の手順と研究資料に進めます。詳しいガイドは現在英語です。
+6 -5
View File
@@ -28,11 +28,10 @@ cloudOS 26.4(`23E5207q`)와 함께 iPhone17,3 iOS 26.6.2(`23G90`), 27.0(`24A435`
## 설치와 빌드
배포된 `.app` 실행에는 Homebrew, Python, Xcode나 별도의 런타임 환경이 필요하지 않습니다. [GitHub Releases](https://github.com/Lakr233/vphone-cli/releases)에서 `vphone-cli-2.0.0.zip`을 다운로드하고 압축을 푼 뒤 앱 안의 CLI를 직접 실행하세요.
Xcode는 나중에 `vphone-workstation`이 통합할 `VPhone.bundle`을 만듭니다. 실행 시 Homebrew, Python, Xcode가 필요하지 않습니다. 번들 안의 CLI를 직접 실행할 수 있습니다.
```sh
ditto -x -k vphone-cli-2.0.0.zip .
./vphone-cli.app/Contents/MacOS/vphone-cli host preflight
.build/XcodeBundle/Build/Products/Debug/VPhone.bundle/Contents/MacOS/vphone-cli host preflight
```
위 예시의 `vphone-cli` 대신 이 앱 내부 경로를 사용할 수 있습니다. 소스 빌드에는 vphoned용 iPhoneOS SDK가 포함된 Xcode가 필요합니다.
@@ -40,8 +39,10 @@ ditto -x -k vphone-cli-2.0.0.zip .
```sh
git clone https://github.com/Lakr233/vphone-cli.git
cd vphone-cli
zsh Scripts/build.sh
.build/release/vphone-cli host preflight
xcodebuild -workspace VPhone.xcworkspace -scheme VPhone \
-configuration Debug -destination 'platform=macOS,arch=arm64' \
-derivedDataPath .build/XcodeBundle build
zsh Scripts/check_aux.sh
```
AMFI 허용 목록을 사용하는 호스트에서는 빌드할 때마다 [호스트 설정 가이드](Guides/host-setup.md)에 따라 서명된 VM 바이너리를 다시 허용하세요. 현재 가이드와 연구 자료는 [문서 목차](README.md)에 모았습니다. 상세 가이드는 현재 영어로 제공됩니다.
+7 -6
View File
@@ -28,20 +28,21 @@ vphone-cli vm launch myphone
## 安装与构建
成品 `.app` 运行时不需要 Homebrew、Python 或 Xcode,也不需要额外安装运行环境。从 [GitHub Releases](https://github.com/Lakr233/vphone-cli/releases) 下载 `vphone-cli-2.0.0.zip`,解压后直接调用应用内的 CLI:
Xcode 现在直接生成供后续 `vphone-workstation` 集成的 `VPhone.bundle`。运行时不需要 Homebrew、Python 或 Xcode;bundle 内没有 app 启动器或自动提权服务。可以直接调用其中的 CLI:
```sh
ditto -x -k vphone-cli-2.0.0.zip .
./vphone-cli.app/Contents/MacOS/vphone-cli host preflight
.build/XcodeBundle/Build/Products/Debug/VPhone.bundle/Contents/MacOS/vphone-cli host preflight
```
上文示例中的 `vphone-cli` 可替换为上述应用内路径。从源码构建则需要 Xcode 的 iPhoneOS SDK,以编译 vphoned:
上文示例中的 `vphone-cli` 可替换为上述 bundle 内路径。从源码构建需要 Xcode 的 iPhoneOS SDK,以编译 vphoned:
```sh
git clone https://github.com/Lakr233/vphone-cli.git
cd vphone-cli
zsh Scripts/build.sh
.build/release/vphone-cli host preflight
xcodebuild -workspace VPhone.xcworkspace -scheme VPhone \
-configuration Debug -destination 'platform=macOS,arch=arm64' \
-derivedDataPath .build/XcodeBundle build
zsh Scripts/check_aux.sh
```
采用 AMFI 白名单的宿主机,每次重编译后都要按[宿主机准备](Guides/host-setup.md)中的步骤重新允许签名后的 VM 程序。
+17 -19
View File
@@ -25,7 +25,7 @@ vphone-cli vm launch myphone
```
To expose the guest HTTP and WebSocket API on the host for local tools or an
app using `VPhoneAPIKit`, opt in when launching:
app using `VPhoneExternalAccessKit`, opt in when launching:
```sh
vphone-cli vm launch myphone --api-listen 127.0.0.1:8765
@@ -34,35 +34,33 @@ vphone-cli vm launch myphone --api-listen 127.0.0.1:8765
The guest runs `icli` commands through the API. See the [API design and usage](Research/vphoned_http_api.md)
for routes, WebSocket messages, and the Swift Kit client.
`vm create` prepares and patches firmware, restores the VM, installs the JB system changes and vphoned, then boots once to check a real vphoned ping. **It stops that verification boot before returning.** Run `vm launch` to keep using the VM. The create flow needs network access for Apple's restore ticket and asks for administrator authentication during CFW installation.
`vm create` prepares and patches firmware, restores the VM, installs the JB system changes and vphoned, then boots once to check a real vphoned ping. **It stops that verification boot before returning.** Run `vm launch` to keep using the VM. The create flow needs network access for Apple's restore ticket and requires the caller to provide root privileges for CFW installation.
For local validation, iPhone17,3 **26.6.2 (23G90)** and **27.0 (24A435)** both reached the lock screen and answered vphoned ping with cloudOS **26.4 (23E5207q)**. See [compatibility and evidence](Documents/Guides/compatibility.md); other firmware combinations are not implied by these results.
## Install or build
## Build the bundle
A distributed `.app` uses macOS system tools and its own bundled binaries; it does not need Homebrew, Python, or Xcode to run. Building from source does not install a separate runtime environment.
Download `vphone-cli-2.0.0.zip` from [GitHub Releases](https://github.com/Lakr233/vphone-cli/releases), extract it, and run the CLI inside the app:
```sh
ditto -x -k vphone-cli-2.0.0.zip .
./vphone-cli.app/Contents/MacOS/vphone-cli host preflight
```
Use that app path in place of `vphone-cli` in the examples above if the CLI is not on your `PATH`.
Building from source needs Xcode, including its iPhoneOS SDK for vphoned:
Xcode builds a self-contained `VPhone.bundle` for a future `vphone-workstation`
to download and load. The bundle contains no app launcher, installer, privileged
service or password prompt. Root access and installation belong to the
workstation. Building from source needs Xcode and its iPhoneOS SDK for vphoned:
```sh
git clone https://github.com/Lakr233/vphone-cli.git
cd vphone-cli
xcodebuild -workspace VPhone.xcworkspace -scheme vphone-app \
xcodebuild -workspace VPhone.xcworkspace -scheme VPhone \
-configuration Debug -destination 'platform=macOS,arch=arm64' \
-derivedDataPath .build/XcodeApp build
.build/XcodeApp/Build/Products/Debug/vphone-app.app/Contents/MacOS/vphone-cli --help
-derivedDataPath .build/XcodeBundle build
.build/XcodeBundle/Build/Products/Debug/VPhone.bundle/Contents/MacOS/vphone-cli --help
```
The Xcode app scheme builds the host tools, guest daemon, and guest components and bundles every binary under `Contents/MacOS`. Run the test schemes in their respective projects and `zsh Scripts/check_aux.sh` to inspect the app. After every rebuild, a host using the AMFI allowlist must allow the new signed VM binary because its cdhash changes. See [host setup](Documents/Guides/host-setup.md).
The `VPhone` scheme builds the host tools, guest daemon, and guest components;
every shipped binary is under `Contents/MacOS`. They use ad hoc code signatures,
with private virtualization entitlements only on `vphone-vm`. Run the test
schemes in their respective projects and `zsh Scripts/check_aux.sh` to inspect
the bundle. An AMFI allowlist must be updated whenever the VM binary's cdhash
changes. See [host setup](Documents/Guides/host-setup.md) and the
[bundle integration contract](Documents/Guides/bundle-integration.md).
## Everyday commands
+1 -3
View File
@@ -27,8 +27,7 @@ commands that read standard input. The process has a 120 second deadline.
## HTTP and WebSocket contract
`GET /openapi.json` is the machine-readable HTTP description. JSON resource
routes cover device state, apps, input, location, Developer Mode, low power
JSON resource routes cover device state, apps, input, location, Developer Mode, low power
mode, clipboard, file listing, and keychain. `GET/PUT
/v1/files/content?path=<absolute-guest-path>` transfer bytes with
`application/octet-stream`; upload writes to a temporary file in the same
@@ -83,7 +82,6 @@ proxy uses NIO channels and closes the paired channel when either side ends.
```sh
vphone-cli vm launch <name> --api-listen 127.0.0.1:8765
curl http://127.0.0.1:8765/v1/health
curl http://127.0.0.1:8765/openapi.json
```
```swift
+24 -10
View File
@@ -3,13 +3,13 @@
set -euo pipefail
root="${0:a:h:h}"
bundle="${1:-$root/.build/XcodeApp/Build/Products/Debug/vphone-app.app}"
bundle="${1:-$root/.build/XcodeBundle/Build/Products/Debug/VPhone.bundle}"
macos="$bundle/Contents/MacOS"
resources="$bundle/Contents/Resources"
[[ -d "$bundle" ]] || { print -u2 "Missing Xcode app: $bundle"; exit 1; }
[[ -d "$bundle" ]] || { print -u2 "Missing Xcode bundle: $bundle"; exit 1; }
for name in vphone-app vphone-vm vphone-cli VPhoneEscalator vphoned vphoned.signed \
for name in vphone-vm vphone-cli VPhoneEscalator vphoned vphoned.signed \
vpregister libswiftCompatibilitySpan.vphone.dylib libcamfix.dylib libvcamcaptured.dylib TweakLoader.dylib \
libAppleParavirtCompilerPluginIOGPUFamily.dylib; do
[[ -f "$macos/$name" ]] || { print -u2 "Missing binary: Contents/MacOS/$name"; exit 1; }
@@ -17,13 +17,17 @@ for name in vphone-app vphone-vm vphone-cli VPhoneEscalator vphoned vphoned.sign
print -u2 "Not a Mach-O: $name"
exit 1
}
/usr/bin/codesign --verify "$macos/$name" || {
print -u2 "Invalid signature: $name"
exit 1
}
done
for name in vphone-archive icli vphone-ask-for-permission vphone-amfi-allow; do
for name in vphone-app VPhoneAMFIAllow vphone-archive icli vphone-ask-for-permission; do
[[ ! -e "$macos/$name" ]] || { print -u2 "Obsolete binary: $name"; exit 1; }
done
for name in vphoned.plist entitlements.plist; do
for name in vphoned.plist VPhoneDaemon.entitlements; do
[[ -f "$resources/scripts/vphoned/$name" ]] || {
print -u2 "Missing guest configuration: $name"
exit 1
@@ -36,13 +40,23 @@ done
/usr/bin/codesign --verify "$macos/VPhoneEscalator"
/usr/bin/codesign --verify "$macos/vphoned.signed"
[[ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$bundle/Contents/Info.plist")" == "vphone-app" ]] || {
print -u2 "The unentitled app launcher is not the bundle executable"
[[ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundlePackageType' "$bundle/Contents/Info.plist")" == "BNDL" ]] || {
print -u2 "The product is not a generic bundle"
exit 1
}
app_entitlements="$(/usr/bin/codesign -d --entitlements - --xml "$bundle" 2>/dev/null || true)"
[[ "$app_entitlements" != *'com.apple.private.virtualization'* ]] || {
print -u2 "The app must not carry private virtualization entitlements"
if /usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$bundle/Contents/Info.plist" >/dev/null 2>&1; then
print -u2 "The container must not declare an executable"
exit 1
fi
while IFS= read -r file; do
if /usr/bin/file "$file" | /usr/bin/grep -q 'Mach-O' && [[ "$file" != "$macos/"* ]]; then
print -u2 "Mach-O outside Contents/MacOS: $file"
exit 1
fi
done < <(/usr/bin/find "$bundle/Contents" -type f)
bundle_entitlements="$(/usr/bin/codesign -d --entitlements - --xml "$bundle" 2>/dev/null || true)"
[[ "$bundle_entitlements" != *'com.apple.private.virtualization'* ]] || {
print -u2 "The bundle must not carry private virtualization entitlements"
exit 1
}
vm_entitlements="$(/usr/bin/codesign -d --entitlements - --xml "$macos/vphone-vm" 2>/dev/null)"
@@ -105,10 +105,6 @@ final class GuestHyperTextHandler: ChannelInboundHandler, RemovableChannelHandle
private func handle(_ head: HTTPRequestHead, body: Data, channel: Channel) {
let path = head.uri.split(separator: "?", maxSplits: 1).first.map(String.init) ?? head.uri
if head.method == .GET, path == "/openapi.json" {
Self.send(APIReply(status: 200, data: OpenAPIDocument.data), on: channel)
return
}
if head.method == .GET, path == "/v1/health" {
Self.send(.json(GuestAPI.health()), on: channel)
return
-79
View File
@@ -1,79 +0,0 @@
import Foundation
enum OpenAPIDocument {
static let data = Data(spec.utf8)
private static let spec = #"""
{
"openapi": "3.1.0",
"info": {
"title": "vphoned API",
"version": "1.0.0",
"description": "Guest API over VSOCK 1339. vphone-vm can forward it to an opt-in host HTTP listener. JSON operations are also available as WebSocket requests on /v1/events."
},
"servers": [{"url": "/"}],
"paths": {
"/v1/health": {"get": {"operationId": "health", "responses": {"200": {"description": "Daemon is running"}}}},
"/v1/device": {"get": {"operationId": "device.snapshot", "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/device/screen": {"get": {"operationId": "device.screen", "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/apps": {"get": {"operationId": "apps.list", "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/apps/launch": {"post": {"operationId": "apps.launch", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/apps/terminate": {"post": {"operationId": "apps.terminate", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/apps/foreground": {"get": {"operationId": "apps.foreground", "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/apps/open-url": {"post": {"operationId": "apps.open_url", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/apps/install": {"post": {"operationId": "apps.install", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/input/touch": {"post": {"operationId": "input.touch", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/input/hid": {"post": {"operationId": "input.hid", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/location": {
"get": {"operationId": "location.current", "responses": {"200": {"$ref": "#/components/responses/Result"}}},
"put": {"operationId": "location.set", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}},
"delete": {"operationId": "location.clear", "responses": {"200": {"$ref": "#/components/responses/Result"}}}
},
"/v1/developer-mode": {"get": {"operationId": "developer_mode.status", "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/developer-mode/enable": {"post": {"operationId": "developer_mode.enable", "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/low-power-mode": {
"get": {"operationId": "power.low_power_mode", "responses": {"200": {"$ref": "#/components/responses/Result"}}},
"put": {"operationId": "power.low_power_mode", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}
},
"/v1/clipboard": {
"get": {"operationId": "clipboard.get", "responses": {"200": {"$ref": "#/components/responses/Result"}}},
"put": {"operationId": "clipboard.set", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}
},
"/v1/clipboard/image": {
"get": {"operationId": "clipboard.image.download", "responses": {"200": {"description": "PNG image"}}},
"put": {"operationId": "clipboard.image.upload", "requestBody": {"required": true, "content": {"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}}}, "responses": {"200": {"description": "Clipboard image set"}}}
},
"/v1/files": {"get": {"operationId": "files.list", "parameters": [{"name": "path", "in": "query", "required": true, "schema": {"type": "string"}}], "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/files/content": {
"get": {"operationId": "files.download", "parameters": [{"$ref": "#/components/parameters/FilePath"}], "responses": {"200": {"description": "File bytes", "content": {"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}}}}},
"put": {"operationId": "files.upload", "parameters": [{"$ref": "#/components/parameters/FilePath"}, {"name": "mode", "in": "query", "schema": {"type": "string", "default": "644", "pattern": "^0?[0-7]{1,3}$"}}], "requestBody": {"required": true, "content": {"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}}}, "responses": {"200": {"description": "File stored"}}}
},
"/v1/files/mkdir": {"post": {"operationId": "files.mkdir", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/files/remove": {"post": {"operationId": "files.remove", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/files/rename": {"post": {"operationId": "files.rename", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/settings/get": {"post": {"operationId": "settings.get", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/settings/set": {"post": {"operationId": "settings.set", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/keychain": {
"get": {"operationId": "keychain.list", "responses": {"200": {"$ref": "#/components/responses/Result"}}},
"post": {"operationId": "keychain.add", "requestBody": {"$ref": "#/components/requestBodies/Parameters"}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}
},
"/v1/rpc": {"post": {"operationId": "rpc", "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Request"}}}}, "responses": {"200": {"$ref": "#/components/responses/Result"}}}},
"/v1/events": {"get": {"operationId": "websocketEvents", "description": "Upgrade to WebSocket. Send Request objects and receive Response or Event objects.", "responses": {"101": {"description": "WebSocket upgrade"}, "426": {"description": "Upgrade required"}}}}
},
"components": {
"parameters": {"FilePath": {"name": "path", "in": "query", "required": true, "schema": {"type": "string", "description": "Absolute guest path"}}},
"schemas": {
"Request": {"type": "object", "required": ["method"], "properties": {"id": {"oneOf": [{"type": "string"}, {"type": "integer"}]}, "method": {"type": "string"}, "params": {"type": "object", "additionalProperties": true}}},
"Response": {"type": "object", "required": ["type", "id"], "properties": {"type": {"const": "response"}, "id": {}, "result": {"type": "object", "additionalProperties": true}, "error": {"$ref": "#/components/schemas/Error"}}},
"Event": {"type": "object", "required": ["type", "event", "data"], "properties": {"type": {"const": "event"}, "event": {"type": "string"}, "data": {"type": "object", "additionalProperties": true}}},
"Error": {"type": "object", "required": ["code", "message"], "properties": {"code": {"type": "string"}, "message": {"type": "string"}}}
},
"requestBodies": {
"Parameters": {"content": {"application/json": {"schema": {"type": "object", "additionalProperties": true}}}}
},
"responses": {"Result": {"description": "A Response envelope with result or error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Response"}}}}}
},
"x-websocket": {"path": "/v1/events", "request": {"$ref": "#/components/schemas/Request"}, "messages": [{"$ref": "#/components/schemas/Response"}, {"$ref": "#/components/schemas/Event"}]}
}
"""#
}
@@ -32,11 +32,31 @@
C10000000000000000000003 /* vpregister */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = vpregister; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
C10000000000000000000035 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
vpregister.m,
);
target = C10000000000000000000081 /* VphonedNative */;
};
C10000000000000000000036 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
unarchive.m,
vphoned_install.m,
vphoned_keychain.m,
vphoned_native.m,
vphoned_vcam.m,
);
target = C10000000000000000000083 /* vpregister */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
C10000000000000000000031 /* Native */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Native; sourceTree = "<group>"; };
C10000000000000000000031 /* Native */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C10000000000000000000035 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, C10000000000000000000036 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Native; sourceTree = "<group>"; };
C10000000000000000000032 /* Daemon */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Daemon; sourceTree = "<group>"; };
C10000000000000000000033 /* Configuration */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Configuration; sourceTree = "<group>"; };
C10000000000000000000034 /* VPRegister */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VPRegister; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -77,7 +97,6 @@
C10000000000000000000031 /* Native */,
C10000000000000000000032 /* Daemon */,
C10000000000000000000033 /* Configuration */,
C10000000000000000000034 /* VPRegister */,
C10000000000000000000052 /* Products */,
);
sourceTree = "<group>";
@@ -154,7 +173,7 @@
dependencies = (
);
fileSystemSynchronizedGroups = (
C10000000000000000000034 /* VPRegister */,
C10000000000000000000031 /* Native */,
);
name = vpregister;
productName = vpregister;
@@ -81,7 +81,7 @@ extension CryptexFilesystemPatcher {
try VPhoneSigner.sign(
fileAt: targetBin,
options: guestSigningOptions(
entitlements: vphonedSrc.appendingPathComponent("entitlements.plist"),
entitlements: vphonedSrc.appendingPathComponent("VPhoneDaemon.entitlements"),
),
)
try setMode(0o755, at: targetBin)
@@ -24,7 +24,7 @@
BB0000000000000000000106 /* AppleMobileDeviceLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = BB0000000000000000000206 /* AppleMobileDeviceLibrary */; };
BB0000000000000000000107 /* libFirmwarePatcher.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000033 /* libFirmwarePatcher.a */; };
BB0000000000000000000108 /* libVPhoneSign.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000032 /* libVPhoneSign.a */; };
BB0000000000000000000109 /* ArgumentParser in FirmwarePatcher */ = {isa = PBXBuildFile; productRef = BB0000000000000000000204 /* ArgumentParser */; };
BB0000000000000000000109 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = BB0000000000000000000204 /* ArgumentParser */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -67,7 +67,7 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
BB0000000000000000000021 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = "<group>"; };
BB0000000000000000000021 /* VPhoneCommand */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VPhoneCommand; sourceTree = "<group>"; };
BB0000000000000000000022 /* VPhoneSign */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VPhoneSign; sourceTree = "<group>"; };
BB0000000000000000000023 /* FirmwarePatcher */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = FirmwarePatcher; sourceTree = "<group>"; };
BB0000000000000000000024 /* FirmwarePatcherTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = FirmwarePatcherTests; sourceTree = "<group>"; };
@@ -101,7 +101,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
BB0000000000000000000109 /* ArgumentParser in FirmwarePatcher */,
BB0000000000000000000109 /* ArgumentParser in Frameworks */,
BB0000000000000000000101 /* Capstone in Frameworks */,
BB0000000000000000000102 /* Img4tool in Frameworks */,
BB0000000000000000000103 /* MachOKit in Frameworks */,
@@ -136,7 +136,7 @@
BB0000000000000000000002 = {
isa = PBXGroup;
children = (
BB0000000000000000000021 /* Sources */,
BB0000000000000000000021 /* VPhoneCommand */,
BB0000000000000000000022 /* VPhoneSign */,
BB0000000000000000000023 /* FirmwarePatcher */,
BB0000000000000000000024 /* FirmwarePatcherTests */,
@@ -176,7 +176,7 @@
BB0000000000000000000123 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
BB0000000000000000000021 /* Sources */,
BB0000000000000000000021 /* VPhoneCommand */,
);
name = VPhoneCommand;
packageProductDependencies = (
@@ -89,6 +89,9 @@ struct Extract: ParsableCommand {
options: options,
progress: verbose ? { print($0.currentPath) } : nil,
)
if !preservePermissions {
try VPhoneHostFilePermissions.makeAccessible(at: destination)
}
if !verbose {
print("extracted \(written) entries to \(destination.path)")
}
@@ -167,6 +170,7 @@ struct Create: ParsableCommand {
excluding: exclude,
progress: verbose ? { print($0.currentPath) } : nil,
)
try VPhoneHostFilePermissions.makeAccessible(at: file)
if !verbose {
print("packed \(written) entries into \(file.path)")
}
@@ -201,6 +205,7 @@ struct Decompress: ParsableCommand {
func run() throws {
try VPhoneArchiveWriter.decompress(file, to: output)
try VPhoneHostFilePermissions.makeAccessible(at: output)
print("decompressed \(file.lastPathComponent) → \(output.path)")
}
}
@@ -44,6 +44,8 @@ struct VPhoneCustomFirmwareInstaller {
do { try invokingUser.restoreOwnership(at: bundle) }
catch { fputs("warning: could not restore VM ownership: \(error)\n", stderr) }
}
do { try VPhoneHostFilePermissions.makeAccessible(at: bundle) }
catch { fputs("warning: could not set VM file permissions: \(error)\n", stderr) }
}
let diskImage = bundle.appendingPathComponent("Disk.img")
guard fm.fileExists(atPath: diskImage.path) else {
@@ -256,7 +258,7 @@ struct VPhoneCustomFirmwareInstaller {
let vphoned = try VPhoneGuestBinaries.resolve("vphoned")
let staged = work.appendingPathComponent("vphoned")
try fm.copyItem(at: vphoned, to: staged)
let entitlementsURL = resources.scriptsDir.appendingPathComponent("vphoned/entitlements.plist")
let entitlementsURL = resources.scriptsDir.appendingPathComponent("vphoned/VPhoneDaemon.entitlements")
let entitlements = try Data(contentsOf: entitlementsURL, options: .mappedIfSafe)
try VPhoneSigner.sign(fileAt: staged,
options: .init(entitlements: entitlements, mergesExisting: true))
@@ -262,6 +262,10 @@ struct VPhoneFirmwarePrepareCommand: ParsableCommand {
}
let name = try VPhoneVirtualMachineSelection.resolveExisting(name, in: lib.library)
let bundle = try lib.library.bundle(named: name)
defer {
try? VPhoneHostFilePermissions.makeAccessible(at: bundle.url)
try? VPhoneHostFilePermissions.makeAccessible(at: resources.ipswCacheDir)
}
try VPhoneFirmwarePreparer.prepare(
iPhoneSource: phone,
cloudOSSource: cloud,
@@ -269,6 +273,8 @@ struct VPhoneFirmwarePrepareCommand: ParsableCommand {
bundle: bundle,
resources: resources,
)
try VPhoneHostFilePermissions.makeAccessible(at: bundle.url)
try VPhoneHostFilePermissions.makeAccessible(at: resources.ipswCacheDir)
}
}
@@ -290,6 +296,10 @@ struct VPhoneFirmwarePatchCommand: ParsableCommand {
func run() throws {
let name = try VPhoneVirtualMachineSelection.resolveExisting(name, in: lib.library)
let bundle = try lib.library.bundle(named: name)
defer {
try? VPhoneHostFilePermissions.makeAccessible(at: bundle.url)
try? VPhoneHostFilePermissions.makeAccessible(at: VPhoneResources.resolve().sealVolumeCacheDir)
}
// In-process pipeline (no subprocess) — CryptexFilesystemPatcher's
// apfs_sealvolume read honors VPHONE_SEAL_DIR from *this* process's
@@ -307,6 +317,8 @@ struct VPhoneFirmwarePatchCommand: ParsableCommand {
enableFrida: frida,
)
let records = try pipeline.patchAll()
try VPhoneHostFilePermissions.makeAccessible(at: bundle.url)
try VPhoneHostFilePermissions.makeAccessible(at: resources.sealVolumeCacheDir)
print("[fw patch] applied \(records.count) JB patches")
}
}
@@ -87,6 +87,8 @@ struct VPhoneFirmwareSealToolCommand: ParsableCommand {
// Lifted out of someone else's signed image; re-seal it so the kernel
// will exec it here.
try Self.run("/usr/bin/codesign", ["--force", "--sign", "-", destination.path])
try VPhoneHostFilePermissions.makeAccessible(at: destination)
try VPhoneHostFilePermissions.makeDirectoryAccessible(at: output)
print(" Downloaded: \(destination.path)")
}
@@ -154,7 +156,7 @@ struct VPhoneFirmwareSealToolCommand: ParsableCommand {
let entities = plist["system-entities"] as? [[String: Any]],
let mount = entities.compactMap({ $0["mount-point"] as? String }).first
else { throw VPhoneRemoteZip.Error.malformed("hdiutil attached nothing with a mount point") }
defer { try? run("/usr/bin/hdiutil", ["detach", mount]) }
defer { _ = try? run("/usr/bin/hdiutil", ["detach", mount]) }
let source = URL(fileURLWithPath: mount).appending(
path: "System/Library/Filesystems/apfs.fs/Contents/Resources/apfs_sealvolume",
@@ -56,6 +56,10 @@ struct VPhoneRestoreCommand: ParsableCommand {
let v = max(VPhoneVerbosity.info, VPhoneVerbosity(count: verboseCount))
let name = try VPhoneVirtualMachineSelection.resolveExisting(name, in: lib.library)
let bundle = try lib.library.bundle(named: name)
defer {
do { try VPhoneHostFilePermissions.makeAccessible(at: bundle.url) }
catch { fputs("warning: could not set VM file permissions: \(error)\n", stderr) }
}
guard let ecidText = VPhoneRestoreOperations.resolveECID(explicit: ecid, bundle: bundle) else {
throw VPhoneRestoreError.ecidUnresolved
}
@@ -172,12 +172,15 @@ struct PatchComponentCommand: ParsableCommand {
let outputDir = output.deletingLastPathComponent()
try FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true)
try patchedData.write(to: output)
try VPhoneHostFilePermissions.makeAccessible(at: output)
try VPhoneHostFilePermissions.makeDirectoryAccessible(at: outputDir)
if let recordsOut {
let url = URL(fileURLWithPath: recordsOut)
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
try encoder.encode(records).write(to: url)
try VPhoneHostFilePermissions.makeAccessible(at: url)
if !quiet {
print("[patch-component] wrote \(records.count) patch records to \(url.path)")
}
@@ -78,6 +78,7 @@ struct VPhoneVirtualMachineWriteManifestCommand: ParsableCommand {
)
}
try manifest.write(to: configURL)
try VPhoneHostFilePermissions.makeAccessible(at: configURL)
print("Created VM manifest: \(configURL.path)")
}
}
@@ -73,9 +73,7 @@ public struct VPhoneVirtualMachineCreator {
private let library: VPhoneLibrary
private let resources: VPhoneResources
/// How to start the guest. A create boots in DFU and once for verification,
/// so the decision about whether an AMFI window
/// is needed is taken once, here, rather than probing amfid (and possibly
/// prompting for sudo) before each one.
/// so the AMFI probe runs once before the multi-stage create pipeline.
private let launcher: VPhoneGuestLaunchPlanner
public init(
@@ -103,6 +101,7 @@ public struct VPhoneVirtualMachineCreator {
let bundleURL = library.url(forName: options.name)
let ownedOutputs = [bundleURL, resources.ipswCacheDir, resources.sealVolumeCacheDir]
var ownershipRestored = false
var permissionsRestored = false
defer {
if let invokingUser, !ownershipRestored {
for output in ownedOutputs {
@@ -112,6 +111,14 @@ public struct VPhoneVirtualMachineCreator {
try? invokingUser.restoreOwnerOfDirectory(at: library.root)
try? invokingUser.restoreOwnerOfDirectory(at: VPhoneResources.userDataRoot())
}
if !permissionsRestored {
for output in ownedOutputs {
do { try VPhoneHostFilePermissions.makeAccessible(at: output) }
catch { fputs("warning: could not set permissions on \(output.path): \(error)\n", stderr) }
}
try? VPhoneHostFilePermissions.makeDirectoryAccessible(at: library.root)
try? VPhoneHostFilePermissions.makeDirectoryAccessible(at: VPhoneResources.userDataRoot())
}
}
if FileManager.default.fileExists(atPath: bundleURL.path) {
throw VPhoneLibraryError.alreadyExists(name: options.name)
@@ -163,6 +170,10 @@ public struct VPhoneVirtualMachineCreator {
try invokingUser.restoreOwnerOfDirectory(at: VPhoneResources.userDataRoot())
}
ownershipRestored = true
for output in ownedOutputs { try VPhoneHostFilePermissions.makeAccessible(at: output) }
try VPhoneHostFilePermissions.makeDirectoryAccessible(at: library.root)
try VPhoneHostFilePermissions.makeDirectoryAccessible(at: VPhoneResources.userDataRoot())
permissionsRestored = true
print("\n=== Done ===")
print("JB VM created; vphoned connected. Guest user environment is untouched.")
}
@@ -170,7 +181,7 @@ public struct VPhoneVirtualMachineCreator {
// MARK: - trace
/// Internal spawn/outcome trace, gated on `.trace` (`-vvv`). Never prints
/// secret env VALUES (e.g. SUDO_PASSWORD) — callers pass only key names.
/// secret environment values — callers pass only key names.
private func trace(_ msg: String, _ v: VPhoneVerbosity) {
guard v.tracesInternals else { return }
print("[trace] \(msg)")
@@ -37,6 +37,10 @@ struct VPhoneVirtualMachineLaunchCommand: ParsableCommand {
}
let name = try VPhoneVirtualMachineSelection.resolveExisting(name, in: lib.library)
let bundle = try lib.library.bundle(named: name)
defer {
do { try VPhoneHostFilePermissions.makeAccessible(at: bundle.url) }
catch { fputs("warning: could not set VM file permissions: \(error)\n", stderr) }
}
let resources = projectRoot.map { VPhoneResources(base: URL(fileURLWithPath: $0)) } ?? .resolve()
let layout = VPhoneLaunchLayout(resources: resources)
@@ -47,6 +47,7 @@ struct VPhoneVirtualMachineExportCommand: ParsableCommand {
in: lib.library,
progress: { done, total in bar.update(done: done, total: total) },
)
try VPhoneHostFilePermissions.makeAccessible(at: outURL)
bar.finish()
print("exported \(name) → \(outURL.path)")
}
@@ -70,6 +71,9 @@ struct VPhoneVirtualMachineImportCommand: ParsableCommand {
in: lib.library,
progress: { done, total in bar.update(done: done, total: total) },
)
try VPhoneHostFilePermissions.makeAccessible(at: bundle.url)
try VPhoneHostFilePermissions.makeDirectoryAccessible(at: lib.library.root)
try VPhoneHostFilePermissions.makeDirectoryAccessible(at: VPhoneResources.userDataRoot())
bar.finish()
print("imported → \(bundle.name)")
}
@@ -47,7 +47,7 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
CC0000000000000000000021 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = "<group>"; };
CC0000000000000000000021 /* VPhoneRestore */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VPhoneRestore; sourceTree = "<group>"; };
CC0000000000000000000022 /* MobileRecoveryCore */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MobileRecoveryCore; sourceTree = "<group>"; };
CC0000000000000000000023 /* MobileRestoreCore */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MobileRestoreCore; sourceTree = "<group>"; };
CC0000000000000000000024 /* VPhoneRestoreTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VPhoneRestoreTests; sourceTree = "<group>"; };
@@ -94,7 +94,7 @@
CC0000000000000000000002 = {
isa = PBXGroup;
children = (
CC0000000000000000000021 /* Sources */,
CC0000000000000000000021 /* VPhoneRestore */,
CC0000000000000000000022 /* MobileRecoveryCore */,
CC0000000000000000000023 /* MobileRestoreCore */,
CC0000000000000000000024 /* VPhoneRestoreTests */,
@@ -130,7 +130,7 @@
CC0000000000000000000123 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
CC0000000000000000000021 /* Sources */,
CC0000000000000000000021 /* VPhoneRestore */,
);
name = VPhoneRestore;
productName = VPhoneRestore;
@@ -8,13 +8,13 @@
EE0000000000000000000031 /* VPhoneEscalator */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = VPhoneEscalator; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
EE0000000000000000000021 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = "<group>"; };
EE0000000000000000000021 /* VPhoneEscalator */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VPhoneEscalator; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
EE0000000000000000000041 /* Frameworks */ = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (); runOnlyForDeploymentPostprocessing = 0; };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
EE0000000000000000000002 = {isa = PBXGroup; children = (EE0000000000000000000021 /* Sources */, EE0000000000000000000003 /* Products */, ); sourceTree = "<group>"; };
EE0000000000000000000002 = {isa = PBXGroup; children = (EE0000000000000000000021 /* VPhoneEscalator */, EE0000000000000000000003 /* Products */, ); sourceTree = "<group>"; };
EE0000000000000000000003 /* Products */ = {isa = PBXGroup; children = (EE0000000000000000000031 /* VPhoneEscalator */, ); name = Products; sourceTree = "<group>"; };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -24,7 +24,7 @@
buildPhases = (EE0000000000000000000051 /* Sources */, EE0000000000000000000041 /* Frameworks */, );
buildRules = ();
dependencies = ();
fileSystemSynchronizedGroups = (EE0000000000000000000021 /* Sources */, );
fileSystemSynchronizedGroups = (EE0000000000000000000021 /* VPhoneEscalator */, );
name = VPhoneEscalator;
productName = VPhoneEscalator;
productReference = EE0000000000000000000031 /* VPhoneEscalator */;
@@ -1,11 +1,8 @@
// vphone-amfi-allow — let an ad-hoc signed binary carry Apple-private
// VPhoneEscalator — let an ad-hoc signed binary carry Apple-private
// entitlements, without writing a single byte into anyone's __TEXT.
//
// This is the project's copy of github.com/Lakr233/amfi-allow (MIT), which is
// one C file and no dependencies, so it is carried here rather than pulled in:
// it cannot be a SwiftPM dependency, because `Scripts/build.sh` has to build it
// for arm64e and SwiftPM only emits arm64. Keep it in step with upstream; the
// only local changes are this header and the usage text.
// This is the project's copy of github.com/Lakr233/amfi-allow (MIT), built
// by its own Xcode target for arm64e. Keep it in step with upstream.
//
// It exists because vphone-vm is the one binary here that holds
// com.apple.private.* entitlements, and amfid refuses it. The bundled helper
@@ -13,11 +10,8 @@
// cdhashes change every time they are signed, so that is a per-build step, not
// a once-per-machine one.
//
// Build (no Xcode.app, no Python, no LLDB):
//
// clang -arch arm64e -O2 -framework CoreFoundation -framework Security \
// -o vphone-amfi-allow vphone-amfi-allow.c
// codesign --force --sign - vphone-amfi-allow
// Build with the VPhoneEscalator Xcode target (arm64e, then ad-hoc signed
// during bundle staging). No Python or LLDB is needed.
//
// ---------------------------------------------------------------------------
// Why this exists
@@ -4,11 +4,11 @@ set -euo pipefail
root="$(cd "${0:a:h}/../../.." && pwd)"
configuration="${CONFIGURATION:?}"
app="${TARGET_BUILD_DIR:?}/${FULL_PRODUCT_NAME:?}"
macos="$app/Contents/MacOS"
resources="$app/Contents/Resources"
bundle="${TARGET_BUILD_DIR:?}/${FULL_PRODUCT_NAME:?}"
macos="$bundle/Contents/MacOS"
resources="$bundle/Contents/Resources"
# Xcode exports the app target's SDK and package paths to build phases. Nested
# Xcode exports the bundle target's SDK and package paths to build phases. Nested
# xcodebuild must resolve each project's own graph, especially the iOS daemon.
build_project() {
/usr/bin/env -i HOME="$HOME" TMPDIR="${TMPDIR:-/tmp}" \
@@ -38,35 +38,34 @@ build_project -project "$root/VPhoneDaemon/VPhoneDaemon.xcodeproj" \
build_project -project "$root/VPhoneExecutable/VPhoneEscalator/VPhoneEscalator.xcodeproj" \
-scheme VPhoneEscalator -configuration "$configuration" \
-destination 'platform=macOS,arch=arm64e' \
-derivedDataPath "$root/.build/XcodeEscalator" CODE_SIGNING_ALLOWED=NO build
-derivedDataPath "$root/.build/XcodeAMFIAllow" CODE_SIGNING_ALLOWED=NO build
/usr/bin/make -C "$root/VPhoneGuestComponents" OUT="$root/.build/guest-components" all
command_products="$root/.build/XcodeCommand/Build/Products/$configuration"
daemon_products="$root/.build/XcodeDaemon/Build/Products/$configuration-iphoneos"
escalator_products="$root/.build/XcodeEscalator/Build/Products/$configuration"
amfi_products="$root/.build/XcodeAMFIAllow/Build/Products/$configuration"
guest_products="$root/.build/guest-components/stage"
/bin/rm -rf "$macos" "$resources"
/bin/mkdir -p "$macos" "$resources/scripts/vphoned" "$resources/guest"
/bin/cp "$TARGET_BUILD_DIR/vphone-vm" "$macos/vphone-vm"
/bin/cp "$command_products/vphone-cli" "$macos/vphone-cli"
/bin/cp "$daemon_products/vphoned" "$macos/vphoned"
/bin/cp "$daemon_products/vphoned" "$macos/vphoned.signed"
/bin/cp "$daemon_products/vpregister" "$macos/vpregister"
/bin/cp "$escalator_products/VPhoneEscalator" "$macos/VPhoneEscalator"
/bin/cp "$amfi_products/VPhoneEscalator" "$macos/VPhoneEscalator"
/bin/cp "$guest_products/camfix/libcamfix.dylib" "$macos/libcamfix.dylib"
/bin/cp "$guest_products/vcamcaptured/libvcamcaptured.dylib" "$macos/libvcamcaptured.dylib"
/bin/cp "$guest_products/tweakloader/TweakLoader.dylib" "$macos/TweakLoader.dylib"
/bin/cp "$guest_products/gpu/libAppleParavirtCompilerPluginIOGPUFamily.dylib" "$macos/libAppleParavirtCompilerPluginIOGPUFamily.dylib"
/bin/cp "$root/VPhoneDaemon/Configuration/vphoned.plist" "$resources/scripts/vphoned/vphoned.plist"
/bin/cp "$root/VPhoneDaemon/Configuration/entitlements.plist" "$resources/scripts/vphoned/entitlements.plist"
/bin/cp "$root/VPhoneDaemon/Configuration/VPhoneDaemon.entitlements" "$resources/scripts/vphoned/VPhoneDaemon.entitlements"
/bin/cp "$guest_products/camfix/libcamfix.plist" "$resources/guest/libcamfix.plist"
/bin/cp "$guest_products/vcamcaptured/libvcamcaptured.plist" "$resources/guest/libvcamcaptured.plist"
/bin/cp "$root/VPhoneExecutable/VPhoneVirtualization/Resources/AppIcon.icns" "$resources/AppIcon.icns"
# vphone-vm needs Swift's span back-deployment library on macOS 15. Xcode's
# app-only Swift library pass prunes the standard library name because it scans
# only the unentitled app executable. Use a private load name for the VM child.
# generic bundle has no main executable. Use a private load name for the VM child.
compatibility_library="$(/usr/bin/xcrun swift-stdlib-tool --print \
--scan-executable "$macos/vphone-vm" --platform macosx | \
/usr/bin/grep '/libswiftCompatibilitySpan.dylib$')"
@@ -75,13 +74,13 @@ compatibility_library="$(/usr/bin/xcrun swift-stdlib-tool --print \
@loader_path/libswiftCompatibilitySpan.vphone.dylib "$macos/vphone-vm"
/bin/rm -f "$macos/libswiftCompatibilitySpan.dylib" \
"$macos/libswiftCompatibilitySpan.dylib.original"
/bin/rm -f "$app/Contents/Frameworks/libswiftCompatibilitySpan.dylib"
/bin/rm -f "$bundle/Contents/Frameworks/libswiftCompatibilitySpan.dylib"
/usr/bin/codesign --force --sign - "$macos/vphone-cli"
/usr/bin/codesign --force --sign - --entitlements "$root/VPhoneDaemon/Configuration/entitlements.plist" "$macos/vphoned"
/usr/bin/codesign --force --sign - --entitlements "$root/VPhoneDaemon/Configuration/entitlements.plist" "$macos/vphoned.signed"
/usr/bin/codesign --force --sign - --entitlements "$root/VPhoneDaemon/Configuration/VPhoneDaemon.entitlements" "$macos/vphoned"
/usr/bin/codesign --force --sign - --entitlements "$root/VPhoneDaemon/Configuration/VPhoneDaemon.entitlements" "$macos/vphoned.signed"
/usr/bin/codesign --force --sign - "$macos/vpregister"
/usr/bin/codesign --force --sign - "$macos/VPhoneEscalator"
/usr/bin/codesign --force --sign - "$macos/libswiftCompatibilitySpan.vphone.dylib"
/usr/bin/codesign --force --sign - --entitlements "$root/VPhoneExecutable/VPhoneVirtualization/Resources/vphone.entitlements" "$macos/vphone-vm"
/usr/bin/codesign --force --sign - "$app"
/usr/bin/codesign --force --sign - --entitlements "$root/VPhoneExecutable/VPhoneVirtualization/Resources/VPhoneVirtualization.entitlements" "$macos/vphone-vm"
/usr/bin/codesign --force --sign - "$bundle"
@@ -3,22 +3,15 @@
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.vphone.app</string>
<!-- The unentitled app launches the separately signed vphone-vm process. -->
<key>CFBundleExecutable</key>
<string>vphone-app</string>
<string>com.vphone.bundle</string>
<key>CFBundleName</key>
<string>VPhone</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>2</string>
<key>CFBundleShortVersionString</key>
<string>2.0.0</string>
<key>LSUIElement</key>
<true/>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>NSLocationUsageDescription</key>
<string>Shares your location with the virtual iPhone so apps running inside it see the same location.</string>
<key>NSLocationWhenInUseUsageDescription</key>
@@ -1,46 +0,0 @@
import AppKit
import Foundation
let app = NSApplication.shared
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps: true)
let arguments: [String]
if CommandLine.arguments.count > 1 {
arguments = Array(CommandLine.arguments.dropFirst())
} else {
let picker = NSOpenPanel()
picker.title = "Choose a virtual iPhone configuration"
picker.prompt = "Launch"
picker.allowedContentTypes = [.propertyList]
picker.allowsOtherFileTypes = false
picker.canChooseDirectories = false
guard picker.runModal() == .OK, let config = picker.url else {
exit(EXIT_SUCCESS)
}
arguments = ["--config", config.path]
}
guard let executableDirectory = Bundle.main.executableURL?.deletingLastPathComponent() else {
fatalError("The application executable has no bundle path")
}
let virtualMachineExecutable = executableDirectory.appendingPathComponent("vphone-vm")
let process = Process()
process.executableURL = virtualMachineExecutable
process.arguments = arguments
do {
try process.run()
process.waitUntilExit()
if process.terminationStatus != 0 {
let alert = NSAlert()
alert.messageText = "The virtual iPhone could not start"
alert.informativeText = "The vphone-vm process exited with status \(process.terminationStatus). Run vphone-cli vm launch in Terminal for the full error."
alert.runModal()
}
} catch {
let alert = NSAlert()
alert.messageText = "The virtual iPhone could not start"
alert.informativeText = error.localizedDescription
alert.runModal()
}
@@ -101,7 +101,6 @@ class VPhoneVirtualMachineAppDelegate: NSObject, NSApplicationDelegate {
let url = try await proxy.start()
apiProxy = proxy
print("[api] HTTP/WebSocket API: \(url.absoluteString)")
print("[api] OpenAPI: \(url.appending(path: "openapi.json").absoluteString)")
}
} else if command.apiListen != nil {
throw VPhoneVirtualMachineError.apiSocketUnavailable
@@ -71,6 +71,7 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
sepStorage: manifest.sepStorage,
)
try manifest.write(to: options.configURL)
try VPhoneHostFilePermissions.makeAccessible(at: options.configURL)
print("[vphone] \(reason)")
}
@@ -88,6 +89,7 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
)
do {
try Self.writeUDIDPrediction(identity: identity, to: outputURL)
try VPhoneHostFilePermissions.makeAccessible(at: outputURL)
print("[vphone] Wrote UDID prediction: \(outputURL.path)")
} catch {
print("[vphone] Warning: failed to write udid-prediction.txt: \(error)")
@@ -102,6 +104,7 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
hardwareModel: hwModel,
options: .allowOverwrite,
)
try VPhoneHostFilePermissions.makeAccessible(at: options.nvramURL)
platform.auxiliaryStorage = auxStorage
platform.hardwareModel = hwModel
@@ -12,7 +12,7 @@
DD0000000000000000000103 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = DD0000000000000000000203 /* NIOCore */; };
DD0000000000000000000104 /* NIOPosix in Frameworks */ = {isa = PBXBuildFile; productRef = DD0000000000000000000204 /* NIOPosix */; };
DD0000000000000000000105 /* libVPhoneVirtualMachineKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DD0000000000000000000032 /* libVPhoneVirtualMachineKit.a */; };
DD0000000000000000000106 /* ArgumentParser in VPhoneVirtualMachineKit */ = {isa = PBXBuildFile; productRef = DD0000000000000000000201 /* ArgumentParser */; };
DD0000000000000000000106 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = DD0000000000000000000201 /* ArgumentParser */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -33,13 +33,12 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
DD0000000000000000000031 /* vphone-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "vphone-app.app"; sourceTree = BUILT_PRODUCTS_DIR; };
DD0000000000000000000031 /* VPhone.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VPhone.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
DD0000000000000000000032 /* libVPhoneVirtualMachineKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libVPhoneVirtualMachineKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
DD0000000000000000000033 /* vphone-vm */ = {isa = PBXFileReference; explicitFileType = compiled.mach-o.executable; includeInIndex = 0; path = "vphone-vm"; sourceTree = BUILT_PRODUCTS_DIR; };
DD0000000000000000000033 /* vphone-vm */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "vphone-vm"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
DD0000000000000000000021 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = "<group>"; };
DD0000000000000000000022 /* UI */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = UI; sourceTree = "<group>"; };
DD0000000000000000000023 /* Resources */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Resources; sourceTree = "<group>"; };
DD0000000000000000000024 /* Build */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Build; sourceTree = "<group>"; };
@@ -60,21 +59,19 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
DD0000000000000000000106 /* ArgumentParser in VPhoneVirtualMachineKit */,
DD0000000000000000000106 /* ArgumentParser in Frameworks */,
DD0000000000000000000102 /* Dynamic in Frameworks */,
DD0000000000000000000103 /* NIOCore in Frameworks */,
DD0000000000000000000104 /* NIOPosix in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
DD0000000000000000000043 /* Frameworks */ = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (); runOnlyForDeploymentPostprocessing = 0; };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
DD0000000000000000000002 = {
isa = PBXGroup;
children = (
DD0000000000000000000021 /* Sources */,
DD0000000000000000000022 /* UI */,
DD0000000000000000000023 /* Resources */,
DD0000000000000000000024 /* Build */,
@@ -86,7 +83,7 @@
DD0000000000000000000003 /* Products */ = {
isa = PBXGroup;
children = (
DD0000000000000000000031 /* vphone-app.app */,
DD0000000000000000000031 /* VPhone.bundle */,
DD0000000000000000000032 /* libVPhoneVirtualMachineKit.a */,
DD0000000000000000000033 /* vphone-vm */,
);
@@ -96,12 +93,10 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
DD0000000000000000000011 /* vphone-app */ = {
DD0000000000000000000011 /* VPhone */ = {
isa = PBXNativeTarget;
buildConfigurationList = DD0000000000000000000081 /* Build configuration list for PBXNativeTarget "vphone-app" */;
buildConfigurationList = DD0000000000000000000081 /* Build configuration list for PBXNativeTarget "VPhone" */;
buildPhases = (
DD0000000000000000000051 /* Sources */,
DD0000000000000000000043 /* Frameworks */,
DD0000000000000000000061 /* Bundle Companion Binaries */,
);
buildRules = (
@@ -109,14 +104,12 @@
dependencies = (
DD0000000000000000000123 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
DD0000000000000000000021 /* Sources */,
name = VPhone;
packageProductDependencies = (
);
name = "vphone-app";
packageProductDependencies = ();
productName = "vphone-app";
productReference = DD0000000000000000000031 /* vphone-app.app */;
productType = "com.apple.product-type.application";
productName = VPhone;
productReference = DD0000000000000000000031 /* VPhone.bundle */;
productType = "com.apple.product-type.bundle";
};
DD0000000000000000000012 /* VPhoneVirtualMachineKit */ = {
isa = PBXNativeTarget;
@@ -151,7 +144,8 @@
DD0000000000000000000053 /* Sources */,
DD0000000000000000000041 /* Frameworks */,
);
buildRules = ();
buildRules = (
);
dependencies = (
DD0000000000000000000121 /* PBXTargetDependency */,
);
@@ -191,7 +185,7 @@
projectDirPath = "";
projectRoot = "";
targets = (
DD0000000000000000000011 /* vphone-app */,
DD0000000000000000000011 /* VPhone */,
DD0000000000000000000012 /* VPhoneVirtualMachineKit */,
DD0000000000000000000013 /* vphone-vm */,
);
@@ -212,7 +206,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/zsh;
shellScript = "\"$SRCROOT/Build/StageApp.sh\"\n";
shellScript = "\"$SRCROOT/Build/StageBundle.sh\"\n";
};
DD0000000000000000000062 /* Build VPhoneKit */ = {
isa = PBXShellScriptBuildPhase;
@@ -232,13 +226,6 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
DD0000000000000000000051 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
DD0000000000000000000052 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -246,7 +233,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
DD0000000000000000000053 /* Sources */ = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (); runOnlyForDeploymentPostprocessing = 0; };
DD0000000000000000000053 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -269,13 +262,12 @@
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = arm64;
CODE_SIGNING_ALLOWED = NO;
EXECUTABLE_NAME = "vphone-app";
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Resources/Info.plist;
MACOSX_DEPLOYMENT_TARGET = 15.0;
PRODUCT_BUNDLE_IDENTIFIER = com.vphone.app;
PRODUCT_BUNDLE_IDENTIFIER = com.vphone.bundle;
PRODUCT_MODULE_NAME = VPhoneVirtualization;
PRODUCT_NAME = "vphone-app";
PRODUCT_NAME = VPhone;
SDKROOT = macosx;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 6.0;
@@ -288,13 +280,12 @@
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = arm64;
CODE_SIGNING_ALLOWED = NO;
EXECUTABLE_NAME = "vphone-app";
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Resources/Info.plist;
MACOSX_DEPLOYMENT_TARGET = 15.0;
PRODUCT_BUNDLE_IDENTIFIER = com.vphone.app;
PRODUCT_BUNDLE_IDENTIFIER = com.vphone.bundle;
PRODUCT_MODULE_NAME = VPhoneVirtualization;
PRODUCT_NAME = "vphone-app";
PRODUCT_NAME = VPhone;
SDKROOT = macosx;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 6.0;
@@ -423,7 +414,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
DD0000000000000000000081 /* Build configuration list for PBXNativeTarget "vphone-app" */ = {
DD0000000000000000000081 /* Build configuration list for PBXNativeTarget "VPhone" */ = {
isa = XCConfigurationList;
buildConfigurations = (
DD0000000000000000000071 /* Debug */,
@@ -1,11 +1,10 @@
// vphone-vm — the process that actually runs a guest.
//
// This is the only binary in the project signed with the private
// virtualization entitlements (Resources/vphone.entitlements), and it is
// virtualization entitlements (Resources/VPhoneVirtualization.entitlements), and it is
// deliberately the smallest thing that can hold them: it parses the boot
// options, becomes an NSApplication, and hands off to VPhoneVirtualMachineAppDelegate.
// The unentitled vphone-app launcher and vphone-cli start this process for
// the boot. Neither launcher carries private virtualization entitlements.
// The unentitled vphone-cli starts this process for the boot.
//
// It takes the boot options directly rather than a `boot` subcommand — this
// binary has exactly one job, so there is nothing to select between.
@@ -1,5 +1,6 @@
import CryptoKit
import Foundation
import VPhoneCoreKit
/// Resolves a local IPSW or downloads one into a reusable, validated cache.
/// Source archives are never rewritten. Remote downloads become visible only
@@ -50,6 +51,8 @@ public enum VPhoneIPSWCache {
let cache = cacheDirectory.appendingPathComponent(cacheName(for: url))
if fm.fileExists(atPath: cache.path) {
if let valid = try? inspect(cache) {
try VPhoneHostFilePermissions.makeAccessible(at: cache)
try VPhoneHostFilePermissions.makeDirectoryAccessible(at: cacheDirectory)
return valid
}
try fm.removeItem(at: cache)
@@ -76,6 +79,8 @@ public enum VPhoneIPSWCache {
try fm.moveItem(at: downloaded, to: pending)
let metadata = try inspect(pending)
try fm.moveItem(at: pending, to: cache)
try VPhoneHostFilePermissions.makeAccessible(at: cache)
try VPhoneHostFilePermissions.makeDirectoryAccessible(at: cacheDirectory)
return Archive(file: cache, version: metadata.version, build: metadata.build)
}
@@ -82,6 +82,9 @@ public enum VPhoneBundleOperations {
)
try manifest.write(to: dir.appendingPathComponent("config.plist"))
try VPhoneHostFilePermissions.makeAccessible(at: dir)
try VPhoneHostFilePermissions.makeDirectoryAccessible(at: library.root)
try VPhoneHostFilePermissions.makeDirectoryAccessible(at: VPhoneResources.userDataRoot())
return VPhoneBundle(url: dir, manifest: manifest)
} catch {
try? fm.removeItem(at: dir)
@@ -114,6 +117,7 @@ public enum VPhoneBundleOperations {
networkConfig: network,
)
try updated.write(to: bundle.configURL)
try VPhoneHostFilePermissions.makeAccessible(at: bundle.configURL)
return VPhoneBundle(url: bundle.url, manifest: updated)
}
@@ -164,6 +168,7 @@ public enum VPhoneBundleOperations {
try fm.copyItem(at: src, to: dst)
}
try resetIdentity(inBundleAt: dst)
try VPhoneHostFilePermissions.makeAccessible(at: dst)
return try VPhoneBundle.load(at: dst)
}
@@ -180,6 +180,7 @@ public enum VPhoneFirmwareIndex {
withIntermediateDirectories: true,
)
try? json.write(to: cache)
try? VPhoneHostFilePermissions.makeAccessible(at: cache)
return json
}
@@ -17,13 +17,12 @@ public struct VPhoneResources: Sendable {
/// Neither obvious alternative works here. `CommandLine.arguments[0]` is a
/// bare name under a PATH or symlink launch, which `URL(fileURLWithPath:)`
/// then resolves against the CWD and lands under `$HOME`.
/// `Bundle.main.executableURL` reads `CFBundleExecutable` out of Info.plist
/// and answers "vphone-app" even when the running binary is `vphone-cli`
/// or `vphone-vm` beside it in `Contents/MacOS`. It cannot find ourselves.
/// `Bundle.main.executableURL` can describe a bundle's main executable
/// instead of the tool actually running beside it in `Contents/MacOS`.
///
/// `_NSGetExecutablePath` has neither problem: it is the path the kernel
/// exec'd, independent of argv and of any plist. Symlinks are resolved so a
/// Homebrew symlink lands on the real binary inside the .app.
/// a command symlink lands on the real binary inside the .bundle.
public static func runningExecutable() -> URL {
var size = UInt32(PATH_MAX)
var buffer = [CChar](repeating: 0, count: Int(size))
@@ -105,8 +104,7 @@ public struct VPhoneResources: Sendable {
if FileManager.default.fileExists(atPath: bundled.path) {
return bundled
}
// Dev fallback: build.sh stages the signed daemon under .build (a
// gitignored build-output dir) rather than cluttering the repo root.
// Dev fallback for a source build outside the bundle.
return base.appendingPathComponent(".build/vphoned.signed")
}
@@ -0,0 +1,62 @@
import Darwin
import Foundation
/// Host-side VM artifacts must remain usable when another local process owns
/// the workstation UI and the command was originally run as root.
public enum VPhoneHostFilePermissions {
/// Make regular files and directories under an output world accessible.
/// Symbolic links are never followed, and special files are left alone.
public static func makeAccessible(at url: URL) throws {
let descriptor = open(url.path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK)
guard descriptor >= 0 else {
if errno == ENOENT { return }
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
defer { close(descriptor) }
try makeAccessible(descriptor: descriptor, path: url.path)
}
public static func makeDirectoryAccessible(at url: URL) throws {
let descriptor = open(url.path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC)
guard descriptor >= 0 else {
if errno == ENOENT { return }
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
defer { close(descriptor) }
var metadata = stat()
guard fstat(descriptor, &metadata) == 0, metadata.st_mode & S_IFMT == S_IFDIR else {
throw POSIXError(.ENOTDIR)
}
guard fchmod(descriptor, 0o777) == 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
}
private static func makeAccessible(descriptor: Int32, path: String) throws {
var metadata = stat()
guard fstat(descriptor, &metadata) == 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
let kind = metadata.st_mode & S_IFMT
if kind == S_IFDIR {
for name in try FileManager.default.contentsOfDirectory(atPath: path) {
let child = openat(descriptor, name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK)
if child < 0 {
if errno == ELOOP || errno == ENOENT { continue }
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
do {
defer { close(child) }
try makeAccessible(
descriptor: child,
path: (path as NSString).appendingPathComponent(name),
)
}
}
}
guard kind == S_IFDIR || kind == S_IFREG else { return }
guard fchmod(descriptor, 0o777) == 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
}
}
@@ -1,6 +1,6 @@
// VPhoneGuestBinaries.swift — where the prebuilt iOS binaries live.
//
// vphoned is cross-compiled on the build machine and shipped in the app.
// vphoned is cross-compiled on the build machine and shipped in the bundle.
// The runtime installs it with an ad-hoc signature and never needs Xcode.
import Foundation
@@ -13,7 +13,7 @@ public enum VPhoneGuestBinaries {
switch self {
case let .missing(name, searched):
"""
no prebuilt guest binary '\(name)'. Build the vphone-app scheme in VPhone.xcworkspace.
no prebuilt guest binary '\(name)'. Build the VPhone scheme in VPhone.xcworkspace.
Looked in: \(searched.map(\.path).joined(separator: ", "))
"""
}
@@ -17,7 +17,7 @@ public enum VPhoneGuestLaunchError: Error, CustomStringConvertible {
return """
\(name) is missing — expected it next to this binary at:
\(url.path)
The install looks incomplete. Rebuild the vphone-app scheme in Xcode.
The bundle is incomplete. Rebuild the VPhone scheme in Xcode.
"""
case let .blockedByAMFI(guest, cdHash):
@@ -40,7 +40,7 @@ public enum VPhoneGuestLaunchError: Error, CustomStringConvertible {
case let .missingEntitlements(guest):
return """
vphone-vm is missing the private PV=3 entitlements: \(guest.path)
Rebuild the vphone-app scheme in Xcode to sign this binary, then allow the new signature
Rebuild the VPhone scheme in Xcode to sign this binary, then allow the new signature
through the host's AMFI policy before launching a VM.
"""
@@ -35,6 +35,7 @@ public struct VPhoneLaunchLayout: Sendable {
try fm.removeItem(at: dst)
}
try fm.copyItem(at: vphoned, to: dst)
try VPhoneHostFilePermissions.makeAccessible(at: dst)
return true
}
}
@@ -15,9 +15,9 @@ import Testing
@Suite(.serialized)
struct ResourcesTests {
@Test func `bundled layout resolves to contents resources`() {
let exe = "/Applications/vphone-cli.app/Contents/MacOS/vphone-cli"
let exe = "/Applications/VPhone.bundle/Contents/MacOS/vphone-cli"
let r = VPhoneResources.resolve(executablePath: exe)
#expect(r.base.path == "/Applications/vphone-cli.app/Contents/Resources")
#expect(r.base.path == "/Applications/VPhone.bundle/Contents/Resources")
}
@Test func `dev layout walks up to project root`() throws {
@@ -0,0 +1,38 @@
import Darwin
import Foundation
import Testing
@testable import VPhoneCoreKit
struct HostFilePermissionsTests {
@Test func `VM outputs become 0777 without following symlinks`() throws {
let base = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
let vm = base.appendingPathComponent("VM")
let nested = vm.appendingPathComponent("Firmware")
let disk = vm.appendingPathComponent("Disk.img")
let image = nested.appendingPathComponent("image")
let outside = base.appendingPathComponent("outside")
defer { try? FileManager.default.removeItem(at: base) }
try FileManager.default.createDirectory(at: nested, withIntermediateDirectories: true)
try Data("disk".utf8).write(to: disk)
try Data("image".utf8).write(to: image)
try Data("outside".utf8).write(to: outside)
try FileManager.default.createSymbolicLink(
at: vm.appendingPathComponent("link"),
withDestinationURL: outside,
)
for path in [vm, nested, disk, image, outside] {
#expect(chmod(path.path, 0o700) == 0)
}
try VPhoneHostFilePermissions.makeAccessible(at: vm)
for path in [vm, nested, disk, image] {
var info = stat()
#expect(stat(path.path, &info) == 0)
#expect(info.st_mode & 0o777 == 0o777)
}
var outsideInfo = stat()
#expect(stat(outside.path, &outsideInfo) == 0)
#expect(outsideInfo.st_mode & 0o777 == 0o700)
}
}