mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
kernel: jb: Fix iOS 27 guest display via IOUC sandbox gate bypass
iOS 27 userland on the 26.4 vphone600 kernel had SpringBoard crash-looping
with no display. Root cause: the IOKit user-client open path runs TWO
independent MAC gates -- a MACF-aggregator check (already patched by
patchIoucFailedMacf) and a separate Sandbox check -- and the Sandbox gate
spuriously DENIES the render server (backboardd) its opens of
IOMobileFramebufferUserClient / IOSurfaceRootUserClient / IOHIDEventService.
27-specific: absent on a native 26.4 userland. Denied the framebuffer,
backboardd can't present (no Apple logo) and vends no main display, so
FBSDisplayMonitor asserts on a nil mainDisplay -> crash-loop.
Add patchIoucFailedSandbox (KernelJBPatchIoucSandbox.swift), mirroring
patchIoucFailedMacf: anchor on the "IOUC %s failed sandbox in process %s"
string, find the deny block (the CBNZ target enclosing the fail-log ADRP),
and rewrite its first instruction with an unconditional B to the
NotPermitted allow-proceed target. Structural anchors only, no hardcoded
offsets. No-op where the gate already allows (native 26.x userlands).
Verified: backboardd opens the framebuffer, SpringBoard runs (0 crashes),
display enumerates (LCD/primary), [CADisplay mainDisplay] resolves, and the
guest GUI renders (confirmed visible over VNC).
Disable three earlier wrong-theory display patches (kept in-tree, off in
findAll, for the record):
- patchParavirtDisplayPrimary: setting primary=1 is actively HARMFUL on 27
-- it becomes the display NAME suffix ("primary-1") and breaks the render
server's exact-name match.
- patchIomfbSwapEnd{VariableSize,HandlerSize}: iOS 27 never uses IOMFB
method 5 (SwapEnd) for present -- confirmed by kernel trace of the real
handler (cmp w2,#0x588) with SpringBoard actively presenting -- so these
are irrelevant on 27 and would break 26.x's native 0x588 SwapEnd.
SwapEnd userland DSC size patch made per-base (cfw_install*, cfw.py,
cfw_patch_iomfb_swapend.py): 26.x validated, harmless on 27.
research/0_binary_patch_comparison.md updated (JB-10b added for the sandbox
gate; JB-02c corrected to disabled/wrong-theory).
Still open: the normal (VZ) render path stays black even though the guest
presents (VNC works) -- 27 uses a present mechanism the 26.4 paravirt-GPU
path doesn't receive. Tracked separately for follow-up.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
committed by
zqxwce
co-authored by
Claude Opus 4.8
parent
679d3d0476
commit
c22da90929
@@ -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, <skip>` → unconditional `b <skip>` 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", <flag>, 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 …,#<primary>` → `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 <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 |
|
||||
| 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,#<size>` -> `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,#<size>` -> `mov w3,#<base-size>` 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) |
|
||||
|
||||
+29
-10
@@ -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
|
||||
|
||||
+29
-10
@@ -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 ════════════════════════════════
|
||||
|
||||
@@ -160,11 +160,16 @@ def main():
|
||||
|
||||
elif cmd == "patch-iomfb-swapend":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: patch_cfw.py patch-iomfb-swapend <chunks_dir> [--dry-run]")
|
||||
print("Usage: patch_cfw.py patch-iomfb-swapend <chunks_dir> "
|
||||
"[--target-size <hex|int>] [--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)
|
||||
|
||||
@@ -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,#<size> <- input-state size (0x548 on 26.0; 0x588 on 26.5)
|
||||
mov w3,#<size> <- input-state size (source; version-specific)
|
||||
mov x4,#0
|
||||
mov x5,#0
|
||||
bl _io_connect_method
|
||||
|
||||
The `mov w3,#<size>` 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,#<size>` 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")
|
||||
|
||||
|
||||
|
||||
@@ -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 <error> (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
|
||||
}
|
||||
}
|
||||
@@ -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 <N>, 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 <ALLOW> ; NotPermitted → allow
|
||||
// ldr w8, [sp,#X] ; cbnz w8, <DENY> ; other error → deny
|
||||
// ... (w0==0 path) ... b <ALLOW>
|
||||
// <DENY>: ...pac cleanup... ADRP "failed sandbox" ...log... return error
|
||||
// <ALLOW>: str wzr,[sp,#X]; ...; bl <proceed-to-open>
|
||||
//
|
||||
// Fix: rewrite the FIRST instruction of the deny block (<DENY>, the CBNZ
|
||||
// target that encloses the fail-log ADRP) with an unconditional B to <ALLOW>
|
||||
// (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, <DENY> 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 <ALLOW> ; 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, <label> (32-bit): high byte 0x35.
|
||||
private func isCbnzW(_ insn: UInt32) -> Bool { ((insn >> 24) & 0xFF) == 0x35 }
|
||||
|
||||
/// Decode CBZ/CBNZ target (imm19, sign-extended, scaled by 4).
|
||||
private func cbTarget(_ insn: UInt32, at pc: Int) -> Int? {
|
||||
let imm19 = (insn >> 5) & 0x7FFFF
|
||||
return pc + Int(Int32(bitPattern: imm19 << 13) >> 13) * 4
|
||||
}
|
||||
|
||||
/// If `insn` is B.EQ <label>, return its target; else nil.
|
||||
/// B.cond: [31:24]=0x54, [4]=0, cond=[3:0]; EQ cond = 0.
|
||||
private func bCondEqTarget(_ insn: UInt32, at pc: Int) -> Int? {
|
||||
guard (insn & 0xFF00_0010) == 0x5400_0000, (insn & 0xF) == 0x0 else { return nil }
|
||||
let imm19 = (insn >> 5) & 0x7FFFF
|
||||
return pc + Int(Int32(bitPattern: imm19 << 13) >> 13) * 4
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// KernelJBPatchParavirtDisplay.swift — JB kernel patch: publish the VM's single
|
||||
// AppleParavirtDisplay as the primary/main display.
|
||||
//
|
||||
// iOS 27 refactored FrontBoard (FBSDisplayMonitor) to REQUIRE a designated main
|
||||
// display: `-[FBSDisplayMonitor _initWithDisplays:mainDisplay:bookendObserver:transformer:]`
|
||||
// hard-asserts `mainDisplay != nil`, and the new `isMainDisplay:` flag is threaded
|
||||
// through display construction. (Groundwork for multi-display/foldable hardware.)
|
||||
//
|
||||
// The VZ paravirt display is published generic: `AppleParavirtDisplay` sets its IOKit
|
||||
// "primary" property from a per-display field that is 0, so CoreDisplay/FrontBoard
|
||||
// never sees a built-in/primary display → mainDisplay stays nil → SpringBoard
|
||||
// crash-loops ("failed to initialize mainDisplay source -> mainDisplay=(null)") →
|
||||
// black screen. iOS 26.x had no main-display requirement, so the same VM booted fine.
|
||||
//
|
||||
// Force the published value to 1. In AppleParavirtDisplay the property is set via
|
||||
// IOKit `setProperty("primary", value, 32)`; the value is loaded into w2 by a
|
||||
// `ldr w2,[xN,#imm]` immediately before the call. Rewrite that load to `mov w2,#1`
|
||||
// so the sole display is always published primary=1.
|
||||
//
|
||||
// Anchor (structural, no hardcoded offsets): the exact "primary\0" cstring xref
|
||||
// whose call site is the `setProperty(key, value, numberOfBits=32)` form — an
|
||||
// `add xN,xN,#<primary>` followed within a few insns by `mov w3,#0x20` then a
|
||||
// `blraa` — distinguishing it from the sibling getProperty("primary") ref. From
|
||||
// that ADRP, the nearest preceding `ldr w2,[…]` is the value load to rewrite.
|
||||
//
|
||||
// Safe for iOS 26.x too: a single display legitimately IS the primary display, so
|
||||
// forcing primary=1 matches reality; 26.x simply didn't require it.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension KernelJBPatcher {
|
||||
@discardableResult
|
||||
func patchParavirtDisplayPrimary() -> Bool {
|
||||
log("\n[JB] AppleParavirtDisplay 'primary' -> 1 (publish VM display as main; iOS 27 FBSDisplayMonitor)")
|
||||
|
||||
guard let (ks, ke) = kernTextRange else {
|
||||
log(" [-] no kernel text range")
|
||||
return false
|
||||
}
|
||||
|
||||
// ADRP+ADD refs to the exact "primary\0" cstring within kext __TEXT_EXEC.
|
||||
let refs = findStringRefs(in: (start: ks, end: ke), string: "primary")
|
||||
guard !refs.isEmpty else {
|
||||
log(" [-] no refs to \"primary\" cstring")
|
||||
return false
|
||||
}
|
||||
|
||||
var hits: [Int] = []
|
||||
for (adrpOff, addOff) in refs {
|
||||
// Confirm the setProperty(key, value, numberOfBits=32) shape: after the
|
||||
// ADD that completes the "primary" pointer, a `mov w3,#0x20` then a
|
||||
// `blraa`. Skips the sibling getProperty("primary") reference.
|
||||
var sawBits = false
|
||||
var sawCall = false
|
||||
var o = addOff + 4
|
||||
var steps = 0
|
||||
while steps < 8, o + 4 <= ke {
|
||||
guard let ins = disasAt(o) else { break }
|
||||
if ins.mnemonic == "mov",
|
||||
ins.operandString.contains("w3"),
|
||||
ins.operandString.contains("0x20") {
|
||||
sawBits = true
|
||||
}
|
||||
if ins.mnemonic == "blraa" {
|
||||
sawCall = true
|
||||
break
|
||||
}
|
||||
o += 4
|
||||
steps += 1
|
||||
}
|
||||
guard sawBits, sawCall else { continue }
|
||||
|
||||
// The setProperty value lives in w2, loaded just before the ADRP via
|
||||
// `ldr w2,[xN,#imm]` (the per-display primary flag == 0). Find it.
|
||||
var l = adrpOff - 4
|
||||
var back = 0
|
||||
while back < 12, l >= ks {
|
||||
if let ins = disasAt(l),
|
||||
ins.mnemonic == "ldr",
|
||||
ins.operandString.hasPrefix("w2,") {
|
||||
hits.append(l)
|
||||
break
|
||||
}
|
||||
l -= 4
|
||||
back += 1
|
||||
}
|
||||
}
|
||||
|
||||
guard hits.count == 1 else {
|
||||
log(" [-] paravirt-display primary value-load not found uniquely (found \(hits.count))")
|
||||
return false
|
||||
}
|
||||
|
||||
let ldrOff = hits[0]
|
||||
guard let movBytes = ARM64Encoder.encodeMovzW(rd: 2, imm16: 1) else {
|
||||
log(" [-] failed to encode mov w2,#1")
|
||||
return false
|
||||
}
|
||||
|
||||
let va = fileOffsetToVA(ldrOff)
|
||||
emit(
|
||||
ldrOff,
|
||||
movBytes,
|
||||
patchID: "paravirt_display_primary",
|
||||
virtualAddress: va,
|
||||
description: "ldr w2,[primary flag] -> mov w2,#1 [publish VM display as primary/main for iOS 27]"
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,13 @@ 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()
|
||||
|
||||
// Group B
|
||||
patchPostValidationAdditional()
|
||||
@@ -65,6 +72,30 @@ public final class KernelJBPatcher: KernelJBPatcherBase, Patcher {
|
||||
// No-op-in-effect for version-matched userlands (ip_mac_return == 0 there).
|
||||
patchExecSecurityPolicyKill()
|
||||
|
||||
// DISABLED (kept off): these three were an earlier, WRONG theory that the
|
||||
// iOS-27 black screen was an IOMFB SwapEnd size/struct ABI issue. Root
|
||||
// cause is actually the IOUC *sandbox* gate denying backboardd the
|
||||
// framebuffer — fixed by patchIoucFailedSandbox() above. Leave these off:
|
||||
// - patchParavirtDisplayPrimary sets the display's "primary" property=1,
|
||||
// which iOS 27 turns into the device NAME suffix ("primary-1"), which
|
||||
// then fails the render server's exact name match → HARMFUL.
|
||||
// - the SwapEnd patches target a present path iOS 27 never takes (method
|
||||
// 5 is never called) and would break 26.x's native 0x588 SwapEnd.
|
||||
//
|
||||
// Publish the VM's single paravirt display as primary=1 so iOS 27's
|
||||
// FBSDisplayMonitor finds a main display (else SpringBoard crash-loops on a
|
||||
// nil mainDisplay assertion → black screen). Harmless for iOS 26.x bases.
|
||||
// patchParavirtDisplayPrimary()
|
||||
|
||||
// Accept iOS 27's native 0x6e0 IOMFBSwapRec (variable-size dispatch)
|
||||
// instead of truncating userland to 0x588 (which misaligns 27's layout).
|
||||
// Paired with leaving iOS 27 userland at native size (cfw_install gate).
|
||||
// patchIomfbSwapEndVariableSize()
|
||||
// The handler has a SECOND internal exact-size gate (cmp w2,#0x588) beyond
|
||||
// the dispatch check; retarget it to iOS 27's 0x6e0 so the native struct
|
||||
// reaches the real swap processing.
|
||||
// patchIomfbSwapEndHandlerSize()
|
||||
|
||||
return patches
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user