cfw: dsc: Add FORCE_DSC_MAXSLIDE opt-in to zero maxSlide on non-27 bases

patch-dsc-maxslide self-gates to a no-op when the shared cache fits the
vphone600 26.x kernel's 6 GiB region, which 26.x/18.x bases always do, so
it only fires on 27. Add a --force flag that bypasses the fits-check and
zeroes maxSlide unconditionally (still idempotent), and a
FORCE_DSC_MAXSLIDE=1 env opt-in in cfw_install.sh that runs it on non-27
bases. Default off; 27 behavior unchanged.

Also fix the installer env-threading in cfw_install_host.sh: a
${VAR:+NAME=val} word produced by expansion is not parsed as a shell
assignment (zsh runs it as a command), so route the assignments through
env. This makes FORCE_DSC_MAXSLIDE reach the installer and repairs the
same latent bug for SPOOF_BUILD.

Verified on a 26.4 JB VM: FORCE_DSC_MAXSLIDE=1 yields on-disk maxSlide=0
and a live shared-cache slide of 0x0 (dyld maps the cache at its
preferred base 0x180000000 in rpcserver_ios), versus the nonzero slide a
stock 26.4 boot picks.

Docs + research/0_binary_patch_comparison.md updated (README and the ja/ko/zh
translations).

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
zqxwce
2026-07-23 17:46:40 +03:00
committed by zqxwce
co-authored by Claude Fable 5
parent fac586a95d
commit c9ad3c7519
9 changed files with 47 additions and 15 deletions
+1
View File
@@ -207,6 +207,7 @@ make cfw_install
# or: make cfw_install_jb # jailbreak variant # or: make cfw_install_jb # jailbreak variant
# or: make cfw_install_exp # experimental variant (JB + research stack) # or: make cfw_install_exp # experimental variant (JB + research stack)
# or: SPOOF_BUILD=23F77 make cfw_install_exp # additionally rewrite ProductBuildVersion # or: SPOOF_BUILD=23F77 make cfw_install_exp # additionally rewrite ProductBuildVersion
# or: FORCE_DSC_MAXSLIDE=1 make cfw_install # force DSC maxSlide=0 on a non-27 base (any variant; 27 does this automatically)
``` ```
## First Boot ## First Boot
+1
View File
@@ -190,6 +190,7 @@ make cfw_install
# または: make cfw_install_jb # 脱獄バリアント # または: make cfw_install_jb # 脱獄バリアント
# または: make cfw_install_exp # 実験バリアント(脱獄 + リサーチパッチスタック) # または: make cfw_install_exp # 実験バリアント(脱獄 + リサーチパッチスタック)
# または: SPOOF_BUILD=23F77 make cfw_install_exp # ProductBuildVersion も書き換え # または: SPOOF_BUILD=23F77 make cfw_install_exp # ProductBuildVersion も書き換え
# または: FORCE_DSC_MAXSLIDE=1 make cfw_install # 27 以外のベースで DSC maxSlide=0 を強制(全バリアント対応、27 は自動)
``` ```
## 初回起動 ## 初回起動
+1
View File
@@ -190,6 +190,7 @@ make cfw_install
# 또는: make cfw_install_jb # 탈옥 변형 # 또는: make cfw_install_jb # 탈옥 변형
# 또는: make cfw_install_exp # 실험 변형 (탈옥 + 연구 패치 스택) # 또는: make cfw_install_exp # 실험 변형 (탈옥 + 연구 패치 스택)
# 또는: SPOOF_BUILD=23F77 make cfw_install_exp # 추가로 ProductBuildVersion 재작성 # 또는: SPOOF_BUILD=23F77 make cfw_install_exp # 추가로 ProductBuildVersion 재작성
# 또는: FORCE_DSC_MAXSLIDE=1 make cfw_install # 27 이외 베이스에서 DSC maxSlide=0 강제 (모든 변형, 27은 자동)
``` ```
## 첫 부팅 ## 첫 부팅
+1
View File
@@ -190,6 +190,7 @@ make cfw_install
# 或:make cfw_install_jb # 越狱变体 # 或:make cfw_install_jb # 越狱变体
# 或:make cfw_install_exp # 实验变体(越狱 + 研究补丁栈) # 或:make cfw_install_exp # 实验变体(越狱 + 研究补丁栈)
# 或:SPOOF_BUILD=23F77 make cfw_install_exp # 同时改写 ProductBuildVersion # 或:SPOOF_BUILD=23F77 make cfw_install_exp # 同时改写 ProductBuildVersion
# 或:FORCE_DSC_MAXSLIDE=1 make cfw_install # 在非 27 基础版本上强制 DSC maxSlide=0(所有变体;27 会自动执行)
``` ```
## 首次启动 ## 首次启动
+2 -2
View File
@@ -184,7 +184,7 @@ 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`. | - | - | - | | 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. | - | - | - | | 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 | | 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). 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 | | 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. **Opt-in for non-27 bases** (added 2026-07-23): `FORCE_DSC_MAXSLIDE=1` in the `cfw_install.sh` environment runs `patch-dsc-maxslide --force` on 18.x/26.x too; `--force` bypasses the fits-check and zeroes `maxSlide` unconditionally (still idempotent — no-op if already 0). Default off, so non-27 bases keep their native ASLR slide unless explicitly requested; the `27.*` path is unchanged. **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 | | 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. 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 | | 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 | | 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 |
@@ -671,7 +671,7 @@ cache rebuild.
| BaseBin hook deployment (`*.dylib` -> `/mnt1/cores`) | - | - | Y (JB-3) | Y (JB-3) | | 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) | | First-boot JB finalization (`vphone_jb_setup.sh`) | - | - | Y (post-boot) | Y (post-boot) |
| 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) | | 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) | | 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, or opt in via `FORCE_DSC_MAXSLIDE=1``--force`) | 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 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) | | 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) | | `watchdogd` surgical 2-insn patch + slot reattest | - | - | - | Y (EXP-JB-3.5) |
+9
View File
@@ -354,7 +354,10 @@ esac
# that our JB code-signing environment produces. That crash-loops every daemon which # that our JB code-signing environment produces. That crash-loops every daemon which
# pins an entitlement peer-requirement (intelligencetasksd/searchpartyd/transparencyd/ # pins an entitlement peer-requirement (intelligencetasksd/searchpartyd/transparencyd/
# bluetoothd/...). Absent on 26.x/18.x libxpc (self-gating patcher no-ops there). # bluetoothd/...). Absent on 26.x/18.x libxpc (self-gating patcher no-ops there).
# FORCE_DSC_MAXSLIDE=1 (default 0): opt in to zeroing maxSlide on non-27 bases,
# whose caches fit and would otherwise self-gate to a no-op (--force bypasses that).
DSC_DIR="$MNT1/System/Cryptexes/OS/System/Library/Caches/com.apple.dyld" DSC_DIR="$MNT1/System/Cryptexes/OS/System/Library/Caches/com.apple.dyld"
FORCE_DSC_MAXSLIDE="${FORCE_DSC_MAXSLIDE:-0}"
case "$IOS_VERSION" in case "$IOS_VERSION" in
27.*) 27.*)
if [[ -d "$DSC_DIR" ]]; then if [[ -d "$DSC_DIR" ]]; then
@@ -366,6 +369,12 @@ case "$IOS_VERSION" in
"$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" patch-xpc-lwcr "$DSC_DIR" "$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" patch-xpc-lwcr "$DSC_DIR"
fi fi
;; ;;
*)
if [[ "$FORCE_DSC_MAXSLIDE" == "1" && -d "$DSC_DIR" ]]; then
echo " [*] Forcing dyld cache maxSlide=0 (opt-in FORCE_DSC_MAXSLIDE=1; base iOS ${IOS_VERSION:-unknown})..."
"$PYTHON3" "$SCRIPT_DIR/patchers/cfw.py" patch-dsc-maxslide "$DSC_DIR" --force
fi
;;
esac esac
# ═══════════ 2/7 PATCH SEPUTIL ════════════════════════════════ # ═══════════ 2/7 PATCH SEPUTIL ════════════════════════════════
+5 -2
View File
@@ -70,8 +70,11 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
echo "[*] running $INSTALLER (files placed on host mounts)..." echo "[*] running $INSTALLER (files placed on host mounts)..."
( cd "$VM_DIR" && CFW_HOST_CONTAINER="$CONT" _VPHONE_PATH="$P" \ # via env: an expansion-produced ${VAR:+NAME=val} isn't parsed as a shell assignment.
${SPOOF_BUILD:+SPOOF_BUILD="$SPOOF_BUILD"} zsh "$SCRIPT_DIR/$INSTALLER" . ) ( cd "$VM_DIR" && env CFW_HOST_CONTAINER="$CONT" _VPHONE_PATH="$P" \
${SPOOF_BUILD:+SPOOF_BUILD="$SPOOF_BUILD"} \
${FORCE_DSC_MAXSLIDE:+FORCE_DSC_MAXSLIDE="$FORCE_DSC_MAXSLIDE"} \
zsh "$SCRIPT_DIR/$INSTALLER" . )
cleanup cleanup
trap - EXIT trap - EXIT
+6 -4
View File
@@ -43,12 +43,13 @@ Commands:
Pairs with the KernelJBPatchIomfbSwap kernel patches (accept 27's 0x6e0 Pairs with the KernelJBPatchIomfbSwap kernel patches (accept 27's 0x6e0
SwapEnd struct). SwapEnd struct).
patch-dsc-maxslide <chunks_dir> [--dry-run] patch-dsc-maxslide <chunks_dir> [--dry-run] [--force]
Zero the dyld_cache_header maxSlide when the userland cache would overflow Zero the dyld_cache_header maxSlide when the userland cache would overflow
the vphone600 26.x kernel's 6 GiB shared region (cache span + maxSlide > the vphone600 26.x kernel's 6 GiB shared region (cache span + maxSlide >
0x180000000, e.g. iOS 27.0). Lets the cache map at slide 0 so launchd's dyld 0x180000000, e.g. iOS 27.0). Lets the cache map at slide 0 so launchd's dyld
can map libSystem. Self-gating (no-op if it already fits); no re-attest needed can map libSystem. Self-gating (no-op if it already fits); no re-attest needed
(header field, not a cs_validate'd code page). (header field, not a cs_validate'd code page). --force zeroes maxSlide even
when the cache fits (non-27 opt-in).
patch-lsd-embedded-reg <chunks_dir> [--dry-run] patch-lsd-embedded-reg <chunks_dir> [--dry-run]
Force lsd's -[_LSDModifyClient clientIsEntitledForEmbeddedRegistrationOperations] Force lsd's -[_LSDModifyClient clientIsEntitledForEmbeddedRegistrationOperations]
@@ -229,10 +230,11 @@ def main():
elif cmd == "patch-dsc-maxslide": elif cmd == "patch-dsc-maxslide":
if len(sys.argv) < 3: if len(sys.argv) < 3:
print("Usage: patch_cfw.py patch-dsc-maxslide <chunks_dir> [--dry-run]") print("Usage: patch_cfw.py patch-dsc-maxslide <chunks_dir> [--dry-run] [--force]")
sys.exit(1) sys.exit(1)
dry_run = "--dry-run" in sys.argv[3:] dry_run = "--dry-run" in sys.argv[3:]
patch_dsc_maxslide(sys.argv[2], dry_run=dry_run) force = "--force" in sys.argv[3:]
patch_dsc_maxslide(sys.argv[2], dry_run=dry_run, force=force)
sys.exit(0) sys.exit(0)
elif cmd == "patch-lsd-embedded-reg": elif cmd == "patch-lsd-embedded-reg":
+21 -7
View File
@@ -22,7 +22,8 @@ setup, NOT a cs_validate'd dylib code page — so, unlike cfw_patch_iomfb_swapen
page re-attestation is required (confirmed empirically: a live-poked cache with page re-attestation is required (confirmed empirically: a live-poked cache with
maxSlide=0 booted with "dyld cache mapped system-wide", 0 panics). maxSlide=0 booted with "dyld cache mapped system-wide", 0 panics).
Self-gating: no-op unless span + maxSlide overflows the region. Self-gating: no-op unless span + maxSlide overflows the region. force=True
(CLI --force) zeroes maxSlide unconditionally, for non-27 bases that opt in.
dyld_cache_header offsets (little-endian u64, stable across recent iOS): dyld_cache_header offsets (little-endian u64, stable across recent iOS):
sharedRegionStart @0xE0, sharedRegionSize @0xE8, maxSlide @0xF0 sharedRegionStart @0xE0, sharedRegionSize @0xE8, maxSlide @0xF0
@@ -42,7 +43,7 @@ OFF_MAX_SLIDE = 0xF0
KERNEL_SHARED_REGION_SIZE = 0x180000000 KERNEL_SHARED_REGION_SIZE = 0x180000000
def patch_dsc_maxslide(chunks_dir, *, kernel_region_size=KERNEL_SHARED_REGION_SIZE, dry_run=False): def patch_dsc_maxslide(chunks_dir, *, kernel_region_size=KERNEL_SHARED_REGION_SIZE, dry_run=False, force=False):
main = os.path.join(chunks_dir, MAIN_CHUNK) main = os.path.join(chunks_dir, MAIN_CHUNK)
if not os.path.isfile(main): if not os.path.isfile(main):
raise FileNotFoundError(f"main DSC chunk not found: {main}") raise FileNotFoundError(f"main DSC chunk not found: {main}")
@@ -56,17 +57,23 @@ def patch_dsc_maxslide(chunks_dir, *, kernel_region_size=KERNEL_SHARED_REGION_SI
maxslide = struct.unpack_from("<Q", hdr, OFF_MAX_SLIDE)[0] maxslide = struct.unpack_from("<Q", hdr, OFF_MAX_SLIDE)[0]
print(f" [.] {MAIN_CHUNK}: start=0x{srstart:X} size=0x{srsize:X} maxSlide=0x{maxslide:X}") print(f" [.] {MAIN_CHUNK}: start=0x{srstart:X} size=0x{srsize:X} maxSlide=0x{maxslide:X}")
if srsize + maxslide <= kernel_region_size: fits = srsize + maxslide <= kernel_region_size
if fits and not force:
print(f" [=] fits: span+maxSlide 0x{srsize + maxslide:X} <= " print(f" [=] fits: span+maxSlide 0x{srsize + maxslide:X} <= "
f"region 0x{kernel_region_size:X}; no change") f"region 0x{kernel_region_size:X}; no change")
return 0 return 0
if maxslide == 0:
print(" [=] maxSlide already 0; no change")
return 0
# Overflow: set maxSlide to 0 so the cache maps at slide 0 within the region. # Set maxSlide to 0 so the cache maps at slide 0 within the region.
new_maxslide = 0 new_maxslide = 0
reason = (f"forced: span+maxSlide 0x{srsize + maxslide:X} fits region "
f"0x{kernel_region_size:X} but --force set" if fits
else f"overflow: span+maxSlide 0x{srsize + maxslide:X} > "
f"region 0x{kernel_region_size:X}")
action = "would set" if dry_run else "set" action = "would set" if dry_run else "set"
print(f" [+] overflow: span+maxSlide 0x{srsize + maxslide:X} > " print(f" [+] {reason}; {action} maxSlide 0x{maxslide:X} -> 0x{new_maxslide:X}")
f"region 0x{kernel_region_size:X}; {action} maxSlide "
f"0x{maxslide:X} -> 0x{new_maxslide:X}")
if not dry_run: if not dry_run:
f.seek(OFF_MAX_SLIDE) f.seek(OFF_MAX_SLIDE)
f.write(struct.pack("<Q", new_maxslide)) f.write(struct.pack("<Q", new_maxslide))
@@ -107,6 +114,13 @@ def _self_test():
with open(c, "rb") as f: with open(c, "rb") as f:
f.seek(OFF_MAX_SLIDE) f.seek(OFF_MAX_SLIDE)
assert struct.unpack("<Q", f.read(8))[0] == 0x20000000 assert struct.unpack("<Q", f.read(8))[0] == 0x20000000
# fits + force: opt-in zeroes maxSlide even though it fits
assert patch_dsc_maxslide(d, force=True) == 1
with open(c, "rb") as f:
f.seek(OFF_MAX_SLIDE)
assert struct.unpack("<Q", f.read(8))[0] == 0
# force + already 0: idempotent no-op
assert patch_dsc_maxslide(d, force=True) == 0
print("self-test OK") print("self-test OK")