Add iPhone17,3 iOS 26.6.1 (23G82) paired with cloudOS 26.4 to the
firmware catalog so it appears in the `fw prepare` picker, `fw catalog`,
and the `--json` report. Document it in the Tested Environments table
across README.md and the ja/ko/zh translations. Bump the hardcoded
pairing/menu count in FirmwarePickerTests 19 -> 20.
Co-Authored-By: Claude Fable 5 <[email protected]>
Reflect fast-by-default compression (--max = xz -9), directory
auto-naming, and the positional import argument in the README and its
ja/ko/zh translations.
Co-Authored-By: Claude Fable 5 <[email protected]>
Export/import UX overhaul, wiring in the counting-pipe and progress-bar
primitives:
- Compression: replace `--compress {fast,balanced,max}` with a fast
(zstd -3) default and a single `--max` (xz -9) flag; drop balanced.
- `vm export --out DIR`: when the destination is an existing directory,
auto-name the archive `<vm>.tzst`/`.txz` and print the resolved path.
- `vm import ARCHIVE`: take the archive as a positional argument instead
of `--in`.
- Progress bars: export runs a two-stage tar pipeline (uncompressed
gnutar producer -> compressing consumer via bsdtar `@-`) so the bar is
driven off the known uncompressed total; import extracts once (was
decompressing twice) and counts the archive as it is fed into `tar -x`.
gnutar avoids the pax-header `@-` mtree misbid on large members and,
unlike ustar, carries files >8 GB.
Import validates the single top-level bundle after extracting into a
private staging dir, still failing fast on an explicit --name collision.
Tests: default is fast zstd; --max is xz; directory auto-naming yields
the right extensions; export/import invoke progress with monotonic,
correctly-totalled callbacks.
Co-Authored-By: Claude Fable 5 <[email protected]>
Two building blocks for streaming progress on vm export/import, unused
until the export/import rework wires them in:
- VPhoneProcessRunner.runCountingTarPipe: drives a /usr/bin/tar consumer
that reads its archive on stdin, invoking a callback with the running
byte total. The source is either a producer tar (export) or a file read
directly (import). SIGPIPE is ignored so a consumer that dies early
surfaces as its exit status instead of killing this process.
- VPhoneProgressBar: a single-line redrawing byte bar that renders to
stderr only when it is a TTY, so piped/--json/GUI-subprocess runs stay
clean and it simply no-ops.
Co-Authored-By: Claude Fable 5 <[email protected]>
The build cross-compiles the guest daemon (vphoned) for iOS, which
requires the iOS SDK bundled with Xcode. Add it to the host
prerequisites in the English, Japanese, Korean, and Chinese READMEs.
Co-Authored-By: Claude Fable 5 <[email protected]>
Export previously hardcoded xz -9 (the densest but slowest libarchive
compressor), forcing a multi-GB Disk.img through the most aggressive
setting on every export.
Add a `--compress {fast,balanced,max}` preset on `vphone vm export`,
default `balanced`:
fast zstd -3
balanced zstd -19 (new default)
max xz -9 (previous behavior)
All presets go through the same system /usr/bin/tar (libarchive) already
used for xz, so no new dependency and no raised platform floor. Import is
unchanged: it already auto-detects the compressor via `tar -tf`/`-xf`, so
zstd and xz archives both import.
Tests assert each preset's magic bytes (zstd vs xz), that fast/max
round-trip, and the default is zstd. README examples updated.
Co-Authored-By: Claude Fable 5 <[email protected]>
The JB first-boot setup adds the Frida repo (deb https://build.frida.re/ ./)
next to the existing Havoc source, so Sileo/apt can install and update Frida
packages. Idempotent — skipped if a build.frida.re source is already present —
and picked up by the same insecure-repo `apt-get update` that follows.
Co-Authored-By: Claude Fable 5 <[email protected]>
- ARM64Encoder round-trips (TBZ/TBNZ bit retarget, movz flag clear) and the
cloudOS 26.4 version-gate compare.
- Reveal procedures and static/runtime validation for both kernel patches;
patch-comparison table rows JB-23b / JB-25c and the opt-in note.
Co-Authored-By: Claude Fable 5 <[email protected]>
On a --frida build, fetch_debs.sh resolves the latest frida iphoneos-arm64
release deb (== re.frida.server: no Depends, rootless /var/jb layout) into the
debs cache; the existing first-boot `dpkg -i` step installs it. VPHONE_FRIDA is
forwarded through the host CFW install. No APT source or dependency resolution.
Co-Authored-By: Claude Fable 5 <[email protected]>
Expose --frida on patch-firmware, patch-component, fw patch, and vm create
(plus `make fw_patch_jb/exp FRIDA=1`), threading it into
KernelJBPatcher.applyFrida via FirmwarePipeline.
The kernel patches apply only when the cloudOS kernel is 26.4+ (the versions
where the shapes were validated); older kernels are left untouched. Baseline
JB/EXP output is byte-identical without --frida (26.4 emits 83 records, 87 with).
Co-Authored-By: Claude Fable 5 <[email protected]>
Two narrowly-scoped patches, emitted only under the Frida opt-in:
- thread_set_state: clear TSSF_CHECK_ENTITLEMENT in the thread_set_state user
setters (mov w6,#0x201 -> #0x1) so Frida can follow an existing thread without
a GUARD_TYPE_MACH_PORT kill, while preserving TSSF_TRANSLATE_TO_USER and the
TH_IN_MACH_EXCEPTION guard.
- vm_map_delete: retarget the immutable-code exception from current-protection
execute (bit 9) to max-protection execute (bit 13) so a debugger-created
RW/max-RWX permanent mapping survives repeated VM_PROT_COPY re-instrumentation
instead of returning KERN_PROTECTION_FAILURE.
Both matchers are semantic (entitlement-string / developer-mode / call-flow
anchored, no hardcoded offsets/VAs/registers/bytes) and fail closed; replacement
bytes come from ARM64Encoder and are Capstone-verified.
Co-Authored-By: Claude Fable 5 <[email protected]>
iOS 27's os_lockdown_mode_enabled() resolves Lockdown Mode via
sysctlbyname("security.mac.lockdown_mode_state_public", ...) and os_crashes
on a -1 return. The vphone base kernel (cloudOS 26.x) does not implement that
MAC sysctl, so the call returns -1/ENOENT and the first daemon to query
Lockdown Mode after "Continuing system boot" -- launchd (pid 1) -- aborts,
panicking the system (initproc exited, namespace 2 subcode 6).
Add cfw_patch_lockdown_mode.py: NOP the `cmn w0,#1; b.eq <os_crash>` gate so
the pre-zeroed output buffer path is taken (Lockdown Mode = disabled);
behavior-neutral on a kernel that implements the sysctl. Wire it into cfw.py
(patch-lockdown-mode) and the cfw_install.sh 27.* DSC-patch block. Also fixes
the 0_binary_patch_comparison.md LWCR note and adds row 16.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01N2bwKrGJWY7o2ArdxibVPe
The patcher resolved `_xpc_token_satisfies_lwcr`, but libxpc's internal
routine carries the standard extra leading underscore in the DSC symbol
table (`__xpc_token_satisfies_lwcr`), so the lookup missed and the patch
silently no-op'd on every iOS 27 build. Resolve against the mangled name,
falling back to the source name.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01N2bwKrGJWY7o2ArdxibVPe
Add the iPhone17,3 27.0 24A5408d + cloudOS 26.4 pairing to the firmware
catalog so it is selectable in `fw prepare` / `vm create`, and bump the two
hardcoded pairing counts in the picker tests (18 -> 19).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01N2bwKrGJWY7o2ArdxibVPe
PyPI has no arm64 macOS wheel for keystone-engine, so pip builds it
from the sdist, whose make-share.sh invokes cmake directly. Without
it the build fails silently and installs bindings with no native
library, which is what the libkeystone repair recovers from.
The unified tool provisions ~/.vphone/venv itself and never runs
setup_venv.sh, so it lost that script's libkeystone handling. It
pip-installs and then verifies with pythonIsUsable, which only probes
ipsw_parser — a venv with keystone bindings and no native library
passes, gets cached as good, and fails much later inside fw patch.
That state is reachable and silent. PyPI has no arm64 macOS wheel
(only macosx_10_14_x86_64), so pip builds keystone-engine from its
sdist, and the sdist's darwin path ignores the build's exit status
(subprocess.call plus a glob that matches nothing) — a failed native
build still installs bindings alone and pip reports success.
Probe that keystone can assemble rather than merely import, and on
failure install a loadable dylib next to the bindings: copy one from
Homebrew if the bottle ships it, else link one from libkeystone.a,
matching setup_venv.sh. The managed venv is repaired in place before
being rebuilt, since a missing dylib is not worth a full re-install.
Verified: repairs a venv whose dylibs were removed, and recovers a
fresh bootstrap that produced bindings-only (pip cache cleared, cmake
off PATH) — pip exits 0, the guard catches it, the venv is accepted.
Co-Authored-By: Claude Fable 5 <[email protected]>
`do shell script … with administrator privileges` runs the command under a
helper process that inherits none of our fds, so AppleScript can only hand
back its output once the command has EXITED — and it rewrites every \n to
\r on the way, which a reader applying terminal semantics (the GUI's log
view) takes for progress-bar overwrites, keeping only the final line. The
CFW install stage therefore sat silent for minutes and then showed one line.
Name our own terminal in the command instead: ttyname(stdout) is an absolute
device path the privileged shell can open, and under the GUI it is already
the pty the app is reading. Output then arrives as produced, with newlines
intact, on a tty the script's children line-buffer to. Gated on `echo` so
quiet verbosity still suppresses it, and skipped when stdout is not a tty.
Co-Authored-By: Claude Fable 5 <[email protected]>
The fw prepare step sent the script's entire stdout/stderr to
/dev/null unless -v was passed, so the aria2c/curl/wget progress bar
was discarded with it. That left the longest phase of the pipeline —
a multi-GB IPSW download — printing nothing for minutes, which reads
as a hang.
Stream it unconditionally. Standalone `fw prepare` already does this
by flooring its verbosity at .info; this brings `vm create` in line.
The script itself is status-line based (~40 echoes, unzip -oq, no
per-file loops), so this adds progress rather than noise.
Co-Authored-By: Claude Fable 5 <[email protected]>
Boots without a VM window or menu bar. The guest keeps its display
device, so the boot chain is unchanged — only the AppKit window, menu
bar and Dock presence are skipped.
Available on `boot` and `vm launch`. `vm create` now uses it for both
setup boots (first boot and boot analysis); --interactive keeps the
window, since it asks the operator to press Enter once the VM has
booted and the window is their only progress cue.
Note: the vphone.sock host control socket does not start in headless
mode — its handler needs the VZVirtualMachineView capture view.
Co-Authored-By: Claude Fable 5 <[email protected]>
`vm create` built the orchestrator Options without setting diskSizeGB, so
the disk was pinned to the 64 GB default no matter what. The downstream
plumbing (Options.diskSizeGB -> NewBundleSpec -> sparse Disk.img truncate)
already existed; this just exposes `-d/--disk-size` on the command and
threads it through, matching the existing `vm new` convention.
Co-Authored-By: Claude Fable 5 <[email protected]>
Show the known iOS ↔ cloudOS firmware pairings, one recommended cloudOS
per iOS build, projected from VPhoneFirmwareCatalog.pairings. Human
output is an aligned table; `--json`/`-j` emits an object carrying the
device plus each pairing's download URLs.
Co-Authored-By: Claude Fable 5 <[email protected]>
`vm info --json` and `vm list --json` encode VPhoneBundleReport, which
carried no network field — so the network mode was absent from JSON even
though the human-readable output prints `net:`. Consumers parsing JSON had
no way to read a VM's network config.
Project the manifest's NetworkConfig into VPhoneBundleReport as `network`,
so both `info` and `list` emit it as structured JSON (mode, macAddress,
bridgeInterface). NetworkConfig gains Equatable (VPhoneBundleReport is
Equatable). The text `net:` line now reads from the same report projection.
Co-Authored-By: Claude Fable 5 <[email protected]>
`vm info` now reports the CFW variant the VM was last restored to, its
predicted UDID, and the product type the guest identifies as.
- udid: read from the bundle's udid-prediction.txt (VPhoneRestoreOps.resolveUDID)
- variant + device: recorded into restore-info.json at CFW-install time, in
both the standalone `cfw install` and the `vm create` orchestrator. device is
iPhone99,11 for every variant except exp, whose DeviceTree rewrite -> iPhone17,3.
- both new restore-info.json fields are optional, so pre-existing bundles decode
unchanged and pick up variant/device on their next install.
Co-Authored-By: Claude Fable 5 <[email protected]>
Adds `--network nat|bridged|none` and `--bridge-interface` to `vm config`,
and wires the boot path to honor the stored networkConfig (it previously
hardcoded NAT and ignored the manifest). `vm info` now shows the mode.
- New VPhoneNetworking: validates the mode, resolves/auto-picks the bridge
interface, and builds the VZ network device — shared by config-time
editing and boot.
- bridged uses VZBridgedNetworkDeviceAttachment (com.apple.vm.networking
entitlement already present); hostOnly is rejected (no native VZ
attachment); none yields no NIC.
- Rename NetworkMode.none -> .off (raw value kept "none") so a NetworkMode?
literal `.none` can't silently bind to Optional.none.
The MAC is left framework-assigned; forcing a custom MAC breaks guest
networking, so no MAC override is exposed.
Co-Authored-By: Claude Fable 5 <[email protected]>
* feat: add --root-popup to elevate CFW host-mount via macOS auth dialog
Adds --root-popup to `cfw install` and `vm create`, elevating the CFW host-mount through macOS's native authentication dialog (osascript -> do shell script with administrator privileges) instead of the script's sudo re-exec. do shell script runs under a bare env, so the vars the bundled scripts read are forwarded inline, plus SUDO_USER so the script's chown-back still returns artifacts to the invoking user. On `vm create`, --sudo-password takes precedence.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* cfw: remove entire .cfw_temp on install cleanup
Replaces the selective `rm -f` of individual temp binaries with `rm -rf "$TEMP_DIR"`, dropping the cached Cryptex DMGs along with the temp files.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
Delete the three large regenerable intermediates once their consumers
finish, keeping the source archives so nothing needs re-downloading:
- built restore firmware (iPhone*_Restore/) after CFW install (its last
consumer — it copies the SystemOS/AppOS cryptexes onto Disk.img)
- extracted base-IPSW dirs (iOS + cloudOS) at end of fw prepare; the
downloaded .ipsw files are kept, so a re-run re-extracts, no re-download
- extracted CFW input dirs (cfw_input/, cfw_jb_input/) after cfw install;
the resources .tar.zst archives are kept
Opt out with --keep-artifacts on `vm create` / `cfw install`, which
threads VPHONE_KEEP_ARTIFACTS to fw_prepare.sh and cfw_install_host.sh.
Co-authored-by: Claude Fable 5 <[email protected]>
Snapshot the restored iOS userland and cloudOS kernel versions to
restore-info.json at the bundle root so they are readable without booting
the VM, rewritten after every successful restore (vm create and vm restore).
Versions are read host-side from the bundle's iPhone*_Restore plists
(iPhone-BuildManifest.plist for iOS, the hybrid BuildManifest.plist for
cloudOS). vm list / vm info / --json surface them; the file lives at the
bundle root so vm export carries it even when the IPSW dir is excluded.
Co-authored-by: Claude Fable 5 <[email protected]>
Restructure the README (Prerequisites/Install/Build up top, a dedicated
SIP/AMFI Relaxation section, Tested Environments moved down) and trim prose.
Regenerate the Korean, Japanese, and Chinese translations to match the new
structure verbatim — same 13 sections in the same order, all commands, URLs,
tables, and identifiers preserved byte-for-byte, only prose and code comments
translated, docs/-relative links and in-language anchors applied.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
The v1.0.2 release build failed at the bundle codesign step:
.build/vphone-cli.app/Contents/MacOS/vphone-cli: code object is not signed at all
In subcomponent: .../Contents/MacOS/vphone-amfidont
Contents/MacOS is the bundle's nested-code directory, so signing the main
executable seals everything there and rejected the vphone-amfidont shell
script as unsigned nested code. (A script only gets a "generic" xattr
signature that wouldn't survive the release zip anyway.)
Move the bundled helper to Contents/Resources/vphone-amfidont, where it is
sealed as an ordinary resource (hashed, survives zip). Resources sits at the
same depth under Contents as MacOS, so the script's `${0:A:h:h:h}` .app
resolution is unchanged. The Homebrew `binary` stanza should point at
Contents/Resources/vphone-amfidont.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
New bundled tool `vphone-amfidont` (zsh script at Contents/MacOS/, exposed
on PATH alongside vphone-cli). It:
- resolves the enclosing vphone-cli.app (`${0:A:h:h:h}`; :A follows a
Homebrew symlink back into the bundle);
- ensures amfidont is installed, offering `xcrun python3 -m pip install`
if it is missing;
- checks (without root) whether an amfidont is already running — only one
can attach to amfid at a time. If so, it escalates to a single sudo to
read the running process's args and the /var/root config, reports whether
this .app is already covered (allowed path or --allow-all), and exits;
- otherwise starts `amfidont daemon --spoof-apple --path <app>`, resolving
amfidont's absolute path first so a pip-user install off root's
secure_path is still found under sudo.
build.sh bundles it next to vphone-cli/ldid in Contents/MacOS.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
`vm create` on an interactive terminal now prompts for whichever firmware
component wasn't passed on the command line, choosing from a known-good
iPhone/cloudOS catalog by friendly name (e.g. "iOS 26.4", "cloudOS 26.4")
rather than raw URLs. Supply one of --iphone-source/--cloudos-source and
only the other is asked for; supply neither and a full pairing is chosen.
Non-interactive runs (or both flags set) pass through unchanged, so
fw_prepare's defaults still fill any gap and scripted use is unaffected.
- VPhoneFirmwareCatalog: 18 iPhone17,3 pairings + 4 distinct cloudOS images.
- VPhoneFirmwarePicker: pure, injectable-I/O resolver (13 unit tests).
- VPhoneFirmwareSelection: TTY adapter (isatty + readLine → stderr prompts).
- Wired into `vm create` before the orchestrator runs; READMEs (+ ja/ko/zh)
document the prompt behavior.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
A whole-codebase audit surfaced path bugs that only bite the bundled .app —
especially when brew puts vphone-cli on $PATH via a symlink, so the process
launches by a bare name from an arbitrary CWD:
- HIGH (crash): CommandLine.arguments[0] was used to locate the running binary
and the resource base (VPhoneResources.resolve default, vm launch bootBinary,
vm create selfExe). Under a bare-name PATH launch argv[0] is just
"vphone-cli", which URL(fileURLWithPath:) resolves against the CWD (e.g.
$HOME/vphone-cli) — so `vm launch` errored "not found" and `vm create` couldn't
respawn to boot. Add VPhoneResources.runningExecutable() using
Bundle.main.executableURL (the kernel-provided path, correct regardless of
argv[0]/CWD/symlink) and route all three through it. One helper fixes every
.resolve() consumer.
- MEDIUM (silent): the extra-deb feature resolved its cache + manifest under the
read-only bundle (Resources/debs, Resources/debs.list). Honor VPHONE_DEBS_DIR
(a writable ~/.vphone/debs the app now sets, mirroring IPSW_DIR/VPHONE_SEAL_DIR)
and bundle debs.list.
- LOW (cosmetic): fw_prepare read ../README.md (absent in the bundle → firmwares
labeled "Not Tested"). Bundle README.md.
Verified: Bundle.main.executableURL yields the real binary under a bare-name
symlink launch (argv[0] → $HOME); build clean; 79 VPhoneCore tests pass.
boot_host_preflight.sh hardcoded RELEASE_BIN=$PROJECT_ROOT/.build/release/
vphone-cli. Inside the bundled .app, PROJECT_ROOT resolves to Contents/
Resources, so it looked for Contents/Resources/.build/release/vphone-cli —
which doesn't exist (the binary is at Contents/MacOS/vphone-cli). Under
--assert-bootable that made the preflight fail with "missing release binary",
blocking `vm launch` from a brew-installed or copied .app.
`vm launch` now passes the running executable (CommandLine.arguments[0]) to the
preflight via VPHONE_CLI_BIN and the script checks that binary; it still falls
back to the dev .build/release path for standalone/`make` invocation.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Foundation `Process` starts children in a NEW process group, so a sudo it
spawns is a *background* member of the controlling terminal — it can't disable
echo or read the tty, so the typed password showed and wasn't delivered. No
stdio wiring fixes that (confirmed: a Process child's pgid != the parent's).
Add VPhoneProcessRunner.runForeground: hand the terminal to the child's process
group via tcsetpgrp (SIGTTOU/SIGTTIN ignored during the swap), restore ours
after. The CFW-install step uses it when no --sudo-password is given, so sudo
owns the tty and reads the password DIRECTLY — vphone-cli never sees it. Its
`echo` flag still honors verbosity: quiet suppresses the install's own output
(stdout/stderr → /dev/null) while sudo's /dev/tty prompt keeps working. With
--sudo-password the unattended askpass path is unchanged; a non-interactive run
with no password fails fast.
Verified under a PTY: without tcsetpgrp the child is background (the bug); with
it the child is foreground; and echo=false hides the child's output while it
stays foreground.
Also serialize LibraryTests: its two VPHONE_LIBRARY_ROOT env tests mutate a
process-global and raced under Swift Testing's parallelism (intermittent
failures) — mark the suite @Suite(.serialized).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
The release workflow ran `make bundle`, which produces a lean .app (binary +
ldid + signcert + icon only) — missing the bundled scripts/patchers/resources/
requirements.txt/vphoned/.tools — so published release assets were not
self-contained and `brew install` copies couldn't run the fw/restore/cfw
pipeline.
Switch to ./scripts/build.sh (the canonical portable build):
- also init the scripts/resources storage submodule + the
scripts/repos/{trustcache,insert_dylib} tool sources
- build .tools/bin/{trustcache,insert_dylib} (mirrors setup_tools steps 2-3;
its venv + sshpass steps aren't needed to build)
- run build.sh, then fail the job if the bundle is missing any runtime asset
or the virtualization entitlement before packaging + uploading
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
- README.md rewritten around the vphone-cli binary (no make): tested-env
matrix, two-path SIP/AMFI setup, firmware variants, commands, short flags,
and a Python-runtime section covering the auto-provisioned venv / portability
- docs/README_{ko,ja,zh}.md regenerated as full translations mirroring it
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
- scripts/build.sh builds+signs the binary, bundles a self-contained .app
(mirrors scripts/patchers/resources/tools/vphoned + requirements.txt into
Contents/Resources) and re-signs; the signed guest daemon stages under
.build (not the repo root)
- CryptexFilesystemPatcher resolves assets via VPhoneResources instead of
CWD-relative paths
- Makefile space-safety; .gitignore updates
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
Full command surface so create → patch → restore → install → boot → manage all
run through vphone-cli, no make required:
- vm list/info/new/config/rename/delete/clone/export/import, launch/stop
(--kernel-debug-port forwarded to the boot binary)
- native end-to-end `vm create` (VPhoneCreateOrchestrator)
- fw prepare/patch, restore + cfw install
- `setup` to provision the Python env up front (also automatic on first use)
- interactive VM selection when a name is omitted, short option aliases, and
-v/-vv/-vvv verbosity
Wire the subcommands in main.swift/VPhoneCLI.swift; adjust VPhoneControl/
VPhoneError/VPhoneVirtualMachine for the config-driven boot path.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
The library the consolidated CLI is built on:
- bundle/library model (VPhoneBundle, VPhoneLibrary, VPhoneVirtualMachineManifest),
bundle ops + reporting, restore helpers (VPhoneRestoreOps)
- host process primitives: VPhoneProcessRunner, VPhoneManagedProcess (spawn +
stdout pattern-match + SIGKILL-escalating terminate), VPhoneLaunchLayout,
VPhoneBootPatterns
- VPhoneResources: bundled-.app vs dev asset resolution, and Python resolution
that provisions a per-user venv (~/.vphone/venv) on demand so the app is
portable — never depends on the repo's .venv
- VPhoneVerbosity (quiet/info/debug/trace) and VPhoneVMPicker
Manifest moves out of the executable target into VPhoneCore. Full unit-test
suite under tests/VPhoneCoreTests.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
* fix AVPBooter DGST patch: restrict search to movz/movk instructions
The DGST constant search matched any instruction string containing
"0x4447", including branch targets like `bl #0x4447c`. On macOS 27
beta 3+ firmware, such a branch appeared before the real DGST movk,
causing a false match and subsequent "x0 setter not found" failure.
Restrict the search to movz/movk/mov mnemonics only, which are the
actual constant-loading instructions.
Fixes#373
* fix iBSS JB nonce patch: use tolerance-based ADRP+ADD search
findRefsToOffset required exact match between ADRP+ADD computed address
and the "boot-nonce" string file offset. On macOS 27 beta 3 firmware,
code references a structure header 2 bytes before the string, causing
the exact match to fail.
Add findRefsNear() which allows up to 16 bytes of negative delta,
matching references to the containing structure rather than the string
itself. This makes the patch resilient to minor layout changes across
firmware versions.
On 26.x bases the EXC_GUARD (Mach port guard) disable patch is not required
to boot, so it is no longer applied unconditionally on regular/jb/exp.
Production apps shipping crash-reporting/RASP SDKs (Bugly, Crashlytics,
KSCrash) call task_swap_exception_ports(), which the research kernel enforces
as a fatal GUARD_TYPE_MACH_PORT violation on launch; a --force-exc-guard flag
(FORCE_EXC_GUARD=1 in the Makefile) re-enables the patch for those cases.
iOS 18 bases and the dev variant keep the patch always-on, since both need it
to boot. The documented violation is corrected from SET_EXCEPTION_BEHAVIOR to
KOBJECT_REPLY_PORT_SEMANTICS, matching the actual reproduction crash logs.
Also add /vm-*/ to .gitignore for multi-VM directories.
Co-authored-by: Paulo Sarrin <[email protected]>
Co-authored-by: zqxwce <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
patch-dsc-maxslide self-gates to a no-op when the shared cache fits the
vphone600 26.x kernel's 6 GiB region, which 26.x/18.x bases always do, so
it only fires on 27. Add a --force flag that bypasses the fits-check and
zeroes maxSlide unconditionally (still idempotent), and a
FORCE_DSC_MAXSLIDE=1 env opt-in in cfw_install.sh that runs it on non-27
bases. Default off; 27 behavior unchanged.
Also fix the installer env-threading in cfw_install_host.sh: a
${VAR:+NAME=val} word produced by expansion is not parsed as a shell
assignment (zsh runs it as a command), so route the assignments through
env. This makes FORCE_DSC_MAXSLIDE reach the installer and repairs the
same latent bug for SPOOF_BUILD.
Verified on a 26.4 JB VM: FORCE_DSC_MAXSLIDE=1 yields on-disk maxSlide=0
and a live shared-cache slide of 0x0 (dyld maps the cache at its
preferred base 0x180000000 in rpcserver_ios), versus the nonzero slide a
stock 26.4 boot picks.
Docs + research/0_binary_patch_comparison.md updated (README and the ja/ko/zh
translations).
Co-Authored-By: Claude Fable 5 <[email protected]>
The JB blanket-neuters Sandbox mpo_vnode_check_open (ops[267] -> allow) so
processes can read /var/jb. FileProvider's fpfs parent-walk relies on the stock
check's EACCES at the domain-container boundary as its terminus; with it
neutered the walk climbs unbounded and ResolverService balloons (~12 GB) ->
vm-compressor-space-shortage jetsam -> backboardd killed -> respring
(27b4/24A5390f).
Keep the global bypass, enforce the real check for the FileProvider daemons
only. On iOS 27, ops[267] is left un-neutered (removed from the JB-09 blanket
list) and retargeted to a code-cave trampoline: inline current_proc
(tpidr_el1 -> uthread+0x3F0 -> proc+0x18), read p_comm (+0x56C), and for
ResolverService/fileproviderd branch to the real vnode_check_open, else return
allow. Register-only, no frame/call. Gated to a 27.x base via applyIOS27; other
bases keep the blanket neuter.
Verified: cave disasm + branch targets correct (final b -> real
vnode_check_open); 26.5 kernelcache byte-identical pre-vs-post (ops[267] stays
neutered there). Documented as JB-29 in research/0_binary_patch_comparison.md.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YNySXhjsxhFG9yZMBHJUqY
iOS 27 XPC servers pin a Lightweight Code Requirement on their listener
(xpc_connection_set_peer_lightweight_code_requirement / the Swift
XPCPeerRequirement.hasEntitlement wrapper). Creating one self-checks via
libxpc _xpc_token_satisfies_lwcr, whose matcher returns a "matched" bool
plus a match_result.error_code and then hard-asserts they agree
(matched == (error_code == AICMR_MATCH), where AICMR_MATCH == 0) via
_os_crash_msg -> brk #1.
Under our JB code-signing environment the matcher writes error_code =
MATCH(0) but returns a failure status, producing the forbidden
(matched=0, error_code=0) pair, so libxpc aborts. Every daemon that pins
an entitlement peer-requirement at startup crash-loops continuously from
boot: intelligencetasksd, searchpartyd, transparencyd, bluetoothd, ...
Fix (cfw_patch_xpc_lwcr.py, 27.*-gated in cfw_install.sh, self-gating on
the symbol): derive "matched" from error_code and drop the abort. The
three instructions "cset w8,ne; eor w8,w0,w8; tbz w8,#0,<abort>" become
"cset w0,eq; nop; nop". The function then returns (error_code == 0),
reproducing stock behavior when the two agree and resolving the
contradiction toward "satisfied" when error_code says MATCH; genuine
allow/deny (error_code != 0) is unchanged. Symbol resolved from the DSC
.symbols table, site located by control-flow shape (Capstone),
replacements from Keystone, modified 16 KiB page re-attested
(cfw_dsc_codesign; CDHash change accepted by the JB AMFI cdhash-trust
patch).
Validated on 17,3_27.0_24A5390f + cloudOS 26.4 (JB, host-mount deploy):
the four crash-loopers disappear from the crash census after boot;
launchd (which links libxpc) boots clean past first unlock.
Co-Authored-By: Claude Fable 5 <[email protected]>