Commit Graph
100 Commits
Author SHA1 Message Date
zqxwceandClaude Opus 4.8 f48fc29a27 cfw: Patch os_lockdown_mode_enabled to not crash on iOS 27b5
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
2026-08-11 14:50:22 +03:00
zqxwceandClaude Opus 4.8 ab456ac67e cfw: Apply the libxpc LWCR patch on iOS 27 (correct mangled symbol)
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
2026-08-11 14:50:22 +03:00
zqxwceandClaude Opus 4.8 6d4855dca3 catalog: Add iOS 27 beta 5 (24A5408d) firmware pairing
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
2026-08-11 14:50:22 +03:00
zqxwce 545fd35e0f deps: Add cmake
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.
2026-08-05 18:24:56 +03:00
zqxwceandClaude Fable 5 5d22d3ed23 vphone-cli: Verify and repair libkeystone in the managed venv
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]>
2026-08-05 18:24:56 +03:00
zqxwceandClaude Fable 5 0491a9521a vphone-cli: Stream CFW install log live under --root-popup
`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]>
2026-08-05 17:33:03 +03:00
zqxwceandClaude Fable 5 8de3c1c5c2 vphone-cli: Show fw prepare output during vm create
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]>
2026-08-05 17:01:19 +03:00
zqxwceandClaude Fable 5 d405b12d64 vphone-cli: Add --headless boot option
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]>
2026-08-05 16:47:34 +03:00
zqxwce 14e48bcf4b requirements: Add missing setuptools 2026-08-05 16:10:06 +03:00
zqxwceandClaude Fable 5 1ceef58106 vphone-cli: Add --disk-size to vm create
`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]>
2026-08-03 14:03:52 +03:00
zqxwceandClaude Fable 5 4397dc7d49 vphone-cli: Add fw catalog with recommended pairings and --json
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]>
2026-08-03 11:55:02 +03:00
zqxwceandClaude Fable 5 d37c06b61d vphone-cli: Add network to vm info/vm list JSON
`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]>
2026-08-03 11:35:34 +03:00
zqxwceandClaude Fable 5 aeba01f13c vphone-cli: Add variant, udid, and device to vm info
`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]>
2026-08-03 11:19:21 +03:00
zqxwceandClaude Fable 5 c6fa19efcb networking: Add vm config --network to edit VM network mode after creation
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]>
2026-08-02 13:13:50 +03:00
zqxwceandClaude Opus 4.8 6aef60bd9a vphone-cli: Add --root-popup to elevate CFW host-mount via macOS auth dialog
* 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]>
2026-08-02 11:20:22 +03:00
zqxwceandClaude Fable 5 744090f694 setup: Remove artifacts when done to save disk space
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]>
2026-07-30 15:06:45 +03:00
zqxwceandClaude Fable 5 966bddb62a setup: Record iOS + cloudOS versions on restore
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]>
2026-07-30 13:27:18 +03:00
zqxwceandClaude Opus 4.8 ecf798e690 docs: streamline README and re-sync ko/ja/zh translations
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
2026-07-28 18:04:45 +03:00
zqxwceandClaude Opus 4.8 8b6ffd2e26 fix: bundle vphone-amfidont in Resources, not MacOS (unbreaks .app signing)
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
2026-07-28 17:26:26 +03:00
zqxwceandClaude Opus 4.8 766d1555fb feat: add vphone-amfidont helper to allow the .app through amfid
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
2026-07-28 16:59:26 +03:00
zqxwceandClaude Opus 4.8 e490a2aaf8 feat(vm create): prompt for firmware pairing when a source is omitted
`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
2026-07-28 16:03:51 +03:00
zqxwce 084e239c36 fix: resolve all paths correctly for the bundled / brew-installed app
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.
2026-07-28 15:21:29 +03:00
zqxwceandClaude Opus 4.8 70ff75067a fix: preflight checks the running binary, not a dev .build path
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]>
2026-07-28 14:48:19 +03:00
zqxwceandClaude Opus 4.8 f79c897dd1 fix: run the CFW-install sudo as a foreground terminal job
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
2026-07-28 14:25:56 +03:00
zqxwceandClaude Opus 4.8 8ecab43818 ci: build the portable .app in the release workflow
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
2026-07-28 13:00:58 +03:00
zqxwceandClaude Opus 4.8 d5283b4186 docs: rewrite README make-free + re-translate ko/ja/zh
- 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
2026-07-27 19:46:27 +03:00
zqxwceandClaude Opus 4.8 37ee2943c6 feat: guest restore/CFW pipeline updates for the consolidated CLI
- pymobiledevice3_bridge.py: colorized restore logs (coloredlogs.install,
  mirroring pmd3's own CLI) gated by a -v count
- cfw_install*.sh / fw_prepare.sh honor VPHONE_PYTHON/IPSW_DIR/VPHONE_SEAL_DIR
  and forward SPOOF_BUILD / FORCE_DSC_MAXSLIDE from the environment
- patch_camera_userland.sh / patch_hv_vmm_userland.sh adjustments

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
2026-07-27 19:46:27 +03:00
zqxwceandClaude Opus 4.8 c5300d0377 feat: standalone .app bundling + versioned resource resolution
- 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
2026-07-27 19:46:27 +03:00
zqxwceandClaude Opus 4.8 cce7898494 feat: drive the whole workflow through the vphone-cli binary
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
2026-07-27 19:46:27 +03:00
zqxwceandClaude Opus 4.8 828be6dbc6 feat: add VPhoneCore library — VM bundle model + host primitives
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
2026-07-27 19:46:27 +03:00
31772b3818 cfw: Add opt-in --force-exc-guard for the EXC_GUARD Mach-port-guard patch
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]>
2026-07-26 08:57:27 +03:00
zqxwceandClaude Fable 5 c9ad3c7519 cfw: dsc: Add FORCE_DSC_MAXSLIDE opt-in to zero maxSlide on non-27 bases
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]>
2026-07-23 17:46:40 +03:00
zqxwceandClaude Opus 4.8 fac586a95d cfw: jb: Fix iOS 27 random resprings (FileProvider-scoped vnode_check_open)
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
2026-07-23 16:50:41 +03:00
zqxwceandClaude Fable 5 0827e0ad53 cfw: jb: Fix iOS 27 daemon crash-loop (libxpc LWCR self-check abort)
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]>
2026-07-23 16:50:41 +03:00
zqxwceandClaude Opus 4.8 decda02ab4 cfw: jb/exp: Fix iOS 27 Campo crash-loop (sandbox mach-lookup exceptions)
iOS 27's Campo (wallpaper renderer) runs under the temporary-sandbox profile
(its own com.apple.private.sandbox.profile:embedded + no-container). On the 26.4
vphone600 kernel that builtin profile predates 27 and denies the backboard/
frontboard launch mach-services, so BKSDisplayServicesStart and then
+[BKSHIDEventDeliveryManager sharedInstance] fail their mach-lookups, log
"backboardd isn't running -- or we couldn't talk to it", and brk ~34ms after
launch -> continuous crash-loop, no wallpaper. JB-02d's container-upcall
force-success stops the exec-time autobox kill but does not grant these services.

Grant them through Campo's own com.apple.security.exception.mach-lookup.global-name
array (the sanctioned escape hatch, honored by temporary-sandbox -- Campo already
ships ~20 such exceptions; these launch services just aren't among them because
27's profile allows them directly). Applied at host-mount build time as step
JB-3b in cfw_install_jb.sh / cfw_install_exp.sh, re-signed with signcert.p12 via
ldid_sign_ent. The service list is merged by the external helper
scripts/patchers/campo_mach_lookup_exceptions.py (plistlib, not plutil -- the
entitlement key's dots would break plutil keypaths).

Hard-gated to 27.* on the mounted rootfs SystemVersion.plist (same gate as the
vpregister/DSC patches): skipped on 26.x/18.x, which don't need it and where
Campo.app also exists.

Verified on-device (17,3_27.0_24A5390f + cloudOS 26.4, JB): Campo launches and
stays up, no BKSDisplayServicesStart/BKSHIDEventDeliveryManager trap. Documented
as entry #14 in research/0_binary_patch_comparison.md.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_014McnrLRb5cZsTrpcBkvkj3
2026-07-23 16:50:41 +03:00
zqxwce 80f631a0bb doc: Add 27b4 and 26.6RC tested envs 2026-07-21 10:50:41 +03:00
zqxwce 5db02d4c42 doc: Remove iOS 26 specification from README 2026-07-20 16:23:50 +03:00
zqxwceandClaude Opus 4.8 3e815dccde docs: Add iOS 27.0 (24A5380h) to tested environments + refresh demo
Add the 27.0 / `17,3_27.0_24A5380h` / `26.4-23E5207q` row to the
tested-environments table across README.md and the ja/ko/zh translations,
and update docs/demo.jpeg.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
2026-07-20 16:20:34 +03:00
zqxwceandClaude Opus 4.8 9becab4031 cfw: Fix iOS 27 DDI (/System/Developer) auto-mount
Productionize the 3-part fix that makes `pymobiledevice3 mounter auto-mount`
land the personalized DDI at /System/Developer on the iOS-27-userland /
26.4-vphone600-kernel (c0ecdb4b) JB hybrid.

- KernelJBPatchDiskImages2.swift (new; wired into KernelJBPatcher.findAll):
  DiskImages2 ABI acceptance. GATE1/GATE2b NOP the CreateDevice/Connect
  cmp#9/b.ne ABI-version rejects (kernel driver v9 vs iOS-27 controller/daemon
  v11). GATE2 widens the RegisterNotificationPort backing array + both
  bound-check field loads (all-or-nothing) to clear the `type < getMaxPorts`
  off-by-one that otherwise fails the attach ("Can't register notification
  port").
- KernelJBPatchSandboxExtended: retarget mac_policy_ops[124]
  (mpo_proc_check_syscall_unix) to the allow stub so MobileStorageMounter's
  mount_apfs can make the mount(2) syscall (unix 167) — else the kernel
  Sandbox denies it ("Protobox: mount_apfs deny(1) syscall-unix 167").
- cfw_patch_diskimagesiod.py + cfw.py + cfw_install.sh (gated to 27.*):
  force -[DIDiskArb isMountCompleteWithExpectedCount:diskTracker:] -> YES so
  MobileStorageMounter's waitForDAMount returns and it performs the real
  nobrowse mount.

GATE2a anchor fix (the critical one): Capstone on this toolchain decodes the
AllocPortsArray size-shift lsl-immediate (a UBFM alias) as 2 operands, not the
xd,xn,#imm 3-operand shape. The original 3-operand + imm==3 match found 0, so
the all-or-nothing silently skipped GATE2 on every build (only manual dd pokes
ever worked). Now matched on mnemonic + destination x1 — the unique
size-writing lsl in AllocPortsArray; the replacement is a fixed mov x1,#0x4000
so the shift amount is irrelevant. Verified from a clean build on c0ecdb4b via
`patch-component --component kernel-jb --records-out`: all five di2 records
emit (createdevice, connect, allocports_size, notif_boundcheck_d8/e8), and
`pmd3 mounter auto-mount` -> rc=0 with the DDI mounted, no poke.

Documented in research/0_binary_patch_comparison.md (JB-09, JB-28, CFW
binary-patch #13).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
2026-07-20 16:20:34 +03:00
zqxwceandClaude Opus 4.8 de725d26aa cfw: Fix iOS 27 Sileo-at-setup — register JB apps via containerized LS API (vpregister)
On iOS 27, -[LSApplicationWorkspace registerApplicationDictionary:] (what the
first-boot `uicache -a` uses) is a deprecated no-op stub — lsd logs "You cannot
use -[LSApplicationWorkspace registerApplicationDictionary:] to register
applications anymore. These interfaces have been deprecated for years." and it
returns NO. So vphone_jb_setup.sh installs Sileo's .deb but never registers it:
the files land in /var/jb/Applications but Sileo never appears on the home
screen. There is no gate to patch here — Apple removed the implementation.

Fix: register JB apps via the modern containerized API instead.
- scripts/vpregister/vpregister.m: standalone helper that registers
  /var/jb/Applications/*.app (or given paths) via
  registerContainerizedApplicationWithInfoDictionaries:...:registrationError:,
  treating a nil error as success (it returns NO even when it registers). Works
  once lsd's embedded-reg gate is patched (cfw_patch_lsd_embedded_reg, applied
  by cfw_install.sh, which cfw_install_jb.sh and cfw_install_exp.sh both chain).
  It lives in its OWN dir (scripts/vpregister/), NOT scripts/vphoned/, so it is
  not swept up by the `scripts/vphoned/*.m` globs that build vphoned
  (cfw_install.sh, cfw_install_dev.sh, and the vphoned Makefile all glob that
  dir) — otherwise its main() collides with vphoned's ("duplicate symbol
  '_main'"). Built separately by the JB/EXP installers.
- cfw_install_jb.sh / cfw_install_exp.sh: build + sign (vphoned entitlements +
  CFW signcert) + deploy vpregister to /cores.
- vphone_jb_setup.sh: after dpkg + uicache -a, invoke /cores/vpregister to
  register JB apps via the containerized path. Guarded by [ -x ].

vpregister verified on 17,3_27.0_24A5380h + cloudOS 26.4 (JB): registers Sileo
via the containerized API (uicache -l 0->1) on the gate-patched VM. The exact
cfw_install.sh vphoned build (VPHONED_SRCS glob + clang) now links cleanly with
vpregister.m relocated. Doc: item 12 in research/0_binary_patch_comparison.md.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
2026-07-20 16:20:34 +03:00
zqxwceandClaude Opus 4.8 82485d988d cfw: Fix iOS 27 app registration — patch lsd embedded-reg gate + vphoned containerized fallback
iOS 27 lsd gates app (re)registration behind
-[_LSDModifyClient clientIsEntitledForEmbeddedRegistrationOperations], an
XPC-peer entitlement check for one of three privileged entitlements
(coreservices.lsaw / installcoordinationd.daemon /
coreservices.can-register-install-results). Missing -> NSOSStatusErrorDomain
-54 (permErr, LSDModifyService.mm:1639), so registerApplicationDictionary: and
registerContainerizedApplicationWithInfoDictionaries: fail and no app can
register — blocking vphoned's installer, TrollStore, and uicache/Sileo. The
entitlement route is unsatisfiable: LS registration is proxied, so the peer lsd
inspects is not the caller (even vphoned with all three in its validated csblob
is rejected).

Fix (two parts):
- cfw_patch_lsd_embedded_reg.py: NOP the gate's final cbz so the check always
  returns YES + re-attest the 16 KiB page (TXM per-page). Wired into cfw.py
  (patch-lsd-embedded-reg) and cfw_install.sh (after patch-dsc-maxslide);
  self-gates on pre-27 userlands (method absent). Method resolved via the DSC's
  own .symbols in-image table (ipsw symaddr/a2s time out on this cache), gate
  located by control-flow shape (cbz/cbnz w0 whose fall-through sets the
  mov w<reg>,#1 result), NOP bytes from Keystone. New CDHash accepted by the JB
  always-true AMFI cdhash-trust patch.
- vphoned vp_register_path: fall back from the (still-gated) plain
  registerApplicationDictionary: to the containerized registration API, which
  works once the gate is patched (treat a nil registrationError as success
  since it returns NO even when it registers).

Verified on 17,3_27.0_24A5380h + cloudOS 26.4 (JB): -54 gone, Sileo registers
(uicache -l 0->1), vphoned installs+registers a test IPA (com.vphone.vptest) to
/var/containers/Bundle/Application end-to-end; clean boot (re-attest correct).
Documented as item 12 in research/0_binary_patch_comparison.md.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
2026-07-20 16:20:34 +03:00
zqxwceandClaude Opus 4.8 43b8e75725 cfw: Make force-kern (patch-iomfb-force-kern) idempotent
Recognize entrypoints already rewritten to `b _kern_Swap*` by a prior run (first
insn is `b` targeting the kern address) and count them as `already` covered
instead of aborting. Previously a reinstall / re-patch of an already-forced DSC
raised "did not retarget required entrypoints" because the trampoline shape was
gone. Now: covered = forced | already; require the REQUIRED set to be a subset
of covered; re-attest only when something was newly written (skip cleanly when
all entrypoints are already forced). Enables the host-mount reinstall loop on an
already-patched VM. No behaviour change on a fresh DSC.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
2026-07-20 16:20:34 +03:00
zqxwceandClaude Opus 4.8 84f4887056 kernel: jb: Add container-manager exec-upcall force-success (iOS 27 wallpaper/temporary-sandbox fix)
The 26.4 vphone600 kernel resolves each process's containers at exec via a
synchronous MIG upcall to containermanagerd over the container-manager host
special port (HOST_CONTAINERD_PORT). iOS 27 DELETED this kernel-side upcall (the
stock 27 kernel has no HOST_CONTAINERD_PORT / CM_KERN_* protocol — container
resolution moved out of the kernel), so 27's containermanagerd no longer
implements the reply server. On the 26.4 kernel running a 27.0 userland the
upcall therefore fails (MACH_SEND_INVALID_DEST) for every platform app -> they
are autoboxed into the restrictive `temporary-sandbox` profile, which denies
mach-lookup com.apple.backboard.display.services -> Campo (the wallpaper
renderer) crash-loops (no wallpaper), plus intelligencetasksd / feedbackd.

Re-registering the host special port is a confirmed dead end: the 26.4 kernel
then SENDS the MIG request and BLOCKS for a reply 27 cannot produce -> early-boot
deadlock (SpringBoard never comes up).

Fix (patchContainerManagerUpcall): flip the `cbz w0,<success>` guard in
_hook_cred_label_update_execve (taken when the upcall returns 0) to an
unconditional `b <success>`, so a failed upcall takes the success path instead
of autobox/kill. Anchored structurally on the "failed to upcall to
containermanagerd" string xref (-> the cbz immediately before the string-load
adrp, preceded by the upcall bl, unique backward branch); replacement b from the
Keystone-backed ARM64Encoder. No-op-in-effect for version-matched userlands
(there the upcall succeeds, so the original cbz already branches to <success>).

- new: sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchContainerUpcall.swift
- KernelJBPatcher.findAll: call patchContainerManagerUpcall after
  patchExecSecurityPolicyKill
- cfw_install.sh: document the port-25 launchd-cache approach as a dead end
  (no active code)
- research/0_binary_patch_comparison.md: add row JB-02d

Validated on-device (iPhone17,3 27.0 24A5380h + cloudOS 26.4 c0ecdb4b, JB):
iOS 27 wallpaper renders, Campo/SpringBoard/backboardd stable, clean boot (no
freeze, no panic). Patcher-level (patch-component --component kernel-jb on the
pristine 26.4 kernelcache): finds 0x1AB0654 cbz w0 -> b 0x1ab0564, emits record
container_manager_upcall_force_success, byte-identical to the on-device-validated
host-poke.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
2026-07-20 16:20:34 +03:00
zqxwceandClaude Opus 4.8 9c1227f89b kernel: jb: Fix iOS 27 native VZ-view display (force the kern present path)
iOS 27 userland on the 26.4 vphone600 kernel booted with a working GUI over the
in-guest TrollVNC capturer, but the normal vphone-cli window (the host VZ view)
stayed black. Root cause, proven at the instruction level in the 27.0 DSC
IOMobileFramebuffer: the host VZVirtualMachineView is fed by the guest
AppleParavirtGPU scanout, which the 26.4 kernel drives ONLY from the IOMFB
userclient SwapEnd (external method 5) -- the `_kern_Swap*` path that 26.x used
(and that the SwapEnd size patch fixed on 26.0/18.x). iOS 27 defaults the
paravirt display's present to IOMFB's parallel `_virt_Swap*` path: `_virt_SwapEnd`
performs no userclient call -- it invokes an in-process callback and hands the
composited IOSurface to a virtual-display consumer -- so frames never enter the
kernel userclient and the paravirt GPU never scans out (live AppleParavirtGPU
scheduler sat idle). TrollVNC still saw frames because it captures the composited
surface in-guest, independent of the paravirt scanout.

Fix = force the display's present back onto the kern/method-5 path, in two halves:

Userland (DSC), new patcher cfw_patch_iomfb_force_kern.py (cfw.py +
cfw_install.sh, 27-gated): the public `_IOMobileFramebufferSwap*` entrypoints are
thin dispatch trampolines (`cbz x0; ldr xN,[x0,#slot]; cbz xN; braaz xN`) that
tail-call a per-connection swap fp (kern or virt impl). Rewrite each trampoline's
first instruction to `b _kern_Swap<Name>` -- args untouched, so this is
behaviourally identical to the connection having selected the kern fp. Fully
dynamic: public + `_kern_` addrs resolved by name via `ipsw dyld symaddr`,
trampoline shape verified by Capstone, branch bytes from the Keystone `asm_at()`
helper, modified DSC code pages re-attested. Runtime: 31 entrypoints retargeted,
4 non-trampoline setters left on virt, required {SwapBegin,SwapEnd,SwapSetLayer}
present.

Kernel (KernelJBPatchIomfbSwap, re-enabled in KernelJBPatcher.findAll): iOS 27's
native SwapEnd struct is 0x6e0 bytes (26.x sent 0x588) and the 26.4 userclient
exact-checks 0x588 in two places, so method 5 would return kIOReturnBadArgument.
patchIomfbSwapEndVariableSize flips the dispatch-table checkStructureInputSize
0x588 -> kIOUCVariableStructureSize; patchIomfbSwapEndHandlerSize retargets the
handler's internal `cmp w2,#0x588` -> #0x6e0. 27's IOMFBSwapRec prefix matches
26.x, so the paravirt swap handler reads valid fields. patchParavirtDisplayPrimary
stays disabled (wrong theory, harmful -- see JB-02c).

Validated on-device (17,3_27.0_24A5380h + cloudOS 26.4 c0ecdb4b, JB): clean boot,
no kIOReturnBadArgument / SwapEnd rejection / SECURITY_POLICY kill / panic, and
the iOS 27 userland now renders AND is interactive in the native VZ view.

research/0_binary_patch_comparison.md: DSC-patch item 9 corrected (27 no longer
size-truncated), new item 11 (force-kern), JB-26/27 rows added, all marked
validated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
2026-07-20 16:20:34 +03:00
zqxwceandClaude Opus 4.8 c22da90929 kernel: jb: Fix iOS 27 guest display via IOUC sandbox gate bypass
iOS 27 userland on the 26.4 vphone600 kernel had SpringBoard crash-looping
with no display. Root cause: the IOKit user-client open path runs TWO
independent MAC gates -- a MACF-aggregator check (already patched by
patchIoucFailedMacf) and a separate Sandbox check -- and the Sandbox gate
spuriously DENIES the render server (backboardd) its opens of
IOMobileFramebufferUserClient / IOSurfaceRootUserClient / IOHIDEventService.
27-specific: absent on a native 26.4 userland. Denied the framebuffer,
backboardd can't present (no Apple logo) and vends no main display, so
FBSDisplayMonitor asserts on a nil mainDisplay -> crash-loop.

Add patchIoucFailedSandbox (KernelJBPatchIoucSandbox.swift), mirroring
patchIoucFailedMacf: anchor on the "IOUC %s failed sandbox in process %s"
string, find the deny block (the CBNZ target enclosing the fail-log ADRP),
and rewrite its first instruction with an unconditional B to the
NotPermitted allow-proceed target. Structural anchors only, no hardcoded
offsets. No-op where the gate already allows (native 26.x userlands).

Verified: backboardd opens the framebuffer, SpringBoard runs (0 crashes),
display enumerates (LCD/primary), [CADisplay mainDisplay] resolves, and the
guest GUI renders (confirmed visible over VNC).

Disable three earlier wrong-theory display patches (kept in-tree, off in
findAll, for the record):
 - patchParavirtDisplayPrimary: setting primary=1 is actively HARMFUL on 27
   -- it becomes the display NAME suffix ("primary-1") and breaks the render
   server's exact-name match.
 - patchIomfbSwapEnd{VariableSize,HandlerSize}: iOS 27 never uses IOMFB
   method 5 (SwapEnd) for present -- confirmed by kernel trace of the real
   handler (cmp w2,#0x588) with SpringBoard actively presenting -- so these
   are irrelevant on 27 and would break 26.x's native 0x588 SwapEnd.

SwapEnd userland DSC size patch made per-base (cfw_install*, cfw.py,
cfw_patch_iomfb_swapend.py): 26.x validated, harmless on 27.

research/0_binary_patch_comparison.md updated (JB-10b added for the sandbox
gate; JB-02c corrected to disabled/wrong-theory).

Still open: the normal (VZ) render path stays black even though the guest
presents (VNC works) -- 27 uses a present mechanism the 26.4 paravirt-GPU
path doesn't receive. Tracked separately for follow-up.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-20 16:20:34 +03:00
zqxwceandClaude Opus 4.8 679d3d0476 kernel: jb: Bypass exec ip_mac_return SECURITY_POLICY kill for newer userlands
Running iOS 27.0 userland on the 26.4 vphone600 kernel, every core platform
daemon (backboardd, cfprefsd, containermanagerd, locationd, CommCenter, ...)
died 3-5ms after xpcproxy spawn with exit reason namespace 9 / code 0x8
(OS_REASON_EXEC / EXEC_EXIT_REASON_SECURITY_POLICY). launchd throttled the
respawns and the boot deadlocked (all CPUs idle) before SpringBoard — no UI,
no networking. Root cause: AMFI's exec MAC hooks reject the 27.0 binaries'
code-sign validation category on the 26.4 kernel, setting imgp->ip_mac_return,
and XNU's exec path (kern_exec.c) SIGKILLs on `if (imgp->ip_mac_return != 0)`.

patch_exec_security_policy_kill flips the `cbz wN, <skip>` guard preceding the
os_reason_create(9,8) call to an unconditional `b <skip>`, making the kill block
unreachable — downstream of AMFI/TXM, so it covers the validation-category
reject regardless of which hook set the verdict. Anchored structurally (movz
w0,#9 ; movz w1,#8 preceded by ldr wN,[xM,#imm] ; cbz wN,<fwd>; W-register cbz
distinguishes the ip_mac_return site from the subsystem-root sibling). No-op in
effect for version-matched userlands (ip_mac_return == 0, so the cbz already
skips). Wired into the JB Group C dispatcher.

Validated on iPhone17,3_27.0_24A5380h + cloudOS 26.4 (c0ecdb4b): 0 SECURITY_POLICY
kills, core daemons launch, networking + SSH (dropbear :22222) come up. Remaining
gate: sandbox denies backboardd the IOMobileFramebuffer/IOSurface user clients,
so SpringBoard traps in FBSDisplayMonitor init (no display) -> UI not up yet.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
2026-07-20 16:20:34 +03:00
zqxwceandClaude Opus 4.8 a38fd3201f cfw: Fit iOS 27 dyld cache in the 26.x kernel shared region (maxSlide=0)
iOS 27.0's dyld shared cache (~5.95 GiB span) plus its 512 MiB header maxSlide
overflows the vphone600 26.x kernel's fixed 6 GiB shared region
(SHARED_REGION_SIZE_ARM64 = 0x180000000). At map time the kernel reserves
span + maxSlide, so _shared_region_map_and_slide returns ENOMEM, dyld cannot map
libSystem, and launchd (pid 1) panics at boot ("initproc failed to start").

Add cfw_patch_dsc_maxslide: zero the dyld_cache_header maxSlide (@0xF0) in the
main chunk when span + maxSlide exceeds the region, so the cache maps at slide 0
(iOS 27.0 fits with ~58 MiB spare). Self-gating (no-op for 26.x/18.x, which fit
with full slide) and no page re-attestation (header metadata, not a cs_validate'd
code page). Wired into cfw_install.sh after the IOMFB SwapEnd gate, so it applies
to regular/jb/exp (jb/exp run cfw_install.sh as their base).

Validated on iPhone17,3_27.0_24A5380h userland + cloudOS 26.4 (c0ecdb4b) JB:
dyld cache maps system-wide, launchd reaches first unlock, vphoned connects as
iOS 27.0.0, 0 panics.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
2026-07-20 16:20:34 +03:00
zqxwceandClaude Opus 4.8 f508d1db4e ci: add release workflow to build and attach signed vphone-cli.app
On a published GitHub Release, build vphone-cli.app on macos-26 via
`make bundle` (ad-hoc codesign + sources/vphone.entitlements, matching a
local build), verify the private virtualization entitlement is embedded,
then zip and upload the app as a release asset.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 16:38:49 +03:00
zqxwceandClaude Opus 4.8 0493adc068 docs: add iOS 18.6.2 to tested environments + Metal caveat
Sync the localized READMEs (ja/ko/zh) tested-environment tables to match
README.md: add the Mac16,11 27.0b2 / 17,3_18.6.2_22G100 / 26.1-23B85 row
and drop the redundant Mac16,8 26.5.1 / 17,3_26.1_23B85 entry.

Note in all four READMEs that GPU/Metal acceleration does not work on
iOS 18.x -- the 18.x Metal/IOGPU framework has no paravirtualized GPU
implementation, so Metal-rendered content (web, images, wallpaper) does
not render; touch, networking, and apps work.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q7gbWrtKLu8rFGmpXHmu9t
2026-07-13 15:40:44 +03:00
zqxwceandClaude Opus 4.8 8781e20c76 cfw: iBoot: disable skywalk fsw netagents on iOS 18 (if_attach_nx=0x3)
iOS 18.6.2 userland on the 26.1 vphone600 kernel has no working DNS: the
guest gets a correct resolver config (DHCP hands out 192.168.64.1, which
configd propagates to State:/Network/Global/DNS) and the path works (raw
UDP DNS to the gateway succeeds), but getaddrinfo fails EAI_NONAME because
mDNSResponder crash-loops. Every crash is identical: EXC_BREAKPOINT in
os_channel_create_extended, hit from Network.framework flow setup
(nw_channel_create_with_attributes). That is a skywalk userspace-channel
trap: the 26.1 kernel advertises skywalk, Network.framework tries to create
a flowswitch channel for its DNS flow, the channel-create syscall trips a
fatal trap in the 18.6.2 libsystem_kernel, and the resolver daemon dies.
26.x userland handles this path; 18.6.2 does not.

Fix: set boot-arg if_attach_nx=0x3 (SKYWALK_NETWORKING_BSD_ONLY =
IF_ATTACH_NX_NETIF_COMPAT | IF_ATTACH_NX_FLOWSWITCH) on iOS 18 bases. This
keeps the netif + flowswitch nexus (interface/host stack unaffected) but
leaves the FSW_TRANSPORT/IP netagents unset, so Network.framework uses the
BSD-socket path and mDNSResponder never creates the crashing channel. The
kernel boot-args come from the patched iBoot (kern.bootargs matches
IBootPatcher.bootArgs), so the arg is baked into the iBEC/LLB boot-args
patch, not the host NVRAM (which iBoot overrides).

- IBootPatcher gains `extraBootArgs`, inserted before the trailing %s in
  the patched boot-args string (ibec/llb).
- FirmwarePipeline sets extraBootArgs="if_attach_nx=0x3" for iOS 18 bases
  (iosBaseIs18), empty otherwise, on the iBEC and LLB factories.

Gated to iOS 18 bases: 26.x keeps the stock boot-args and is untouched.
Validated at runtime first (setting net.link.generic.system.enable_netagent
=0 makes DNS resolve reliably), then confirmed end-to-end on a fresh
17,3_18.6.2_22G100 jb restore: kern.bootargs shows if_attach_nx=0x3 and
DNS/networking works.

Documented in research/0_binary_patch_comparison.md (iBEC/LLB boot-args row).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q7gbWrtKLu8rFGmpXHmu9t
2026-07-13 15:40:44 +03:00
zqxwceandClaude Opus 4.8 b90f699583 vphone-cli: guest-side touch injection for iOS 18 bases
iOS 18.x userland on the 26.1 vphone600 kernel has no working touch: the
VZ USB touchscreen dext receives input reports (InputReportCount climbs)
but emits zero digitizer events on the 26.1 kernel, so backboardd's event
queue stays empty and the UI never sees touches. The identical device on
a 26.x base produces digitizer events normally, so it is a guest-side
dext<->kernel report->event ABI break, not a host or descriptor issue.

Fix: inject digitizer events guest-side via vphoned, bypassing the broken
dext -- the same IOHIDEventSystemClientDispatchEvent path vphoned already
uses for the Home key. vp_hid_touch() builds a Hand parent + digitizer
finger child event (display-integrated) and dispatches it.

- vphoned_hid.{h,m}: vp_hid_touch(phase,x,y) + digitizer dlsyms.
- vphoned.m: "touch" command; hello now reports the guest iOS version and
  a "touch" capability.
- VPhoneControl: sendTouch(), guestIOSVersion, and useGuestTouchInjection
  (connected && caps has "touch" && iOS major < 26).
- VPhoneVirtualMachineView.sendTouchEvent: routes to vphoned when
  useGuestTouchInjection, else the native VZ USB multitouch path.

Gated to iOS 18 bases: 26.x guests report major >= 26, so the gate is
false and touch uses the unchanged native USB path (no regression). The
new vphoned code is compiled into all builds but stays inert on 26.x.
Ships via the existing vphoned hash-mismatch auto-update; no re-restore.

Verified on 17,3_18.6.2_22G100: socket tap -> correct digitizer event
(coordinates match), swipe unlocks to home, tap on the Settings icon
launches Settings.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q7gbWrtKLu8rFGmpXHmu9t
2026-07-13 15:40:44 +03:00
zqxwceandClaude Fable 5 911152881e kernel: apply EXC_GUARD (Mach port guard) disable for iOS 18 bases
iOS 18.6.2 userland on the 26.1 vphone600 kernel crash-loops
runningboardd and SpringBoard, so the UI never takes over from the boot
logo. Crash reports show EXC_GUARD / GUARD_TYPE_MACH_PORT "flavor 10":
the 26.1 kernel fatally enforces a Mach port guard that iOS 18's older
userland trips (hardening the 18.x daemons predate).

The existing patchExcGuardBehavior() (kernel patch 27,
thread_guard_violation -> RET) fixes exactly this but was gated to the
dev variant only. Wire it to also apply on regular/jb/exp when the
iPhone base is iOS 18:

- FirmwarePipeline reads the base ProductVersion from
  iPhone-BuildManifest.plist (fw_prepare preserves it; the live
  BuildManifest reads the cloudOS 26.1 version, not the base) and sets
  applyExcGuard for 18.* bases.
- KernelPatcher gains an applyExcGuard flag; patch 27 now runs when
  isDev || applyExcGuard.

Scoped to iOS 18 bases only — 26.x bases boot without it, never set
applyExcGuard, and are unaffected. Validated on 17,3_18.6.2_22G100:
fw_patch_jb logs "iPhone base iOS: 18.6.2 (enabling ...)" and applies
thread_guard_violation -> RET at foff 0xB4FFCC. With this plus the
display SwapEnd fix, 18.6.2 boots to the Setup Assistant UI with
runningboardd and SpringBoard stable.

The keystore AppleSEPKeyStore sel-135 UnsupportedMode seen during
bring-up turned out to be non-fatal (tolerated by the CredentialManager)
and is not needed for boot, so no keystore patch is included.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-13 15:40:44 +03:00
zqxwceandClaude Fable 5 a67e0f3939 cfw: extend IOMobileFramebuffer SwapEnd display fix to iOS 18.x
iOS 18.6.2 userland on the 26.1 vphone600 kernel hits the same
IOMobileFramebuffer SwapEnd ABI mismatch as 26.0/26.0.1: userland sends
a smaller external-method-5 SwapEnd state than the 0x560 the userclient
expects, so SwapEnd returns kIOReturnBadArgument and the host VZ display
stays black (the guest still renders — the Apple logo is visible over
VNC, just not in the vphone-cli view). 18.6.2 sends 0x514 (26.0 sent
0x548).

cfw_patch_iomfb_swapend is already semantic + idempotent (it discovers
the source immediate and rewrites it to 0x560), so no patcher change is
needed — only the install-time gate. Extend the gate in cfw_install.sh
and cfw_install_dev.sh to fire when ProductVersion starts with 18. as
well as 26.0.

Scoped to those versions only: 26.1/26.3/26.4/26.5 match neither branch
of the gate and are unaffected. Document the widened scope in
research/0_binary_patch_comparison.md (patch row 9).

Part of ongoing iPhone17,3 18.6.2 + cloudOS 26.1 bring-up. Validated on
17,3_18.6.2_22G100: the Apple logo now renders in the vphone-cli view.
Full boot to the UI additionally needs two kernelcache fixes (keystore
sel-135 force-success and the mach-port EXC_GUARD disable) that are
validated at runtime but not yet folded into KernelPatcher — those will
be gated to iOS 18 bases so the working 26.x variants stay untouched.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-13 15:40:44 +03:00
zqxwce aa39b7194d cfw: patchers: Make iomfb patch dynamic instead of hard coded bytes 2026-07-08 11:28:36 +03:00
zqxwceandClaude Opus 4.8 33072cf954 cfw/jb: Add dynamic deb download + install to machine setup
Fetch debs listed in debs.list into a cached debs/ dir (skipping already
-cached files, honoring manually-added ones), stage the whole cache into
the per-boot preboot dir alongside Sileo, and install on first boot.

- fetch_debs.sh: manifest fetch, atomic download, non-fatal on failure,
  chowns cache back to invoking user under sudo
- cfw_install_jb.sh / cfw_install_exp.sh: fetch + stage into $BOOT_HASH/debs
- vphone_jb_setup.sh (5b): idempotent install — skip packages already at
  >= the staged version, install the rest in one dpkg -i so inter-package
  deps resolve in a single pass
- debs.list tracked manifest; debs/ cache gitignored

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 21:41:55 +03:00
zqxwceandClaude Opus 4.8 fcc30e1657 general: Remove SSH-ramdisk CFW install path; host-mount is the sole flow
The ramdisk-based install (build/send an SSH ramdisk, iproxy-forward, then
push CFW files over SSH and flip the boot snapshot with snaputil in-VM) has
been fully replaced by the host-mount path: cfw_install_host.sh mounts the
VM's Disk.img on the host, places every file locally, and flips the boot
snapshot offline via tools/apfs_snap_rename.py. This removes all remaining
ramdisk generation, delivery, and usage — no legacy fallback.

Deleted:
  - scripts/ramdisk_build.py, scripts/ramdisk_send.sh
Renamed:
  - scripts/cfw_host_mode.sh -> scripts/cfw_transport.sh (was a conditional
    "host-mode override"; now the sole, unconditionally-sourced transport)

setup_machine.sh: drop the USE_RAMDISK_CFW=1 branch and every iproxy/ramdisk
helper (usbmux UDID resolution, port picking, start/stop iproxy, wait-for-
ramdisk-ssh), all RAMDISK_*/IPROXY_* vars, the cleanup() iproxy handling, and
the orphaned cfw_install_target var. Only the host-mount cfw_install_host call
remains.

cfw_install{,_dev,_jb,_exp}.sh: delete the SSH transport (SSH_* vars, SSH_OPTS,
sshpass prereq/_sshpass/_ssh_retry, ssh_cmd/scp_to/scp_from/remote_file_exists/
remote_mount, wait_for_device_ssh_ready) and the dead ramdisk-mechanism body
blocks (snaputil snapshot flip, dropbearkey host-key pre-generation, halt-over-
SSH, CFW_SKIP_HALT). The transport is now sourced unconditionally from
cfw_transport.sh. dropbear -R generates host keys at first boot; the offline
apfs_snap_rename.py does the boot-source flip. Dropped the vestigial
CFW_HOST_MODE gate.

Also: pymobiledevice3_bridge.py (ramdisk-send command already gone), Makefile
(ramdisk targets/help/IRECOVERY_ECID removed), README + ja/ko/zh (install flow
rewritten to host-mount), AGENTS.md/CLAUDE.md architecture tree, and stale
comments in vphone_jb_setup.sh, VPhoneCLI.swift, apfs_snap_rename.py,
cfw_patch_post_restore_dt.py.

Verified booting via make setup_machine.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XZVS9oqVNmHJzpb8mFtKJx
2026-07-06 15:15:01 +03:00
zqxwceandClaude Opus 4.8 321c7117b3 setup_machine: Set ramdisk-free host-mount CFW install as default
Replace the "Ramdisk + CFW phase" (boot_dfu + ramdisk_build/send + iproxy
+ wait_ssh + cfw_install*) with cfw_install_host run after the restore
phase, while the VM is off. The legacy ramdisk path is kept behind
USE_RAMDISK_CFW=1.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XZVS9oqVNmHJzpb8mFtKJx
2026-07-06 15:15:01 +03:00
zqxwceandClaude Opus 4.8 d28754e89e cfw: Implement ramdisk-free host-mount CFW install
Add a host-mount install path that replaces the DFU + ramdisk_send +
iproxy + SSH transport: mount the VM's Disk.img volumes on the host, place
every CFW file locally, then flip the boot snapshot offline
(tools/apfs_snap_rename.py). The resulting VM is identical to the
ramdisk-installed one, minus the round-trips.

