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]>
vphone-cli
Boot a virtual iPhone (iOS 26) via Apple's Virtualization.framework using PCC research VM infrastructure.
Tested Environments
| Host | iPhone | CloudOS |
|---|---|---|
| Mac16,12 26.3 | 17,3_26.1_23B85 |
26.1-23B85 |
| Mac16,12 26.3 | 17,3_26.3_23D127 |
26.1-23B85 |
| Mac16,12 26.3 | 17,3_26.3_23D127 |
26.3-23D128 |
| Mac16,12 26.3 | 17,3_26.3.1_23D8133 |
26.3-23D128 |
| Mac16,11 26.2 | 17,3_26.4_23E246 |
26.4-23E5207q |
| Mac16,11 26.2 | 17,3_26.5_23F77 |
26.4-23E5207q |
Firmware Variants
Five patch variants are available with increasing levels of security bypass:
| Variant | Boot Chain | CFW | Make Targets |
|---|---|---|---|
| Patchless | 4 patches | 2 phases | fw_patch_less + boot_less |
| Regular | 42 patches | 10 phases | fw_patch + cfw_install |
| Development | 53 patches | 12 phases | fw_patch_dev + cfw_install_dev |
| Jailbreak | 113 patches | 14 phases | fw_patch_jb + cfw_install_jb |
| Experimental | 141 patches | 18 phases | fw_patch_exp + cfw_install_exp |
JB finalization (symlinks, Sileo, apt, TrollStore) runs automatically on first boot via
/cores/vphone_jb_setup.shLaunchDaemon. Monitor progress:/var/log/vphone_jb_setup.log.
Experimental (EXP) is a JB superset that patches the kernel and DSC to make some Apple services think the device is not a VM, while keeping VM-specific services (graphics passthrough, compute/accel fast paths) working correctly. Other variants are deliberately NOT affected.
See research/0_binary_patch_comparison.md for the detailed per-component breakdown.
Prerequisites
Host OS: macOS 15+ (Sequoia) is required for PV=3 virtualization.
Configure SIP/AMFI — required for private Virtualization.framework entitlements and unsigned binary workflows.
Boot into Recovery (long press power button), open Terminal, then choose one setup path:
-
Option 1: Fully disable SIP + AMFI boot-arg (most permissive)
In Recovery:
csrutil disable csrutil allow-research-guests enableAfter restarting into macOS:
sudo nvram boot-args="amfi_get_out_of_my_way=1 -v"Restart once more.
-
Option 2: Keep SIP mostly enabled, disable only debug restrictions, use
amfidontoramfreeIn Recovery:
csrutil enable --without debug csrutil allow-research-guests enableAfter restarting into macOS:
# Using amfidont: xcrun python3 -m pip install amfidont sudo amfidont --path [PATH_TO_VPHONE_DIR] # OR Using amfree: brew install retX0/tap/amfree sudo amfree --path [PATH_TO_VPHONE_DIR]Repo helper (for amfidont):
make amfidont_allow_vphoneThis helper computes the current signed
vphone-cliCDHash and uses the URL-encoded project path form observed byAMFIPathValidator.
The Patchless variant requires either the use of option 1 or amfidont with the
-Sflag (sudo amfidont -S --path [PATH_TO_VPHONE_DIR])
Install dependencies:
You will need both brew dependencies and Git Submodule dependencies.
- Brew dependencies:
brew install aria2 wget gnu-tar openssl@3 ldid-procursus sshpass keystone libusb ipsw zstd
scripts/fw_prepare.sh prefers aria2c for faster multi-connection downloads and falls back to curl or wget when needed.
- Git Submodules
Submodules — this repo uses git submodules for resources, vendored Swift deps, and toolchain sources under scripts/repos/. Clone with:
git clone --recurse-submodules https://github.com/Lakr233/vphone-cli.git
Quick Start
make setup_machine # full automation through "First Boot" (includes restore/ramdisk/CFW)
# options: NONE_INTERACTIVE=1 SUDO_PASSWORD=...
# LESS=1 for patchless variant (- AMFI, SSV, Img4, TXM bypasses)
# DEV=1 for dev variant (+ TXM entitlement/debug bypasses)
# JB=1 for jailbreak variant (+ full security bypass)
# EXP=1 for experimental variant (JB + research patches: hv_vmm rename, DT identity, post-restore rewrite)
# SPOOF_BUILD=<id> (EXP only) Rewrite SystemVersion.plist ProductBuildVersion to <id>, e.g. 23F77
Manual Setup
make setup_tools # install brew deps, build trustcache + insert_dylib, create Python venv (pymobiledevice3, aria2c included)
make build # build + sign vphone-cli
make vm_new # create VM directory with manifest (config.plist)
# options: CPU=8 MEMORY=8192 DISK_SIZE=64
make fw_prepare # download IPSWs, extract, merge, generate manifest
make fw_patch # patch boot chain (regular variant)
# or: sudo make fw_patch_less # patchless variant (- AMFI, SSV, Img4, TXM bypasses)
# or: make fw_patch_dev # dev variant (+ TXM entitlement/debug bypasses)
# or: make fw_patch_jb # jailbreak variant (+ full security bypass)
# or: make fw_patch_exp # experimental variant (JB + research stack)
Cleaning
make clean # remove build/tooling artifacts only
make clean CLEAN_VM=1 # also remove vm/ after confirmation
make clean CLEAN_IPSW=1 # also remove ipsws/ after confirmation
Default clean never removes vm/ or ipsws/.
VM Configuration
Starting from v1.0, VM configuration is stored in vm/config.plist. Set CPU, memory, and disk size during VM creation:
# Create VM with custom configuration
make vm_new CPU=16 MEMORY=16384 DISK_SIZE=128
# Boot automatically reads from config.plist
make boot
The manifest stores all VM settings (CPU, memory, screen, ROMs, storage) and is compatible with security-pcc's VMBundle.Config format.
Restore
You'll need two terminals for the restore process. Keep terminal 1 running while using terminal 2.
# terminal 1
make boot_dfu # boot VM in DFU mode (keep running)
# terminal 2
make restore_get_shsh # fetch SHSH blob
make restore # flash firmware via pymobiledevice3 restore backend
# or: make restore_offline # offline restore (decrypts AEA images in place, uses cached .shsh blob)
# for the first time should be ran with internet access for AEA decryption
Install Custom Firmware
Stop the DFU boot in terminal 1 (Ctrl+C), then boot into DFU again for the ramdisk:
# terminal 1
make boot_dfu # keep running
# terminal 2
sudo make ramdisk_build # build signed SSH ramdisk
make ramdisk_send # send to device
Once the ramdisk is running (you should see Running server in the output), open a third terminal for the usbmux tunnel, then install CFW from terminal 2:
# terminal 3 — keep running
python3 -m pymobiledevice3 usbmux forward 2222 22
# terminal 2
make cfw_install
# or: make cfw_install_jb # jailbreak variant
# or: make cfw_install_exp # experimental variant (JB + research stack)
# or: SPOOF_BUILD=23F77 make cfw_install_exp # additionally rewrite ProductBuildVersion
First Boot
Stop the DFU boot in terminal 1 (Ctrl+C), then:
make boot
After cfw_install_jb, the jailbreak variant will have Sileo and TrollStore available on first boot. You can use Sileo to install openssh-server for SSH access.
For the regular/development variant, the VM gives you a direct console. When you see bash-4.4#, press Enter and run these commands to initialize the shell environment and generate SSH host keys:
export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11:/usr/games:/iosbinpack64/usr/local/sbin:/iosbinpack64/usr/local/bin:/iosbinpack64/usr/sbin:/iosbinpack64/usr/bin:/iosbinpack64/sbin:/iosbinpack64/bin'
mkdir -p /var/dropbear
cp /iosbinpack64/etc/profile /var/profile
cp /iosbinpack64/etc/motd /var/motd
# generate SSH host keys (required for SSH to work)
dropbearkey -t rsa -f /var/dropbear/dropbear_rsa_host_key
dropbearkey -t ecdsa -f /var/dropbear/dropbear_ecdsa_host_key
shutdown -h now
Note: Without the host key generation step, dropbear (SSH server) will accept connections but immediately close them because it has no keys to perform the SSH handshake.
Subsequent Boots
make boot
In a separate terminal, start usbmux forward tunnels:
python3 -m pymobiledevice3 usbmux forward 2222 22222 # SSH (dropbear)
python3 -m pymobiledevice3 usbmux forward 2222 22 # SSH (JB: if you install openssh-server from Sileo)
python3 -m pymobiledevice3 usbmux forward 5901 5901 # VNC
python3 -m pymobiledevice3 usbmux forward 5910 5910 # RPC
Connect via:
- SSH (JB):
ssh -p 2222 [email protected](password:alpine) - SSH (Regular/Dev):
ssh -p 2222 [email protected](password:alpine) - VNC:
vnc://127.0.0.1:5901 - RPC:
rpcclient -p 5910 127.0.0.1
VM Backup & Switch
Save and switch between multiple VM environments (e.g. different iOS builds or firmware variants). Backups are stored in vm.backups/ using rsync --sparse for efficient sparse disk handling.
make vm_backup NAME=26.1-clean # save current VM
rm -rf vm && make vm_new # start fresh for a different build
# ... fw_prepare, fw_patch, restore, cfw_install, boot
make vm_backup NAME=26.3-jb # save the new one too
make vm_list # list all saved backups
make vm_switch NAME=26.1-clean # swap between them
Note: Always stop the VM before backup/switch/restore.
FAQ
Before anything else — run
git pullto make sure you have the latest version.
Q: I get zsh: killed ./vphone-cli when trying to run it.
AMFI/debug restrictions are not bypassed correctly. Choose one setup path:
-
Option 1 (full AMFI disable):
sudo nvram boot-args="amfi_get_out_of_my_way=1 -v" -
Option 2 (debug restrictions only): use Recovery mode
csrutil enable --without debug(no full SIP disable), then install/loadamfidontoramfreewhile keeping AMFI otherwise enabled. For this repo,make amfidont_allow_vphonepackages the required encoded-path and CDHash allowlist startup (if using amfidont).
Q: make boot / make boot_dfu starts and then fails with VZErrorDomain Code=2 "Virtualization is not available on this hardware."
The host itself is running inside an Apple virtual machine, so nested
Virtualization.framework guest boot is unavailable. Run the boot flow on a
non-nested macOS 15+ host instead. make boot_host_preflight will show this as
Model Name: Apple Virtual Machine 1 with kern.hv_vmm_present=1.
make boot / make boot_dfu now fail fast through boot_binary_check before
attempting VM startup on that kind of host.
Q: System apps (App Store, Messages, etc.) won't download or install.
During iOS setup, do not select Japan or European Union as your region. These regions enforce additional regulatory checks (e.g., sideloading disclosures, camera shutter requirements) that the virtual machine cannot satisfy, which prevents system apps from being downloaded and installed. Choose any other region (e.g., United States) to avoid this issue.
Q: I'm stuck on the "Press home to continue" screen.
Connect via VNC (vnc://127.0.0.1:5901) and right-click anywhere on the screen (two-finger click on a Mac trackpad). This simulates the home button press.
Q: How do I get SSH access?
Install openssh-server from Sileo (available on the jailbreak variant after first boot).
Q: SSH doesn't work after installing openssh-server.
Reboot the VM. The SSH server will start automatically on the next boot.
Q: Can I install .tipa files?
Yes. The install menu supports both .ipa and .tipa packages. Drag and drop or use the file picker.
Q: Can I update to a newer iOS version?
Yes. Override fw_prepare with the IPSW URL for the version you want:
export IPHONE_SOURCE=/path/to/some_os.ipsw
export CLOUDOS_SOURCE=/path/to/some_os.ipsw
make fw_prepare
make fw_patch
Our patches are applied via binary analysis, not static offsets, so newer versions should work. If something breaks, ask AI for help.
Q: I used restore_offline and I am stuck in the setup screen
The device is trying to contact apple for the setup, and you are probably not connected to the internet if you used restore_offline.
You can bypass most of the setup screen by making the device supervised:
python3 -m pymobiledevice3 profile supervise vphone
Automation
vphone-cli exposes a host control socket (vm/vphone.sock) for programmatic VM interaction — screenshots, touch injection, swipe gestures, hardware keys, and clipboard. Every action returns a compact grayscale screenshot inline, enabling AI-driven E2E testing workflows.
See vphone-mcp for an MCP server that wraps this socket with high-level tools (open apps by name, navigate back, scroll, type text) usable from Claude Code or Claude Desktop.
