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
This commit is contained in:
Lakr
2026-03-07 18:07:27 +08:00
parent 6c4165c7fe
commit b9b462d23f
22 changed files with 447 additions and 582 deletions
+1 -1
View File
@@ -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
+6 -17
View File
@@ -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" .
+1
View File
@@ -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"),
+3 -5
View File
@@ -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
+1 -1
View File
@@ -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) を参照してください。
## 前提条件
+1 -1
View File
@@ -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)를 참조하세요.
## 사전 요구 사항
+1 -1
View File
@@ -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) 了解各组件的详细分项对比。
## 先决条件
@@ -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=<method>`) 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`
@@ -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)
@@ -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)
---
@@ -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/<hash>/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`
@@ -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
+72 -56
View File
@@ -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 ""
+46 -18
View File
@@ -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
-82
View File
@@ -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=<jb_patch_method>)")
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()
-97
View File
@@ -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"
+15 -3
View File
@@ -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 <binary> <dylib_path>")
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}")
-241
View File
@@ -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("<I", data, 0)[0]
if magic != 0xFEEDFACF:
return -1
ncmds = struct.unpack_from("<I", data, 16)[0]
offset = 32 # sizeof(mach_header_64)
earliest = len(data)
for _ in range(ncmds):
cmd, cmdsize = struct.unpack_from("<II", data, offset)
if cmd == 0x19: # LC_SEGMENT_64
nsects = struct.unpack_from("<I", data, offset + 64)[0]
sect_off = offset + 72
for _ in range(nsects):
file_off = struct.unpack_from("<I", data, sect_off + 48)[0]
size = struct.unpack_from("<Q", data, sect_off + 40)[0]
if file_off > 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("<I", data, base)[0]
if magic != 0xFEEDFACF:
return False
ncmds = struct.unpack_from("<I", data, base + 16)[0]
offset = base + 32
for _ in range(ncmds):
cmd, cmdsize = struct.unpack_from("<II", data, offset)
if cmd in (0xC, 0xD, 0x18, 0x1F, 0x80000018):
# LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB, LC_LAZY_LOAD_DYLIB,
# LC_REEXPORT_DYLIB, LC_LOAD_UPWARD_DYLIB
name_offset = struct.unpack_from("<I", data, offset + 8)[0]
name_end = data.index(0, offset + name_offset)
name = data[offset + name_offset : name_end].decode(
"ascii", errors="replace"
)
if name == dylib_path:
return True
offset += cmdsize
return False
def _strip_codesig(data, base):
"""Strip LC_CODE_SIGNATURE if it's the last load command.
Zeros out the command bytes and decrements ncmds/sizeofcmds.
Returns the cmdsize of the removed command, or 0 if not stripped.
Since the binary will be re-signed by ldid, this is always safe.
"""
ncmds = struct.unpack_from("<I", data, base + 16)[0]
sizeofcmds = struct.unpack_from("<I", data, base + 20)[0]
offset = base + 32
last_offset = -1
last_cmd = 0
last_cmdsize = 0
for i in range(ncmds):
cmd, cmdsize = struct.unpack_from("<II", data, offset)
if i == ncmds - 1:
last_offset = offset
last_cmd = cmd
last_cmdsize = cmdsize
offset += cmdsize
if last_cmd != 0x1D: # LC_CODE_SIGNATURE
return 0
# Zero out the LC_CODE_SIGNATURE command
data[last_offset : last_offset + last_cmdsize] = b"\x00" * last_cmdsize
# Update header
struct.pack_into("<I", data, base + 16, ncmds - 1)
struct.pack_into("<I", data, base + 20, sizeofcmds - last_cmdsize)
print(f" Stripped LC_CODE_SIGNATURE ({last_cmdsize} bytes freed)")
return last_cmdsize
def _inject_lc_load_dylib(data, base, dylib_path):
"""Inject LC_LOAD_DYLIB into a single Mach-O slice starting at `base`.
Strategy (matches optool/insert_dylib behavior):
1. Try to fit new LC in existing zero-padding after load commands.
2. If not enough space, strip LC_CODE_SIGNATURE (re-signed by ldid anyway).
3. If still not enough, allow header to overflow into section data
(same approach as optool — the overwritten bytes are typically stub
code that the jailbreak hook replaces).
Returns True on success.
"""
magic = struct.unpack_from("<I", data, base)[0]
if magic != 0xFEEDFACF:
print(f" [-] Not a 64-bit Mach-O at offset 0x{base:X}")
return False
ncmds = struct.unpack_from("<I", data, base + 16)[0]
sizeofcmds = struct.unpack_from("<I", data, base + 20)[0]
# Build the LC_LOAD_DYLIB command
name_bytes = dylib_path.encode("ascii") + b"\x00"
name_offset_in_cmd = 24 # sizeof(dylib_command) header
cmd_size = _align(name_offset_in_cmd + len(name_bytes), 8)
lc_data = bytearray(cmd_size)
struct.pack_into("<I", lc_data, 0, 0xC) # cmd = LC_LOAD_DYLIB
struct.pack_into("<I", lc_data, 4, cmd_size) # cmdsize
struct.pack_into("<I", lc_data, 8, name_offset_in_cmd) # name offset
struct.pack_into("<I", lc_data, 12, 2) # timestamp
struct.pack_into("<I", lc_data, 16, 0) # current_version
struct.pack_into("<I", lc_data, 20, 0) # compat_version
lc_data[name_offset_in_cmd : name_offset_in_cmd + len(name_bytes)] = name_bytes
# Check available space
header_end = base + 32 + sizeofcmds # end of current load commands
first_section = _find_first_section_offset(data[base:])
if first_section < 0:
print(f" [-] Could not determine section offsets")
return False
first_section_abs = base + first_section
available = first_section_abs - header_end
print(
f" Header end: 0x{header_end:X}, first section: 0x{first_section_abs:X}, "
f"available: {available}, need: {cmd_size}"
)
if available < cmd_size:
# Strip LC_CODE_SIGNATURE to reclaim header space (re-signed by ldid)
freed = _strip_codesig(data, base)
if freed > 0:
ncmds = struct.unpack_from("<I", data, base + 16)[0]
sizeofcmds = struct.unpack_from("<I", data, base + 20)[0]
header_end = base + 32 + sizeofcmds
available = first_section_abs - header_end
print(f" After strip: available={available}, need={cmd_size}")
if available < cmd_size:
overflow = cmd_size - available
# Allow up to 256 bytes overflow (same behavior as optool/insert_dylib)
if overflow > 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("<I", data, base + 16, ncmds + 1)
struct.pack_into("<I", data, base + 20, sizeofcmds + cmd_size)
return True
def inject_dylib(filepath, dylib_path):
"""Inject LC_LOAD_DYLIB into a Mach-O binary (thin or universal/FAT).
Equivalent to: optool install -c load -p <dylib_path> -t <filepath>
"""
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
# ══════════════════════════════════════════════════════════════════
-4
View File
@@ -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 ==="
+35 -5
View File
@@ -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 ""
-1
View File
@@ -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()
}
@@ -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