From b9b462d23fbd9bad62b5181d60f3a98c56222c8b Mon Sep 17 00:00:00 2001 From: Lakr Date: Sat, 7 Mar 2026 18:07:27 +0800 Subject: [PATCH 01/10] JB install: use external insert_dylib, preserve launchd entitlements, deploy pre-built dylibs - Replace Python cfw_inject_dylib.py with tyilo/insert_dylib (built by setup_tools) - Use --weak flag for LC_LOAD_WEAK_DYLIB injection (avoids crash on missing dylib) - Preserve original launchd entitlements on re-sign (fixes "operation not permitted") - Deploy dylibs from pre-built basebin payload instead of building from source - Remove launchdhook, systemhook, treblehook sources (no longer needed) - Print GDB debug stub port after VM starts - Cleanup: remove test scripts, rename patch comparison doc --- AGENTS.md | 2 +- Makefile | 23 +- Package.swift | 1 + README.md | 8 +- docs/README_ja.md | 2 +- docs/README_ko.md | 2 +- docs/README_zh.md | 2 +- ...riants.md => 0_binary_patch_comparison.md} | 47 +--- ...a_protection_seputil_macf_investigation.md | 2 +- ...ng_b19_mount_dounmount_strategy_compare.md | 2 +- ..._launchdhook_assertion_handoff_20260306.md | 254 ++++++++++++++++++ .../patch_hook_cred_label_update_execve.md | 2 +- scripts/cfw_install.sh | 128 +++++---- scripts/cfw_install_jb.sh | 64 +++-- scripts/fw_patch_test.py | 82 ------ scripts/jb_patch_autotest.sh | 97 ------- scripts/patchers/cfw.py | 18 +- scripts/patchers/cfw_inject_dylib.py | 241 ----------------- scripts/setup_machine.sh | 4 - scripts/setup_tools.sh | 40 ++- sources/vphone-cli/VPhoneControl.swift | 1 - sources/vphone-cli/VPhoneVirtualMachine.swift | 7 + 22 files changed, 447 insertions(+), 582 deletions(-) rename research/{00_patch_comparison_all_variants.md => 0_binary_patch_comparison.md} (74%) create mode 100644 research/boot_launchdhook_assertion_handoff_20260306.md delete mode 100644 scripts/fw_patch_test.py delete mode 100644 scripts/jb_patch_autotest.sh delete mode 100644 scripts/patchers/cfw_inject_dylib.py diff --git a/AGENTS.md b/AGENTS.md index 5b0ebe2..f6c3fd2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Virtual iPhone boot tool using Apple's Virtualization.framework with PCC researc - If blocked or waiting on user input, write the exact blocker and next action in `/TODO.md`. - If not exists, continue existing work until complete. If exists, follow `/TODO.md` instructions. -For any changes applying new patches, also update research/00_patch_comparison_all_variants.md. Dont forget this. +For any changes applying new patches, also update research/0_binary_patch_comparison.md. Dont forget this. ## Local Skills diff --git a/Makefile b/Makefile index 7cb8e3c..1821237 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,6 @@ CFW_INPUT ?= cfw_input RESTORE_UDID ?= RESTORE_ECID ?= IRECOVERY_ECID ?= -SSH_PORT ?= 2222 # ─── Build info ────────────────────────────────────────────────── GIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") @@ -48,10 +47,9 @@ help: @echo " SKIP_PROJECT_SETUP=1 Skip setup_tools/build" @echo " NONE_INTERACTIVE=1 Auto-continue prompts + boot analysis" @echo " SUDO_PASSWORD=... Preload sudo credential for setup flow" - @echo " PATCH=patch_xxx Apply single JB patch test on top of dev patch" @echo "" @echo "Setup (one-time):" - @echo " make setup_tools Install all tools (brew, trustcache, libimobiledevice, venv)" + @echo " make setup_tools Install all tools (brew, trustcache, insert_dylib, libimobiledevice, venv)" @echo "" @echo "Build:" @echo " make build Build + sign vphone-cli" @@ -70,8 +68,6 @@ help: @echo " make fw_patch Patch boot chain (6 components)" @echo " make fw_patch_dev Patch boot chain (dev mode TXM patcher)" @echo " make fw_patch_jb Run fw_patch + JB extension patches (WIP)" - @echo " make fw_patch_test PATCH=... Apply one JB kernel patch method (after fw_patch_dev)" - @echo " make jb_patch_autotest Run setup_machine per JB patch method with logs" @echo "" @echo "Restore:" @echo " make restore_get_shsh Fetch SHSH blob from device" @@ -86,7 +82,7 @@ help: @echo " make cfw_install_dev Install CFW mods via SSH (dev mode)" @echo " make cfw_install_jb Install CFW + JB extensions (jetsam/procursus/basebin)" @echo "" - @echo "Variables: VM_DIR=$(VM_DIR) CPU=$(CPU) MEMORY=$(MEMORY) DISK_SIZE=$(DISK_SIZE) SSH_PORT=$(SSH_PORT)" + @echo "Variables: VM_DIR=$(VM_DIR) CPU=$(CPU) MEMORY=$(MEMORY) DISK_SIZE=$(DISK_SIZE)" # ═══════════════════════════════════════════════════════════════════ # Setup @@ -101,7 +97,6 @@ setup_machine: fi SUDO_PASSWORD="$(SUDO_PASSWORD)" \ NONE_INTERACTIVE="$(NONE_INTERACTIVE)" \ - PATCH="$(PATCH)" \ zsh $(SCRIPTS)/setup_machine.sh \ $(if $(filter 1 true yes YES TRUE,$(JB)),--jb,) \ $(if $(filter 1 true yes YES TRUE,$(DEV)),--dev,) \ @@ -199,7 +194,7 @@ boot_dfu: build # Firmware pipeline # ═══════════════════════════════════════════════════════════════════ -.PHONY: fw_prepare fw_patch fw_patch_dev fw_patch_jb fw_patch_test jb_patch_autotest +.PHONY: fw_prepare fw_patch fw_patch_dev fw_patch_jb fw_prepare: cd $(VM_DIR) && bash "$(CURDIR)/$(SCRIPTS)/fw_prepare.sh" @@ -213,12 +208,6 @@ fw_patch_dev: fw_patch_jb: cd $(VM_DIR) && $(PYTHON) "$(CURDIR)/$(SCRIPTS)/fw_patch_jb.py" . -fw_patch_test: - cd $(VM_DIR) && PATCH="$(PATCH)" $(PYTHON) "$(CURDIR)/$(SCRIPTS)/fw_patch_test.py" . - -jb_patch_autotest: - zsh "$(CURDIR)/$(SCRIPTS)/jb_patch_autotest.sh" - # ═══════════════════════════════════════════════════════════════════ # Restore # ═══════════════════════════════════════════════════════════════════ @@ -257,10 +246,10 @@ ramdisk_send: .PHONY: cfw_install cfw_install_dev cfw_install_jb cfw_install: - cd $(VM_DIR) && SSH_PORT="$(SSH_PORT)" zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" . + cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" . cfw_install_dev: - cd $(VM_DIR) && SSH_PORT="$(SSH_PORT)" zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_dev.sh" . + cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_dev.sh" . cfw_install_jb: - cd $(VM_DIR) && SSH_PORT="$(SSH_PORT)" zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb.sh" . + cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb.sh" . diff --git a/Package.swift b/Package.swift index 6f42dec..80d3c11 100644 --- a/Package.swift +++ b/Package.swift @@ -7,6 +7,7 @@ let package = Package( platforms: [ .macOS(.v15), ], + products: [], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.1"), .package(url: "https://github.com/mhdhejazi/Dynamic", from: "1.2.0"), diff --git a/README.md b/README.md index 017b9dd..ec15b0c 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Three patch variants are available with increasing levels of security bypass: `66` = default JB kernel method plan; `78` = default + optional kernel methods (`VPHONE_JB_ENABLE_OPTIONAL=1`). -See [research/00_patch_comparison_all_variants.md](./research/00_patch_comparison_all_variants.md) for the detailed per-component breakdown. +See [research/0_binary_patch_comparison.md](./research/0_binary_patch_comparison.md) for the detailed per-component breakdown. ## Prerequisites @@ -85,21 +85,19 @@ git clone --recurse-submodules https://github.com/Lakr233/vphone-cli.git ```bash make setup_machine # full automation through "First Boot" (includes restore/ramdisk/CFW) -# options: NONE_INTERACTIVE=1 SUDO_PASSWORD=... PATCH=patch_xxx +# options: NONE_INTERACTIVE=1 SUDO_PASSWORD=... ``` ## Manual Setup ```bash -make setup_tools # install brew deps, build trustcache + libimobiledevice, create Python venv +make setup_tools # install brew deps, build trustcache, clone insert_dylib, build libimobiledevice, create Python venv make build # build + sign vphone-cli make vm_new # create vm/ directory (ROMs, disk, SEP storage) make fw_prepare # download IPSWs, extract, merge, generate manifest make fw_patch # patch boot chain (regular variant) # or: make fw_patch_dev # dev variant (+ TXM entitlement/debug bypasses) # or: make fw_patch_jb # jailbreak variant (+ full security bypass) (WIP) -# or: make fw_patch_test PATCH=patch_xxx # one JB kernel method on top of dev patch -# or: make jb_patch_autotest # test all JB methods (single-thread, full setup flow) ``` ## Restore diff --git a/docs/README_ja.md b/docs/README_ja.md index 8b04137..22ebbcb 100644 --- a/docs/README_ja.md +++ b/docs/README_ja.md @@ -26,7 +26,7 @@ Apple の Virtualization.framework と PCC の研究用 VM インフラを使用 `66` は JB のデフォルトカーネルパッチ計画、`78` はデフォルト + オプションカーネルパッチ(`VPHONE_JB_ENABLE_OPTIONAL=1`)です。 -詳細なコンポーネントごとの内訳については [research/00_patch_comparison_all_variants.md](../research/00_patch_comparison_all_variants.md) を参照してください。 +詳細なコンポーネントごとの内訳については [research/0_binary_patch_comparison.md](../research/0_binary_patch_comparison.md) を参照してください。 ## 前提条件 diff --git a/docs/README_ko.md b/docs/README_ko.md index c16fb3a..45e039d 100644 --- a/docs/README_ko.md +++ b/docs/README_ko.md @@ -26,7 +26,7 @@ PCC 리서치 VM 인프라와 Apple의 Virtualization.framework를 사용하여 `66`은 JB 기본 커널 패치 플랜, `78`은 기본 + 선택 커널 패치(`VPHONE_JB_ENABLE_OPTIONAL=1`)입니다. -컴포넌트별 상세 분류는 [research/00_patch_comparison_all_variants.md](../research/00_patch_comparison_all_variants.md)를 참조하세요. +컴포넌트별 상세 분류는 [research/0_binary_patch_comparison.md](../research/0_binary_patch_comparison.md)를 참조하세요. ## 사전 요구 사항 diff --git a/docs/README_zh.md b/docs/README_zh.md index 013dc56..b869839 100644 --- a/docs/README_zh.md +++ b/docs/README_zh.md @@ -26,7 +26,7 @@ `66` 表示 JB 默认内核补丁计划;`78` 表示默认 + 可选内核补丁(`VPHONE_JB_ENABLE_OPTIONAL=1`)。 -详见 [research/00_patch_comparison_all_variants.md](../research/00_patch_comparison_all_variants.md) 了解各组件的详细分项对比。 +详见 [research/0_binary_patch_comparison.md](../research/0_binary_patch_comparison.md) 了解各组件的详细分项对比。 ## 先决条件 diff --git a/research/00_patch_comparison_all_variants.md b/research/0_binary_patch_comparison.md similarity index 74% rename from research/00_patch_comparison_all_variants.md rename to research/0_binary_patch_comparison.md index 9e512bc..4115a3e 100644 --- a/research/00_patch_comparison_all_variants.md +++ b/research/0_binary_patch_comparison.md @@ -74,8 +74,6 @@ ### JB-Only Kernel Methods (Reference List) -Current default schedule note (2026-03-06): `patch_cred_label_update_execve` remains temporarily excluded from `_PATCH_METHODS` pending staged re-validation. `patch_syscallmask_apply_to_proc` has been rebuilt around the real syscallmask apply wrapper and is re-enabled after focused PCC 26.1 dry-run validation plus user-side boot confirmation; refreshed XNU/IDA review also confirms historical C22 was the all-ones-mask variant, not a `NULL`-mask install. `patch_hook_cred_label_update_execve` has also been rebuilt as a faithful upstream C23 wrapper trampoline: it retargets sandbox `mac_policy_ops[18]` to a cave that copies `VSUID`/`VSGID` owner state into the pending credential, sets `P_SUGID`, and branches back to the original wrapper. `patch_iouc_failed_macf` has been rebuilt as a narrow branch-level gate patch: the old repo-only entry early-return on `0xFFFFFE000825B0C0` was discarded, and A5-v2 now patches the post-`mac_iokit_check_open` `CBZ W0, allow` gate at `0xFFFFFE000825BA98` to unconditional allow while preserving the surrounding IOUserClient setup flow. `patch_vm_fault_enter_prepare` was retargeted to the upstream PCC 26.1 research `cs_bypass` gate and re-enabled for dry-run validation. `patch_bsd_init_auth` has been retargeted to the real `_bsd_init` rootauth failure branch and re-enabled for staged validation. Fresh IDA re-analysis shows JB-14 previously used a false-positive matcher; it now targets the real `_bsd_init` rootauth failure branch using in-function Capstone-decoded control-flow semantics and is semantically redundant with base patch #3 when JB is layered on top of `fw_patch`. For JB-16, the historical hit at `0xFFFFFE000836E1F0` is now treated as semantically wrong: it patches the `"SecureRoot"` name-check gate inside `AppleARMPE::callPlatformFunction`, not the `"SecureRootName"` deny return consumed by `IOSecureBSDRoot()`. The implementation was retargeted on 2026-03-06 to `0xFFFFFE000836E464` (`CSEL W22, WZR, W9, NE -> MOV W22, #0`) and re-enabled in `KernelJBPatcher._GROUP_B_METHODS` pending restore/boot validation. - | # | Group | Method | Function | Purpose | JB Enabled | | ----- | ----- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------: | | JB-01 | A | `patch_amfi_cdhash_in_trustcache` | `AMFIIsCDHashInTrustCache` | Always return true + store hash | Y | @@ -104,12 +102,6 @@ Current default schedule note (2026-03-06): `patch_cred_label_update_execve` rem | JB-24 | B | `patch_vm_fault_enter_prepare` | `_vm_fault_enter_prepare` | Force `cs_bypass` fast path in runtime fault validation | Y | | JB-25 | B | `patch_vm_map_protect` | `_vm_map_protect` | Skip upstream write-downgrade gate in `vm_map_protect` | Y | -JB rework note (2026-03-06, remaining active methods): `JB-01`, `JB-08`, `JB-09`, `JB-06`, `JB-11`, `JB-12`, `JB-13`, `JB-17`, `JB-19`, and `JB-23` have now also been rechecked against `/Users/qaq/Desktop/patch_fw.py`, IDA PCC 26.1 research, `research/reference/xnu`, and focused dry-runs on both PCC 26.1 research/release. Of these, `JB-09` was materially pulled back to the upstream `mac_policy_ops` table-entry rewrite model (common allow stub retarget, matching `patch_fw.py` offsets) instead of per-hook body stubs; `JB-06` dropped its broad AMFI-text fallback; `JB-12` tightened to the exact early `ldr/cbz/bl/cbz` guard pair; and `JB-19` now requires a unique `krn.`-anchored verifyPermission gate across all string refs. The remaining six (`JB-01`, `JB-08`, `JB-11`, `JB-13`, `JB-17`, `JB-23`) matched upstream offsets and semantics without further retarget. - -JB retarget note (2026-03-06): `JB-15`, `JB-18`, `JB-20`, `JB-21`, `JB-22`, and `JB-25` were rechecked against `/Users/qaq/Desktop/patch_fw.py`, IDA PCC 26.1 research, and `research/reference/xnu`. Current preferred runtime behavior is to match the known-good upstream semantic gate unless binary+source evidence clearly disproves it. In this pass, `JB-22` was pulled back from a helper-return rewrite to the upstream early `pid == 0` gate, and `JB-20` was pulled back from the later preboot-fallback compare to the upstream first root-mount compare. - -JB-24 note (2026-03-06): the old derived matcher hit the `VM_PAGE_CONSUME_CLUSTERED()` lock/unlock sequence inside `vm_fault_enter_prepare`, i.e. `pmap_lock_phys_page()` / `pmap_unlock_phys_page()`. The implementation is now retargeted to the upstream PCC 26.1 research `cs_bypass` gate at `0x00BA9E1C` / `0xFFFFFE0007BADE1C`. - ## CFW Installation Patches ### Binary Patches Applied Over SSH Ramdisk @@ -145,6 +137,7 @@ JB-24 note (2026-03-06): the old derived matcher hit the `VM_PAGE_CONSUME_CLUSTE | SSH readiness wait before install | Y (`wait_for_device_ssh_ready`) | - | Y (inherited from base run) | | launchd jetsam patch (`patch-launchd-jetsam`) | - | Y (base-flow injection) | Y (JB-1) | | launchd dylib injection (`inject-dylib /b`) | - | - | Y (JB-1) | + | Procursus bootstrap deployment | - | - | Y (JB-2) | | BaseBin hook deployment (`*.dylib` -> `/mnt1/cores`) | - | - | Y (JB-3) | | Additional input resources | `cfw_input` | `cfw_input` + `resources/cfw_dev/rpcserver_ios` | `cfw_input` + `cfw_jb_input` | @@ -184,41 +177,3 @@ JB-24 note (2026-03-06): the old derived matcher hit the `VM_PAGE_CONSUME_CLUSTE | iOS 26.1 (`23B85`) | 14 | 59 | | iOS 26.3 (`23D127`) | 14 | 59 | -## Automation Notes (2026-03-06) - -- `scripts/setup_machine.sh` non-interactive flow fix: renamed local variable `status` to `boot_state` in first-boot log wait and boot-analysis wait helpers to avoid zsh `status` read-only special parameter collision. -- `scripts/setup_machine.sh` non-interactive first-boot wait fix: replaced `(( waited++ ))` with `(( ++waited ))` in `monitor_boot_log_until` to avoid `set -e` abort when arithmetic expression evaluates to `0`. -- `scripts/jb_patch_autotest.sh` loop fix for sweep stability under `set -e`: replaced `((idx++))` with `(( ++idx ))`. -- `scripts/jb_patch_autotest.sh` zsh compatibility fix: renamed per-case result variable `status` to `case_status` to avoid `status` read-only special parameter collision. -- `scripts/jb_patch_autotest.sh` selection logic update: - - default run now excludes methods listed in `KernelJBPatcher._DEV_SINGLE_WORKING_METHODS` (pending-only sweep). - - set `JB_AUTOTEST_INCLUDE_WORKING=1` to include already-working methods and run the full list. -- Sweep run record: - - `setup_logs/jb_patch_tests_20260306_114417` (2026-03-06): aborted at `[1/20]` with `read-only variable: status` in `jb_patch_autotest.sh`. - - `setup_logs/jb_patch_tests_20260306_115027` (2026-03-06): rerun after `status` fix, pending-only mode (`Total methods: 19`). -- Final run result from `jb_patch_tests_20260306_115027` at `2026-03-06 13:17`: - - Finished: 19/19 (`PASS=15`, `FAIL=4`, all fails `rc=2`). - - Failing methods at that time: `patch_bsd_init_auth`, `patch_io_secure_bsd_root`, `patch_vm_fault_enter_prepare`, `patch_cred_label_update_execve`. - - 2026-03-06 follow-up: `patch_io_secure_bsd_root` failure is now attributed to a wrong-site patch in `AppleARMPE::callPlatformFunction` (`"SecureRoot"` gate at `0xFFFFFE000836E1F0`), not the intended `"SecureRootName"` deny-return path. The code was retargeted the same day to `0xFFFFFE000836E464` and re-enabled for the next restore/boot check. - - 2026-03-06 follow-up: `patch_bsd_init_auth` was retargeted after confirming the old matcher was hitting unrelated code; keep disabled in default schedule until a fresh clean-baseline boot test passes. - - Final case: `[19/19] patch_syscallmask_apply_to_proc` (`PASS`). - - 2026-03-06 re-analysis: that historical `PASS` is now treated as a false positive for functionality, because the recorded bytes landed at `0xfffffe00093ae6e4`/`0xfffffe00093ae6e8` inside `_profile_syscallmask_destroy` underflow handling, not in `_proc_apply_syscall_masks`. - - 2026-03-06 code update: `scripts/patchers/kernel_jb_patch_syscallmask.py` was rebuilt to target the real syscallmask apply wrapper structurally and now dry-runs on `PCC-CloudOS-26.1-23B85 kernelcache.research.vphone600` with 3 writes: `0x02395530`, `0x023955E8`, and cave `0x00AB1720`. User-side boot validation succeeded the same day. -- 2026-03-06 follow-up: `patch_kcall10` was rebuilt from the old ABI-unsafe pseudo-10-arg design into an ABI-correct `sysent[439]` cave. Focused dry-run on `PCC-CloudOS-26.1-23B85 kernelcache.research.vphone600` now emits 4 writes: cave `0x00AB1720`, `sy_call` `0x0073E180`, `sy_arg_munge32` `0x0073E188`, and metadata `0x0073E190`; the method was re-enabled in `_GROUP_C_METHODS`. - - Observed failure symptom in current failing set: first boot panic before command injection (or boot process early exit). -- Post-run schedule change (per user request): - - commented out failing methods from default `KernelJBPatcher._PATCH_METHODS` schedule in `scripts/patchers/kernel_jb.py`: - - `patch_bsd_init_auth` - - `patch_io_secure_bsd_root` - - `patch_vm_fault_enter_prepare` - - `patch_cred_label_update_execve` -- 2026-03-06 re-research note for `patch_cred_label_update_execve`: - - old entry-time early-return strategy was identified as boot-unsafe because it skipped AMFI exec-time `csflags` and entitlement propagation entirely. - - implementation was reworked to a success-tail trampoline that preserves normal AMFI processing and only clears restrictive `csflags` bits on the success path. - - default JB schedule still keeps the method disabled until the reworked strategy is boot-validated. -- Manual DEV+single (`setup_machine` + `PATCH=`) working set now includes: - - `patch_amfi_cdhash_in_trustcache` - - `patch_amfi_execve_kill_path` - - `patch_task_conversion_eval_internal` - - `patch_sandbox_hooks_extended` - - `patch_post_validation_additional` diff --git a/research/boot_data_protection_seputil_macf_investigation.md b/research/boot_data_protection_seputil_macf_investigation.md index 973aa2e..0917a6a 100644 --- a/research/boot_data_protection_seputil_macf_investigation.md +++ b/research/boot_data_protection_seputil_macf_investigation.md @@ -51,7 +51,7 @@ Patch action per entry remains: Updated: - `research/kernel_patch_jb/patch_sandbox_hooks_extended.md` -- `research/00_patch_comparison_all_variants.md` +- `research/0_binary_patch_comparison.md` ## Local Validation (static) diff --git a/research/boot_hang_b19_mount_dounmount_strategy_compare.md b/research/boot_hang_b19_mount_dounmount_strategy_compare.md index 17e778b..5875092 100644 --- a/research/boot_hang_b19_mount_dounmount_strategy_compare.md +++ b/research/boot_hang_b19_mount_dounmount_strategy_compare.md @@ -262,7 +262,7 @@ The triage results from those knobs are preserved in this document and in: - `vm/ab_matrix_b19_mnt_20260305_034127.csv` - `TODO.md` (Boot Hang Research + Progress Update sections) -- `research/00_patch_comparison_all_variants.md` (Kernelcache section) +- `research/0_binary_patch_comparison.md` (Kernelcache section) --- diff --git a/research/boot_launchdhook_assertion_handoff_20260306.md b/research/boot_launchdhook_assertion_handoff_20260306.md new file mode 100644 index 0000000..6411b6b --- /dev/null +++ b/research/boot_launchdhook_assertion_handoff_20260306.md @@ -0,0 +1,254 @@ +# Launchdhook Assertion Handoff (2026-03-06) + +## Scope + +This note captures the current userspace-side findings for the failing `fw_patch_jb + cfw_install_jb` path. +It is intended as a handoff artifact for follow-up work on the `fix-boot` branch. + +The current symptom is no longer "launchd does not start". +The updated symptom is: + +- `launchd` starts +- injected `launchdhook.dylib` definitely loads +- `launchd` then hits an early internal assertion before the expected `bash` / follow-on job chain stabilizes + +## Executive Summary + +### Confirmed + +- The original JB `LC_LOAD_DYLIB /cores/launchdhook.dylib` approach is structurally unsafe on the current `launchd` sample because there is not enough load-command slack. +- A short-path alias experiment fixed the Mach-O header-space problem: + - `/cores/launchdhook.dylib` requires 56 bytes and overruns into `__TEXT,__text` + - `/cores/b` still requires 40 bytes and also overruns + - `/b` requires 32 bytes and fits exactly after removing `LC_CODE_SIGNATURE` +- Runtime test with `/b` proves the short-path alias loads successfully, but the main failure remains: + - `launchdhook.dylib` prints its startup logs + - `launchd` then asserts early: `launchd + 59944 ... 0xffffffffffffffff` + +### Current conclusion + +The short-path `/b` alias fixes the **injection-space** problem, but does **not** fix the **launchd assertion**. +So the remaining problem is now more likely in the hook logic (especially early XPC / daemon config hooks) than in the raw load-command insertion path. + +## Evidence Collected + +### 1. Mach-O injection space audit + +Local dry-run against `vm/.cfw_temp/launchd` established the following: + +- Existing load-command slack before the first section: 16 bytes +- After stripping `LC_CODE_SIGNATURE`: 32 bytes +- Required command sizes: + - `/cores/launchdhook.dylib` -> 56 bytes + - `/cores/b` -> 40 bytes + - `/b` -> 32 bytes + +Observed effect of the original long-path injection: + +- `LC_LOAD_DYLIB /cores/launchdhook.dylib` overwrote the beginning of `__TEXT,__text` +- first instructions at the start of the text section were replaced by injected path bytes + +Observed effect of the short-path injection: + +- `LC_LOAD_DYLIB /b` fits exactly in the available 32 bytes after `LC_CODE_SIGNATURE` removal +- no additional overwrite into `__TEXT,__text` is needed for that path + +### 2. Device-side mount and payload verification + +Inside ramdisk shell, manual mount and inspection showed: + +- `/dev/disk1s1` mounted at `/mnt1` +- `/dev/disk1s5` mounted at `/mnt5` +- `/mnt1/b` exists and is a Mach-O dylib +- `/mnt1/cores/launchdhook.dylib` exists and is a Mach-O dylib +- `/mnt1/cores/systemhook.dylib` and `/mnt1/cores/libellekit.dylib` are also present + +Important clarification: + +- `/.b` is an existing hidden root directory on this filesystem and is unrelated to the alias experiment +- the experiment path is `/b`, not `/.b` + +### 3. Runtime serial log after switching to `/b` + +The following lines appeared during boot: + +- `set JB_ROOT_PATH = /private/preboot//jb-vphone/procursus` +- `=========== hello from launchdhook.dylib ===========` +- `=========== bye from launchdhook.dylib ===========` +- `com.apple.xpc.launchd ... assertion failed: ... launchd + 59944 ... 0xffffffffffffffff` + +Interpretation: + +- `/b` injection is working +- `launchdhook.dylib` is loaded and runs its initializer path +- the failure is no longer attributable to the long path not loading or to the Mach-O injection missing outright + +## Source-Backed Analysis from Dopamine BaseBin + +Source tree used: + +- `/Users/qaq/Documents/GitHub/Dopamine/BaseBin` + +### 1. launchdhook initialization order + +From `Dopamine/BaseBin/launchdhook/src/main.m`, the constructor initializes hooks in this order: + +1. `initXPCHooks();` +2. `initDaemonHooks();` +3. `initSpawnHooks();` +4. `initIPCHooks();` +5. `initJetsamHook();` + +This matters because the current assertion happens very early, after `launchdhook` has definitely run. +That makes the earlier hooks higher-priority suspects than spawn-time behavior. + +### 2. What `initDaemonHooks()` actually does + +From `Dopamine/BaseBin/launchdhook/src/daemon_hook.m`: + +- hooks `xpc_dictionary_get_value` +- rewrites behavior for these keys: + - `LaunchDaemons` + - `Paths` + - `com.apple.private.xpc.launchd.userspace-reboot` + +Behavior summary: + +- appends jailbreak daemon plist entries from: + - `JBROOT_PATH("/basebin/LaunchDaemons")` + - `JBROOT_PATH("/Library/LaunchDaemons")` +- appends those same directories to `Paths` +- conditionally returns `com.apple.private.iowatchdog.user-access` when `userspace-reboot` is false/missing + +This hook touches exactly the kind of launchd configuration objects that are consulted during early daemon/bootstrap setup. + +### 3. What `initSpawnHooks()` actually does + +From `Dopamine/BaseBin/launchdhook/src/spawn_hook.c`: + +- hooks `__posix_spawn` +- during early boot, it intentionally avoids broad injection until `xpcproxy` appears +- once `xpcproxy` is seen, it flips out of early-boot mode and uses `posix_spawn_hook_shared(...)` + +Interpretation: + +- spawn hook is real, but it is comparatively later than the daemon config hook +- given the current assertion timing, `initSpawnHooks()` is no longer the top suspect + +### 4. What `initXPCHooks()` does + +From `Dopamine/BaseBin/launchdhook/src/xpc_hook.c`: + +- hooks `xpc_receive_mach_msg` +- participates in jbserver message handling and filtering inside launchd/XPC path + +This is also an early-launchd hook and remains a second-tier suspect if daemon-hook isolation does not clear the assertion. + +### 5. Runtime jetsam hook vs our static jetsam patch + +From `Dopamine/BaseBin/launchdhook/src/jetsam_hook.c`: + +- Dopamine also installs a runtime hook on `memorystatus_control` +- this is separate from the repo's static `scripts/patchers/cfw_patch_jetsam.py` binary patch + +Therefore two different "jetsam" mechanisms now exist in the failing path: + +- static launchd branch patch +- runtime `memorystatus_control` hook + +This does not prove either is the current cause, but it means the term "jetsam patch" must be disambiguated in future debugging. + +## Current Suspect Ranking + +### Highest probability + +1. **`initDaemonHooks()` / `daemon_hook.m`** + - hooks `xpc_dictionary_get_value` + - mutates `LaunchDaemons` and `Paths` + - timing matches the observed early `launchd` assertion better than spawn-time logic + +### Medium probability + +2. **`initXPCHooks()` / `xpc_hook.c`** + - also runs before spawn hook + - directly changes launchd/XPC message handling + +3. **static `patch-launchd-jetsam` matcher** + - still considered risky because its matching strategy is heuristic and not CFG-constrained + - but the `/b` experiment shows the assertion survives after fixing the obvious load-command overflow issue + +### Lower probability for the current symptom timing + +4. **`initSpawnHooks()` / `spawn_hook.c`** + - still relevant for later `bash` / job launch failures + - but no longer the best first suspect for the early `launchd + 59944` assertion + +## Recommended Isolation Order for `fix-boot` + +### Stage 1: no-daemon-hook control + +Goal: + +- keep `launchdhook.dylib` loading +- keep `/b` short-path alias experiment in place +- disable only `initDaemonHooks()` + +Reason: + +- this is the cleanest test of the current top suspect +- if the assertion disappears, the root issue is inside `daemon_hook.m` + +### Stage 2: no-xpc-hook control + +If stage 1 still asserts: + +- restore daemon hook or keep it off, but disable `initXPCHooks()` next +- test whether the assertion is tied to XPC receive hook path instead + +### Stage 3: no-spawn-hook control + +Only after stages 1 and 2: + +- disable `initSpawnHooks()` +- use this to isolate later `bash` / child-process failures if the launchd assertion is already gone or moves later + +### Stage 4: revisit static launchd jetsam patch + +If all runtime-hook controls still fail: + +- re-audit `scripts/patchers/cfw_patch_jetsam.py` +- prefer a source-backed or CFG-backed site selection instead of the current backward-scan heuristic + +## Concrete Handoff Notes for Claude + +### Facts + +- `/b` injection is confirmed working on-device +- `launchdhook.dylib` definitely runs +- launchd still asserts at `launchd + 59944` +- Dopamine source confirms `initDaemonHooks()` runs before `initSpawnHooks()` + +### Inference + +- the early assertion is more likely to be caused by `daemon_hook.m` or `xpc_hook.c` than by `spawn_hook.c` + +### Best next change + +Implement a **minimal no-daemon-hook build** first: + +- edit `Dopamine/BaseBin/launchdhook/src/main.m` +- temporarily disable only `initDaemonHooks();` +- rebuild `launchdhook.dylib` +- keep `/b` alias loading strategy unchanged for the control run + +## Related Files + +- `scripts/cfw_install_jb.sh` +- `scripts/patchers/cfw_inject_dylib.py` +- `scripts/patchers/cfw_patch_jetsam.py` +- `research/boot_jb_mount_failure_investigation.md` +- `research/boot_hang_b19_mount_dounmount_strategy_compare.md` +- `Dopamine/BaseBin/launchdhook/src/main.m` +- `Dopamine/BaseBin/launchdhook/src/daemon_hook.m` +- `Dopamine/BaseBin/launchdhook/src/spawn_hook.c` +- `Dopamine/BaseBin/launchdhook/src/xpc_hook.c` diff --git a/research/kernel_patch_jb/patch_hook_cred_label_update_execve.md b/research/kernel_patch_jb/patch_hook_cred_label_update_execve.md index 9803c1b..1b2facc 100644 --- a/research/kernel_patch_jb/patch_hook_cred_label_update_execve.md +++ b/research/kernel_patch_jb/patch_hook_cred_label_update_execve.md @@ -132,7 +132,7 @@ Observed output: - `scripts/patchers/kernel_jb_patch_hook_cred_label.py` now implements faithful upstream C23 semantics - `scripts/patchers/kernel_jb.py` includes `patch_hook_cred_label_update_execve` in the active Group C schedule -- `research/00_patch_comparison_all_variants.md` should describe C23 as a faithful wrapper trampoline, not as a mis-targeted early-return patch +- `research/0_binary_patch_comparison.md` should describe C23 as a faithful wrapper trampoline, not as a mis-targeted early-return patch ## Practical Effect diff --git a/scripts/cfw_install.sh b/scripts/cfw_install.sh index 7087257..311b06e 100755 --- a/scripts/cfw_install.sh +++ b/scripts/cfw_install.sh @@ -198,8 +198,8 @@ check_prereqs() { # ── Cleanup trap (unmount DMGs on error) ─────────────────────── cleanup_on_exit() { - safe_detach "$TEMP_DIR/mnt_sysos" - safe_detach "$TEMP_DIR/mnt_appos" + safe_detach "$TEMP_DIR/mnt_sysos" 2>/dev/null || true + safe_detach "$TEMP_DIR/mnt_appos" 2>/dev/null || true } trap cleanup_on_exit EXIT @@ -234,42 +234,7 @@ echo " AppOS: $CRYPTEX_APPOS" echo "" echo "[1/7] Installing Cryptex (SystemOS + AppOS)..." -SYSOS_DMG="$TEMP_DIR/CryptexSystemOS.dmg" -APPOS_DMG="$TEMP_DIR/CryptexAppOS.dmg" -MNT_SYSOS="$TEMP_DIR/mnt_sysos" -MNT_APPOS="$TEMP_DIR/mnt_appos" - -# Decrypt SystemOS AEA (cached — skip if already decrypted) -if [[ ! -f "$SYSOS_DMG" ]]; then - echo " Extracting AEA key..." - AEA_KEY=$(ipsw fw aea --key "$RESTORE_DIR/$CRYPTEX_SYSOS") - echo " key: $AEA_KEY" - echo " Decrypting SystemOS..." - aea decrypt -i "$RESTORE_DIR/$CRYPTEX_SYSOS" -o "$SYSOS_DMG" -key-value "$AEA_KEY" -else - echo " Using cached SystemOS DMG" -fi - -# Copy AppOS (unencrypted, cached) -if [[ ! -f "$APPOS_DMG" ]]; then - cp "$RESTORE_DIR/$CRYPTEX_APPOS" "$APPOS_DMG" -else - echo " Using cached AppOS DMG" -fi - -# Detach any leftover mounts from previous runs -safe_detach "$MNT_SYSOS" -safe_detach "$MNT_APPOS" -mkdir -p "$MNT_SYSOS" "$MNT_APPOS" -assert_mount_under_vm "$MNT_SYSOS" "SystemOS mountpoint" -assert_mount_under_vm "$MNT_APPOS" "AppOS mountpoint" - -echo " Mounting SystemOS..." -sudo hdiutil attach -mountpoint "$MNT_SYSOS" "$SYSOS_DMG" -nobrowse -owners off -echo " Mounting AppOS..." -sudo hdiutil attach -mountpoint "$MNT_APPOS" "$APPOS_DMG" -nobrowse -owners off - -# Mount device rootfs (tolerate already-mounted) +# Mount device rootfs first to check existing state echo " Mounting device rootfs rw..." remote_mount /dev/disk1s1 /mnt1 @@ -296,28 +261,79 @@ else fi fi -ssh_cmd "/bin/rm -rf /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS" -ssh_cmd "/bin/mkdir -p /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS" -ssh_cmd "/bin/chmod 0755 /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS" +# Check if Cryptexes already exist on device (skip the slow copy if so) +CRYPTEX_OS_COUNT=$(ssh_cmd "/bin/ls /mnt1/System/Cryptexes/OS/ 2>/dev/null | /usr/bin/wc -l" | tr -d ' ') +CRYPTEX_APP_COUNT=$(ssh_cmd "/bin/ls /mnt1/System/Cryptexes/App/ 2>/dev/null | /usr/bin/wc -l" | tr -d ' ') -# Copy Cryptex files to device -echo " Copying Cryptexes to device (this takes ~3 minutes)..." -scp_to "$MNT_SYSOS/." "/mnt1/System/Cryptexes/OS" -scp_to "$MNT_APPOS/." "/mnt1/System/Cryptexes/App" +if [[ "${CRYPTEX_OS_COUNT:-0}" -gt 0 && "${CRYPTEX_APP_COUNT:-0}" -gt 0 ]]; then + echo " [*] Cryptexes already installed on device (OS=${CRYPTEX_OS_COUNT} entries, App=${CRYPTEX_APP_COUNT} entries), skipping" -# Create dyld symlinks (ln -sf is idempotent) -echo " Creating dyld symlinks..." -ssh_cmd "/bin/ln -sf ../../../System/Cryptexes/OS/System/Library/Caches/com.apple.dyld \ - /mnt1/System/Library/Caches/com.apple.dyld" -ssh_cmd "/bin/ln -sf ../../../../System/Cryptexes/OS/System/DriverKit/System/Library/dyld \ - /mnt1/System/DriverKit/System/Library/dyld" + # Still ensure dyld symlinks exist + ssh_cmd "/bin/ln -sf ../../../System/Cryptexes/OS/System/Library/Caches/com.apple.dyld \ + /mnt1/System/Library/Caches/com.apple.dyld" + ssh_cmd "/bin/ln -sf ../../../../System/Cryptexes/OS/System/DriverKit/System/Library/dyld \ + /mnt1/System/DriverKit/System/Library/dyld" -# Unmount Cryptex DMGs -echo " Unmounting Cryptex DMGs..." -safe_detach "$MNT_SYSOS" -safe_detach "$MNT_APPOS" + echo " [+] Cryptex skipped (already present)" +else + SYSOS_DMG="$TEMP_DIR/CryptexSystemOS.dmg" + APPOS_DMG="$TEMP_DIR/CryptexAppOS.dmg" + MNT_SYSOS="$TEMP_DIR/mnt_sysos" + MNT_APPOS="$TEMP_DIR/mnt_appos" -echo " [+] Cryptex installed" + # Decrypt SystemOS AEA (cached — skip if already decrypted) + if [[ ! -f "$SYSOS_DMG" ]]; then + echo " Extracting AEA key..." + AEA_KEY=$(ipsw fw aea --key "$RESTORE_DIR/$CRYPTEX_SYSOS") + echo " key: $AEA_KEY" + echo " Decrypting SystemOS..." + aea decrypt -i "$RESTORE_DIR/$CRYPTEX_SYSOS" -o "$SYSOS_DMG" -key-value "$AEA_KEY" + else + echo " Using cached SystemOS DMG" + fi + + # Copy AppOS (unencrypted, cached) + if [[ ! -f "$APPOS_DMG" ]]; then + cp "$RESTORE_DIR/$CRYPTEX_APPOS" "$APPOS_DMG" + else + echo " Using cached AppOS DMG" + fi + + # Detach any leftover mounts from previous runs + safe_detach "$MNT_SYSOS" + safe_detach "$MNT_APPOS" + mkdir -p "$MNT_SYSOS" "$MNT_APPOS" + assert_mount_under_vm "$MNT_SYSOS" "SystemOS mountpoint" + assert_mount_under_vm "$MNT_APPOS" "AppOS mountpoint" + + echo " Mounting SystemOS..." + sudo hdiutil attach -mountpoint "$MNT_SYSOS" "$SYSOS_DMG" -nobrowse -owners off + echo " Mounting AppOS..." + sudo hdiutil attach -mountpoint "$MNT_APPOS" "$APPOS_DMG" -nobrowse -owners off + + ssh_cmd "/bin/rm -rf /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS" + ssh_cmd "/bin/mkdir -p /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS" + ssh_cmd "/bin/chmod 0755 /mnt1/System/Cryptexes/App /mnt1/System/Cryptexes/OS" + + # Copy Cryptex files to device + echo " Copying Cryptexes to device (this takes ~3 minutes)..." + scp_to "$MNT_SYSOS/." "/mnt1/System/Cryptexes/OS" + scp_to "$MNT_APPOS/." "/mnt1/System/Cryptexes/App" + + # Create dyld symlinks (ln -sf is idempotent) + echo " Creating dyld symlinks..." + ssh_cmd "/bin/ln -sf ../../../System/Cryptexes/OS/System/Library/Caches/com.apple.dyld \ + /mnt1/System/Library/Caches/com.apple.dyld" + ssh_cmd "/bin/ln -sf ../../../../System/Cryptexes/OS/System/DriverKit/System/Library/dyld \ + /mnt1/System/DriverKit/System/Library/dyld" + + # Unmount Cryptex DMGs + echo " Unmounting Cryptex DMGs..." + safe_detach "$MNT_SYSOS" + safe_detach "$MNT_APPOS" + + echo " [+] Cryptex installed" +fi # ═══════════ 2/7 PATCH SEPUTIL ════════════════════════════════ echo "" diff --git a/scripts/cfw_install_jb.sh b/scripts/cfw_install_jb.sh index b3735dc..b0943d0 100755 --- a/scripts/cfw_install_jb.sh +++ b/scripts/cfw_install_jb.sh @@ -159,9 +159,18 @@ fi scp_from "/mnt1/sbin/launchd.bak" "$TEMP_DIR/launchd" +# Extract original entitlements before patching (must preserve for spawn permissions) +echo " Extracting original entitlements..." +ldid -e "$TEMP_DIR/launchd" > "$TEMP_DIR/launchd.entitlements" 2>/dev/null || true +if [[ -s "$TEMP_DIR/launchd.entitlements" ]]; then + echo " [+] Preserved launchd entitlements" +else + echo " [!] No entitlements found on original launchd" +fi + # Inject launchdhook via short root alias to avoid Mach-O header overflow. # Keep the full /cores/launchdhook.dylib copy on disk for compatibility, but -# load /b from launchd because this launchd sample only has room for a 32-byte +# load /b from launchd because this launchd sample only has room for a short # LC_LOAD_DYLIB command after stripping LC_CODE_SIGNATURE. if [[ -d "$JB_INPUT_DIR/basebin" ]]; then echo " Injecting LC_LOAD_DYLIB for /b (short launchdhook alias)..." @@ -169,7 +178,13 @@ if [[ -d "$JB_INPUT_DIR/basebin" ]]; then fi python3 "$SCRIPT_DIR/patchers/cfw.py" patch-launchd-jetsam "$TEMP_DIR/launchd" -ldid_sign "$TEMP_DIR/launchd" + +# Re-sign with original entitlements to avoid "operation not permitted" on spawn +if [[ -s "$TEMP_DIR/launchd.entitlements" ]]; then + ldid -S"$TEMP_DIR/launchd.entitlements" -M "-K$VM_DIR/$CFW_INPUT/signcert.p12" "$TEMP_DIR/launchd" +else + ldid_sign "$TEMP_DIR/launchd" +fi scp_to "$TEMP_DIR/launchd" "/mnt1/sbin/launchd" ssh_cmd "/bin/chmod 0755 /mnt1/sbin/launchd" @@ -196,45 +211,58 @@ if [[ -f "$SILEO_DEB" ]]; then scp_to "$SILEO_DEB" "/mnt5/$BOOT_HASH/org.coolstar.sileo_2.5.1_iphoneos-arm64.deb" fi -ssh_cmd "/bin/mkdir -p /mnt5/$BOOT_HASH/jb-vphone" -ssh_cmd "/bin/chmod 0755 /mnt5/$BOOT_HASH/jb-vphone" -ssh_cmd "/usr/sbin/chown 0:0 /mnt5/$BOOT_HASH/jb-vphone" -ssh_cmd "/usr/bin/tar --preserve-permissions -xkf /mnt5/$BOOT_HASH/bootstrap-iphoneos-arm64.tar \ - -C /mnt5/$BOOT_HASH/jb-vphone/" -ssh_cmd "/bin/mv /mnt5/$BOOT_HASH/jb-vphone/var /mnt5/$BOOT_HASH/jb-vphone/procursus" -ssh_cmd "/bin/mkdir -p /mnt5/$BOOT_HASH/jb-vphone/procursus" -ssh_cmd "/bin/mv /mnt5/$BOOT_HASH/jb-vphone/procursus/jb/* /mnt5/$BOOT_HASH/jb-vphone/procursus 2>/dev/null || true" -ssh_cmd "/bin/rm -rf /mnt5/$BOOT_HASH/jb-vphone/procursus/jb" +JB_DIR_NAME="jb-vphone" +ssh_cmd "/bin/rm -rf /mnt5/$BOOT_HASH/jb" +ssh_cmd "/bin/rm -rf /mnt5/$BOOT_HASH/$JB_DIR_NAME" +ssh_cmd "/bin/mkdir -p /mnt5/$BOOT_HASH/$JB_DIR_NAME" +ssh_cmd "/bin/chmod 0755 /mnt5/$BOOT_HASH/$JB_DIR_NAME" +ssh_cmd "/usr/sbin/chown 0:0 /mnt5/$BOOT_HASH/$JB_DIR_NAME" +ssh_cmd "/usr/bin/tar --preserve-permissions -xf /mnt5/$BOOT_HASH/bootstrap-iphoneos-arm64.tar \ + -C /mnt5/$BOOT_HASH/$JB_DIR_NAME/" +ssh_cmd "/bin/mv /mnt5/$BOOT_HASH/$JB_DIR_NAME/var /mnt5/$BOOT_HASH/$JB_DIR_NAME/procursus" +ssh_cmd "/bin/mv /mnt5/$BOOT_HASH/$JB_DIR_NAME/procursus/jb/* /mnt5/$BOOT_HASH/$JB_DIR_NAME/procursus 2>/dev/null || true" +ssh_cmd "/bin/rm -rf /mnt5/$BOOT_HASH/$JB_DIR_NAME/procursus/jb" ssh_cmd "/bin/rm -f /mnt5/$BOOT_HASH/bootstrap-iphoneos-arm64.tar" rm -f "$BOOTSTRAP_TAR" +# NOTE: /var/jb symlink is created at runtime by launchdhook.dylib +# (Data volume is encrypted and not mountable from ramdisk). + echo " [+] procursus bootstrap installed" # ═══════════ JB-3 DEPLOY BASEBIN HOOKS ═════════════════════════ BASEBIN_DIR="$JB_INPUT_DIR/basebin" + if [[ -d "$BASEBIN_DIR" ]]; then echo "" echo "[JB-3] Deploying BaseBin hooks to /cores/..." + # Clean previous dylibs before re-uploading + echo " Cleaning old /cores/ dylibs..." + ssh_cmd "/bin/rm -rf /mnt1/cores" ssh_cmd "/bin/mkdir -p /mnt1/cores" ssh_cmd "/bin/chmod 0755 /mnt1/cores" + # Install all pre-built dylibs from basebin payload for dylib in "$BASEBIN_DIR"/*.dylib; do [[ -f "$dylib" ]] || continue dylib_name="$(basename "$dylib")" echo " Installing $dylib_name..." - # Re-sign with our certificate before deploying ldid_sign "$dylib" scp_to "$dylib" "/mnt1/cores/$dylib_name" ssh_cmd "/bin/chmod 0755 /mnt1/cores/$dylib_name" - - if [[ "$dylib_name" == "launchdhook.dylib" ]]; then - echo " Installing short launchdhook alias at /b..." - scp_to "$dylib" "/mnt1/b" - ssh_cmd "/bin/chmod 0755 /mnt1/b" - fi done + # Short alias for launchdhook (header space is tight) + if [[ -f "$BASEBIN_DIR/launchdhook.dylib" ]]; then + echo " Installing short launchdhook alias at /b..." + cp "$BASEBIN_DIR/launchdhook.dylib" "$TEMP_DIR/b" + ldid_sign "$TEMP_DIR/b" + ssh_cmd "/bin/rm -f /mnt1/b" + scp_to "$TEMP_DIR/b" "/mnt1/b" + ssh_cmd "/bin/chmod 0755 /mnt1/b" + fi + echo " [+] BaseBin hooks deployed" fi diff --git a/scripts/fw_patch_test.py b/scripts/fw_patch_test.py deleted file mode 100644 index 7de773a..0000000 --- a/scripts/fw_patch_test.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -""" -fw_patch_test.py — apply a single JB kernel patch method onto a dev-patched image. - -Usage: - PATCH=patch_xxx python3 fw_patch_test.py [vm_directory] -""" - -import os -import sys - -from fw_patch import find_file, find_restore_dir, load_firmware, save_firmware -from patchers.kernel_jb import KernelJBPatcher - - -def _build_single_patch_plan(patcher, method_name): - all_methods = getattr(KernelJBPatcher, "_PATCH_METHODS", ()) - if method_name not in all_methods: - available = "\n".join(f" - {name}" for name in all_methods) - raise ValueError( - f"Unknown JB patch method: {method_name}\nAvailable methods:\n{available}" - ) - if not callable(getattr(patcher, method_name, None)): - raise ValueError(f"Method is not callable on patcher: {method_name}") - return (method_name,) - - -def patch_kernelcache_single(data, method_name): - patcher = KernelJBPatcher(data) - plan = _build_single_patch_plan(patcher, method_name) - original_plan = patcher._PATCH_METHODS - patcher._PATCH_METHODS = plan - try: - patches = list(patcher.find_all()) - finally: - patcher._PATCH_METHODS = original_plan - - if not patches: - print(f" [-] No patches emitted by method: {method_name}") - return False - - for off, patch_bytes, _ in patches: - data[off : off + len(patch_bytes)] = patch_bytes - - print(f" [+] {len(patches)} patch(es) emitted by {method_name}") - return True - - -def main(): - method_name = os.environ.get("PATCH", "").strip() - if not method_name: - print("[-] PATCH environment variable is required (example: PATCH=)") - sys.exit(1) - - vm_dir = sys.argv[1] if len(sys.argv) > 1 else os.getcwd() - vm_dir = os.path.abspath(vm_dir) - if not os.path.isdir(vm_dir): - print(f"[-] Not a directory: {vm_dir}") - sys.exit(1) - - restore_dir = find_restore_dir(vm_dir) - if not restore_dir: - print(f"[-] No *Restore* directory found in {vm_dir}") - sys.exit(1) - - kernel_path = find_file(restore_dir, ["kernelcache.research.vphone600"], "kernelcache") - - print(f"[*] VM directory: {vm_dir}") - print(f"[*] Restore directory: {restore_dir}") - print(f"[*] Testing JB method: {method_name}") - print(f"[*] Target file: {kernel_path}") - - im4p, data, was_im4p, original_raw = load_firmware(kernel_path) - if not patch_kernelcache_single(data, method_name): - sys.exit(1) - - save_firmware(kernel_path, im4p, data, was_im4p, original_raw) - print("[+] Single JB patch test applied successfully") - - -if __name__ == "__main__": - main() diff --git a/scripts/jb_patch_autotest.sh b/scripts/jb_patch_autotest.sh deleted file mode 100644 index cf24c26..0000000 --- a/scripts/jb_patch_autotest.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/zsh -# jb_patch_autotest.sh — run full setup_machine flow for each JB kernel patch method. -# Strategy: apply each single JB kernel method on top of the dev baseline, one case at a time. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -cd "$PROJECT_ROOT" - -LOG_ROOT="${PROJECT_ROOT}/setup_logs/jb_patch_tests_$(date +%Y%m%d_%H%M%S)" -SUMMARY_CSV="${LOG_ROOT}/summary.csv" -MASTER_LOG="${LOG_ROOT}/run.log" -INCLUDE_WORKING="${JB_AUTOTEST_INCLUDE_WORKING:-0}" - -mkdir -p "$LOG_ROOT" -touch "$MASTER_LOG" - -if [[ -x "${PROJECT_ROOT}/.venv/bin/python3" ]]; then - PYTHON_BIN="${PROJECT_ROOT}/.venv/bin/python3" -else - PYTHON_BIN="$(command -v python3)" -fi - -PATCH_METHODS=("${(@f)$( - cd "${PROJECT_ROOT}/scripts" && "$PYTHON_BIN" - <<'PY' -import os -from patchers.kernel_jb import KernelJBPatcher - -def _env_enabled(name, default=False): - raw = os.environ.get(name) - if raw is None: - return default - return raw.strip().lower() in {"1", "true", "yes", "on"} - -include_working = _env_enabled("JB_AUTOTEST_INCLUDE_WORKING", default=False) -all_methods = list(getattr(KernelJBPatcher, "_PATCH_METHODS", ())) -if include_working: - selected_methods = all_methods -else: - working = set(getattr(KernelJBPatcher, "_DEV_SINGLE_WORKING_METHODS", ())) - selected_methods = [m for m in all_methods if m not in working] - -for method in selected_methods: - print(method) -PY -)}") - -if (( ${#PATCH_METHODS[@]} == 0 )); then - echo "[*] No JB patch methods selected (all already marked working or list empty)" | tee -a "$MASTER_LOG" - echo "[*] Set JB_AUTOTEST_INCLUDE_WORKING=1 to run the full list." | tee -a "$MASTER_LOG" - exit 0 -fi - -echo "index,patch,status,exit_code,log_file" >"$SUMMARY_CSV" -echo "[*] JB patch single-method automation started" | tee -a "$MASTER_LOG" -echo "[*] Logs: $LOG_ROOT" | tee -a "$MASTER_LOG" -echo "[*] Include already-working methods: ${INCLUDE_WORKING}" | tee -a "$MASTER_LOG" -echo "[*] Total methods: ${#PATCH_METHODS[@]}" | tee -a "$MASTER_LOG" - -idx=0 -for patch_method in "${PATCH_METHODS[@]}"; do - (( ++idx )) - case_log="${LOG_ROOT}/$(printf '%02d' "$idx")_${patch_method}.log" - - { - echo "" - echo "============================================================" - echo "[*] [$idx/${#PATCH_METHODS[@]}] Testing PATCH=${patch_method}" - echo "============================================================" - } | tee -a "$MASTER_LOG" - - set +e - # Test matrix assumption: each JB kernel method is validated on top of dev patch baseline. - case_skip_project_setup="${SKIP_PROJECT_SETUP:-1}" - echo "[*] Env: NONE_INTERACTIVE=1 DEV=1 SKIP_PROJECT_SETUP=${case_skip_project_setup} PATCH=${patch_method}" | tee -a "$MASTER_LOG" - SUDO_PASSWORD="${SUDO_PASSWORD:-}" \ - NONE_INTERACTIVE=1 \ - DEV=1 \ - SKIP_PROJECT_SETUP="${case_skip_project_setup}" \ - PATCH="$patch_method" \ - make setup_machine >"$case_log" 2>&1 - rc=$? - set -e - - if (( rc == 0 )); then - case_status="PASS" - else - case_status="FAIL" - fi - - echo "${idx},${patch_method},${case_status},${rc},${case_log}" >>"$SUMMARY_CSV" - echo "[*] Result: ${case_status} (rc=${rc}) log=${case_log}" | tee -a "$MASTER_LOG" -done - -echo "" -echo "[*] Completed JB patch automation. Summary: $SUMMARY_CSV" | tee -a "$MASTER_LOG" diff --git a/scripts/patchers/cfw.py b/scripts/patchers/cfw.py index 1833f37..116096b 100755 --- a/scripts/patchers/cfw.py +++ b/scripts/patchers/cfw.py @@ -46,14 +46,12 @@ if __name__ == "__main__": from patchers.cfw_patch_cache_loader import patch_launchd_cache_loader from patchers.cfw_patch_mobileactivationd import patch_mobileactivationd from patchers.cfw_patch_jetsam import patch_launchd_jetsam - from patchers.cfw_inject_dylib import inject_dylib from patchers.cfw_daemons import parse_cryptex_paths, inject_daemons else: from .cfw_patch_seputil import patch_seputil from .cfw_patch_cache_loader import patch_launchd_cache_loader from .cfw_patch_mobileactivationd import patch_mobileactivationd from .cfw_patch_jetsam import patch_launchd_jetsam - from .cfw_inject_dylib import inject_dylib from .cfw_daemons import parse_cryptex_paths, inject_daemons @@ -110,8 +108,22 @@ def main(): if len(sys.argv) < 4: print("Usage: patch_cfw.py inject-dylib ") sys.exit(1) - if not inject_dylib(sys.argv[2], sys.argv[3]): + import subprocess, shutil + insert_dylib_bin = shutil.which("insert_dylib") + if not insert_dylib_bin: + # Check .tools/bin/ relative to project root + project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + candidate = os.path.join(project_root, ".tools", "bin", "insert_dylib") + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + insert_dylib_bin = candidate + if not insert_dylib_bin: + print("[-] insert_dylib not found. Run: make setup_tools") sys.exit(1) + rc = subprocess.run( + [insert_dylib_bin, "--weak", "--inplace", "--all-yes", sys.argv[3], sys.argv[2]], + ).returncode + if rc != 0: + sys.exit(rc) else: print(f"Unknown command: {cmd}") diff --git a/scripts/patchers/cfw_inject_dylib.py b/scripts/patchers/cfw_inject_dylib.py deleted file mode 100644 index 164decf..0000000 --- a/scripts/patchers/cfw_inject_dylib.py +++ /dev/null @@ -1,241 +0,0 @@ -"""LC_LOAD_DYLIB injection module.""" - -from .cfw_asm import * - -def _align(n, alignment): - return (n + alignment - 1) & ~(alignment - 1) - - -def _find_first_section_offset(data): - """Find the file offset of the earliest section data in the Mach-O. - - This tells us how much space is available after load commands. - For fat/universal binaries, we operate on the first slice. - """ - magic = struct.unpack_from(" 0 and size > 0 and file_off < earliest: - earliest = file_off - sect_off += 80 - offset += cmdsize - return earliest - - -def _get_fat_slices(data): - """Parse FAT (universal) binary header and return list of (offset, size) tuples. - - Returns [(0, len(data))] for thin binaries. - """ - magic = struct.unpack_from(">I", data, 0)[0] - if magic == 0xCAFEBABE: # FAT_MAGIC - nfat = struct.unpack_from(">I", data, 4)[0] - slices = [] - for i in range(nfat): - off = 8 + i * 20 - slice_off = struct.unpack_from(">I", data, off + 8)[0] - slice_size = struct.unpack_from(">I", data, off + 12)[0] - slices.append((slice_off, slice_size)) - return slices - elif magic == 0xBEBAFECA: # FAT_MAGIC_64 - nfat = struct.unpack_from(">I", data, 4)[0] - slices = [] - for i in range(nfat): - off = 8 + i * 32 - slice_off = struct.unpack_from(">Q", data, off + 8)[0] - slice_size = struct.unpack_from(">Q", data, off + 16)[0] - slices.append((slice_off, slice_size)) - return slices - else: - return [(0, len(data))] - - -def _check_existing_dylib(data, base, dylib_path): - """Check if the dylib is already loaded in this Mach-O slice.""" - magic = struct.unpack_from(" 0: - ncmds = struct.unpack_from(" 256: - print(f" [-] Would overflow {overflow} bytes into section data (too much)") - return False - print( - f" [!] Header overflow: {overflow} bytes into section data " - f"(same as optool — binary will be re-signed)" - ) - - # Write the new load command at the end of existing commands - data[header_end : header_end + cmd_size] = lc_data - - # Update header: ncmds += 1, sizeofcmds += cmd_size - struct.pack_into(" -t - """ - data = bytearray(open(filepath, "rb").read()) - slices = _get_fat_slices(bytes(data)) - - injected = 0 - for slice_off, slice_size in slices: - if _check_existing_dylib(data, slice_off, dylib_path): - print(f" [!] Dylib already loaded in slice at 0x{slice_off:X}, skipping") - injected += 1 - continue - - if _inject_lc_load_dylib(data, slice_off, dylib_path): - print( - f" [+] Injected LC_LOAD_DYLIB '{dylib_path}' at slice 0x{slice_off:X}" - ) - injected += 1 - - if injected == len(slices): - open(filepath, "wb").write(data) - print(f" [+] Wrote {filepath} ({injected} slice(s) patched)") - return True - else: - print(f" [-] Only {injected}/{len(slices)} slices patched") - return False - - -# ══════════════════════════════════════════════════════════════════ -# BuildManifest parsing -# ══════════════════════════════════════════════════════════════════ - - diff --git a/scripts/setup_machine.sh b/scripts/setup_machine.sh index 4546948..59d7866 100755 --- a/scripts/setup_machine.sh +++ b/scripts/setup_machine.sh @@ -875,7 +875,6 @@ Options: Environment: NONE_INTERACTIVE=1 Auto-continue first-boot prompts + run final boot analysis. - PATCH=patch_xxx Run `make fw_patch_test` after the main fw_patch target. SUDO_PASSWORD=... Preload sudo credential via askpass. EOF exit 0 @@ -930,9 +929,6 @@ main() { run_make "Firmware prep" vm_new run_make "Firmware prep" fw_prepare run_make "Firmware patch" "$fw_patch_target" - if [[ -n "${PATCH:-}" ]]; then - run_make "Firmware patch test" fw_patch_test - fi echo "" echo "=== Restore phase ===" diff --git a/scripts/setup_tools.sh b/scripts/setup_tools.sh index 05afb65..169b8cb 100644 --- a/scripts/setup_tools.sh +++ b/scripts/setup_tools.sh @@ -2,7 +2,7 @@ # setup_tools.sh — Install all required host tools for vphone-cli # # Installs brew packages, builds trustcache from source, -# builds libimobiledevice toolchain, and creates Python venv. +# clones insert_dylib, builds libimobiledevice toolchain, and creates Python venv. # # Run: make setup_tools @@ -12,9 +12,22 @@ SCRIPT_DIR="${0:a:h}" PROJECT_DIR="${SCRIPT_DIR:h}" TOOLS_PREFIX="${TOOLS_PREFIX:-$PROJECT_DIR/.tools}" +clone_or_update() { + local url="$1" + local dir="$2" + + if [[ -d "$dir/.git" ]]; then + git -C "$dir" fetch --depth 1 origin --quiet + git -C "$dir" reset --hard FETCH_HEAD --quiet + git -C "$dir" clean -fdx --quiet + else + git clone --depth 1 "$url" "$dir" --quiet + fi +} + # ── Brew packages ────────────────────────────────────────────── -echo "[1/4] Checking brew packages..." +echo "[1/5] Checking brew packages..." BREW_PACKAGES=(gnu-tar openssl@3 ldid-procursus sshpass) BREW_MISSING=() @@ -34,7 +47,7 @@ fi # ── Trustcache ───────────────────────────────────────────────── -echo "[2/4] trustcache" +echo "[2/5] trustcache" TRUSTCACHE_BIN="$TOOLS_PREFIX/bin/trustcache" if [[ -x "$TRUSTCACHE_BIN" ]]; then @@ -58,14 +71,31 @@ else echo " Installed: $TRUSTCACHE_BIN" fi +# ── insert_dylib ─────────────────────────────────────────────── + +echo "[3/5] insert_dylib" + +INSERT_DYLIB_BIN="$TOOLS_PREFIX/bin/insert_dylib" +if [[ -x "$INSERT_DYLIB_BIN" ]]; then + echo " Already built: $INSERT_DYLIB_BIN" +else + INSERT_DYLIB_DIR="$TOOLS_PREFIX/src/insert_dylib" + mkdir -p "${INSERT_DYLIB_DIR:h}" + clone_or_update "https://github.com/tyilo/insert_dylib" "$INSERT_DYLIB_DIR" + echo " Building insert_dylib..." + mkdir -p "$TOOLS_PREFIX/bin" + clang -o "$INSERT_DYLIB_BIN" "$INSERT_DYLIB_DIR/insert_dylib/main.c" -framework Security -O2 + echo " Installed: $INSERT_DYLIB_BIN" +fi + # ── Libimobiledevice ────────────────────────────────────────── -echo "[3/4] libimobiledevice" +echo "[4/5] libimobiledevice" bash "$SCRIPT_DIR/setup_libimobiledevice.sh" # ── Python venv ──────────────────────────────────────────────── -echo "[4/4] Python venv" +echo "[5/5] Python venv" zsh "$SCRIPT_DIR/setup_venv.sh" echo "" diff --git a/sources/vphone-cli/VPhoneControl.swift b/sources/vphone-cli/VPhoneControl.swift index d154e9a..de18358 100644 --- a/sources/vphone-cli/VPhoneControl.swift +++ b/sources/vphone-cli/VPhoneControl.swift @@ -127,7 +127,6 @@ class VPhoneControl { self?.connection = conn self?.performHandshake(fd: conn.fileDescriptor) case let .failure(error): - print("[control] vsock: \(error.localizedDescription), retrying...") DispatchQueue.main.asyncAfter(deadline: .now() + 3) { self?.attemptConnect() } diff --git a/sources/vphone-cli/VPhoneVirtualMachine.swift b/sources/vphone-cli/VPhoneVirtualMachine.swift index 9db50a6..cb3da21 100644 --- a/sources/vphone-cli/VPhoneVirtualMachine.swift +++ b/sources/vphone-cli/VPhoneVirtualMachine.swift @@ -292,6 +292,13 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate { } else { print("[vphone] VM started — booting normally") } + + // Print auto-assigned debug stub port after VM starts + if let debugStub = Dynamic(virtualMachine)._configuration._debugStub.asAnyObject { + if let port = Dynamic(debugStub).port.asInt, port > 0 { + print("[vphone] Kernel GDB debug stub listening on tcp://127.0.0.1:\(port)") + } + } } // MARK: - Delegate From b3ed19232b45c2ca61a094c766820666a345c3d0 Mon Sep 17 00:00:00 2001 From: Lakr Date: Sat, 7 Mar 2026 18:07:46 +0800 Subject: [PATCH 02/10] Update resources --- scripts/resources | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/resources b/scripts/resources index e8563f7..ee9a284 160000 --- a/scripts/resources +++ b/scripts/resources @@ -1 +1 @@ -Subproject commit e8563f72455e90393615209b23fef1273a125178 +Subproject commit ee9a2845b351ad8a262debb75644db41834eaaa2 From 048f4c7cc110d7a5c6607fe717d8c961ea009f68 Mon Sep 17 00:00:00 2001 From: Lakr Date: Sat, 7 Mar 2026 18:23:34 +0800 Subject: [PATCH 03/10] docs: fix outdated patch counts, remove process investigation notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update firmware variant table (CLAUDE.md, AGENTS.md): correct patch totals to 51/64/126, add VPhoneMenuBattery.swift, setup_venv_linux.sh, tail_jb_patch_logs.sh to architecture tree - kernel_patcher_verification.md: 25→26 patches (patch_apfs_get_dev_by_role) - kernel_patch_sandbox.py: docstring 16-25→17-26 - kernel_patch_sandbox_hooks: consolidate to single 17-26 validation file - txm_jb_patches.md: fix txm_jb.py references → txm_dev.py - 0_binary_patch_comparison.md: split kernel counts (28 base + 59 JB methods) - kernel_jb_patch_notes.md: add 2026-03-06 retarget notes for bsd_init_auth, io_secure_bsd_root, vm_fault_enter_prepare - Remove 7 boot investigation process notes (boot_*.md) --- AGENTS.md | 15 +- research/0_binary_patch_comparison.md | 11 +- ...a_protection_seputil_macf_investigation.md | 115 ---- ...ng_b19_mount_dounmount_strategy_compare.md | 628 ------------------ .../boot_jb_mount_failure_investigation.md | 223 ------- ...oot_launchd_data_volume_lookup_analysis.md | 160 ----- ..._launchdhook_assertion_handoff_20260306.md | 254 ------- research/boot_log_fail_vs_success_analysis.md | 107 --- research/boot_mount_phase1_failure_matrix.md | 195 ------ research/kernel_jb_patch_notes.md | 25 +- ...l_patch_sandbox_hooks_17_26_validation.md} | 12 +- research/kernel_patcher_verification.md | 29 +- research/txm_jb_patches.md | 12 +- scripts/patchers/kernel_patch_sandbox.py | 4 +- 14 files changed, 70 insertions(+), 1720 deletions(-) delete mode 100644 research/boot_data_protection_seputil_macf_investigation.md delete mode 100644 research/boot_hang_b19_mount_dounmount_strategy_compare.md delete mode 100644 research/boot_jb_mount_failure_investigation.md delete mode 100644 research/boot_launchd_data_volume_lookup_analysis.md delete mode 100644 research/boot_launchdhook_assertion_handoff_20260306.md delete mode 100644 research/boot_log_fail_vs_success_analysis.md delete mode 100644 research/boot_mount_phase1_failure_matrix.md rename research/{kernel_patch_sandbox_hooks_21_26_validation.md => kernel_patch_sandbox_hooks_17_26_validation.md} (87%) diff --git a/AGENTS.md b/AGENTS.md index f6c3fd2..ef98741 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,9 +31,9 @@ For any changes applying new patches, also update research/0_binary_patch_compar | Variant | Boot Chain | CFW | Make Targets | | ------------------- | :--------: | :-------: | ---------------------------------- | -| **Regular** | 38 patches | 10 phases | `fw_patch` + `cfw_install` | -| **Development** | 49 patches | 12 phases | `fw_patch_dev` + `cfw_install_dev` | -| **Jailbreak (WIP)** | 84 patches | 14 phases | `fw_patch_jb` + `cfw_install_jb` | +| **Regular** | 51 patches | 10 phases | `fw_patch` + `cfw_install` | +| **Development** | 64 patches | 12 phases | `fw_patch_dev` + `cfw_install_dev` | +| **Jailbreak (WIP)** | 126 patches | 14 phases | `fw_patch_jb` + `cfw_install_jb` | See `research/` for detailed firmware pipeline, component origins, patch breakdowns, and boot flow documentation. @@ -73,6 +73,7 @@ sources/ ├── VPhoneMenuConnect.swift # Connect menu — devmode, ping, version, file browser ├── VPhoneMenuInstall.swift # Install menu — IPA installation to guest ├── VPhoneMenuRecord.swift # Record menu — screen recording controls + ├── VPhoneMenuBattery.swift # Battery menu — battery status display │ │ # IPA installation ├── VPhoneIPAInstaller.swift # IPA extraction, signing, and installation @@ -89,8 +90,8 @@ scripts/ ├── patchers/ # Python patcher modules │ ├── iboot.py # iBoot patcher (iBSS/iBEC/LLB) │ ├── iboot_jb.py # JB: iBoot nonce skip -│ ├── kernel.py # Kernel patcher (25 patches) -│ ├── kernel_jb.py # JB: kernel patches (~34) +│ ├── kernel.py # Kernel patcher (26 patches) +│ ├── kernel_jb.py # JB: kernel patches (~40) │ ├── txm.py # TXM patcher │ ├── txm_dev.py # Dev: TXM entitlements/debugger/dev mode @@ -111,7 +112,9 @@ scripts/ ├── setup_machine.sh # Full automation (setup → first boot) ├── setup_tools.sh # Install deps, build toolchain, create venv ├── setup_venv.sh # Create Python venv -└── setup_libimobiledevice.sh # Build libimobiledevice from source +├── setup_venv_linux.sh # Create Python venv (Linux) +├── setup_libimobiledevice.sh # Build libimobiledevice from source +└── tail_jb_patch_logs.sh # Tail JB patch log output research/ # Detailed firmware/patch documentation ``` diff --git a/research/0_binary_patch_comparison.md b/research/0_binary_patch_comparison.md index 4115a3e..eb8cc45 100644 --- a/research/0_binary_patch_comparison.md +++ b/research/0_binary_patch_comparison.md @@ -77,7 +77,7 @@ | # | Group | Method | Function | Purpose | JB Enabled | | ----- | ----- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------: | | 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 | 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-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 | @@ -153,12 +153,13 @@ | iBEC | 3 | 3 | 3 | | LLB | 6 | 6 | 6 | | TXM | 1 | 12 | 12 | -| Kernel | 28 | 28 | 53 | -| Boot chain total | 41 | 52 | 78 | +| Kernel (base) | 28 | 28 | 28 | +| Kernel (JB methods) | - | - | 59 | +| Boot chain total | 41 | 52 | 112 | | CFW binary patches | 4 | 5 | 6 | | CFW installed components | 6 | 7 | 8 | | CFW total | 10 | 12 | 14 | -| Grand total | 51 | 64 | 92 | +| Grand total | 51 | 64 | 126 | ## Ramdisk Variant Matrix @@ -166,7 +167,7 @@ | ------------- | ------------------- | -------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------- | | `RAMDISK` | `make fw_patch` | release TXM + base TXM patch (1) | base kernel (28), legacy `*.ramdisk` preferred else derive from pristine CloudOS | restore kernel from `fw_patch` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | | `DEV+RAMDISK` | `make fw_patch_dev` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_dev` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | -| `JB+RAMDISK` | `make fw_patch_jb` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_jb` (53) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | +| `JB+RAMDISK` | `make fw_patch_jb` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_jb` (28+59) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | ## Cross-Version Dynamic Snapshot diff --git a/research/boot_data_protection_seputil_macf_investigation.md b/research/boot_data_protection_seputil_macf_investigation.md deleted file mode 100644 index 0917a6a..0000000 --- a/research/boot_data_protection_seputil_macf_investigation.md +++ /dev/null @@ -1,115 +0,0 @@ -# Data-Protection Panic Investigation (2026-03-05) - -## Symptom - -On JB boot, system reaches `launchd` boot tasks but then panics with: - -- `Boot task failed: data-protection - exited due to exit(60)` -- repeated host-side retries: - - `[control] vsock: ... Connection reset by peer, retrying...` -- and critical kernel log: - - `IOUC AppleSEPUserClient failed MACF in process pid 6, seputil` - -## What This Confirms - -This is not an early-kernel boot hang. The failure occurs in userspace boot task `data-protection`, where `seputil` cannot pass MACF/IOKit authorization to open SEP user client paths. - -## Static Root-Cause Trace (kernelcache.research.vphone600) - -Using local disassembly on the current vphone600 research kernel: - -1. `failed MACF` log string xref resolves to IOKit MAC dispatch function at: - - function start VA: `0xFFFFFE000825B0C0` -2. The deny path emits `failed MACF` after policy callback dispatch. -3. Callback slot used in this path is: - - `policy->ops + 0x648` => index `201` -4. Current sandbox-extended patch set (before this fix) did not include `ops[201..210]`. -5. Sandbox ops table for this kernel has non-null handlers at: - - `201..210` (`0xFFFFFE00093A654C` ... `0xFFFFFE00093A598C`) - -Interpretation: IOKit MAC hooks remained active and could deny `AppleSEPUserClient` access, matching runtime `IOUC ... failed MACF`. - -## Mitigation Implemented - -### 1) Kernel patcher extension - -Updated: - -- `scripts/patchers/kernel_jb_patch_sandbox_extended.py` - -Added hook indices: - -- `201..210` as `iokit_check_201` ... `iokit_check_210` - -Patch action per entry remains: - -- `mov x0,#0` -- `ret` - -### 2) Documentation updates - -Updated: - -- `research/kernel_patch_jb/patch_sandbox_hooks_extended.md` -- `research/0_binary_patch_comparison.md` - -## Local Validation (static) - -Ran `patch_sandbox_hooks_extended()` against current `kernelcache.research.vphone600`: - -- before extension: `52` writes (`26` hooks) -- after extension: `72` writes (`36` hooks) - -New emitted entries include: - -- `_hook_iokit_check_201` ... `_hook_iokit_check_210` - -## Runtime Validation Pending - -Not yet executed in this turn. Required E2E confirmation: - -1. `make fw_patch_jb` -2. restore flow (so patched kernel is installed) -3. `make cfw_install_jb` -4. `make boot` -5. verify disappearance of: - - `IOUC AppleSEPUserClient failed MACF ... seputil` - - `Boot task failed: data-protection - exited due to exit(60)` - -## Notes - -- Canonical `mpo_iokit_*` names for these indices are not fully symbol-resolved in local KC symbols; index-based labeling is used intentionally to avoid incorrect naming. - -## 2026-03-06 Follow-up (still failing after ops[201..210] extension) - -Observed runtime still reports: - -- `IOUC AppleAPFSUserClient failed MACF in process pid 4, mount` -- `IOUC AppleSEPUserClient failed MACF in process pid 6, seputil` - -So per-policy sandbox hook stubs alone are insufficient on this path. - -### Additional Mitigation Added - -Introduced a dedicated JB patch: - -- `patch_iouc_failed_macf` in `scripts/patchers/kernel_jb_patch_iouc_macf.py` - -Method: - -- Anchor on `"failed MACF"` xref. -- Resolve centralized IOUC MACF gate function. -- Apply low-risk early return at `fn+4/fn+8`: - - `mov x0, xzr` - - `retab` - -Current static hit on this kernel: - -- function start: `0xfffffe000825b0c0` -- patched: - - `0xfffffe000825b0c4` - - `0xfffffe000825b0c8` - -Related doc: - -- `research/kernel_patch_jb/patch_iouc_failed_macf.md` diff --git a/research/boot_hang_b19_mount_dounmount_strategy_compare.md b/research/boot_hang_b19_mount_dounmount_strategy_compare.md deleted file mode 100644 index 5875092..0000000 --- a/research/boot_hang_b19_mount_dounmount_strategy_compare.md +++ /dev/null @@ -1,628 +0,0 @@ -# Boot Hang Focus: B19 + (B11/B12) Strategy Comparison - -Date: 2026-03-05 -Target binary family: `kernelcache.research.vphone600` (iOS 26.1 / 23B85) - -## Final Outcome (2026-03-05) - -This investigation is complete for the current delivery gate (bootability). - -- B19/MNT strategy switching did **not** recover boot: - - matrix artifact: `vm/ab_matrix_b19_mnt_20260305_034127.csv` - - result: 9/9 combinations failed (`code=2`, watchdog timeout) -- JB-only bisect isolated a stable bootable subset: - - PASS: `A1-A4` - - PASS: `A1-A4 + B5-B8` - - FAIL: tested combinations including `B9+` -- Current boot-safe default in `kernel_jb.py`: - - enabled: `A1-A4 + B5-B8` - - disabled: `B9-B20`, `C21-C24` -- E2E success evidence: - - `vm/testing_exec_watch_20260305_050051.log` - - `vm/testing_exec_watch_20260305_050146.log` - - both reached restore-ready markers (USB mux activated + waiting-for-host gate) - -## Scope - -This note compares two patching styles for boot-hang triage: - -1. B19 (`_IOSecureBSDRoot`) patch style mismatch: - - upstream known-to-work fixed site - - current dynamic patcher site -2. B11/B12 (`___mac_mount` / `_dounmount`) patch style mismatch: - - upstream fixed-site patches - - current dynamic strict-shape patches - -The goal is to make A/B testing reproducible with concrete trigger points, pseudocode, and expected runtime effects. - ---- - -## 1) B19 `_IOSecureBSDRoot` mismatch - -### 1.1 Trigger points (clean kernel) - -- `0x0128B598` (`VA 0xFFFFFE000828F598`) in `sub_FFFFFE000828F42C` - - before: `b.ne #0x128b5bc` - - upstream patch: `b #0x128b5bc` (`0x14000009`) -- `0x01362090` (`VA 0xFFFFFE0008366090`) in `sub_FFFFFE0008366008` - - before: `cbz w0, #0x1362234` - - current dynamic patch: `b #0x1362234` (`0x14000069`) - -Current patched checkpoint confirms: - -- `0x01362090` is patched (`b`) -- `0x0128B598` remains unpatched (`b.ne`) - -### 1.2 Function logic and pseudocode - -#### A) Upstream site function (`sub_FFFFFE000828F42C`) - -High-level logic: - -1. query `"SecureRootName"` from `IOPlatformExpert` -2. run provider call -3. release objects -4. if return code equals `0xE00002C1`, branch to fallback path (`sub_FFFFFE0007C6AA58`) - -Pseudocode: - -```c -ret = IOPlatformExpert->call("SecureRootName", ...); -release(...); -if (ret == 0xE00002C1) { - return fallback_path(); -} -return ret; -``` - -Patch effect at `0x128B598` (`b.ne -> b`): - -- always take fallback path, regardless whether `ret == 0xE00002C1`. - -#### B) Dynamic site function (`sub_FFFFFE0008366008`) - -Key branch: - -- `0x136608C`: callback for `"SecureRoot"` -- `0x1366090`: `cbz w0, #0x1362234` (branch into `"SecureRootName"` block) -- `0x1366234` onward: `"SecureRootName"` handling block - -Pseudocode: - -```c -if (matches("SecureRoot")) { - ok = callback("SecureRoot"); - if (ok == 0) goto SecureRootNameBlock; // cbz w0 - // SecureRoot success/failure handling path -} - -SecureRootNameBlock: -if (matches("SecureRootName")) { - // name-based validation + state sync -} -``` - -Patch effect at `0x1362090` (`cbz -> b`): - -- always jump into `SecureRootNameBlock`, regardless `ok`. - -### 1.3 A/B variants to test - -1. `B19-A` (upstream helper only): - - patch only `0x128B598` - - keep `0x1362090` original -2. `B19-B` (dynamic main only): - - patch only `0x1362090` - - keep `0x128B598` original -3. `B19-C` (both): - - patch both sites - -### 1.4 Expected observables - -- Boot logs around: - - `apfs_find_named_root_snapshot_xid` - - `Need authenticator (81)` - - transition into init / panic frame -- Panic signatures: - - null-deref style FAR near low address (for current failure class) - - stack path involving mount/security callback chain - ---- - -## 2) B11/B12 (`___mac_mount` / `_dounmount`) mismatch - -### 2.1 Trigger points (clean kernel) - -#### Upstream fixed-offset style - -- `0x00CA5D54`: `tbnz w28, #5, #0xca5f18` -> `nop` -- `0x00CA5D88`: `ldrb w8, [x8, #1]` -> `mov x8, xzr` -- `0x00CA8134`: `bl #0xc92ad8` -> `nop` - -#### Current dynamic style (checkpoint) - -- `0x00CA4EAC`: `cbnz w0, #0xca4ec8` -> `nop` (B11) -- `0x00CA81FC`: `bl #0xc9bdbc` -> `nop` (B12) - -And in checkpoint: - -- upstream sites remain original (`0xCA5D54`, `0xCA5D88`, `0xCA8134` unchanged) -- dynamic sites are patched (`0xCA4EAC`, `0xCA81FC` are `nop`) - -### 2.2 Function logic and pseudocode - -#### A) `___mac_mount`-related branch (dynamic site near `0xCA4EA8`) - -Disassembly window: - -- `0xCA4EA8`: `bl ...` -- `0xCA4EAC`: `cbnz w0, deny` -- deny target writes non-zero return (`mov w0, #1`) - -Pseudocode: - -```c -ret = mac_policy_check(...); -if (ret != 0) { // cbnz w0 - return EPERM_like_error; -} -continue_mount(); -``` - -Dynamic patch (`0xCA4EAC -> nop`) effect: - -- ignore `ret != 0` branch and continue mount path. - -#### B) Upstream `___mac_mount` two-site style (`0xCA5D54`, `0xCA5D88`) - -Disassembly window: - -- `0xCA5D54`: `tbnz w28, #5, ...` -- `0xCA5D88`: `ldrb w8, [x8, #1]` - -Pseudocode (behavioral interpretation): - -```c -if (flag_bit5_set(w28)) goto restricted_path; -w8 = *(u8 *)(x8 + 1); -... -``` - -Upstream patches: - -- remove bit-5 gate branch (`tbnz -> nop`) -- force register state (`ldrb -> mov x8, xzr`) - -This is broader state manipulation than dynamic deny-branch patching. - -#### C) `_dounmount` path - -Upstream site: - -- `0xCA8134`: `bl #0xc92ad8` -> `nop` - -Dynamic site: - -- `0xCA81FC`: `bl #0xc9bdbc` -> `nop` - -Pseudocode (generic): - -```c -... prepare args ... -ret = mac_or_policy_call_X(...); // site differs between two strategies -... -ret2 = mac_or_policy_call_Y(...); -``` - -Difference: - -- upstream and dynamic disable different call sites in unmount path; -- not equivalent by construction. - -### 2.3 A/B variants to test - -1. `MNT-A` (upstream-only style): - - apply `0xCA5D54`, `0xCA5D88`, `0xCA8134` - - keep `0xCA4EAC`, `0xCA81FC` original -2. `MNT-B` (dynamic-only style): - - apply `0xCA4EAC`, `0xCA81FC` - - keep `0xCA5D54`, `0xCA5D88`, `0xCA8134` original -3. `MNT-C` (both styles): - - apply all five sites - ---- - -## 3) Combined test matrix (recommended) - -For minimal triage noise, run a 3x3 matrix: - -- B19 mode: `B19-A`, `B19-B`, `B19-C` -- mount mode: `MNT-A`, `MNT-B`, `MNT-C` - -Total 9 combinations, each from the same clean baseline kernel. - -Record per run: - -1. last APFS logs before failure/success -2. whether `Need authenticator (81)` appears -3. panic presence and panic PC/FAR -4. whether init proceeds past current hang point - ---- - -## 4) Historical A/B Knobs (used during triage, now removed) - -During the triage phase, temporary runtime knobs were introduced to toggle -upstream-vs-dynamic strategies for B11/B12/B13/B14/B19 and execute the matrix. - -Those knobs are no longer part of the default runtime path after stabilization; -the shipped default now hard-selects the boot-safe subset (`A1-A4 + B5-B8`). - -The triage results from those knobs are preserved in this document and in: - -- `vm/ab_matrix_b19_mnt_20260305_034127.csv` -- `TODO.md` (Boot Hang Research + Progress Update sections) -- `research/0_binary_patch_comparison.md` (Kernelcache section) - ---- - -## 5) Practical note - -Do not mix incremental patching across already-patched binaries when comparing these modes. -Always regenerate from clean baseline before each combination, otherwise branch-site interactions can mask true causality. - ---- - -## 6) Additional non-equivalent points (beyond B19/B11/B12) - -This section answers "还有没有别的不一样的" with boot-impact-focused mismatches. - -### 6.1 B13 `_bsd_init auth` is not the same logical site - -#### Trigger points - -- upstream fixed site: `0x00F6D95C` in `sub_FFFFFE0007F6D2B8` -- current dynamic site: `0x00FA2A78` in `sub_FFFFFE0007FA2838` - -#### Function logic (high level) - -- `sub_FFFFFE0007F6D2B8` is a workqueue/thread-call state machine. -- `sub_FFFFFE0007FA2838` is another lock/CAS-heavy control path. - -Neither decompilation corresponds to `_bsd_init` body semantics directly. - -#### Pseudocode (site-level) - -`0xF6D95C` neighborhood: - -```c -... -call unlock_or_wakeup(...); // BL at 0xF6D95C -... -``` - -`0xFA2A78` neighborhood: - -```c -... -stats_counter++; -x2 = x9; // MOV at 0xFA2A78 -cas_release(lock, x2, 0); -... -``` - -#### Risk - -- This is a strong false-equivalence signal. -- If this patch is intended as `_bsd_init` auth bypass, current dynamic hit should be treated as suspect. - -### 6.2 B14 `_spawn_validate_persona` strategy changed from 2xNOP to forced branch - -#### Trigger points - -- upstream fixed sites: `0x00FA7024`, `0x00FA702C` (same function `sub_FFFFFE0007FA6F7C`) -- current dynamic site: `0x00FA694C` (function `sub_FFFFFE0007FA6858`) - -#### Function logic and loop relevance - -In `sub_FFFFFE0007FA6858`, there is an explicit spin loop: - -- `0xFA6ACC`: `LDADD ...` -- `0xFA6AD4`: `B.EQ 0xFA6ACC` (self-loop) - -Pseudocode: - -```c -do { - old = atomic_fetch_add(counter, 1); -} while (old == target); // tight spin at 0xFA6ACC/0xFA6AD4 -``` - -And same function calls: - -- `sub_FFFFFE0007B034E4` (at `0xFA6A94`) -- `sub_FFFFFE0007B040CC` (at `0xFA6AA8`) - -Your panic signature previously mapped into this call chain, so this mismatch is high-priority for 100% CPU / hang triage. - -### 6.3 B9 `_vm_fault_enter_prepare` does not hit the same function - -#### Trigger points - -- upstream fixed site: `0x00BA9E1C` in `sub_FFFFFE0007BA9C48` -- current dynamic site: `0x00BA9BB0` in `sub_FFFFFE0007BA9944` - -#### Pseudocode (site-level) - -`0xBA9E1C`: - -```c -// parameter setup right before BL -ldp x4, x5, [sp, ...]; -bl helper(...); -``` - -`0xBA9BB0`: - -```c -if (w25 == 3) w21 = 2; else w21 = w25; // csel -``` - -These are structurally unrelated. - -### 6.4 B10 `_vm_map_protect` site differs in same large function - -#### Trigger points - -- upstream fixed site: `0x00BC024C` -- current dynamic site: `0x00BC012C` -- both inside `sub_FFFFFE0007BBFA48` - -#### Pseudocode (site-level) - -`0xBC012C`: - -```c -perm = cond ? perm_a : perm_b; // csel -``` - -`0xBC024C`: - -```c -// different control block; not the same selection point -... -``` - -Even in the same function, these are not equivalent branch gates. - -### 6.5 B15 `_task_for_pid` and B17 shared-region are also shifted - -#### Trigger points - -- B15 upstream: `0x00FC383C` (`sub_FFFFFE0007FC34B4`) -- B15 dynamic: `0x00FFF83C` (`sub_FFFFFE0007FFF824`) - -- B17 upstream: `0x010729CC` -- B17 dynamic: `0x01072A88` -- both in `sub_FFFFFE000807272C`, but not same instruction role - -#### Risk - -- These are unlikely to explain early APFS/init mount failure alone, but they are still non-equivalent and should not be assumed interchangeable. - ---- - -## 7) Practical triage order for 100% virtualization CPU - -Given current evidence, prioritize: - -1. B14 strategy A/B first (upstream `0xFA7024/0xFA702C` vs dynamic `0xFA694C`). -2. B13 strategy A/B next (`0xF6D95C` vs `0xFA2A78`). -3. Then B19 and MNT matrix. - -Reason: B14 path contains a known tight spin construct and directly calls the function chain previously observed in panic mapping. - ---- - -## 8) Normal boot baseline signature (for pass/fail triage) - -Use the following runtime markers as "normal startup reached restore-ready stage" baseline: - -1. USB bring-up checkpoint completes: - - `CHECKPOINT END: MAIN:[0x040E] enable_usb` -2. Network checkpoint enters and exits without device requirement: - - `CHECKPOINT BEGIN: MAIN:[0x0411] config_network_interface` - - `no device required to enable network interface, skipping` - - `CHECKPOINT END: MAIN:[0x0411] config_network_interface` -3. Restore daemon enters host-wait state: - - `waiting for host to trigger start of restore [timeout of 120 seconds]` -4. USB/NCM path activates and host loopback socket churn appears: - - `IOUSBDeviceController::setupDeviceSetConfiguration: configuration 0 -> 1` - - `AppleUSBDeviceMux::message - kMessageInterfaceWasActivated` - - repeated `sock ... accepted ... 62078 ...` then `sock ... closed` -5. BSD network interface bring-up for `anpi0` succeeds: - - `configureDatagramSizeOnBSDInterface() [anpi0] ... returning 0x00000000` - - `enableBSDInterface() [anpi0], returning 0x00000000` - - `configureIPv6LLOnBSDInterface() [anpi0], IPv6 enable returning 0x00000000` - - `disableTrafficShapingOnBSDInterface() [anpi0], disable traffic shaping returning 0x00000000` - -Practical rule: - -- If A/B variant run reaches marker #3 and then shows #4/#5 progression, treat it as "boot path not stuck in early kernel loop". -- If run stalls before marker #1/#2 completion or never reaches #3, prioritize kernel-side loop/panic investigation. - ---- - -## 9) Why the failing sets are currently excluded - -Short answer: they are not equivalent rewrites on this firmware, and multiple -sites land in different control contexts than expected upstream references. - -IDA-backed findings used for exclusion: - -1. B9 differs by function, not just offset: - - dynamic `0xBA9BB0` in `sub_FFFFFE0007BA9944` - - upstream `0xBA9E1C` in `sub_FFFFFE0007BA9C48` -2. B10 is same large function but different decision blocks: - - dynamic `0xBC012C` vs upstream `0xBC024C` in `sub_FFFFFE0007BBFA48` -3. B13 differs by function and behavior: - - dynamic `0xFA2A78` in `sub_FFFFFE0007FA2838` - - upstream `0xF6D95C` in `sub_FFFFFE0007F6D2B8` -4. B14 dynamic path sits in the spin-loop-containing function: - - `sub_FFFFFE0007FA6858` has `0xFA6ACC`/`0xFA6AD4` tight loop - - same path calls `sub_FFFFFE0007B034E4` and `sub_FFFFFE0007B040CC` - -Given this mismatch profile, enabling B9+ as a default set is high risk for -boot regressions until each method is re-derived and validated individually on -the exact kernel build. - ---- - -## 10) Final operational state - -- Default JB boot profile: `A1-A4 + B5-B8` only -- Verified by `BASE_PATCH=jb make testing_exec` reproducibility runs: - - `vm/testing_exec_watch_20260305_050051.log` - - `vm/testing_exec_watch_20260305_050146.log` -- Delivery stance: - - prioritize bootability and deterministic restore-ready progression - - reintroduce B9+ / Group C only behind per-method revalidation - ---- - -## 11) New Field Finding: "restore done but system still not fully up" (`make boot`) - -Source: interactive serial output from `make boot` on 2026-03-05 (user report). - -### 11.1 What the log proves - -This run is **not** failing at the old restore-ready gate and **not** the old -early kernel boot-hang class. - -Observed progression: - -1. APFS root/data/xART/preboot mounts complete in kernel/userspace handoff. -2. `launchd` starts and executes boot tasks. -3. `mount-phase-1`, `mount-phase-2`, `finish-restore`, `init-with-data-volume`, - `keybag`, `usermanagerd` tasks are reached. -4. Log shows: - - `Early boot complete. Continuing system boot.` - - `hello from launchdhook.dylib` / `bye from launchdhook.dylib` - -So the pipeline already crossed into JB userspace initialization. - -### 11.2 Suspicious signals in this run - -1. Early `launchd` assertion: - - `com.apple.xpc.launchd ... assertion failed ... 0xffffffffffffffff` -2. Ignition warning: - - `libignition: cryptex1 sniff: ignition failed: 8` - - then fallback path continues (`ignition disabled`) and boot tasks proceed. -3. `vphoned` host side repeatedly reports: - - `Connection reset by peer` - - indicates daemon channel is not yet stable/ready during this phase. - -### 11.3 Most likely fault domain (ranked) - -1. **JB-1 launchd modification path (highest probability)**: - - `patch-launchd-jetsam` dynamic branch rewrite may select an incorrect - conditional in some launchd builds. - - `inject-dylib /cores/launchdhook.dylib` adds early runtime side effects. - - The assertion appears in `launchd` startup window, matching this stage. -2. **JB hook/runtime environment coupling**: - - `JB_ROOT_PATH` and BaseBin hook expectations under preboot hash path. - - If path/state is incomplete, startup can degrade without immediate kernel panic. -3. **Less likely: kernel B9+ regression** - - Current default already excludes B9+ and this log clearly reaches deep - userspace boot tasks, so this symptom class is different from earlier - watchdog/restore-gate failures. - -### 11.4 Practical triage to confirm - -Use same restored disk and isolate JB userspace phases: - -1. Baseline control: - - Boot with dev/regular userspace flow (no JB-1 launchd dylib injection). -2. Re-enable only JB-1: - - apply jetsam patch alone first, then add dylib injection. -3. Add JB-2/JB-3 incrementally: - - procursus bootstrap, then BaseBin hooks. -4. Capture first regression point and lock to the exact phase. - -### 11.5 Conclusion for this report - -- Current symptom ("restore completes but cannot fully start") is now best - modeled as a **post-restore userspace startup regression**, centered around - JB launchd modification/hook stages, not the previous kernel early-boot hang. - ---- - -## 12) Failed vs Successful Boot Log Comparison (same device class, 2026-03-05) - -Compared inputs: - -- Failing side: `vphone-cli` (startup-hang-fix branch) user-provided `make boot` log. -- Successful side: `vphone-cli-dev` (main) user-provided `make boot` log. - -### 12.1 Signals that appear in both logs (low-priority/noise for this issue) - -The following lines appear on the successful boot too, so they are unlikely to -be the direct blocker for "cannot fully start": - -1. `apfs_find_named_root_snapshot_xid ... No such file or directory (2)` -2. `TXM [Error]: selector: 45 | 78` and `failed to set boot uuid ... 78` -3. `libignition ... cryptex1 sniff: ignition failed: 8` then `ignition disabled` -4. `MKB_INIT: No system keybag found on filesystem.` -5. `mount: failed to migrate Media Keys, error = c002` -6. `Overprovision setup failed ... Ignoring...` - -These are therefore weak root-cause candidates for this specific regression. - -### 12.2 Differential signals (high-value) - -Only/primarily observed in failing run: - -1. Early `launchd` assertion: - - `assertion failed ... launchd + 59944 ... 0xffffffffffffffff` -2. JB launchd hook footprint: - - `hello from launchdhook.dylib` - - `set JB_ROOT_PATH = /private/preboot//jb-vphone/procursus` -3. Host control channel never stabilizes to a healthy daemon session - before manual stop (`Connection reset by peer` keeps repeating). - -Observed in successful run (and absent in failing excerpt): - -1. Host eventually reaches: - - `[control] connected to vphoned v1 ...` - - `[control] pushing update ...` -2. No corresponding early `launchd` assertion line in provided success log. - -### 12.3 Most likely causes (ranked by differential evidence) - -1. **`patch-launchd-jetsam` dynamic hit risk (top suspect)** - The patcher selects a conditional branch dynamically using string xref + - backward window + return-block heuristic. A wrong branch rewrite can produce - launchd internal assertion failures while still allowing partial boot-task logs. - -2. **`launchd` dylib injection (`/cores/launchdhook.dylib`) side effects** - Hook runs very early in launchd lifecycle; if environment/setup assumptions - are not met, boot can degrade without immediate kernel panic. - -3. **JB-1 combined effect (jetsam patch + dylib injection), not kernel B9+** - Kernel path already reaches deep userspace tasks in both cases; this no longer - matches the previous watchdog/restore-gate kernel hang signature. - -### 12.4 Recommended isolation sequence (to convert suspicion -> proof) - -Use same restored disk, only vary JB-1 components: - -1. `launchd` unmodified control. -2. Apply jetsam patch only. -3. Apply dylib injection only. -4. Apply both (current JB-1). - -Record for each: - -- whether `launchd assertion failed ... 0xffffffffffffffff` appears -- whether `[control] connected to vphoned v1` appears -- time to first stable userspace service set. diff --git a/research/boot_jb_mount_failure_investigation.md b/research/boot_jb_mount_failure_investigation.md deleted file mode 100644 index fc5c309..0000000 --- a/research/boot_jb_mount_failure_investigation.md +++ /dev/null @@ -1,223 +0,0 @@ -# JB Mount Failure Investigation (2026-03-04) - -## Symptom - -- `make setup_machine JB=1` reached `cfw_install_jb` and failed at: - - `Failed to mount /dev/disk1s1 at /mnt1 (opts=rw).` - -## Runtime Evidence (Normal Boot) - -From `make boot` serial log: - -- APFS mount tasks fail with permission errors: - - `mount_apfs: volume could not be mounted: Operation not permitted` - - `mount: /private/xarts failed with 77` - - `mount: /private/preboot failed with 77` - - launchd panics: `boot task failure: mount-phase-1 - exited due to exit(77)` -- Ignition/boot path shows entitlement-like failure: - - `handle_get_dev_by_role:13101: disk1s1 This operation needs entitlement` - -This indicates failure in APFS role-based device lookup during early boot mount tasks. - -## Runtime Evidence (DEV Control Run, 2026-03-04) - -From a separate `fw_patch_dev + cfw_install_dev` boot log (not JB): - -- `mount-phase-1` succeeded for xART: - - `disk1s3 mount-complete volume xART` - - `/dev/disk1s3 on /private/xarts ...` -- launch progressed to: - - `data-protection` - - `finish-obliteration` - - `detect-installed-roots` - - `mount-phase-2` - -Interpretation: APFS boot-mount path can work on this build/kernel family after recent APFS gate changes. -This does **not** prove JB flow is fixed; it is a control signal showing the kernel-side path is not universally broken. - -## Flow Separation (Critical) - -- The successful `xART mount-complete` / `mount-phase-2` log is from DEV pipeline: - - `fw_patch_dev` + `cfw_install_dev` -- JB pipeline remains: - - `fw_patch_jb` + `cfw_install_jb` -- `cfw_install_jb` does **not** call `cfw_install_dev`; it runs base `cfw_install.sh` first, then JB-only phases. - -## Kernel Artifact Checks - -### 1) Ramdisk kernel identity - -- `vm/Ramdisk/krnl.img4` payload hash was byte-identical to: - - `vm/iPhone17,3_26.1_23B85_Restore/kernelcache.research.vphone600` - -So ramdisk boot was using the same restore kernel payload (no accidental file mismatch in `ramdisk_build`). - -### 2) Patchability state (current VM kernel) - -On `vm/iPhone17,3_26.1_23B85_Restore/kernelcache.research.vphone600`: - -- Base APFS patches: - - `patch_apfs_vfsop_mount_cmp` -> not patchable (already applied) - - `patch_apfs_mount_upgrade_checks` -> not patchable (already applied) -- Key JB patches: - - `patch_mac_mount` -> patchable - - `patch_dounmount` -> patchable - - `patch_kcall10` -> patchable - -Interpretation: kernel is base-patched, but critical JB mount/syscall extensions are still missing. - -### 3) Reference hash comparison - -- CloudOS source `kernelcache.research.vphone600` payload: - - `b6846048f3a60eab5f360fcc0f3dcb5198aa0476c86fb06eb42f6267cdbfcae0` -- VM restore kernel payload: - - `b0523ff40c8a08626549a33d89520cca616672121e762450c654f963f65536a0` - -So restore kernel is modified vs source, but not fully JB-complete. - -## IDA Deep-Dive (APFS mount-phase-1 path) - -### 1) Failing function identified - -- APFS function: `sub_FFFFFE000948EB10` (log name: `handle_get_dev_by_role`) -- Trigger string in function: - - `"%s:%d: %s This operation needs entitlement\\n"` (line 13101) -- Caller xref: - - `sub_FFFFFE000947CFE4` dispatches to `sub_FFFFFE000948EB10` - -### 2) Gate logic at failure site - -The deny path is reached if either check fails: - -- Context gate: - - `BL sub_FFFFFE0007CCB994` - - `CBZ X0, deny` -- "Entitlement" gate (APFS role lookup privilege gate): - - `ADRL X1, "com.apple.apfs.get-dev-by-role"` - - `BL sub_FFFFFE000940CFC8` - - `CBZ W0, deny` -- Secondary role-path gate (role == 2 volume-group path): - - `BL sub_FFFFFE000817C240` - - `CBZ W0, deny` (to line 13115 block) - -The deny block logs line `13101` and returns failure. - -### 3) Patch sites (current vphone600 kernelcache) - -- File offsets: - - `0x0248AB50` — context gate branch (`CBZ X0, deny`) - - `0x0248AB64` — role-lookup privilege gate (`CBZ W0, deny`) - - `0x0248AC24` — secondary role==2 deny branch (`CBZ W0, deny`) -- All three patched to `NOP` in the additive APFS patch. - -### 4) Additional APFS EPERM(1) return paths in `apfs_vfsop_mount` - -Function: - -- `sub_FFFFFE0009478848` (`apfs_vfsop_mount`) - -Observed EPERM-relevant deny blocks: - -- Root-mount privilege deny: - - log string: `"%s:%d: not allowed to mount as root\n"` - - xref site: `0xFFFFFE000947905C` - - error return: sets `W25 = 1` -- Verification-mount privilege deny: - - log string: `"%s:%d: not allowed to do a verification mount of %s (is_suser %s ; uid %d)\n"` - - xref site: `0xFFFFFE0009479CA0` - - error return: sets `W25 = 1` - -Important relation to existing Patch 13: - -- At `0xFFFFFE0009479044` (same function), current code is `CMP X0, X0` (patched form), - which forces the following `B.EQ` path and should bypass one root privilege check in this region. -- Therefore, if JB still reports `mount_apfs ... Operation not permitted`, remaining EPERM candidates - include other deny branches (including the verification-mount gate path above), not only `handle_get_dev_by_role`. - -## Root Cause (Updated, Two-Stage) - -Stage 1 (confirmed and mitigated): - -- APFS `handle_get_dev_by_role` entitlement/role deny gates were a concrete mount-phase-1 blocker. -- Additive patch now NOPs all three relevant deny branches. - -Stage 2 (still under investigation, JB-only): - -- DEV control run can pass `mount-phase-1`/`mount-phase-2`. -- JB failures must be analyzed with JB-only artifacts/logs and likely involve JB-only deltas - (launchd dylib injection, BaseBin hooks, or JB preboot/bootstrap interaction), in addition to any remaining kernel checks. - -## Mitigation Implemented - -### A) Ramdisk kernel split (updated implementation) - -- `scripts/fw_patch_jb.py` - - no longer creates a ramdisk snapshot file -- `scripts/ramdisk_build.py` - - derives ramdisk kernel source internally: - - uses legacy `kernelcache.research.vphone600.ramdisk` if present - - otherwise derives from pristine CloudOS `kernelcache.research.vphone600` - under `ipsws/*CloudOS*/` using base `KernelPatcher` - - builds: - - `Ramdisk/krnl.ramdisk.img4` from derived/base source - - `Ramdisk/krnl.img4` from post-JB restore kernel -- `scripts/ramdisk_send.sh` - - prefers `krnl.ramdisk.img4` when present. - -### B) Additive APFS boot-mount gate bypass (new) - -- Added new base kernel patch method: - - `KernelPatchApfsMountMixin.patch_apfs_get_dev_by_role_entitlement()` -- Added to base kernel patch sequence in `scripts/patchers/kernel.py`. -- Behavior: - - NOPs three deny branches in `handle_get_dev_by_role` - - does not modify existing filesystem patches (APFS snapshot/seal/graft/mount/sandbox hooks remain unchanged). - -### C) JB-only differential identified (for next isolation) - -Compared with DEV flow, JB adds unique early-boot risk factors: - -- launchd binary gets `LC_LOAD_DYLIB` injection for `/cores/launchdhook.dylib` -- `launchdhook.dylib`/BaseBin environment strings include: - - `JB_ROOT_PATH` - - `JB_TWEAKLOADER_PATH` - - explicit launchdhook startup logs (`hello` / `bye`) -- procursus/bootstrap content is written under preboot hash path (`/mnt5//jb-vphone`) - -These do not prove causality yet, but they are the primary JB-only candidates after Stage-1 APFS gate mitigation. - -## Next Validation - -1. Kernel/JB isolation run (requested): - - `make fw_patch_jb` - - `make ramdisk_build` - - `make ramdisk_send` - - run `cfw_install_dev` (not JB) on this JB-patched firmware baseline -2. Compare normal boot result: - - If `mount-phase-1/2` succeeds: strong evidence issue is in JB-only userspace phases. - - If it still fails with `EPERM`: continue kernel/APFS deny-path tracing. -3. If step 2 succeeds, add back JB phases incrementally: - - first JB-1 (launchd inject + jetsam patch) - - then JB-2 (preboot bootstrap) - - then JB-3 (BaseBin hooks) - and capture first regression point. - -## 2026-03-05 Follow-up (Data-Protection / SEP UserClient MACF) - -A later failure mode moved past mount-phase and failed in `data-protection`: - -- `IOUC AppleSEPUserClient failed MACF ... seputil` -- `Boot task failed: data-protection - exited due to exit(60)` - -This was traced to unpatched IOKit MAC policy hook range (`ops[201..210]`) in -the sandbox extended hook set. Mitigation and patch details are documented in: - -- `research/boot_data_protection_seputil_macf_investigation.md` - -Follow-up (2026-03-06): - -- Even after `ops[201..210]` extension, runtime still showed: - - `IOUC AppleAPFSUserClient failed MACF ...` - - `IOUC AppleSEPUserClient failed MACF ...` -- A second-stage mitigation was added: - - `patch_iouc_failed_macf` (central IOUC MACF gate low-risk early return). diff --git a/research/boot_launchd_data_volume_lookup_analysis.md b/research/boot_launchd_data_volume_lookup_analysis.md deleted file mode 100644 index 20af586..0000000 --- a/research/boot_launchd_data_volume_lookup_analysis.md +++ /dev/null @@ -1,160 +0,0 @@ -# launchd Boot Sequence Analysis (mount-phase-1 / data volume lookup) - -Date: 2026-03-06 -Target binary in IDA: launchd (Darwin Bootstrapper 7.0.0, libxpc_executables-3089.42.1~6) - -## Scope - -This analysis focuses on the log segment: - -- `mount-phase-1` -- `failed to lookup data volume - Attribute not found` -- `mount: data volume missing, but not required in env: 1` - -and answers where launchd is involved vs where `mount`/APFS is actually failing. - -## Ground Truth from launchd (IDA) - -### 1) Boot task source is embedded plist in launchd - -- Embedded config plist string contains: - - `Boot` dictionary - - `mount-phase-1` -> `Program=/sbin/mount`, `ProgramArguments=["mount","-P","1"]` - - `data-protection` -> `Program=/usr/libexec/init_data_protection`, `CSIdentityOverride=com.apple.seputil` - -### 2) Boot task dictionary wiring - -- `sub_10004D978`: - - reads launchd `__TEXT,__config` - - extracts `Boot` dictionary - - stores into `qword_10007F138` - -- `sub_100047B94`: - - lookup task from `qword_10007F138` - - runs gate check (`sub_100047C8C`) - - executes task by calling `sub_100047DD0` - -### 3) Boot sequence order (relevant slice) - -- `start` -> `sub_1000489A4` -> async `sub_100048590` -- `sub_100048590` task order includes: - - `mount-phase-1` - - `data-protection` - - `finish-obliteration` - - `detect-installed-roots` - - `mount-phase-2` - -### 4) Task execution and failure handling - -- `sub_100047DD0`: - - logs `Doing boot task` - - `posix_spawnp()` actual executable - - for non-async tasks waits via `sub_100049180` - -- `sub_100049180`: - - checks exit status - - if `RequireSuccess=true` and exit is failure -> calls `sub_100048D0C` - -- `sub_100048D0C`: - - logs `Boot task failed: %s` - - logs `Panicking in 3 seconds.` - - then panics - -## Key Conclusion: where the quoted error originates - -`failed to lookup data volume - Attribute not found` is **not** generated by launchd internals. - -In this path, launchd is only the orchestrator: - -1. launchd runs `/sbin/mount -P 1` for `mount-phase-1` -2. `mount` prints `failed to lookup data volume...` -3. launchd only sees child exit result, then decides whether to panic based on `RequireSuccess` - -So this error’s primary fault domain is in `mount` + APFS/IOKit interactions, not in launchd task scheduler code. - -## `mount -P 1` call chain (IDA-verified) - -Target binary: `research/artifacts/launchd_23B85/mount.from_vm_disk.current` - -- `start` @ `0x100003DC8` - - phase path calls `sub_100003480(&env)` then `sub_100003674()` -- `sub_100003480` @ `0x100003480` - - reads `IODeviceTree:/filesystems/fstab` property `os_env_type` - - calls `APFSContainerGetBootDevice(&CFString)` - - builds `/dev/` string in global buffer -- `sub_100003674` @ `0x100003674` - - calls `APFSVolumeRoleFind(, 0x40, &CFArray)` - - on non-zero return: - - `fprintf("%sfailed to lookup data volume - %s\n", ..., strerror(ret & 0x3fff))` - - if single match, converts CFString -> data volume path - -Important: - -- `0x40` is the queried role selector in this build. -- `Attribute not found` corresponds to `ENOATTR` (`93`) after `ret & 0x3fff`. -- phase-1 can continue with warning, but this often cascades into `data-protection` failure in your failing trace set. - -## Updated Causality (with new control evidence) - -User-provided control result: - -- `cfw_install` (without JB extras) reproduces the same failure. -- TXM path is known-good in this setup. - -Implication: - -- JB-only userspace deltas are no longer primary suspects for this error. -- Current highest-confidence differentiator is kernel state/patch delta. - -## Plausible causes (re-ranked) - -### A. kernel-side causes (highest probability now) - -1. APFS role/device lookup path is denied/altered by kernel policy path for `mount` (`IOUC AppleAPFSUserClient ... MACF` class of failure). -2. Kernel APFS patch interaction causes role metadata read path to return "attribute not found". -3. Kernel patch ordering or overlap in `fw_patch_jb` modifies behavior that minimal/non-JB flow does not. - -### B. mount userspace path causes (still relevant, but secondary to A) - -1. `mount -P 1` phase logic expects Data role metadata that is unavailable under current kernel behavior. -2. Userspace APFS query path receives transformed errno/status from kernel and prints attribute-missing message. - -### C. launchd-level causes (currently de-prioritized) - -1. launchd task definition mismatch. -2. spawn-level failures before mount logic. -3. task gating differences. - -These are less consistent with the new control result and with observed mount-origin log text. - -## Practical meaning - -- For this failure, launchd reverse already gives enough certainty that launchd is orchestrator only. -- Next decisive work should move to APFS userspace API return-site tracing and corresponding kernel handlers. -- Detailed `mount -P 1` failure/hang matrix is documented in: - - `research/boot_mount_phase1_failure_matrix.md` - -## Most likely kernel-side silent-fail line (current ranking) - -1. `APFSVolumeRoleFind` path reaches APFS userclient method and gets transformed deny/error (most consistent with `IOUC AppleAPFSUserClient failed MACF ... mount` history). -2. Base APFS entitlement bypass patch (`patch_apfs_get_dev_by_role_entitlement`, patch #16) matched the wrong deny branch or altered control flow for role lookup. -3. Base sandbox-op stubs (mount/vnode related) hit an unintended target due ops-table drift. - -Lower probability for this exact string: - -- launchd plist/task ordering itself -- fstab format/ramdisk missing (would produce different dominant signatures) - -## Artifact notes - -Current extracted launchd sample (for reproducible local reference): - -- `research/artifacts/launchd_23B85/launchd.from_vm_disk.current` - - sha256: `411d730c95d99a088e94b673eff3fa73d6d3cc778b24b476cd0b7866cd037443` -- `research/artifacts/launchd_23B85/launchd.plist.from_vm_disk.current` - - sha256: `dc972e30220b3e9e8323d23ce4a4737d849893dd79e305693de902ff65ddacab` - -Observed in this sample: - -- no `/cores/launchdhook.dylib` load command -- launchd embedded boot task plist present and readable diff --git a/research/boot_launchdhook_assertion_handoff_20260306.md b/research/boot_launchdhook_assertion_handoff_20260306.md deleted file mode 100644 index 6411b6b..0000000 --- a/research/boot_launchdhook_assertion_handoff_20260306.md +++ /dev/null @@ -1,254 +0,0 @@ -# Launchdhook Assertion Handoff (2026-03-06) - -## Scope - -This note captures the current userspace-side findings for the failing `fw_patch_jb + cfw_install_jb` path. -It is intended as a handoff artifact for follow-up work on the `fix-boot` branch. - -The current symptom is no longer "launchd does not start". -The updated symptom is: - -- `launchd` starts -- injected `launchdhook.dylib` definitely loads -- `launchd` then hits an early internal assertion before the expected `bash` / follow-on job chain stabilizes - -## Executive Summary - -### Confirmed - -- The original JB `LC_LOAD_DYLIB /cores/launchdhook.dylib` approach is structurally unsafe on the current `launchd` sample because there is not enough load-command slack. -- A short-path alias experiment fixed the Mach-O header-space problem: - - `/cores/launchdhook.dylib` requires 56 bytes and overruns into `__TEXT,__text` - - `/cores/b` still requires 40 bytes and also overruns - - `/b` requires 32 bytes and fits exactly after removing `LC_CODE_SIGNATURE` -- Runtime test with `/b` proves the short-path alias loads successfully, but the main failure remains: - - `launchdhook.dylib` prints its startup logs - - `launchd` then asserts early: `launchd + 59944 ... 0xffffffffffffffff` - -### Current conclusion - -The short-path `/b` alias fixes the **injection-space** problem, but does **not** fix the **launchd assertion**. -So the remaining problem is now more likely in the hook logic (especially early XPC / daemon config hooks) than in the raw load-command insertion path. - -## Evidence Collected - -### 1. Mach-O injection space audit - -Local dry-run against `vm/.cfw_temp/launchd` established the following: - -- Existing load-command slack before the first section: 16 bytes -- After stripping `LC_CODE_SIGNATURE`: 32 bytes -- Required command sizes: - - `/cores/launchdhook.dylib` -> 56 bytes - - `/cores/b` -> 40 bytes - - `/b` -> 32 bytes - -Observed effect of the original long-path injection: - -- `LC_LOAD_DYLIB /cores/launchdhook.dylib` overwrote the beginning of `__TEXT,__text` -- first instructions at the start of the text section were replaced by injected path bytes - -Observed effect of the short-path injection: - -- `LC_LOAD_DYLIB /b` fits exactly in the available 32 bytes after `LC_CODE_SIGNATURE` removal -- no additional overwrite into `__TEXT,__text` is needed for that path - -### 2. Device-side mount and payload verification - -Inside ramdisk shell, manual mount and inspection showed: - -- `/dev/disk1s1` mounted at `/mnt1` -- `/dev/disk1s5` mounted at `/mnt5` -- `/mnt1/b` exists and is a Mach-O dylib -- `/mnt1/cores/launchdhook.dylib` exists and is a Mach-O dylib -- `/mnt1/cores/systemhook.dylib` and `/mnt1/cores/libellekit.dylib` are also present - -Important clarification: - -- `/.b` is an existing hidden root directory on this filesystem and is unrelated to the alias experiment -- the experiment path is `/b`, not `/.b` - -### 3. Runtime serial log after switching to `/b` - -The following lines appeared during boot: - -- `set JB_ROOT_PATH = /private/preboot//jb-vphone/procursus` -- `=========== hello from launchdhook.dylib ===========` -- `=========== bye from launchdhook.dylib ===========` -- `com.apple.xpc.launchd ... assertion failed: ... launchd + 59944 ... 0xffffffffffffffff` - -Interpretation: - -- `/b` injection is working -- `launchdhook.dylib` is loaded and runs its initializer path -- the failure is no longer attributable to the long path not loading or to the Mach-O injection missing outright - -## Source-Backed Analysis from Dopamine BaseBin - -Source tree used: - -- `/Users/qaq/Documents/GitHub/Dopamine/BaseBin` - -### 1. launchdhook initialization order - -From `Dopamine/BaseBin/launchdhook/src/main.m`, the constructor initializes hooks in this order: - -1. `initXPCHooks();` -2. `initDaemonHooks();` -3. `initSpawnHooks();` -4. `initIPCHooks();` -5. `initJetsamHook();` - -This matters because the current assertion happens very early, after `launchdhook` has definitely run. -That makes the earlier hooks higher-priority suspects than spawn-time behavior. - -### 2. What `initDaemonHooks()` actually does - -From `Dopamine/BaseBin/launchdhook/src/daemon_hook.m`: - -- hooks `xpc_dictionary_get_value` -- rewrites behavior for these keys: - - `LaunchDaemons` - - `Paths` - - `com.apple.private.xpc.launchd.userspace-reboot` - -Behavior summary: - -- appends jailbreak daemon plist entries from: - - `JBROOT_PATH("/basebin/LaunchDaemons")` - - `JBROOT_PATH("/Library/LaunchDaemons")` -- appends those same directories to `Paths` -- conditionally returns `com.apple.private.iowatchdog.user-access` when `userspace-reboot` is false/missing - -This hook touches exactly the kind of launchd configuration objects that are consulted during early daemon/bootstrap setup. - -### 3. What `initSpawnHooks()` actually does - -From `Dopamine/BaseBin/launchdhook/src/spawn_hook.c`: - -- hooks `__posix_spawn` -- during early boot, it intentionally avoids broad injection until `xpcproxy` appears -- once `xpcproxy` is seen, it flips out of early-boot mode and uses `posix_spawn_hook_shared(...)` - -Interpretation: - -- spawn hook is real, but it is comparatively later than the daemon config hook -- given the current assertion timing, `initSpawnHooks()` is no longer the top suspect - -### 4. What `initXPCHooks()` does - -From `Dopamine/BaseBin/launchdhook/src/xpc_hook.c`: - -- hooks `xpc_receive_mach_msg` -- participates in jbserver message handling and filtering inside launchd/XPC path - -This is also an early-launchd hook and remains a second-tier suspect if daemon-hook isolation does not clear the assertion. - -### 5. Runtime jetsam hook vs our static jetsam patch - -From `Dopamine/BaseBin/launchdhook/src/jetsam_hook.c`: - -- Dopamine also installs a runtime hook on `memorystatus_control` -- this is separate from the repo's static `scripts/patchers/cfw_patch_jetsam.py` binary patch - -Therefore two different "jetsam" mechanisms now exist in the failing path: - -- static launchd branch patch -- runtime `memorystatus_control` hook - -This does not prove either is the current cause, but it means the term "jetsam patch" must be disambiguated in future debugging. - -## Current Suspect Ranking - -### Highest probability - -1. **`initDaemonHooks()` / `daemon_hook.m`** - - hooks `xpc_dictionary_get_value` - - mutates `LaunchDaemons` and `Paths` - - timing matches the observed early `launchd` assertion better than spawn-time logic - -### Medium probability - -2. **`initXPCHooks()` / `xpc_hook.c`** - - also runs before spawn hook - - directly changes launchd/XPC message handling - -3. **static `patch-launchd-jetsam` matcher** - - still considered risky because its matching strategy is heuristic and not CFG-constrained - - but the `/b` experiment shows the assertion survives after fixing the obvious load-command overflow issue - -### Lower probability for the current symptom timing - -4. **`initSpawnHooks()` / `spawn_hook.c`** - - still relevant for later `bash` / job launch failures - - but no longer the best first suspect for the early `launchd + 59944` assertion - -## Recommended Isolation Order for `fix-boot` - -### Stage 1: no-daemon-hook control - -Goal: - -- keep `launchdhook.dylib` loading -- keep `/b` short-path alias experiment in place -- disable only `initDaemonHooks()` - -Reason: - -- this is the cleanest test of the current top suspect -- if the assertion disappears, the root issue is inside `daemon_hook.m` - -### Stage 2: no-xpc-hook control - -If stage 1 still asserts: - -- restore daemon hook or keep it off, but disable `initXPCHooks()` next -- test whether the assertion is tied to XPC receive hook path instead - -### Stage 3: no-spawn-hook control - -Only after stages 1 and 2: - -- disable `initSpawnHooks()` -- use this to isolate later `bash` / child-process failures if the launchd assertion is already gone or moves later - -### Stage 4: revisit static launchd jetsam patch - -If all runtime-hook controls still fail: - -- re-audit `scripts/patchers/cfw_patch_jetsam.py` -- prefer a source-backed or CFG-backed site selection instead of the current backward-scan heuristic - -## Concrete Handoff Notes for Claude - -### Facts - -- `/b` injection is confirmed working on-device -- `launchdhook.dylib` definitely runs -- launchd still asserts at `launchd + 59944` -- Dopamine source confirms `initDaemonHooks()` runs before `initSpawnHooks()` - -### Inference - -- the early assertion is more likely to be caused by `daemon_hook.m` or `xpc_hook.c` than by `spawn_hook.c` - -### Best next change - -Implement a **minimal no-daemon-hook build** first: - -- edit `Dopamine/BaseBin/launchdhook/src/main.m` -- temporarily disable only `initDaemonHooks();` -- rebuild `launchdhook.dylib` -- keep `/b` alias loading strategy unchanged for the control run - -## Related Files - -- `scripts/cfw_install_jb.sh` -- `scripts/patchers/cfw_inject_dylib.py` -- `scripts/patchers/cfw_patch_jetsam.py` -- `research/boot_jb_mount_failure_investigation.md` -- `research/boot_hang_b19_mount_dounmount_strategy_compare.md` -- `Dopamine/BaseBin/launchdhook/src/main.m` -- `Dopamine/BaseBin/launchdhook/src/daemon_hook.m` -- `Dopamine/BaseBin/launchdhook/src/spawn_hook.c` -- `Dopamine/BaseBin/launchdhook/src/xpc_hook.c` diff --git a/research/boot_log_fail_vs_success_analysis.md b/research/boot_log_fail_vs_success_analysis.md deleted file mode 100644 index a6ce8a9..0000000 --- a/research/boot_log_fail_vs_success_analysis.md +++ /dev/null @@ -1,107 +0,0 @@ -# Boot Log Comparison Analysis (fail vs success) - -Date: 2026-03-06 -Scope: compare `/Users/qaq/Desktop/boot.fail.log` and `/Users/qaq/Desktop/boot.success.log` for the current startup failure investigation. - -## Executive Verdict - -- The fail path is a `launchd` userspace panic caused by `data-protection` task `SIGTRAP`, not an APFS kernel panic. -- The immediate trigger in fail log is that mount could not find the APFS Data volume metadata: - - `failed to lookup data volume - Attribute not found` - - `mount: data volume missing, but not required in env: 1` -- APFS itself does load in both logs with the same version (`2632.40.15`) and continues mounting volumes. -- Based on these two logs alone, evidence is stronger for "Data volume discovery/mapping issue" than "APFS patch count too high". - -## Key Evidence - -1. APFS module load is healthy in both runs - -- Fail: `apfs_module_start ... com.apple.filesystems.apfs, v2632.40.15` (line 178) -- Success: same APFS load/version (line 250) -- Interpretation: no direct sign that APFS kext fails to initialize in fail run. - -2. First hard divergence is in mount-phase-1 Data volume resolution - -- Fail: - - `failed to lookup data volume - Attribute not found` (line 420) - - `mount: data volume missing, but not required in env: 1` (line 421) -- Success: - - `mount: found boot container: /dev/disk1, data volume: /dev/disk1s2 env: 1` (line 423) -- Interpretation: fail run cannot resolve data volume metadata; success run can. - -3. data-protection task outcome differs immediately after that - -- Fail: - - `(data-protection) : exited due to SIGTRAP` (line 432) - - `Boot task failed: data-protection - exited due to SIGTRAP` (line 433) - - `userspace panic` follows (line 458) -- Success: - - `init_data_protection: Gigalocker initialization completed` (line 434) - - boot continues into `mount-phase-2` and beyond (line 502+) -- Interpretation: the crash is in boot task flow after data volume lookup failure, not in APFS module load. - -4. Success path shows APFS warnings that are non-fatal - -- `mount: failed to migrate Media Keys, error = c002` (line 522) -- `mount_phase_two ... Overprovision setup failed ... Ignoring...` (line 560) -- Interpretation: APFS/AKS warnings can be tolerated when data volume path is intact; these are not the blocking condition here. - -## Additional Differences That Confound Direct "Patch Count" Attribution - -- Different host build/hash inputs: - - vphoned `GIT_HASH` differs (`e4456e9` vs `fd08c43`) - - binary path differs (`vphone-cli` vs `vphone-cli-dev`) - - `vphoned` signed hash differs -- Different device identity: - - ECID differs across logs -- Different APFS checkpoint state: - - Fail: `cleanly-unmounted`, largest xid `198` - - Success: `reloading after unclean unmount`, largest xid `491` - -These differences mean this is not a strict A/B test of only "APFS patch count". - -## Assessment of "APFS patch applied too much?" - -Current confidence: low-to-medium for that hypothesis from logs alone. - -What logs support: - -- The failure does involve APFS mount phase and data-protection. - -What logs do not support: - -- No APFS module crash/oops/panic. -- No explicit APFS patch integrity failure. -- The strongest fail signal is missing data volume attribute, not APFS code-path abort. - -More likely from current evidence: - -- APFS container/volume role metadata mismatch, or -- environment/image drift between the two runs, causing different boot task assumptions. - -## Suggested Next Validation (minimal and decisive) - -1. Re-run with identical binaries and same VM snapshot, toggling only APFS-related patch set. -2. Capture APFS volume-role metadata before boot task (expect `disk1s2` Data role to be discoverable). -3. Compare generated firmware/CFW artifacts checksums between fail/success pipelines. -4. If failure reproduces only with APFS patch delta, then bisect APFS patch subset around data-volume lookup path. - -## Bottom Line - -From these two logs, the actionable breakpoint is: - -- "data volume lookup failed" -> "data-protection SIGTRAP" -> userspace panic. - -This is a stronger lead than "APFS patch count over-applied", and should be the first branch to validate. - -## Update (Control Run) - -New control signal from user: - -- Same failure reproduces with `cfw_install` (without JB extras). -- TXM is known working in this control. - -Updated implication: - -- The prior "JB userspace difference" suspicion should be de-prioritized. -- Current primary suspect becomes kernel delta (especially APFS/IOUC/MACF-related behavior under `mount -P 1`). diff --git a/research/boot_mount_phase1_failure_matrix.md b/research/boot_mount_phase1_failure_matrix.md deleted file mode 100644 index 9436861..0000000 --- a/research/boot_mount_phase1_failure_matrix.md +++ /dev/null @@ -1,195 +0,0 @@ -# mount `-P 1` Failure / Hang Matrix (IDA) - -Date: 2026-03-06 -Target: `research/artifacts/launchd_23B85/mount.from_vm_disk.current` - -## What `mount -P 1` actually does - -Main entry: `start` @ `0x100003DC8` - -For `-P 1` (phase-1): - -1. Parse phase from `-P` and set global `dword_1000101F4 = 1`. -2. Call `setfsent()` and iterate fstab entries. -3. Resolve boot container/device via `sub_100003480` (`APFSContainerGetBootDevice`). -4. Resolve data volume via `sub_100003674` (`APFSVolumeRoleFind`). -5. Print either: - - `mount: found boot container: ..., data volume: ..., env: ...` - - or `mount: data volume missing, but not required in env: ...` -6. Continue mounting entries with pass number == phase via `sub_1000045B0` (exec `mount_`). - -Important: for phase-1, missing data volume is normally **not fatal**. - -### Critical implementation detail (IDA) - -- `sub_100003674` calls: - - `APFSVolumeRoleFind(, 0x40, &outArray)` -- If return != 0: - - prints `failed to lookup data volume - %s` with `strerror(ret & 0x3fff)` -- `Attribute not found` in your log maps to Darwin `ENOATTR(93)`. - -### Important caveat on `93` provenance - -- Confirmed fact: - - `mount` sees `APFSVolumeRoleFind` return value whose low 14 bits are `93`. -- Not yet proven: - - that kernel returns `93` directly. -- Also plausible: - - APFS userspace layer maps another kernel/APFS status to `ENOATTR` before returning to `mount`. - -## All plausible causes for phase-1 fail/hang - -### A) Early argument / mode errors (immediate fail) - -1. Invalid `-P` value - - message: `-P flag requires a valid mount phase number` - - location: `start` (`0x1000039C4`..`0x100003A70`) - -2. Invalid invocation shape (bad argv combination) - - falls into usage path (`sub_1000043B0`) - -### B) Boot container / APFS role lookup path - -1. Cannot read filesystem info from IORegistry (`os_env_type`) - - message: `failed to get filesystem info` - - function: `sub_100003480` - -2. `APFSContainerGetBootDevice` failure - - message: `failed to get boot device - ...` (with retry loop outside restore env) - - function: `sub_100003480` - -3. `APFSVolumeRoleFind` failure - - message: `failed to lookup data volume - ...` - - function: `sub_100003674` - -4. Multiple Data volumes found - - message: `found multiple data volumes` - - function: `sub_100003674` - -Note: - -- phase-1 usually continues after (3)/(4). -- phase-2 has stricter fatal behavior on missing data volume in env=1. - -### C) fstab traversal / entry filtering issues - -1. `setfsent()` failure - - message: `mount: can't get filesystem checklist` - - fatal for phase run - -2. Entry type / spec/path invalidity - - examples: - - `%s: invalid special file or file system.` - - `%s: unknown special file or file system.` - - `You must specify a filesystem type with -t.` - -These are input/config failures before actual fs-specific helper mount. - -### D) Per-filesystem mount helper failures (major phase-1 failure source) - -Dispatcher: `sub_1000045B0` - -1. FSKit path failure (`sub_100000BC0`) - - messages: - - `File system named %s not found` - - `File system named %s unable to mount` - - `FSKit unavailable` - -2. `fork()` / `waitpid()` / child process control failures - - messages include wait/fork warnings in helper path - -3. `exec` failure for `mount_` helpers - - tries `/sbin/mount_` then fallback paths under `/System/Library/Filesystems/...` - - if all fail -> returns mapped failure code - -4. Helper exits non-zero or gets signaled - - parent treats as mount failure and propagates code - -This bucket is the most common direct reason phase-1 exits non-zero. - -### E) Ramdisk special path failures (if ramdisk entry is hit in phase-1) - -Ramdisk path: `sub_100002688` + `sub_100002C34` + command wrapper `sub_100002EA4` - -Possible failures: - -1. preflight format/option parsing fail (`Ramdisk fstab not in expected format.`) -2. `mount_tmpfs` exec or command-run failures -3. copyfile / final mount / umount failures - -Not always relevant, but can fail phase-1 if fstab phase-1 includes ramdisk flow. - -### F) Kernel / IOKit policy-deny mediated failures (high-probability in your current repro) - -From your runtime evidence and control results: - -1. `mount` process can hit IOUC/MACF deny path on APFS UserClient access. -2. userspace may surface this as role/attribute lookup failure string, while root cause is kernel-side deny/altered return. - -Given: - -- same failure reproduces with non-JB `cfw_install` -- TXM known-good - -current priority remains kernel delta analysis. - -## Kernel patch candidates for this specific signature (ranked) - -### 1) Base patch #16 (`patch_apfs_get_dev_by_role_entitlement`) — highest - -Why high: - -- It directly targets APFS get-dev-by-role gate, which is exactly adjacent to `APFSVolumeRoleFind` behavior. -- It NOPs conditional branches by pattern heuristics; a false match can silently alter return path while keeping system alive. -- Symptom shape fits: boot container lookup can still succeed, but role lookup returns `ENOATTR`. -- Live-kernel validation status: - - patch #16 is present (all 3 target branches are `nop` at runtime). - - therefore current question is semantic side effect, not "patch missing". - -### 2) Base sandbox hook patch (`patch_sandbox_hooks`) — medium - -Why medium: - -- Touches mount/vnode MACF paths by ops-table index. -- If ops index resolution drifts, wrong function may be stubbed and produce semantic corruption instead of crash. - -### 3) Base APFS mount checks (#13/#14) — lower for this exact error - -Why lower: - -- These primarily alter mount authorization/upgrade checks. -- Less directly tied to role-attribute lookup API return code, but still in APFS mount vicinity. - -## What to do next (action order) - -1. Confirm userland return-site: - - break at `sub_1000036C0` (`BL _APFSVolumeRoleFind`) and inspect `w0` after return. - - expected failing value path: `w0 & 0x3fff == 93`. -2. Correlate with kernel-side return path in the same boot: - - break/trace APFS kernel role lookup function return (`handle_get_dev_by_role` path) and record final returned `w0`. - - determine whether kernel returns `93`, `22`, or other value when userspace later sees `93`. -3. Correlate with kernel log at same timestamp: - - look for `IOUC AppleAPFSUserClient failed MACF in process mount`. -4. Do base-kernel patch isolation first (not JB methods): - - run with base patch #16 disabled (or reverted) while keeping others unchanged. - - if failure clears, root cause is narrowed to entitlement-bypass matcher. -5. If #16 is not root cause, isolate base sandbox hook patch. -6. Only then continue to JB-only methods (`VPHONE_JB_DISABLE_METHODS`), because your latest control says non-JB install still reproduces. - -## Hang/stall-specific points (not just hard fail) - -1. Boot-device lookup retry loop (`APFSContainerGetBootDevice`) with sleep retries. -2. Child `mount_` helper blocking in kernel/IO path (parent waiting in `waitpid`). -3. External command wrapper (`sub_100002EA4`) blocking while waiting for command output/exit. - -These produce "looks stuck" behavior even before explicit non-zero exit. - -## Practical triage checklist for phase-1 - -1. Confirm exact failing subpath: - - preflight/APFS lookup vs helper mount vs waitpid/exec. -2. Correlate with kernel log at same timestamp: - - especially `IOUC AppleAPFSUserClient ... mount`. -3. Separate: - - non-fatal data-volume warning in phase-1 - - true fatal return path that makes launchd panic on `RequireSuccess`. diff --git a/research/kernel_jb_patch_notes.md b/research/kernel_jb_patch_notes.md index 346791a..cf40409 100644 --- a/research/kernel_jb_patch_notes.md +++ b/research/kernel_jb_patch_notes.md @@ -1,6 +1,6 @@ # Kernel JB Remaining Patches — Research Notes -Last updated: 2026-03-04 +Last updated: 2026-03-07 ## Overview @@ -12,6 +12,11 @@ Last updated: 2026-03-04 Two methods added since initial document: `patch_shared_region_map`, `patch_io_secure_bsd_root`. Three previously failing patches (`patch_nvram_verify_permission`, `patch_thid_should_crash`, `patch_hook_cred_label_update_execve`) have been implemented — see details below. +On 2026-03-06, three patches were retargeted after IDA-MCP re-analysis revealed their matchers were hitting wrong sites: +- `patch_bsd_init_auth` — was hitting `exec_handle_sugid` instead of the real `bsd_init` rootauth gate +- `patch_io_secure_bsd_root` — was patching the `"SecureRoot"` dispatch branch instead of the `"SecureRootName"` deny-return +- `patch_vm_fault_enter_prepare` — was NOPing a `pmap_lock_phys_page()` call instead of the upstream `cs_bypass` gate + Upstream reference: `/Users/qaq/Documents/GitHub/super-tart-vphone/CFW/patch_fw.py` Test kernel: `vm/iPhone17,3_26.1_23B85_Restore/kernelcache.release.vphone600` (IM4P-wrapped, bvx2 compressed) @@ -383,6 +388,24 @@ Should have moderate caller count (hundreds). **Problem**: Needed `_vfs_context_current` and `_vnode_getattr` — 0 symbols available. **Solution**: Eliminated `_vfs_context_current` entirely — shellcode constructs vfs_context inline on stack via `mrs x8, tpidr_el1` + `stp x8, x0, [sp, #0x70]`. `_vnode_getattr` found via "vnode_getattr" string anchor. Hook index found dynamically (scan first 30 ops entries). Code cave allocated via `_find_code_cave(180)`. +### patch_bsd_init_auth — RETARGETED (2026-03-06) + +**Historical repo behavior**: matched `ldr x0,[xN,#0x2b8]; cbz x0; bl` pattern, which landed on `exec_handle_sugid` at `0xFFFFFE0007FB09DC` — a false positive caused by `/dev/null` string overlap in the heuristic scoring. +**Problem**: the old matcher targeted the wrong function entirely; patching `exec_handle_sugid` instead of the real `bsd_init` rootauth gate could break boot by mutating an exec/credential path. +**Current status**: retargeted to the real `FSIOC_KERNEL_ROOTAUTH` return check in `bsd_init`. The new matcher recovers `bsd_init` via in-kernel string xrefs, locates the rootvp panic block (`"rootvp not authenticated after mounting"`), finds the unique in-function indirect call (`BLRAA`) preceded by the `0x80046833` (`FSIOC_KERNEL_ROOTAUTH`) literal, and NOPs the subsequent `CBNZ W0, panic`. Live patch hit: `0xFFFFFE0007F7B98C` / file offset `0x00F7798C`. See `research/kernel_patch_jb/patch_bsd_init_auth.md`. + +### patch_io_secure_bsd_root — RETARGETED (2026-03-06) + +**Historical repo behavior**: fallback heuristic selected the first `BL* + CBZ W0` site in `AppleARMPE::callPlatformFunction`, landing on the `"SecureRoot"` name-match gate at `0xFFFFFE000836E1F0` / file offset `0x0136A1F0`. This changed generic platform-function dispatch routing, not just the deny return. +**Problem**: the patched branch was the `isEqualTo("SecureRoot")` check, not the `"SecureRootName"` policy result used by `IOSecureBSDRoot()`. The old `CBZ->B` rewrite could corrupt control flow for unrelated platform-function calls. +**Current status**: retargeted to the final `"SecureRootName"` deny-return selector: `CSEL W22, WZR, W9, NE` at `0xFFFFFE000836E464` / file offset `0x0136A464` is replaced with `MOV W22, #0`. This preserves the string comparison, callback synchronization, and state updates, and only forces the final policy return from `kIOReturnNotPrivileged` to success. See `research/kernel_patch_jb/patch_io_secure_bsd_root.md`. + +### patch_vm_fault_enter_prepare — RETARGETED (2026-03-06) + +**Historical repo behavior**: matcher looked for `BL(rare) + LDRB [xN,#0x2c] + TBZ` and NOPed the BL at `0xFFFFFE0007BB898C`, which was actually a `pmap_lock_phys_page()` call inside the `VM_PAGE_CONSUME_CLUSTERED` macro — breaking lock/unlock pairing in the VM fault path. +**Problem**: the derived matcher overfit the wrong local shape. The upstream 26.1 patch targeted the `cs_bypass` fast-path gate (`TBZ W22, #3`), not the clustered-page lock helper. NOPing only the lock acquire while the unlock still ran caused unbalanced lock state, explaining boot failures. +**Current status**: retargeted to the upstream semantic site — `TBZ W22, #3, ...` (where W22 bit 3 = `fault_info->cs_bypass`) at file offset `0x00BA9E1C` / VA `0xFFFFFE0007BADE1C` is replaced with `NOP`, forcing the `cs_bypass` fast path unconditionally. This matches XNU's `vm_fault_cs_check_violation()` logic and preserves lock pairing and page accounting. See `research/kernel_patch_jb/patch_vm_fault_enter_prepare.md`. + --- ## Environment Notes diff --git a/research/kernel_patch_sandbox_hooks_21_26_validation.md b/research/kernel_patch_sandbox_hooks_17_26_validation.md similarity index 87% rename from research/kernel_patch_sandbox_hooks_21_26_validation.md rename to research/kernel_patch_sandbox_hooks_17_26_validation.md index a3ccdd4..75f2651 100644 --- a/research/kernel_patch_sandbox_hooks_21_26_validation.md +++ b/research/kernel_patch_sandbox_hooks_17_26_validation.md @@ -1,4 +1,4 @@ -# Kernel Patch Validation: Sandbox Hooks 21-26 (Regular/Development) +# Kernel Patch Validation: Sandbox Hooks 17-26 (Regular/Development) Date: 2026-03-05 @@ -6,6 +6,8 @@ Date: 2026-03-05 Validate the following non-JB kernel patches on a freshly prepared (unpatched) firmware kernelcache: +- 17/18 `file_check_mmap`: `mov x0,#0` + `ret` +- 19/20 `mount_check_mount`: `mov x0,#0` + `ret` - 21/22 `mount_check_remount`: `mov x0,#0` + `ret` - 23/24 `mount_check_umount`: `mov x0,#0` + `ret` - 25/26 `vnode_check_rename`: `mov x0,#0` + `ret` @@ -27,6 +29,8 @@ Patch flow under test: 1. `_find_sandbox_ops_table_via_conf()` to locate `mac_policy_conf` 2. `mpc_ops` pointer to read function entries by index 3. `HOOK_INDICES`: + - `file_check_mmap = 36` + - `mount_check_mount = 87` - `mount_check_remount = 88` - `mount_check_umount = 91` - `vnode_check_rename = 120` @@ -73,7 +77,7 @@ From direct `KernelPatcher` run on clean payload (in-memory, no file write): Using IDA DB and disassembly/decompile on the same firmware family: -- Entry sites match the three hook slots above. +- Entry sites match the five hook slots above. - For `vnode_check_rename`, downstream body includes rename-related path monitoring logic (`pathmonitor_prepare_rename`), confirming semantic alignment with rename hook behavior. - Note: current IDA database had these entry points already recognized as patched stubs; additional inspection was performed from `entry+8` into original body for semantic validation. @@ -81,8 +85,8 @@ Using IDA DB and disassembly/decompile on the same firmware family: Status: **working for now**. -For clean `fw_prepare` kernelcache, the 21-26 sandbox hook patches: +For clean `fw_prepare` kernelcache, the 17-26 sandbox hook patches: - resolve through the correct `mac_policy_ops` table, -- hit the expected three hook entry addresses, +- hit the expected five hook entry addresses, - and rewrite exactly the first two instructions to `mov x0,#0; ret`. diff --git a/research/kernel_patcher_verification.md b/research/kernel_patcher_verification.md index 45a3989..fcde947 100644 --- a/research/kernel_patcher_verification.md +++ b/research/kernel_patcher_verification.md @@ -51,8 +51,8 @@ apply the dynamic patcher to a freshly extracted vresearch101 kernelcache. Result: **byte-identical** output between hardcoded and dynamic patching. -- `KernelPatcher` patches found: 25 -- Hardcoded patches applied: 25 +- `KernelPatcher` patches found: 26 +- Hardcoded patches applied: 26 - `cmp -l /tmp/kc_vphone600_upstream.raw /tmp/kc_vphone600_dynamic.raw`: no output (files identical) @@ -77,16 +77,17 @@ Offsets and 32-bit patch values, taken from `patch_fw.py`: | 13 | 0x2475044 | 0xEB00001F | \_apfs_vfsop_mount cmp x0,x0 | | 14 | 0x2476C00 | 0x52800000 | \_apfs_mount_upgrade_checks mov w0,#0 | | 15 | 0x248C800 | 0x52800000 | \_handle_fsioc_graft mov w0,#0 | -| 16 | 0x23AC528 | 0xD2800000 | \_hook_file_check_mmap mov x0,#0 | -| 17 | 0x23AC52C | 0xD65F03C0 | \_hook_file_check_mmap ret | -| 18 | 0x23AAB58 | 0xD2800000 | \_hook_mount_check_mount mov x0,#0 | -| 19 | 0x23AAB5C | 0xD65F03C0 | \_hook_mount_check_mount ret | -| 20 | 0x23AA9A0 | 0xD2800000 | \_hook_mount_check_remount mov x0,#0 | -| 21 | 0x23AA9A4 | 0xD65F03C0 | \_hook_mount_check_remount ret | -| 22 | 0x23AA80C | 0xD2800000 | \_hook_mount_check_umount mov x0,#0 | -| 23 | 0x23AA810 | 0xD65F03C0 | \_hook_mount_check_umount ret | -| 24 | 0x23A5514 | 0xD2800000 | \_hook_vnode_check_rename mov x0,#0 | -| 25 | 0x23A5518 | 0xD65F03C0 | \_hook_vnode_check_rename ret | +| 16 | | | \_handle_get_dev_by_role entitlement bypass | +| 17 | 0x23AC528 | 0xD2800000 | \_hook_file_check_mmap mov x0,#0 | +| 18 | 0x23AC52C | 0xD65F03C0 | \_hook_file_check_mmap ret | +| 19 | 0x23AAB58 | 0xD2800000 | \_hook_mount_check_mount mov x0,#0 | +| 20 | 0x23AAB5C | 0xD65F03C0 | \_hook_mount_check_mount ret | +| 21 | 0x23AA9A0 | 0xD2800000 | \_hook_mount_check_remount mov x0,#0 | +| 22 | 0x23AA9A4 | 0xD65F03C0 | \_hook_mount_check_remount ret | +| 23 | 0x23AA80C | 0xD2800000 | \_hook_mount_check_umount mov x0,#0 | +| 24 | 0x23AA810 | 0xD65F03C0 | \_hook_mount_check_umount ret | +| 25 | 0x23A5514 | 0xD2800000 | \_hook_vnode_check_rename mov x0,#0 | +| 26 | 0x23A5518 | 0xD65F03C0 | \_hook_vnode_check_rename ret | ## TXM Patch Details @@ -125,7 +126,7 @@ pyimg4 im4p extract \ Dynamic patcher results: -- Patches found/applied: 25 +- Patches found/applied: 26 - TXM patch location: `0xFA6B98` (NOP `tbnz w8, #0, #0xfa6c80`) - Patched output: `/tmp/kc_vresearch1_dynamic.raw` @@ -134,7 +135,7 @@ Dynamic patcher results: For vphone600, the dynamic patcher output is byte-identical to the legacy hardcoded patch list, indicating functional equivalence on this kernelcache. The same dynamic patcher also successfully patches the freshly extracted -vresearch101 kernelcache with the expected TXM NOP and a full 25-patch set. +vresearch101 kernelcache with the expected TXM NOP and a full 26-patch set. ## Targeted Re-Verification (2026-03-05) diff --git a/research/txm_jb_patches.md b/research/txm_jb_patches.md index ffd5fce..9d7d67e 100644 --- a/research/txm_jb_patches.md +++ b/research/txm_jb_patches.md @@ -1,6 +1,6 @@ # TXM Jailbreak Patch Analysis -Analysis of 6 logical TXM jailbreak patches (11 instruction modifications) applied by `txm_jb.py` on the RESEARCH variant +Analysis of 6 logical TXM jailbreak patches (11 instruction modifications) applied by `txm_dev.py` on the RESEARCH variant of TXM from iPhone17,3 / PCC-CloudOS 26.x. ## TXM Execution Model @@ -314,7 +314,7 @@ falls through to the version checks which return success for version <= 5. This effectively bypasses CodeSignature hash validation --- the hash data exists in the blob but the hash-present flag is suppressed, so the consistency check passes. -### `txm_jb.py` dynamic finder: `patch_selector24_hash_extraction_nop()` +### `txm_dev.py` dynamic finder: `patch_selector24_force_pass()` Scans for `mov w0, #0xa1` as a unique anchor to locate the CS hash validator function, finds PACIBSP to determine function start, then matches the pattern @@ -412,7 +412,7 @@ Universal entitlement lookup function. When `a1 != 0`, it resolves the manifest' entitlement dictionary and searches for the named key via `sub_FFFFFFF017036294`. Returns a composite status word where bit 0 indicates the entitlement was found. -### `txm_jb.py` dynamic finder: `patch_get_task_allow_force_true()` +### `txm_dev.py` dynamic finder: `patch_get_task_allow_force_true()` Searches for string refs to `"get-task-allow"`, then scans forward for the pattern `BL X / TBNZ w0, #0, Y`. Patches the BL to `MOV X0, #1`. @@ -495,7 +495,7 @@ Since the validator returns the pointer unchanged, `x20` (raw arg) and the valid pointer both refer to the same object. The shellcode's `STRB W0, [X20, #0x30]` writes to the correct location. -### `txm_jb.py` dynamic finder: `patch_selector42_29_shellcode()` +### `txm_dev.py` dynamic finder: `patch_selector42_29_shellcode()` 1. Finds the "debugger gate function" via string refs to `"com.apple.private.cs.debugger"` 2. Locates the dispatch stub by matching `BTI j / MOV X0, X20 / BL / MOV X1, X21 / MOV X2, X22 / BL debugger_gate / B` @@ -557,7 +557,7 @@ branches to the success path, bypassing both the entitlement check and the fallback flag check. This allows any process to create debug memory mappings regardless of whether it has `com.apple.private.cs.debugger`. -### `txm_jb.py` dynamic finder: `patch_debugger_entitlement_force_true()` +### `txm_dev.py` dynamic finder: `patch_debugger_entitlement_force_true()` Searches for string refs to `"com.apple.private.cs.debugger"`, then matches the pattern: `mov x0, #0 / mov x2, #0 / bl X / tbnz w0, #0, Y`. Patches the BL @@ -645,7 +645,7 @@ if ( (byte_FFFFFFF017070F24 & 1) == 0 ) return 27; // developer mode not enabled ``` -### `txm_jb.py` dynamic finder: `patch_developer_mode_bypass()` +### `txm_dev.py` dynamic finder: `patch_developer_mode_bypass()` Searches for string refs to `"developer mode enabled due to system policy configuration"`, then scans backwards for a `tbz/tbnz/cbz/cbnz` instruction diff --git a/scripts/patchers/kernel_patch_sandbox.py b/scripts/patchers/kernel_patch_sandbox.py index 09f51de..7328834 100644 --- a/scripts/patchers/kernel_patch_sandbox.py +++ b/scripts/patchers/kernel_patch_sandbox.py @@ -5,11 +5,11 @@ from .kernel_asm import MOV_X0_0, RET class KernelPatchSandboxMixin: def patch_sandbox_hooks(self): - """Patches 16-25: Stub Sandbox MACF hooks with mov x0,#0; ret. + """Patches 17-26: Stub Sandbox MACF hooks with mov x0,#0; ret. Uses mac_policy_ops struct indices from XNU source (xnu-11215+). """ - self._log("\n[16-25] Sandbox MACF hooks") + self._log("\n[17-26] Sandbox MACF hooks") ops_table = self._find_sandbox_ops_table_via_conf() if ops_table is None: From cfee3ea076c7f5a1eb49655507838225c9648b49 Mon Sep 17 00:00:00 2001 From: Lakr Date: Sat, 7 Mar 2026 18:34:49 +0800 Subject: [PATCH 04/10] Add JB finalizer script; remove IPA signing UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add scripts/cfw_install_jb_post.sh — an idempotent SSH-based finalizer to complete JB bootstrap on a normally-booted vphone (creates /var/jb symlink, fixes ownership, runs prep_bootstrap, creates markers, installs Sileo, and runs apt; requires sshpass). Add Makefile help, .PHONY and target cfw_install_jb_finalize to invoke the script. Remove host-side IPA signing/installing and related UI: delete VPhoneSigner, VPhoneIPAInstaller, VPhoneMenuInstall and remove signer/ipaInstaller fields and menu items/callbacks from the vphone-cli UI (also removed the DevMode enable WIP flow). Misc: minor table/formatting tweaks in AGENTS.md and research docs. --- AGENTS.md | 8 +- Makefile | 6 +- research/0_binary_patch_comparison.md | 37 ++-- research/kernel_jb_patch_notes.md | 1 + research/kernel_patcher_verification.md | 54 ++--- scripts/cfw_install_jb_post.sh | 191 ++++++++++++++++++ sources/vphone-cli/VPhoneAppDelegate.swift | 4 - sources/vphone-cli/VPhoneControl.swift | 12 -- sources/vphone-cli/VPhoneIPAInstaller.swift | 164 --------------- sources/vphone-cli/VPhoneMenuConnect.swift | 18 -- sources/vphone-cli/VPhoneMenuController.swift | 3 - sources/vphone-cli/VPhoneMenuInstall.swift | 116 ----------- sources/vphone-cli/VPhoneSigner.swift | 157 -------------- 13 files changed, 246 insertions(+), 525 deletions(-) create mode 100755 scripts/cfw_install_jb_post.sh delete mode 100644 sources/vphone-cli/VPhoneIPAInstaller.swift delete mode 100644 sources/vphone-cli/VPhoneMenuInstall.swift delete mode 100644 sources/vphone-cli/VPhoneSigner.swift diff --git a/AGENTS.md b/AGENTS.md index ef98741..2a1d850 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,10 +29,10 @@ For any changes applying new patches, also update research/0_binary_patch_compar ## Firmware Variants -| Variant | Boot Chain | CFW | Make Targets | -| ------------------- | :--------: | :-------: | ---------------------------------- | -| **Regular** | 51 patches | 10 phases | `fw_patch` + `cfw_install` | -| **Development** | 64 patches | 12 phases | `fw_patch_dev` + `cfw_install_dev` | +| Variant | Boot Chain | CFW | Make Targets | +| ------------------- | :---------: | :-------: | ---------------------------------- | +| **Regular** | 51 patches | 10 phases | `fw_patch` + `cfw_install` | +| **Development** | 64 patches | 12 phases | `fw_patch_dev` + `cfw_install_dev` | | **Jailbreak (WIP)** | 126 patches | 14 phases | `fw_patch_jb` + `cfw_install_jb` | See `research/` for detailed firmware pipeline, component origins, patch breakdowns, and boot flow documentation. diff --git a/Makefile b/Makefile index 1821237..183e001 100644 --- a/Makefile +++ b/Makefile @@ -81,6 +81,7 @@ help: @echo " make cfw_install Install CFW mods via SSH" @echo " make cfw_install_dev Install CFW mods via SSH (dev mode)" @echo " make cfw_install_jb Install CFW + JB extensions (jetsam/procursus/basebin)" + @echo " make cfw_install_jb_finalize Finalize JB bootstrap on live device (symlinks/sileo/apt)" @echo "" @echo "Variables: VM_DIR=$(VM_DIR) CPU=$(CPU) MEMORY=$(MEMORY) DISK_SIZE=$(DISK_SIZE)" @@ -243,7 +244,7 @@ ramdisk_send: # CFW # ═══════════════════════════════════════════════════════════════════ -.PHONY: cfw_install cfw_install_dev cfw_install_jb +.PHONY: cfw_install cfw_install_dev cfw_install_jb cfw_install_jb_finalize cfw_install: cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" . @@ -253,3 +254,6 @@ cfw_install_dev: cfw_install_jb: cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb.sh" . + +cfw_install_jb_finalize: + $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") $(if $(SSH_PASS),SSH_PASS="$(SSH_PASS)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb_post.sh" diff --git a/research/0_binary_patch_comparison.md b/research/0_binary_patch_comparison.md index eb8cc45..513c489 100644 --- a/research/0_binary_patch_comparison.md +++ b/research/0_binary_patch_comparison.md @@ -77,7 +77,7 @@ | # | Group | Method | Function | Purpose | JB Enabled | | ----- | ----- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------: | | 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-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-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 | @@ -130,19 +130,19 @@ ### CFW Installer Flow Matrix (Script-Level) -| Flow Item | Regular (`cfw_install.sh`) | Dev (`cfw_install_dev.sh`) | JB (`cfw_install_jb.sh`) | -| ---------------------------------------------------- | ------------------------------- | ----------------------------------------------- | --------------------------------------------- | -| Base CFW phases (1/7 -> 7/7) | Runs directly | Runs directly | Runs via `CFW_SKIP_HALT=1 zsh cfw_install.sh` | -| Dev overlay (`rpcserver_ios` replacement) | - | Y (`apply_dev_overlay`) | - | -| SSH readiness wait before install | Y (`wait_for_device_ssh_ready`) | - | Y (inherited from base run) | -| launchd jetsam patch (`patch-launchd-jetsam`) | - | Y (base-flow injection) | Y (JB-1) | -| launchd dylib injection (`inject-dylib /b`) | - | - | Y (JB-1) | +| Flow Item | Regular (`cfw_install.sh`) | Dev (`cfw_install_dev.sh`) | JB (`cfw_install_jb.sh`) | +| --------------------------------------------- | ------------------------------- | -------------------------- | --------------------------------------------- | +| Base CFW phases (1/7 -> 7/7) | Runs directly | Runs directly | Runs via `CFW_SKIP_HALT=1 zsh cfw_install.sh` | +| Dev overlay (`rpcserver_ios` replacement) | - | Y (`apply_dev_overlay`) | - | +| SSH readiness wait before install | Y (`wait_for_device_ssh_ready`) | - | Y (inherited from base run) | +| launchd jetsam patch (`patch-launchd-jetsam`) | - | Y (base-flow injection) | Y (JB-1) | +| launchd dylib injection (`inject-dylib /b`) | - | - | Y (JB-1) | -| Procursus bootstrap deployment | - | - | Y (JB-2) | -| BaseBin hook deployment (`*.dylib` -> `/mnt1/cores`) | - | - | Y (JB-3) | -| Additional input resources | `cfw_input` | `cfw_input` + `resources/cfw_dev/rpcserver_ios` | `cfw_input` + `cfw_jb_input` | -| Extra tool requirement beyond base | - | - | `zstd` | -| Halt behavior | Halts unless `CFW_SKIP_HALT=1` | Halts unless `CFW_SKIP_HALT=1` | Always halts after JB phases | +| Procursus bootstrap deployment | - | - | Y (JB-2) | +| BaseBin hook deployment (`*.dylib` -> `/mnt1/cores`) | - | - | Y (JB-3) | +| Additional input resources | `cfw_input` | `cfw_input` + `resources/cfw_dev/rpcserver_ios` | `cfw_input` + `cfw_jb_input` | +| Extra tool requirement beyond base | - | - | `zstd` | +| Halt behavior | Halts unless `CFW_SKIP_HALT=1` | Halts unless `CFW_SKIP_HALT=1` | Always halts after JB phases | ## Summary @@ -163,11 +163,11 @@ ## Ramdisk Variant Matrix -| Variant | Pre-step | `Ramdisk/txm.img4` | `Ramdisk/krnl.ramdisk.img4` | `Ramdisk/krnl.img4` | Effective kernel used by `ramdisk_send.sh` | -| ------------- | ------------------- | -------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------- | -| `RAMDISK` | `make fw_patch` | release TXM + base TXM patch (1) | base kernel (28), legacy `*.ramdisk` preferred else derive from pristine CloudOS | restore kernel from `fw_patch` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | -| `DEV+RAMDISK` | `make fw_patch_dev` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_dev` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | -| `JB+RAMDISK` | `make fw_patch_jb` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_jb` (28+59) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | +| Variant | Pre-step | `Ramdisk/txm.img4` | `Ramdisk/krnl.ramdisk.img4` | `Ramdisk/krnl.img4` | Effective kernel used by `ramdisk_send.sh` | +| ------------- | ------------------- | -------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------- | +| `RAMDISK` | `make fw_patch` | release TXM + base TXM patch (1) | base kernel (28), legacy `*.ramdisk` preferred else derive from pristine CloudOS | restore kernel from `fw_patch` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | +| `DEV+RAMDISK` | `make fw_patch_dev` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_dev` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | +| `JB+RAMDISK` | `make fw_patch_jb` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_jb` (28+59) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` | ## Cross-Version Dynamic Snapshot @@ -177,4 +177,3 @@ | PCC 26.3 (`23D128`) | 14 | 59 | | iOS 26.1 (`23B85`) | 14 | 59 | | iOS 26.3 (`23D127`) | 14 | 59 | - diff --git a/research/kernel_jb_patch_notes.md b/research/kernel_jb_patch_notes.md index cf40409..051f07b 100644 --- a/research/kernel_jb_patch_notes.md +++ b/research/kernel_jb_patch_notes.md @@ -13,6 +13,7 @@ Two methods added since initial document: `patch_shared_region_map`, `patch_io_s Three previously failing patches (`patch_nvram_verify_permission`, `patch_thid_should_crash`, `patch_hook_cred_label_update_execve`) have been implemented — see details below. On 2026-03-06, three patches were retargeted after IDA-MCP re-analysis revealed their matchers were hitting wrong sites: + - `patch_bsd_init_auth` — was hitting `exec_handle_sugid` instead of the real `bsd_init` rootauth gate - `patch_io_secure_bsd_root` — was patching the `"SecureRoot"` dispatch branch instead of the `"SecureRootName"` deny-return - `patch_vm_fault_enter_prepare` — was NOPing a `pmap_lock_phys_page()` call instead of the upstream `cs_bypass` gate diff --git a/research/kernel_patcher_verification.md b/research/kernel_patcher_verification.md index fcde947..2e893d0 100644 --- a/research/kernel_patcher_verification.md +++ b/research/kernel_patcher_verification.md @@ -60,34 +60,34 @@ Result: **byte-identical** output between hardcoded and dynamic patching. Offsets and 32-bit patch values, taken from `patch_fw.py`: -| # | Offset (hex) | Patch value | Purpose | -| --- | -----------: | ----------: | ------------------------------------------ | -| 1 | 0x2476964 | 0xD503201F | \_apfs_vfsop_mount root snapshot NOP | -| 2 | 0x23CFDE4 | 0xD503201F | \_authapfs_seal_is_broken NOP | -| 3 | 0x00F6D960 | 0xD503201F | \_bsd_init rootvp NOP | -| 4 | 0x163863C | 0x52800000 | \_proc_check_launch_constraints mov w0,#0 | -| 5 | 0x1638640 | 0xD65F03C0 | \_proc_check_launch_constraints ret | -| 6 | 0x12C8138 | 0xD2800020 | \_PE_i_can_has_debugger mov x0,#1 | -| 7 | 0x12C813C | 0xD65F03C0 | \_PE_i_can_has_debugger ret | -| 8 | 0x00FFAB98 | 0xD503201F | TXM post-validation NOP (tbnz) | -| 9 | 0x16405AC | 0x6B00001F | postValidation cmp w0,w0 | -| 10 | 0x16410BC | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (1) | -| 11 | 0x16410C8 | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (2) | -| 12 | 0x242011C | 0x52800000 | \_apfs_graft mov w0,#0 | -| 13 | 0x2475044 | 0xEB00001F | \_apfs_vfsop_mount cmp x0,x0 | -| 14 | 0x2476C00 | 0x52800000 | \_apfs_mount_upgrade_checks mov w0,#0 | -| 15 | 0x248C800 | 0x52800000 | \_handle_fsioc_graft mov w0,#0 | +| # | Offset (hex) | Patch value | Purpose | +| --- | -----------: | ----------: | ------------------------------------------- | +| 1 | 0x2476964 | 0xD503201F | \_apfs_vfsop_mount root snapshot NOP | +| 2 | 0x23CFDE4 | 0xD503201F | \_authapfs_seal_is_broken NOP | +| 3 | 0x00F6D960 | 0xD503201F | \_bsd_init rootvp NOP | +| 4 | 0x163863C | 0x52800000 | \_proc_check_launch_constraints mov w0,#0 | +| 5 | 0x1638640 | 0xD65F03C0 | \_proc_check_launch_constraints ret | +| 6 | 0x12C8138 | 0xD2800020 | \_PE_i_can_has_debugger mov x0,#1 | +| 7 | 0x12C813C | 0xD65F03C0 | \_PE_i_can_has_debugger ret | +| 8 | 0x00FFAB98 | 0xD503201F | TXM post-validation NOP (tbnz) | +| 9 | 0x16405AC | 0x6B00001F | postValidation cmp w0,w0 | +| 10 | 0x16410BC | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (1) | +| 11 | 0x16410C8 | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (2) | +| 12 | 0x242011C | 0x52800000 | \_apfs_graft mov w0,#0 | +| 13 | 0x2475044 | 0xEB00001F | \_apfs_vfsop_mount cmp x0,x0 | +| 14 | 0x2476C00 | 0x52800000 | \_apfs_mount_upgrade_checks mov w0,#0 | +| 15 | 0x248C800 | 0x52800000 | \_handle_fsioc_graft mov w0,#0 | | 16 | | | \_handle_get_dev_by_role entitlement bypass | -| 17 | 0x23AC528 | 0xD2800000 | \_hook_file_check_mmap mov x0,#0 | -| 18 | 0x23AC52C | 0xD65F03C0 | \_hook_file_check_mmap ret | -| 19 | 0x23AAB58 | 0xD2800000 | \_hook_mount_check_mount mov x0,#0 | -| 20 | 0x23AAB5C | 0xD65F03C0 | \_hook_mount_check_mount ret | -| 21 | 0x23AA9A0 | 0xD2800000 | \_hook_mount_check_remount mov x0,#0 | -| 22 | 0x23AA9A4 | 0xD65F03C0 | \_hook_mount_check_remount ret | -| 23 | 0x23AA80C | 0xD2800000 | \_hook_mount_check_umount mov x0,#0 | -| 24 | 0x23AA810 | 0xD65F03C0 | \_hook_mount_check_umount ret | -| 25 | 0x23A5514 | 0xD2800000 | \_hook_vnode_check_rename mov x0,#0 | -| 26 | 0x23A5518 | 0xD65F03C0 | \_hook_vnode_check_rename ret | +| 17 | 0x23AC528 | 0xD2800000 | \_hook_file_check_mmap mov x0,#0 | +| 18 | 0x23AC52C | 0xD65F03C0 | \_hook_file_check_mmap ret | +| 19 | 0x23AAB58 | 0xD2800000 | \_hook_mount_check_mount mov x0,#0 | +| 20 | 0x23AAB5C | 0xD65F03C0 | \_hook_mount_check_mount ret | +| 21 | 0x23AA9A0 | 0xD2800000 | \_hook_mount_check_remount mov x0,#0 | +| 22 | 0x23AA9A4 | 0xD65F03C0 | \_hook_mount_check_remount ret | +| 23 | 0x23AA80C | 0xD2800000 | \_hook_mount_check_umount mov x0,#0 | +| 24 | 0x23AA810 | 0xD65F03C0 | \_hook_mount_check_umount ret | +| 25 | 0x23A5514 | 0xD2800000 | \_hook_vnode_check_rename mov x0,#0 | +| 26 | 0x23A5518 | 0xD65F03C0 | \_hook_vnode_check_rename ret | ## TXM Patch Details diff --git a/scripts/cfw_install_jb_post.sh b/scripts/cfw_install_jb_post.sh new file mode 100755 index 0000000..de82474 --- /dev/null +++ b/scripts/cfw_install_jb_post.sh @@ -0,0 +1,191 @@ +#!/bin/zsh +# cfw_install_jb_post.sh — Finalize JB bootstrap on a normally-booted vphone. +# +# Runs after `cfw_install_jb` + first normal boot. Connects to the live device +# via SSH and sets up procursus symlinks, markers, Sileo, and apt packages. +# +# Every step is idempotent — safe to re-run at any point. +# All binary paths are discovered dynamically (no hardcoded /bin, /sbin, etc.). +# +# Usage: make cfw_install_jb_finalize [SSH_PORT=22222] [SSH_PASS=alpine] +set -euo pipefail + +SCRIPT_DIR="${0:a:h}" + +# ── Configuration ─────────────────────────────────────────────── +SSH_PORT="${SSH_PORT:-22222}" +SSH_PASS="${SSH_PASS:-alpine}" +SSH_USER="root" +SSH_HOST="localhost" +SSH_RETRY="${SSH_RETRY:-3}" +SSHPASS_BIN="" +SSH_OPTS=( + -o StrictHostKeyChecking=no + -o UserKnownHostsFile=/dev/null + -o PreferredAuthentications=password + -o ConnectTimeout=30 + -q +) + +# ── Helpers ───────────────────────────────────────────────────── +die() { + echo "[-] $*" >&2 + exit 1 +} + +_sshpass() { + "$SSHPASS_BIN" -p "$SSH_PASS" "$@" +} + +_ssh_retry() { + local attempt rc label + label=${2:-cmd} + for ((attempt = 1; attempt <= SSH_RETRY; attempt++)); do + "$@" && return 0 + rc=$? + [[ $rc -ne 255 ]] && return $rc + echo " [${label}] connection lost (attempt $attempt/$SSH_RETRY), retrying in 3s..." >&2 + sleep 3 + done + return 255 +} + +# Raw ssh — no PATH prefix +ssh_raw() { + _ssh_retry _sshpass ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "$@" +} + +# ssh with discovered PATH prepended +ssh_cmd() { + ssh_raw "$RENV $*" +} + +# ── Prerequisites ────────────────────────────────────────────── +command -v sshpass &>/dev/null || die "Missing sshpass. Run: make setup_tools" +SSHPASS_BIN="$(command -v sshpass)" + +echo "[*] cfw_install_jb_post.sh — Finalizing JB bootstrap..." +echo " Target: ${SSH_USER}@${SSH_HOST}:${SSH_PORT}" +echo "" + +# ── Verify SSH connectivity ──────────────────────────────────── +echo "[*] Checking SSH connectivity..." +ssh_raw "echo ready" >/dev/null 2>&1 || die "Cannot reach device on ${SSH_HOST}:${SSH_PORT}. Is the VM booted normally?" +echo "[+] Device reachable" + +# ── Discover remote PATH ────────────────────────────────────── +# Uses only shell builtins (test -d, echo) — works with empty PATH. +echo "[*] Discovering remote binary directories..." +DISCOVERED_PATH=$(ssh_raw 'P=""; \ + for d in \ + /var/jb/usr/bin /var/jb/bin /var/jb/sbin /var/jb/usr/sbin \ + /iosbinpack64/bin /iosbinpack64/usr/bin /iosbinpack64/sbin /iosbinpack64/usr/sbin \ + /usr/bin /usr/sbin /bin /sbin; do \ + [ -d "$d" ] && P="$P:$d"; \ + done; \ + echo "${P#:}"') + +[[ -n "$DISCOVERED_PATH" ]] || die "Could not discover any binary directories on device" +echo " PATH=$DISCOVERED_PATH" + +# This gets prepended to every ssh_cmd call +RENV="export PATH='$DISCOVERED_PATH' TERM='xterm-256color';" + +# Quick sanity: verify we can run ls now +ssh_cmd "ls / >/dev/null" || die "PATH discovery succeeded but 'ls' still not found" +echo "[+] Remote environment ready" + +# ═══════════ 1/6 SYMLINK /var/jb ══════════════════════════════ +echo "" +echo "[1/6] Creating /private/var/jb symlink..." + +# Find 96-char boot manifest hash — use shell glob (no ls dependency) +BOOT_HASH=$(ssh_cmd 'for d in /private/preboot/*/; do \ + b="${d%/}"; b="${b##*/}"; \ + [ "${#b}" = 96 ] && echo "$b" && break; \ +done') +[[ -n "$BOOT_HASH" ]] || die "Could not find 96-char boot manifest hash in /private/preboot" +echo " Boot manifest hash: $BOOT_HASH" + +JB_TARGET="/private/preboot/$BOOT_HASH/jb-vphone/procursus" +ssh_cmd "test -d '$JB_TARGET'" || die "Procursus directory not found at $JB_TARGET. Run cfw_install_jb first." + +CURRENT_LINK=$(ssh_cmd "readlink /private/var/jb 2>/dev/null || true") +if [[ "$CURRENT_LINK" == "$JB_TARGET" ]]; then + echo " [*] Symlink already correct, skipping" +else + ssh_cmd "ln -sf '$JB_TARGET' /private/var/jb" + echo " [+] /private/var/jb -> $JB_TARGET" +fi + +# ═══════════ 2/6 FIX OWNERSHIP / PERMISSIONS ═════════════════ +echo "" +echo "[2/6] Fixing mobile Library ownership..." + +ssh_cmd "mkdir -p /var/jb/var/mobile/Library/Preferences" +ssh_cmd "chown -R 501:501 /var/jb/var/mobile/Library" +ssh_cmd "chmod 0755 /var/jb/var/mobile/Library" +ssh_cmd "chown -R 501:501 /var/jb/var/mobile/Library/Preferences" +ssh_cmd "chmod 0755 /var/jb/var/mobile/Library/Preferences" + +echo " [+] Ownership set" + +# ═══════════ 3/6 RUN prep_bootstrap.sh ════════════════════════ +echo "" +echo "[3/6] Running prep_bootstrap.sh..." + +if ssh_cmd "test -f /var/jb/prep_bootstrap.sh"; then + # Skip interactive password prompt (uses uialert GUI — not usable over SSH) + ssh_cmd "NO_PASSWORD_PROMPT=1 /var/jb/prep_bootstrap.sh" + echo " [+] prep_bootstrap.sh completed" + echo " [!] Terminal password was NOT set (automated mode)." + echo " To set it manually: ssh in and run: passwd" +else + echo " [*] prep_bootstrap.sh already ran (deleted itself), skipping" +fi + +# ═══════════ 4/6 CREATE MARKER FILES ═════════════════════════ +echo "" +echo "[4/6] Creating marker files..." + +for marker in .procursus_strapped .installed_dopamine; do + if ssh_cmd "test -f /var/jb/$marker"; then + echo " [*] $marker already exists, skipping" + else + ssh_cmd "touch /var/jb/$marker && chown 0:0 /var/jb/$marker && chmod 0644 /var/jb/$marker" + echo " [+] $marker created" + fi +done + +# ═══════════ 5/6 INSTALL SILEO ══════════════════════════════ +echo "" +echo "[5/6] Installing Sileo..." + +SILEO_DEB_PATH="/private/preboot/$BOOT_HASH/org.coolstar.sileo_2.5.1_iphoneos-arm64.deb" + +if ssh_cmd "dpkg -s org.coolstar.sileo >/dev/null 2>&1"; then + echo " [*] Sileo already installed, skipping" +else + ssh_cmd "test -f '$SILEO_DEB_PATH'" || die "Sileo deb not found at $SILEO_DEB_PATH. Was it uploaded by cfw_install_jb?" + ssh_cmd "dpkg -i '$SILEO_DEB_PATH'" + echo " [+] Sileo installed" +fi + +ssh_cmd "uicache -a 2>/dev/null || true" +echo " [+] uicache refreshed" + +# ═══════════ 6/6 APT SETUP ═════════════════════════════════ +echo "" +echo "[6/6] Running apt setup..." + +ssh_cmd "apt-get update -qq && apt-get install -y -qq libkrw0-tfp0 2>/dev/null || true" +echo " [+] apt update + libkrw0-tfp0 done" + +ssh_cmd "apt-get upgrade -y -qq 2>/dev/null || true" +echo " [+] apt upgrade done" + +# ═══════════ DONE ═══════════════════════════════════════════ +echo "" +echo "[+] JB finalization complete!" +echo " Next: open Sileo on device, add source https://ellekit.space, install ElleKit" +echo " Then reboot the device for full JB environment." diff --git a/sources/vphone-cli/VPhoneAppDelegate.swift b/sources/vphone-cli/VPhoneAppDelegate.swift index 99696eb..409ce2b 100644 --- a/sources/vphone-cli/VPhoneAppDelegate.swift +++ b/sources/vphone-cli/VPhoneAppDelegate.swift @@ -135,10 +135,6 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate { mc.locationProvider = provider } mc.screenRecorder = VPhoneScreenRecorder() - if let signer = VPhoneSigner() { - mc.signer = signer - mc.ipaInstaller = VPhoneIPAInstaller(signer: signer) - } menuController = mc // Wire location toggle through onConnect/onDisconnect diff --git a/sources/vphone-cli/VPhoneControl.swift b/sources/vphone-cli/VPhoneControl.swift index de18358..8e735fc 100644 --- a/sources/vphone-cli/VPhoneControl.swift +++ b/sources/vphone-cli/VPhoneControl.swift @@ -261,24 +261,12 @@ class VPhoneControl { let enabled: Bool } - struct DevModeEnableResult { - let alreadyEnabled: Bool - let message: String - } - func sendDevModeStatus() async throws -> DevModeStatus { let (resp, _) = try await sendRequest(["t": "devmode", "action": "status"]) let enabled = resp["enabled"] as? Bool ?? false return DevModeStatus(enabled: enabled) } - func sendDevModeEnable() async throws -> DevModeEnableResult { - let (resp, _) = try await sendRequest(["t": "devmode", "action": "enable"]) - let alreadyEnabled = resp["already_enabled"] as? Bool ?? false - let message = resp["msg"] as? String ?? "" - return DevModeEnableResult(alreadyEnabled: alreadyEnabled, message: message) - } - func sendPing() async throws { _ = try await sendRequest(["t": "ping"]) } diff --git a/sources/vphone-cli/VPhoneIPAInstaller.swift b/sources/vphone-cli/VPhoneIPAInstaller.swift deleted file mode 100644 index 0a08736..0000000 --- a/sources/vphone-cli/VPhoneIPAInstaller.swift +++ /dev/null @@ -1,164 +0,0 @@ -import Foundation - -// MARK: - IPA Installer - -/// Host-side IPA installer. Uses VPhoneSigner for re-signing, -/// ideviceinstaller for USB installation via usbmuxd. -@MainActor -class VPhoneIPAInstaller { - let signer: VPhoneSigner - private let ideviceInstallerURL: URL - private let ideviceIdURL: URL - - init?(signer: VPhoneSigner, bundle: Bundle = .main) { - guard let execURL = bundle.executableURL else { return nil } - let macosDir = execURL.deletingLastPathComponent() - - ideviceInstallerURL = macosDir.appendingPathComponent("ideviceinstaller") - ideviceIdURL = macosDir.appendingPathComponent("idevice_id") - - let fm = FileManager.default - guard fm.fileExists(atPath: ideviceInstallerURL.path), - fm.fileExists(atPath: ideviceIdURL.path) - else { return nil } - - self.signer = signer - } - - // MARK: - Install - - /// Install an IPA. If `resign` is true, re-sign all Mach-O binaries - /// preserving their original entitlements before installing. - func install(ipaURL: URL, resign: Bool) async throws { - let udid = try await getUDID() - print("[ipa] device UDID: \(udid)") - - var installURL = ipaURL - var tempDir: URL? - - if resign { - let dir = FileManager.default.temporaryDirectory - .appendingPathComponent("vphone-cli-resign-\(UUID().uuidString)") - tempDir = dir - installURL = try await resignIPA(ipaURL: ipaURL, tempDir: dir) - } - - defer { - if let tempDir { - try? FileManager.default.removeItem(at: tempDir) - } - } - - print("[ipa] installing \(installURL.lastPathComponent) to \(udid)...") - let result = try await signer.run( - ideviceInstallerURL, - arguments: ["-u", udid, "install", installURL.path] - ) - guard result.status == 0 else { - let msg = result.stderr.isEmpty ? result.stdout : result.stderr - throw IPAError.installFailed(msg.trimmingCharacters(in: .whitespacesAndNewlines)) - } - print("[ipa] installed successfully") - } - - // MARK: - UDID Discovery - - private func getUDID() async throws -> String { - let result = try await signer.run(ideviceIdURL, arguments: ["-l"]) - guard result.status == 0 else { - throw IPAError.noDevice - } - let udids = result.stdout - .components(separatedBy: .newlines) - .map { $0.trimmingCharacters(in: .whitespaces) } - .filter { !$0.isEmpty } - guard let first = udids.first else { - throw IPAError.noDevice - } - return first - } - - // MARK: - Re-sign IPA - - private func resignIPA(ipaURL: URL, tempDir: URL) async throws -> URL { - let fm = FileManager.default - try fm.createDirectory(at: tempDir, withIntermediateDirectories: true) - - // Unzip - print("[ipa] extracting \(ipaURL.lastPathComponent)...") - let unzip = try await signer.run( - URL(fileURLWithPath: "/usr/bin/unzip"), - arguments: ["-o", ipaURL.path, "-d", tempDir.path] - ) - guard unzip.status == 0 else { - throw IPAError.extractFailed(unzip.stderr) - } - - // Remove macOS resource fork files that break iOS installd - _ = try? await signer.run( - URL(fileURLWithPath: "/usr/bin/find"), - arguments: [tempDir.path, "-name", "._*", "-delete"] - ) - _ = try? await signer.run( - URL(fileURLWithPath: "/usr/bin/find"), - arguments: [tempDir.path, "-name", ".DS_Store", "-delete"] - ) - - // Find Payload/*.app - let payloadDir = tempDir.appendingPathComponent("Payload") - guard fm.fileExists(atPath: payloadDir.path) else { - throw IPAError.invalidIPA("no Payload directory") - } - let contents = try fm.contentsOfDirectory(atPath: payloadDir.path) - guard let appName = contents.first(where: { $0.hasSuffix(".app") }) else { - throw IPAError.invalidIPA("no .app bundle in Payload") - } - let appDir = payloadDir.appendingPathComponent(appName) - - // Walk and re-sign all Mach-O files - let machoFiles = signer.findMachOFiles(in: appDir) - print("[ipa] re-signing \(machoFiles.count) Mach-O binaries...") - - for file in machoFiles { - do { - try await signer.signFile(at: file, tempDir: tempDir) - } catch { - print("[ipa] warning: \(error)") - } - } - - // Re-zip (use zip from the temp dir so Payload/ is at the root) - let outputIPA = tempDir.appendingPathComponent("resigned.ipa") - print("[ipa] re-packaging...") - let zip = try await signer.run( - URL(fileURLWithPath: "/usr/bin/zip"), - arguments: ["-r", "-y", outputIPA.path, "Payload"], - currentDirectory: tempDir - ) - guard zip.status == 0 else { - throw IPAError.repackFailed(zip.stderr) - } - - return outputIPA - } - - // MARK: - Errors - - enum IPAError: Error, CustomStringConvertible { - case noDevice - case extractFailed(String) - case invalidIPA(String) - case repackFailed(String) - case installFailed(String) - - var description: String { - switch self { - case .noDevice: "no device found (is the VM running?)" - case let .extractFailed(msg): "failed to extract IPA: \(msg)" - case let .invalidIPA(msg): "invalid IPA: \(msg)" - case let .repackFailed(msg): "failed to repackage IPA: \(msg)" - case let .installFailed(msg): "install failed: \(msg)" - } - } - } -} diff --git a/sources/vphone-cli/VPhoneMenuConnect.swift b/sources/vphone-cli/VPhoneMenuConnect.swift index 3312d05..8af497b 100644 --- a/sources/vphone-cli/VPhoneMenuConnect.swift +++ b/sources/vphone-cli/VPhoneMenuConnect.swift @@ -9,7 +9,6 @@ extension VPhoneMenuController { menu.addItem(makeItem("File Browser", action: #selector(openFiles))) menu.addItem(NSMenuItem.separator()) menu.addItem(makeItem("Developer Mode Status", action: #selector(devModeStatus))) - menu.addItem(makeItem("Enable Developer Mode [WIP]", action: #selector(devModeEnable))) menu.addItem(NSMenuItem.separator()) menu.addItem(makeItem("Ping", action: #selector(sendPing))) menu.addItem(makeItem("Guest Version", action: #selector(queryGuestVersion))) @@ -36,23 +35,6 @@ extension VPhoneMenuController { } } - @objc func devModeEnable() { - Task { - do { - let result = try await control.sendDevModeEnable() - showAlert( - title: "Developer Mode", - message: result.message.isEmpty - ? (result.alreadyEnabled ? "Developer Mode already enabled." : "Developer Mode enabled.") - : result.message, - style: .informational - ) - } catch { - showAlert(title: "Developer Mode", message: "\(error)", style: .warning) - } - } - } - @objc func sendPing() { Task { do { diff --git a/sources/vphone-cli/VPhoneMenuController.swift b/sources/vphone-cli/VPhoneMenuController.swift index 6fb4813..a672a1b 100644 --- a/sources/vphone-cli/VPhoneMenuController.swift +++ b/sources/vphone-cli/VPhoneMenuController.swift @@ -17,8 +17,6 @@ class VPhoneMenuController { var locationReplayStopItem: NSMenuItem? var screenRecorder: VPhoneScreenRecorder? var recordingItem: NSMenuItem? - var signer: VPhoneSigner? - var ipaInstaller: VPhoneIPAInstaller? init(keyHelper: VPhoneKeyHelper, control: VPhoneControl) { self.keyHelper = keyHelper @@ -47,7 +45,6 @@ class VPhoneMenuController { mainMenu.addItem(buildKeysMenu()) mainMenu.addItem(buildTypeMenu()) mainMenu.addItem(buildConnectMenu()) - mainMenu.addItem(buildInstallMenu()) mainMenu.addItem(buildLocationMenu()) mainMenu.addItem(buildRecordMenu()) mainMenu.addItem(buildBatteryMenu()) diff --git a/sources/vphone-cli/VPhoneMenuInstall.swift b/sources/vphone-cli/VPhoneMenuInstall.swift deleted file mode 100644 index 443cf94..0000000 --- a/sources/vphone-cli/VPhoneMenuInstall.swift +++ /dev/null @@ -1,116 +0,0 @@ -import AppKit -import UniformTypeIdentifiers - -// MARK: - Install Menu - -extension VPhoneMenuController { - func buildInstallMenu() -> NSMenuItem { - let item = NSMenuItem() - let menu = NSMenu(title: "Install") - menu.addItem(makeItem("Install Package (.ipa) [WIP]", action: #selector(installPackage))) - menu.addItem(makeItem("Install Package with Resign (.ipa) [WIP]", action: #selector(installPackageResign))) - menu.addItem(NSMenuItem.separator()) - menu.addItem(makeItem("Upload Binary to Guest", action: #selector(uploadBinary))) - menu.addItem(makeItem("Upload Binary with Resign to Guest", action: #selector(uploadBinaryResign))) - item.submenu = menu - return item - } - - // MARK: - IPA Install - - @objc func installPackage() { - pickAndInstall(resign: false) - } - - @objc func installPackageResign() { - pickAndInstall(resign: true) - } - - private func pickAndInstall(resign: Bool) { - let panel = NSOpenPanel() - panel.title = "Select IPA" - panel.allowedContentTypes = [.init(filenameExtension: "ipa")!] - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - - guard panel.runModal() == .OK, let url = panel.url else { return } - - guard let installer = ipaInstaller else { - showAlert( - title: "Install Package", - message: "IPA installer not available (bundled tools missing).", - style: .warning - ) - return - } - - Task { - do { - try await installer.install(ipaURL: url, resign: resign) - showAlert( - title: "Install Package", - message: "Successfully installed \(url.lastPathComponent).", - style: .informational - ) - } catch { - showAlert( - title: "Install Package", - message: "\(error)", - style: .warning - ) - } - } - } - - // MARK: - Upload Binary - - @objc func uploadBinary() { - pickAndUploadBinary(resign: false) - } - - @objc func uploadBinaryResign() { - pickAndUploadBinary(resign: true) - } - - private func pickAndUploadBinary(resign: Bool) { - let panel = NSOpenPanel() - panel.title = "Select Binary to Upload" - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - - guard panel.runModal() == .OK, let url = panel.url else { return } - - Task { - do { - var data = try Data(contentsOf: url) - - if resign { - guard let signer else { - showAlert( - title: "Upload Binary", - message: "Signing tools not available (bundled tools missing).", - style: .warning - ) - return - } - data = try await signer.resign(data: data, filename: url.lastPathComponent) - } - - let filename = url.lastPathComponent - let remotePath = "/var/root/Library/Caches/\(filename)" - try await control.uploadFile(path: remotePath, data: data, permissions: "755") - showAlert( - title: "Upload Binary", - message: "Uploaded \(filename) to \(remotePath) (\(data.count) bytes)\(resign ? " [resigned]" : "").", - style: .informational - ) - } catch { - showAlert( - title: "Upload Binary", - message: "\(error)", - style: .warning - ) - } - } - } -} diff --git a/sources/vphone-cli/VPhoneSigner.swift b/sources/vphone-cli/VPhoneSigner.swift deleted file mode 100644 index e4ca68c..0000000 --- a/sources/vphone-cli/VPhoneSigner.swift +++ /dev/null @@ -1,157 +0,0 @@ -import Foundation - -// MARK: - Code Signer - -/// Host-side code signing using bundled ldid + signcert.p12. -/// Preserves existing entitlements when re-signing. -@MainActor -class VPhoneSigner { - private let ldidURL: URL - private let signcertURL: URL - - init?(bundle: Bundle = .main) { - guard let execURL = bundle.executableURL else { return nil } - let macosDir = execURL.deletingLastPathComponent() - let resourcesDir = macosDir - .deletingLastPathComponent() - .appendingPathComponent("Resources") - - ldidURL = macosDir.appendingPathComponent("ldid") - signcertURL = resourcesDir.appendingPathComponent("signcert.p12") - - let fm = FileManager.default - guard fm.fileExists(atPath: ldidURL.path), - fm.fileExists(atPath: signcertURL.path) - else { return nil } - } - - // MARK: - Sign Binary - - /// Re-sign a single Mach-O binary in-memory. Preserves existing entitlements. - func resign(data: Data, filename: String) async throws -> Data { - let tempDir = FileManager.default.temporaryDirectory - .appendingPathComponent("vphone-cli-sign-\(UUID().uuidString)") - try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) - defer { try? FileManager.default.removeItem(at: tempDir) } - - let binaryURL = tempDir.appendingPathComponent(filename) - try data.write(to: binaryURL) - - try await signFile(at: binaryURL, tempDir: tempDir) - - return try Data(contentsOf: binaryURL) - } - - /// Re-sign a Mach-O binary on disk in-place. Preserves existing entitlements. - func signFile(at url: URL, tempDir: URL) async throws { - let entsResult = try await run(ldidURL, arguments: ["-e", url.path]) - let entsXML = entsResult.stdout.trimmingCharacters(in: .whitespacesAndNewlines) - let cert = signcertURL.path - - var args: [String] - if !entsXML.isEmpty, entsXML.hasPrefix(" [URL] { - let fm = FileManager.default - guard let enumerator = fm.enumerator( - at: directory, - includingPropertiesForKeys: [.isRegularFileKey], - options: [.skipsHiddenFiles] - ) else { return [] } - - var results: [URL] = [] - for case let url as URL in enumerator { - guard let values = try? url.resourceValues(forKeys: [.isRegularFileKey]), - values.isRegularFile == true, - Self.isMachO(at: url) - else { continue } - results.append(url) - } - return results - } - - static func isMachO(at url: URL) -> Bool { - guard let fh = try? FileHandle(forReadingFrom: url) else { return false } - defer { try? fh.close() } - guard let data = try? fh.read(upToCount: 4), data.count == 4 else { return false } - let magic = data.withUnsafeBytes { $0.load(as: UInt32.self) } - return magic == 0xFEED_FACF // MH_MAGIC_64 - || magic == 0xCFFA_EDFE // MH_CIGAM_64 - || magic == 0xFEED_FACE // MH_MAGIC - || magic == 0xCEFA_EDFE // MH_CIGAM - || magic == 0xCAFE_BABE // FAT_MAGIC - || magic == 0xBEBA_FECA // FAT_CIGAM - } - - // MARK: - Process Runner - - struct ProcessResult: Sendable { - let stdout: String - let stderr: String - let status: Int32 - } - - func run( - _ executable: URL, - arguments: [String], - currentDirectory: URL? = nil - ) async throws -> ProcessResult { - let execPath = executable.path - let args = arguments - let dirPath = currentDirectory?.path - - return try await Task.detached { - let process = Process() - process.executableURL = URL(fileURLWithPath: execPath) - process.arguments = args - if let dirPath { - process.currentDirectoryURL = URL(fileURLWithPath: dirPath) - } - - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - process.standardOutput = stdoutPipe - process.standardError = stderrPipe - - try process.run() - process.waitUntilExit() - - let outData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() - let errData = stderrPipe.fileHandleForReading.readDataToEndOfFile() - - return ProcessResult( - stdout: String(data: outData, encoding: .utf8) ?? "", - stderr: String(data: errData, encoding: .utf8) ?? "", - status: process.terminationStatus - ) - }.value - } - - // MARK: - Errors - - enum SignError: Error, CustomStringConvertible { - case ldidFailed(String, String) - - var description: String { - switch self { - case let .ldidFailed(file, msg): "failed to sign \(file): \(msg)" - } - } - } -} From 9a25b6958c46ce6efea4bec27726e66329ea2e3a Mon Sep 17 00:00:00 2001 From: Lakr Date: Sat, 7 Mar 2026 18:40:02 +0800 Subject: [PATCH 05/10] Update setup_machine.sh --- scripts/setup_machine.sh | 74 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/scripts/setup_machine.sh b/scripts/setup_machine.sh index 59d7866..c9a8a35 100755 --- a/scripts/setup_machine.sh +++ b/scripts/setup_machine.sh @@ -550,6 +550,52 @@ wait_for_first_boot_prompt_auto() { esac } +wait_for_device_ssh() { + local port="${1:-22222}" + local timeout="${2:-120}" + local pass="${3:-alpine}" + local sshpass_bin waited=0 + + sshpass_bin="$(command -v sshpass || true)" + [[ -x "$sshpass_bin" ]] || die "sshpass not found (run: make setup_tools)" + + echo "[*] Waiting for device SSH on localhost:${port} (timeout=${timeout}s)..." + while (( waited < timeout )); do + if [[ -n "$BOOT_PID" ]] && ! kill -0 "$BOOT_PID" 2>/dev/null; then + die "VM exited while waiting for device SSH." + fi + if "$sshpass_bin" -p "$pass" ssh \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o PreferredAuthentications=password \ + -o ConnectTimeout=5 -q \ + -p "$port" root@localhost "echo ready" >/dev/null 2>&1; then + echo "[+] Device SSH is ready on port ${port}" + return + fi + if (( waited == 0 || waited % 10 == 0 )); then + echo " waiting... ${waited}s elapsed" + fi + sleep 2 + (( waited += 2 )) + done + die "Device SSH not ready after ${timeout}s" +} + +halt_device_ssh() { + local port="${1:-22222}" + local pass="${2:-alpine}" + local sshpass_bin + sshpass_bin="$(command -v sshpass)" + echo "[*] Halting device via SSH..." + "$sshpass_bin" -p "$pass" ssh \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o PreferredAuthentications=password \ + -o ConnectTimeout=10 -q \ + -p "$port" root@localhost "halt" 2>/dev/null || true +} + run_boot_analysis() { local boot_state @@ -984,6 +1030,34 @@ main() { rm -f "$BOOT_FIFO" || true BOOT_FIFO="" + if [[ "$JB_MODE" -eq 1 ]]; then + echo "" + echo "=== JB Finalize ===" + echo "[*] Booting VM normally for JB bootstrap finalization..." + + check_vm_storage_locks + mkdir -p "$LOG_DIR" + : > "$BOOT_LOG" + (make boot >"$BOOT_LOG" 2>&1) & + BOOT_PID=$! + + sleep 2 + if ! kill -0 "$BOOT_PID" 2>/dev/null; then + echo "[-] make boot exited early during JB finalize stage." + tail -n 40 "$BOOT_LOG" 2>/dev/null || true + die "JB finalize boot failed." + fi + + wait_for_device_ssh 22222 120 + + run_make "JB finalize" cfw_install_jb_finalize SSH_PORT=22222 + + halt_device_ssh 22222 + echo "[*] Waiting for VM shutdown..." + wait "$BOOT_PID" || true + BOOT_PID="" + fi + echo "" echo "=== Done ===" echo "Setup completed." From 84af29302913831cb40ce7f66f55963605c1a41b Mon Sep 17 00:00:00 2001 From: Lakr Date: Sat, 7 Mar 2026 18:44:09 +0800 Subject: [PATCH 06/10] Window title: show VPHONE+/- for daemon status, subtitle shows ECID --- sources/vphone-cli/VPhoneAppDelegate.swift | 3 ++- sources/vphone-cli/VPhoneVirtualMachine.swift | 6 +++++- sources/vphone-cli/VPhoneWindowController.swift | 10 +++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/sources/vphone-cli/VPhoneAppDelegate.swift b/sources/vphone-cli/VPhoneAppDelegate.swift index 409ce2b..8b21159 100644 --- a/sources/vphone-cli/VPhoneAppDelegate.swift +++ b/sources/vphone-cli/VPhoneAppDelegate.swift @@ -118,7 +118,8 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate { screenHeight: cli.screenHeight, screenScale: cli.screenScale, keyHelper: keyHelper, - control: control + control: control, + ecid: vm.ecidHex ) windowController = wc diff --git a/sources/vphone-cli/VPhoneVirtualMachine.swift b/sources/vphone-cli/VPhoneVirtualMachine.swift index cb3da21..5b92e79 100644 --- a/sources/vphone-cli/VPhoneVirtualMachine.swift +++ b/sources/vphone-cli/VPhoneVirtualMachine.swift @@ -6,6 +6,8 @@ import Virtualization @MainActor class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate { let virtualMachine: VZVirtualMachine + /// ECID hex string resolved from machineIdentifier (e.g. "0x0012345678ABCDEF"). + let ecidHex: String? /// Read handle for VM serial output. private var serialOutputReadHandle: FileHandle? /// Synthetic battery source for runtime charge/connectivity updates. @@ -57,7 +59,8 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate { platform.machineIdentifier = machineIdentifier if let identity = Self.resolveDeviceIdentity(machineIdentifier: machineIdentifier) { - print("[vphone] ECID: 0x\(identity.ecidHex)") + ecidHex = "0x\(identity.ecidHex)" + print("[vphone] ECID: \(ecidHex!)") print("[vphone] Predicted UDID: \(identity.udid)") do { let outputURL = try Self.writeUDIDPrediction( @@ -68,6 +71,7 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate { print("[vphone] Warning: failed to write udid-prediction.txt: \(error)") } } else { + ecidHex = nil print("[vphone] Warning: failed to resolve ECID from machineIdentifier") } diff --git a/sources/vphone-cli/VPhoneWindowController.swift b/sources/vphone-cli/VPhoneWindowController.swift index 470e65e..30db2d4 100644 --- a/sources/vphone-cli/VPhoneWindowController.swift +++ b/sources/vphone-cli/VPhoneWindowController.swift @@ -12,7 +12,7 @@ class VPhoneWindowController: NSObject, NSToolbarDelegate { func showWindow( for vm: VZVirtualMachine, screenWidth: Int, screenHeight: Int, screenScale: Double, - keyHelper: VPhoneKeyHelper, control: VPhoneControl + keyHelper: VPhoneKeyHelper, control: VPhoneControl, ecid: String? ) { self.control = control @@ -36,8 +36,8 @@ class VPhoneWindowController: NSObject, NSToolbarDelegate { window.isReleasedWhenClosed = false window.contentAspectRatio = windowSize - window.title = "vphone" - window.subtitle = "daemon connecting..." + window.title = "VPHONE-" + window.subtitle = ecid ?? "" window.contentView = vmView window.center() @@ -57,12 +57,12 @@ class VPhoneWindowController: NSObject, NSToolbarDelegate { window.makeFirstResponder(view) NSApp.activate(ignoringOtherApps: true) - // Poll vphoned status for subtitle + // Poll vphoned status for title indicator statusTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self, weak window] _ in Task { @MainActor in guard let self, let window, let control = self.control else { return } - window.subtitle = control.isConnected ? "daemon connected" : "daemon connecting..." + window.title = control.isConnected ? "VPHONE+" : "VPHONE-" } } } From 1cbc389abc0033440d4a56e9b995378ded7f05d2 Mon Sep 17 00:00:00 2001 From: Lakr Date: Sat, 7 Mar 2026 18:46:48 +0800 Subject: [PATCH 07/10] Update VPhoneWindowController.swift --- sources/vphone-cli/VPhoneWindowController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/vphone-cli/VPhoneWindowController.swift b/sources/vphone-cli/VPhoneWindowController.swift index 30db2d4..0079530 100644 --- a/sources/vphone-cli/VPhoneWindowController.swift +++ b/sources/vphone-cli/VPhoneWindowController.swift @@ -36,7 +36,7 @@ class VPhoneWindowController: NSObject, NSToolbarDelegate { window.isReleasedWhenClosed = false window.contentAspectRatio = windowSize - window.title = "VPHONE-" + window.title = "VPHONE ⏳" window.subtitle = ecid ?? "" window.contentView = vmView window.center() @@ -62,7 +62,7 @@ class VPhoneWindowController: NSObject, NSToolbarDelegate { [weak self, weak window] _ in Task { @MainActor in guard let self, let window, let control = self.control else { return } - window.title = control.isConnected ? "VPHONE+" : "VPHONE-" + window.title = control.isConnected ? "VPHONE 🔗" : "VPHONE ⛓️‍💥" } } } From a576f5edf4c9e05901dfbbc984df12cac2726f02 Mon Sep 17 00:00:00 2001 From: Lakr Date: Sat, 7 Mar 2026 18:47:20 +0800 Subject: [PATCH 08/10] Update VPhoneVirtualMachine.swift --- sources/vphone-cli/VPhoneVirtualMachine.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/vphone-cli/VPhoneVirtualMachine.swift b/sources/vphone-cli/VPhoneVirtualMachine.swift index 5b92e79..21edcce 100644 --- a/sources/vphone-cli/VPhoneVirtualMachine.swift +++ b/sources/vphone-cli/VPhoneVirtualMachine.swift @@ -59,7 +59,7 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate { platform.machineIdentifier = machineIdentifier if let identity = Self.resolveDeviceIdentity(machineIdentifier: machineIdentifier) { - ecidHex = "0x\(identity.ecidHex)" + ecidHex = identity.ecidHex print("[vphone] ECID: \(ecidHex!)") print("[vphone] Predicted UDID: \(identity.udid)") do { From c0fda232e04477babc7e053d06f47191d45214bd Mon Sep 17 00:00:00 2001 From: Lakr Date: Sat, 7 Mar 2026 19:03:18 +0800 Subject: [PATCH 09/10] Fix marker creation using shell builtin instead of touch After prep_bootstrap.sh changes the login shell to bash, `touch` is not available in the remote PATH. Use `: >` (shell builtin) to create empty marker files without depending on an external binary. --- scripts/cfw_install_jb_post.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cfw_install_jb_post.sh b/scripts/cfw_install_jb_post.sh index de82474..1b13d8a 100755 --- a/scripts/cfw_install_jb_post.sh +++ b/scripts/cfw_install_jb_post.sh @@ -152,7 +152,7 @@ for marker in .procursus_strapped .installed_dopamine; do if ssh_cmd "test -f /var/jb/$marker"; then echo " [*] $marker already exists, skipping" else - ssh_cmd "touch /var/jb/$marker && chown 0:0 /var/jb/$marker && chmod 0644 /var/jb/$marker" + ssh_cmd ": > /var/jb/$marker && chown 0:0 /var/jb/$marker && chmod 0644 /var/jb/$marker" echo " [+] $marker created" fi done From 46eb61f51daaca7ec494475e521c235634edc447 Mon Sep 17 00:00:00 2001 From: Lakr Date: Sat, 7 Mar 2026 19:04:31 +0800 Subject: [PATCH 10/10] JB finalize: randomize SSH port via iproxy instead of hardcoded 22222 Use pick_random_ssh_port + iproxy to forward a random local port to guest:22222 for isolation, matching the ramdisk stage pattern. --- scripts/setup_machine.sh | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/scripts/setup_machine.sh b/scripts/setup_machine.sh index c9a8a35..8aa76d3 100755 --- a/scripts/setup_machine.sh +++ b/scripts/setup_machine.sh @@ -1048,11 +1048,35 @@ main() { die "JB finalize boot failed." fi - wait_for_device_ssh 22222 120 + local jb_ssh_port jb_iproxy_pid jb_iproxy_log + local iproxy_bin="${PROJECT_ROOT}/.limd/bin/iproxy" - run_make "JB finalize" cfw_install_jb_finalize SSH_PORT=22222 + jb_ssh_port="$(pick_random_ssh_port)" \ + || die "Failed to allocate a random local SSH port for JB finalize" - halt_device_ssh 22222 + jb_iproxy_log="${LOG_DIR}/iproxy_jb_${jb_ssh_port}.log" + : > "$jb_iproxy_log" + + echo "[*] Waiting for device UDID=${DEVICE_UDID} on USBMux..." + wait_for_iproxy_target_udid + + echo "[*] Starting iproxy ${jb_ssh_port} -> 22222 (target_udid=${IPROXY_TARGET_UDID})..." + ("$iproxy_bin" -u "$IPROXY_TARGET_UDID" "$jb_ssh_port" 22222 >"$jb_iproxy_log" 2>&1) & + jb_iproxy_pid=$! + sleep 1 + if ! kill -0 "$jb_iproxy_pid" 2>/dev/null; then + echo "[-] iproxy exited early. Log:" + tail -n 40 "$jb_iproxy_log" || true + die "iproxy for JB finalize failed to start." + fi + echo "[+] iproxy running (pid=$jb_iproxy_pid, log=$jb_iproxy_log)" + + wait_for_device_ssh "$jb_ssh_port" 120 + + run_make "JB finalize" cfw_install_jb_finalize SSH_PORT="$jb_ssh_port" + + halt_device_ssh "$jb_ssh_port" + stop_process_tree "$jb_iproxy_pid" 2>/dev/null || true echo "[*] Waiting for VM shutdown..." wait "$BOOT_PID" || true BOOT_PID=""