Firmware Manifest & Component Origins
The erase install firmware is a hybrid of three source sets:
- PCC vresearch101ap — boot chain (LLB/iBSS/iBEC/iBoot) and security monitors (SPTM/TXM)
- PCC vphone600ap — runtime components (DeviceTree, SEP, KernelCache, RecoveryMode)
- iPhone 17,3 — OS image, trust caches, filesystem
The VM hardware identifies as vresearch101ap (BDID 0x90) in DFU mode, so the
BuildManifest identity must use vresearch101ap fields for TSS/SHSH signing. However,
runtime components use the vphone600 variant because its DeviceTree sets MKB dt=1
(allows boot without system keybag), its SEP firmware matches the vphone600 device tree,
and hardware target reports as vphone600ap for proper iPhone emulation.
vphone-cli fw prepare resolves both IPSWs, merges cloudOS firmware into the
iPhone restore directory, then generates the hybrid BuildManifest in Swift.
1. Multi-Source IPSW Comparison
Identity Count Overview
26.0 and 26.0.1 have no matching public PCC vphone600 drop in this repo's tested set, so
it is prepared as iPhone 26.0 or 26.0.1 userspace plus the 26.1 PCC boot/runtime stack.
The 26.0 and 26.0.1 IOMobileFramebuffer dyld-cache patch in CFW install fixes the
only observed GUI ABI mismatch (SwapEnd payload size 0x548 -> 0x560).
CloudOS 26.1 Identity Structure (6 identities)
Key distinctions:
- CloudOS[2] vs [4] (vphone600ap): [2] uses RELEASE boot chain + release kernelcache; [4] uses RESEARCH_RELEASE + research kernelcache + txm.iphoneos.research.im4p
- CloudOS[3] vs [5] (vresearch101ap): Same pattern — [3] is RELEASE, [5] is RESEARCH_RELEASE
- vphone600ap has components vresearch101ap lacks: RecoveryMode, AppleLogo, Battery*, RestoreLogo, SEP (vphone600 variant)
- vresearch101ap has only 20 manifest keys (no UI assets, no RecoveryMode)
vphone600ap vs vresearch101ap Key Differences
2. Component Source Tracing
Boot Chain (from PCC vresearch101ap)
Security Monitors (from PCC, shared across board configs)
Runtime Components (from PCC vphone600ap)
Important: KernelCache (installed to disk, patched) uses the research variant. RestoreKernelCache (used during restore process only) uses the release variant. Only vphone600ap identities carry RecoveryMode — vresearch101ap does not.
OS / Filesystem (from iPhone)
Ramdisk (from PCC)
3. Why the Hybrid Approach
Why Not All-vresearch101?
The vresearch101ap device tree sets MKB dt=0, causing a fatal keybag error during boot:
Also missing the RecoveryMode entry.
Why Not All-vphone600?
The DFU hardware identifies as BDID 0x90 (vresearch101ap). Using vphone600ap identity
(BDID 0x91) fails TSS/SHSH signing and idevicerestore identity matching
(Unable to find a matching build identity).
Solution
vresearch101ap identity fields for DFU/TSS + vphone600 runtime components for a working
boot environment. The vphone600ap device tree sets dt=1, allowing boot without a
pre-existing system keybag:
The SEP firmware must match the device tree (vphone600 SEP with vphone600 DT).
4. Patched Components Summary
All 6 patched components in fw_patch.py come from PCC (cloudOS):
All 4 CFW-patched binaries in patchers/cfw.py / cfw_install.sh come from iPhone:
5. idevicerestore Identity Selection
Source: idevicerestore/src/idevicerestore.c lines 2195-2242
Matching Algorithm
idevicerestore selects a Build Identity by iterating through all BuildIdentities and returning the first match based on two fields:
Info.DeviceClass— case-insensitive match against devicehardware_modelInfo.Variant— substring match against the requested variant string
For DFU erase restore, the search variant is "Erase Install (IPSW)" (defined in idevicerestore.h).
Matching Modes
Critical: Partial matching excludes variants containing "Research". This means:
"Darwin Cloud Customer Erase Install (IPSW)"— matches (contains "Erase Install (IPSW)", no "Research")"Research Darwin Cloud Customer Erase Install (IPSW)"— skipped (contains "Research")
What idevicerestore Does NOT Check
- ApBoardID / ApChipID (used after selection, not for matching)
- Identity index or count (no hardcoded indices)
Conclusion for Single Identity
A BuildManifest with one identity works fine. The loop iterates once, and if DeviceClass and Variant match, it's returned. No minimum identity count required.
6. TSS/SHSH Signing
The TSS request sent to gs.apple.com includes:
ApBoardID = 144(0x90) — must match vresearch101apApChipID = 65025(0xFE01)Ap,ProductType = ComputeModule14,2Ap,Target = VRESEARCH101AP- Digests for all 21 manifest components
Apple's TSS server signs based on these identity fields + component digests. Using vphone600ap identity (BDID 0x91) would fail because the DFU device reports BDID 0x90.
7. Final Design: Single DFU Erase Identity
Since vphone-cli always boots via DFU restore, only one Build Identity is needed.
Identity Metadata (fw_manifest.py)
Source Variable Map
All 21 Manifest Entries
Full Manifest Component List
8. Restore.plist
TL;DR
Boot chain = vresearch101 (matches DFU hardware); runtime = vphone600 (keybag-less boot); OS = iPhone.
The firmware is a PCC shell wrapping an iPhone core. The vresearch101 boot chain handles DFU/TSS signing. The vphone600 device tree + SEP + kernel provide the runtime environment. The iPhone userland is patched post-install for activation bypass, jailbreak tools, and persistent SSH/VNC.
9. DeviceTree identity rewrite
The vphone600ap DT carries 13 properties whose values contain the literals
vphone600 / VPHONE600 / iPhone99,11 / vmapple / vresearch. Most are
load-bearing (kernel kext-matching, restore-time identity, IORegistry path
structure) and cannot be rewritten without breaking boot or restore.
DeviceTreePatcher rewrites the 8 that are either userland-facing identity
fields or SoC-generation descriptors with non-binding semantics.
Properties intentionally left at vphone600 values (with the empirical reason for each):
device-tree.target-type("VPHONE600") — Tier 2 attempt broke restore; iBoot / restored_external read this and reject the device when the value doesn't match the BuildManifest's signed identity.device-tree.model("iPhone99,11") — Tier 1 attempt broke restore; same reason astarget-type.device-tree.compatible[0]("VPHONE600AP") — IOKit's platform-expert matching at boot binds against the FIRST entry ofcompatible. Rewriting it toD47APforces_PE_init_platform_expertto look for a D47AP-claiming kext, which doesn't exist in our kernelcache. Panic guaranteed.device-tree/arm-io.compatible/device_type/soc-generation(vmapple1 / vresearch1-io / VResearch1) — bind the vmapple1 IO controller kext, GIC, and PCIe drivers. Rename → no kext claims the SoC IO → boot panic.device-tree/arm-io/gic.compatible,device-tree/arm-io/pcie.compatible— same family as arm-io entries.device-tree/product/vphone600-gestalt-variants(node name) — referenced by libMobileGestalt-equivalent code that looks up the subtree by literal node name. Rename breaks the lookup.
Compatible multi-string surgical rewrite (#3). The original blob is:
Patched (slot length preserved at 48):
AppleVirtualPlatformARM shifts 1 byte earlier but every consumer walks
the blob by NUL-terminators, none depend on a fixed byte offset within
the property. IOKit's platform-expert match against VPHONE600AP (first
entry) is unaffected; the second entry, which userland walks of the
compatible[] list see when enumerating identifiers, now reports
iPhone17,3.
Risk classification.
- LOW (#3, #11, #12) — read only by userland identity APIs (libMobileGestalt, IORegistry queries, App Store device class). No iBoot / restored_external / TSS dependency.
- HIGHER (#2, #10) —
target-sub-typeis in the same restore-signed family astarget-type(already proven to break restore);fdr-product-typeis read by Factory-Data-Restore code paths that may check it against the BuildManifest. If restore fails after a build that enables these, remove them first and try the remaining LOW-risk three in isolation.
Wiring: all 5 patches are declared in
Sources/FirmwarePatcher/DeviceTree/DeviceTreePatcher.swift under
propertyPatches. The serializer rebuilds the entire tree on every run,
so each new value is written at the same slot length as the original.
The compatible patch uses a new PropertyValue.bytes(Data) case that
takes a pre-built byte blob (necessary because the multi-string layout
contains embedded NULs).
10. Post-restore DT identity rewrite (EXP-JB-6)
The properties under "known-bad from prior rounds" (root model, root
target-type) cannot be edited at fw_patch time because
restored_external / iBoot's restore mode cross-checks them against the
BuildManifest's signed SupportedProductTypes. But that cross-check
fires only during installation. After restore completes, the boot
path is
The image4 bypass patches we already ship are what allow every fw_patch-time IM4P modification to boot at all — they make the IM4M's stored digest irrelevant on subsequent boots. So an additional IM4P modification applied after restore completes is in-policy with the project's existing trust-chain bypass.
The EXP-JB-6 install step exploits this (in cfw_install_exp.sh
only — JB and DEV variants do NOT run it). While the install pipeline
still has /mnt5 (the preboot volume) mounted in ramdisk mode, it:
- Pulls
/mnt5/<boot-hash>/usr/standalone/firmware/devicetree.img4to the host. - Runs
scripts/patchers/cfw_patch_post_restore_dt.py, which:- Unwraps img4 → IM4P → decompresses LZFSE → DT blob (via pyimg4).
- Applies the three restore-unsafe edits (table below).
- Re-compresses LZFSE → repacks IM4P → repacks IMG4 with the original IM4M.
- Pushes the modified img4 back to the same path.
- The device reboots out of ramdisk; iBoot loads the modified DT;
kernel populates
machine_infofrom the new property values.
The compatible rewrite is a reorder, not a replacement: VPHONE600AP
stays in the list (now as the second entry), so IOKit's platform-expert
match against the AppleVMApple1IO kext still binds. compatible[0]
becomes D47AP, which is what userland's hw.model resolves to.
Userland-visible effects after the 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 reflects the new ProductType.
- Every
MGCopyAnswerkey backed byIOPlatformExpertDeviceproperties picks up the new values.
Idempotence: the patcher reads the existing DT, applies edits, and writes back only if there's a difference. A second run on an already-patched devicetree.img4 detects target-state-already-met and exits without rewriting.
Wiring:
scripts/patchers/cfw_patch_post_restore_dt.py— host-side patcher. Uses pyimg4 for img4↔IM4P+LZFSE round-trip; mirrors the DT format parser/serializer fromDeviceTreePatcher.swift.scripts/cfw_install_exp.sh [EXP-JB-6]— install step that runs after JB-5 (LaunchDaemon deploy) and before the CLEANUP/umount block, in the EXP install script only. Discovers the boot-manifest-hash via the sameget_boot_manifest_hashhelper used by earlier install steps. Tolerates a missing devicetree.img4 with warn-and-skip rather than aborting the install. JB and DEV install scripts do NOT carry this step.
Why this is restore-safe (and the fw_patch-time version was not):
- fw_patch-time path modifies the IM4P bytes that are sent during
restore. iBoot's restore mode reads each IM4P and (via the image4
validation callback the iBSS/iBEC/LLB patches already neuter) accepts
its digest mismatch — but some other path inside
restored_externalenforces the top-levelSupportedProductTypescheck independently and rejects DT.model values that don't appear in that list. - EXP-JB-6 post-restore path never re-runs the BuildManifest check. Subsequent boots validate IM4P contents against the IM4M only — which the image4 bypass patches already wave through. So the same byte change that's fatal at restore is harmless after restore.
11. Build-version rewrite (EXP-JB-7, opt-in)
Opt-in step. EXP-JB-7 runs only when the SPOOF_BUILD environment
variable is set — e.g. make setup_machine EXP=1 SPOOF_BUILD=23F77, or
make cfw_install_exp SPOOF_BUILD=23F77 if you're re-running just the
CFW phase. When omitted/empty the step is skipped entirely and the
build identifier stays at whatever the IPSW shipped. Only the EXP
install script runs this step; JB and DEV variants are not affected.
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:
The EXP-JB-7 install step rewrites the ProductBuildVersion key in both
plists to a target value (currently 23F77):
ProductVersion (26.1), ProductName (iPhone OS), BuildID,
SystemImageID, and ProductCopyright are left untouched.
Wiring:
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]— install step that runs after EXP-JB-6 (post-restore DT) and before CLEANUP, in the EXP install script only. For each of the two plist paths it scp_from's the file to host, runs the patcher with target23F77, scp_to's the file back. Tolerates missing-on-device with warn+continue. JB and DEV install scripts do NOT carry this step.
What this does not flip:
sysctl kern.osversion— populated at boot from a kernel global initialized from boot args, not fromSystemVersion.plist. The kernel image we ship was built against23B78(the PCC vphone600 / vresearch101 build identifier; visible in 621 kext-Info.plist embeddedDTPlatformBuild/DTSDKBuildblobs inside the kernelcache but never read bykern.osversionat runtime). To flipkern.osversionwe'd need to either rebuild the kernelcache with a differentOS_BUILD_VERSIONor patch the kernel boot args path — out of scope for this round.SystemVersionCompat.plist— carries23B34a/19.1, a legacy iOS-19 marker forMacCatalyst-style version queries. Not user- visible, deliberately untouched.- DSC dylib internals — the apparent
23B85byte sequence in DSC chunk.21is a Swift mangled-name UUID-uniquing substring (_TtC11MediaCoreUIP33_98519F523B8515A67EEFBCB0824D82807Counter), not a build identifier. Patching it would corrupt a Swift symbol name.
Order of operations: this step runs after EXP-JB-6 (the DT identity
rewrite) so that any post-restore identity work targeting /mnt5 has
completed before we modify the Cryptex's SystemVersion.plist (which
lives on the same volume).