mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
cfw: Fix iOS 27 DDI (/System/Developer) auto-mount
Productionize the 3-part fix that makes `pymobiledevice3 mounter auto-mount`
land the personalized DDI at /System/Developer on the iOS-27-userland /
26.4-vphone600-kernel (c0ecdb4b) JB hybrid.
- KernelJBPatchDiskImages2.swift (new; wired into KernelJBPatcher.findAll):
DiskImages2 ABI acceptance. GATE1/GATE2b NOP the CreateDevice/Connect
cmp#9/b.ne ABI-version rejects (kernel driver v9 vs iOS-27 controller/daemon
v11). GATE2 widens the RegisterNotificationPort backing array + both
bound-check field loads (all-or-nothing) to clear the `type < getMaxPorts`
off-by-one that otherwise fails the attach ("Can't register notification
port").
- KernelJBPatchSandboxExtended: retarget mac_policy_ops[124]
(mpo_proc_check_syscall_unix) to the allow stub so MobileStorageMounter's
mount_apfs can make the mount(2) syscall (unix 167) — else the kernel
Sandbox denies it ("Protobox: mount_apfs deny(1) syscall-unix 167").
- cfw_patch_diskimagesiod.py + cfw.py + cfw_install.sh (gated to 27.*):
force -[DIDiskArb isMountCompleteWithExpectedCount:diskTracker:] -> YES so
MobileStorageMounter's waitForDAMount returns and it performs the real
nobrowse mount.
GATE2a anchor fix (the critical one): Capstone on this toolchain decodes the
AllocPortsArray size-shift lsl-immediate (a UBFM alias) as 2 operands, not the
xd,xn,#imm 3-operand shape. The original 3-operand + imm==3 match found 0, so
the all-or-nothing silently skipped GATE2 on every build (only manual dd pokes
ever worked). Now matched on mnemonic + destination x1 — the unique
size-writing lsl in AllocPortsArray; the replacement is a fixed mov x1,#0x4000
so the shift amount is irrelevant. Verified from a clean build on c0ecdb4b via
`patch-component --component kernel-jb --records-out`: all five di2 records
emit (createdevice, connect, allocports_size, notif_boundcheck_d8/e8), and
`pmd3 mounter auto-mount` -> rc=0 with the DDI mounted, no poke.
Documented in research/0_binary_patch_comparison.md (JB-09, JB-28, CFW
binary-patch #13).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
This commit is contained in:
committed by
zqxwce
co-authored by
Claude Opus 4.8
parent
de725d26aa
commit
9becab4031
@@ -122,6 +122,8 @@
|
||||
|
||||
### JB-Only Kernel Methods (Reference List)
|
||||
|
||||
> **⚠ iOS-27 hard-gate (2026-07-20).** Every patch below whose Purpose cites "iOS 27" / "27.0 on the 26.4 kernel" is **hard-gated to a 27.x base** via `KernelJBPatcher.applyIOS27` — set by `FirmwarePipeline` from the iPhone base `ProductVersion` (mirroring the `applyExcGuard`/iOS-18 mechanism at patch 27 above; standalone `patch-component` defaults it true, override with `--target-os`). Gated set: **JB-02b, JB-02d, JB-09 (the `ops[124]` entry only — the 201..316 hooks stay), JB-10b, JB-26, JB-27, JB-28**. On an 18.x/26.x base **none** of them run. This supersedes the per-row "No-op-in-effect for version-matched userlands" notes: those describe the effect *if applied*, but **JB-27 is NOT a no-op on 26.x** — its `cmp 0x588→0x6e0` retarget makes the 26.x userclient reject its own native 0x588 SwapEnd → dead display (the 26.5 regression that motivated the hard-gate). **Verified 2026-07-20:** a 26.5 base produces a **byte-identical** kernelcache to pre-branch `main` (`patch-component --component kernel-jb --target-os 26.5` vs main's output — `cmp` clean, 83 records each), and a 27.0 base additionally emits exactly the 11 gated records.
|
||||
|
||||
| # | Group | Method | Function | Purpose | JB Enabled |
|
||||
| ----- | ----- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------: |
|
||||
| JB-01 | A | `patch_amfi_cdhash_in_trustcache` | `AMFIIsCDHashInTrustCache` | Always return true + store hash | Y |
|
||||
@@ -134,7 +136,7 @@
|
||||
| JB-06 | B | `patch_post_validation_additional` | `_postValidation` (additional) | Disable SHA256-only hash-type reject | Y |
|
||||
| JB-07 | C | `patch_syscallmask_apply_to_proc` | syscallmask 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 alternative | Y |
|
||||
| JB-08 | A | `patch_task_conversion_eval_internal` | `_task_conversion_eval_internal` | Allow task conversion | Y |
|
||||
| JB-09 | A | `patch_sandbox_hooks_extended` | Sandbox MACF ops (extended) | Stub remaining 30+ sandbox hooks (incl. IOKit 201..210)| Y |
|
||||
| JB-09 | A | `patch_sandbox_hooks_extended` | Sandbox MACF ops (extended) | Stub remaining 30+ sandbox hooks (incl. IOKit 201..210) + `mpo_proc_check_syscall_unix` (`ops[124]`) → allow. **The ops[124] hook is the iOS-27 DDI-mount syscall fix:** MobileStorageMounter's `mount_apfs` needs the `mount(2)` syscall (unix 167) to mount the personalized DDI at `/System/Developer`; on the 26.4-kernel / 27-userland hybrid the kernel Sandbox otherwise denies it (`Protobox: mount_apfs deny(1) syscall-unix 167`). Index calibrated against the reference-XNU `mac_policy_ops` struct order (co-verified: mpo_vnode_check_open=267, mpo_vnode_check_fsgetpath=316). Validated live on `c0ecdb4b` 26.4 (DDI auto-mounts). | Y |
|
||||
| JB-10 | A | `patch_iouc_failed_macf` | IOUC MACF shared gate | A5-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 intact | Y |
|
||||
| JB-10b| A | `patch_iouc_failed_sandbox` | IOUC *sandbox* shared gate (string `"IOUC %s failed sandbox in process %s"`) | **THE iOS-27 display fix (CONFIRMED 2026-07-15).** Sibling to JB-10: the IOUserClient open path has a SEPARATE Sandbox gate beyond the MACF one. On 27 userland / 26.4 kernel it spuriously DENIES the render server (backboardd) its IOMobileFramebuffer/IOSurface/HID userclient opens (27-specific: ABSENT on native 26.4; backboardd absent from every `IOUserClientCreator`) → no present (no Apple logo) + `mainDisplay=nil` → SpringBoard FBSDisplayMonitor crash-loop. Gate shape (same fn as JB-10): `blraa` sandbox check (PAC-indirect) → `cmp w0,#0xe00002c7` (kIOReturnNotPermitted) `b.eq <ALLOW>`; `ldr w8,[sp,#x]; cbnz w8,<DENY>` (other error → deny block w/ fail-log ADRP → returns error); w0==0 path → `b <ALLOW>`. Patch rewrites `<DENY>`'s first insn → `b <ALLOW>` (deny → allow-proceed), leaving the w0==0 path intact. Anchor is structural (fail-log string xref → the CBNZ whose target encloses it → the preceding `b.eq` allow target). Verified: backboardd then holds an IOMFB userclient, `[CADisplay mainDisplay]` resolves to `LCD/primary`, SpringBoard runs with 0 crashes. | Y |
|
||||
| JB-11 | B | `patch_proc_security_policy` | `_proc_security_policy` | Bypass security policy | Y |
|
||||
@@ -154,6 +156,7 @@
|
||||
| JB-25 | B | `patch_vm_map_protect` | `_vm_map_protect` | Skip upstream write-downgrade gate. Shape A (26.1–26.4) active; **Shape B (26.5) disabled 2026-07-05** — widened the `~VM_PROT_WRITE` COW strip (vm_map.c:6202) instead of the RWX gate (vm_map.c:5997), breaking COW and crashing the debugger (SPTM `VIOLATION_ILLEGAL_MAP`). Retired on 26.5+: SPTM code-mod (debugger + Substrate tweaks) uses write-then-flip via `vm_protect(VM_PROT_COPY)` → `XNU_USER_DEBUG`, so no RWX patch is needed. | Y/N |
|
||||
| JB-26 | B | `patch_iomfb_swapend_variable_size` | IOMFB userclient method-5 (SwapEnd) `__DATA_CONST` dispatch entry (`checkStructureInputSize`) | **iOS-27 VZ-view fix, kernel half — paired with DSC force-kern (DSC-patch item 11).** The 26.4 userclient's method-5 dispatch entry hard-checks `checkStructureInputSize == 0x588`; forced-kern iOS 27 sends its native `0x6e0`. Rewrite the size field to `kIOUCVariableStructureSize (0xFFFFFFFF)` so `IOUserClient::externalMethod` accepts 27's struct and reaches the handler. Anchor (structural): the sole `__DATA_CONST` entry `{ptr(ptrauth, top-byte≥0x80), scalarIn=0, structIn=0x588, scalarOut=0, structOut=0}` (verified unique; decompressed file-off 0x9c7228). No-op-in-effect for version-matched 26.x (sends 0x588). Re-enabled 2026-07-15 (was disabled when 27 present-path was still unknown). | Y |
|
||||
| JB-27 | B | `patch_iomfb_swapend_handler_size` | method-5 handler internal size gate (`cmp w2,#0x588 ; b.ne <err>`) | Companion to JB-26: beyond the dispatch-table check the handler re-checks the struct size (`cmp w2,#0x588 ; b.ne <kIOReturnBadArgument>`; verified unique at decompressed file-off 0x16ae22c; success path forwards the raw struct ptr to a `vtable+0x590` paravirt swap method). Retarget the `cmp` immediate to `0x6e0` so forced-kern iOS 27's native SwapEnd reaches real swap processing (27's IOMFBSwapRec prefix matches 26.x → handler reads valid fields). Semantic anchor (`cmp w2,#imm` word + following `b.ne` decode). Enabled together with JB-26 + DSC force-kern for iOS 27. | Y |
|
||||
| JB-28 | A | `patch_disk_images2_client_abi` | `com.apple.driver.AppleDiskImages2` kext: `DIDeviceCreatorUserClient::CreateDevice` + `DIDeviceIOUserClient::Connect` ABI-version gates, and the `AllocPortsArray`/`RegisterNotificationPort` notif-port sizing | **iOS-27 DDI (`/System/Developer`) auto-mount — attach layer.** `pymobiledevice3 mounter auto-mount` on the 26.4-kernel / 27-userland hybrid fails at attach: the kernel DiskImages2 driver is ABI v9, the 27 userland's DiskImages2 controller/daemon is ABI v11 (`DIDeviceCreatorUserClient::CreateDevice: Incompatible client: expected ABI version 9 actual 11`). Three layers: **GATE1** NOPs the CreateDevice controller-ABI `cmp #9 ; b.ne` reject; **GATE2b** NOPs the Connect daemon-ABI `cmp #9 ; b.ne` reject (both anchored on the C++ signature cstring → unique `cmp #9`/`b.ne`; version-robust; no-op-in-effect on version-matched userlands where ABI 9==9). **GATE2** (array + 2 bound checks) fixes a RegisterNotificationPort off-by-one (userland registers at index==maxPorts, one past the array) by widening the AllocPortsArray allocation (`lsl x1,xN,#3` → `mov x1,#0x4000`) AND both bound-check field loads (`ldrh [.,#0xd8]`/`ldr [.,#0xe8]` → `mov wD,#0x800`); applied **all-or-nothing** (widening the bound checks without the backing array would let RegisterNotificationPort write past it → corruption) and skipped/logged on builds whose notif-port codegen differs (the off-by-one is 26.4-hybrid-specific). Pairs with the sandbox ops[124] allow (JB-09) and the diskimagesiod userland patch (CFW binary-patch #13). Anchors structural (Capstone decode of the pinned function's instructions); replacement bytes from the Keystone-backed `ARM64Encoder`. **GATE2a anchor note:** the AllocPortsArray size-shift is matched on `lsl` mnemonic + destination `x1` (the unique size-writing lsl in the function) — NOT a 3-operand `lsl xd,xn,#imm` shape, because Capstone on this toolchain decodes the lsl-immediate (a UBFM alias) as **2 operands**; requiring 3 operands makes GATE2 silently skip (the all-or-nothing returns a harmless no-op). Verified on the `c0ecdb4b` 26.4 deployment kernel via `patch-component --component kernel-jb --records-out`: all five di2 records emit (`di2_createdevice_abi`, `di2_connect_abi`, `di2_allocports_size`, `di2_notif_boundcheck_d8`, `di2_notif_boundcheck_e8`) — always verify the di2 records EMIT, not merely that the JB suite reports "no failures". No-op-in-effect for version-matched userlands. | Y |
|
||||
|
||||
### EXP-Only Kernel Methods (Reference List)
|
||||
|
||||
@@ -180,9 +183,10 @@ do NOT execute these).
|
||||
| 7 | cstring byte 5 mangle `'h' → 'X'` (`"kern.hv_vmm_present"` → `"kern.Xv_vmm_present"`) + per-page slot-hash re-attestation, BLACKLIST semantics — **EXP only** | DSC dylibs | Companion 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/a | Removed 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. | - | - | - |
|
||||
| 9 | `mov w3,#<size>` -> `mov w3,#<base-size>` in `_kern_SwapEnd` — **26.0/26.0.1 and 18.x** | DSC `IOMobileFramebuffer` | Fixes host VZ GUI black-screen with the available PCC vphone600 userclient: the userclient does an exact `checkStructureInputSize` check on external-method-5 (SwapEnd) input, so a userland whose `_kern_SwapEnd` sends a different-sized state gets `kIOReturnBadArgument` and the host display stays black (guest still renders — the Apple logo is visible over VNC, just not in the vphone-cli view). **The accepted size is a property of the base kernel, not the userland**: 26.1 base -> **0x560**, 26.4 base (xnu-12377) -> **0x588**. The 0x588 value is confirmed two ways: the sole dispatch-shaped entry in `kernelcache.*.vphone600` with `checkStructureInputSize==0x588` (scalarIn=0, scalarOut=0, structOut=0, preceded by a ptrauth code ptr, at decompressed file offset 0x9c7228), and empirically — native 26.5 userland sends 0x588 and displays correctly on this stack. Source (userland-sent) sizes observed: 18.6.2 = 0x514, 26.0/26.0.1 = 0x548, 27.0 (24A5380h) = 0x6e0. The patcher is semantic (anchors on `mov w1,#5` -> `mov w3,#imm` -> `mov x4,#0`/`mov x5,#0` -> `bl` inside `_kern_SwapEnd`) and idempotent — rewrites the size to `--target-size` regardless of source and re-attests the modified DSC page. Install gate: `26.0*` / `18.*` -> 0x560 (26.1 base). Validated after host install on `17,3_26.0_23A341`, `17,3_26.0.1_23A355`, and `17,3_18.6.2_22G100` (Apple logo renders) against the 26.1 base. **CORRECTION (2026-07-15): iOS 27.0 is NO LONGER handled here.** 27 presents the paravirt display via IOMFB's `_virt_*` callback path — external method 5 is NEVER called — so no SwapEnd *size* change can help 27 (confirmed by kernel trace + live AppleParavirtGPU idle scheduler). iOS 27 now uses **force-kern (item 11)** to route present back onto method 5; this row applies to 26.0/26.0.1/18.x only. | Y | Y | Y |
|
||||
| 10 | Zero `maxSlide` in `dyld_cache_header` (`@0xF0`) — **iOS 27.0 / any userland whose cache overflows the 6 GiB region** | DSC `dyld_shared_cache_arm64e` header | Fixes pid-1 `launchd` panic at boot on the vphone600 26.x kernel. The kernel reserves `SHARED_REGION_SIZE_ARM64 = 0x180000000` (6 GiB) and, at map time, needs room for the cache's mapped span **plus** the header `maxSlide` (ASLR range). iOS 27.0's cache (span `0x17c830000` ≈ 5.95 GiB) + `maxSlide 0x20000000` = `0x19c830000` > 6 GiB, so `_shared_region_map_and_slide` returns `ENOMEM`, dyld cannot map `libSystem.B.dylib`, and `launchd` panics (`initproc failed to start`). Zeroing `maxSlide` (LE u64) in the main chunk maps the cache at slide 0 (fits with ~58 MiB spare). Self-gating (`patch-dsc-maxslide`): no-op unless span + maxSlide > `0x180000000`, so 26.x / 18.x are untouched. **No** page re-attestation (header metadata, not a `cs_validate`'d code page — confirmed empirically). Validated on `17,3_27.0_24A5380h` + cloudOS 26.4 (`c0ecdb4b…`): `dyld cache mapped system-wide`, launchd reaches first unlock, vphoned connects as iOS 27.0.0, 0 panics. See `scripts/patchers/cfw_patch_dsc_maxslide.py`. | Y | Y | Y |
|
||||
| 10 | Zero `maxSlide` in `dyld_cache_header` (`@0xF0`) — **iOS 27.0 / any userland whose cache overflows the 6 GiB region** | DSC `dyld_shared_cache_arm64e` header | Fixes pid-1 `launchd` panic at boot on the vphone600 26.x kernel. The kernel reserves `SHARED_REGION_SIZE_ARM64 = 0x180000000` (6 GiB) and, at map time, needs room for the cache's mapped span **plus** the header `maxSlide` (ASLR range). iOS 27.0's cache (span `0x17c830000` ≈ 5.95 GiB) + `maxSlide 0x20000000` = `0x19c830000` > 6 GiB, so `_shared_region_map_and_slide` returns `ENOMEM`, dyld cannot map `libSystem.B.dylib`, and `launchd` panics (`initproc failed to start`). Zeroing `maxSlide` (LE u64) in the main chunk maps the cache at slide 0 (fits with ~58 MiB spare). Install gate: **`27.*`** (hard-gated in `cfw_install.sh` as of 2026-07-20 — an 18.x/26.x base skips it entirely). The patcher additionally self-gates (`patch-dsc-maxslide`): no-op unless span + maxSlide > `0x180000000`, kept as defense-in-depth so 26.x / 18.x are untouched even if the install gate were removed. **No** page re-attestation (header metadata, not a `cs_validate`'d code page — confirmed empirically). Validated on `17,3_27.0_24A5380h` + cloudOS 26.4 (`c0ecdb4b…`): `dyld cache mapped system-wide`, launchd reaches first unlock, vphoned connects as iOS 27.0.0, 0 panics. See `scripts/patchers/cfw_patch_dsc_maxslide.py`. | Y | Y | Y |
|
||||
| 11 | Retarget public `_IOMobileFramebufferSwap*` trampolines -> `b _kern_Swap*` (force-kern) — **iOS 27.0** | DSC `IOMobileFramebuffer` | **iOS-27 VZ-view (host paravirt-GPU scanout) fix, userland half.** The host `VZVirtualMachineView` is fed by the guest `AppleParavirtGPU` scanout, which the 26.4 kernel drives ONLY from the IOMFB userclient SwapEnd (external method 5) — the `_kern_Swap*` path. iOS 27 defaults the paravirt display's present to IOMFB's parallel `_virt_Swap*` path (`_virt_SwapEnd` does no userclient call — it invokes an in-process callback `blraaz [conn+0xe68]` and hands the IOSurface to a virtual-display consumer), so the paravirt GPU never scans out → host VZ window black (guest still composites; GUI visible over in-guest TrollVNC; AppleParavirtGPU `SchedulerState` idle). The public `_IOMobileFramebufferSwap*` entrypoints are thin trampolines (`cbz x0; ldr xN,[x0,#slot]; cbz xN; braaz xN`) that tail-call the per-connection swap fp (kern or virt impl). This patch rewrites each trampoline's first insn to `b _kern_Swap<Name>`, forcing present onto method 5 regardless of how 27 classified the display (tail-call, args intact → behaviourally identical to selecting the kern fp). Fully dynamic: public + `_kern_` addrs resolved by name via `ipsw dyld symaddr`, trampoline shape verified by Capstone, branch bytes from Keystone `asm_at()`, modified DSC code pages re-attested. Requires ≥{SwapBegin,SwapEnd,SwapSetLayer} or raises (dry-run retargets 31 entrypoints on 24A5380h, skips 4 non-trampolines). **Pairs with the JB kernel patches (`patchIomfbSwapEndVariableSize` + `patchIomfbSwapEndHandlerSize`)** which relax the 26.4 userclient's two exact `0x588` size gates to accept 27's native `0x6e0` IOMFBSwapRec (prefix matches 26.x, so the paravirt swap handler reads valid fields). Install gate: `27.*`. See `scripts/patchers/cfw_patch_iomfb_force_kern.py`. **VALIDATED on-device (2026-07-15, `17,3_27.0_24A5380h` + cloudOS 26.4 `c0ecdb4b…`, JB): iOS 27 userland renders AND is interactive in the native VZ view (not just TrollVNC); clean boot — no `kIOReturnBadArgument`/SwapEnd rejection/panic.** Runtime confirmed 31 entrypoints retargeted (4 non-trampoline setters left on virt). | Y | Y | Y |
|
||||
| 12 | NOP `-[_LSDModifyClient clientIsEntitledForEmbeddedRegistrationOperations]` entitlement gate + per-page re-attest — **iOS 27.0** | DSC `CoreServices` (LaunchServices) | **iOS-27 app-registration fix.** lsd gates `-[_LSDModifyClient performPostInstallationRegistration:operationUUID:reply:]` (and the containerized/rebuild registration paths) behind `clientIsEntitledForEmbeddedRegistrationOperations`, which does `xpc_connection_copy_entitlement_value` on the XPC peer for any of `com.apple.private.coreservices.lsaw` / `com.apple.private.installcoordinationd.daemon` / `com.apple.private.coreservices.can-register-install-results`. A client without one gets `NSOSStatusErrorDomain -54` (permErr, `LSDModifyService.mm:1639`), so `registerApplicationDictionary:` / `registerContainerizedApplicationWithInfoDictionaries:` fail and no app can (re)register — blocking vphoned's installer, TrollStore, and uicache/Sileo alike. The entitlement route is a dead end even for a launchd platform daemon (vphoned) whose validated csblob (`csops CS_OPS_ENTITLEMENTS_BLOB`) contains all three: LS registration is proxied, so the XPC peer lsd inspects is not the registering process. Fix: NOP the final `cbz w0, <not_entitled>` (the conditional branch whose fall-through sets the `mov w<reg>,#1` result) so the method always returns YES. Fully dynamic: method resolved via the DSC's own `.symbols` in-image local-symbol table (ipsw `symaddr -a`/`a2s` time out on this cache), gate located by control-flow shape in Capstone, NOP from Keystone `asm("nop")`, modified 16 KiB page re-attested (`cfw_dsc_codesign.py`; TXM enforces per-page). The resulting CDHash change is accepted by the JB always-true AMFI cdhash-trust patch. Self-gating (`patch-lsd-embedded-reg`): no-op on pre-iOS-27 userlands where the method is absent. **Pairs with vphoned's `vp_register_path` containerized-registration fallback** (`registerContainerizedApplicationWithInfoDictionaries:...:registrationError:`, treating a nil `registrationError` as success since it returns NO even when it registers). Also paired with **`/cores/vpregister`** (built + deployed by `cfw_install_jb.sh` / `cfw_install_exp.sh`, invoked by `vphone_jb_setup.sh` at first boot): it registers JB app bundles (Sileo) via the same containerized API, because `uicache -a`'s `registerApplicationDictionary:` is a deprecated no-op on iOS 27 (lsd logs *"you cannot use ... to register applications anymore. These interfaces have been deprecated for years."*). **VALIDATED (2026-07-17, `17,3_27.0_24A5380h` + cloudOS 26.4, JB): -54 gone; Sileo registers (`uicache -l` 0→1) via `vpregister`; vphoned installs+registers a test IPA (`com.vphone.vptest`) to `/var/containers/Bundle/Application/` end-to-end. Clean boot (re-attest correct; no CoreServices page rejection).** See `scripts/patchers/cfw_patch_lsd_embedded_reg.py` and `scripts/vphoned/vpregister.m`. | Y | Y | Y |
|
||||
| 12 | NOP `-[_LSDModifyClient clientIsEntitledForEmbeddedRegistrationOperations]` entitlement gate + per-page re-attest — **iOS 27.0** | DSC `CoreServices` (LaunchServices) | **iOS-27 app-registration fix.** lsd gates `-[_LSDModifyClient performPostInstallationRegistration:operationUUID:reply:]` (and the containerized/rebuild registration paths) behind `clientIsEntitledForEmbeddedRegistrationOperations`, which does `xpc_connection_copy_entitlement_value` on the XPC peer for any of `com.apple.private.coreservices.lsaw` / `com.apple.private.installcoordinationd.daemon` / `com.apple.private.coreservices.can-register-install-results`. A client without one gets `NSOSStatusErrorDomain -54` (permErr, `LSDModifyService.mm:1639`), so `registerApplicationDictionary:` / `registerContainerizedApplicationWithInfoDictionaries:` fail and no app can (re)register — blocking vphoned's installer, TrollStore, and uicache/Sileo alike. The entitlement route is a dead end even for a launchd platform daemon (vphoned) whose validated csblob (`csops CS_OPS_ENTITLEMENTS_BLOB`) contains all three: LS registration is proxied, so the XPC peer lsd inspects is not the registering process. Fix: NOP the final `cbz w0, <not_entitled>` (the conditional branch whose fall-through sets the `mov w<reg>,#1` result) so the method always returns YES. Fully dynamic: method resolved via the DSC's own `.symbols` in-image local-symbol table (ipsw `symaddr -a`/`a2s` time out on this cache), gate located by control-flow shape in Capstone, NOP from Keystone `asm("nop")`, modified 16 KiB page re-attested (`cfw_dsc_codesign.py`; TXM enforces per-page). The resulting CDHash change is accepted by the JB always-true AMFI cdhash-trust patch. Install gate: **`27.*`** (hard-gated in `cfw_install.sh` as of 2026-07-20 — an 18.x/26.x base does not apply it). The patcher additionally self-gates (`patch-lsd-embedded-reg`): no-op on pre-iOS-27 userlands where the method is absent. **Pairs with vphoned's `vp_register_path` containerized-registration fallback** (`registerContainerizedApplicationWithInfoDictionaries:...:registrationError:`, treating a nil `registrationError` as success since it returns NO even when it registers). Also paired with **`/cores/vpregister`** (built + deployed by `cfw_install_jb.sh` / `cfw_install_exp.sh`, invoked by `vphone_jb_setup.sh` at first boot — 27-gated by DEPLOYMENT: `cfw_install_jb.sh`/`cfw_install_exp.sh` copy `/cores/vpregister` only when the mounted rootfs `SystemVersion.plist` is `27.*`, and the setup script's `[ -x /cores/vpregister ]` presence check is the runtime gate. Do NOT gate the invocation on a guest `sw_vers` check — the hybrid guest does not reliably report the 27 userland version at first boot, which silently skipped registration): it registers JB app bundles (Sileo) via the same containerized API, because `uicache -a`'s `registerApplicationDictionary:` is a deprecated no-op on iOS 27 (lsd logs *"you cannot use ... to register applications anymore. These interfaces have been deprecated for years."*). **VALIDATED (2026-07-17, `17,3_27.0_24A5380h` + cloudOS 26.4, JB): -54 gone; Sileo registers (`uicache -l` 0→1) via `vpregister`; vphoned installs+registers a test IPA (`com.vphone.vptest`) to `/var/containers/Bundle/Application/` end-to-end. Clean boot (re-attest correct; no CoreServices page rejection).** See `scripts/patchers/cfw_patch_lsd_embedded_reg.py` and `scripts/vphoned/vpregister.m`. | Y | Y | Y |
|
||||
| 13 | `mov x0,#1; ret` on `-[DIDiskArb isMountCompleteWithExpectedCount:diskTracker:]` — **iOS 27.0** | `diskimagesiod` | **iOS-27 DDI (`/System/Developer`) auto-mount — mount-gate.** After the personalized DDI attaches (kernel side: JB-28 + JB-09), MobileStorageMounter waits on diskimagesiod's `-[DIDiskArb waitForDAMountWithExpectedCount:diskTracker:]` before it does the real (nobrowse) mount at `/System/Developer`. That wait loops until `isMountComplete` (= `callbackReached \|\| (appearedDiskCount>=expectedCount && mountedDiskCount>=mountableDiskCount)`) is YES; on the 26.4-kernel / 27-userland hybrid it never becomes true (not all of the DMG's IOMedia "appear" to diskimagesiod's DiskArbitration session, and diskarbitrationd never auto-mounts the volume), so the wait hangs and pmd3 times out. diskimagesiod itself does NOT mount the DDI (its `-[DIDiskArb mountWithDeviceName:...]` is dead code) — it only gates MobileStorageMounter. Forcing `isMountComplete` → YES lets the wait return so MobileStorageMounter proceeds. IMP resolved via LC_SYMTAB or ObjC metadata (selector → `__objc_selrefs` → `__TEXT,__objc_methlist` relative method list → IMP); prologue overwritten `mov x0,#1 ; ret` (safe — the method returns to the caller's unsigned LR without pushing a frame). **Gated to 27.\*** in `cfw_install.sh` (same `$IOS_VERSION` as the DSC patches): on a version-matched userland the native wait completes correctly and forcing it early could race the real mount, so it is NOT applied there. Embedded sandbox profile + private DA/apfs entitlements preserved on re-sign (`ldid_sign_ent`). **Validated on `c0ecdb4b` 26.4 + 27.0 userland: `pmd3 mounter auto-mount` → rc=0, DDI at `/System/Developer`, idempotent across fresh boots.** See `scripts/patchers/cfw_patch_diskimagesiod.py`. | Y | Y | Y |
|
||||
|
||||
### Installed Components
|
||||
|
||||
@@ -663,8 +667,8 @@ cache rebuild.
|
||||
| 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) |
|
||||
| IOMobileFramebuffer SwapEnd payload-size patch (`26.0/26.0.1`,`18.x` -> 0x560 / 26.1 base; `27.0` -> 0x588 / 26.4 base) | Y | Y | Y (inherited from base run) | Y (inherited from base run) |
|
||||
| dyld cache `maxSlide` zero (`patch-dsc-maxslide`; only when cache overflows the 6 GiB region, e.g. iOS 27.0) | Y | Y | Y (inherited from base run) | Y (inherited from base run) |
|
||||
| IOMobileFramebuffer SwapEnd payload-size patch (install-gated `26.0*`/`18.*` -> 0x560 / 26.1 base; **27.0 does NOT use this — it uses force-kern, next section**) | Y | Y | Y (inherited from base run) | Y (inherited from base run) |
|
||||
| dyld cache `maxSlide` zero (`patch-dsc-maxslide`; **install-gated `27.*`** as of 2026-07-20 — iOS 27's cache overflows the 6 GiB region; 18.x/26.x skip it) | Y | Y | Y (inherited from base run) | Y (inherited from base run) |
|
||||
| DSC pre-patch (`kern.hv_vmm_present` byte-5 mangle + slot reattest) | - | - | - | Y (pre-step, before base CFW) |
|
||||
| DSC camera patches (12 patches across CMCapture / CoreMediaIO / AVFCapture / libMobileGestalt) | - | - | - | Y (pre-step, same cryptex mount as hv_vmm) |
|
||||
| `watchdogd` surgical 2-insn patch + slot reattest | - | - | - | Y (EXP-JB-3.5) |
|
||||
|
||||
@@ -71,6 +71,16 @@ ldid_sign() {
|
||||
ldid "${args[@]}" "$file"
|
||||
}
|
||||
|
||||
# Like ldid_sign but re-applies an entitlements plist (for binaries whose
|
||||
# entitlements must survive the re-sign, e.g. diskimagesiod's embedded sandbox
|
||||
# profile + private DA/apfs entitlements).
|
||||
ldid_sign_ent() {
|
||||
local file="$1" ent="$2" bundle_id="${3:-}"
|
||||
local args=("-S$ent" -M "-K$VM_DIR/$CFW_INPUT/signcert.p12")
|
||||
[[ -n "$bundle_id" ]] && args+=("-I$bundle_id")
|
||||
ldid "${args[@]}" "$file"
|
||||
}
|
||||
|
||||
host_hdiutil() {
|
||||
local rc
|
||||
# SUDO_PASSWORD flow exports SUDO_ASKPASS: go straight to sudo -A so
|
||||
@@ -367,6 +377,31 @@ ldid_sign "$TEMP_DIR/seputil" "com.apple.seputil"
|
||||
cp -R "$TEMP_DIR/seputil" "$MNT1/usr/libexec/seputil"
|
||||
/bin/chmod 0755 $MNT1/usr/libexec/seputil
|
||||
|
||||
# ── DDI (/System/Developer) auto-mount — diskimagesiod (iOS 27 only) ──
|
||||
# Force -[DIDiskArb isMountCompleteWithExpectedCount:diskTracker:] → YES so
|
||||
# MobileStorageMounter proceeds to mount the iOS-27 personalized DDI (its
|
||||
# waitForDAMount otherwise hangs forever on the 26.4 vphone600 hybrid: only some
|
||||
# IOMedia appear to diskimagesiod's DA session + DA never auto-mounts). Pairs
|
||||
# with the DiskImages2 ABI + sandbox mac_policy_ops[124] JB kernel patches.
|
||||
# Entitlements (embedded sandbox profile + private DA/apfs) preserved on re-sign.
|
||||
# Gated to 27.*: on a version-matched userland the native waitForDAMount completes
|
||||
# correctly, and forcing the wait to return early could race the real mount — so
|
||||
# it is NOT applied there (uses the same $IOS_VERSION as the DSC patches above).
|
||||
case "$IOS_VERSION" in
|
||||
27.*)
|
||||
echo " Patching diskimagesiod (DDI auto-mount, iOS $IOS_VERSION)..."
|
||||
if ! [[ -e "$MNT1/usr/libexec/diskimagesiod.bak" ]]; then
|
||||
/bin/cp "$MNT1/usr/libexec/diskimagesiod" "$MNT1/usr/libexec/diskimagesiod.bak"
|
||||
fi
|
||||
ldid -e "$MNT1/usr/libexec/diskimagesiod.bak" > "$TEMP_DIR/diskimagesiod.ent.plist"
|
||||
cp "$MNT1/usr/libexec/diskimagesiod.bak" "$TEMP_DIR/diskimagesiod"
|
||||
"$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" patch-diskimagesiod "$TEMP_DIR/diskimagesiod"
|
||||
ldid_sign_ent "$TEMP_DIR/diskimagesiod" "$TEMP_DIR/diskimagesiod.ent.plist" "com.apple.diskimagesiod"
|
||||
cp -R "$TEMP_DIR/diskimagesiod" "$MNT1/usr/libexec/diskimagesiod"
|
||||
/bin/chmod 0755 "$MNT1/usr/libexec/diskimagesiod"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Rename gigalocker (mv to same name is fine on re-run)
|
||||
echo " Renaming gigalocker..."
|
||||
mount_vol s3 "$MNT3"
|
||||
|
||||
+17
-1
@@ -77,6 +77,13 @@ Commands:
|
||||
escalates to a kernel panic. Also recomputes the affected
|
||||
CodeDirectory slot hash via cfw_macho_codesign.
|
||||
|
||||
patch-diskimagesiod <binary>
|
||||
Force -[DIDiskArb isMountCompleteWithExpectedCount:diskTracker:] to return
|
||||
YES so MobileStorageMounter proceeds to mount the iOS-27 personalized DDI at
|
||||
/System/Developer (its waitForDAMount otherwise hangs forever on the 26.4
|
||||
vphone600 hybrid). Pairs with the DiskImages2 ABI + sandbox
|
||||
mac_policy_ops[124] JB kernel patches. No-op-in-effect on matched userlands.
|
||||
|
||||
inject-daemons <launchd.plist> <daemon_dir>
|
||||
Inject bash/dropbear/trollvnc into launchd.plist.
|
||||
|
||||
@@ -111,6 +118,7 @@ if __name__ == "__main__":
|
||||
from patchers.cfw_patch_lsd_embedded_reg import patch_lsd_embedded_reg
|
||||
from patchers.cfw_patch_camera_dsc import apply_all_camera_patches
|
||||
from patchers.cfw_patch_watchdogd import patch_watchdogd
|
||||
from patchers.cfw_patch_diskimagesiod import patch_diskimagesiod
|
||||
from patchers.cfw_daemons import parse_cryptex_paths, inject_daemons, patch_dropbear_plist
|
||||
else:
|
||||
from .cfw_patch_seputil import patch_seputil
|
||||
@@ -124,6 +132,7 @@ else:
|
||||
from .cfw_patch_lsd_embedded_reg import patch_lsd_embedded_reg
|
||||
from .cfw_patch_camera_dsc import apply_all_camera_patches
|
||||
from .cfw_patch_watchdogd import patch_watchdogd
|
||||
from .cfw_patch_diskimagesiod import patch_diskimagesiod
|
||||
from .cfw_daemons import parse_cryptex_paths, inject_daemons, patch_dropbear_plist
|
||||
|
||||
|
||||
@@ -247,6 +256,13 @@ def main():
|
||||
# exception (unparseable binary / no anchor) is fatal.
|
||||
sys.exit(0)
|
||||
|
||||
elif cmd == "patch-diskimagesiod":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: patch_cfw.py patch-diskimagesiod <binary>")
|
||||
sys.exit(1)
|
||||
if not patch_diskimagesiod(sys.argv[2]):
|
||||
sys.exit(1)
|
||||
|
||||
elif cmd == "inject-daemons":
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: patch_cfw.py inject-daemons <launchd.plist> <daemon_dir>")
|
||||
@@ -285,7 +301,7 @@ def main():
|
||||
print("Commands: cryptex-paths, patch-seputil, patch-launchd-cache-loader, patch-camera-dsc,")
|
||||
print(" patch-mobileactivationd, patch-launchd-jetsam,")
|
||||
print(" patch-hv-vmm-dsc, patch-iomfb-swapend, patch-iomfb-force-kern, patch-dsc-maxslide, patch-lsd-embedded-reg, patch-watchdogd,")
|
||||
print(" inject-daemons, patch-dropbear-plist, inject-dylib")
|
||||
print(" patch-diskimagesiod, inject-daemons, patch-dropbear-plist, inject-dylib")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
"""diskimagesiod patch module — iOS 27 DDI (/System/Developer) auto-mount fix.
|
||||
|
||||
Forces -[DIDiskArb isMountCompleteWithExpectedCount:diskTracker:] to return YES.
|
||||
|
||||
Why: `pymobiledevice3 mounter auto-mount` attaches the personalized DDI, then
|
||||
MobileStorageMounter waits on diskimagesiod's -[DIDiskArb
|
||||
waitForDAMountWithExpectedCount:diskTracker:] before it performs the real
|
||||
(nobrowse) mount of the DDI volume at /System/Developer. That wait loops until
|
||||
`isMountComplete` returns YES, which is
|
||||
`callbackReached || (appearedDiskCount >= expectedCount && mountedDiskCount >= mountableDiskCount)`.
|
||||
On the iOS-27-userland / 26.4-vphone600-kernel hybrid this never becomes true:
|
||||
only some of the DMG's IOMedia ever "appear" to diskimagesiod's DiskArbitration
|
||||
session (appearedDiskCount < expectedCount), and diskarbitrationd never
|
||||
auto-mounts the volume (mountedDiskCount stays 0), so the wait hangs forever and
|
||||
pmd3 times out. diskimagesiod itself does NOT mount the DDI (its
|
||||
-[DIDiskArb mountWithDeviceName:...] is dead code); it only gates
|
||||
MobileStorageMounter. Forcing isMountComplete to YES lets the wait return
|
||||
immediately so MobileStorageMounter proceeds and mounts the DDI.
|
||||
|
||||
Pairs with the JB kernel patches that make the DDI attachable + mountable:
|
||||
- DiskImages2 ABI pokes (KernelJBPatchDiskImages2*) — the v9-kernel accepts the
|
||||
v11 userland attach.
|
||||
- Sandbox mac_policy_ops[124] (mpo_proc_check_syscall_unix) → allow stub
|
||||
(KernelJBPatchSandboxExtended) — lets MobileStorageMounter's mount_apfs make
|
||||
the mount(2) syscall (unix 167), otherwise the kernel Sandbox denies it
|
||||
("Protobox: mount_apfs deny(1) syscall-unix 167").
|
||||
|
||||
No-op-in-effect on version-matched userlands (there the wait completes on its
|
||||
own, so returning YES early changes nothing observable).
|
||||
"""
|
||||
|
||||
from .cfw_asm import *
|
||||
from .cfw_asm import _log_asm
|
||||
|
||||
_SELECTOR = "isMountCompleteWithExpectedCount:diskTracker:"
|
||||
|
||||
|
||||
def _find_imp_via_objc_metadata(data):
|
||||
"""Resolve the method IMP through ObjC runtime metadata (relative method lists).
|
||||
|
||||
selector cstring -> __objc_selrefs entry -> relative method-list entry -> IMP.
|
||||
"""
|
||||
sections = parse_macho_sections(data)
|
||||
|
||||
sel_foff = data.find(_SELECTOR.encode() + b"\x00")
|
||||
if sel_foff < 0:
|
||||
print(f" [-] Selector '{_SELECTOR}' not found in binary")
|
||||
return -1
|
||||
|
||||
sel_va = -1
|
||||
for _sec_name, (sva, ssz, sfoff) in sections.items():
|
||||
if sfoff <= sel_foff < sfoff + ssz:
|
||||
sel_va = sva + (sel_foff - sfoff)
|
||||
break
|
||||
if sel_va < 0:
|
||||
print(f" [-] Could not compute VA for selector at foff:0x{sel_foff:X}")
|
||||
return -1
|
||||
print(f" Selector at foff:0x{sel_foff:X} va:0x{sel_va:X}")
|
||||
|
||||
selrefs = find_section(
|
||||
sections,
|
||||
"__DATA_CONST,__objc_selrefs",
|
||||
"__DATA,__objc_selrefs",
|
||||
"__AUTH_CONST,__objc_selrefs",
|
||||
)
|
||||
selref_va = -1
|
||||
if selrefs:
|
||||
sr_va, sr_size, sr_foff = selrefs
|
||||
for i in range(0, sr_size, 8):
|
||||
ptr = struct.unpack_from("<Q", data, sr_foff + i)[0]
|
||||
# Handle chained fixups: exact, 48-bit-masked, or low-32-bit match.
|
||||
if (ptr == sel_va
|
||||
or (ptr & 0x0000FFFFFFFFFFFF) == sel_va
|
||||
or (ptr & 0xFFFFFFFF) == (sel_va & 0xFFFFFFFF)):
|
||||
selref_va = sr_va + i
|
||||
break
|
||||
if selref_va < 0:
|
||||
print(" [-] Selref not found (chained fixups may obscure pointers)")
|
||||
return -1
|
||||
print(f" Selref va:0x{selref_va:X}")
|
||||
|
||||
# Relative method lists live in __TEXT,__objc_methlist on modern toolchains
|
||||
# (older layouts keep them in __objc_const — try both).
|
||||
objc_const = find_section(
|
||||
sections,
|
||||
"__TEXT,__objc_methlist",
|
||||
"__DATA_CONST,__objc_const",
|
||||
"__DATA,__objc_const",
|
||||
"__AUTH_CONST,__objc_const",
|
||||
)
|
||||
if objc_const:
|
||||
oc_va, oc_size, oc_foff = objc_const
|
||||
# Relative method entry: { int32 name_rel, int32 types_rel, int32 imp_rel }.
|
||||
# name_rel may resolve to the selref (uniqued SEL*) or, in "direct
|
||||
# selector" method lists, straight to the selector cstring — accept both.
|
||||
for i in range(0, oc_size - 12, 4):
|
||||
entry_foff = oc_foff + i
|
||||
entry_va = oc_va + i
|
||||
rel_name = struct.unpack_from("<i", data, entry_foff)[0]
|
||||
if entry_va + rel_name in (selref_va, sel_va):
|
||||
imp_field_foff = entry_foff + 8
|
||||
imp_field_va = entry_va + 8
|
||||
rel_imp = struct.unpack_from("<i", data, imp_field_foff)[0]
|
||||
imp_va = imp_field_va + rel_imp
|
||||
imp_foff = va_to_foff(bytes(data), imp_va)
|
||||
if imp_foff >= 0:
|
||||
print(f" Found via relative method list: IMP va:0x{imp_va:X} foff:0x{imp_foff:X}")
|
||||
return imp_foff
|
||||
print(f" [!] IMP va:0x{imp_va:X} could not be mapped to file offset")
|
||||
return -1
|
||||
|
||||
|
||||
def patch_diskimagesiod(filepath):
|
||||
"""Force -[DIDiskArb isMountCompleteWithExpectedCount:diskTracker:] → YES.
|
||||
|
||||
Anchor strategies (in order):
|
||||
1. LC_SYMTAB symbol containing "isMountCompleteWithExpectedCount".
|
||||
2. ObjC metadata: selector -> selref -> relative method list -> IMP.
|
||||
Then overwrite the method prologue with `mov x0,#1 ; ret`.
|
||||
"""
|
||||
data = bytearray(open(filepath, "rb").read())
|
||||
|
||||
imp_foff = -1
|
||||
imp_va = find_symbol_va(bytes(data), "isMountCompleteWithExpectedCount")
|
||||
if imp_va > 0:
|
||||
imp_foff = va_to_foff(bytes(data), imp_va)
|
||||
if imp_foff >= 0:
|
||||
print(f" Found via symtab: va:0x{imp_va:X} -> foff:0x{imp_foff:X}")
|
||||
|
||||
if imp_foff < 0:
|
||||
imp_foff = _find_imp_via_objc_metadata(data)
|
||||
|
||||
if imp_foff < 0:
|
||||
print(" [-] Dynamic anchor not found — all strategies exhausted")
|
||||
return False
|
||||
|
||||
if imp_foff + 8 > len(data):
|
||||
print(f" [-] IMP offset 0x{imp_foff:X} out of bounds")
|
||||
return False
|
||||
|
||||
print(" Before:")
|
||||
_log_asm(data, imp_foff, 4, imp_foff)
|
||||
|
||||
# Return YES immediately. Overwriting the prologue (pacibsp; stp...) is safe:
|
||||
# the function returns to the caller's (unsigned) LR without ever pushing a
|
||||
# frame. Mirrors patch_mobileactivationd.
|
||||
data[imp_foff:imp_foff + 4] = MOV_X0_1
|
||||
data[imp_foff + 4:imp_foff + 8] = RET
|
||||
|
||||
print(" After:")
|
||||
_log_asm(data, imp_foff, 4, imp_foff)
|
||||
|
||||
open(filepath, "wb").write(data)
|
||||
print(f" [+] Patched isMountComplete at 0x{imp_foff:X}: mov x0, #1; ret")
|
||||
return True
|
||||
@@ -0,0 +1,240 @@
|
||||
// KernelJBPatchDiskImages2.swift — JB kernel patch: DiskImages2 ABI acceptance.
|
||||
//
|
||||
// Productionizes the known-good DiskImages2 pokes that make the 26.4 vphone600
|
||||
// kernel accept the iOS-27 userland's DiskImages2 client (kernel driver ABI v9 vs
|
||||
// daemon/controller ABI v11) and fix a RegisterNotificationPort off-by-one. Without
|
||||
// these the iOS-27 personalized DDI never attaches (kernel:
|
||||
// "DIDeviceCreatorUserClient::CreateDevice: Incompatible client: expected ABI
|
||||
// version 9 actual 11"), so `pymobiledevice3 mounter auto-mount` fails at attach.
|
||||
//
|
||||
// Pairs with the sandbox mac_policy_ops[124] allow (KernelJBPatchSandboxExtended)
|
||||
// and the diskimagesiod isMountComplete→YES userland patch (cfw_install) that
|
||||
// together let the attached DDI actually mount at /System/Developer.
|
||||
//
|
||||
// All sites are in the com.apple.driver.AppleDiskImages2 kext inside the MH_FILESET
|
||||
// kernelcache. The existing JB patches scan the whole kernel text, which covers the
|
||||
// kext; every anchor here is a globally-unique C++ signature / AssertMacros cstring,
|
||||
// or a shape made unique by first pinning the enclosing function — so no DI2-range
|
||||
// restriction is required.
|
||||
//
|
||||
// Layers:
|
||||
// - GATE1/GATE2b (ABI reject NOP): version-robust — anchored on the C++ signature
|
||||
// cstring + a unique `cmp #9 ; b.ne`. No-op-in-effect on version-matched userlands
|
||||
// (ABI 9 == 9, so the b.ne isn't taken anyway).
|
||||
// - GATE2 (notification-ports array + bound-check widen): the off-by-one is specific
|
||||
// to the 26.4-kernel / iOS-27-userland hybrid, and its instruction shapes vary by
|
||||
// kernel build. Applied ALL-OR-NOTHING (widening the bound checks without also
|
||||
// widening the backing array would let RegisterNotificationPort write past the
|
||||
// array); on builds whose codegen doesn't present all three sites the whole gate
|
||||
// is skipped (logged) — the ABI gates above remain the essential attach fix.
|
||||
//
|
||||
// Guardrails: no hardcoded offsets/VAs/bytes in patch logic — matching is from
|
||||
// Capstone decode, replacement bytes from the Keystone-backed ARM64/ARM64Encoder
|
||||
// helpers.
|
||||
|
||||
import Capstone
|
||||
import Foundation
|
||||
|
||||
extension KernelJBPatcher {
|
||||
/// Apply all DiskImages2 ABI pokes. Wired into KernelJBPatcher.findAll().
|
||||
@discardableResult
|
||||
func patchDiskImages2ClientAbi() -> Bool {
|
||||
var ok = true
|
||||
ok = patchDiskImages2CreateDeviceAbi() && ok
|
||||
ok = patchDiskImages2ConnectAbi() && ok
|
||||
ok = patchDiskImages2NotificationPortArray() && ok
|
||||
return ok
|
||||
}
|
||||
|
||||
// MARK: - GATE 1 / GATE 2b: ABI-version reject b.ne → NOP
|
||||
|
||||
/// `DIDeviceCreatorUserClient::CreateDevice` rejects a client whose controller ABI
|
||||
/// (`cmp wN,#9 ; b.ne <reject>`) != 9. NOP the b.ne so the ABI-11 iOS-27 client is
|
||||
/// accepted. Function pinned by its unique C++ signature cstring.
|
||||
@discardableResult
|
||||
func patchDiskImages2CreateDeviceAbi() -> Bool {
|
||||
log("\n[JB] DiskImages2 GATE1: CreateDevice controller-ABI b.ne -> nop")
|
||||
return nopAbiVersionGate(
|
||||
funcSig: "static IOReturn DIDeviceCreatorUserClient::CreateDevice(OSObject *, void *, IOExternalMethodArguments *)",
|
||||
patchID: "di2_createdevice_abi",
|
||||
desc: "nop [DI2 CreateDevice controller-ABI cmp#9/b.ne gate]"
|
||||
)
|
||||
}
|
||||
|
||||
/// `DIDeviceIOUserClient::Connect` rejects a client whose daemon ABI
|
||||
/// (`cmp wN,#9 ; b.ne <reject>`) != 9. NOP the b.ne. Same shape, different function.
|
||||
@discardableResult
|
||||
func patchDiskImages2ConnectAbi() -> Bool {
|
||||
log("\n[JB] DiskImages2 GATE2b: Connect daemon-ABI b.ne -> nop")
|
||||
return nopAbiVersionGate(
|
||||
funcSig: "static IOReturn DIDeviceIOUserClient::Connect(OSObject *, void *, IOExternalMethodArguments *)",
|
||||
patchID: "di2_connect_abi",
|
||||
desc: "nop [DI2 Connect daemon-ABI cmp#9/b.ne gate]"
|
||||
)
|
||||
}
|
||||
|
||||
/// Pin the function via its unique signature cstring, then NOP the unique
|
||||
/// `cmp wN,#9 ; b.ne` inside it.
|
||||
private func nopAbiVersionGate(funcSig: String, patchID: String, desc: String) -> Bool {
|
||||
guard let sigOff = buffer.findString(funcSig) else {
|
||||
log(" [-] signature string not found: \(funcSig.prefix(48))…")
|
||||
return false
|
||||
}
|
||||
let refs = findStringRefs(sigOff)
|
||||
guard let ref = refs.first, let funcStart = findFunctionStart(ref.adrpOff) else {
|
||||
log(" [-] no xref/function for signature string")
|
||||
return false
|
||||
}
|
||||
let funcEnd = findFuncEnd(funcStart, maxSize: 0x2000)
|
||||
|
||||
// CMP Wn,#9 (== SUBS WZR,Wn,#9) immediately followed by B.NE.
|
||||
var hits: [Int] = []
|
||||
var off = funcStart
|
||||
while off + 8 <= funcEnd {
|
||||
defer { off += 4 }
|
||||
guard let cmp = disasAt(off), cmp.mnemonic == "cmp",
|
||||
let ops = cmp.aarch64?.operands, ops.count == 2,
|
||||
ops[0].type == AARCH64_OP_REG,
|
||||
(disasm.firstRegisterName(cmp)?.hasPrefix("w") ?? false),
|
||||
ops[1].type == AARCH64_OP_IMM, ops[1].imm == 9
|
||||
else { continue }
|
||||
guard let nxt = disasAt(off + 4), nxt.mnemonic == "b.ne" else { continue }
|
||||
hits.append(off + 4) // the b.ne
|
||||
}
|
||||
|
||||
guard hits.count == 1 else {
|
||||
log(" [-] expected 1 cmp#9/b.ne gate, found \(hits.count)")
|
||||
return false
|
||||
}
|
||||
let bneOff = hits[0]
|
||||
emit(bneOff, ARM64.nop, patchID: patchID, virtualAddress: fileOffsetToVA(bneOff), description: desc)
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - GATE 2 (a/b/c): notification-ports array + bound checks widen
|
||||
|
||||
/// Widen the notification-ports array allocation AND both bound-check fields to
|
||||
/// 0x800 entries, fixing the RegisterNotificationPort off-by-one (userland
|
||||
/// registers at index == maxPorts, one past the array).
|
||||
///
|
||||
/// ALL-OR-NOTHING: widening the bound checks (type < 0x800) without also widening
|
||||
/// the backing array would let RegisterNotificationPort write past the array
|
||||
/// (memory corruption). So all three sites are located first and patched only if
|
||||
/// all are present. On kernel builds whose notif-port codegen differs (the
|
||||
/// off-by-one is specific to the 26.4-kernel / iOS-27-userland hybrid), the whole
|
||||
/// gate is skipped — the version-robust ABI gates (GATE1/GATE2b) are the essential
|
||||
/// attach fix.
|
||||
@discardableResult
|
||||
func patchDiskImages2NotificationPortArray() -> Bool {
|
||||
log("\n[JB] DiskImages2 GATE2: widen notification-ports array + bound checks")
|
||||
|
||||
guard let allocSite = findDI2AllocPortsSizeSite() else {
|
||||
log(" [~] AllocPortsArray size-shift not present on this kernel — skipping GATE2 (build-specific notif-port codegen; GATE1/GATE2b are the essential fix)")
|
||||
return true
|
||||
}
|
||||
guard let (rnpStart, rnpEnd) = findDI2RegisterNotifFunc(),
|
||||
let f1 = findUniqueFieldLoad(funcStart: rnpStart, funcEnd: rnpEnd, mnemonic: "ldrh", disp: 0xD8, requireWDest: false),
|
||||
let f2 = findUniqueFieldLoad(funcStart: rnpStart, funcEnd: rnpEnd, mnemonic: "ldr", disp: 0xE8, requireWDest: true)
|
||||
else {
|
||||
log(" [~] notification-port bound-check loads not both present — skipping GATE2 (all-or-nothing)")
|
||||
return true
|
||||
}
|
||||
|
||||
// All three located — apply together.
|
||||
var ok = applyDI2AllocPortsSize(at: allocSite)
|
||||
ok = applyFieldLoadMov800(at: f1, patchID: "di2_notif_boundcheck_d8",
|
||||
desc: "mov wD,#0x800 [DI2 RegisterNotificationPort bound-check field1 @+0xd8]") && ok
|
||||
ok = applyFieldLoadMov800(at: f2, patchID: "di2_notif_boundcheck_e8",
|
||||
desc: "mov wD,#0x800 [DI2 RegisterNotificationPort bound-check field2 @+0xe8]") && ok
|
||||
return ok
|
||||
}
|
||||
|
||||
/// Locate the AllocPortsArray allocator size arg `lsl x1, xN, #3` (count << 3 ==
|
||||
/// count * 8), unique within the function. Function pinned by its C++ signature.
|
||||
private func findDI2AllocPortsSizeSite() -> Int? {
|
||||
guard let sigOff = buffer.findString(
|
||||
"static IOReturn DIDeviceIOUserClient::AllocPortsArray(OSObject *, void *, IOExternalMethodArguments *)"
|
||||
) else { return nil }
|
||||
let refs = findStringRefs(sigOff)
|
||||
guard let ref = refs.first, let funcStart = findFunctionStart(ref.adrpOff) else { return nil }
|
||||
let funcEnd = findFuncEnd(funcStart, maxSize: 0x1000)
|
||||
|
||||
// The allocator size arg `lsl x1, xN, #3` (count << 3 == count * 8). Capstone on
|
||||
// this toolchain decodes the lsl-immediate (a UBFM alias) as 2 operands (not the
|
||||
// xd,xn,#imm 3-operand shape), so match on mnemonic + destination x1 — the only
|
||||
// lsl that writes the size register, unique within AllocPortsArray. Replacement is
|
||||
// a fixed `mov x1,#0x4000`, so the original shift amount is irrelevant.
|
||||
var hits: [Int] = []
|
||||
var off = funcStart
|
||||
while off + 4 <= funcEnd {
|
||||
defer { off += 4 }
|
||||
guard let lsl = disasAt(off), lsl.mnemonic == "lsl",
|
||||
disasm.firstRegisterName(lsl) == "x1"
|
||||
else { continue }
|
||||
hits.append(off)
|
||||
}
|
||||
return hits.count == 1 ? hits[0] : nil
|
||||
}
|
||||
|
||||
/// Pin the RegisterNotificationPort function via its unique AssertMacros cstring.
|
||||
private func findDI2RegisterNotifFunc() -> (Int, Int)? {
|
||||
guard let sOff = buffer.findString("!notification_ports[ type ]") else { return nil }
|
||||
let refs = findStringRefs(sOff)
|
||||
guard let ref = refs.first, let funcStart = findFunctionStart(ref.adrpOff) else { return nil }
|
||||
return (funcStart, findFuncEnd(funcStart, maxSize: 0x400))
|
||||
}
|
||||
|
||||
/// Locate a unique `<mnemonic> wD,[xB,#disp]` field load in [funcStart,funcEnd).
|
||||
private func findUniqueFieldLoad(funcStart: Int, funcEnd: Int, mnemonic: String, disp: Int64, requireWDest: Bool) -> Int? {
|
||||
var hits: [Int] = []
|
||||
var off = funcStart
|
||||
while off + 4 <= funcEnd {
|
||||
defer { off += 4 }
|
||||
guard let ins = disasAt(off), ins.mnemonic == mnemonic,
|
||||
let ops = ins.aarch64?.operands, ops.count == 2,
|
||||
ops[0].type == AARCH64_OP_REG,
|
||||
ops[1].type == AARCH64_OP_MEM, ops[1].mem.disp == disp
|
||||
else { continue }
|
||||
if requireWDest, !(disasm.firstRegisterName(ins)?.hasPrefix("w") ?? false) { continue }
|
||||
hits.append(off)
|
||||
}
|
||||
return hits.count == 1 ? hits[0] : nil
|
||||
}
|
||||
|
||||
/// Rewrite the AllocPortsArray size shift to `mov x1,#0x4000` (0x800 entries * 8).
|
||||
private func applyDI2AllocPortsSize(at lslOff: Int) -> Bool {
|
||||
guard let name = disasm.firstRegisterName(disasAt(lslOff)!),
|
||||
let dst = xRegIndex(name),
|
||||
let bytes = ARM64Encoder.encodeMovzX(rd: dst, imm16: 0x4000, shift: 0)
|
||||
else { log(" [-] could not encode mov x1,#0x4000"); return false }
|
||||
emit(lslOff, bytes,
|
||||
patchID: "di2_allocports_size",
|
||||
virtualAddress: fileOffsetToVA(lslOff),
|
||||
description: "mov x1,#0x4000 [DI2 AllocPortsArray widen notif-ports alloc to 0x800 entries]")
|
||||
return true
|
||||
}
|
||||
|
||||
/// Rewrite a bound-check field load to `mov wD,#0x800`, deriving wD from the decode.
|
||||
private func applyFieldLoadMov800(at ldOff: Int, patchID: String, desc: String) -> Bool {
|
||||
guard let name = disasm.firstRegisterName(disasAt(ldOff)!),
|
||||
let dst = wRegIndex(name),
|
||||
let bytes = ARM64Encoder.encodeMovzW(rd: dst, imm16: 0x800, shift: 0)
|
||||
else { log(" [-] could not encode mov wD,#0x800"); return false }
|
||||
emit(ldOff, bytes, patchID: patchID, virtualAddress: fileOffsetToVA(ldOff), description: desc)
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Register-name → index helpers
|
||||
|
||||
private func wRegIndex(_ name: String) -> UInt32? {
|
||||
if name == "wzr" { return 31 }
|
||||
guard name.hasPrefix("w"), let n = UInt32(name.dropFirst()), n < 31 else { return nil }
|
||||
return n
|
||||
}
|
||||
|
||||
private func xRegIndex(_ name: String) -> UInt32? {
|
||||
if name == "xzr" { return 31 }
|
||||
guard name.hasPrefix("x"), let n = UInt32(name.dropFirst()), n < 31 else { return nil }
|
||||
return n
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,10 @@ extension KernelJBPatcher {
|
||||
return false
|
||||
}
|
||||
|
||||
// Extended hook index table (name → ops slot index).
|
||||
let hookIndices: [(String, Int)] = [
|
||||
// Extended hook index table (name → ops slot index). Entries 201..316 are the
|
||||
// base JB sandbox bypass and apply on every JB base; the iOS-27-only
|
||||
// syscall-unix entry (124) is appended below, gated on applyIOS27.
|
||||
var hookIndices: [(String, Int)] = [
|
||||
("iokit_check_201", 201),
|
||||
("iokit_check_202", 202),
|
||||
("iokit_check_203", 203),
|
||||
@@ -69,6 +71,17 @@ extension KernelJBPatcher {
|
||||
("vnode_check_fsgetpath", 316),
|
||||
]
|
||||
|
||||
// iOS-27-only: mpo_proc_check_syscall_unix[124] → allow lets the mount_apfs that
|
||||
// MobileStorageMounter spawns make the mount(2) syscall (unix 167) for the iOS-27
|
||||
// personalized DDI (/System/Developer). Gated so a 26.x base keeps its syscall-unix
|
||||
// MAC filter intact (else kernel Sandbox: "Protobox: mount_apfs deny(1) syscall-unix
|
||||
// 167"). Index 124 is calibrated against this kernel's mac_policy_ops layout: the
|
||||
// vnode_check_open==267 / vnode_check_fsgetpath==316 entries above match the
|
||||
// reference XNU struct order exactly, so mpo_proc_check_syscall_unix==124 holds.
|
||||
if applyIOS27 {
|
||||
hookIndices.append(("proc_check_syscall_unix", 124))
|
||||
}
|
||||
|
||||
var patched = 0
|
||||
for (hookName, idx) in hookIndices {
|
||||
let entryOff = opsTable + idx * 8
|
||||
|
||||
@@ -33,13 +33,21 @@ public final class KernelJBPatcher: KernelJBPatcherBase, Patcher {
|
||||
patchTaskConversionEvalInternal()
|
||||
patchSandboxHooksExtended()
|
||||
patchIoucFailedMacf()
|
||||
// iOS 27 userland on the 26.4 kernel: the IOKit user-client open path's
|
||||
// Sandbox gate (separate from the MACF gate above) spuriously denies the
|
||||
// render server (backboardd) its IOMobileFramebuffer/IOSurface/HID user
|
||||
// clients → no present (no Apple logo) + nil main display (SpringBoard
|
||||
// crash-loop). Bypass it, mirroring the MACF gate. No-op where the gate
|
||||
// already allows (native 26.x userlands).
|
||||
patchIoucFailedSandbox()
|
||||
|
||||
// iOS-27-only (gated — a 26.x base skips these entirely). Both target a 27
|
||||
// userland on the 26.4 kernel:
|
||||
// - IOUC sandbox gate bypass: the IOKit user-client open path's Sandbox gate
|
||||
// (separate from the MACF gate above) spuriously denies backboardd its
|
||||
// IOMFB/IOSurface/HID user clients → no present + nil main display →
|
||||
// SpringBoard crash-loop. Mirrors the MACF gate.
|
||||
// - DiskImages2 DDI ABI (kernel driver v9 vs iOS-27 controller/daemon v11) +
|
||||
// RegisterNotificationPort off-by-one, so the personalized DDI attaches
|
||||
// (/System/Developer auto-mount). Pairs with the sandbox ops[124] allow
|
||||
// and the diskimagesiod isMountComplete→YES userland patch (cfw_install).
|
||||
if applyIOS27 {
|
||||
patchIoucFailedSandbox()
|
||||
patchDiskImages2ClientAbi()
|
||||
}
|
||||
|
||||
// Group B
|
||||
patchPostValidationAdditional()
|
||||
|
||||
Reference in New Issue
Block a user