Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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/
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
126 changes: 126 additions & 0 deletions LINUX.md
Original file line number Diff line number Diff line change
@@ -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.
69 changes: 69 additions & 0 deletions LINUX.zh-CN.md
Original file line number Diff line number Diff line change
@@ -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 机器大概率也能跑起来。列在这里只是作为"已验证可用"的参考配置。
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
68 changes: 68 additions & 0 deletions dmgutil.sh
Original file line number Diff line number Diff line change
@@ -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}/"
}
Loading