mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-02 08:04:32 +08:00
Build distributable VPhone bundle and remove OpenAPI document
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)から現在の手順と研究資料に進めます。詳しいガイドは現在英語です。
|
||||
|
||||
@@ -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)에 모았습니다. 상세 가이드는 현재 영어로 제공됩니다.
|
||||
|
||||
@@ -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 程序。
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -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 = (
|
||||
|
||||
+5
@@ -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)")
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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))
|
||||
+12
@@ -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")
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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",
|
||||
+4
@@ -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
|
||||
}
|
||||
+3
@@ -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)")
|
||||
}
|
||||
+1
@@ -78,6 +78,7 @@ struct VPhoneVirtualMachineWriteManifestCommand: ParsableCommand {
|
||||
)
|
||||
}
|
||||
try manifest.write(to: configURL)
|
||||
try VPhoneHostFilePermissions.makeAccessible(at: configURL)
|
||||
print("Created VM manifest: \(configURL.path)")
|
||||
}
|
||||
}
|
||||
+15
-4
@@ -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)")
|
||||
+4
@@ -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)
|
||||
|
||||
+4
@@ -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)")
|
||||
}
|
||||
+3
-3
@@ -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 */;
|
||||
|
||||
+5
-11
@@ -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
|
||||
+15
-16
@@ -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"
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
+28
-37
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user