From decda02ab41af89e1a068eb9b1aaa0737790ceb1 Mon Sep 17 00:00:00 2001 From: zqxwce Date: Tue, 21 Jul 2026 19:44:57 +0300 Subject: [PATCH] cfw: jb/exp: Fix iOS 27 Campo crash-loop (sandbox mach-lookup exceptions) iOS 27's Campo (wallpaper renderer) runs under the temporary-sandbox profile (its own com.apple.private.sandbox.profile:embedded + no-container). On the 26.4 vphone600 kernel that builtin profile predates 27 and denies the backboard/ frontboard launch mach-services, so BKSDisplayServicesStart and then +[BKSHIDEventDeliveryManager sharedInstance] fail their mach-lookups, log "backboardd isn't running -- or we couldn't talk to it", and brk ~34ms after launch -> continuous crash-loop, no wallpaper. JB-02d's container-upcall force-success stops the exec-time autobox kill but does not grant these services. Grant them through Campo's own com.apple.security.exception.mach-lookup.global-name array (the sanctioned escape hatch, honored by temporary-sandbox -- Campo already ships ~20 such exceptions; these launch services just aren't among them because 27's profile allows them directly). Applied at host-mount build time as step JB-3b in cfw_install_jb.sh / cfw_install_exp.sh, re-signed with signcert.p12 via ldid_sign_ent. The service list is merged by the external helper scripts/patchers/campo_mach_lookup_exceptions.py (plistlib, not plutil -- the entitlement key's dots would break plutil keypaths). Hard-gated to 27.* on the mounted rootfs SystemVersion.plist (same gate as the vpregister/DSC patches): skipped on 26.x/18.x, which don't need it and where Campo.app also exists. Verified on-device (17,3_27.0_24A5390f + cloudOS 26.4, JB): Campo launches and stays up, no BKSDisplayServicesStart/BKSHIDEventDeliveryManager trap. Documented as entry #14 in research/0_binary_patch_comparison.md. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014McnrLRb5cZsTrpcBkvkj3 --- research/0_binary_patch_comparison.md | 1 + scripts/cfw_install_exp.sh | 31 ++++++++++ scripts/cfw_install_jb.sh | 31 ++++++++++ .../patchers/campo_mach_lookup_exceptions.py | 59 +++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 scripts/patchers/campo_mach_lookup_exceptions.py diff --git a/research/0_binary_patch_comparison.md b/research/0_binary_patch_comparison.md index b68fe7d..13594e1 100644 --- a/research/0_binary_patch_comparison.md +++ b/research/0_binary_patch_comparison.md @@ -187,6 +187,7 @@ do NOT execute these). | 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`, 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, ` (the conditional branch whose fall-through sets the `mov w,#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 | +| 14 | Merge backboard/frontboard launch mach-services into `com.apple.security.exception.mach-lookup.global-name` + `ldid_sign_ent` re-sign — **iOS 27.0** | `/Applications/Campo.app/Campo` | **iOS-27 Campo (wallpaper renderer) crash-loop fix — sandbox half.** Campo declares `com.apple.private.sandbox.profile:embedded = temporary-sandbox` + `no-container`, so it runs under the `temporary-sandbox` profile. On the 26.4 vphone600 kernel that builtin profile predates iOS 27 and DENIES the `mach-lookup` of the backboard/frontboard launch services, so `BKSDisplayServicesStart` (looks up `com.apple.backboard.display.services`) and then `+[BKSHIDEventDeliveryManager sharedInstance]` (HID) fail, log *"backboardd isn't running -- or we couldn't talk to it"*, and `brk #0` ~34 ms after launch → continuous crash-loop, no wallpaper (launchd eventually throttles it off). **JB-02d is NOT sufficient here:** it stops the exec-time container-manager-upcall autobox *KILL*, but Campo is still placed in `temporary-sandbox` (its own declared profile — confirmed the JB-02d `b`-flip is present in the running kernel yet Campo still traps), and that profile still denies the lookups. Fix: append the needed services to Campo's OWN `com.apple.security.exception.mach-lookup.global-name` array — the Apple-sanctioned escape hatch, which `temporary-sandbox` honors (Campo already ships ~20 such exceptions; these launch services simply aren't among them because 27's profile allows them directly). Services added (backboard names exact, from `com.apple.backboardd.plist` MachServices): `com.apple.backboard.display.services`, `com.apple.iohideventsystem`, `com.apple.CARenderServer`, `com.apple.backboard.hid.services`, `com.apple.backboard.hid-services.xpc`, `com.apple.backboard.TouchDeliveryPolicyServer`, `com.apple.backboard.system-app-server`, `com.apple.backboard.watchdog`, `com.apple.backboard.oswatchdog`, `com.apple.backboard.altsysapp`, `com.apple.AttentionAwareness`, `PurpleSystemEventPort`, `PurpleWorkspacePort`, `com.apple.frontboard.systemappservices`, `com.apple.frontboard.workspace`, `com.apple.frontboardservices.systemappmanager`, `com.apple.frontboard.watchdog`. Merge is done by the external helper `scripts/patchers/campo_mach_lookup_exceptions.py` (Python `plistlib`, not `plutil` — the entitlement key contains dots that `plutil` keypaths would mis-split; idempotent, preserves the existing array); re-signed with the JB `signcert.p12` via `ldid_sign_ent` (AMFI enforcement is relaxed on the research VM, so the re-signed cdhash loads and the entitlements are honored). Applied at host-mount build time in `cfw_install_jb.sh` / `cfw_install_exp.sh` step **[JB-3b]**, **hard-gated to `27.*`** via the mounted rootfs `SystemVersion.plist` `ProductVersion` (same gate as the vpregister/DSC patches) — skipped entirely on 26.x/18.x, which don't need it and where `Campo.app` also exists. **VALIDATED on-device (2026-07-21, `17,3_27.0_24A5390f` + cloudOS 26.4, JB): Campo launches and stays up (stable pid, no BKSDisplayServicesStart/BKSHIDEventDeliveryManager trap, no crash-loop); first-cut display-only exception advanced the trap from display→HID, confirming the mechanism, then the full service set cleared it.** See `scripts/cfw_install_jb.sh` / `scripts/cfw_install_exp.sh` step JB-3b and the merge helper `scripts/patchers/campo_mach_lookup_exceptions.py`. | - | - | Y | ### Installed Components diff --git a/scripts/cfw_install_exp.sh b/scripts/cfw_install_exp.sh index fa9c652..e6fda8d 100755 --- a/scripts/cfw_install_exp.sh +++ b/scripts/cfw_install_exp.sh @@ -446,6 +446,37 @@ cp -R "$TEMP_DIR/debugserver" "$MNT1/usr/libexec/debugserver" echo " [+] debugserver entitlements patched" +# ═══════════ JB-3b CAMPO SANDBOX FIX (iOS 27 only) ════════════ +# Grant Campo the backboard/frontboard mach-lookups the 26.4 temporary-sandbox denies (see 0_binary_patch_comparison.md #14). +# 27-gated on the mounted rootfs SystemVersion.plist: 26.x userlands don't need it and Campo.app exists there too. +CAMPO_BIN="$MNT1/Applications/Campo.app/Campo" +CAMPO_BASE_IOS=$(/usr/bin/plutil -extract ProductVersion raw -o - "$MNT1/System/Library/CoreServices/SystemVersion.plist" 2>/dev/null || true) +case "$CAMPO_BASE_IOS" in +27.*) + if [[ -f "$CAMPO_BIN" ]]; then + echo "" + echo "[JB-3b] Granting Campo backboard/frontboard mach-lookup exceptions (iOS $CAMPO_BASE_IOS)..." + cp "$CAMPO_BIN" "$TEMP_DIR/Campo" + ldid -e "$TEMP_DIR/Campo" > "$TEMP_DIR/Campo.entitlements" 2>/dev/null || true + if [[ -s "$TEMP_DIR/Campo.entitlements" ]]; then + "$PYTHON3" "$SCRIPT_DIR/patchers/campo_mach_lookup_exceptions.py" "$TEMP_DIR/Campo.entitlements" + ldid_sign_ent "$TEMP_DIR/Campo" "$TEMP_DIR/Campo.entitlements" + cp -R "$TEMP_DIR/Campo" "$CAMPO_BIN" + /bin/chmod 0755 "$CAMPO_BIN" + echo " [+] Campo re-signed with backboard/frontboard mach-lookup exceptions" + else + echo " [!] Could not read Campo entitlements; skipping Campo sandbox fix" + fi + else + echo "[JB-3b] Campo.app not present in this OS image; skipping Campo sandbox fix" + fi + ;; +*) + echo "[JB-3b] skip Campo sandbox fix (base iOS ${CAMPO_BASE_IOS:-unknown} — 27-only)" + ;; +esac + + # ═══════════ EXP-JB-3.5 PATCH watchdogd hv_vmm_present cache ══ # # Background: the kernel-side OID rename (KernelEXPPatchHvVmmRename) diff --git a/scripts/cfw_install_jb.sh b/scripts/cfw_install_jb.sh index 1e0e9ef..b6f0cfb 100755 --- a/scripts/cfw_install_jb.sh +++ b/scripts/cfw_install_jb.sh @@ -277,6 +277,37 @@ cp -R "$TEMP_DIR/debugserver" "$MNT1/usr/libexec/debugserver" echo " [+] debugserver entitlements patched" +# ═══════════ JB-3b CAMPO SANDBOX FIX (iOS 27 only) ════════════ +# Grant Campo the backboard/frontboard mach-lookups the 26.4 temporary-sandbox denies (see 0_binary_patch_comparison.md #14). +# 27-gated on the mounted rootfs SystemVersion.plist (same source as vpregister/DSC gates): 26.x userlands don't need it and Campo.app exists there too. +CAMPO_BIN="$MNT1/Applications/Campo.app/Campo" +CAMPO_BASE_IOS=$(/usr/bin/plutil -extract ProductVersion raw -o - "$MNT1/System/Library/CoreServices/SystemVersion.plist" 2>/dev/null || true) +case "$CAMPO_BASE_IOS" in +27.*) + if [[ -f "$CAMPO_BIN" ]]; then + echo "" + echo "[JB-3b] Granting Campo backboard/frontboard mach-lookup exceptions (iOS $CAMPO_BASE_IOS)..." + cp "$CAMPO_BIN" "$TEMP_DIR/Campo" + ldid -e "$TEMP_DIR/Campo" > "$TEMP_DIR/Campo.entitlements" 2>/dev/null || true + if [[ -s "$TEMP_DIR/Campo.entitlements" ]]; then + "$PYTHON3" "$SCRIPT_DIR/patchers/campo_mach_lookup_exceptions.py" "$TEMP_DIR/Campo.entitlements" + ldid_sign_ent "$TEMP_DIR/Campo" "$TEMP_DIR/Campo.entitlements" + cp -R "$TEMP_DIR/Campo" "$CAMPO_BIN" + /bin/chmod 0755 "$CAMPO_BIN" + echo " [+] Campo re-signed with backboard/frontboard mach-lookup exceptions" + else + echo " [!] Could not read Campo entitlements; skipping Campo sandbox fix" + fi + else + echo "[JB-3b] Campo.app not present in this OS image; skipping Campo sandbox fix" + fi + ;; +*) + echo "[JB-3b] skip Campo sandbox fix (base iOS ${CAMPO_BASE_IOS:-unknown} — 27-only)" + ;; +esac + + # ═══════════ JB-4 INSTALL PROCURSUS BOOTSTRAP ══════════════════ echo "" echo "[JB-4] Installing procursus bootstrap..." diff --git a/scripts/patchers/campo_mach_lookup_exceptions.py b/scripts/patchers/campo_mach_lookup_exceptions.py new file mode 100644 index 0000000..bd2053b --- /dev/null +++ b/scripts/patchers/campo_mach_lookup_exceptions.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Merge Campo's needed backboard/frontboard launch mach-services into an +entitlements plist's mach-lookup exception array, in place. See +0_binary_patch_comparison.md #14. + +Usage: campo_mach_lookup_exceptions.py +""" +import plistlib +import sys + +EXCEPTION_KEY = "com.apple.security.exception.mach-lookup.global-name" + +SERVICES = [ + "com.apple.backboard.display.services", + "com.apple.iohideventsystem", + "com.apple.CARenderServer", + "com.apple.backboard.hid.services", + "com.apple.backboard.hid-services.xpc", + "com.apple.backboard.TouchDeliveryPolicyServer", + "com.apple.backboard.system-app-server", + "com.apple.backboard.watchdog", + "com.apple.backboard.oswatchdog", + "com.apple.backboard.altsysapp", + "com.apple.AttentionAwareness", + "PurpleSystemEventPort", + "PurpleWorkspacePort", + "com.apple.frontboard.systemappservices", + "com.apple.frontboard.workspace", + "com.apple.frontboardservices.systemappmanager", + "com.apple.frontboard.watchdog", +] + + +def merge(path): + with open(path, "rb") as f: + entitlements = plistlib.load(f) + + existing = list(entitlements.get(EXCEPTION_KEY, [])) + added = [s for s in SERVICES if s not in existing] + entitlements[EXCEPTION_KEY] = existing + added + + with open(path, "wb") as f: + plistlib.dump(entitlements, f) + + print(" [+] Campo mach-lookup exception count: %d (+%d added)" + % (len(entitlements[EXCEPTION_KEY]), len(added))) + + +def main(argv): + if len(argv) != 2: + print("usage: campo_mach_lookup_exceptions.py ", + file=sys.stderr) + return 2 + merge(argv[1]) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv))