Patch Comparison: Regular / Development / Jailbreak / Experimental

May 24, 2026 · View on GitHub

EXP is a JB superset. Everything in the baseline tables below that is Y for JB is also Y for EXP. The columns are kept at three variants to avoid noise — the only place EXP and JB diverge is the Experimental additions below, all of which are EXP-only (JB and the other variants are deliberately unaffected). The EXP-only items, taken together:

  • KernelKernelEXPPatcher runs the hv_vmm_present sysctl OID rename plus kernel-internal caller cstring/sandbox-profile-token mangle (formerly wired into JB Group B as JB-26 — moved out).
  • DeviceTree at fw_patch time — 8 identity-rewrite property patches (Tier 1b + 1c) flipping userland-visible identity surfaces toward D47AP / iPhone17,3.
  • DSC user-mode — byte-5 cstring mangle of kern.hv_vmm_present with a sign-in blacklist + per-page slot re-attestation (cfw_patch_hv_vmm_dsc.py), companion to the kernel rename.
  • watchdogd (EXP-JB-3.5) — surgical 2-instruction patch + slot re-attest; forces the cached "am I a VM?" byte to 1 so watchdogd's clean-exit branch runs.
  • Post-restore DT rewrite (EXP-JB-6) — host-side rewrite of devicetree.img4 on the ramdisk's mounted rootfs for the three restore-fatal identity properties (root model, target-type, compatible[0]) that broke restore when applied at fw_patch time.
  • SystemVersion.plist ProductBuildVersion (EXP-JB-7, opt-in) — gated on SPOOF_BUILD=<id>. Rewrites the build identifier in the rootfs and cryptex copies of SystemVersion.plist.

Boot Chain Patches

AVPBooter

#PatchPurposeRegularDevJB
1mov x0, #0DGST signature validation bypassYYY

iBSS

