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
2026-05-24 18:18:52 +03:00
2026-03-11 04:22:50 +08:00
2026-03-01 02:47:52 +09:00
2026-03-04 13:34:02 +08:00
2026-03-11 04:22:50 +08:00
2026-05-24 18:18:52 +03:00

vphone-cli

Boot a virtual iPhone (iOS 26) via Apple's Virtualization.framework using PCC research VM infrastructure.

poc

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.sh LaunchDaemon. 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 enable
    

    After 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 amfidont or amfree

    In Recovery:

    csrutil enable --without debug
    csrutil allow-research-guests enable
    

    After 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_vphone
    

    This helper computes the current signed vphone-cli CDHash and uses the URL-encoded project path form observed by AMFIPathValidator.

The Patchless variant requires either the use of option 1 or amfidont with the -S flag (sudo amfidont -S --path [PATH_TO_VPHONE_DIR])

Install dependencies:

You will need both brew dependencies and Git Submodule dependencies.

  1. 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.

  1. 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:

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 pull to 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/load amfidont or amfree while keeping AMFI otherwise enabled. For this repo, make amfidont_allow_vphone packages 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.

Acknowledgements

S
Description
No description provided
Readme MIT
93 MiB
Languages
Swift 58.6%
Objective-C 17%
Shell 11.6%
Python 11.5%
Makefile 1.3%