diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..843fc39 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Generated firmware output (produced by get_files.sh / fix_perms.sh) +/firmware/ +/firmware-*/ + +# ipsw's local extraction cache (created by get_files.sh) +/ipsw_db/ + +# Downloaded iOS sysroot bundle, used to patch the ramdisk on iphoneos targets +/sysroot/ +/sysroot.tar.gz + +# Scratch mount points, in case one is left behind after a failed run +/mnt/ + +# qemu-sptm build output (the submodule ships its own .gitignore too, but this +# covers the common case of running `mkdir build` from the repo root) +/qemu-sptm/build/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a53f80d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ +# Changelog + +## 2026-08-29 — Linux host support + boot-noise fixes + +Adds a Linux-hosted path through the firmware-prep pipeline (`get_files.sh` / +`fix_perms.sh`), which upstream only supports on macOS, plus two kernel-side +patches that silence harmless-but-noisy console spam on any host. See +[LINUX.md](LINUX.md) / [LINUX.zh-CN.md](LINUX.zh-CN.md) for full details and +rationale. + +### Added +- `dmgutil.sh` — shared cross-platform helpers (`dmg_attach`, `dmg_detach`, + `copy_tree`) for mounting/unmounting `ramdisk.dmg` and copying directory + trees into it. macOS still goes through `hdiutil`/`ditto`; Linux goes + through the `linux-apfs-rw` kernel module and `cp -a --remove-destination`. +- `patch_bootkc.py` — truncates a known-unconditional, always-noisy + `shared_region: ... check_np(...)` printf format string in `bootkc` by + overwriting its first byte with a NUL. Zero instruction changes; verified + byte-identical on both an iOS and a macOS `bootkc`. Wired into + `get_files.sh`'s `main()`, so it runs automatically on every fetch. +- `LINUX.md` / `LINUX.zh-CN.md` — setup guide for the Linux host path + (ipsw/ldid/linux-apfs-rw build instructions), an explanation of both + boot-noise patches, and the host hardware this was verified on. +- `.gitignore` — excludes generated/downloaded output (`firmware/`, + `firmware-*/`, `ipsw_db/`, `sysroot/`, `sysroot.tar.gz`, `mnt/`, + `qemu-sptm/build/`). + +### Changed +- `get_files.sh` — sources `dmgutil.sh`; `ensure_installed` checks for + `ldid` on Linux; `patch_ramdisk` no longer exits early on non-Darwin + hosts and uses `dmg_attach`/`dmg_detach`/`copy_tree` plus + `ldid -Cadhoc -S` / `ldid -h` in place of `codesign` on Linux; new + `patch_bootkc` step runs right after `bootkc` is downloaded. +- `fix_perms.sh` — sources `dmgutil.sh`, uses `dmg_attach`/`dmg_detach`, and + chowns to `0:0` instead of `root:wheel` on Linux (same uid/gid pair). +- `dt_fixup.py` — removes the `sep` device-tree node and sets + `sepfw-load-at-boot=0`, stopping `AppleCredentialManager`'s infinite + `ACMTRM: waitForSEPEndpoint: timed out waiting for AppleSEPManager` retry + loop (this VM never emulates a SEP). Applies to iOS and macOS guests + equally, independent of host OS. +- `run.sh` — added `trm_enabled=0 hidrm_enabled=0` to `BOOT_ARGS` as a + secondary, harmless mitigation for the same SEP-retry noise (kept even + though the device-tree fix above is what actually stops it). + +All host-OS-specific behavior is gated behind `[[ "$(uname)" == "Darwin" ]]` +checks, so none of the above changes anything about the macOS code path. diff --git a/LINUX.md b/LINUX.md new file mode 100644 index 0000000..a6baf26 --- /dev/null +++ b/LINUX.md @@ -0,0 +1,126 @@ +# Running darwin-vm on Linux + +This document covers how to run darwin-vm's firmware-preparation pipeline +(`get_files.sh` / `fix_perms.sh`) on a Linux host, without a Mac. This isn't +part of upstream darwin-vm (which assumes macOS for the firmware-prep step, +since it shells out to `hdiutil`/`ditto`/`codesign`); everything here layers +on top of it and doesn't change the macOS path at all. + +See [LINUX.zh-CN.md](LINUX.zh-CN.md) for the Chinese version of this doc. + +## Extra host dependencies + +Besides what the main README already lists (`jq`, `wget`), you'll also need: + +- **ipsw** (github.com/blacktop/ipsw). `go install .../ipsw@latest` fails + because its go.mod has `replace` directives; build from a clone instead: + ``` + git clone https://github.com/blacktop/ipsw.git + cd ipsw && go build -o ipsw ./cmd/ipsw + sudo cp ipsw /usr/local/bin/ + ``` +- **ldid** (github.com/ProcursusTeam/ldid). Stands in for macOS's + `codesign` for ad-hoc signing; its `-S`/`-h` output is textually + compatible with `codesign -s -` / `codesign -d -vvv`. + ``` + sudo apt-get install -y libplist-dev + git clone https://github.com/ProcursusTeam/ldid.git + cd ldid && make + sudo cp ldid /usr/local/bin/ + ``` +- **linux-apfs-rw** kernel module (github.com/linux-apfs/linux-apfs-rw). + `firmware/ramdisk.dmg` turns out to be a raw APFS container (no UDIF/HFS+ + wrapper), so it's mounted directly using this out-of-tree, experimental + read-write APFS driver. Build it against your running kernel's headers: + ``` + sudo apt-get install -y linux-headers-$(uname -r) + git clone https://github.com/linux-apfs/linux-apfs-rw.git + cd linux-apfs-rw && make + sudo modprobe libcrc32c + sudo insmod apfs.ko + ``` + `insmod` doesn't persist across reboots — re-run it after every reboot (or + set up `depmod`/`modules-load.d` yourself). + +## What changed to make this work + +- **`dmgutil.sh`** (new) centralizes DMG mount/unmount and directory-copy + logic used by `get_files.sh` and `fix_perms.sh`. On macOS it's a thin + wrapper over `hdiutil`/`ditto` — unchanged behavior. On Linux it mounts via + the `apfs` kernel module and copies with `cp -a --remove-destination` (the + `--remove-destination` matters: the Linux apfs driver's experimental write + support doesn't implement `O_TRUNC`, so overwriting an existing file has to + unlink-then-recreate rather than truncate-in-place, or `cp` fails with + "Operation not supported"). +- **`get_files.sh` / `fix_perms.sh`** now `source dmgutil.sh` and no longer + bail out with "this isn't a Mac" on Linux — the ramdisk-patching and + permission-fixing steps run the same way on both platforms, just through + `dmgutil.sh`'s cross-platform helpers. `codesign` is swapped for + `ldid -Cadhoc -S` / `ldid -h` on Linux. +- `chown root:wheel` becomes `chown 0:0` on Linux — same uid/gid pair, since + macOS's `wheel` group is gid 0, same as Linux's `root` group. XNU only + checks the numeric ids. + +None of this touches the macOS code path: every new branch is gated behind +`[[ "$(uname)" == "Darwin" ]]`, so pulling this code onto a Mac is safe — it +runs the exact same `hdiutil`/`ditto`/`codesign` commands as before. + +## Boot-noise patches + +Two log lines print constantly in this VM because it doesn't emulate a SEP +(Secure Enclave Processor) or provide a real dyld shared cache. Neither +affects correctness — commands still run and return correct output — but +the first is a genuine infinite retry loop and the second reprints on every +single process launch, so both were silenced. These patches apply to iOS +*and* macOS guests (both share the same XNU code paths) and are host-OS +agnostic — they're equally worth carrying over if you build firmware on an +actual Mac. + +1. **`ACMTRM: waitForSEPEndpoint: timed out waiting for AppleSEPManager`** + (repeats every ~5s forever). `AppleCredentialManager` believes a SEP + exists (per the device tree) and retries forever. Neither the + `trm_enabled=0` boot-arg nor a `sepfw-load-at-boot=0` device-tree property + stopped it — what does work is removing the `sep` node from the device + tree entirely, in `dt_fixup.py`: + ```python + d['arm-io'].remove_child('sep') + ``` + so the driver never finds a SEP nub to probe in the first place. + (`run.sh`'s `BOOT_ARGS` also still sets `trm_enabled=0 hidrm_enabled=0` as + a harmless belt-and-braces measure, kept even though it wasn't sufficient + on its own.) + +2. **`shared_region: %p [%d(%s)] check_np(...) vm_shared_region_start_address() + returned 0x1`** — printed unconditionally (no boot-arg or sysctl we could + find gates it) on every `check_np()` syscall, i.e. on every single process + launch. `patch_bootkc.py` (new, wired into `get_files.sh`'s `main()` right + after `bootkc` is downloaded) truncates this printf's format string in + place by overwriting its first byte with a NUL. That's a 1-byte data + patch and zero instruction changes: printf-family functions never read + their vararg list when the format string has no `%` directives, so this + can't change behavior beyond suppressing the print. Verified against both + an iOS (`iPhone17,3`) and a macOS (`Mac16,10`) `bootkc` — the string + exists exactly once, byte-identical, in both. + +Both patches run automatically as part of `get_files.sh`; there's no manual +step, and no risk of forgetting them on a re-run. + +## Tested host configuration + +The main README's compatibility table is about which *guest* devices/OS +versions boot. The Linux-hosting path above was additionally verified on +this host: + +| | | +|---|---| +| CPU | AMD Ryzen 9 9950X (16 cores / 32 threads) | +| Motherboard | ASUS ROG Crosshair X870E Hero | +| GPU | NVIDIA GeForce RTX 2080 Ti | +| Memory | 64 GB | +| Architecture | x86_64 | +| OS | Ubuntu 24.04 LTS, kernel 7.0.0-30-generic | + +None of this is a hard requirement — the pipeline is single-threaded CPU +work plus a software-emulated qemu VM, so far more modest Linux hardware +should work fine. It's listed here only as the known-good reference +configuration this was actually tested on. diff --git a/LINUX.zh-CN.md b/LINUX.zh-CN.md new file mode 100644 index 0000000..27fc5bf --- /dev/null +++ b/LINUX.zh-CN.md @@ -0,0 +1,69 @@ +# 在 Linux 上运行 darwin-vm + +本文档说明如何在**不用 Mac** 的情况下,在 Linux 主机上跑通 darwin-vm 的固件准备流程(`get_files.sh` / `fix_perms.sh`)。这不是上游 darwin-vm 自带的功能——它默认固件准备这一步必须在 macOS 上做(因为要调用 `hdiutil`/`ditto`/`codesign`);这里的所有改动都是叠加在原有流程之上的,**完全不影响 macOS 那条路径**。 + +英文版见 [LINUX.md](LINUX.md)。 + +## 额外的主机依赖 + +除了主 README 里已经列出的 `jq`、`wget`,Linux 主机还需要: + +- **ipsw**(github.com/blacktop/ipsw)。直接 `go install .../ipsw@latest` 会失败(它的 go.mod 里有 `replace` 指令),需要克隆源码自己编译: + ``` + git clone https://github.com/blacktop/ipsw.git + cd ipsw && go build -o ipsw ./cmd/ipsw + sudo cp ipsw /usr/local/bin/ + ``` +- **ldid**(github.com/ProcursusTeam/ldid)。用来替代 macOS 的 `codesign` 做临时(ad-hoc)签名;它的 `-S`/`-h` 输出格式跟 `codesign -s -` / `codesign -d -vvv` 是文本兼容的,可以直接复用原来的 grep 解析逻辑。 + ``` + sudo apt-get install -y libplist-dev + git clone https://github.com/ProcursusTeam/ldid.git + cd ldid && make + sudo cp ldid /usr/local/bin/ + ``` +- **linux-apfs-rw 内核模块**(github.com/linux-apfs/linux-apfs-rw)。实测发现 `firmware/ramdisk.dmg` 其实是一个**裸的 APFS 容器**(没有 UDIF/HFS+ 那层包装),所以直接用这个树外的、实验性的读写 APFS 驱动挂载即可。需要对应当前内核版本的 headers: + ``` + sudo apt-get install -y linux-headers-$(uname -r) + git clone https://github.com/linux-apfs/linux-apfs-rw.git + cd linux-apfs-rw && make + sudo modprobe libcrc32c + sudo insmod apfs.ko + ``` + `insmod` 不会在重启后自动生效——每次重启都要重新加载一次(或者自己配置 `depmod`/`modules-load.d`)。 + +## 具体改了什么 + +- **`dmgutil.sh`(新增)**:把 DMG 挂载/卸载、目录拷贝的逻辑统一收拢到这一个文件里,供 `get_files.sh` 和 `fix_perms.sh` 共用。在 macOS 上它只是 `hdiutil`/`ditto` 的一层薄封装,行为跟原来完全一样;在 Linux 上则用 `apfs` 内核模块挂载,用 `cp -a --remove-destination` 拷贝文件(这个 `--remove-destination` 是必须的:Linux 版 apfs 驱动的实验性写支持没实现 `O_TRUNC`,所以覆盖已存在的文件必须走"先删除再创建",而不是"原地截断",否则 `cp` 会报 "Operation not supported")。 +- **`get_files.sh` / `fix_perms.sh`**:现在会 `source dmgutil.sh`,并且不再在 Linux 上直接打印"这不是 Mac"然后退出——打补丁 ramdisk、修复权限这两步在两个平台上走的是同一套逻辑,只是底层调用 `dmgutil.sh` 里对应平台的实现。`codesign` 在 Linux 上对应换成了 `ldid -Cadhoc -S` / `ldid -h`。 +- `chown root:wheel` 在 Linux 上变成了 `chown 0:0`——数值上是同一对 uid/gid(macOS 的 `wheel` 组 gid 就是 0,跟 Linux 的 `root` 组一样),XNU 只看数字 id,不看组名。 + +以上这些改动**完全不影响 macOS 路径**:每一处新增分支都用 `[[ "$(uname)" == "Darwin" ]]` 做了判断,所以把这份改过的代码直接拿到 Mac 上跑是安全的——走的还是原来一模一样的 `hdiutil`/`ditto`/`codesign` 调用。 + +## 启动噪音的两处补丁 + +这个 VM 里有两条日志会持续刷屏,原因都是**没有模拟真正的 SEP(安全隔区协处理器)**、也**没有真正的 dyld 共享缓存**。两条都不影响功能正确性——命令照样能跑、结果照样正确——但第一条是真正的无限重试循环,第二条则是每次起新进程都会重新打印一次,所以都做了处理。这两个补丁对 **iOS 和 macOS 客户机都生效**(两者共用同一套 XNU 代码路径),而且跟宿主机是 Mac 还是 Linux 无关——如果你在真正的 Mac 上构建固件,同样值得带上这两个改动。 + +1. **`ACMTRM: waitForSEPEndpoint: timed out waiting for AppleSEPManager`**(每隔约 5 秒刷一次,无限重复)。`AppleCredentialManager` 根据设备树认为这台设备"应该有 SEP",于是永久重试。试过 `trm_enabled=0` 这个 boot-arg,也试过把设备树属性 `sepfw-load-at-boot` 设成 0,**都没能止住**——真正有效的做法是在 `dt_fixup.py` 里把 `sep` 这个设备树节点整个删掉: + ```python + d['arm-io'].remove_child('sep') + ``` + 这样对应的驱动从一开始就找不到 SEP 节点可探测,也就不会去重试了。(`run.sh` 里的 `BOOT_ARGS` 仍然保留了 `trm_enabled=0 hidrm_enabled=0`,虽然单独用没能解决问题,但留着无害,算是双重保险。) + +2. **`shared_region: %p [%d(%s)] check_np(...) vm_shared_region_start_address() returned 0x1`**——这条是**无条件打印**的(翻遍能想到的 boot-arg 和 sysctl 都没能关掉它),只要有进程调用 `check_np()` 系统调用(也就是几乎每次起新进程)就会打印一次。`patch_bootkc.py`(新增脚本,已经接入 `get_files.sh` 的 `main()`,在下载完 `bootkc`之后自动执行)会原地把这条 `printf` 格式字符串的**第一个字节改成 `\0`**,把它截断成空字符串。这只是改了 1 个字节的数据,**没有改动任何一条指令**:printf 系函数在格式字符串里没有 `%` 占位符时根本不会去读可变参数列表,所以这个改动除了"不再打印这行日志"之外不可能产生任何其他副作用。已经在 iOS(`iPhone17,3`)和 macOS(`Mac16,10`)两份 `bootkc` 上分别验证过——这段字符串在两边都**只出现一次、字节完全相同**。 + +这两个补丁都是 `get_files.sh` 自动执行的一部分,不需要手动操作,也不用担心重新拉取固件之后忘记补。 + +## 已验证的主机配置 + +主 README 里的兼容性表格说的是**客户机**(跑在 VM 里的 iOS/macOS 设备型号)。上面这套 Linux 宿主机方案是在下面这台机器上验证通过的: + +| 项目 | 配置 | +|---|---| +| CPU | AMD Ryzen 9 9950X(16核 / 32线程) | +| 主板 | ASUS ROG Crosshair X870E Hero | +| 显卡 | NVIDIA GeForce RTX 2080 Ti | +| 内存 | 64 GB | +| 架构 | x86_64 | +| 操作系统 | Ubuntu 24.04 LTS,内核 7.0.0-30-generic | + +这些配置**都不是硬性要求**——整套流程主要是单线程 CPU 工作加一个软件模拟的 qemu 虚拟机,性能远低于此的 Linux 机器大概率也能跑起来。列在这里只是作为"已验证可用"的参考配置。 diff --git a/README.md b/README.md index 5b00a9e..9442ceb 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ Run iOS/ macOS in Qemu. Supports emulating iPhone 17, 16, 15, 14, 13, and 12 (A19-A14) and M5-M1 Macs (tested with Macbook Air and Mac Mini). You can debug the kernel, edit the root filesystem, and run a root shell + custom programs. +> **Running the firmware-prep step on Linux instead of a Mac?** See +> [LINUX.md](LINUX.md) ([中文](LINUX.zh-CN.md)) for the extra dependencies and +> patches needed — everything below still assumes macOS. + Features: - Runs a lightweight debuggable iOS/ macOS (Darwin) system with custom filesystem. - Boots you directly into a root shell in just a few seconds. diff --git a/dmgutil.sh b/dmgutil.sh new file mode 100755 index 0000000..623efec --- /dev/null +++ b/dmgutil.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# Shared helpers for mounting/ unmounting firmware/ramdisk.dmg (an Apple APFS +# container image), used by get_files.sh and fix_perms.sh. +# +# On macOS this just wraps hdiutil. On Linux there's no hdiutil, so we use the +# linux-apfs-rw kernel module instead: https://github.com/linux-apfs/linux-apfs-rw +# +# git clone https://github.com/linux-apfs/linux-apfs-rw.git +# cd linux-apfs-rw && make +# sudo modprobe libcrc32c +# sudo insmod apfs.ko +# +# This module's write support is experimental, which is fine for our purposes +# (we're building a disposable VM disk image, not a system you rely on). + +# Mounts $1 (ramdisk.dmg) read-write at mountpoint $2. +# $3 selects whether on-disk ownership is honored: +# on - present real on-disk uid/gid (used by fix_perms.sh to inspect/ fix them) +# off - present the invoking user as the owner of everything, so unprivileged +# mv/mkdir/cp can freely restructure the filesystem (used by get_files.sh) +dmg_attach() { + local dmg="$1" mountpoint="$2" owners="$3" + + if [[ "$(uname)" == "Darwin" ]]; then + hdiutil attach -owners "${owners}" -mountpoint "${mountpoint}" "${dmg}" + return $? + fi + + if ! grep -q '^apfs ' /proc/modules; then + echo "error: the 'apfs' kernel module isn't loaded." 1>&2 + echo " get it from https://github.com/linux-apfs/linux-apfs-rw" 1>&2 + echo " build with 'make', then 'sudo modprobe libcrc32c && sudo insmod apfs.ko'" 1>&2 + return 1 + fi + + local opts="loop,readwrite" + [[ "${owners}" == "off" ]] && opts+=",uid=$(id -u),gid=$(id -g)" + sudo mount -t apfs -o "${opts}" "${dmg}" "${mountpoint}" +} + +dmg_detach() { + local mountpoint="$1" + + if [[ "$(uname)" == "Darwin" ]]; then + hdiutil detach "${mountpoint}" + return $? + fi + + sudo umount "${mountpoint}" +} + +# Recursively copies the *contents* of directory $1 into (possibly existing) +# directory $2, merging with anything already there (macOS's ditto does this; +# cp -a needs a trailing "/." on the source to get the same behavior). +copy_tree() { + local src="$1" dst="$2" + + if [[ "$(uname)" == "Darwin" ]]; then + ditto "${src}" "${dst}" + return $? + fi + + mkdir -p "${dst}" + # --remove-destination: the Linux apfs driver's experimental write support + # doesn't implement O_TRUNC (opening an existing file for overwrite), so + # we have to unlink-then-create instead of truncate-in-place. + cp -a --remove-destination "${src}/." "${dst}/" +} diff --git a/dt_fixup.py b/dt_fixup.py index 4487099..2e74872 100755 --- a/dt_fixup.py +++ b/dt_fixup.py @@ -198,6 +198,12 @@ def fixup(d, nvram_file): d['arm-io'].remove_child('dockchannel-uart') + # We don't emulate a SEP. Removing the node entirely (same trick as the + # 'sgx' removal in fixup_sptm) stops AppleCredentialManager/ ACMTRM from + # ever discovering a SEP nub to probe, instead of just marking it + # unavailable via a property (which didn't stop the retry spam). + d['arm-io'].remove_child('sep') + # disable RTC timeout in IOKitInitializeTime # IOKitInitializeTime waits for the IORTC resource which never appears since # we don't load a driver for it. AppleARMPE::start checks for a "no-rtc" key @@ -215,6 +221,13 @@ def fixup(d, nvram_file): # This fixes panic(cpu 0 caller 0xfffffff008b8e7b8): "AMFI: No PMGR?\n" @ConfigurationSettings.cpp:388 d['defaults'].props['vmm-present'] = "u32:1" + # We don't emulate a SEP, so tell the kernel not to expect one. Without + # this, AppleCredentialManager/ ACMTRM believe SEP is present (per the + # stock sepfw-load-at-boot=1) and spam + # "ACMTRM: waitForSEPEndpoint: timed out waiting for AppleSEPManager" + # to the console forever. + d['chosen'].props['sepfw-load-at-boot'] = "u32:0" + amcc=d['chosen']['lock-regs']['amcc'] amcc.props['aperture-count'] = "u32:1" amcc.props['aperture-size'] = "u32:0x4000" diff --git a/fix_perms.sh b/fix_perms.sh index d9a8294..646a37b 100755 --- a/fix_perms.sh +++ b/fix_perms.sh @@ -1,6 +1,9 @@ #!/bin/bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/dmgutil.sh" + fixup_perms() { local ramdisk="${1}" livemount="$(mktemp -d)" @@ -10,25 +13,30 @@ fixup_perms() { exit 1 fi - if ! hdiutil attach -owners on -mountpoint "${livemount}" "${ramdisk}"; then + if ! dmg_attach "${ramdisk}" "${livemount}" on; then echo "mount failed" rmdir "${livemount}" exit 1 fi echo "mounted ${ramdisk} on ${livemount}" - trap 'hdiutil detach ${livemount}; rmdir ${livemount}' EXIT + trap 'dmg_detach "${livemount}"; rmdir "${livemount}"' EXIT + + # root:wheel on macOS and root:root (0:0) on Linux are the same uid/gid + # pair (0/0); XNU only cares about the numeric ids. + local owner_group="root:wheel" + [[ "$(uname)" != "Darwin" ]] && owner_group="0:0" - echo "This will run: sudo chown -R root:wheel ${livemount}/bin ${livemount}/System ${livemount}/libexec" + echo "This will run: sudo chown -R ${owner_group} ${livemount}/bin ${livemount}/System ${livemount}/libexec" read -r -p "Are you sure? (y/n) " response echo "${response}" case "${response}" in [Yy]) - sudo chown -R root:wheel "${livemount}/bin" "${livemount}/System" + sudo chown -R "${owner_group}" "${livemount}/bin" "${livemount}/System" if [[ -d "${livemount}/libexec" ]]; then - sudo chown -R root:wheel "${livemount}/libexec" + sudo chown -R "${owner_group}" "${livemount}/libexec" fi echo "done!" ;; @@ -44,11 +52,6 @@ main() { exit 1 fi - if [[ "$(uname)" != "Darwin" ]]; then - echo "you need to run this on a Mac" - exit 1 - fi - fixup_perms "${1}" } diff --git a/get_files.sh b/get_files.sh index 52a59f2..7b1e85d 100755 --- a/get_files.sh +++ b/get_files.sh @@ -1,6 +1,9 @@ #!/bin/bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/dmgutil.sh" + # Default device: iPhone 16 on iOS 27.0 beta 7 (latest at time of writing) # I picked iPhone 16 as the default instead of iPhone 17, as it doesn't have MTE and therefore runs faster. # (note that iPhone 16's device name is confusingly "iPhone17,3") @@ -14,6 +17,7 @@ IOS_SYSROOT_TARFILE="sysroot.tar.gz" IOS_SYSROOT="sysroot" ADT_FIXUP="./dt_fixup.py" +BOOTKC_FIXUP="./patch_bootkc.py" NVRAM_BIN="nvram.bin" BUILD_TC="./build_tc.py" @@ -43,6 +47,12 @@ ensure_installed() { if [[ ! -x $(command -v "wget") ]]; then die "missing wget command (brew install wget)" fi + + if [[ "$(uname)" != "Darwin" ]]; then + if [[ ! -x $(command -v "ldid") ]]; then + die "missing ldid command (used in place of codesign on Linux; see https://github.com/ProcursusTeam/ldid)" + fi + fi } setup_dirs() { @@ -112,6 +122,18 @@ patch_dtree() { mv "${dtree}_patch" "${dtree}" } +patch_bootkc() { + local bootkc + bootkc="${FW_DIR}/bootkc" + + if [[ ! -f "${bootkc}" ]]; then + die "No bootkc (${bootkc})" + fi + + "${BOOTKC_FIXUP}" "${bootkc}" "${bootkc}_patch" + mv "${bootkc}_patch" "${bootkc}" +} + get_firmware() { get_file "kernelcache.release.${KERNEL_EXT}" "bootkc" @@ -164,11 +186,6 @@ patch_ramdisk() { local ramdisk ramdisk="${FW_DIR}/ramdisk.dmg" - if [[ "$(uname)" != "Darwin" ]]; then - echo "This isn't a Mac, so we can't patch the ramdisk- stopping here" - exit 0 - fi - echo "Patching ${ramdisk}" livemount="$(mktemp -d)" @@ -177,21 +194,32 @@ patch_ramdisk() { die "something's wrong with the livemount, stopping here" fi - # mount with -owners off to perform complicated FS ops without root, later - # we can chown everything to root. - if ! hdiutil attach -owners off -mountpoint "${livemount}" "${ramdisk}"; then + # mount with owners "off" to perform complicated FS ops without root, + # later we can chown everything to root (see fix_perms.sh). + if ! dmg_attach "${ramdisk}" "${livemount}" off; then rmdir "${livemount}" die "mount failed" fi echo "mounted ${ramdisk} on ${livemount}" - trap 'hdiutil detach ${livemount}; rmdir ${livemount}' EXIT + trap 'dmg_detach "${livemount}"; rmdir "${livemount}"' EXIT if [[ -d "${livemount}/System/Library/LaunchDaemons.old" ]]; then echo "already patched" return fi + local sign_cmd hash_cmd + if [[ "$(uname)" == "Darwin" ]]; then + sign_cmd=(codesign -s -) + hash_cmd=(codesign -d -vvv) + else + # ldid is a Linux-friendly stand-in for ad-hoc codesign; its -S/-h + # output is compatible with the codesign parsing below. + sign_cmd=(ldid -Cadhoc -S) + hash_cmd=(ldid -h) + fi + mv "${livemount}/System/Library/LaunchDaemons" "${livemount}/System/Library/LaunchDaemons.old" mkdir "${livemount}/System/Library/LaunchDaemons" @@ -205,10 +233,10 @@ patch_ramdisk() { exit 1 fi - ditto "${IOS_SYSROOT}/bin" "${livemount}/bin" - ditto "${IOS_SYSROOT}/libexec" "${livemount}/libexec" + copy_tree "${IOS_SYSROOT}/bin" "${livemount}/bin" + copy_tree "${IOS_SYSROOT}/libexec" "${livemount}/libexec" echo "signing binaries..." - find "${livemount}/bin" -type f -exec codesign -s - {} \; + find "${livemount}/bin" -type f -exec "${sign_cmd[@]}" {} \; ;; 'macosx') cp "${MAC_PLIST}" "${livemount}/System/Library/LaunchDaemons" @@ -219,7 +247,7 @@ patch_ramdisk() { esac echo "building trustcache..." - find "${livemount}" -type f -exec codesign -d -vvv {} \; 2>&1 | grep -i cdhash= | cut -d= -f2- > "${FW_DIR}/all_hashes" + find "${livemount}" -type f -exec "${hash_cmd[@]}" {} \; 2>&1 | grep -i cdhash= | cut -d= -f2- > "${FW_DIR}/all_hashes" "${BUILD_TC}" "${FW_DIR}/all_hashes" "${FW_DIR}/ramdisk.tc" } @@ -230,6 +258,7 @@ main() { get_firmware get_ramdisk patch_dtree + patch_bootkc patch_ramdisk echo "done!" } diff --git a/patch_bootkc.py b/patch_bootkc.py new file mode 100755 index 0000000..a8b8493 --- /dev/null +++ b/patch_bootkc.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +Silences known-unconditional debug kprintf()s in a release bootkc that spam +the console on every process launch / every few seconds, without affecting +kernel behavior. This VM never has a real SEP or a real dyld shared cache, so +these particular log lines are always benign noise here. + +Technique: each target is a printf-style format string embedded in the +kernelcache's string table. We truncate it in place by overwriting its first +byte with a NUL terminator, turning "shared_region: ... returned 0x%x\n" into +"" (an empty, no-argument format string). This changes zero instructions and +zero code paths - printf-family functions never touch the vararg list when +the format has no '%' directives - so it can't affect program correctness in +any way. If Apple changes the wording (or drops the message) in a future +build, the string just won't be found and we skip it with a warning rather +than failing the build. +""" +import argparse + +# Each entry is matched literally and, if present exactly once, truncated. +NOISY_STRINGS = [ + # AppleCredentialManager/ ACMTRM has its own retry-loop log line + # ("waitForSEPEndpoint: timed out...") that's silenced by removing the + # 'sep' device tree node instead (see dt_fixup.py) - that stops the + # subsystem from ever probing SEP in the first place, which is cleaner + # than patching this string. This one, on the other hand, has no such + # device-tree lever: it's printed unconditionally on every check_np() + # syscall (i.e. on every single process launch), regardless of any + # boot-arg or sysctl, because there's no real dyld shared region in this + # ramdisk environment. + b"shared_region: %p [%d(%s)] check_np(0x%llx) vm_shared_region_start_address() returned 0x%x\n", +] + +def patch(data: bytes) -> bytes: + data = bytearray(data) + for needle in NOISY_STRINGS: + count = data.count(needle) + if count == 0: + print(f"warning: pattern not found, skipping: {needle!r}") + continue + if count > 1: + print(f"warning: pattern found {count} times (expected 1), skipping to be safe: {needle!r}") + continue + offset = data.find(needle) + data[offset] = 0 + print(f"patched 1 string at offset {hex(offset)}: {needle!r}") + return bytes(data) + +def main(): + p = argparse.ArgumentParser(prog='patch_bootkc') + p.add_argument('bootkc', type=argparse.FileType('rb', 0)) + p.add_argument('out', type=argparse.FileType('wb', 0)) + args = p.parse_args() + + data = args.bootkc.read() + patched = patch(data) + assert len(patched) == len(data), "patch must not change file size" + args.out.write(patched) + +if __name__ == "__main__": + main() diff --git a/run.sh b/run.sh index f54d08c..655df29 100755 --- a/run.sh +++ b/run.sh @@ -3,7 +3,7 @@ set -euo pipefail FIRMWARE_DIR="firmware" QEMU="qemu-sptm/build/qemu-system-aarch64" -BOOT_ARGS="rd=md0 serial=3 -v -noprogress wdt=-1 wlan-olyhal-abort" +BOOT_ARGS="rd=md0 serial=3 -v -noprogress wdt=-1 wlan-olyhal-abort trm_enabled=0 hidrm_enabled=0" fix_tty() { stty sane