- cfw_host_mode.sh: sourced by cfw_install*.sh when CFW_HOST_MODE=1;
  overrides ssh_cmd/scp_to/scp_from/remote_mount/remote_file_exists to run
  locally against volumes mounted at $CFW_HOST_MNT/mnt{1,3,5} (host / is
  read-only, so device /mntN tokens are remapped), routes /usr/bin/tar to
  gtar (bsdtar lacks the GNU flags), and skips snaputil/dropbearkey/halt.
- cfw_install{,_dev,_jb,_exp}.sh: source the shim after their helper defs
  (host-mode is opt-in; the SSH path is unchanged). Also reword progress
  messages that assumed the ramdisk/SSH flow ("~3 minutes" scp, "to
  device", "Reboot the device") to read correctly in both modes.
- cfw_install_host.sh + `make cfw_install_host VARIANT=regular|dev|jb|exp`:
  attach the image, run the chosen installer under CFW_HOST_MODE as root,
  detach, and run the offline snapshot flip. Restores ownership of the
  host-side artifacts it creates (vm/.vphoned.signed, .cfw_temp, ...) to
  the invoking user afterward, so a later user-run `make boot` isn't
  blocked by root-owned files.

Runs as root (owners-honored mounts / chown / cp) via a sudo re-exec. No
authenticated-root/ARV change needed; mount_apfs -o rw honors owners.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XZVS9oqVNmHJzpb8mFtKJx
2026-07-06 15:15:01 +03:00
zqxwceandClaude Opus 4.8 b91f6f4575 tools: Add offline APFS root-snapshot rename (boot-source flip)
Renames the com.apple.os.update-<hash> system snapshot in place on a
powered-off Disk.img so the patched guest kernel boots the live volume
instead of the sealed snapshot -- same effect as in-VM snaputil, but done
offline on the host (no mount, no fs_snapshot syscall, no kernel/ARV gate,
no host security change).

The name lives in two b-tree records (snap_metadata value + snap_name
key), normally one leaf node; a same-length rename preserves name_len and
single-snapshot b-tree order, so only that block's APFS fletcher64
changes. Auto-detects the snapshot in valid b-tree blocks (ignores
identical strings baked into on-volume binaries); --dry-run, idempotent.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XZVS9oqVNmHJzpb8mFtKJx
2026-07-06 15:15:01 +03:00
zqxwceandClaude Fable 5 8d63bbc840 cfw/ramdisk: route hdiutil straight to sudo -A when SUDO_ASKPASS is set
When SUDO_PASSWORD is supplied, setup_machine.sh exports SUDO_ASKPASS.
Previously hdiutil ran unprivileged first (cfw_install.sh, ramdisk_build.py)
or via plain interactive sudo (cfw_install_dev/exp.sh), both of which
triggered a password prompt.

Now, when SUDO_ASKPASS is present, hdiutil goes straight to `sudo -A` so it
never runs unprivileged first and never prompts. When it is absent, every
call site keeps its original behavior verbatim.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-05 18:26:58 +03:00
zqxwceandClaude Fable 5 81b0cd828c [kernel-jb] disable vm_map_protect Shape B (crashes debugger on 26.4+)
The 26.5 Shape B W^X matcher (mov #5 -> #7) mis-targets vm_map.c:6202
`prot &= ~VM_PROT_WRITE` -- the copy-on-write-preservation write-strip --
instead of the RWX/ALLEXEC gate at vm_map.c:5997. Widening it keeps write on
code-page protections, so a debugger's breakpoint write lands a writable PTE on
the code frame; SPTM rejects that (VIOLATION_ILLEGAL_MAP, page_fte->type=0xf),
killing any debugged process on 26.4+.

Isolation confirmed: with cs_bypass (patch_vm_fault_enter_prepare) left enabled
and Shape B disabled, the debugger works -- Shape B was the sole cause.

Retired on 26.5+ (not retargeted): on SPTM, code modification uses write-then-flip
via vm_protect(VM_PROT_COPY) -> XNU_USER_DEBUG. The debugger, MobileSubstrate
tweaks, and the JB's own plugins (vcc_patch_two_nops in libvcamcaptured.m) all use
this path; none need non-MAP_JIT RWX. Shape A (26.1-26.4, the real ALLEXEC gate)
is unaffected.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-05 17:39:00 +03:00
zqxwceandClaude Fable 5 729c3b644c cfw_install: jb/exp: Flip launchd hook to opt-out via DISABLE_LAUNCHD_HOOK
Inject launchdhook into pid 1 by default again for JB/EXP; set
DISABLE_LAUNCHD_HOOK=1 to skip. Also fixes a doubled-prefix typo in
cfw_install_exp.sh that made the previous env flag unreadable there.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-05 15:09:22 +03:00
zqxwceandClaude Fable 5 421ff3ed23 Generate dropbear host keys with ramdisk's trustcached dropbearkey
The ramdisk-time host-key pre-seed added in c6ed3a1 invoked
/mnt1/iosbinpack64/usr/local/bin/dropbearkey, an iPhone-Distribution-signed
third-party binary. Inside the SSH ramdisk its cdhash is in no active trust
cache, so AMFI SIGKILLs it (exit 137), aborting cfw_install_jb/dev.

Use the ramdisk's own /usr/local/bin/dropbearkey (from ssh.tar.gz, re-signed
and trustcached at build time), which runs and writes fresh keys to the same
Data-volume target. The first-boot generator in vphone_jb_setup.sh is
unaffected — it runs in the JB'd kernel where the iosbinpack64 binary is valid.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-05 14:27:33 +03:00
zqxwceandClaude Opus 4.8 3e05c89826 setup: Default to non-interactive, add INTERACTIVE opt-in
setup_machine now runs non-interactive by default (auto-continue first-boot
stages + boot analysis). Pass INTERACTIVE=1 to restore the live prompts.
The old NON_INTERACTIVE external knob is dropped; it remains only as the
internal computed flag the first-boot prompts read.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-22 12:15:06 +03:00
zqxwceandClaude Opus 4.8 3b45ba2571 test: firmware_patches: Add full-pipeline cross-version firmware patch gate
test_jb_kernel_patches.sh only exercises the `kernel-jb` component, so it can't
see drift in the boot chain (iBSS/iBEC/LLB), the base KernelPatcher, TXM, or
DeviceTree — where real 26.x builds silently skipped sub-patches.

Add test_firmware_patches.sh (`make test_fw_patches`): runs the full
patch-firmware pipeline for the jb and exp variants over each locally-prepared
cloudOS firmware (assembled into a disposable VM dir; ipsws/ is never mutated)
and fails on any component that emits a `[-]` line. Because patchAll() only
throws on a zero-patch component, a partial skip never changes the exit code —
so the gate scans stdout for `[-]`.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-22 12:15:06 +03:00
zqxwceandClaude Opus 4.8 aa0000a65b KernelPatchApfsMount: Re-anchor handle_get_dev_by_role deny gates on stable strings
The patch-16 entitlement-gate matcher keyed on source line IDs
(`mov w8, #0x332D / #0x333B`); on 26.4 both the value and the register drifted,
so it found nothing and silently skipped (worked on 26.1/26.3).

Re-anchor the deny blocks on their stable panic strings ("This operation needs
entitlement" and "lookup takes place out of a volume group, but the source
volume is in one"), scoped to the routine by also requiring the
`handle_get_dev_by_role` function-name string — this excludes the adjacent
`handle_volume_class_keybag_op`, which shares the entitlement message. Yields the
same 3 gates on 26.1/26.3 (parity) and now 3 on 26.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-22 12:15:06 +03:00
zqxwceandClaude Opus 4.8 9808bd24b9 IBootPatcher: Retarget bootx-precondition and LLB rootfs size gate across cloudOS
Two pre-existing iBoot patches silently skipped on some cloudOS versions
(verified by cross-version disassembly of the vresearch101 iBEC/LLB payloads):

- bootx precondition (iBEC): the `BL <bit-getter>; TBZ w0,#0 -> panic` construct
  is a 26.4+ iBoot addition and is genuinely absent on 26.1/26.3 (which boot fine
  without it). Treat "construct not present" as an informational skip, not a
  failure; ambiguity (>1 gate) still hard-fails.

- LLB rootfs size gate: the bare `cmp x8, #0x400` is not unique — 26.4 LLB has
  three (only one is the size gate, the other two are followed by `b.hi`). Anchor
  on the `cmp x8,#0x400 ; b.hs` pair, unique on 26.1/26.3/26.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-22 12:15:06 +03:00
zqxwceandClaude Opus 4.7 44a69c0e9a camera: libvcamcaptured 26.x version-agnostic patches
The 26.5 implementation embedded multiple build-specific values:
  - a hardcoded byte offset 1056 to find `_sSourceList`
  - a hardcoded stack-frame offset (#576) in the per-source filter scan
  - hardcoded ivar offsets 0x08/0x18/0x10/0x18/0x48 on
    BWFigCaptureDevice / BWFigCaptureStream
  - hardcoded image VMAs 0x1ae6ff05c, 0x1ae2bd414, 0x1ae2c353c for
    three error-suppression byte-patches
  - two `#if 0` blocks pinning more 0x1ae* VMAs

Refactor every site to a runtime-resolved equivalent.

1. _sSourceList: structural ARM64 anchor chain rooted at the exported
   FigCaptureSourceServerStart symbol — every link is a stable pattern
   that survives DSC byte-offset shifts, stub-call layout changes, and
   LC_SYMTAB local-symbol stripping:

     FigCaptureSourceServerStart   (exported, retained on every build)
       walk for `cmn x?, #0x1 ; b.ne <wrapper>`     (onceToken check)
     wrapper                       (single-insn `bl <cold.1>` site)
     cold.1                        (static helper; 5-6 instructions)
       `adrp x1, ... ; add x1, x1, #imm`            (block-constant addr)
     block constant                (struct __Block_literal in __DATA_CONST)
       +0x10  = invoke pointer (PAC-stripped) = dispatch_once body
     init block-invoke
       walk for `bl <X> ; adrp + str x0, [Xn, #imm]` pairs
                                                    (each "store fn result
                                                     into a static global")
       pick the first slot whose stored value is a heap CFArray
                                                    (filters the lock-store
                                                     at #0 — that's a void*
                                                     mutex handle, not an
                                                     array)

   LC_SYMTAB is still consulted first as a fast deterministic path for
   builds that happen to retain `_sSourceList` as a regular nlist entry;
   the structural chain is what actually fires on stock 26.1/26.3.1/26.5
   DSCs (which strip static data symbols).

2. Per-source filter LDR x2 anchor: mask the imm12, accepting any
   sp-relative 64-bit load into x2 regardless of the compiler-chosen
   stack-frame slot.

3. BWFigCaptureDevice / BWFigCaptureStream ivar offsets: resolved at
   synth-class init via class_getInstanceVariable + ivar_getOffset on
   the parent class. Required ivars (deviceID, portType, uniqueID)
   abort class registration on miss; the streaming BOOL is optional
   (skip the YES poke instead of aborting). New vcc_resolve_ivar
   helper walks a NULL-terminated candidate-name list to tolerate
   underscore-prefix convention differences.

4. -[FigCaptureCameraSourcePipeline requiresMasterClock] prologue:
   resolved via LC_SYMTAB by name (two underscore-prefix variants),
   PAC-stripped, gated on a `pacibsp` insn1 sanity anchor before
   rewriting to `mov w0, #0 ; ret`. The function isn't in the ObjC
   method table on observed builds (so class_replaceMethod won't
   intercept) — the byte-patch is the only working path.

5. _cs_addObjectToStreamsAttributes and -[BWFigVideoCaptureStream
   initWithCaptureStream:…] -12783 bail sites: both prepare the
   OSStatus via MOVN encodings (0x12863dd4 for w20, 0x12863dc8 for
   w8). The new vcc_scan_and_patch helper finds every occurrence of
   each encoding in __text and rewrites it to MOVZ #0. -12783 is a
   capture-specific OSStatus and the daemon's only consumer in the
   VM is the synth source, so over-application is benign.

Validator fixes (kept from the original 26.1/26.5 work):
  - arm64e ISA class-pointer mask: 0x00007FFFFFFFFFF8 (44-bit class
    field, bits 3-46) per libobjc's ISA_MASK. The previous mask
    captured bit 47 (magic-signature region), so two pointers to the
    same class produced different masked values when bit 47 differed.
  - Pointer dereferences during slot validation gated by
    `malloc_zone_from_ptr` so a stale/bogus heap pointer in a
    candidate slot can't trap the daemon during init. (vm_read /
    vm_read_overwrite were considered but cameracaptured's sandbox
    returns KERN_DENIED on intra-task vm_read on iOS 26.x.)

Helpers in scripts/vcamcaptured/libvcamcaptured.m:
  vcc_safe_read_ptr                    pointer-read wrapper
  vcc_slot_value_is_cfarray            malloc_zone + ISA-class check
  vcc_collect_call_then_store_globals  walk a function body for
                                       "BL <X>; adrp + str x0,
                                       [Xn, #imm]" pairs
  vcc_resolve_ivar                     class_getInstanceVariable
                                       wrapper with candidate-name list
  vcc_scan_and_patch                   __text scan + per-occurrence
                                       vcc_patch_word wrapper
  VCC_ISA_CLASS_MASK                   arm64e 44-bit class-pointer mask

The two dead `#if 0` byte-patch blocks (referencing 0x1ae2b5284 and
0x1ae2b4c90, with the captureSession_buildGraphWithConfiguration
thumbnail / preview-sink bail-bypass commentary) are removed along
with their explanatory comments. scripts/cfw_install_exp.sh's
comment that mistakenly described a non-existent "Patch #6" inside
_captureSourceServer_handleCopySourcesMessage is rewritten to
describe the actual DSC patches (NU short-circuit + AVF authorization,
both already version-agnostic via `ipsw dyld symaddr`).

Validated end-to-end on:
  iOS 26.1   build 23B85
  iOS 26.3.1
  iOS 26.5   build 23F77

All three return the same `vphone:vcam:0` synthetic camera as the
default video device and deliver real JPEG frames through the modern
AVCapturePhoto delegate path in continuitycaptured / Camera.app.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-20 01:12:16 +03:00
zqxwceandClaude Opus 4.7 1aab0d6e25 camera: discover AVCapturePhoto + resolvedSettings selectors at runtime
Replace the two giant hardcoded selectors (the 27-arg AVCapturePhoto
init and the 32-arg +resolvedSettingsWithUniqueID:… factory) with a
prefix-lookup + label-driven NSInvocation builder. This survives
Apple adding or removing args between iOS releases without code
changes: the discovered selector decides arg count and order, the
resolver block fills the args we care about by label
("timestamp", "photoSurface", "uniqueID", "photoDimensions", etc.),
and unknown labels get nil/zero from the runtime type encoding.

  cfx_find_selector_by_prefix(cls, prefix, classMethod)
      Walks class_copyMethodList on cls (or its metaclass for class
      methods), returns the matching selector with the most colons.
      Highest-arg-count match wins so a future Apple revision that
      adds a new arg in the middle is still found.

  cfx_normalize_first_label(NSString *)
      Strips "initWith" / "resolvedSettingsWith" and lowercases the
      first char of the remainder so the leading component matches
      the same label convention as the rest of the selector.

  cfx_invoke_with_labeled_args(target, selector, resolver)
      Builds the NSInvocation, iterates selector components, calls
      the resolver block once per arg with (label, typeEnc, outBuf).
      Block writes the value via the appropriate cast (CMTime,
      IOSurfaceRef, __unsafe_unretained id, NSInteger, etc.) or
      leaves outBuf zeroed.

Builders refactored:
  - cfx_build_resolved_settings now fills only uniqueID +
    photoDimensions + previewDimensions; everything else stays
    zero/nil (Apple's impl tolerates that on builds where the
    factory itself works at all).
  - cfx_build_avcapturephoto_with_request fills timestamp,
    photoSurface, photoSurfaceSize, processedFileType, metadata,
    captureRequest, sequenceCount, photoCount, sourceDeviceType.
    Every other surface/dictionary arg defaults to nil.

End state: net +169/-96 lines, zero hardcoded full selectors,
photo synthesis remains functionally identical on 26.5 and is
prepared for arg-list drift on future iOS revisions.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-20 01:12:16 +03:00
zqxwceandClaude Opus 4.7 35a7bd44d8 camera: load libcamfix into every AVFoundation client via TweakLoader Filter.Frameworks
Universal injection mechanism: any process where AVFoundation is
loaded (Camera.app, continuitycaptured, third-party apps, system
daemons — anything dyld pulls AVF into) automatically gets libcamfix
via TweakLoader. No per-bundle plist filter, no allowlist entries.

scripts/tweakloader/TweakLoader.m

  New Filter.Frameworks key. A tweak's plist may list framework
  names; TweakLoader matches the path containing
  "/<name>.framework/". Already-loaded frameworks trigger an immediate
  dlopen; not-yet-loaded frameworks register a
  _dyld_register_func_for_add_image callback that fires when the
  named framework appears.

  Two-tier engagement:
    - Framework-filtered tweaks scan + schedule in EVERY process,
      self-limiting at runtime. Cost in non-AVF processes is one
      dir scan + a few plist parses + one callback registration.
    - Non-framework tweaks (Bundles/Executables or no filter) keep
      the existing .app/+kVPhoneAllowedDaemonPaths gate so we don't
      drop arbitrary tweaks into launch-critical daemons.

  CRITICAL safety: dyld invokes add-image callbacks SYNCHRONOUSLY
  inside its loader lock. dlopen from within that callback recurses
  and can deadlock or crash early daemons. The actual dlopen is
  handed off to a background queue (dispatch_async) so it runs after
  dyld is idle.

  Defensive: each per-tweak block is @try/@catch wrapped so a
  malformed plist or Foundation quirk in an early-boot daemon can't
  crash the process and trigger a launchd respawn loop.

scripts/camfix/libcamfix.m

  Constructor no longer eagerly installs hooks. Instead registers a
  _dyld_register_func_for_add_image callback and installs hooks the
  first time AVFCapture's mach header is observed (idempotent via
  dispatch_once). Whether libcamfix loads before or after AVFCapture,
  hooks land exactly once.

  cfx_capturePhoto_hook now drives the MODERN
  -[<AVCapturePhotoCaptureDelegate> captureOutput:
  didFinishProcessingPhoto:error:] path in addition to the deprecated
  CMSampleBuffer one. The synthesized AVCapturePhoto uses nil
  captureRequest (there's no CAMCaptureEngine outside Camera.app —
  msgSend to nil during init returns 0 safely). Photos tagged with
  associated JPEG/CGImage so fileDataRepresentation /
  CGImageRepresentation return our bytes regardless of which delegate
  protocol the client implements.

scripts/camfix/libcamfix.plist

  Filter.Frameworks = ["AVFoundation"]. Replaces the previous
  Bundles=["com.apple.camera"] filter.

scripts/cfw_install_exp.sh

  build_libcamfix install_name reverted to /var/jb/Library/
  MobileSubstrate/DynamicLibraries/libcamfix.dylib (TweakLoader
  location). [JB-4.2] deploys dylib + plist together.

Verified on fresh `make setup_machine` install of 26.5:

  - 373+ distinct AVF-using processes auto-load libcamfix at boot,
    including watchdogd / amfid / backboardd / SpringBoard /
    cameracaptured / continuitycaptured.
  - Camera.app: preview live, photos save, shutter works past
    many consecutive captures.
  - continuitycaptured: a vanilla AVCapturePhotoCaptureDelegate
    using the documented capturePhotoWithSettings:delegate: API gets
    a real 1280x720 JFIF JPEG via the modern delegate path.
  - Full reboot cycle stable.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-20 01:12:16 +03:00
zqxwceandClaude Opus 4.7 ba70b6f523 camera: libcamfix substrate plugin + Camera.app photo delivery via CAMCaptureEngine
libcamfix.dylib is loaded into Camera.app (com.apple.camera) by
TweakLoader (the .app/-rule covers Camera.app automatically; the plist
filters to Bundles=[com.apple.camera]). It bridges the vphone shm
frames from libvcamcaptured into Camera.app's normal photo + preview
pipeline so the user can take real photos via the standard shutter.

Hooks (only fire for connections backed by AVCaptureDevice uid
"vphone:vcam:0"):

  - _setActiveFormat:    substitute device.formats.firstObject when
                         the session-preset->format lookup hands us a
                         nil format (would otherwise throw at launch).
  - capturePhoto         deliver a CMSampleBuffer (built from shm) via
                         the deprecated didFinishProcessingPhotoSample
                         delegate path — kept for test harnesses that
                         use the documented AVCapturePhotoOutput API.
  - beginMomentCapture / commitMomentCaptureToPhotoWithUniqueID:
                         Camera.app's actual shutter path. Skip orig
                         (would throw), stash the delegate at begin,
                         drive a synthesized AVCapturePhoto at commit.
  - cancelMomentCaptureWithUniqueID:    no-op (orig would throw).
  - AVCaptureSession _setRunning: / _setInterrupted: setters swallowed
                         for vcam-bound sessions, and isRunning /
                         isInterrupted getters force YES / NO so
                         Camera.app's "preview live" poll keeps the
                         viewfinder visible past ~4-5 s.
  - AVCaptureVideoPreviewLayer:  scan UIApplication.windows at 1 Hz
                         for layers bound to a vcam session and pump
                         CGImage frames into layer.contents at 30 Hz.
  - AVCapturePhoto fileDataRepresentation / CGImageRepresentation:
                         when the photo we synthesized is the receiver,
                         return the JPEG / CGImage we built from shm
                         instead of asking the (non-existent) photo
                         surface to encode itself.
  - CAMStillImageCaptureRequest: dynamically add three stubs
                         (resolvedSettings, unresolvedSettings,
                         lensStabilizationSupported) so AVCapturePhoto's
                         private 27-arg init does not throw on the
                         CAM-internal request we pass in.

Synthesized AVCapturePhoto construction:
  - extract the real CAMStillImageCaptureRequest for the current uid
    from CAMCaptureEngine._resultsQueueRegisteredStillImageRequests
    (Camera.app's pending-photo dict),
  - hand-build a minimal AVCaptureResolvedPhotoSettings via
    class_createInstance + ivar writes for uniqueID + dimensions +
    empty NSArray ivars (CFRetained so the dealloc chain stays valid),
  - feed both into AVCapturePhoto's documented 27-arg
    initWithTimestamp:photoSurface:... via NSInvocation,
  - tag the photo with the JPEG bytes via objc_setAssociatedObject
    so the fileDataRepresentation hook returns them.

Full AVF + CAM internal delegate sequence fired at commit time:
willBeginCaptureBeforeResolvingSettingsForUniqueID,
willBeginCaptureForResolvedSettings, willCapturePhotoForResolvedSettings,
didCapturePhotoForResolvedSettings, didFinishProcessingPhoto:error:,
didFinishCaptureForResolvedSettings:error:,
_didFinishStillImageCaptureForUniqueID:error:, and crucially
captureOutput:readyForResponsiveRequestAfterResolvedSettings:.
Without that last "responsive ready" signal AVF's 2-deep pipeline
never frees its slots and Camera.app's shutter stops accepting
input after the 2nd capture.

Install wiring in scripts/cfw_install_exp.sh:
  - build_libcamfix() — clang -arch arm64e -fobjc-arc -Os, frameworks
    AVFoundation / CoreImage / CoreMedia / CoreVideo / Foundation /
    ImageIO / IOSurface / MobileCoreServices / Photos / QuartzCore /
    UIKit, ldid-signed.
  - [JB-4.2] scp the dylib + plist into procursus/Library/MobileSubstrate/
    DynamicLibraries (same location as libvcamcaptured) and chmod /
    chown so TweakLoader picks them up on next boot.

End state: Camera.app on EXP shows live preview from the host-supplied
vcam frames, the shutter takes real photos that get saved into Photos
via Camera.app's own pipeline (no PHPhotoLibrary back-channel), and
the shutter button keeps working across many consecutive captures.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-20 01:12:16 +03:00
zqxwceandClaude Opus 4.7 9b4e25569b camera: libvcamcaptured + cameracaptured TweakLoader allowlist + install wiring
libvcamcaptured.dylib is loaded into /usr/libexec/cameracaptured via
TweakLoader and registers a synthetic FigCaptureSource backed by the
vphone shm region. From AVF's point of view there is now a normal
"vphone:vcam:0" camera device that streams BGRA frames at the
session's requested width/height.

  scripts/vcamcaptured/                .gitignore (drop built .dylib),
                                       Makefile, libvcamcaptured.m,
                                       libvcamcaptured.plist (filter:
                                       Executables=["cameracaptured"]).

  scripts/tweakloader/TweakLoader.m    add /usr/libexec/cameracaptured
                                       to kVPhoneAllowedDaemonPaths so
                                       TweakLoader engages in a daemon
                                       (not just .app/) processes.

  scripts/cfw_install_exp.sh           build_libvcamcaptured() helper
                                       (clang + CoreMedia/CoreVideo/
                                       Foundation, ldid-signed) and
                                       new [JB-4.1] section that
                                       scp's the dylib + plist into
                                       procursus/Library/MobileSubstrate/
                                       DynamicLibraries.

Pairs with the host vphone-cli camera server + vphoned vcam vsock
listener already in this branch.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-20 01:12:16 +03:00
zqxwceandClaude Opus 4.7 c8de9c9cb7 camera: vphoned guest-side vcam vsock listener + shm writer
Guest-side counterpart to VPhoneCameraServer. vphoned runs inside
the VM and now hosts an extra vsock listener on port 1338 that
receives BGRA frames from the host and writes them into a memory-
mapped shm region at
/var/jb/var/mobile/Library/vphone-vcam-frame.shm with a packed
header (seq monotonic, width/height, bytes_per_row, pixel_format,
timestamp_ns, frame_index, pixels_length).

  vphoned_vcam.h        packed shm header layout + filename const.
  vphoned_vcam.m        VPVcamServer: accept loop, header parse,
                        seq-bump write, file-based debug log for
                        first-boot post-mortem.
  vphoned.m             Boot the vcam server alongside the existing
                        port-1337 control daemon.

Pairs with the host VPhoneCameraServer (1338 client) and the guest
libvcamcaptured (shm reader) added in the following commits.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-20 01:12:16 +03:00
zqxwceandClaude Opus 4.7 0223ad139c camera: Host-side virtual camera pipe (vphone-cli)
Mac-side feeder for the virtual camera. The host generates frames
(test pattern or video file) and pushes BGRA buffers over vsock to
the guest daemon vphoned, which mirrors them into the shm region
that the in-VM libvcamcaptured + libcamfix consume.

  VPhoneCameraServer        @MainActor vsock client to vphoned port
                            1338. Reconnects on transport failure
                            so the host can survive guest reboots.
  VPhoneFrameProducer       Test-pattern (hue-rolling gradient) and
                            VideoFile producers; emit BGRA at the
                            session's requested width/height/fps.
  VPhoneMenuCamera          Camera menu: pick source, start/stop
                            the feeder, show stream status.
  VPhoneMenuController/Connect/AppDelegate
                            Wire the menu into the existing menu
                            bar and start the server alongside the
                            other vsock services at VM boot.

End state: with a vphoned listener on the guest side, host-generated
BGRA frames appear in the shm region the camera daemon reads from.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-20 01:12:16 +03:00
zqxwceandClaude Opus 4.7 9bb5b87279 camera: Make Camera.app launch on EXP firmware
Three pieces of plumbing that together let Camera.app reach the
viewfinder UI on the EXP build:

1. DeviceTree: synthesize a /product/camera node so AVCaptureDevice
   discovery + permission gating finds a "front" camera slot for
   subsequent vcam injection (FirmwarePatcher/DeviceTreePatcher.swift).

2. DSC patch family in scripts/patchers/cfw_patch_camera_dsc.py:
     - NeutrinoCore short-circuit: rewrite the five
       +[_NUStyleTransfer*Processor processWithInputs:arguments:output:
       error:] class methods to `mov w0, #0; ret`. Without this Camera.app
       crashes inside NeutrinoCore the moment it tries to render the
       style picker.
     - AVCaptureDevice auth always-authorized:
       +[AVCaptureDevice authorizationStatusForMediaType:] -> `mov w0, #3;
       ret` (AVAuthorizationStatusAuthorized=3). Any process that probes
       camera authorization gets "Authorized" without going through TCC.

3. scripts/patch_camera_userland.sh + cfw.py registration so the install
   pipeline applies the two patches above against the chunked DSC during
   `make cfw_install_exp`.

Camera.app now launches and shows preview UI on EXP, even though the
actual vcam pipeline is wired up by the libvcamcaptured / libcamfix
commits that follow.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-20 01:12:16 +03:00
zqxwce d15d218fe2 ramdisk: Remove trollstore from RAMDISK_REMOVE list 2026-05-25 15:12:59 +03:00
zqxwce 35a33cac9f readme: Update tested environments 2026-05-24 18:18:52 +03:00
zqxwce a1732f7191 variants: Update patch count documentation 2026-05-24 18:18:52 +03:00
zqxwce f8388d085e IBootPatcher: Add bootx-handoff patch for 26.4+ 2026-05-24 18:18:52 +03:00
zqxwce 9eea357142 ramdisk_build: Remove usr/trollstore from ramdisk to free needed space 2026-05-24 18:18:52 +03:00
zqxwce 045d8050a3 install: Add opt-in ProductBuildVersion rewrite via SPOOF_BUILD (EXP-JB-7)
Adds the final EXP-only step: rewrite the userland-visible
`ProductBuildVersion` in `SystemVersion.plist` to a chosen build
identifier. Gated on the `SPOOF_BUILD` env var — when unset/empty, the
step is skipped entirely and the build identifier stays at whatever the
IPSW shipped.

The iPhone IPSW we install from ships with build identifier `23B85`
(iOS 26.1). iOS displays this string in Settings -> General -> About ->
"Build" and exposes it through `MGCopyAnswer("BuildVersion")`,
CoreFoundation's `_CFCopyServerVersionDictionary`, App Store telemetry,
and every other framework path that reads
`/System/Library/CoreServices/SystemVersion.plist`.

The build identifier lives in exactly two on-device plist files. Both
are plain XML/binary plists (no Apple-side per-file signature), and
both live on volumes that are writable at install time:

  /System/Library/CoreServices/SystemVersion.plist                                  (rootfs)
  /private/preboot/Cryptexes/OS/System/Library/CoreServices/SystemVersion.plist     (preboot)

EXP-JB-7 rewrites the `ProductBuildVersion` key in both plists to the
target value (typical: `23F77`). `ProductVersion` (`26.1`),
`ProductName` (`iPhone OS`), `BuildID`, `SystemImageID`, and
`ProductCopyright` are left untouched.

Order of operations: EXP-JB-7 runs AFTER EXP-JB-6 (post-restore DT
rewrite) so the post-restore identity work on `/mnt5` has completed
before the Cryptex's SystemVersion.plist (same volume) is touched.

Does NOT flip:
  - `sysctl kern.osversion` — comes from a kernel global initialized
    from boot args at boot time, not from this plist. To change it
    would require rebuilding the kernelcache with a different
    `OS_BUILD_VERSION` or patching the boot-args path — out of scope.
  - `SystemVersionCompat.plist` — carries a legacy iOS-19 marker for
    MacCatalyst-style queries; not user-visible, deliberately untouched.

- `scripts/patchers/cfw_patch_build_version.py` — host-side
  plistlib-based rewriter. Auto-detects XML vs binary plist format
  and preserves it on write. Idempotent — a re-run on an
  already-patched plist exits without rewriting.
- `scripts/cfw_install_exp.sh` — EXP-JB-7 phase: gated on
  `SPOOF_BUILD`; for each of the two plist paths it scp_from's the
  file to host, runs the patcher with the target id, scp_to's the
  file back. Tolerates missing-on-device with warn+continue.

Invocation:
  make setup_machine EXP=1 SPOOF_BUILD=23F77
  make cfw_install_exp SPOOF_BUILD=23F77

JB and DEV install scripts do NOT carry this step.
2026-05-18 16:14:55 +03:00
zqxwce 8bc903644f devicetree: Add post-restore identity rewrite for restore-fatal properties (EXP-JB-6)
The three restore-fatal DT root properties (root `model`, root
`target-type`, root `compatible[0]`) cannot be edited at fw_patch
time because `restored_external` / iBoot's restore mode cross-checks
them against the BuildManifest's signed `SupportedProductTypes`.
Editing them in the IPSW's `devicetree.im4p` causes the restore to
fail partway through.

But the cross-check fires ONLY during installation. After restore
completes, subsequent boots validate IM4P contents against the IM4M
only, which the existing iBSS/iBEC/LLB `image4_validate_property_callback`
bypass patches accept regardless. So an additional DT edit applied
AFTER restore but BEFORE the device reboots into the rootfs is
in-policy with the project's existing trust-chain bypass.

EXP-JB-6 exploits this. While the install pipeline still has `/mnt5`
(the preboot volume) mounted on the ramdisk, `cfw_install_exp.sh`:

  1. scp_from's `/mnt5/<boot-hash>/usr/standalone/firmware/devicetree.img4`
     to the host.
  2. Runs `scripts/patchers/cfw_patch_post_restore_dt.py`, which:
       - Unwraps IMG4 -> IM4P -> LZFSE-decompresses -> DT flat-binary
         blob (via pyimg4).
       - Rewrites three root properties:
           root `model`:       iPhone99,11        -> iPhone17,3
           root `target-type`: VPHONE600          -> D47
           root `compatible`:  reorder so D47AP is first, VPHONE600AP
                                second (IOKit's AppleVMApple1IO platform
                                bind still resolves via the second
                                entry; userland reads only the first
                                for `hw.model`).
       - Re-compresses LZFSE -> repacks IM4P -> repacks IMG4 with the
         ORIGINAL IM4M (the per-board ticket survives — the image4
         bypass already accepts any payload contents).
  3. scp_to's the modified img4 back to the same path.
  4. The device reboots out of ramdisk; iBoot loads the modified DT;
     kernel populates `machine_info` from the new property values.

Idempotent: the patcher detects target-state-already-met and exits
without rewriting.

Userland effects on next boot:
  - `sysctl hw.machine` -> "iPhone17,3" (was "iPhone99,11")
  - `sysctl hw.product` -> "iPhone17,3" (was "iPhone99,11")
  - `sysctl hw.model`   -> "D47AP"      (was "VPHONE600AP")
  - Settings -> General -> About -> Model Identifier picks up the new
    ProductType after the gestalt cache rebuilds.

- `scripts/patchers/cfw_patch_post_restore_dt.py` — host-side
  img4 <-> IM4P <-> DT flat-binary round-trip via pyimg4. Mirrors the
  DT format parser/serializer from `DeviceTreePatcher.swift`.
- `scripts/cfw_install_exp.sh` — EXP-JB-6 phase: discovers the
  boot-manifest-hash via the same `get_boot_manifest_hash` helper used
  by earlier install steps; tolerates a missing devicetree.img4 with
  warn-and-skip. JB and DEV install scripts do NOT carry this step.
2026-05-18 16:14:55 +03:00
zqxwce f22ed3e4bf devicetree: Add 8 identity-rewrite properties on EXP variant at fw_patch time
Splits `DeviceTreePatcher`'s property-patch list into two arrays:

  - `basePropertyPatches` (4 entries: `serial-number`,
    `home-button-type`, `artwork-device-subtype`,
    `island-notch-location`) — applied for every variant. Behaviour
    identical to pre-split.

  - `identityPropertyPatches` (8 entries — Tier 1b + 1c userland-facing
    identity surfaces) — applied only when `includeIdentityPatches` is
    true, which `FirmwarePipeline` sets exactly when `variant == .exp`.

The 8 EXP-only identity properties flip userland-visible identity toward
D47AP / iPhone17,3:

  - Tier 1b (5 properties, slot-length-preserving rewrites):
      device-tree.target-sub-type:        VPHONE600AP -> D47AP
      device-tree.compatible[1]:          iPhone99,11 -> iPhone17,3
                                           (reorder, VPHONE600AP kept first
                                            so IOKit's AppleVMApple1IO bind
                                            still resolves)
      device-tree/product.fdr-product-type: iPhone99,11 -> iPhone17,3
      device-tree/product.sub-product-type: iPhone99,11 -> iPhone17,3
      device-tree/product.unique-model:     VPHONE600AP -> D47AP

  - Tier 1c (3 properties — IOKit secondary matchers + Gestalt subtree
    rename, matched against the real D47AP DT):
      device-tree/arm-io.device_type:      vresearch1-io -> t8140-io
      device-tree/arm-io.soc-generation:   VResearch1    -> H17
      device-tree/product/vphone600-gestalt-variants.name (node rename):
                                            vphone600-gestalt-variants
                                            -> d47-gestalt-variants

Root `model` and root `target-type` are deliberately NOT in this list —
both have been empirically shown to break restore (signed-identity
cross-check in `restored_external`). Those edits run post-restore in a
later commit as EXP-JB-6.

- `sources/FirmwarePatcher/DeviceTree/DeviceTreePatcher.swift` — adds
  `includeIdentityPatches: Bool = false` to init (backwards-compatible
  default), stores it, splits `propertyPatches` into two static lists,
  iterates base first and then optionally identity in `applyPatches`.
- `sources/FirmwarePatcher/Pipeline/FirmwarePipeline.swift` — passes
  `includeIdentityPatches: variant == .exp` into the DT factory.

JB and other variants (regular, dev, less) leave the device's identity
properties untouched.
2026-05-18 16:14:55 +03:00
zqxwce 674a86bfd4 watchdogd: Add surgical hv_vmm_present cache patch (EXP-JB-3.5)
After the kernel-side OID rename (`KernelEXPPatchHvVmmRename`),
`sysctlbyname("kern.hv_vmm_present", ...)` returns ENOENT on this image.
`/usr/libexec/watchdogd` caches that answer at startup. On ENOENT the
cached byte stays at its BSS-zero default (`0`) and the downstream
`cbz w0, ...` at the IOWatchdog-lookup site takes a branch into
`_os_crash` -> `brk #1`; launchd's `_PanicOnCrash =
PanicOnConsecutiveCrash = true` flag in `com.apple.watchdogd.plist`
escalates the SIGTRAP to a kernel panic.

The cstring-mangle approach used for DSC dylibs doesn't apply here: we
want this binary to behave as if the sysctl returned 1, not as if it
returned ENOENT. Solution is a surgical 2-instruction patch that forces
the cached "am I a VM?" byte to 1 regardless of the sysctl result.

- `scripts/patchers/cfw_patch_watchdogd.py` — capstone-anchored pattern
  matcher + Keystone-assembled 2-insn patch. Two functions match the
  canonical caching shape on iPhone17,3 / iOS 26.1; both are patched.
  Net effect: `cbnz w0, skip` -> NOP and `cset wN, ne` -> `mov wN, #1`,
  forcing the cached byte to 1. watchdogd's pre-existing "detected
  virtual machine environment, exiting..." clean-exit branch runs
  instead of the trap path. Idempotent.
- `scripts/patchers/cfw_macho_codesign.py` — generic standalone-Mach-O
  page-hash re-attestation. Parses `LC_CODE_SIGNATURE` directly, reads
  page size from each `CS_CodeDirectory` header (4 KiB on watchdogd —
  not the DSC's 16 KiB), handles short tail slot length
  (`codeLimit - (n-1)*pageSize`), and updates every present CD. The
  resulting cdHash change is accepted by the JB `patch_amfi_cdhash_in_trustcache`
  kernel patch which accepts any cdHash; the patcher does NOT re-sign
  with ldid (preserving the original Apple-issued code-signing
  identifier is required for launchd's boot-task identity validation).
- `scripts/patchers/cfw.py` — adds `patch-watchdogd` subcommand.
- `scripts/patch_hv_vmm_userland.sh` — adds `watchdogd <binary>` op.
- `scripts/cfw_install_exp.sh` — invokes the patcher at step
  `[EXP-JB-3.5]` on the live `/mnt1/usr/libexec/watchdogd` (scp-down,
  patch, scp-up, chmod 0755). JB and DEV install scripts do NOT run
  this step.
2026-05-18 16:14:55 +03:00
zqxwce af90c9a903 userland: Add DSC hv_vmm_present byte-5 mangle with sign-in blacklist and slot reattest (EXP only)
Companion user-mode patches to the kernel-side OID rename. Mangles
byte 5 of every `kern.hv_vmm_present` cstring inside DSC dylibs EXCEPT
those in `DONT_PATCH_INSTALL_NAMES` (sign-in / device-likeness consumers,
~15 entries). Patched dylibs query the renamed OID and get the truthful
1 (graphics + accel passthrough); blacklisted dylibs keep the original
cstring, hit ENOENT on the renamed kernel, defensively cache 0 ("not
running on a VM") for sign-in / device-attestation surfaces.

- `scripts/patchers/cfw_patch_hv_vmm_dsc.py` — DSC orchestrator. Walks
  every `kern.hv_vmm_present\\0` cstring in any executable mapping,
  resolves the containing dylib via Mach-O-header walk-back +
  LC_ID_DYLIB, applies the byte-5 mangle to non-blacklisted dylibs, and
  drives slot-hash re-attestation in the chunk's `CS_CodeDirectory`.
- `scripts/patchers/cfw_dsc_chunks.py` — pure-Python helper for the
  chunked DSC layout: vmaddr<->chunk-fileoff mapping, install-name
  walk-back, byte-level read/write at a vmaddr.
- `scripts/patchers/cfw_dsc_codesign.py` — per-page SHA-256 slot-hash
  re-attestation for DSC chunks (16 KiB pages). Required on
  `codeSigningMonitor == 2` (TXM) hardware where per-page hash checks
  would otherwise SIGKILL the patched dylibs at first demand-page-in.
- `scripts/patchers/cfw_patch_hv_vmm.py` — standalone Mach-O variant of
  the cstring mangle (kept for completeness; the historical
  standalone-binary loop step was removed in favor of the
  blacklist-flip-via-kernel-rename design).
- `scripts/patchers/cfw_patch_hv_vmm_rootfs.py` — rootfs-path inventory
  shared with the install scripts (former JB-3.5 / 6.5/7 loop input).
- `scripts/patch_hv_vmm_userland.sh` — thin wrapper used by the install
  script (`dsc` and `standalone` operations; `watchdogd` is added by
  the next commit).
- `scripts/patchers/cfw.py` — new subcommands: `patch-hv-vmm`,
  `patch-hv-vmm-dsc`, `list-hv-vmm-rootfs-paths`.
- `scripts/cfw_install_exp.sh` — adds a pre-step that decrypts the
  SystemOS Cryptex AEA into the cache location `cfw_install.sh` already
  uses, mounts it, applies the DSC patch, and unmounts. The unmodified
  base `cfw_install.sh` then picks up the cached (patched) DMG.

Research docs in this commit describe the full EXP variant comprehensively
(`research/0_binary_patch_comparison.md` top-of-doc note, EXP-Only
Kernel Methods section, DSC userland subsection, plus forward references
to EXP-JB-3.5 / EXP-JB-6 / EXP-JB-7 subsections wired up by the following
commits; `research/firmware_manifest_and_origins.md` sections 9-11
similarly forward-describe the DT and build-version pieces).

JB and DEV variants are NOT affected: their install scripts don't invoke
any of this.
2026-05-18 16:14:55 +03:00
zqxwce ee5e7d0fa2 kernel: Introduce EXP variant and hv_vmm_present OID rename (KernelEXPPatcher)
Introduces a new `exp` firmware variant on top of JB and ports the
`hv_vmm_present` sysctl rename to it.

Variant infrastructure
----------------------
- Adds `case exp` to `FirmwarePipeline.Variant`,
  `VPhoneCLI.PatchFirmwareCLI.VariantOption`, and
  `VPhoneVirtualMachine.Variant`. Every `switch variant` block in
  `FirmwarePipeline.buildComponentList` includes the new case.
- Adds `make fw_patch_exp` + `make cfw_install_exp` Makefile targets
  and an `EXP=1` flag for `make setup_machine`, mutually exclusive with
  `JB=1` / `DEV=1` / `LESS=1`. `SPOOF_BUILD` env var is threaded
  through for later use by the build-version step.
- Adds `--exp` to `scripts/setup_machine.sh` alongside `--jb` /
  `--dev` / `--less`. The post-install JB-Finalize block also fires
  for EXP (since EXP inherits the JB rootfs deployment).
- Adds `scripts/cfw_install_exp.sh` as the EXP install script
  starting point: identical phase set to `cfw_install_jb.sh` (JB-1..JB-5)
  with banner/header/footer updated for EXP. Subsequent commits in this
  branch add the EXP-only experimental phases on top.
- Updates the variants table in `AGENTS.md` / `README.md` and the
  three translated READMEs to include the new `Experimental (EXP)`
  row, plus a paragraph describing what EXP adds on top of JB.

Kernel patch
------------
- New `KernelEXPPatcher` orchestrator in
  `sources/FirmwarePatcher/Kernel/`, chained after `KernelPatcher` +
  `KernelJBPatcher` for the `.exp` variant only. Inherits
  `KernelJBPatcherBase` to reuse the JB symbol-table / ADRP-BL index /
  branch-encoder infrastructure.
- New `KernelEXPPatchHvVmmRename` patch in
  `sources/FirmwarePatcher/Kernel/EXPPatches/`. Part A flips byte 0 of
  the NUL-delimited `\\0hv_vmm_present\\0` cstring (the sysctl OID's
  `oid_name` value) — `'h'` -> `'X'` — so the kernel resolves
  `sysctlbyname("kern.hv_vmm_present")` as ENOENT and
  `sysctlbyname("kern.Xv_vmm_present")` to the OID's real int value.
  Part B mangles byte 5 of every kernel-internal occurrence of
  `kern.hv_vmm_present` so callers keep hitting the renamed OID; two
  byte-aligned forms are covered (NUL-delimited cstring + sandbox-profile
  name-token with trailing `\\x0f`).
- Patch IDs are `kernelcache_exp.hv_vmm_oid_rename` and
  `kernelcache_exp.hv_vmm_internal_caller_mangle`. Idempotent.
- `KernelJBPatcher` is unchanged at the call-site level (no
  `patchHvVmmRename` call); its docstring is updated to point at
  `KernelEXPPatcher` for the EXP-only patch.

JB and other variants are NOT affected: `cfw_install_jb.sh` and
`cfw_install_dev.sh` are untouched in this commit.
2026-05-18 16:14:55 +03:00
zqxwce 77d4a04c55 window: Show guest IP in subtitle once vphoned connects
vphoned now reports its primary non-loopback IPv4 address in the hello
response (preferring en*/pdp_ip* interfaces).
2026-04-30 12:27:59 +03:00
zqxwce 856576e93b docs: Add restore_offline documentation 2026-04-27 13:32:25 +03:00
zqxwce 120d6f9862 venv: Fix python3 locating for enviornments using uv 2026-04-20 17:23:32 +03:00
zqxwce 3b48ce6cf3 iosbinpack64: dev_overlay: Move dev overlay to before install 2026-04-20 17:23:32 +03:00
zqxwce 367209a1e4 amfidont: Simplify start_amfidont_for_vphone.sh 2026-04-15 16:41:07 +03:00
zqxwce 7d09a1bb0e patchless: Add support for non SIP/AMFI disabled systems 2026-04-15 16:41:07 +03:00
zqxwce 45c3df7609 setup_tools: Limit apfs_sealvolume download to patchless variant only 2026-04-08 18:31:14 +03:00
zqxwce 20d3f1a217 pymobiledevice3: Replace most external tools with pymobiledevice3 2026-04-03 13:47:09 +03:00
zqxwce 30fcc05ca5 refactor: Move all manual clones to be submodules (#218) 2026-03-16 01:40:05 +09:00
zqxwce 4b052cc1ca setup_machine: Fix (( waited++ )) causing exit on first iteration (#199)
In the first iteration, waited would be 0 and cause the expression to be evaluated to `(( 0 ))`, which exists as it returns 1.
2026-03-13 01:11:13 +08:00
zqxwce e040c3e422 dtree: Implement device tree patching (#170) 2026-03-10 02:52:21 +08:00
zqxwce ede318a29b setup_machine: Add missing quotation in send_first_boot_commands (#157) 2026-03-09 13:40:11 +08:00
zqxwce d48ad72fa4 launch_daemons: Readd dropbear back as default in all variants (#155) 2026-03-09 01:17:02 +08:00
zqxwce 48d33b19ef ramdisk_build: Set default value for sudo password to None so prompt would show (#154) 2026-03-08 22:59:42 +08:00