iOS Researcher
‹ Back to home

Patching Metal Graphics into vphone

Feb 2026 wh1te4ever/super-tart-vphone

wh1te4ever's vphone VM gets a working paravirt GPU, but Metal needs a shader compiler plugin trapped in the PCC dyld shared cache — extracted images have no relocations and can't load. Clean-room reimplementation from RE, ~230 lines of C++.

main.mm build.sh

Disclaimer: No Apple software is redistributed here. The source files are an original, clean-room reimplementation containing only function prototypes and new code. No binaries, libraries, or code from Apple's dyld shared cache, disk images, or SDKs are included. The dylib links against nothing at build time — all symbols are resolved at runtime via -undefined dynamic_lookup against libraries already present on the device.

Overview

vphone by wh1te4ever gets a working paravirtualized GPU from the kernel driver, but Metal won't initialize without a userspace shader compiler plugin. Apple ships one in the PCC dyld shared cache, but it can't be extracted — the Mach-O comes out with broken relocations and dyld won't load it. This is a clean-room reimplementation of that plugin (~230 lines of C++) built from reverse engineering the original. It compiles against the device's libLLVM and libGPUCompiler at runtime, and gets installed into the Metal bundle on rootfs via SSH ramdisk.

All of the VM infrastructure — the paravirtualized GPU driver, the boot chain, the SSH ramdisk, fix_boot.py, and everything else that makes this possible — is wh1te4ever's work. This post only covers the compiler plugin shim.

AI-assisted install

Save main.mm and build.sh to CFW/libAppleParavirtCompilerPluginIOGPUFamily/, then feed the following to Claude Code, Codex, or similar:

# Prompt for AI agent:
#
# The vphone VM needs the Metal compiler plugin built and installed.
# Source: CFW/libAppleParavirtCompilerPluginIOGPUFamily/main.mm
# Build:  ./build.sh
#
# Steps:
#   1. Run ./build.sh to compile the dylib
#   2. Get VM IP: tart ip vphone — use for all SSH/SCP below
#   3. Boot DFU:  ./.build/debug/tart run vphone --dfu &
#   4. Boot SSH ramdisk:  cd CFW && ./boot_rd.sh
#   5. SSH in (pw: alpine), mount rootfs:
#        /sbin/mount_apfs -o rw /dev/disk1s1 /mnt1
#   6. SCP the dylib into the bundle dir on rootfs
#   7. Re-sign the bundle on device:
#        ldid -S /mnt1/System/Library/Extensions/AppleParavirtGPUMetalIOGPUFamily.bundle/libAppleParavirtCompilerPluginIOGPUFamily.dylib
#   8. Halt:  sync && /sbin/halt
#   9. Kill tart, normal boot:  tart run vphone --vnc-experimental
#
# The rest of the bundle is already on rootfs from fix_boot.py.

Manual install

# build
cd CFW/libAppleParavirtCompilerPluginIOGPUFamily
chmod +x build.sh && ./build.sh

# get VM IP
VM_IP=$(tart ip vphone)

# boot into DFU, then SSH ramdisk
./.build/debug/tart run vphone --dfu &
cd CFW && ./boot_rd.sh

# mount rootfs read-write
ssh root@$VM_IP "/sbin/mount_apfs -o rw /dev/disk1s1 /mnt1"

# copy dylib into the Metal bundle
scp libAppleParavirtCompilerPluginIOGPUFamily.dylib \
    root@$VM_IP:/mnt1/System/Library/Extensions/AppleParavirtGPUMetalIOGPUFamily.bundle/

# re-sign the dylib on device
ssh root@$VM_IP "ldid -S /mnt1/System/Library/Extensions/AppleParavirtGPUMetalIOGPUFamily.bundle/libAppleParavirtCompilerPluginIOGPUFamily.dylib"

# halt, then normal boot
ssh root@$VM_IP "sync && /sbin/halt"
./.build/debug/tart run vphone --vnc-experimental

SSH password: alpine

Verify


Why this is necessary

The VM's paravirtualized GPU kernel driver works fine on its own. Metal, however, requires a userspace compiler plugin at:

/System/Library/Extensions/AppleParavirtGPUMetalIOGPUFamily.bundle

The main binary, Info.plist, and code signature all copy cleanly from the PCC disk image. The compiler plugin is the problem — it only exists inside the PCC dyld_shared_cache.

Extracting it doesn't help. The extracted Mach-O has cache-relative vmaddrs, no LC_DYLD_CHAINED_FIXUPS, and no rebase/bind opcodes. dyld refuses to load it. Without the plugin, backboardd crashes on launch and you get a black screen.

What the plugin does

Four exported functions:

Internally these call into libGPUCompiler and libLLVM from the device DSC. Two linker flags matter:

-miphoneos-version-min=26.1 # must match or be ≤ the device's iOS version
-undefined dynamic_lookup   # resolve symbols at runtime from the DSC
-not_for_dyld_shared_cache  # don't try to slide into the shared cache

Important: Without -miphoneos-version-min, the deployment target defaults to your Xcode SDK version. If that's newer than the device's iOS (e.g. SDK 26.2 vs device on 26.1), dyld will refuse to load the dylib. Match the flag to your vphone's iOS version.

RE notes

Reversed from the PCC dyld shared cache. The original has 47 functions, mostly C++ thunks wrapping LLVM calls.

The AppleParavirtCompiler class has a vtable and 3 instance members. arm64e auth_stubs made it straightforward to confirm the full import set — every cross-image call goes through a PAC stub with the target symbol name in the GOT.

The shader entry point search walks air.vertex, air.fragment, and air.kernel metadata nodes in the AIR module. The original implementation uses the C++ LLVM API directly; the reimplementation uses the stable C API instead (exported since LLVM 3.x), which avoids ABI fragility across toolchain versions.