#PatchPurposeRegularDevJB
1Serial labels (2x)"Loaded iBSS" in serial logYYY
2image4_validate_property_callbackSignature bypass (b.ne -> NOP, mov x0,x22 -> mov x0,#0)YYY
3Skip generate_nonceKeep apnonce stable for SHSH (tbz -> unconditional b)--Y

iBEC

#PatchPurposeRegularDevJB
1Serial labels (2x)"Loaded iBEC" in serial logYYY
2image4_validate_property_callbackSignature bypassYYY
3Boot-args redirectADRP+ADD -> serial=3 -v debug=0x2014e %sYYY
4Modern bootx-handoff panic bypassIBootPatcher.patchBootxPrecondition NOPs gate TBZ via structural anchor (no hash/line tied); no-op pre-26.4YYY
5Ramdisk boot-args overwriteramdisk_build.py:patch_ibec_bootargs rewrites string to ... rd=md0 ... wdt=-1 ... (ramdisk-send iBEC only)YYY

LLB

#PatchPurposeRegularDevJB
1Serial labels (2x)"Loaded LLB" in serial logYYY
2image4_validate_property_callbackSignature bypassYYY
3Boot-args redirectADRP+ADD -> serial=3 -v debug=0x2014e %sYYY
4Rootfs bypass (5 patches)Allow edited rootfs loadingYYY
5Panic bypassNOP cbnz after mov w8,#0x328 checkYYY

TXM

#PatchPurposeRegularDevJB
1Trustcache binary-search bypassbl hash_cmp -> mov x0, #0YYY
2Selector24 bypass: mov w0, #0xa1Return PASS (byte 1 = 0) after prologue-YY
3Selector24 bypass: b <epilogue>Skip validation, jump to register restore-YY
4get-task-allow (selector 41|29)bl -> mov x0, #1-YY
5Selector42|29 shellcode: branch to caveRedirect dispatch stub to shellcode-YY
6Selector42|29 shellcode: NOP padUDF -> NOP in code cave-YY
7Selector42|29 shellcode: mov x0, #1Set return value to true-YY
8Selector42|29 shellcode: strb w0, [x20, #0x30]Set manifest flag-YY
9Selector42|29 shellcode: mov x0, x20Restore context pointer-YY
10Selector42|29 shellcode: branch backReturn from shellcode to stub+4-YY
11Debugger entitlement (selector 42|37)bl -> mov w0, #1-YY
12Developer mode bypassNOP conditional guard before deny path-YY

Kernelcache

Base Patches (All Variants)

#PatchFunctionPurposeRegularDevJB
1NOP tbnz w8,#5_apfs_vfsop_mountSkip root snapshot sealed-volume checkYYY
2NOP conditional_authapfs_seal_is_brokenSkip root volume seal panicYYY
3NOP conditional_bsd_initSkip rootvp not-authenticated panicYYY
4-5mov w0,#0; ret_proc_check_launch_constraintsBypass launch constraintsYYY
6-7mov x0,#1 (2x)PE_i_can_has_debuggerEnable kernel debuggerYYY
8NOP_postValidationSkip AMFI post-validationYYY
9cmp w0,w0_postValidationForce comparison trueYYY
10-11mov w0,#1 (2x)_check_dyld_policy_internalAllow dyld loadingYYY
12mov w0,#0_apfs_graftAllow APFS graftYYY
13cmp x0,x0_apfs_vfsop_mountSkip mount checkYYY
14mov w0,#0_apfs_mount_upgrade_checksAllow mount upgradeYYY
15mov w0,#0_handle_fsioc_graftAllow fsioc graftYYY
16NOP (3x)handle_get_dev_by_roleBypass APFS role-lookup deny gates for boot mountsYYY
17-26mov x0,#0; ret (5 hooks)Sandbox MACF ops tableStub 5 sandbox hooksYYY
27PACIBSP→RET_thread_guard_violationDisable EXC_GUARD delivery (match production behavior)-Y-

JB-Only Kernel Methods (Reference List)

#GroupMethodFunctionPurposeJB Enabled
JB-01Apatch_amfi_cdhash_in_trustcacheAMFIIsCDHashInTrustCacheAlways return true + store hashY
JB-02Apatch_amfi_execve_kill_pathAMFI execve kill return siteConvert shared kill return from deny to allow (superseded by C21; standalone only)N
JB-03Cpatch_cred_label_update_execve_cred_label_update_execveReworked C21-v3: C21-v1 already boots; v3 keeps split late exits and additionally ORs success-only helper bits 0xC after clearing 0x3F00; still disabled pending boot validationN
JB-04Cpatch_hook_cred_label_update_execvesandbox mpo_cred_label_update_execve wrapper (ops[18] -> sub_FFFFFE00093BDB64)Faithful upstream C23 trampoline: copy VSUID/VSGID owner state into pending cred, set P_SUGID, then branch back to wrapperY
JB-05Cpatch_kcall10sysent[439] (SYS_kas_info replacement)Rebuilt ABI-correct kcall cave: target + 7 args -> uint64 x0; re-enabled after focused dry-run validationY
JB-06Bpatch_post_validation_additional_postValidation (additional)Disable SHA256-only hash-type rejectY
JB-07Cpatch_syscallmask_apply_to_procsyscallmask apply wrapper (_proc_apply_syscall_masks path)Faithful upstream C22: mutate installed Unix/Mach/KOBJ masks to all-ones via structural cave, then continue into setter; distinct from NULL-mask alternativeY
JB-08Apatch_task_conversion_eval_internal_task_conversion_eval_internalAllow task conversionY
JB-09Apatch_sandbox_hooks_extendedSandbox MACF ops (extended)Stub remaining 30+ sandbox hooks (incl. IOKit 201..210)Y
JB-10Apatch_iouc_failed_macfIOUC MACF shared gateA5-v2: patch only the post-mac_iokit_check_open deny gate (CBZ W0, allow -> B allow) and keep the rest of the IOUserClient open path intactY
JB-11Bpatch_proc_security_policy_proc_security_policyBypass security policyY
JB-12Bpatch_proc_pidinfo_proc_pidinfoAllow pid 0 infoY
JB-13Bpatch_convert_port_to_map_convert_port_to_map_with_flavorSkip kernel map panicY
JB-14Bpatch_bsd_init_auth_bsd_init rootauth-failure branchIgnore FSIOC_KERNEL_ROOTAUTH failure in bsd_init; same gate as base patch #3 when layeredY
JB-15Bpatch_dounmount_dounmountAllow unmount via upstream coveredvp cleanup-call NOPY
JB-16Bpatch_io_secure_bsd_rootAppleARMPE::callPlatformFunction ("SecureRootName" return select), called from IOSecureBSDRootForce "SecureRootName" policy return to success without altering callback flow; implementation retargeted 2026-03-06Y
JB-17Bpatch_load_dylinker_load_dylinkerSkip strict LC_LOAD_DYLINKER == "/usr/lib/dyld" gateY
JB-18Bpatch_mac_mount___mac_mountUpstream mount-role wrapper bypass (tbnz NOP + role-byte zeroing)Y
JB-19Bpatch_nvram_verify_permission_verifyPermission (NVRAM)Allow NVRAM writesY
JB-20Bpatch_shared_region_map_shared_region_map_and_slide_setupForce root-vs-process-root mount compare to succeed before Cryptex fallbackY
JB-21Bpatch_spawn_validate_persona_spawn_validate_personaUpstream dual-cbz persona helper bypassY
JB-22Bpatch_task_for_pid_task_for_pidAllow task_for_pid via upstream early pid == 0 gate NOPY
JB-23Bpatch_thid_should_crash_thid_should_crashPrevent GUARD_TYPE_MACH_PORT crashY
JB-24Bpatch_vm_fault_enter_prepare_vm_fault_enter_prepareForce cs_bypass fast path in runtime fault validationY
JB-25Bpatch_vm_map_protect_vm_map_protectSkip upstream write-downgrade gate in vm_map_protectY

EXP-Only Kernel Methods (Reference List)

Runs in KernelEXPPatcher.findAll() (chained after KernelPatcher + KernelJBPatcher for the .exp variant only — JB and other variants do NOT execute these).

#GroupMethodFunctionPurposeEXP Enabled
EXP-01Bpatch_hv_vmm_renamesysctl OID name cstring "hv_vmm_present""Xv_vmm_present" (Part A) + every kernel-internal occurrence of kern.hv_vmm_present cstring/sandbox-profile token mangled at byte 5 (Part B)Rename the kern.hv_vmm_present OID's name in place ('h' → 'X' at offset 0 of the 14-byte cstring). After this: sysctlbyname("kern.hv_vmm_present") returns ENOENT; sysctlbyname("kern.Xv_vmm_present") returns the original int value (1). Part B mangles every kernel-internal caller — AMFI, IOCryptoAcceleratorFamily, sandbox-profile token, apfs — so they keep hitting the renamed OID. Companion to the user-mode blacklist-flip mangle in cfw_patch_hv_vmm_dsc.py.Y

CFW Installation Patches

Binary Patches Applied Over SSH Ramdisk

#PatchBinaryPurposeRegularDevJB
1/%s.gl -> /AA.glseputilGigalocker UUID fixYYY
2NOP cache validationlaunchd_cache_loaderAllow modified launchd.plistYYY
3mov x0,#1; retmobileactivationdActivation bypassYYY
4Plist injectionlaunchd.plistbash/dropbear/trollvnc/vphoned daemonsYYY
5b (skip jetsam guard)launchdPrevent jetsam panic on boot-YY
6LC_LOAD_DYLIB injectionlaunchdLoad short alias /b (copy of launchdhook.dylib) at launch--Y
7cstring byte 5 mangle 'h' → 'X' ("kern.hv_vmm_present""kern.Xv_vmm_present") + per-page slot-hash re-attestation, BLACKLIST semantics — EXP onlyDSC dylibsCompanion to EXP kernel rename (KernelEXPPatcher.patchHvVmmRename). The mangle is applied to every DSC dylib EXCEPT those in DONT_PATCH_INSTALL_NAMES (sign-in / device-likeness consumers, ~15 entries). Patched dylibs query kern.Xv_vmm_present and get the truthful 1 (graphics / accel passthrough). Blacklisted dylibs keep the original cstring, hit ENOENT on the renamed kernel, cache 0, lie about VM presence. On codeSigningMonitor == 2 hardware the byte-mangle alone causes CODESIGNING/Invalid Page SIGKILL because TXM enforces per-page hashes; the re-attestation pass recomputes the SHA-256 slot in the chunk's CS_CodeDirectory for every modified 16 KiB page. See scripts/patchers/cfw_dsc_codesign.py and cfw_patch_hv_vmm_dsc.py.---
8(removed — was: standalone-binary mangle in 6 rootfs Mach-Os via SSH)n/aRemoved in the blacklist-flip redesign. With the EXP kernel rename in place, the 6 rootfs binaries (MobileActivationMigrator, CheckerBoard, StoreKitUISceneService, storekitd, appstored, CorePrescriptionService) get the desired "cache 0 / not in a VM" behavior for free: they keep their original cstring, hit ENOENT on the renamed kernel sysctl, defensive cbnz w0, skip leaves the cached byte at BSS-zero. No SSH-time standalone patch needed.---

Installed Components

#ComponentDescriptionRegularDevJB
1Cryptex SystemOS + AppOSDecrypt AEA + mount + copy to deviceYYY
2GPU driverAppleParavirtGPUMetalIOGPUFamily bundleYYY
3iosbinpack64Jailbreak tools (base set)YYY
4iosbinpack64 dev overlayReplace rpcserver_ios with dev build-Y-
5vphonedvsock HID/control daemon (built + signed)YYY
6LaunchDaemonsbash/dropbear/trollvnc/rpcserver_ios/vphoned plistsYYY
7Procursus bootstrapBootstrap filesystem + optional Sileo deb--Y
8BaseBin hookssystemhook.dylib / launchdhook.dylib / libellekit.dylib -> /cores/ plus /b alias for launchdhook.dylib--Y
9TweakLoader.dylibLean user-tweak loader built from source and installed to /var/jb/usr/lib/TweakLoader.dylib--Y

kern.hv_vmm_present user-mode patcher (EXP only)

Companion to the EXP kernel patcher (KernelEXPPatcher.patchHvVmmRename). 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, and defensively cache 0 ("not running on a VM") for sign-in / device-attestation surfaces. Source-of-truth research: research/hv_vmm_present_usermode_xrefs.md.

JB and other variants are NOT affected by this patcher.

Patch shape (every site) — cstring mangle:

Before (cstring section bytes, 20 bytes total):
    "kern.hv_vmm_present\0"
    6B 65 72 6E 2E 68 76 5F 76 6D 6D 5F 70 72 65 73 65 6E 74 00

After (1 byte change at offset 0):
    "Xern.hv_vmm_present\0"
    58 65 72 6E 2E 68 76 5F 76 6D 6D 5F 70 72 65 73 65 6E 74 00
    ^^

The kernel's name-to-MIB translation fails with ENOENT when the caller asks for "Xern.hv_vmm_present", so sysctlbyname returns -1. The canonical post-call check (cbnz w0, skip or cmp w0,#0 ; b.ne skip) then takes the skip-cache path; the cached "is_vmm" byte stays at its initial value (BSS-zero = 0).

We don't modify executable code at all — only one byte of read-only string data. The kernel call still happens (with the wrong name), so any sysctl-tracing infrastructure can still see activity.

Idempotent: a re-scan for the literal "kern.hv_vmm_present\0" finds no occurrences in already-mangled dylibs, so the patcher does no work on a re-run.

DSC-side patches — driven by an explicit whitelist (PATCH_INSTALL_NAMES in scripts/patchers/cfw_patch_hv_vmm_dsc.py) applied to chunks under SystemOS/System/Library/Caches/com.apple.dyld/. Comment a line in the whitelist to skip that dylib on the next install — useful for bisecting which consumer is responsible for an observable change.

DylibComponent role (paraphrased)
usr/lib/libMobileGestalt.dylibBacks MGCopyAnswer("hv-vmm-present") — highest fan-in
PrivateFrameworks/AAAFoundation.framework/AAAFoundationApple ID anti-abuse plumbing
PrivateFrameworks/AuthKit.framework/AuthKitSign-in-with-Apple-ID / iCloud auth
PrivateFrameworks/IDSFoundation.framework/IDSFoundationApple Identity Service core (iMessage / FaceTime backbone)
PrivateFrameworks/DeviceIdentity.framework/DeviceIdentityDevice-binding / device class identity
PrivateFrameworks/DeviceCheckInternal.framework/...DeviceCheck attestation
PrivateFrameworks/MobileActivation.framework/...Activation flow
PrivateFrameworks/ApplePushService.framework/...APNS client (claims device characteristics on connect)
PrivateFrameworks/AppStoreUtilities.framework/...Store / IAP support
PrivateFrameworks/CorePrescription.framework/...Health prescription store sync gate
PrivateFrameworks/CoreCDP.framework/CoreCDPCDP (cloud key-vault / iCloud Drive plumbing)
PrivateFrameworks/EmailFoundation.framework/...Mail account heuristics
PrivateFrameworks/PhotoFoundation.framework/...Photos asset visibility heuristics
PrivateFrameworks/FindMyBase.framework/FindMyBaseFind My anti-spoof
PrivateFrameworks/AirPlaySupport.framework/...AirPlay receiver gate
PrivateFrameworks/TrialServer.framework/TrialServerA/B / trial-rollout exclude-VM gate
PrivateFrameworks/VisionKitCore.framework/VisionKitCoreVisionKit
PrivateFrameworks/DVTInstrumentsUtilities.framework/...Xcode Instruments support
PrivateFrameworks/WatchdogServiceManagement.framework/...Watchdog manager
Frameworks/CoreVideo.framework/CoreVideoCoreVideo pipeline

Standalone-binary patches (6 files, applied to the device rootfs over SSH)

PathRole
/System/Library/DataClassMigrators/MobileActivationMigrator.migrator/MobileActivationMigratorActivation migration helper
/Applications/CheckerBoard.app/CheckerBoardApple internal accessibility test app
/Applications/StoreKitUISceneService.app/StoreKitUISceneServiceStoreKit UI host
/System/Library/Frameworks/StoreKit.framework/Support/storekitdStoreKit / IAP daemon
/System/Library/PrivateFrameworks/AppStoreDaemon.framework/Support/appstoredApp Store daemon
/System/Library/PrivateFrameworks/CorePrescription.framework/XPCServices/CorePrescriptionService.xpc/CorePrescriptionServiceCorePrescription XPC service

Explicitly NOT patched (compute / accel — patching here turns off VM fast-paths that exist so the lib doesn't try to touch real silicon ANE / AGX / hardware codecs):

System/Library/Frameworks/CoreML.framework/CoreML
System/Library/PrivateFrameworks/Espresso.framework/Espresso
System/Library/PrivateFrameworks/AppleNeuralEngine.framework/AppleNeuralEngine
System/Library/PrivateFrameworks/CoreRE.framework/CoreRE
System/Library/PrivateFrameworks/RenderBox.framework/RenderBox
System/Library/PrivateFrameworks/WebGPU.framework/WebGPU
System/Library/PrivateFrameworks/caulk.framework/caulk
System/Library/PrivateFrameworks/IOSurfaceAccelerator.framework/IOSurfaceAccelerator
System/Library/ExtensionKit/Extensions/HostInferenceProviderService.appex/HostInferenceProviderService

Wiring

  • scripts/patchers/cfw_patch_hv_vmm.py — standalone cstring patcher (used for the on-device files): finds the "kern.hv_vmm_present\0" cstring in the Mach-O's __cstring section and rewrites its first byte ('k''X').
  • scripts/patchers/cfw_dsc_chunks.py — chunked-DSC byte-level helper (DSCChunks(chunks_dir)): vmaddr↔chunk-fileoff mapping, cstring scan over executable mappings, byte read/write at a vmaddr, and Mach-O header walk-back to resolve a vmaddr to the dylib install name (LC_ID_DYLIB).
  • scripts/patchers/cfw_patch_hv_vmm_dsc.py — DSC-native orchestrator. No external ipsw dependency. For every "kern.hv_vmm_present\0" occurrence in any executable mapping, walks back to the containing dylib's Mach-O header, reads LC_ID_DYLIB, and — if the install name is in the explicit PATCH_INSTALL_NAMES whitelist — rewrites the first byte of the cstring through DSCChunks.write_at_vma. Pure Python. Whitelist-based by design so an operator can comment out individual entries to bisect.
  • scripts/patchers/cfw.py patch-hv-vmm <binary> — standalone-Mach-O subcommand (used for the 6 on-device files).
  • scripts/patchers/cfw.py patch-hv-vmm-dsc <chunks_dir> — DSC subcommand (used while the SystemOS Cryptex DMG is still mounted on the host, before the device copy).
  • scripts/patch_hv_vmm_userland.sh — thin wrapper used by the install scripts.
  • scripts/cfw_install_exp.sh — EXP install script. Pre-step before invoking cfw_install.sh: decrypts the SysOS Cryptex into the cache location cfw_install.sh already uses, mounts it, applies the DSC patch, unmounts. The unmodified cfw_install.sh then sees the cached (already-patched) DMG. Standalone watchdogd is patched later via SSH at step [EXP-JB-3.5].
  • scripts/cfw_install_jb.sh and scripts/cfw_install_dev.sh — unchanged from pre-experimental baseline. Neither runs the DSC patcher.

kern.hv_vmm_present kernel patcher — Part A + Part B (EXP only)

The KernelEXPPatchHvVmmRename Swift patcher (in KernelEXPPatcher.findAll(), chained after KernelPatcher and KernelJBPatcher for the .exp variant only) renames the sysctl OID and rewrites every kernel-internal occurrence of the old name so kexts continue to find it under the new name. Two parts. JB and other variants do NOT run this patcher.

Part A — OID name rename. Finds the OID's oid_name cstring as the NUL-delimited bytes \0hv_vmm_present\0 (exactly one match required in the kernelcache; on iPhone17,3 / iOS 26.1 this lives at file offset 0x964e0 inside com.apple.kernel). Flips byte 0 of the cstring 'h' (0x68) → 'X' (0x58). After the patch, the kernel's sysctl_register_oid keeps the OID's MIB and value (1) intact but the name resolver returns ENOENT for kern.hv_vmm_present and returns 1 for kern.Xv_vmm_present.

Part B — kernel-internal caller mangle. After Part A, any kernel-side sysctlbyname("kern.hv_vmm_present", …) call gets ENOENT and falls into the caller's "not in a VM" branch — which on the bring-up build caused AMFI to panic with AMFI: No PMGR? (ConfigurationSettings.cpp:388) during ramdisk boot. Part B mangles every kernel-internal occurrence of the kern.hv_vmm_present name so callers continue to find the renamed OID. The mangle flips byte 5 of the inner cstring ('h' after kern.) → 'X', producing kern.Xv_vmm_present.

Two byte-aligned forms are searched, both anchored at the kern.hv_vmm_present substring:

FormNeedleWhere it livesMangle delta within needle
(i) NUL-delimited cstring\0kern.hv_vmm_present\0__TEXT,__cstring of any kext that calls sysctlbyname by full name+6 (skip leading NUL + 5)
(ii) Sandbox-profile name tokenkern.hv_vmm_present\x0fInside a compiled sandbox-profile blob within com.apple.security.sandbox. The \x0f byte is the sandbox-profile end-of-name marker; the token has no leading NUL.+5

On iPhone17,3 / iOS 26.1 / 23B85 the universe is 5 occurrences (verified by raw substring scan over the kernelcache buffer):

File offsetFileset entryForm
0x541d56com.apple.driver.AppleMobileFileIntegrity(i) cstring
0x81bdc3com.apple.iokit.IOCryptoAcceleratorFamily(i) cstring
0xa6618bcom.apple.security.sandbox(ii) sandbox-profile name token
0xbb0d55com.apple.security.sandbox(i) cstring
0xbce1f9com.apple.filesystems.apfs(i) cstring

Part B emits one patch record per match (5 total, plus Part A's 1) under patch IDs kernelcache_exp.hv_vmm_internal_caller_mangle and kernelcache_exp.hv_vmm_oid_rename. Idempotent: a re-run detects already-mangled bytes (kern.Xv_vmm_present instead of kern.hv_vmm_present) and reports the patch as already applied.

Note on the sandbox-profile occurrence. This was missed by the original Part B because its needle required NUL on both sides. The sandbox-profile blob stores OID names as TLV-framed tokens where the trailing byte is \x0f (sandbox EOT) rather than a NUL. Without the second needle, sandboxed callers that interpret the profile's kern.hv_vmm_present-matching rule would still match against the OLD name, while the OID itself has been renamed — so the rule's ALLOW/DENY/audit action would never fire. With the second needle, the rule's name token is rewritten to kern.Xv_vmm_present and sandboxed callers that hit the renamed OID match the (rewritten) rule as intended. Covered occurrence verified on iPhone17,3 / iOS 26.1 / 23B85 at file offset 0xa6618b.

watchdogd surgical hv_vmm_present cache patch (EXP only)

Why a dedicated patch. After the EXP 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 (+0x58e0) takes a branch into a _os_crash wrapper that does 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 elsewhere doesn't apply here because we want this binary to behave as if the sysctl returned 1, not as if it returned ENOENT.

Patch shape. Two-instruction surgical edit at every site in watchdogd that has the canonical caching shape:

adrp x0, <page>
add  x0, x0, #<off>          ; "kern.hv_vmm_present"
...arg setup...
bl   _sysctlbyname
cbnz w0, <skip>              ; <-- patched: NOP
ldur w8, [x29, #-4]
cmp  w8, #0
cset wN, ne                  ; <-- patched: mov wN, #1
adrp xM, <page>
strb wN, [xM, #<imm>]        ; cached "am I a VM?" byte

Net effect: the cached byte is forced to 1 regardless of the sysctl result, and watchdogd's pre-existing "detected virtual machine environment, exiting..." clean-exit branch runs instead of the trap path. Two functions in watchdogd match this shape on iPhone17,3 / iOS 26.1; both are patched.

Code signing. The byte edit invalidates the SHA-256 slot hashes for the 4 KiB pages containing the modifications in watchdogd's own CS_CodeDirectory. The patcher recomputes those slot hashes in place via cfw_macho_codesign.reattest_modified_offsets (4 KiB page size read from the CD, correct tail-slot length, all present CDs). The resulting CD mutation also changes the cdHash, but the existing JB kernel patch patch_amfi_cdhash_in_trustcache accepts any cdHash, so AMFI's trust-cache check still passes at execve. The patcher does NOT re-sign with ldid — preserving the original Apple-issued code-signing identifier (com.apple.watchdogd) is required for launchd's boot-task identity validation; an earlier attempt to re-sign other rootfs binaries with ldid_sign tripped this check on mobile_obliterator.

Wiring.

  • scripts/patchers/cfw_macho_codesign.py — standalone-Mach-O page-hash re-attestation (parallel to cfw_dsc_codesign.py but parses LC_CODE_SIGNATURE directly, uses page size from the CD header, handles short tail slot, updates every present CD).
  • scripts/patchers/cfw_patch_watchdogd.py — capstone-anchored pattern matcher + Keystone-assembled 2-insn patch + slot reattest. Idempotent.
  • scripts/patchers/cfw.py patch-watchdogd <binary> — CLI subcommand.
  • scripts/patch_hv_vmm_userland.sh watchdogd <binary> — thin shim used by the install script.
  • 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.

DeviceTree identity properties at fw_patch time (EXP only)

DeviceTreePatcher carries two property-patch lists: basePropertyPatches (4 entries — serial-number, home-button-type, artwork-device-subtype, island-notch-location) applied for every variant, and identityPropertyPatches (8 entries) applied only when includeIdentityPatches is true, which FirmwarePipeline sets exactly when variant == .exp.

The 8 EXP-only identity properties (no restore-fatal ones — those go through EXP-JB-6 post-restore):

#Node pathPropertyOld → NewRisk
1device-treetarget-sub-typeVPHONE600APD47APHIGHER
2device-treecompatible[1]iPhone99,11iPhone17,3 (slot-preserving)LOW
3device-tree/productfdr-product-typeiPhone99,11iPhone17,3HIGHER
4device-tree/productsub-product-typeiPhone99,11iPhone17,3LOW
5device-tree/productunique-modelVPHONE600APD47APLOW
6device-tree/arm-iodevice_typevresearch1-iot8140-ioMEDIUM
7device-tree/arm-iosoc-generationVResearch1H17MEDIUM-LOW
8device-tree/product/vphone600-gestalt-variantsname (node rename)vphone600-gestalt-variantsd47-gestalt-variantsLOW-MEDIUM

Root model and root target-type are deliberately NOT in this list — both have been empirically shown to break restore. Those edits run post-restore as EXP-JB-6.

Post-restore DT identity rewrite (EXP-JB-6, EXP only)

After the restore daemon's BuildManifest identity check has passed, cfw_install_exp.sh step [EXP-JB-6] scp's devicetree.img4 down from the mounted rootfs (/mnt5/<boot-hash>/usr/standalone/firmware/), runs scripts/patchers/cfw_patch_post_restore_dt.py, and scp's the rewritten img4 back. The Python patcher unwraps the IM4P via pyimg4, parses the DT flat-binary, rewrites three restore-fatal root properties, and repacks preserving the IMG4's original IM4M ticket. The iBSS/iBEC/LLB image4_validate_property_callback bypass (existing JB patch) accepts the modified payload at next boot.

#PropertyOld → New
1root modeliPhone99,11iPhone17,3
2root target-typeVPHONE600D47
3root compatiblereorder ["VPHONE600AP", "iPhone99,11", "AVP-ARM"]["D47AP", "VPHONE600AP", "AVP-ARM"] (keeps VPHONE600AP in second slot so IOKit's AppleVMApple1IO kext binding still resolves; userland reads only the first entry for hw.model)

Idempotent. Skipped if already-rewritten DT is detected.

SystemVersion.plist ProductBuildVersion rewrite (EXP-JB-7, EXP only, opt-in)

Gated on the SPOOF_BUILD env var. When cfw_install_exp.sh is invoked with e.g. SPOOF_BUILD=23F77, step [EXP-JB-7] runs scripts/patchers/cfw_patch_build_version.py (plistlib-based, format-preserving) on both the rootfs and cryptex copies of SystemVersion.plist to rewrite the ProductBuildVersion key to the specified id. Without SPOOF_BUILD, the step is skipped and the build version stays at the original IPSW value.

FileTouched if SPOOF_BUILD=<id>
/mnt1/System/Library/CoreServices/SystemVersion.plist (rootfs)Y
/mnt5/Cryptexes/OS/System/Library/CoreServices/SystemVersion.plist (cryptex)Y

kern.osversion is unaffected — that comes from a kernel global initialized from boot args, not from this plist. Userland MG cache picks up the new build identifier on first boot after the gestalt cache rebuild.

CFW Installer Flow Matrix (Script-Level)

Flow ItemRegular (cfw_install.sh)Dev (cfw_install_dev.sh)JB (cfw_install_jb.sh)EXP (cfw_install_exp.sh)
Base CFW phases (1/7 -> 7/7)Runs directlyRuns directlyRuns via CFW_SKIP_HALT=1 zsh cfw_install.shRuns via CFW_SKIP_HALT=1 zsh cfw_install.sh
Dev overlay (rpcserver_ios replacement)-Y (apply_dev_overlay)--
SSH readiness wait before installY (wait_for_device_ssh_ready)-Y (inherited from base run)Y (inherited from base run)
launchd jetsam patch (patch-launchd-jetsam)-Y (base-flow injection)Y (JB-1)Y (JB-1)
launchd dylib injection (inject-dylib /b)--Y (JB-1)Y (JB-1)
Procursus bootstrap deployment--Y (JB-2)Y (JB-2)
BaseBin hook deployment (*.dylib -> /mnt1/cores)--Y (JB-3)Y (JB-3)
First-boot JB finalization (vphone_jb_setup.sh)--Y (post-boot)Y (post-boot)
DSC pre-patch (kern.hv_vmm_present byte-5 mangle + slot reattest)---Y (pre-step, before base CFW)
watchdogd surgical 2-insn patch + slot reattest---Y (EXP-JB-3.5)
Post-restore DT identity rewrite (devicetree.img4)---Y (EXP-JB-6)
SystemVersion.plist ProductBuildVersion rewrite---Y (EXP-JB-7, opt-in via SPOOF_BUILD)
Additional input resourcescfw_inputcfw_input + resources/cfw_dev/rpcserver_ioscfw_input + cfw_jb_inputcfw_input + cfw_jb_input
Extra tool requirement beyond base--zstdzstd
Halt behaviorHalts unless CFW_SKIP_HALT=1Halts unless CFW_SKIP_HALT=1Always halts after JB phasesAlways halts after EXP phases

Summary

ComponentRegularDevJBEXP
AVPBooter1111
iBSS2233
iBEC4444
LLB6666
TXM1121212
Kernel (base)28292828
Kernel (JB methods)--5959
Kernel (EXP methods, hv_vmm)---6
DeviceTree base properties4444
DeviceTree EXP identity properties---8
Boot chain total4658117131
CFW binary patches (base)4566
CFW EXP-only steps---4 (DSC pre-patch, watchdogd EXP-JB-3.5, post-restore DT EXP-JB-6, build-version EXP-JB-7 opt-in)
CFW installed components6799
CFW total10121519
Grand total5670132150

Ramdisk Variant Matrix

VariantPre-stepRamdisk/txm.img4Ramdisk/krnl.ramdisk.img4Ramdisk/krnl.img4Effective kernel used by ramdisk_send.sh
RAMDISKmake fw_patchrelease TXM + base TXM patch (1)base kernel (28), legacy *.ramdisk preferred else derive from pristine CloudOSrestore kernel from fw_patch (28)krnl.ramdisk.img4 preferred, fallback krnl.img4
DEV+RAMDISKmake fw_patch_devrelease TXM + base TXM patch (1)base kernel (28), same derivation rulerestore kernel from fw_patch_dev (29)krnl.ramdisk.img4 preferred, fallback krnl.img4
JB+RAMDISKmake fw_patch_jbrelease TXM + base TXM patch (1)base kernel (28), same derivation rulerestore kernel from fw_patch_jb (28+59)krnl.ramdisk.img4 preferred, fallback krnl.img4
EXP+RAMDISKmake fw_patch_exprelease TXM + base TXM patch (1)base kernel (28), same derivation rulerestore kernel from fw_patch_exp (28+59+6)krnl.ramdisk.img4 preferred, fallback krnl.img4

Cross-Version Dynamic Snapshot

CaseTXM_JB_PATCHESKERNEL_JB_PATCHES
PCC 26.1 (23B85)1459
PCC 26.3 (23D128)1459
iOS 26.1 (23B85)1459
iOS 26.3 (23D127)1459

Swift Migration Notes (2026-03-10)

  • Swift FirmwarePatcher now matches the Python reference patch output across all checked components:
    • avpbooter 1/1
    • ibss 4/4
    • ibec 7/7
    • llb 13/13
    • txm 1/1
    • txm_dev 12/12
    • kernelcache 28/28
    • ibss_jb 1/1
    • kernelcache_jb 84/84
  • JB parity fixes completed in Swift:
    • C23 vnode_getattr resolution now follows the Python backward BL scan and resolves 0x00CD44F8.
    • C22 syscallmask cave encodings were corrected and centralized in ARM64Constants.swift.
    • Task-conversion matcher masks and kernel-text scan range were corrected, restoring the patch at 0x00B0C400.
    • jbDecodeBranchTarget() now correctly decodes cbz/cbnz, restoring the real _bsd_init rootauth gate at 0x00F7798C.
    • IOUC MACF matching now uses Python-equivalent disassembly semantics for the aggregator shape, restoring the deny-to-allow patch at 0x01260644.
  • C24 kcall10 cave instruction bytes were re-verified against macOS clang/as; no Swift byte changes were needed.
  • The Swift pipeline is now directly invokable from the product binary:
    • vphone-cli patch-firmware --vm-directory <dir> --variant {regular|dev|jb}
    • vphone-cli patch-component --component {txm|kernel-base} --input <file> --output <raw> is available for non-firmware tooling that still needs a single patched payload during ramdisk packaging
    • default loader now preserves IM4P containers via IM4PHandler
    • DeviceTree patching now uses the real Swift DeviceTreePatcher in the pipeline
    • project make fw_patch, make fw_patch_dev, and make fw_patch_jb targets now invoke this Swift pipeline via the unsigned debug vphone-cli build, while the signed release build remains reserved for VM boot/DFU paths
    • on 2026-03-11, the legacy Python firmware patcher entrypoints and patch modules were temporarily restored from pre-removal history for parity/debug work.
    • after byte-for-byte parity was revalidated against Python on 26.1 and 26.3 for regular, dev, and jb, those legacy firmware-patcher Python sources and transient comparison/export helpers were removed again so the repo keeps Swift as the single firmware-patching implementation.
  • Swift pipeline follow-up fixes completed after CLI bring-up:
    • findFile() now supports glob patterns such as AVPBooter*.bin instead of treating them as literal paths.
    • JB variant sequencing now runs base iBSS/kernel patchers first, then the JB extension patchers.
    • Sequential pipeline application now merges each patcher's PatchRecord writes onto the shared output buffer while keeping later patcher searches anchored to the original payload, matching the standalone Swift/Python validation model.
    • apply() now reuses an already-populated patches array instead of re-running findAll(), so patch-firmware / patch-component no longer double-scan or double-print the same component diagnostics on a single invocation.
    • unaligned integer reads across the firmware patcher now go through a shared safe Data.loadLE(...) helper, fixing the JB IM4P crash (Swift/UnsafeRawPointer.swift:449 misaligned raw pointer load).
    • TXMPatcher now preserves pristine Python parity by preferring the legacy trustcache binary-search site when present, and only falls back to the selector24 hash-flags call chain (ldr x1, [x20,#0x38] -> add x2, sp, #4 -> bl -> ldp x0, x1, [x20,#0x30] -> add x2, sp, #8 -> bl) when rerunning on a VM tree that already carries the dev/JB selector24 early-return patch.
    • scripts/fw_prepare.sh now deletes stale sibling *Restore* directories in the working VM directory before patching continues, so a fresh make fw_prepare && make fw_patch cannot accidentally select an older prepared firmware tree (for example 26.1) when a newer one (for example 26.3) was just generated.
  • IM4P/output parity fixes completed after synthetic full-pipeline comparison:
    • IM4PHandler.save() no longer forces a generic LZFSE re-encode.
    • Swift now rebuilds IM4Ps in the same effective shape as the Python patch flow and only preserves trailing PAYP metadata for TXM (trxm) and kernelcache (krnl).
    • IBootPatcher serial labels now match Python casing exactly (Loaded iBSS, Loaded iBEC, Loaded LLB).
    • DeviceTreePatcher now serializes the full patched flat tree, matching Python dtree.py, instead of relying on in-place property writes alone.
  • Synthetic CLI dry-run status on 2026-03-10 using IM4P-backed inputs under ipsws/patch_refactor_input:
    • regular: 58 patch records
    • dev: 69 patch records
    • jb: 154 patch records
  • Full synthetic Python-vs-Swift pipeline comparison status on 2026-03-10 using scripts/compare_swift_python_pipeline.py:
    • regular: all 7 component payloads match
    • dev: all 7 component payloads match
    • jb: all 7 component payloads match
  • Real prepared-firmware Python-vs-Swift pipeline comparison status on 2026-03-10 using vm/ after make fw_prepare:
    • historical note: the now-removed scripts/compare_swift_python_pipeline.py cloned only the prepared *Restore* tree plus AVPBooter*.bin, AVPSEPBooter*.bin, and config.plist, avoiding No space left on device failures from copying Disk.img after make vm_new.
    • regular: all 7 component payloads match
    • dev: all 7 component payloads match
    • jb: all 7 component payloads match
  • Runtime validation blocker observed on 2026-03-10:
    • NONE_INTERACTIVE=1 SKIP_PROJECT_SETUP=1 make setup_machine JB=1 reaches the Swift patch stage and reports [patch-firmware] applied 154 patches for jb, then fails when the flow transitions into make boot_dfu.
    • make boot_dfu originally failed at launch-policy time with exit 137 / signal 9 because the release vphone-cli could not launch on this host.
    • amfidont was then validated on-host:
      • it can attach to /usr/libexec/amfid
      • the initial path allow rule failed because AMFIPathValidator reports URL-encoded paths (/Volumes/My%20Shared%20Files/...)
      • rerunning amfidont with the encoded project path and the release-binary CDHash allows the signed release vphone-cli to launch
      • this workflow is now packaged as make amfidont_allow_vphone / scripts/start_amfidont_for_vphone.sh
    • With launch policy bypassed, make boot_dfu advances into VM setup, emits vm/udid-prediction.txt, and then fails with VZErrorDomain Code=2 "Virtualization is not available on this hardware."
    • VPhoneAppDelegate startup failure handling was tightened so these fatal boot/DFU startup errors now exit non-zero; make boot_dfu now reports make: *** [boot_dfu] Error 1 for the nested-virtualization failure instead of incorrectly returning success.
    • The host itself is a nested Apple VM (Model Name: Apple Virtual Machine 1, kern.hv_vmm_present=1), so the remaining blocker is lack of nested Virtualization.framework availability rather than firmware patching or AMFI bypass.
    • boot_binary_check now uses strict host preflight and fails earlier on this class of host with make: *** [boot_binary_check] Error 3, avoiding a wasted VM-start attempt once the nested-virtualization condition is already known.
    • Added make boot_host_preflight / scripts/boot_host_preflight.sh to capture this state in one command:
      • model: Apple Virtual Machine 1
      • kern.hv_vmm_present: 1
      • SIP: disabled
      • allow-research-guests: disabled
      • current kern.bootargs: empty
      • next-boot nvram boot-args: amfi_get_out_of_my_way=1 -v (staged on 2026-03-10; requires reboot before it affects launch policy)
      • spctl --status: assessments enabled
      • spctl --assess rejects the signed release binary
      • unsigned debug vphone-cli --help: exit 0
      • signed release vphone-cli --help: exit 137
      • freshly signed debug control binary --help: exit 137

Automation Notes (2026-03-06)

  • scripts/setup_machine.sh non-interactive flow fix: renamed local variable status to boot_state in first-boot log wait and boot-analysis wait helpers to avoid zsh status read-only special parameter collision.
  • scripts/setup_machine.sh non-interactive first-boot wait fix: replaced (( waited++ )) with (( ++waited )) in monitor_boot_log_until to avoid set -e abort when arithmetic expression evaluates to 0.
  • scripts/jb_patch_autotest.sh loop fix for sweep stability under set -e: replaced ((idx++)) with (( ++idx )).
  • scripts/jb_patch_autotest.sh zsh compatibility fix: renamed per-case result variable status to case_status to avoid status read-only special parameter collision.
  • scripts/jb_patch_autotest.sh selection logic update:
    • default run now excludes methods listed in KernelJBPatcher._DEV_SINGLE_WORKING_METHODS (pending-only sweep).
    • set JB_AUTOTEST_INCLUDE_WORKING=1 to include already-working methods and run the full list.
  • Sweep run record:
    • setup_logs/jb_patch_tests_20260306_114417 (2026-03-06): aborted at [1/20] with read-only variable: status in jb_patch_autotest.sh.
    • setup_logs/jb_patch_tests_20260306_115027 (2026-03-06): rerun after status fix, pending-only mode (Total methods: 19).
  • Final run result from jb_patch_tests_20260306_115027 at 2026-03-06 13:17:
    • Finished: 19/19 (PASS=15, FAIL=4, all fails rc=2).
    • Failing methods at that time: patch_bsd_init_auth, patch_io_secure_bsd_root, patch_vm_fault_enter_prepare, patch_cred_label_update_execve.
    • 2026-03-06 follow-up: patch_io_secure_bsd_root failure is now attributed to a wrong-site patch in AppleARMPE::callPlatformFunction ("SecureRoot" gate at 0xFFFFFE000836E1F0), not the intended "SecureRootName" deny-return path. The code was retargeted the same day to 0xFFFFFE000836E464 and re-enabled for the next restore/boot check.
    • 2026-03-06 follow-up: patch_bsd_init_auth was retargeted after confirming the old matcher was hitting unrelated code; keep disabled in default schedule until a fresh clean-baseline boot test passes.
    • Final case: [19/19] patch_syscallmask_apply_to_proc (PASS).
    • 2026-03-06 re-analysis: that historical PASS is now treated as a false positive for functionality, because the recorded bytes landed at 0xfffffe00093ae6e4/0xfffffe00093ae6e8 inside _profile_syscallmask_destroy underflow handling, not in _proc_apply_syscall_masks.
    • 2026-03-06 code update: scripts/patchers/kernel_jb_patch_syscallmask.py was rebuilt to target the real syscallmask apply wrapper structurally and now dry-runs on PCC-CloudOS-26.1-23B85 kernelcache.research.vphone600 with 3 writes: 0x02395530, 0x023955E8, and cave 0x00AB1720. User-side boot validation succeeded the same day.
  • 2026-03-06 follow-up: patch_kcall10 was rebuilt from the old ABI-unsafe pseudo-10-arg design into an ABI-correct sysent[439] cave. Focused dry-run on PCC-CloudOS-26.1-23B85 kernelcache.research.vphone600 now emits 4 writes: cave 0x00AB1720, sy_call 0x0073E180, sy_arg_munge32 0x0073E188, and metadata 0x0073E190; the method was re-enabled in _GROUP_C_METHODS.
    • Observed failure symptom in current failing set: first boot panic before command injection (or boot process early exit).
  • Post-run schedule change (per user request):
    • commented out failing methods from default KernelJBPatcher._PATCH_METHODS schedule in scripts/patchers/kernel_jb.py:
      • patch_bsd_init_auth
      • patch_io_secure_bsd_root
      • patch_vm_fault_enter_prepare
      • patch_cred_label_update_execve
  • 2026-03-06 re-research note for patch_cred_label_update_execve:
    • old entry-time early-return strategy was identified as boot-unsafe because it skipped AMFI exec-time csflags and entitlement propagation entirely.
    • implementation was reworked to a success-tail trampoline that preserves normal AMFI processing and only clears restrictive csflags bits on the success path.
    • default JB schedule still keeps the method disabled until the reworked strategy is boot-validated.
  • Manual DEV+single (setup_machine + PATCH=<method>) working set now includes:
    • patch_amfi_cdhash_in_trustcache
    • patch_amfi_execve_kill_path
    • patch_task_conversion_eval_internal
    • patch_sandbox_hooks_extended
    • patch_post_validation_additional
  • 2026-03-07 host-side note:
    • reviewed private Virtualization.framework display APIs against the recorder pipeline in sources/vphone-cli/VPhoneScreenRecorder.swift.
    • replaced the old AppKit-first recorder path with a private-display-only implementation built around hidden VZGraphicsDisplay._takeScreenshotWithCompletionHandler: capture.
    • added still screenshot actions that can copy the captured image to the pasteboard or save a PNG to disk using the same private capture path.
    • make build is used as the sanity check path; live VM validation is still needed to confirm the exact screenshot object type returned on macOS 15.
  • 2026-03-15 tooling source sync update:
    • removed ad-hoc git clone source fetching from scripts/setup_tools.sh and scripts/setup_libimobiledevice.sh.
    • added pinned git-submodule sources under scripts/repos/ for: trustcache, insert_dylib, libplist, libimobiledevice-glue, libusbmuxd, libtatsu, libimobiledevice, libirecovery, idevicerestore.
    • setup scripts now initialize required submodules via git submodule update --init --recursive <path> and stage build copies under local tool build directories.