Integrate iPadOS 27 startup fixes and runtime input preparation

This commit is contained in:
bluseliu50 committed 2026-11-02 02:35:30 +08:00
1 parent a4419c8af7
commit 474cfeeb4e
24 files changed
+1079 -109

No files matched your search

+7
View File
@@ -103,6 +103,13 @@ An `--owner-trust-cache` bundle is copied without the `[payload] trust_cache_sha
## Data ramdisk
When the selected kernel configuration enables `CONFIG_XNU_OWNER_DATA_MOUNT`,
the payload requires `--data-ramdisk FILE` or `[build] data_ramdisk` in the local
build configuration. Without a generated `.config`, this check reads the
selected device defconfig. A persistent overlay still needs its original seed
to publish the Data volume; changing the seed is not a way to resume old Data.
Missing seeds stop packaging before the payload is written.
A Data seed can be delivered in two forms:
| Form | ADT placeholder | Behavior |
+16
View File
@@ -28,6 +28,22 @@ Status: joins WPA2-Personal, gets DHCP, Safari loads pages. Throughput is low.
| `sheng-wifi=5` | Start Wi-Fi. Without it the module never runs. |
| `sheng-wifi-aj=0` | No join at boot; Settings joins. |
### iPadOS 27 service startup
The sheng board DTS exports WCN7850 as `/arm-io/wlan` using
`unixnu,adt-name = "wlan"`. The stock 24A446 `com.apple.wifid` job checks
`_LimitLoadToDeviceTree` before launchd loads it. Without a matching node,
the kernel's `join.txt` path can still connect and exchange packets while
Settings shows an empty Wi-Fi page and cannot enable the switch.
`/chosen/mac-address-wifi0` and the IOService network objects do not satisfy
this device-tree condition.
The 27 payload defaults include `sheng-wifi=5`. After changing the DTS,
rebuild the payload and boot image, then inspect the ADT inside the image.
Host checks confirm the node satisfies the startup condition; starting
wifid, enabling the Settings switch and scanning still require a device
test. Remove `sheng-autoreset` from a manual test image's boot arguments.
### Facts
- Each of the six PMU rails needs RPMh mode, voltage and enable requests. Without the mode request two rails stay off and the link never trains.
+38
View File
@@ -19,12 +19,44 @@ matching code signature before reusing a map.
comparison. It transfers unambiguous names from unique code matches and resolves
those names in the actual native kernel. The map records the reference hash.
The sheng iPadOS 27 build runs `build-fileset-release-reference.py --tree . --lto`
before prelinking. Keep LTO enabled for this comparison: the non-LTO reference
loses matches needed by original kexts, including corecrypto's startup call to
`kprintf`. With `--allow-missing`, such a call reaches `unixnu_fileset_unmapped`
at runtime even though packaging succeeds.
The reference builder records its mode, source, SDK, archive, compiler and
version inputs and verifies the output digest before reusing it. Unrecorded
references are rebuilt. Different inputs use separate build directories;
LTO export inspection reads the generated LTO object and non-LTO file list.
After a reference change, regenerate the map, kernel, payload and boot image,
and inspect the unresolved-target report before hardware testing.
The 24A446 fixture preserves 23 import identities recovered from the retained
RELEASE-LTO map and cross-checked against its boot image, plus the observed
corecrypto `kprintf` import. Their original function digests and recovery
provenance are recorded; the current native targets are resolved by symbol.
The fixture also identifies 13 previously missing vnode operation descriptors by
`vdesc_name`, four metaclass pointer slots by their chained targets, and the
`gIODTPHandleKey` and `audio_active` globals by their source consumers. Unbound
vnode descriptors can make APFS registration read a zone field as an operation
index in `vfs_fsadd`; they must resolve to the native descriptors initialized by
VFS, not preserve old collection data addresses.
The emitter places native segments within the collection's code, constant and
data ranges, applies native split-segment records, rebuilds chained fixups and
function starts, and recomputes corecrypto's code HMAC. The collection root uses
the native link address. The ordinary prelink `--slide` value does not apply to
filesets. The native input executable is unchanged.
Native adapters to retained kext functions declare fixed targets as `N_ABS`
`<stub>.original` symbols and use `BTI c; B` stubs. These branches lack linker
split-segment records, so the emitter recomputes them after placement and checks
every destination against retained executable ranges. The report records each
binding in `native_original_branches`. Builders must not pre-subtract a cached
native slide. Absolute data pointers in GOT entries keep their fixed values.
Missing mappings stop emission by default. `--allow-missing` produces an
experimental image and the JSON report lists every unresolved target by kext.
An image that passes structural checks still requires complete mappings and
@@ -43,3 +75,9 @@ The catalog records implementation scope, hardware and OS requirements, board de
```
[Component catalog](/reference/components)
The 24A446 packaging profile additionally checks a regression policy against the
original (unslid) imported targets. New unresolved data or stubbed calls fail
before replacing the last kernel candidate. This caught two lost order-based
matches during a full rebuild: `IOCircularDataQueueCopyLatest` and `get_aiotask`;
the reviewed fixture now identifies both by original digest and native symbol.
+40
View File
@@ -124,3 +124,43 @@ Booting the artifacts is a separate step with its own record.
4. Record the run in the `packaging` area of `porting.toml`.
[Build and boot](/building), [Device repository](/porting/device-repository)
## Runtime inputs for sheng iPadOS 27
The payload's `runtime` table enables preparation before kernel compilation.
Apple static/System/App trust modules come from the selected IPSW unchanged. Turnip,
AdrenoMetal and the board audio HAL are built from source; their CDHashes are
appended as a separate owner trust module. Wallpaper overlays come from the
read-only OS image. The official Xiaomi fastboot archive still supplies the
firmware inputs through the board preflight scripts.
An existing installation must reuse its original `data.dbr` with
`--data-ramdisk FILE`. GPU and audio updates travel in the addon and do not edit
that seed. The runtime step checks its APFS volume UUID against the generated
kernel configuration and records the input SHA-256 values in the package manifest.
A payload-only build refuses a different Data UUID and asks for a kernel rebuild.
For a **first installation**, `scripts/package.sh --os 27 --init-data` creates an
APFS seed from the IPSW's initial `private/var`, uses the board's `data_gib` and
Setup preferences, and binds the new UUID into the kernel configuration. This
writes host build files only; it does not erase or flash a device. Keep the seed
at `out/sheng/ipados-27.0.1-24A446/sheng_ipados27/runtime/data.dbr` for every later
build of that installation. Its content identity, not just its UUID, identifies
the persistent Data store. Regenerating it cannot preserve an older installation.
The package workflow restores this seed from an installation cache, or accepts
an original seed URL with a required SHA-256. A missing cache stops the build
unless `init_data` is explicitly selected. The workflow uploads a separate seed
backup with one-day retention. Download and retain it: Actions caches are not a
permanent backup. The current device's seed is not supplied by either firmware
archive. Automatic runtime generation currently targets the iPadOS 27 profile.
`--gpu-addon FILE` and its matching `--owner-trust-cache FILE` remain available
for reproducing an existing image. Omit both to compile the addon and generate
its trust bundle. These generated binaries and installation seeds are not Git
inputs.
For the 24A446 fileset, `regression_policy` bounds the unresolved references by
original kernel address and importing component. New gaps fail packaging even
with `--allow-missing`; this check does not qualify the remaining experimental
paths or replace a device boot test.
+5
View File
@@ -41,6 +41,11 @@ initrd = "Payloads/scratch-data.img"
args = "-v serial=3"
```
所选内核配置启用了 `CONFIG_XNU_OWNER_DATA_MOUNT` 时,必须通过
`--data-ramdisk FILE` 或本地构建配置的 `[build] data_ramdisk` 提供种子。
没有生成 `.config` 时检查所选设备 defconfig。持久化覆盖层也需要原始种子来
发布 Data 卷;更换种子不能恢复旧 Data。缺少种子会在写负载前报错。
提供 Data 种子时存在 `initrd`。`args` 是 `[payload] kernel_args` 和 `--kernel-arg` 合并后的命令行。
## ADT sidecar
+14
View File
@@ -26,6 +26,20 @@ Ath12kWifi 在自己的兼容层(`shim/`)上编译 Linux 的 ath12k、MHI
| `sheng-wifi=5` | 启动 Wi-Fi。没有它模块不会运行。 |
| `sheng-wifi-aj=0` | 启动时不加入网络,由设置加入。 |
### iPadOS 27 服务启动
sheng 的板级 DTS 通过 `unixnu,adt-name = "wlan"` 将 WCN7850 导出为
`/arm-io/wlan`。24A446 原生 `com.apple.wifid` 任务带有
`_LimitLoadToDeviceTree` 条件,launchd 加载前会检查。缺少匹配节点时,
内核的 `join.txt` 路径仍能连接并收发数据,但设置中的 Wi-Fi 页面为空,
开关无法打开。`/chosen/mac-address-wifi0` 和 IOService 网络对象不能满足
这个设备树条件。
27 的默认 payload 参数包含 `sheng-wifi=5`。修改 DTS 后需要重新生成
payload 和 boot 镜像,并检查镜像内的 ADT。主机检查确认节点满足启动条件;
wifid 启动、设置开关和扫描功能仍需上机验证。手动测试镜像的启动参数应移除
`sheng-autoreset`。
### 事实
- 六条 PMU 电源轨各需要 RPMh 的模式、电压和使能请求。缺少模式请求时有两条保持关闭,链路无法训练。
+32
View File
@@ -5,6 +5,27 @@
`--fileset-reference FILE` 可以提供带符号的对照内核。唯一的代码匹配可提供符号名,
最终地址仍从实际编译的原生内核获取。映射记录中会保存对照文件的哈希。
sheng 的 iPadOS 27 构建在预链接前运行
`build-fileset-release-reference.py --tree . --lto`。该对照内核需要启用
LTO;非 LTO 对照会丢失原生 kext 所需的匹配,包括 corecrypto 启动时对
`kprintf` 的调用。启用 `--allow-missing` 时,打包可以成功,但该调用会在
运行时进入 `unixnu_fileset_unmapped` 并触发 panic。
对照内核构建脚本记录模式、源码、SDK、归档库、编译器和版本输入,复用前还
会核对输出哈希。没有记录的旧产物会重建;输入改变时使用独立构建目录。
LTO 模式从生成的 LTO 对象和非 LTO 文件列表检查导出符号。对照内核改变后,
需要重新生成映射、内核、payload 和 boot 镜像,并在上机前检查未解析目标报告。
24A446 的符号记录保留了从旧 RELEASE-LTO 映射恢复、并与对应启动镜像交叉
核对的 23 个导入函数身份,以及此次定位的 corecrypto `kprintf` 导入。
记录包含原始函数字节哈希和恢复依据;当前原生内核的目标地址仍按符号重新解析。
固定记录还按 `vdesc_name` 识别 13 个此前缺失的 vnode 操作描述符,按链式
指针目标识别 4 个 metaclass 指针槽,并通过源码调用方识别 `gIODTPHandleKey`
和 `audio_active`。APFS 注册时会读取这些描述符的操作下标;保留旧地址可能
把 zone 数据当成下标,导致 `vfs_fsadd` 越界。必须绑定到由新内核 VFS 初始化
的描述符。
对于 arm64e fileset 内核集合,`emit_prelink.py` 会选择 fileset 路径并生成
函数和数据映射。`--fileset-map FILE` 可以指定已有映射,输入哈希和匹配器版本
必须一致。输出保留所选根组件及其全部依赖。
@@ -14,6 +35,12 @@
HMAC。集合根头使用原生内核的链接地址,普通预链接的 `--slide` 不用于 fileset。
原生内核输入文件保持不变。
原生适配器通过 `BTI c; B` 跳板调用保留的 kext 函数,固定目标由 `N_ABS`
符号 `<stub>.original` 声明。这类跳转没有链接器分段重定位记录,因此输出工具
在布局完成后重新计算跳转,并核验目标位于保留组件的可执行范围内。报告中的
`native_original_branches` 记录每条绑定。构建脚本不得预先减去缓存的原生内核
位移;GOT 中的绝对数据指针保持固定值。
默认遇到缺失映射就停止。`--allow-missing` 可以生成实验映像,JSON 报告按
kext 列出所有未解析目标。结构验证通过后,仍需要补全映射并进行硬件验证,
才能确认启动兼容性。
@@ -39,3 +66,8 @@ XnuPrelink 将构建的 XNU 内核绑定到所选原始 kext,并写出预链
```
[组件目录](/zh/reference/components)
24A446 打包配置还会按原始、未滑动的导入地址检查回归清单。新增未解析数据或
stub 调用会在替换上次内核产物前报错。完整重编曾丢失两个依赖顺序的匹配:
`IOCircularDataQueueCopyLatest` 和 `get_aiotask`;现在已根据源码和反汇编,
用原函数摘要及 native 符号固定其身份。
+29
View File
@@ -106,3 +106,32 @@ remove = [ ... ]
4. 把运行记录到 `porting.toml` 的 `packaging` 区域。
[构建与启动](/zh/building)、[设备仓库](/zh/porting/device-repository)
## sheng iPadOS 27 运行输入
`payload.runtime` 在编译内核前准备运行输入。Apple 静态、System 和 App 信任模块从所选
IPSW 原样提取;Turnip、AdrenoMetal 和设备音频 HAL 从源码编译,其 CDHash 写入
单独的 owner 信任模块。壁纸覆盖文件从只读 OS 镜像生成。小米官方 fastboot 固件
继续由设备 preflight 脚本下载、校验和提取。
保留现有安装的数据时,必须通过 `--data-ramdisk FILE` 复用原来的 `data.dbr`。
GPU 和音频更新放在 addon 中,不修改 seed。构建会检查 APFS 卷 UUID,将它绑定到
本次生成的内核配置,并把输入 SHA-256 写入打包清单。只打包 payload 时,若 UUID
与内核配置不同则报错,要求重新编译内核。
**首次安装**可用 `scripts/package.sh --os 27 --init-data`,从 IPSW 的初始
`private/var` 生成 APFS seed,采用设备配置的 `data_gib` 和 Setup 偏好,并把新
UUID 写入内核配置。该命令只写本机构建文件,不擦除或刷写设备。此后应一直保留
`out/sheng/ipados-27.0.1-24A446/sheng_ipados27/runtime/data.dbr`。
持久化 Data 绑定的是 seed 的内容身份,仅保留 UUID 或从同一 IPSW 重做都不能替代原文件。
Actions 会恢复安装对应的 seed 缓存,也支持原 seed 的下载 URL 加必填 SHA-256。
缓存丢失时默认停止,只有明确选择 `init_data` 才会新建。工作流另提供保留一天的
seed 备份下载;请自行长期保存,Actions 缓存不能作为永久备份。当前设备的原 seed
无法从官方固件包恢复。自动生成运行输入目前只适用于 iPadOS 27。
要复现已有镜像,可同时指定 `--gpu-addon FILE` 和配套的
`--owner-trust-cache FILE`;省略二者则从源码构建。生成的二进制和 seed 不提交 Git。
24A446 的 `regression_policy` 按原内核地址和导入组件限制未解析引用;即使启用
`--allow-missing`,新增缺口也会使打包失败。这不代表剩余实验路径已验证,仍需上机启动测试。
@@ -31,6 +31,7 @@
"kernel" = "build/xnu/ios-obj/DEVELOPMENT_ARM64_BCM2837/mach.development.bcm2837.unstripped.noctf"
"kernelcache_sha256" = "38e6d9fa5f789b8228f4617ef4c6d2a3fd8ab564abdc9b62920f1db53fc1780a"
"slide" = "-0x1000000"
regression_policy = "scripts/policies/ipados-27.0.1-24A446/prelink-regression.json"
# The AMFI wrapper that calls _initializeAppleMobileFileIntegrity and returns 0; the kernel patch calls it early.
"early_security_init" = "com.apple.driver.AppleMobileFileIntegrity=0xfffffe00092551ac"
# Bring-up: match against the symbol-bearing RELEASE build of the same XNU source
+10 -2
View File
@@ -5,6 +5,7 @@ and the launchd probe daemon with its job plist (NativeAdrenoProbeDaemon.cpp).
Output (ignored, never committed): build/adreno-metal-plugin/NativeAdrenoMetal.bundle with a 4 KiB-page ad-hoc signature, and
trust.json with its CDHash. The bundle is staged into the Data seed by add-gpu-plugin-data.py and trusted through an extra
owner trust module (make-owner-trust-bundle.py); neither changes the original trust modules, AMFI or any Apple signature."""
import argparse
import json
import plistlib
import re
@@ -15,7 +16,11 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
# --target ios27: the arm64e build for iPadOS 27 (24A446), whose render server is an arm64e process.
IOS27 = sys.argv[1:] == ['--target', 'ios27']
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--target', choices=('ios', 'ios27'), default='ios')
parser.add_argument('--no-replay', action='store_true', help='omit the optional host simulation capture used by diagnostic probes')
args = parser.parse_args()
IOS27 = args.target == 'ios27'
OUT = ROOT / ('build/adreno-metal-plugin27' if IOS27 else 'build/adreno-metal-plugin')
bundle = OUT / 'NativeAdrenoMetal.bundle'
if OUT.exists():
@@ -30,7 +35,10 @@ if not TURNIP.is_file():
raise SystemExit('Build Turnip first: python3 scripts/build-turnip-darwin.py --target ' + ('ios27' if IOS27 else 'ios'))
subprocess.run(['python3', ROOT / 'scripts/gen-turnip-smoke-shaders.py'], check=True)
# Turnip's first submission, recorded on the host, for the plugin's prefix replay (build/turnip-smoke/TurnipReplay.h).
subprocess.run(['python3', ROOT / 'scripts/gen-turnip-replay.py'], check=True)
if args.no_replay:
(ROOT / 'build/turnip-smoke/TurnipReplay.h').unlink(missing_ok=True)
else:
subprocess.run(['python3', ROOT / 'scripts/gen-turnip-replay.py'], check=True)
BRIDGE_TARGET = 'ios27' if IOS27 else 'ios'
subprocess.run(['python3', ROOT / 'scripts/build-adreno-metal-bridge.py', '--target', BRIDGE_TARGET], check=True)
BRIDGE = ROOT / 'build/adreno-metal-bridge' / BRIDGE_TARGET / 'libadreno_metal_bridge.a'
+3 -5
View File
@@ -92,11 +92,9 @@ sdk = run(['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'], text=True).strip()
binding_objects = [OUT / 'original-bindings.o']
if FILESET:
# As for the Legacy display (build-gop-legacy-display-27.py): original functions are reached through owned stubs in kernel text
# (signed and moved like native code) whose branch lands on the original after the collection slide, less the measured move of the
# native kernel (native-slide); the metaclass is data, reached through the GOT at its plain address.
# (signed and moved like native code). XnuPrelink retargets their branches after placing native text.
# The metaclass is data, reached through the GOT at its fixed collection address.
slide = int(plan['collection_slide'], 16)
native_slide_file = ROOT / 'build/native-legacy-display/native-slide'
native_slide = int(native_slide_file.read_text(), 16) if native_slide_file.exists() else 0
text, addresses = '.text\n.p2align 2\n', ''
for n, a in sorted(bindings.items()):
if n not in undefined:
@@ -104,7 +102,7 @@ if FILESET:
if n.endswith('gMetaClassE'):
addresses += f'.globl {n}\n.set {n}, {a + slide:#x}\n'
else:
addresses += f'.globl {n}.original\n.set {n}.original, {(a + slide - native_slide) & ((1 << 64) - 1):#x}\n'
addresses += f'.globl {n}.original\n.set {n}.original, {a + slide:#x}\n'
text += f'.globl {n}\n{n}:\n bti c\n b {n}.original\n'
asm.write_text(text)
(OUT / 'original-addresses.S').write_text(addresses)
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""Build payload runtime inputs from a pinned IPSW and the selected sources.
Invoked by unixnu.runtime with a JSON request; no device ID or private binary is
stored in source control. Existing Data seeds are copied into no writable mount.
"""
import argparse
import contextlib
import json
import os
from pathlib import Path
import plistlib
import re
import struct
import subprocess
import sys
import tempfile
import zlib
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'scripts'))
from mesa_patch_identity import validate_recorded_patches
def run(*command, **kwargs):
subprocess.run([str(part) for part in command], check=True, **kwargs)
def script(name, *args, **kwargs):
run(sys.executable, ROOT / 'scripts' / name, *args, **kwargs)
@contextlib.contextmanager
def os_mount(image, work):
# A developer may already have this exact OS mounted for inspection. Reuse
# only a read-only attachment, and leave its lifetime with its owner.
info = plistlib.loads(subprocess.check_output(['hdiutil', 'info', '-plist']))
for entry in info.get('images', []):
if Path(entry.get('image-path', '')).resolve() != Path(image).resolve():
continue
if entry.get('writeable', True):
raise SystemExit('The IPSW OS image is already attached writable; require an unchanged read-only input')
mounts = [Path(item['mount-point']) for item in entry.get('system-entities', [])
if item.get('mount-point') and (Path(item['mount-point']) / 'sbin/launchd').is_file()]
if len(mounts) == 1:
yield mounts[0]
return
mount = work / 'os-mount'
mount.mkdir()
attached = plistlib.loads(subprocess.check_output(['hdiutil', 'attach', '-readonly', '-nobrowse',
'-mountpoint', str(mount), '-plist', str(image)]))
device = attached['system-entities'][0]['dev-entry']
try:
yield mount
finally:
run('hdiutil', 'detach', device)
def checkout(path, pin):
if not path.exists():
path.mkdir(parents=True)
run('git', 'init', '-q', path)
run('git', '-C', path, 'fetch', '--depth=1', pin.get('mirror', pin['repository']), pin['commit'])
run('git', '-C', path, 'checkout', '--detach', 'FETCH_HEAD')
return True
revision = subprocess.check_output(['git', '-C', str(path), 'rev-parse', 'HEAD'], text=True).strip()
if revision != pin['commit']:
raise SystemExit(f'{path}: source revision differs from its pin; preserve and inspect the checkout')
return False
def build_plugin(target):
pins = json.loads((ROOT / 'scripts/data/adreno-compiler-inputs.json').read_text())
mesa = ROOT / '.cache/mesa-work'
fresh = checkout(mesa, pins['mesa'])
if fresh:
for patch in pins['patches']:
run('git', '-C', mesa, 'apply', ROOT / patch)
validate_recorded_patches(ROOT, mesa, pins)
translator = json.loads((ROOT / 'scripts/data/gpu-metal-vulkan-inputs.json').read_text())['translator']
checkout(ROOT / '.cache/gpu-reference/metal2vulkan', translator)
venv = ROOT / '.cache/mesa-venv'
if not (venv / 'bin/python').is_file():
run(sys.executable, '-m', 'venv', venv)
run(venv / 'bin/python', '-m', 'pip', 'install', '-r', ROOT / 'scripts/requirements-adreno-compiler.txt')
script('build-turnip-darwin.py', '--target', target)
# Replay is a diagnostic capture from a host GPU simulation, not a runtime
# dependency. Do not require unrelated test fixtures in a release build.
script('build-adreno-metal-plugin.py', '--target', target, '--no-replay')
return ROOT / 'build' / ('adreno-metal-plugin27' if target == 'ios27' else 'adreno-metal-plugin')
def addon_hashes(path, work):
data = path.read_bytes()
if data[:8] != b'NGPUADD2' or len(data) < 24 or struct.unpack_from('<Q', data, 16)[0] != len(data):
raise SystemExit('invalid GPU addon')
at, executables = 24, []
for _ in range(struct.unpack_from('<I', data, 8)[0]):
length, mode, size, packed = struct.unpack_from('<IIQQ', data, at)
at += 24
name = data[at:at + length - 1].decode()
if not 1 < length <= 160 or name.startswith('/') or '..' in Path(name).parts:
raise SystemExit('invalid addon path')
at += (length + 7) & ~7
body = zlib.decompress(data[at:at + packed])
at += (packed + 7) & ~7
if len(body) != size:
raise SystemExit('invalid addon entry size')
output = work / name
output.parent.mkdir(parents=True, exist_ok=True)
output.write_bytes(body)
output.chmod(mode & 0o777)
if body[:4] in (b'\xcf\xfa\xed\xfe', b'\xca\xfe\xba\xbe'):
executables.append(output)
if at != len(data) or not executables:
raise SystemExit('invalid addon length or no executables')
hashes = []
for executable in executables:
run('codesign', '--verify', '--strict', executable)
detail = subprocess.run(['codesign', '--display', '--verbose=4', str(executable)],
check=True, capture_output=True, text=True).stderr
match = re.search(r'^CDHash=([0-9a-f]{40})$', detail, re.M)
if not match:
raise SystemExit('no CDHash for ' + str(executable))
hashes.append(match[1])
return sorted(set(hashes))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--request', type=Path, required=True)
request = json.loads(parser.parse_args().request.read_text())
out = Path(request['output'])
seed = Path(request['data_ramdisk'])
addon = Path(request['gpu_addon']) if request.get('gpu_addon') else None
trust = Path(request['trust_cache']) if request.get('trust_cache') else None
with tempfile.TemporaryDirectory(prefix='prepare-', dir=out) as td:
work = Path(td)
if not seed.is_file() or addon is None:
with os_mount(request['os_image'], work) as mount:
if not seed.is_file():
if not request['init_data']:
raise SystemExit('refuse to generate Data without --init-data')
script('make-data-seed.py', mount, seed, '--gib', str(request['data_gib']),
'--build', request['build'], *(['--skip-setup'] if request.get('skip_setup') else []))
if addon is None:
plugin = build_plugin(request['target'])
extra = []
for profile in request.get('wallpapers', []):
destination = work / profile
script('make-wallpaper-overlay.py', mount, destination, profile)
extra += ['--wallpaper-floating' if profile.endswith('-floating') else '--wallpaper', destination]
candidate = work / 'gpuaddon.bin'
script('make-gpu-addon.py', '--publish', '--no-daemon', *extra, candidate,
env=dict(os.environ, ADRENO_PLUGIN_DIR=str(plugin)))
if request.get('audio_tool'):
audio = work / 'audio'
run(sys.executable, Path(request['device']) / request['audio_tool'], audio, request['audio_target'])
combined = work / 'combined.bin'
script('addon-add-dir.py', candidate, combined, 'audio/ShengAudio.driver', audio / 'ShengAudio.driver')
candidate = combined
addon = out / 'gpuaddon.bin'
candidate.replace(addon)
if trust is None:
original = work / 'apple-trust.bundle'
script('bundle-ipsw-trust.py', original, *request['trust_modules'])
hashes = addon_hashes(addon, work / 'addon')
candidate = work / 'owner-trust.bundle'
script('trust-append-module.py', original, candidate, *hashes)
trust = out / 'owner-trust.bundle'
candidate.replace(trust)
(out / 'generated.json').write_text(json.dumps(dict(data_ramdisk=str(seed), gpu_addon=str(addon),
trust_cache=str(trust)), indent=2) + '\n')
if __name__ == '__main__':
main()
+167 -98
View File
@@ -10,101 +10,170 @@ import fnmatch
from pathlib import Path
import subprocess
repo = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--tree', required=True, type=Path, help='prepared XNU build tree of the OS profile')
parser.add_argument('--lto', action='store_true', help='enable the RELEASE default link-time optimization')
options = parser.parse_args()
mode = 'lto' if options.lto else 'snapshot'
root = options.tree.resolve()
output = root / 'build/xnu/fileset-reference'
final = output / 'mach.release.bcm2837.unstripped.noctf'
if final.is_file():
# An earlier run of this step left the comparison kernel in place.
print(f'reference present: {final}')
raise SystemExit(0)
output.mkdir(parents=True, exist_ok=True)
source = output / 'source'
if not source.exists():
shutil.copytree(root/'.cache/xnu-work', source, symlinks=True,
ignore=shutil.ignore_patterns('.git'))
# The shared tree currently has this observation hook at file scope. The
# reference build disables observation hooks; repair only its private copy.
sleh = source / 'osfmk/arm64/sleh.c'
text = sleh.read_text()
misplaced = '\n\tboot_progress_user_breakpoint(state, esr);\n#if __has_feature(ptrauth_calls)'
if misplaced in text:
sleh.write_text(text.replace(misplaced, '\n#if __has_feature(ptrauth_calls)', 1))
manifest = {str(p.relative_to(source)): hashlib.sha256(p.read_bytes()).hexdigest()
for p in source.rglob('*') if p.is_file()}
(output/'source-sha256.json').write_text(json.dumps(manifest,indent=2)+'\n')
sdk = root / '.cache/fakeroot'
archives = [root/'build/corecrypto-2023/libcorecrypto_kernel_noasm-namespaced.a']
forced = [root/f'build/{folder}/{name}' for folder,name in (
('libpthread-kernel','libpthread_kernel.a'),('armdt-platform','libArmDTPlatform.a'),('armcpu','libArmCpu.a'))]
assert all(p.is_file() for p in archives+forced)
env = dict(os.environ, KERNEL_SDKROOT=str(sdk),
XNU_AVAILABILITY_PL=str(sdk/'usr/local/libexec/availability.pl'))
flags = '-Wno-reserved-identifier -Wno-pre-c11-compat -Wno-missing-designated-field-initializers -Wno-suggest-override -Wno-unnecessary-virtual-specifier -Wno-suggest-destructor-override -Wno-ms-bitfield-padding -DXNU_NATIVE_PAN=1 -DXNU_SOURCE_CORECRYPTO14=1 -DXNU_BOOT_PROGRESS=0'
args = ['make','-C',str(source),'build','SDKROOT=iphoneos',
'ARCH_CONFIGS=ARM64','MACHINE_CONFIGS=BCM2837','KERNEL_CONFIGS=RELEASE',
'ARCH_STRING_FOR_CURRENT_MACHINE_CONFIG=arm64e','LARGE_MEMORY_DEFINE=-UARM_LARGE_MEMORY',
'ARM64_LINK_ADDR=0xfffffff007004000','ARM64_PLKSEG_ADDR=0xfffffff004004000',
f'DEPLOYMENT_TARGET_FLAGS=-miphoneos-version-min={os.environ.get("UNIXNU_IOS_VERSION", "26.0")} -DXNU_TARGET_OS_IOS',
f'DEPLOYMENT_LINKER_FLAGS=-Wl,-ios_version_min,{os.environ.get("UNIXNU_IOS_VERSION", "26.0")}',
f'RC_DARWIN_KERNEL_VERSION={os.environ.get("UNIXNU_XNU_DARWIN", "25.5.0")}',
f'RC_ProjectSourceVersion={os.environ.get("UNIXNU_XNU_VERSION", "12377.121.6")}',
f'BUILD_LTO={int(options.lto)}','BOUND_CHECKS=0','BUILD_WERROR=0','MAKEJOBS=-j8',
'LDFLAGS_KERNEL_LTO=-miphoneos-version-min=26.0 -Wl,-mllvm,-inline-threshold=100',
f'CFLAGS_EXTRA={flags}',
'copyio.o_CFLAGS_ADD=-march=armv8.1-a','pcb.o_CFLAGS_ADD=-march=armv8.1-a',
'LD_KERNEL_ARCHIVES='+' '.join([f'-L{sdk}/usr/local/lib/kernel','-lfirehose_kernel',
*map(str,archives),*(f'-Wl,-force_load,{p}' for p in forced)]),
'XNU_EXTERNAL_KERNEL_ARCHIVES='+' '.join(map(str,archives+forced)),
f'OBJROOT={output}/{mode}-obj',f'SYMROOT={output}/{mode}-sym',f'DSTROOT={output}/{mode}-dst']
(output/f'{mode}-command.json').write_text(json.dumps(args,indent=2)+'\n')
with (output/f'{mode}-build.log').open('w') as log:
result = subprocess.run(args,cwd=root,env=env,stdout=log,stderr=subprocess.STDOUT)
print('RELEASE reference exit',result.returncode,'log',output/f'{mode}-build.log',flush=True)
# The bring-up patch calls native hooks from the kernel core behind weak_import declarations; on arm64e
# the authenticated pointer of an undefined weak symbol fails the link. The private copy stubs them: this
# comparison kernel never runs, the prelink step reads its layout.
build_log = (output / f'{mode}-build.log').read_text()
stub_names = sorted(set(re.findall(r'^\s+"(_(?:native_[a-z0-9_]+|boot_log_rounds))"', build_log, re.M)))
if stub_names:
(output / 'native-stubs.c').write_text(''.join(f'long {name[1:]};\n' for name in stub_names)) # the linker names carry the leading underscore
iphoneos = subprocess.check_output(['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'], text=True).strip()
subprocess.run(['xcrun', 'clang', '-arch', 'arm64e', '-mkernel', '-isysroot', iphoneos, '-c',
'-o', str(output / 'native-stubs.o'), str(output / 'native-stubs.c')], check=True)
at = next(i for i, a in enumerate(args) if a.startswith('LD_KERNEL_ARCHIVES='))
args[at] += f' -Wl,-force_load,{output}/native-stubs.o'
# Forced exports the built objects cannot provide: remove them from this private copy and link again
# (the symbols are absent from the comparison kernel either way; the first link can stop exactly there,
# and its file list is what names the objects that were built).
objects = (output / f'{mode}-obj/RELEASE_ARM64_BCM2837/link.filelist').read_text().splitlines()
names = set()
for i in range(0, len(objects), 100):
listed = subprocess.run(['xcrun', 'nm', '-gUj', *objects[i:i + 100]], capture_output=True, text=True, check=True)
names.update(symbol for symbol in listed.stdout.splitlines() if symbol.startswith('_') and ' ' not in symbol)
removed = []
for filename in ('Private.exports', 'Private.arm64.exports'):
path = output / 'source/config' / filename
lines = path.read_text().splitlines(keepends=True)
rejected = {line.strip() for line in lines
if line.strip().startswith('_') and ':' not in line and not any(fnmatch.fnmatchcase(n, line.strip()) for n in names)}
removed.extend(sorted(rejected))
path.write_text(''.join(line for line in lines if line.strip() not in rejected))
import json as _json
record = output / 'omitted-unavailable-forced-exports.json'
previous = _json.loads(record.read_text()) if record.exists() else []
record.write_text(_json.dumps(sorted(set(previous + removed)), indent=2) + chr(10))
print('removed', len(removed), 'unavailable forced exports; linking again')
with (output / f'{mode}-build.log').open('a') as log:
result = subprocess.run(args, cwd=root, env=env, stdout=log, stderr=subprocess.STDOUT)
print('RELEASE reference exit', result.returncode, 'log', output / f'{mode}-build.log', flush=True)
if result.returncode:
raise SystemExit(result.returncode)
import shutil as _shutil
built = output / f'{mode}-obj/RELEASE_ARM64_BCM2837/mach.release.bcm2837.unstripped'
_shutil.copyfile(built, final)
print('reference', final)
def file_hash(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def tree_hash(path):
"""Hash inputs by content; follow SDK header-directory links as the compiler does."""
if not path.is_dir():
raise FileNotFoundError(path)
digest = hashlib.sha256()
for directory, dirs, files in os.walk(path, followlinks=True):
dirs[:] = sorted(d for d in dirs if d != '.git')
for name in sorted(files):
item = Path(directory) / name
digest.update(str(item.relative_to(path)).encode() + b'\0')
digest.update(file_hash(item).encode() + b'\0')
return digest.hexdigest()
def reusable(final, record, inputs):
try:
saved = json.loads(record.read_text())
return saved['inputs'] == inputs and saved['sha256'] == file_hash(final)
except (OSError, ValueError, KeyError):
return False
def linked_objects(directory, lto):
if lto:
# XNU splits the final link into non-LTO objects and the code generated
# by its intermediate LTO link. There is no link.filelist in this mode.
objects = (directory / 'nonltolink.filelist').read_text().splitlines()
generated = directory / 'justlto.o'
objects += [str(p) for p in sorted(generated.glob('*.o'))] if generated.is_dir() else [str(generated)]
else:
objects = (directory / 'link.filelist').read_text().splitlines()
objects += [str(directory / name) for name in
('version.o', 'lastkerneldataconst.o', 'lastkernelconstructor.o')
if (directory / name).is_file()]
return [str(Path(name) if Path(name).is_absolute() else directory / name)
for name in objects if name.strip()]
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--tree', required=True, type=Path, help='prepared XNU build tree of the OS profile')
parser.add_argument('--lto', action='store_true', help='enable the RELEASE default link-time optimization')
options = parser.parse_args()
mode = 'lto' if options.lto else 'snapshot'
root = options.tree.resolve()
build_root = root / 'build/xnu/fileset-reference'
final = build_root / 'mach.release.bcm2837.unstripped.noctf'
cache_record = final.with_suffix('.json')
sdk = root / '.cache/fakeroot'
archives = [root/'build/corecrypto-2023/libcorecrypto_kernel_noasm-namespaced.a']
forced = [root/f'build/{folder}/{name}' for folder,name in (
('libpthread-kernel','libpthread_kernel.a'),('armdt-platform','libArmDTPlatform.a'),('armcpu','libArmCpu.a'))]
inputs = {
'mode': mode,
'script': file_hash(Path(__file__)),
'source': tree_hash(root / '.cache/xnu-work'),
'sdk': tree_hash(sdk),
'archives': {str(p.relative_to(root)): file_hash(p) for p in archives + forced},
'versions': {name: os.environ.get(name, default) for name, default in (
('UNIXNU_IOS_VERSION', '26.0'), ('UNIXNU_XNU_DARWIN', '25.5.0'),
('UNIXNU_XNU_VERSION', '12377.121.6'))},
'compiler': subprocess.check_output(['xcrun', 'clang', '--version'], text=True),
'environment': {name: value for name, value in os.environ.items()
if name.startswith(('XNU_', 'RC_', 'CCC_')) or name in ('CC', 'CXX', 'LD')},
}
if reusable(final, cache_record, inputs):
print(f'reference inputs and digest match ({mode}): {final}')
return
# Separate workspaces prevent non-LTO objects and stale patched sources from
# being reused after a mode, source, SDK, archive or compiler change.
key = hashlib.sha256(json.dumps(inputs, sort_keys=True).encode()).hexdigest()
output = build_root / f'{mode}-{key[:16]}'
output.mkdir(parents=True, exist_ok=True)
(output / 'inputs.json').write_text(json.dumps(inputs, indent=2) + '\n')
source = output / 'source'
if not source.exists():
shutil.copytree(root/'.cache/xnu-work', source, symlinks=True,
ignore=shutil.ignore_patterns('.git'))
# The shared tree currently has this observation hook at file scope. The
# reference build disables observation hooks; repair only its private copy.
sleh = source / 'osfmk/arm64/sleh.c'
text = sleh.read_text()
misplaced = '\n\tboot_progress_user_breakpoint(state, esr);\n#if __has_feature(ptrauth_calls)'
if misplaced in text:
sleh.write_text(text.replace(misplaced, '\n#if __has_feature(ptrauth_calls)', 1))
manifest = {str(p.relative_to(source)): hashlib.sha256(p.read_bytes()).hexdigest()
for p in source.rglob('*') if p.is_file()}
(output/'source-sha256.json').write_text(json.dumps(manifest,indent=2)+'\n')
env = dict(os.environ, KERNEL_SDKROOT=str(sdk),
XNU_AVAILABILITY_PL=str(sdk/'usr/local/libexec/availability.pl'))
flags = '-Wno-reserved-identifier -Wno-pre-c11-compat -Wno-missing-designated-field-initializers -Wno-suggest-override -Wno-unnecessary-virtual-specifier -Wno-suggest-destructor-override -Wno-ms-bitfield-padding -DXNU_NATIVE_PAN=1 -DXNU_SOURCE_CORECRYPTO14=1 -DXNU_BOOT_PROGRESS=0'
args = ['make','-C',str(source),'build','SDKROOT=iphoneos',
'ARCH_CONFIGS=ARM64','MACHINE_CONFIGS=BCM2837','KERNEL_CONFIGS=RELEASE',
'ARCH_STRING_FOR_CURRENT_MACHINE_CONFIG=arm64e','LARGE_MEMORY_DEFINE=-UARM_LARGE_MEMORY',
'ARM64_LINK_ADDR=0xfffffff007004000','ARM64_PLKSEG_ADDR=0xfffffff004004000',
f'DEPLOYMENT_TARGET_FLAGS=-miphoneos-version-min={os.environ.get("UNIXNU_IOS_VERSION", "26.0")} -DXNU_TARGET_OS_IOS',
f'DEPLOYMENT_LINKER_FLAGS=-Wl,-ios_version_min,{os.environ.get("UNIXNU_IOS_VERSION", "26.0")}',
f'RC_DARWIN_KERNEL_VERSION={os.environ.get("UNIXNU_XNU_DARWIN", "25.5.0")}',
f'RC_ProjectSourceVersion={os.environ.get("UNIXNU_XNU_VERSION", "12377.121.6")}',
f'BUILD_LTO={int(options.lto)}','BOUND_CHECKS=0','BUILD_WERROR=0','MAKEJOBS=-j8',
f'LDFLAGS_KERNEL_LTO=-miphoneos-version-min={inputs["versions"]["UNIXNU_IOS_VERSION"]} -Wl,-mllvm,-inline-threshold=100',
f'CFLAGS_EXTRA={flags}',
'copyio.o_CFLAGS_ADD=-march=armv8.1-a','pcb.o_CFLAGS_ADD=-march=armv8.1-a',
'LD_KERNEL_ARCHIVES='+' '.join([f'-L{sdk}/usr/local/lib/kernel','-lfirehose_kernel',
*map(str,archives),*(f'-Wl,-force_load,{p}' for p in forced)]),
'XNU_EXTERNAL_KERNEL_ARCHIVES='+' '.join(map(str,archives+forced)),
f'OBJROOT={output}/{mode}-obj',f'SYMROOT={output}/{mode}-sym',f'DSTROOT={output}/{mode}-dst']
(output/f'{mode}-command.json').write_text(json.dumps(args,indent=2)+'\n')
with (output/f'{mode}-build.log').open('w') as log:
result = subprocess.run(args,cwd=root,env=env,stdout=log,stderr=subprocess.STDOUT)
print('RELEASE reference exit',result.returncode,'log',output/f'{mode}-build.log',flush=True)
# The bring-up patch calls native hooks from the kernel core behind weak_import declarations; on arm64e
# the authenticated pointer of an undefined weak symbol fails the link. The private copy stubs them: this
# comparison kernel never runs, the prelink step reads its layout.
build_log = (output / f'{mode}-build.log').read_text()
stub_names = sorted(set(re.findall(r'^\s+"(_(?:native_[a-z0-9_]+|boot_log_rounds))"', build_log, re.M)))
if stub_names:
(output / 'native-stubs.c').write_text(''.join(f'long {name[1:]};\n' for name in stub_names)) # the linker names carry the leading underscore
iphoneos = subprocess.check_output(['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'], text=True).strip()
subprocess.run(['xcrun', 'clang', '-arch', 'arm64e', '-mkernel', '-isysroot', iphoneos, '-c',
'-o', str(output / 'native-stubs.o'), str(output / 'native-stubs.c')], check=True)
at = next(i for i, a in enumerate(args) if a.startswith('LD_KERNEL_ARCHIVES='))
args[at] += f' -Wl,-force_load,{output}/native-stubs.o'
# Forced exports the built objects cannot provide: remove them from this private copy and link again
# (the symbols are absent from the comparison kernel either way; the first link can stop exactly there,
# and its file list is what names the objects that were built).
objects = linked_objects(output / f'{mode}-obj/RELEASE_ARM64_BCM2837', options.lto)
objects += list(map(str, archives + forced))
names = set()
for i in range(0, len(objects), 100):
listed = subprocess.run(['xcrun', 'nm', '-gUj', *objects[i:i + 100]], capture_output=True, text=True, check=True)
names.update(symbol for symbol in listed.stdout.splitlines() if symbol.startswith('_') and ' ' not in symbol)
removed = []
for filename in ('Private.exports', 'Private.arm64.exports'):
path = output / 'source/config' / filename
lines = path.read_text().splitlines(keepends=True)
rejected = {line.strip() for line in lines
if line.strip().startswith('_') and ':' not in line and not any(fnmatch.fnmatchcase(n, line.strip()) for n in names)}
removed.extend(sorted(rejected))
path.write_text(''.join(line for line in lines if line.strip() not in rejected))
import json as _json
record = output / 'omitted-unavailable-forced-exports.json'
previous = _json.loads(record.read_text()) if record.exists() else []
record.write_text(_json.dumps(sorted(set(previous + removed)), indent=2) + chr(10))
print('removed', len(removed), 'unavailable forced exports; linking again')
with (output / f'{mode}-build.log').open('a') as log:
result = subprocess.run(args, cwd=root, env=env, stdout=log, stderr=subprocess.STDOUT)
print('RELEASE reference exit', result.returncode, 'log', output / f'{mode}-build.log', flush=True)
if result.returncode:
raise SystemExit(result.returncode)
built = output / f'{mode}-obj/RELEASE_ARM64_BCM2837/mach.release.bcm2837.unstripped'
temporary = final.with_suffix('.tmp')
shutil.copyfile(built, temporary)
os.replace(temporary, final)
cache_record.write_text(json.dumps({'inputs': inputs, 'sha256': file_hash(final),
'build': str(output)}, indent=2) + '\n')
print('reference', final)
if __name__ == '__main__':
main()
+17
View File
@@ -90,6 +90,23 @@ def main():
except Error:
pass
expect(payload.check_args(['-v', 'a=1', 'b=x,y']) == '-v a=1 b=x,y', 'kernel arguments')
with tempfile.TemporaryDirectory() as name:
root = Path(name)
(root / 'configs').mkdir()
(root / 'configs/boot_defconfig').write_text('CONFIG_XNU_OWNER_DATA_MOUNT=y\n')
ctx = argparse.Namespace(config={}, config_path=root / '.config', config_name='boot_defconfig', device=root)
for configured in (False, True):
if configured:
ctx.config_path.write_text('CONFIG_XNU_OWNER_DATA_MOUNT=y\n')
ctx.config = parse_config(ctx.config_path.read_text())
try:
payload.check_data_input(ctx, None)
expect(False, 'owner Data payload accepted without its seed')
except Error as error:
expect('--data-ramdisk' in str(error), 'missing seed diagnostic')
payload.check_data_input(ctx, root / 'seed.dbr')
ctx.config = {'XNU_OWNER_DATA_MOUNT': False}
payload.check_data_input(ctx, None)
for bad in (['a b'], ['x' * 1100], ['a;b']):
try:
payload.check_args(bad)
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Create a first-install APFS Data seed from a read-only IPSW OS mount.
The resulting seed is installation state. Keep it for subsequent builds: APFS
UUIDs and transaction metadata make a new invocation a different installation.
"""
import argparse
import json
import os
from pathlib import Path
import plistlib
import shutil
import stat
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'drivers/DarwinBoot/Test'))
import packed_ramdisk
def merge(mount, home, name, values):
path = mount / home / 'Library/Preferences' / (name + '.plist')
path.parent.mkdir(parents=True, exist_ok=True)
content = plistlib.loads(path.read_bytes()) if path.is_file() else {}
content.update(values)
path.write_bytes(plistlib.dumps(content, fmt=plistlib.FMT_BINARY))
path.chmod(0o644)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('os_mount', type=Path)
parser.add_argument('output', type=Path)
parser.add_argument('--gib', type=int, required=True)
parser.add_argument('--build', required=True)
parser.add_argument('--skip-setup', action='store_true')
args = parser.parse_args()
source = args.os_mount / 'private/var'
if not (args.os_mount / 'sbin/launchd').is_file() or not source.is_dir():
parser.error('not an IPSW OS volume')
if args.output.exists():
parser.error('refuse to overwrite an existing installation seed')
if not 3 <= args.gib <= 1024:
parser.error('--gib must be in 3..1024')
args.output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix='new-data-', dir=args.output.parent) as td:
work = Path(td)
raw = work / 'seed.cdr'
subprocess.run(['hdiutil', 'create', '-size', '32m', '-layout', 'NONE', '-fs', 'APFS',
'-volname', 'Data', '-type', 'UDTO', raw], check=True)
mount = work / 'mount'
mount.mkdir()
attached = plistlib.loads(subprocess.check_output(['hdiutil', 'attach', '-nobrowse', '-owners', 'off',
'-mountpoint', mount, '-plist', raw]))
device = attached['system-entities'][0]['dev-entry']
try:
for current, dirs, files in os.walk(source, followlinks=False):
dirs[:] = [name for name in dirs if name != 'staged_system_apps']
dest = mount / Path(current).relative_to(source)
dest.mkdir(exist_ok=True)
dest.chmod(stat.S_IMODE(Path(current).stat().st_mode))
for name in list(dirs) + files:
src, dst = Path(current) / name, dest / name
if src.is_symlink():
dst.symlink_to(os.readlink(src))
if name in dirs:
dirs.remove(name)
elif src.is_file():
shutil.copy2(src, dst)
for name in ('keybags', 'mobile/tmp', 'tmp', 'run', 'containers/Bundle',
'containers/Data/System', 'containers/Shared/SystemGroup', 'containers/Temp'):
(mount / name).mkdir(parents=True, exist_ok=True)
(mount / 'tmp').chmod(0o1777)
if args.skip_setup:
merge(mount, 'mobile', 'com.apple.purplebuddy', dict(SetupDone=True,
SetupFinishedAllSteps=True, SetupVersion=11, daemonizeNeedsToRun=False))
merge(mount, 'mobile', 'com.apple.purplebuddy.notbackedup', {name: False for name in (
'ShouldRunDiagnosticsMiniBuddy', 'ShouldRunHSA2MiniBuddy', 'ShouldRunKeychainSyncMiniBuddy',
'ShouldRunMesaMiniBuddy', 'ShouldRunPaymentMiniBuddy', 'ShouldRunStandardMiniBuddy', 'SilentICDPUpgrade')})
merge(mount, 'mobile', 'com.apple.managedconfiguration.notbackedup',
dict(LockdownActivationIndicatesCloudConfigurationAvailable=False))
merge(mount, 'mobile', 'com.apple.springboard', dict(BKNoWatchdogs=True,
FBLaunchWatchdogExceptions={'com.apple.purplebuddy': 100.0}, MCForceShowCloudConfigurationUI=False))
merge(mount, 'mobile', 'com.apple.migration', dict(LastSystemVersion=args.build,
DMUserDataDisposition=dict(SyntheticDidUpgrade=False, buildVersion=args.build)))
for home in ('root', 'mobile'):
for name in ('backboardd', '.GlobalPreferences'):
merge(mount, home, name, dict(CAEnableDeepFramebuffer=False,
CAEnableFramebufferCompression=False, CAEnableIOSurfaceCompression=False))
finally:
subprocess.run(['hdiutil', 'detach', device], check=True)
base = work / 'base.dbr'
packed_ramdisk.pack(raw, base)
candidate = work / 'data.dbr'
subprocess.run([sys.executable, ROOT / 'scripts/seed-data-grow.py', '--gib', str(args.gib), base, candidate], check=True)
candidate.replace(args.output)
print(json.dumps(dict(output=str(args.output), first_install=True, preserve_for_future_builds=True)))
if __name__ == '__main__':
main()
@@ -0,0 +1,62 @@
{
"kernelcache_sha256": "38e6d9fa5f789b8228f4617ef4c6d2a3fd8ab564abdc9b62920f1db53fc1780a",
"description": "Remaining unresolved references of the 24A446 startup baseline. This bounds regressions, not runtime qualification.",
"required_original_branches": [
"__ZN25IOMobileFramebufferLegacyC2EPK11OSMetaClass",
"__ZN11IOHIDDevice10handleOpenEP9IOServicejPv"
],
"missing": {
"com.apple.driver.AppleARMWatchdogTimer": {
"0xfffffe00070446a8": 1
},
"com.apple.iokit.IOHIDFamily": {
"0xfffffe000b8b3ce8": 1
}
},
"stubbed": {
"com.apple.driver.AppleARMPlatform": {
"0xfffffe000aebcc3c": 1,
"0xfffffe000aebdee4": 1,
"0xfffffe000aebdef4": 1,
"0xfffffe000b433e68": 1,
"0xfffffe000b4ca1d0": 1,
"0xfffffe000b4ca260": 1,
"0xfffffe000b4ca2e8": 1
},
"com.apple.driver.AppleARMWatchdogTimer": {
"0xfffffe000ad53f60": 1,
"0xfffffe000aebdee4": 1
},
"com.apple.driver.ApplePMGR": {
"0xfffffe000ad6df18": 1,
"0xfffffe000ad6e204": 1
},
"com.apple.iokit.IOCryptoAcceleratorFamily": {
"0xfffffe000aec5fe0": 1
},
"com.apple.iokit.IOGPUFamily": {
"0xfffffe000ae14d08": 1
},
"com.apple.iokit.IOHIDFamily": {
"0xfffffe000b417540": 1,
"0xfffffe000b430ab8": 1,
"0xfffffe000b430bd4": 1
},
"com.apple.iokit.IOStorageFamily": {
"0xfffffe000b430ab8": 1,
"0xfffffe000b430bd4": 1
},
"com.apple.iokit.IOSurface": {
"0xfffffe000b4b6724": 1
},
"com.apple.security.AppleImage4": {
"0xfffffe000b5a3ba8": 1
},
"com.apple.security.Image4": {
"0xfffffe000b2a1694": 1,
"0xfffffe000b2a170c": 1,
"0xfffffe000b2a1778": 1,
"0xfffffe000b2a1798": 1
}
}
}
+108
View File
@@ -0,0 +1,108 @@
"""A cached comparison kernel must belong to the requested build inputs."""
import importlib.util
import json
import os
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location(
'reference', Path(__file__).with_name('build-fileset-release-reference.py'))
reference = importlib.util.module_from_spec(spec)
spec.loader.exec_module(reference)
class ReferenceCacheTests(unittest.TestCase):
def test_lto_filelist_paths_are_relative_to_the_object_directory(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / 'nonltolink.filelist').write_text('lastkernelconstructor.o\nnonlto.o\n')
self.assertEqual(reference.linked_objects(root, True),
[str(root / name) for name in
('lastkernelconstructor.o', 'nonlto.o', 'justlto.o')])
def test_builder_rebuilds_snapshot_as_lto_then_reuses_matching_output(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
files = ['.cache/xnu-work/osfmk/arm64/sleh.c',
'.cache/xnu-work/config/Private.exports',
'.cache/xnu-work/config/Private.arm64.exports',
'.cache/fakeroot/usr/include/header.h',
'build/corecrypto-2023/libcorecrypto_kernel_noasm-namespaced.a',
'build/libpthread-kernel/libpthread_kernel.a',
'build/armdt-platform/libArmDTPlatform.a',
'build/armcpu/libArmCpu.a']
for name in files:
path = root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text('')
builds = []
def make(command, **kwargs):
if command[:2] == ['xcrun', 'nm']:
return SimpleNamespace(returncode=0, stdout='')
values = dict(arg.split('=', 1) for arg in command if '=' in arg)
mode = values['BUILD_LTO']
builds.append(mode)
output = Path(values['OBJROOT']) / 'RELEASE_ARM64_BCM2837'
output.mkdir(parents=True, exist_ok=True)
if mode == '1':
(output / 'nonltolink.filelist').write_text('')
(output / 'justlto.o').write_bytes(b'object')
else:
(output / 'link.filelist').write_text('')
(output / 'mach.release.bcm2837.unstripped').write_text(mode)
return SimpleNamespace(returncode=0)
argv = ['reference', '--tree', str(root)]
with patch.object(reference.subprocess, 'run', side_effect=make), \
patch.object(reference.subprocess, 'check_output', return_value='compiler'), \
patch('sys.argv', argv):
reference.main()
self.assertEqual(builds, ['0', '0'])
argv.append('--lto')
reference.main()
self.assertEqual(builds, ['0', '0', '1', '1'])
reference.main()
self.assertEqual(builds, ['0', '0', '1', '1'])
final = root / 'build/xnu/fileset-reference/mach.release.bcm2837.unstripped.noctf'
self.assertEqual(final.read_text(), '1')
def test_old_snapshot_cannot_satisfy_lto_request(self):
with tempfile.TemporaryDirectory() as directory:
final = Path(directory) / 'reference'
record = Path(directory) / 'record.json'
final.write_bytes(b'comparison kernel')
snapshot = {'mode': 'snapshot', 'source': 'old'}
self.assertFalse(reference.reusable(final, record, snapshot))
record.write_text(json.dumps({'inputs': snapshot, 'sha256': reference.file_hash(final)}))
self.assertTrue(reference.reusable(final, record, snapshot))
self.assertFalse(reference.reusable(final, record, dict(snapshot, mode='lto')))
self.assertFalse(reference.reusable(final, record, dict(snapshot, source='changed')))
final.write_bytes(b'corrupt kernel')
self.assertFalse(reference.reusable(final, record, snapshot))
def test_source_content_and_linked_sdk_headers_invalidate_cache(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root / 'source'
source.mkdir()
header = source / 'header.h'
header.write_text('old')
timestamp = header.stat().st_mtime_ns
first = reference.tree_hash(source)
header.write_text('new')
os.utime(header, ns=(timestamp, timestamp))
self.assertNotEqual(first, reference.tree_hash(source))
sdk = root / 'sdk'
sdk.mkdir()
(sdk / 'include').symlink_to(source, target_is_directory=True)
first = reference.tree_hash(sdk)
header.write_text('next')
self.assertNotEqual(first, reference.tree_hash(sdk))
if __name__ == '__main__':
unittest.main()
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Regressions for first-install input handling and fileset startup gaps."""
import argparse
import copy
import json
from pathlib import Path
import struct
import tempfile
import unittest
import uuid
from unittest.mock import patch
from unixnu.config import parse_config
from unixnu.kernel import check_prelink_regressions
from unixnu import runtime
from unixnu.util import Error
ROOT = Path(__file__).resolve().parents[1]
IDENTITY = '2A5E9027-D9E1-40F9-983C-41BD3F1994F5'
def seed(path):
block = bytearray(4096)
block[32:36] = b'APSB'
block[240:256] = uuid.UUID(IDENTITY).bytes
path.write_bytes(struct.pack('<8sIIQQ', b'DBRDISK1', 1, 4096, 4096, 1) + struct.pack('<QQ', 0, 4096) + block)
class RuntimeInputs(unittest.TestCase):
def context(self, out, explicit=True):
work = out / 'runtime'
work.mkdir()
data = work / 'data.dbr'
seed(data)
trust, addon = work / 'trust.bundle', work / 'addon.bin'
trust.write_bytes(b'trust')
addon.write_bytes(b'NGPUADD2')
config = out / '.config'
config.write_text(f'CONFIG_XNU_OWNER_DATA_MOUNT=y\nCONFIG_XNU_OWNER_DATA_VOLUME_UUID="{IDENTITY}"\n')
return argparse.Namespace(
board={'payload': {'runtime': {'tool': 'scripts/build-boot-runtime.py'}}},
tree=ROOT, device=out, work=out, config_path=config,
need_config=lambda: parse_config(config.read_text()),
args=argparse.Namespace(data_ramdisk=str(data) if explicit else None,
owner_trust_cache=str(trust), trust_cache=None, gpu_addon=str(addon), init_data=False))
def test_reuse_keeps_seed_bytes(self):
with tempfile.TemporaryDirectory() as td:
ctx = self.context(Path(td))
before = Path(ctx.args.data_ramdisk).read_bytes()
with patch.object(runtime, 'Ipsw', side_effect=AssertionError('must not fetch or regenerate explicit inputs')):
runtime.prepare(ctx, kernel_build=True)
self.assertEqual(Path(ctx.args.data_ramdisk).read_bytes(), before)
self.assertEqual(runtime.seed_uuid(ROOT, Path(ctx.args.data_ramdisk)), IDENTITY)
def test_missing_cache_requires_first_install(self):
with tempfile.TemporaryDirectory() as td:
ctx = self.context(Path(td), explicit=False)
(ctx.work / 'runtime/data.dbr').unlink()
with patch.object(runtime, 'Ipsw', side_effect=AssertionError('must fail before fetching')):
with self.assertRaisesRegex(Error, 'FIRST installation'):
runtime.prepare(ctx, kernel_build=True)
def test_payload_cannot_change_compiled_uuid(self):
with tempfile.TemporaryDirectory() as td:
ctx = self.context(Path(td))
ctx.config_path.write_text('CONFIG_XNU_OWNER_DATA_VOLUME_UUID="00000000-0000-0000-0000-000000000000"\n')
with self.assertRaisesRegex(Error, 'rebuild the kernel'):
runtime.prepare(ctx)
runtime.prepare(ctx, kernel_build=True)
self.assertEqual(ctx.need_config()['XNU_OWNER_DATA_VOLUME_UUID'], IDENTITY)
def test_new_gap_rejected_but_resolved_gap_allowed(self):
policy = dict(missing={}, stubbed={'owner': {'0x1000': 1}}, required_original_branches=['adapter'])
record = dict(collection_slide='0x100', missing={}, stubbed={'owner': {'0x1100': 1}},
native_original_branches=[{'symbol': 'adapter'}])
check_prelink_regressions(record, policy)
worse = copy.deepcopy(record)
worse['stubbed']['owner']['0x1200'] = 1
with self.assertRaisesRegex(Error, 'prelink regression'):
check_prelink_regressions(worse, policy)
record['stubbed'] = {}
check_prelink_regressions(record, policy)
record['native_original_branches'] = []
with self.assertRaisesRegex(Error, 'adapter branch'):
check_prelink_regressions(record, policy)
if __name__ == '__main__':
unittest.main()
+6 -3
View File
@@ -5,12 +5,12 @@ import platform
import shutil
import sys
from . import bootimg, components, feeds, firmware, ipsw, kernel, package, patches, payload, pipeline, profiles, tree, userdata
from . import bootimg, components, feeds, firmware, ipsw, kernel, package, patches, payload, pipeline, profiles, runtime, tree, userdata
from .config import LOCAL_KINDS, Context, parse_config
from .util import Error, expand, git, log
STEPS = ('kernel', 'package', 'firmware', 'payload')
OPTIONAL_STEPS = ('userdata',) # large; built only when named
OPTIONAL_STEPS = ('userdata', 'runtime')
def common():
@@ -34,7 +34,8 @@ def common():
p.add_argument('--trust-cache', metavar='FILE', help='trust cache bundle for the payload')
p.add_argument('--owner-trust-cache', metavar='FILE', help='owner-signed trust cache bundle that carries the original caches and extra modules; replaces --trust-cache and skips the board allow-list')
p.add_argument('--skip-setup', action='store_true', help='mark Setup as done in the Data seed, so the Hello pages are skipped (needs --data-ramdisk; optional)')
p.add_argument('--data-ramdisk', metavar='FILE', help='Data ramdisk seed for the payload (an input; not built here)')
p.add_argument('--data-ramdisk', metavar='FILE', help='preserved Data ramdisk seed for this installation')
p.add_argument('--init-data', action='store_true', help='create and retain a new Data seed from the IPSW for a FIRST installation; requires kernel build')
p.add_argument('--gpu-addon', metavar='FILE', help='GPU addon file (scripts/make-gpu-addon.py) the kernel writes into the Data volume at boot')
p.add_argument('--unique-chip-id', metavar='HEX', help='platform seed for the device tree; or UNIXNU_UNIQUE_CHIP_ID')
p.add_argument('--ipsw', metavar='PATH_OR_URL', help='the target IPSW: a local file or an https URL (default: url of board.toml [ipsw]); or UNIXNU_IPSW')
@@ -158,6 +159,8 @@ def cmd_build(ctx):
if 'firmware' in steps:
need_uefi(ctx) # fail before fetching anything
modules = prepare(ctx, steps + (['kernel'] if 'prelink' in steps else []))
if 'payload' in steps or 'runtime' in steps:
runtime.prepare(ctx, kernel_build='kernel' in steps)
if 'kernel' in steps:
kernel.build(ctx, modules)
kernel.prelink(ctx)
+20
View File
@@ -236,6 +236,21 @@ def stage_native(kernel, output):
return output
def check_prelink_regressions(record, policy):
"""An experimental allow-missing build may improve, but cannot add gaps."""
slide = int(record['collection_slide'], 16)
for kind in ('missing', 'stubbed'):
for owner, targets in record[kind].items():
for target, count in targets.items():
original = hex((int(target, 16) - slide) & ((1 << 64) - 1))
allowed = policy[kind].get(owner, {}).get(original, 0)
if count > allowed:
raise Error(f'prelink regression: {kind} {owner} original {original}, {count} sites (allowed {allowed})')
branches = {entry['symbol'] for entry in record.get('native_original_branches', [])}
if set(policy.get('required_original_branches', [])) - branches:
raise Error('prelink lacks required absolute adapter branch relocations')
def prelink(ctx):
"""Produce out/<device>/kernel/kernel.macho from the XNU build and the original kernelcache."""
spec = ctx.board.get('kernel', {}).get('prelink')
@@ -277,6 +292,11 @@ def prelink(ctx):
run(command + ['--output', candidate], cwd=ctx.tree, log_file=ctx.logs / 'prelink.log')
if not candidate.is_file():
raise Error('prelink completed without writing a kernel candidate')
if spec.get('regression_policy'):
policy = json.loads((ctx.tree / spec['regression_policy']).read_text())
if policy['kernelcache_sha256'] != sha256_file(cache):
raise Error('prelink regression policy names another kernelcache')
check_prelink_regressions(json.loads(candidate.with_suffix('.json').read_text()), policy)
success = True
finally:
if candidate_map.is_file():
+16
View File
@@ -38,6 +38,21 @@ def check_args(arguments):
return line
def check_data_input(ctx, data):
"""An owner-Data kernel needs a seed even when it resumes a persistent overlay."""
if data:
return
from .config import parse_config
config = ctx.config
if not ctx.config_path.is_file() and ctx.config_name:
selected = ctx.device / 'configs' / ctx.config_name
if selected.is_file():
config = parse_config(selected.read_text())
if config.get('XNU_OWNER_DATA_MOUNT'):
raise Error('CONFIG_XNU_OWNER_DATA_MOUNT requires --data-ramdisk FILE (or [build] data_ramdisk); '
'persistent Data still needs its original seed to publish the volume')
def check_native_root(kernel, arguments, vmm_present=False):
"""Refuse the known bare-metal AMFI panic before the volume is written.
@@ -111,6 +126,7 @@ def build(ctx, kernel=None):
if allowed and not owner_trust and sha256_file(trust) not in allowed:
raise Error(f'{trust} is not one of the trust caches this board accepts (board.toml [payload] trust_cache_sha256)')
data = expand(args.data_ramdisk) if getattr(args, 'data_ramdisk', None) else None
check_data_input(ctx, data)
if data and not data.is_file():
raise Error(f'Data ramdisk not found: {data}')
if getattr(args, 'skip_setup', False):
+5
View File
@@ -38,6 +38,11 @@ def write_manifest(ctx, modules, patch_order, files):
'revisions': revisions, 'patches': [str(path) for path in patch_order],
'kernel_command_line_bytes': len(command_line(ctx)),
'outputs': {name: {'bytes': path.stat().st_size, 'sha256': sha256_file(path)} for name, path in files.items()}}
runtime = ctx.work / 'runtime/inputs.json'
if runtime.is_file():
inputs = json.loads(runtime.read_text())
manifest['runtime'] = {'data_volume_uuid': inputs['data_volume_uuid'],
'inputs': {key: value['sha256'] for key, value in inputs['inputs'].items()}}
target = ctx.work / 'package.manifest.json'
target.write_text(json.dumps(manifest, indent=2, sort_keys=True) + '\n')
return target
+112
View File
@@ -0,0 +1,112 @@
"""Prepare reproducible runtime inputs before compiling the kernel.
The board supplies a builder. An existing Data seed is immutable: generating a
replacement is an explicit first-install operation, never a cache-miss fallback.
"""
import importlib.util
import json
import os
import re
import sys
import uuid
from pathlib import Path
from .ipsw import Ipsw
from .util import Error, log, run, sha256_file
def seed_uuid(tree, path):
spec = importlib.util.spec_from_file_location('runtime_packed_ramdisk', tree / 'drivers/DarwinBoot/Test/packed_ramdisk.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
_, extents = module.regions(path.read_bytes())
identities = set()
for _, data in extents:
for at in range(0, len(data), 4096):
block = data[at:at + 4096]
if block[32:36] == b'APSB':
identities.add(str(uuid.UUID(bytes=bytes(block[240:256]))).upper())
if len(identities) != 1:
raise Error(f'{path}: require a packed APFS seed with exactly one volume UUID')
return identities.pop()
def prepare(ctx, kernel_build=False):
spec = ctx.board.get('payload', {}).get('runtime')
if not spec or not spec.get('enabled', True):
from .payload import check_data_input
check_data_input(ctx, getattr(ctx.args, 'data_ramdisk', None))
return
if getattr(ctx, '_runtime_prepared', False):
return
args = ctx.args
out = ctx.work / 'runtime'
out.mkdir(parents=True, exist_ok=True)
seed = Path(args.data_ramdisk).expanduser().resolve() if args.data_ramdisk else out / 'data.dbr'
init = bool(getattr(args, 'init_data', False))
if init and args.data_ramdisk:
raise Error('--init-data cannot replace --data-ramdisk; use one first-install seed or reuse the original')
if init and not kernel_build:
raise Error('--init-data needs a kernel build to bind its new Data volume UUID')
if not seed.is_file() and not init:
raise Error('No preserved Data seed. Pass --data-ramdisk FILE for an existing installation, '
'or --init-data for a FIRST installation; a missing cache never regenerates Data automatically')
previous = out / 'inputs.json'
if not args.data_ramdisk and seed.is_file() and previous.is_file():
expected = json.loads(previous.read_text())['inputs']['data_ramdisk']['sha256']
if sha256_file(seed) != expected:
raise Error('Saved Data seed differs from its recorded SHA-256; restore the original installation seed')
owner = getattr(args, 'owner_trust_cache', None)
trust = owner or getattr(args, 'trust_cache', None)
addon = getattr(args, 'gpu_addon', None)
for name, path in [('Data seed', args.data_ramdisk), ('trust bundle', trust), ('GPU addon', addon)]:
if path and not Path(path).expanduser().is_file():
raise Error(f'{name} not found: {path}')
# Preserve explicit inputs. A source-built addon must be paired with newly
# collected CDHashes, so an explicit trust bundle alone is not sufficient.
if trust and not addon:
raise Error('Pass both --gpu-addon and its --owner-trust-cache, or omit both to build them from source')
request = dict(spec, tree=str(ctx.tree), device=str(ctx.device), output=str(out),
data_ramdisk=str(seed), init_data=init,
gpu_addon=str(Path(addon).expanduser().resolve()) if addon else None,
trust_cache=str(Path(trust).expanduser().resolve()) if trust else None)
if not seed.is_file() or not trust or not addon:
source = Ipsw(ctx)
if not seed.is_file() or not addon:
request['os_image'] = str(source.disk_image('OS'))
if not trust:
request['trust_modules'] = [str(source.extract(name)) for name in spec['trust_components']]
request_path = out / 'request.json'
request_path.write_text(json.dumps(request, indent=2) + '\n')
report = out / 'generated.json'
report.unlink(missing_ok=True)
run([sys.executable, ctx.tree / spec['tool'], '--request', request_path], cwd=ctx.tree,
env=dict(os.environ, UNIXNU_DEVICE=str(ctx.device)), log_file=ctx.logs / 'runtime.log')
if not report.is_file():
raise Error('runtime builder did not produce generated.json')
generated = json.loads(report.read_text())
seed = Path(generated['data_ramdisk'])
trust, addon = generated['trust_cache'], generated['gpu_addon']
owner = trust
identity = seed_uuid(ctx.tree, seed)
config = ctx.need_config()
if identity != config.get('XNU_OWNER_DATA_VOLUME_UUID'):
if not kernel_build:
raise Error('Data seed UUID differs from the compiled configuration; rebuild the kernel with this seed')
text = ctx.config_path.read_text()
text, count = re.subn(r'^CONFIG_XNU_OWNER_DATA_VOLUME_UUID=.*$',
f'CONFIG_XNU_OWNER_DATA_VOLUME_UUID="{identity}"', text, flags=re.M)
if count != 1:
raise Error('runtime Data requires exactly one CONFIG_XNU_OWNER_DATA_VOLUME_UUID')
ctx.config_path.write_text(text)
ctx.need_config()
args.data_ramdisk, args.gpu_addon = str(seed), str(addon)
if owner:
args.owner_trust_cache = str(trust)
else:
args.trust_cache = str(trust)
inputs = {name: {'path': str(path), 'sha256': sha256_file(Path(path))}
for name, path in [('data_ramdisk', seed), ('trust_cache', trust), ('gpu_addon', addon)]}
(out / 'inputs.json').write_text(json.dumps(dict(inputs=inputs, data_volume_uuid=identity), indent=2) + '\n')
log(f'runtime: Data UUID {identity}; preserved seed sha256 {inputs["data_ramdisk"]["sha256"]}')
ctx._runtime_prepared = True