diff --git a/research/0_binary_patch_comparison.md b/research/0_binary_patch_comparison.md index face339..ede8035 100644 --- a/research/0_binary_patch_comparison.md +++ b/research/0_binary_patch_comparison.md @@ -127,6 +127,7 @@ | JB-01 | A | `patch_amfi_cdhash_in_trustcache` | `AMFIIsCDHashInTrustCache` | Always return true + store hash | Y | | JB-02 | A | `patch_amfi_execve_kill_path` | AMFI execve kill return site | Convert shared kill return from deny to allow (superseded by C21; standalone only) | N | | JB-02b| C | `patch_exec_security_policy_kill` | XNU exec `imgp->ip_mac_return` gate (kern_exec, `os_reason_create(OS_REASON_EXEC, EXEC_EXIT_REASON_SECURITY_POLICY)` site) | Flip `cbz wN, ` → unconditional `b ` so the exec-time MAC-verdict `SECURITY_POLICY` kill is unreachable. **Needed to run a userland NEWER than the kernel (iOS 27.0 on the 26.4 kernel):** AMFI's exec hooks reject the newer binaries' code-sign validation category, setting `ip_mac_return != 0` → core daemons (backboardd/cfprefsd/containermanagerd/…) die at exec (`namespace 9 / code 0x8`) → boot deadlock. Validated on 27.0/26.4: 0 SECURITY_POLICY kills, daemons launch, networking + SSH come up. No-op-in-effect for version-matched userlands (ip_mac_return == 0 there, so the original cbz already skips). | Y | +| JB-02c| B | `patch_paravirt_display_primary` | `AppleParavirtDisplay` IOKit `setProperty("primary", , 32)` value load (`ldr w2,[xN,#imm]`) | Rewrite the value load to `mov w2,#1` so the VM's single paravirt display is published `primary=1`. **Needed for the display on iOS 27:** 27 refactored FrontBoard (`FBSDisplayMonitor`) to REQUIRE a designated main display (`_initWithDisplays:mainDisplay:` hard-asserts non-nil; new `isMainDisplay:` threading — foldable/multi-display groundwork). The paravirt display was published `primary=0` / `Transport=Other`, so CoreDisplay/FrontBoard saw no built-in main display → `mainDisplay=nil` → SpringBoard crash-loops (`failed to initialize mainDisplay source`) → black screen. Anchor (structural): exact `"primary\0"` cstring xref whose site is the `setProperty(key,value,32)` form (`add …,#` → `mov w3,#0x20` → `blraa`), then the nearest preceding `ldr w2,[…]`. **DISABLED — WRONG THEORY, HARMFUL on iOS 27 (2026-07-15).** primary=1 was based on the mistaken belief that iOS 27 keys mainDisplay off the IOMFB `primary` property. It does not; instead 27 reads the `primary` property VALUE and appends it as the display-name suffix → the display is named `"primary-1"`, which then FAILS the render server's exact-name match → still no mainDisplay. The real cause was the IOUC sandbox gate (see JB-10b), which is the actual fix. Keep this OFF (primary must stay 0 so the name is `"primary"`). | N | | JB-03 | C | `patch_cred_label_update_execve` | `_cred_label_update_execve` | Reworked 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 validation | N | | JB-04 | C | `patch_hook_cred_label_update_execve` | sandbox `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 wrapper | Y | | JB-05 | C | `patch_kcall10` | `sysent[439]` (`SYS_kas_info` replacement) | Rebuilt ABI-correct kcall cave: `target + 7 args -> uint64 x0`; re-enabled after focused dry-run validation | Y | @@ -135,6 +136,7 @@ | 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-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 `; `ldr w8,[sp,#x]; cbnz w8,` (other error → deny block w/ fail-log ADRP → returns error); w0==0 path → `b `. Patch rewrites ``'s first insn → `b ` (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 | | JB-12 | B | `patch_proc_pidinfo` | `_proc_pidinfo` | Allow pid 0 info | Y | | JB-13 | B | `patch_convert_port_to_map` | `_convert_port_to_map_with_flavor` | Skip kernel map panic | Y | @@ -175,7 +177,7 @@ do NOT execute these). | 6 | Weak dylib load injection | `launchd` | Load short alias `/b` (copy of `launchdhook.dylib`) at launch. On by default; set `DISABLE_LAUNCHD_HOOK=1` to skip because this pid-1 hook path is boot-critical and has produced boot-analysis failures | - | Y | Y | | 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,#` -> `mov w3,#0x560` 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: userland sends a smaller external-method-5 SwapEnd state than the 26.1-era 0x560 the userclient expects, so SwapEnd returns `kIOReturnBadArgument` and the host display stays black (guest still renders — the Apple logo is visible over VNC, just not in the vphone-cli view). Source sizes observed: 26.0/26.0.1 = 0x548, 18.6.2 = 0x514. 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 0x560 regardless of source and re-attests the modified DSC page. Install gate fires when `ProductVersion` starts with `26.0` or `18.`. 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 now renders in the vphone-cli view); 26.1 remains unmodified. | Y | Y | Y | +| 9 | `mov w3,#` -> `mov w3,#` in `_kern_SwapEnd` — **26.0/26.0.1, 18.x, and 27.0** | 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. **Target is chosen per base**: because reliably reading it from the kernelcache needs the IOMFB userclient dispatch table (a blind shape-scan is ambiguous — 8 candidates), the install keys the target off the userland version as an interim proxy for the validated base pairing — `27.*` -> 0x588 (26.4 base), `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; 26.4-base + 27.0 (-> 0x588) patched + re-attested clean on the 24A5380h DSC, pending on-device display confirmation. TODO: replace the version proxy with real per-kernel `checkStructureInputSize` detection. | 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 | ### Installed Components @@ -657,7 +659,7 @@ 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 and 26.0.1` only) | Y | Y | Y (inherited from base run) | Y (inherited from base run) | +| 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) | | 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) | diff --git a/scripts/cfw_install.sh b/scripts/cfw_install.sh index 343dffe..ce106f9 100755 --- a/scripts/cfw_install.sh +++ b/scripts/cfw_install.sh @@ -283,19 +283,38 @@ else echo " [+] Cryptex installed" fi -# Some userland versions send an IOMobileFramebuffer SwapEnd state smaller than -# the 26.1-era 0x560 the PCC vphone600 userclient expects, so SwapEnd returns -# kIOReturnBadArgument and the host VZ display stays black (guest still renders; -# visible over VNC). Known: 26.0/26.0.1 send 0x548, 18.x sends 0x514 (18.6.2). -# Patch only that immediate in the installed DSC; do not replace frameworks or -# normalize GPU metadata. The patcher is semantic + idempotent (rewrites the -# SwapEnd size to 0x560, no-op if already 0x560). +# Some userland versions send an IOMobileFramebuffer SwapEnd state whose size +# differs from what the PCC vphone600 userclient expects (an exact +# checkStructureInputSize check), so SwapEnd returns kIOReturnBadArgument and +# the host VZ display stays black (guest still renders; visible over VNC). +# +# The accepted size is a property of the BASE KERNEL, not the userland: +# - 26.1 base: userclient expects 0x560 +# - 26.4 base (xnu-12377, current): userclient expects 0x588 +# Reliably reading it from the kernelcache needs the IOMFB userclient dispatch +# table (a blind shape-scan is ambiguous — 8 candidates), so until that dynamic +# detection lands we key the target off the userland version as a proxy for the +# validated base pairing: +# - 27.x runs on the 26.4 base -> 0x588 +# - 26.0/26.0.1 and 18.x validated on 26.1 base -> 0x560 +# Known userland-sent sizes: 18.x -> 0x514, 26.0/26.0.1 -> 0x548, 27.0 -> 0x6e0. +# Patch only that immediate in the installed DSC; the patcher is semantic + +# idempotent (rewrites the SwapEnd size to the target, no-op if already there). +# NOTE: iOS 27 is intentionally NOT truncated here — its swap struct (0x6e0) has +# a new layout, so size-truncation to 0x588 feeds the kernel misaligned data. +# Instead the JB kernel patch `patchIomfbSwapEndVariableSize` makes the userclient +# accept iOS 27's native 0x6e0 struct (variable-size dispatch), so 27 must send +# its native size — leave it unpatched here. IOS_VERSION=$(/usr/bin/plutil -extract ProductVersion raw -o - "$MNT1/System/Library/CoreServices/SystemVersion.plist" 2>/dev/null || true) -if [[ "$IOS_VERSION" == 26.0* || "$IOS_VERSION" == 18.* ]]; then - echo " [*] Patching IOMobileFramebuffer SwapEnd payload size (iOS $IOS_VERSION -> 0x560)..." +IOMFB_TARGET="" +case "$IOS_VERSION" in + 26.0*|18.*) IOMFB_TARGET=0x560 ;; +esac +if [[ -n "$IOMFB_TARGET" ]]; then + echo " [*] Patching IOMobileFramebuffer SwapEnd payload size (iOS $IOS_VERSION -> $IOMFB_TARGET)..." DSC_DIR="$MNT1/System/Cryptexes/OS/System/Library/Caches/com.apple.dyld" [[ -d "$DSC_DIR" ]] || die "dyld cache dir missing: $DSC_DIR" - "$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" patch-iomfb-swapend "$DSC_DIR" + "$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" patch-iomfb-swapend "$DSC_DIR" --target-size "$IOMFB_TARGET" fi # Newer userlands ship a dyld shared cache that nearly fills the vphone600 26.x diff --git a/scripts/cfw_install_dev.sh b/scripts/cfw_install_dev.sh index 08213f2..1913e86 100755 --- a/scripts/cfw_install_dev.sh +++ b/scripts/cfw_install_dev.sh @@ -300,19 +300,38 @@ safe_detach "$MNT_APPOS" echo " [+] Cryptex installed" -# Some userland versions send an IOMobileFramebuffer SwapEnd state smaller than -# the 26.1-era 0x560 the PCC vphone600 userclient expects, so SwapEnd returns -# kIOReturnBadArgument and the host VZ display stays black (guest still renders; -# visible over VNC). Known: 26.0/26.0.1 send 0x548, 18.x sends 0x514 (18.6.2). -# Patch only that immediate in the installed DSC; do not replace frameworks or -# normalize GPU metadata. The patcher is semantic + idempotent (rewrites the -# SwapEnd size to 0x560, no-op if already 0x560). +# Some userland versions send an IOMobileFramebuffer SwapEnd state whose size +# differs from what the PCC vphone600 userclient expects (an exact +# checkStructureInputSize check), so SwapEnd returns kIOReturnBadArgument and +# the host VZ display stays black (guest still renders; visible over VNC). +# +# The accepted size is a property of the BASE KERNEL, not the userland: +# - 26.1 base: userclient expects 0x560 +# - 26.4 base (xnu-12377, current): userclient expects 0x588 +# Reliably reading it from the kernelcache needs the IOMFB userclient dispatch +# table (a blind shape-scan is ambiguous — 8 candidates), so until that dynamic +# detection lands we key the target off the userland version as a proxy for the +# validated base pairing: +# - 27.x runs on the 26.4 base -> 0x588 +# - 26.0/26.0.1 and 18.x validated on 26.1 base -> 0x560 +# Known userland-sent sizes: 18.x -> 0x514, 26.0/26.0.1 -> 0x548, 27.0 -> 0x6e0. +# Patch only that immediate in the installed DSC; the patcher is semantic + +# idempotent (rewrites the SwapEnd size to the target, no-op if already there). +# NOTE: iOS 27 is intentionally NOT truncated here — its swap struct (0x6e0) has +# a new layout, so size-truncation to 0x588 feeds the kernel misaligned data. +# Instead the JB kernel patch `patchIomfbSwapEndVariableSize` makes the userclient +# accept iOS 27's native 0x6e0 struct (variable-size dispatch), so 27 must send +# its native size — leave it unpatched here. IOS_VERSION=$(/usr/bin/plutil -extract ProductVersion raw -o - "$MNT1/System/Library/CoreServices/SystemVersion.plist" 2>/dev/null || true) -if [[ "$IOS_VERSION" == 26.0* || "$IOS_VERSION" == 18.* ]]; then - echo " [*] Patching IOMobileFramebuffer SwapEnd payload size (iOS $IOS_VERSION -> 0x560)..." +IOMFB_TARGET="" +case "$IOS_VERSION" in + 26.0*|18.*) IOMFB_TARGET=0x560 ;; +esac +if [[ -n "$IOMFB_TARGET" ]]; then + echo " [*] Patching IOMobileFramebuffer SwapEnd payload size (iOS $IOS_VERSION -> $IOMFB_TARGET)..." DSC_DIR="$MNT1/System/Cryptexes/OS/System/Library/Caches/com.apple.dyld" [[ -d "$DSC_DIR" ]] || die "dyld cache dir missing: $DSC_DIR" - "$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" patch-iomfb-swapend "$DSC_DIR" + "$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" patch-iomfb-swapend "$DSC_DIR" --target-size "$IOMFB_TARGET" fi # ═══════════ 2/7 PATCH SEPUTIL ════════════════════════════════ diff --git a/scripts/patchers/cfw.py b/scripts/patchers/cfw.py index 5b413a9..146564b 100755 --- a/scripts/patchers/cfw.py +++ b/scripts/patchers/cfw.py @@ -160,11 +160,16 @@ def main(): elif cmd == "patch-iomfb-swapend": if len(sys.argv) < 3: - print("Usage: patch_cfw.py patch-iomfb-swapend [--dry-run]") + print("Usage: patch_cfw.py patch-iomfb-swapend " + "[--target-size ] [--dry-run]") sys.exit(1) dry_run = "--dry-run" in sys.argv[3:] + kwargs = {} + if "--target-size" in sys.argv: + i = sys.argv.index("--target-size") + kwargs["target_size"] = int(sys.argv[i + 1], 0) try: - patch_iomfb_swapend(sys.argv[2], dry_run=dry_run) + patch_iomfb_swapend(sys.argv[2], dry_run=dry_run, **kwargs) except ValueError as e: print(f"[-] {e}") sys.exit(1) diff --git a/scripts/patchers/cfw_patch_iomfb_swapend.py b/scripts/patchers/cfw_patch_iomfb_swapend.py index 73188b7..37e7d08 100644 --- a/scripts/patchers/cfw_patch_iomfb_swapend.py +++ b/scripts/patchers/cfw_patch_iomfb_swapend.py @@ -1,27 +1,41 @@ -"""Patch iOS 26.0 and 26.0.1 IOMobileFramebuffer SwapEnd payload size. +"""Patch IOMobileFramebuffer SwapEnd payload size to match the base kernel. -The PCC vphone600 26.1 kernel-side IOMobileFramebuffer external method 5 -expects the newer 0x560-byte swap state. iOS 26.0 and 26.0.1 userland send a -smaller state (0x548), so SwapEnd returns kIOReturnBadArgument and the VM -display stays black. +The SwapEnd input-state size is enforced kernel-side: the PCC vphone600 +kernel's IOMobileFramebufferUserClient external method 5 (SwapEnd / +swap_submit) does an exact `checkStructureInputSize` check. A userland whose +`_kern_SwapEnd` sends a different size gets kIOReturnBadArgument, so no frame +is presented and the host VZ display stays black (the 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 (older): userclient expects 0x560. + - 26.4 base (xnu-12377, current): userclient expects 0x588. 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), and empirically — native 26.5 userland + sends 0x588 and displays correctly on this stack. + +Known userland-sent sizes: 18.6.2 -> 0x514, 26.0/26.0.1 -> 0x548, +26.5 -> 0x588 (native match on 26.4), 27.0 (24A5380h) -> 0x6e0. `_kern_SwapEnd` sets up an external-method-5 call: ldr w0, [x0,#0x14] add x2, x19,#0x18 mov w1,#5 <- external method selector 5 - mov w3,# <- input-state size (0x548 on 26.0; 0x588 on 26.5) + mov w3,# <- input-state size (source; version-specific) mov x4,#0 mov x5,#0 bl _io_connect_method -The `mov w3,#` immediate is what this patcher rewrites to 0x560, the -size the 26.1 userclient accepts. The site is located dynamically: resolve -`_kern_SwapEnd`, disassemble it with Capstone, and anchor on the semantic -call-setup shape (selector `mov w1,#5` then `mov w3,#imm` then the zeroed -`mov x4,#0`/`mov x5,#0` and the `bl`). Nothing about the source size is -hardcoded, so this fires on 26.0 and 26.0.1 alike; the replacement immediate -comes from the Keystone-backed `asm()` helper. +The `mov w3,#` immediate is what this patcher rewrites to the target +size (default 0x588 for the 26.4 base; override via `target_size`). The site +is located dynamically: resolve `_kern_SwapEnd`, disassemble it with Capstone, +and anchor on the semantic call-setup shape (selector `mov w1,#5` then +`mov w3,#imm` then the zeroed `mov x4,#0`/`mov x5,#0` and the `bl`). Nothing +about the source size is hardcoded — it is discovered, never matched — so this +fires on any userland; the replacement immediate comes from the +Keystone-backed `asm()` helper. """ import os @@ -44,11 +58,13 @@ except ImportError: # direct self-test execution IOMFB = "/System/Library/PrivateFrameworks/IOMobileFramebuffer.framework/IOMobileFramebuffer" SWAPEND_SYMBOL = "_kern_SwapEnd" -# External-method selector for SwapEnd, and the input-state size the 26.1 -# vphone600 userclient accepts. TARGET_SIZE is the semantic goal, not an -# anchor — the source immediate (0x548 on 26.0) is discovered, never matched. +# External-method selector for SwapEnd, and the input-state size the 26.4 +# vphone600 userclient accepts (checkStructureInputSize). TARGET_SIZE is the +# semantic goal, not an anchor — the source immediate (e.g. 0x6e0 on 27.0) is +# discovered, never matched. Override via patch_iomfb_swapend(target_size=...) +# when building against a different base kernel (26.1 base wants 0x560). SWAPEND_SELECTOR = 5 -TARGET_SIZE = 0x560 +TARGET_SIZE = 0x588 def _resolve_symbol(dsc_path, image, symbol): @@ -114,7 +130,8 @@ def _find_swap_size_insn(insns): return None -def patch_iomfb_swapend(chunks_dir, *, dsc_path=None, dry_run=False): +def patch_iomfb_swapend(chunks_dir, *, dsc_path=None, target_size=TARGET_SIZE, + dry_run=False): chunks = DSCChunks(chunks_dir) print(f" [.] {chunks!r}") @@ -134,17 +151,17 @@ def patch_iomfb_swapend(chunks_dir, *, dsc_path=None, dry_run=False): _reg, cur_size = _mov_reg_imm(target) insn_vma = target.address - new_bytes = asm(f"mov w3, #{TARGET_SIZE}") + new_bytes = asm(f"mov w3, #{target_size}") if len(new_bytes) != 4: raise RuntimeError(f"expected 4 bytes, got {len(new_bytes)}") - if cur_size == TARGET_SIZE: - print(f" [=] already 0x{TARGET_SIZE:X} at 0x{insn_vma:X}; " + if cur_size == target_size: + print(f" [=] already 0x{target_size:X} at 0x{insn_vma:X}; " f"re-attesting page only") else: action = "would patch" if dry_run else "patched" print(f" [+] {action} {IOMFB} {SWAPEND_SYMBOL} size " - f"0x{cur_size:X} -> 0x{TARGET_SIZE:X} at 0x{insn_vma:X}") + f"0x{cur_size:X} -> 0x{target_size:X} at 0x{insn_vma:X}") if not dry_run: chunks.write_at_vma(insn_vma, new_bytes) @@ -178,7 +195,8 @@ def _self_test(): reg, imm = _mov_reg_imm(target) assert (reg, imm) == ("w3", 0x548), (reg, hex(imm)) assert target.address == 0x1000 + 12, hex(target.address) - assert asm(f"mov w3, #{TARGET_SIZE}") == bytes.fromhex("03ac8052") + assert TARGET_SIZE == 0x588, hex(TARGET_SIZE) + assert asm(f"mov w3, #{TARGET_SIZE}") == bytes.fromhex("03b18052") print("self-test OK") diff --git a/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchIomfbSwap.swift b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchIomfbSwap.swift new file mode 100644 index 0000000..c94f1cf --- /dev/null +++ b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchIomfbSwap.swift @@ -0,0 +1,144 @@ +// KernelJBPatchIomfbSwap.swift — JB kernel patch: make the IOMobileFramebuffer +// SwapEnd userclient accept the caller's native swap-struct size. +// +// Background: iOS 27's `_kern_SwapEnd` sends a 0x6e0-byte IOMFBSwapRec, but the +// 26.4 kernel's IOMobileFramebufferUserClient external-method-5 dispatch does an +// EXACT `checkStructureInputSize == 0x588` check → 27's call is rejected +// (kIOReturnBadArgument), no frame presented. The userland size patch (truncate +// 0x6e0 -> 0x588) makes the check pass but feeds the 26.4 handler a truncated / +// misaligned struct (iOS 27's layout ≠ 26.4's) → still no valid frame. +// +// This patch flips that dispatch entry's `checkStructureInputSize` to +// kIOUCVariableStructureSize (0xffffffff) so the kernel accepts the caller's +// native size and passes the full, correctly-laid-out struct to the handler. +// Combined with LEAVING iOS 27's userland at its native 0x6e0 (no truncation), +// the handler then reads its fields from an authentic iOS-27 IOMFBSwapRec — which +// works iff iOS 27's layout is a superset of what the 26.4 handler reads. +// +// Anchor (structural, no hardcoded offsets): the sole IOExternalMethodDispatch +// entry whose shape matches the SwapEnd selector — an 8-byte ptrauth-signed code +// pointer followed by checkScalarInputCount==0, checkStructureInputSize==0x588, +// checkScalarOutputCount==0, checkStructureOutputSize==0. Verified unique in the +// vphone600 26.4 kernelcache. Scanned in __DATA_CONST (where the dispatch table +// lives), 8-byte aligned. +// +// NOTE: only meaningful for an iOS-27 build; on a 26.x userland the native size +// is 0x588 anyway. Variable-size is safe for callers that send >= the fields the +// handler reads (26.5=0x588, 27=0x6e0 both do). + +import Foundation + +extension KernelJBPatcher { + private static let swapEndExpectedSize: UInt32 = 0x588 // 26.4 kernel's native SwapEnd struct size + private static let swapEndIOS27Size: UInt32 = 0x6e0 // iOS 27's native IOMFBSwapRec size + private static let kIOUCVariableStructureSize: UInt32 = 0xFFFF_FFFF + + /// The swap_submit handler has a SECOND, internal exact-size gate beyond the + /// dispatch table's checkStructureInputSize: + /// cmp w2, #0x588 ; b.ne (w2 = structureInputSize) + /// With iOS 27 sending 0x6e0 this branches to the error path (kIOReturnBadArgument, + /// swap aborted) even after the dispatch check is relaxed — so no frame is ever + /// presented (no Apple logo, no UI). Retarget the compare to iOS 27's size so the + /// handler takes the success path and processes the native struct. Anchor is the + /// unique `cmp w2, #0x588` immediately followed by `b.ne` (semantic; the 0x588 is + /// the SwapEnd struct size the handler gates on — the value being changed). Only + /// the imm12 field is rewritten, preserving the rest of the instruction. + @discardableResult + func patchIomfbSwapEndHandlerSize() -> Bool { + log("\n[JB] IOMFB swap_submit handler size gate cmp w2,#0x588 -> #0x6e0 (accept iOS 27 native struct)") + + guard let (ks, ke) = kernTextRange else { + log(" [-] no kernel text range") + return false + } + + // cmp w2,#imm == SUBS wzr,w2,#imm : 0x71000000 | imm12<<10 | Rn(2)<<5 | Rd(31) + let cmpW2Old: UInt32 = 0x7100_0000 | (Self.swapEndExpectedSize << 10) | (2 << 5) | 31 + + var hits: [Int] = [] + var off = ks + while off + 8 <= ke { + if buffer.readU32(at: off) == cmpW2Old { + // Confirm the following instruction is a conditional b.ne (the gate). + if let nxt = disasAt(off + 4), nxt.mnemonic == "b.ne" { + hits.append(off) + } + } + off += 4 + } + + guard hits.count == 1 else { + log(" [-] swap_submit handler size gate (cmp w2,#0x588 -> b.ne) not found uniquely (found \(hits.count))") + return false + } + + let cmpOff = hits[0] + // Rewrite only the imm12 field [21:10] to the iOS 27 size. + var word = buffer.readU32(at: cmpOff) + word = (word & ~(UInt32(0xFFF) << 10)) | (Self.swapEndIOS27Size << 10) + var le = word.littleEndian + var newBytes = Data(count: 4) + withUnsafeBytes(of: &le) { newBytes.replaceSubrange(0..<4, with: $0) } + + let va = fileOffsetToVA(cmpOff) + emit( + cmpOff, + newBytes, + patchID: "iomfb_swapend_handler_size", + virtualAddress: va, + description: "swap_submit cmp w2,#0x588 -> #0x6e0 [accept iOS 27 native SwapEnd struct]" + ) + return true + } + + @discardableResult + func patchIomfbSwapEndVariableSize() -> Bool { + log("\n[JB] IOMFB SwapEnd dispatch checkStructureInputSize -> variable (accept iOS 27 native struct)") + + guard let seg = segments.first(where: { $0.name == "__DATA_CONST" }), seg.fileSize > 0 else { + log(" [-] no __DATA_CONST segment") + return false + } + let start = Int(seg.fileOffset) + let end = start + Int(seg.fileSize) + + var hits: [Int] = [] + var off = start + while off + 24 <= end { + // entry: ptr(8) scalarIn(4) structIn(4) scalarOut(4) structOut(4) + let structIn = buffer.readU32(at: off + 12) + if structIn == Self.swapEndExpectedSize { + let scalarIn = buffer.readU32(at: off + 8) + let scalarOut = buffer.readU32(at: off + 16) + let structOut = buffer.readU32(at: off + 20) + let ptrHi = buffer.readU32(at: off + 4) // top word of the 8-byte fn ptr + let topByte = ptrHi >> 24 + if scalarIn == 0, scalarOut == 0, structOut == 0, topByte >= 0x80 { + hits.append(off) + } + } + off += 8 // pointer-aligned dispatch entries + } + + guard hits.count == 1 else { + log(" [-] SwapEnd dispatch entry not found uniquely (found \(hits.count))") + return false + } + + let entryOff = hits[0] + let sizeFieldOff = entryOff + 12 + var newBytes = Data(count: 4) + var v = Self.kIOUCVariableStructureSize.littleEndian + withUnsafeBytes(of: &v) { newBytes.replaceSubrange(0..<4, with: $0) } + + let va = fileOffsetToVA(sizeFieldOff) + emit( + sizeFieldOff, + newBytes, + patchID: "iomfb_swapend_variable_size", + virtualAddress: va, + description: "SwapEnd checkStructureInputSize 0x588 -> variable [accept iOS 27 native IOMFBSwapRec]" + ) + return true + } +} diff --git a/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchIoucSandbox.swift b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchIoucSandbox.swift new file mode 100644 index 0000000..1f450ba --- /dev/null +++ b/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchIoucSandbox.swift @@ -0,0 +1,110 @@ +// KernelJBPatchIoucSandbox.swift — JB kernel patch: IOUC *sandbox* gate bypass. +// +// Sibling to patchIoucFailedMacf. The IOKit user-client open path runs TWO +// independent MAC gates: a MACF-aggregator check ("IOUC %s failed MACF in +// process %s", handled by patchIoucFailedMacf) and a Sandbox check ("IOUC %s +// failed sandbox in process %s"). Only the MACF one was patched. +// +// On iOS 27 userland atop the 26.4 vphone600 kernel, the Sandbox gate +// spuriously DENIES the render server (backboardd) its opens of +// IOMobileFramebufferUserClient / IOSurfaceRootUserClient / IOHIDEventService +// (confirmed via serial "IOUC IOMobileFramebufferUserClient failed sandbox in +// process pid , backboardd" — ABSENT on a native 26.4 userland, which +// displays fine, and backboardd is absent from every IOUserClientCreator). With +// the render server denied the framebuffer there is no present (no Apple logo) +// and no vended main display (SpringBoard's FBSDisplayMonitor asserts on a nil +// mainDisplay → crash-loop). +// +// Real shape of the gate (offsets illustrative): +// blraa x8, x17 ; sandbox check (PAC-indirect, NOT a plain BL) +// mov w8, #0x2c7 ; movk w8,#0xe000,lsl#16 ; w8 = kIOReturnNotPermitted +// str w0, [sp,#X] ; cmp w0,w8 ; b.eq ; NotPermitted → allow +// ldr w8, [sp,#X] ; cbnz w8, ; other error → deny +// ... (w0==0 path) ... b +// : ...pac cleanup... ADRP "failed sandbox" ...log... return error +// : str wzr,[sp,#X]; ...; bl +// +// Fix: rewrite the FIRST instruction of the deny block (, the CBNZ +// target that encloses the fail-log ADRP) with an unconditional B to +// (the B.EQ / NotPermitted allow-proceed target). This turns the denied open +// into an allowed one while leaving the w0==0 (already-allowed) path untouched. +// Anchor is structural (fail-log string → xref → the CBNZ whose target encloses +// it → the immediately-preceding B.EQ allow target); no hardcoded offsets. + +import Foundation + +extension KernelJBPatcher { + @discardableResult + func patchIoucFailedSandbox() -> Bool { + log("\n[JB] IOUC sandbox gate: deny-block → allow redirect") + + guard let failStrOff = buffer.findString("IOUC %s failed sandbox in process %s") else { + log(" [-] IOUC failed-sandbox format string not found") + return false + } + let refs = findStringRefs(failStrOff) + guard !refs.isEmpty else { + log(" [-] no xrefs for IOUC failed-sandbox format string") + return false + } + + for (adrpOff, _) in refs { + guard let funcStart = findFunctionStart(adrpOff) else { continue } + let funcEnd = findFuncEnd(funcStart, maxSize: 0x2000) + + // Find the CBNZ Wn, whose target encloses the fail-log ADRP. + var off = funcStart + while off < adrpOff { + defer { off += 4 } + let insn = buffer.readU32(at: off) + guard isCbnzW(insn) else { continue } + guard let denyEntry = cbTarget(insn, at: off) else { continue } + // The fail-log ADRP must sit inside the deny block. + guard denyEntry <= adrpOff, adrpOff < denyEntry + 0x60, + denyEntry > funcStart, denyEntry < funcEnd else { continue } + + // The allow target is the NotPermitted B.EQ, a couple insns before + // the CBNZ (cmp ; b.eq ; ldr ; cbnz). Search a small window. + var allowTarget = -1 + for back in stride(from: off - 4, through: off - 0x14, by: -4) where back > funcStart { + let bi = buffer.readU32(at: back) + if let t = bCondEqTarget(bi, at: back), t > funcStart, t < funcEnd { + allowTarget = t + break + } + } + guard allowTarget >= 0 else { continue } + + guard let patchBytes = ARM64Encoder.encodeB(from: denyEntry, to: allowTarget) else { continue } + let delta = allowTarget - denyEntry + let va = fileOffsetToVA(denyEntry) + log(" [+] IOUC sandbox gate fn=0x\(String(format: "%X", funcStart)), cbnz=0x\(String(format: "%X", off)), deny=0x\(String(format: "%X", denyEntry)) → allow=0x\(String(format: "%X", allowTarget))") + emit(denyEntry, patchBytes, + patchID: "iouc_sandbox_gate", + virtualAddress: va, + description: "b #\(delta >= 0 ? "" : "-")0x\(String(format: "%X", abs(delta))) [IOUC sandbox deny → allow]") + return true + } + } + + log(" [-] narrow IOUC sandbox deny branch not found") + return false + } + + /// CBNZ Wt,