Integrate iPadOS 27 startup fixes and runtime input preparation
This commit is contained in:
1 parent
a4419c8af7
commit
474cfeeb4e
24 files changed
+1079
-109
No files matched your search
@@ -103,6 +103,13 @@ An `--owner-trust-cache` bundle is copied without the `[payload] trust_cache_sha
|
|||||||
|
|
||||||
## Data ramdisk
|
## 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:
|
A Data seed can be delivered in two forms:
|
||||||
|
|
||||||
| Form | ADT placeholder | Behavior |
|
| Form | ADT placeholder | Behavior |
|
||||||
|
|||||||
@@ -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=5` | Start Wi-Fi. Without it the module never runs. |
|
||||||
| `sheng-wifi-aj=0` | No join at boot; Settings joins. |
|
| `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
|
### 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.
|
- 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.
|
||||||
|
|||||||
@@ -19,12 +19,44 @@ matching code signature before reusing a map.
|
|||||||
comparison. It transfers unambiguous names from unique code matches and resolves
|
comparison. It transfers unambiguous names from unique code matches and resolves
|
||||||
those names in the actual native kernel. The map records the reference hash.
|
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
|
The emitter places native segments within the collection's code, constant and
|
||||||
data ranges, applies native split-segment records, rebuilds chained fixups and
|
data ranges, applies native split-segment records, rebuilds chained fixups and
|
||||||
function starts, and recomputes corecrypto's code HMAC. The collection root uses
|
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
|
the native link address. The ordinary prelink `--slide` value does not apply to
|
||||||
filesets. The native input executable is unchanged.
|
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
|
Missing mappings stop emission by default. `--allow-missing` produces an
|
||||||
experimental image and the JSON report lists every unresolved target by kext.
|
experimental image and the JSON report lists every unresolved target by kext.
|
||||||
An image that passes structural checks still requires complete mappings and
|
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)
|
[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.
|
||||||
@@ -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`.
|
4. Record the run in the `packaging` area of `porting.toml`.
|
||||||
|
|
||||||
[Build and boot](/building), [Device repository](/porting/device-repository)
|
[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.
|
||||||
@@ -41,6 +41,11 @@ initrd = "Payloads/scratch-data.img"
|
|||||||
args = "-v serial=3"
|
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` 合并后的命令行。
|
提供 Data 种子时存在 `initrd`。`args` 是 `[payload] kernel_args` 和 `--kernel-arg` 合并后的命令行。
|
||||||
|
|
||||||
## ADT sidecar
|
## ADT sidecar
|
||||||
|
|||||||
@@ -26,6 +26,20 @@ Ath12kWifi 在自己的兼容层(`shim/`)上编译 Linux 的 ath12k、MHI
|
|||||||
| `sheng-wifi=5` | 启动 Wi-Fi。没有它模块不会运行。 |
|
| `sheng-wifi=5` | 启动 Wi-Fi。没有它模块不会运行。 |
|
||||||
| `sheng-wifi-aj=0` | 启动时不加入网络,由设置加入。 |
|
| `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 的模式、电压和使能请求。缺少模式请求时有两条保持关闭,链路无法训练。
|
- 六条 PMU 电源轨各需要 RPMh 的模式、电压和使能请求。缺少模式请求时有两条保持关闭,链路无法训练。
|
||||||
|
|||||||
@@ -5,6 +5,27 @@
|
|||||||
`--fileset-reference FILE` 可以提供带符号的对照内核。唯一的代码匹配可提供符号名,
|
`--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 路径并生成
|
对于 arm64e fileset 内核集合,`emit_prelink.py` 会选择 fileset 路径并生成
|
||||||
函数和数据映射。`--fileset-map FILE` 可以指定已有映射,输入哈希和匹配器版本
|
函数和数据映射。`--fileset-map FILE` 可以指定已有映射,输入哈希和匹配器版本
|
||||||
必须一致。输出保留所选根组件及其全部依赖。
|
必须一致。输出保留所选根组件及其全部依赖。
|
||||||
@@ -14,6 +35,12 @@
|
|||||||
HMAC。集合根头使用原生内核的链接地址,普通预链接的 `--slide` 不用于 fileset。
|
HMAC。集合根头使用原生内核的链接地址,普通预链接的 `--slide` 不用于 fileset。
|
||||||
原生内核输入文件保持不变。
|
原生内核输入文件保持不变。
|
||||||
|
|
||||||
|
原生适配器通过 `BTI c; B` 跳板调用保留的 kext 函数,固定目标由 `N_ABS`
|
||||||
|
符号 `<stub>.original` 声明。这类跳转没有链接器分段重定位记录,因此输出工具
|
||||||
|
在布局完成后重新计算跳转,并核验目标位于保留组件的可执行范围内。报告中的
|
||||||
|
`native_original_branches` 记录每条绑定。构建脚本不得预先减去缓存的原生内核
|
||||||
|
位移;GOT 中的绝对数据指针保持固定值。
|
||||||
|
|
||||||
默认遇到缺失映射就停止。`--allow-missing` 可以生成实验映像,JSON 报告按
|
默认遇到缺失映射就停止。`--allow-missing` 可以生成实验映像,JSON 报告按
|
||||||
kext 列出所有未解析目标。结构验证通过后,仍需要补全映射并进行硬件验证,
|
kext 列出所有未解析目标。结构验证通过后,仍需要补全映射并进行硬件验证,
|
||||||
才能确认启动兼容性。
|
才能确认启动兼容性。
|
||||||
@@ -39,3 +66,8 @@ XnuPrelink 将构建的 XNU 内核绑定到所选原始 kext,并写出预链
|
|||||||
```
|
```
|
||||||
|
|
||||||
[组件目录](/zh/reference/components)
|
[组件目录](/zh/reference/components)
|
||||||
|
|
||||||
|
24A446 打包配置还会按原始、未滑动的导入地址检查回归清单。新增未解析数据或
|
||||||
|
stub 调用会在替换上次内核产物前报错。完整重编曾丢失两个依赖顺序的匹配:
|
||||||
|
`IOCircularDataQueueCopyLatest` 和 `get_aiotask`;现在已根据源码和反汇编,
|
||||||
|
用原函数摘要及 native 符号固定其身份。
|
||||||
@@ -106,3 +106,32 @@ remove = [ ... ]
|
|||||||
4. 把运行记录到 `porting.toml` 的 `packaging` 区域。
|
4. 把运行记录到 `porting.toml` 的 `packaging` 区域。
|
||||||
|
|
||||||
[构建与启动](/zh/building)、[设备仓库](/zh/porting/device-repository)
|
[构建与启动](/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`,新增缺口也会使打包失败。这不代表剩余实验路径已验证,仍需上机启动测试。
|
||||||
+1
-1
Submodule kits/XnuPrelink updated: b74ed7c74a...b1888c1245.
@@ -31,6 +31,7 @@
|
|||||||
"kernel" = "build/xnu/ios-obj/DEVELOPMENT_ARM64_BCM2837/mach.development.bcm2837.unstripped.noctf"
|
"kernel" = "build/xnu/ios-obj/DEVELOPMENT_ARM64_BCM2837/mach.development.bcm2837.unstripped.noctf"
|
||||||
"kernelcache_sha256" = "38e6d9fa5f789b8228f4617ef4c6d2a3fd8ab564abdc9b62920f1db53fc1780a"
|
"kernelcache_sha256" = "38e6d9fa5f789b8228f4617ef4c6d2a3fd8ab564abdc9b62920f1db53fc1780a"
|
||||||
"slide" = "-0x1000000"
|
"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.
|
# The AMFI wrapper that calls _initializeAppleMobileFileIntegrity and returns 0; the kernel patch calls it early.
|
||||||
"early_security_init" = "com.apple.driver.AppleMobileFileIntegrity=0xfffffe00092551ac"
|
"early_security_init" = "com.apple.driver.AppleMobileFileIntegrity=0xfffffe00092551ac"
|
||||||
# Bring-up: match against the symbol-bearing RELEASE build of the same XNU source
|
# Bring-up: match against the symbol-bearing RELEASE build of the same XNU source
|
||||||
|
|||||||
@@ -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
|
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
|
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."""
|
owner trust module (make-owner-trust-bundle.py); neither changes the original trust modules, AMFI or any Apple signature."""
|
||||||
|
import argparse
|
||||||
import json
|
import json
|
||||||
import plistlib
|
import plistlib
|
||||||
import re
|
import re
|
||||||
@@ -15,7 +16,11 @@ from pathlib import Path
|
|||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
# --target ios27: the arm64e build for iPadOS 27 (24A446), whose render server is an arm64e process.
|
# --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')
|
OUT = ROOT / ('build/adreno-metal-plugin27' if IOS27 else 'build/adreno-metal-plugin')
|
||||||
bundle = OUT / 'NativeAdrenoMetal.bundle'
|
bundle = OUT / 'NativeAdrenoMetal.bundle'
|
||||||
if OUT.exists():
|
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'))
|
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)
|
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).
|
# 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'
|
BRIDGE_TARGET = 'ios27' if IOS27 else 'ios'
|
||||||
subprocess.run(['python3', ROOT / 'scripts/build-adreno-metal-bridge.py', '--target', BRIDGE_TARGET], check=True)
|
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'
|
BRIDGE = ROOT / 'build/adreno-metal-bridge' / BRIDGE_TARGET / 'libadreno_metal_bridge.a'
|
||||||
|
|||||||
@@ -92,11 +92,9 @@ sdk = run(['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'], text=True).strip()
|
|||||||
binding_objects = [OUT / 'original-bindings.o']
|
binding_objects = [OUT / 'original-bindings.o']
|
||||||
if FILESET:
|
if FILESET:
|
||||||
# As for the Legacy display (build-gop-legacy-display-27.py): original functions are reached through owned stubs in kernel text
|
# 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
|
# (signed and moved like native code). XnuPrelink retargets their branches after placing native text.
|
||||||
# native kernel (native-slide); the metaclass is data, reached through the GOT at its plain address.
|
# The metaclass is data, reached through the GOT at its fixed collection address.
|
||||||
slide = int(plan['collection_slide'], 16)
|
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', ''
|
text, addresses = '.text\n.p2align 2\n', ''
|
||||||
for n, a in sorted(bindings.items()):
|
for n, a in sorted(bindings.items()):
|
||||||
if n not in undefined:
|
if n not in undefined:
|
||||||
@@ -104,7 +102,7 @@ if FILESET:
|
|||||||
if n.endswith('gMetaClassE'):
|
if n.endswith('gMetaClassE'):
|
||||||
addresses += f'.globl {n}\n.set {n}, {a + slide:#x}\n'
|
addresses += f'.globl {n}\n.set {n}, {a + slide:#x}\n'
|
||||||
else:
|
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'
|
text += f'.globl {n}\n{n}:\n bti c\n b {n}.original\n'
|
||||||
asm.write_text(text)
|
asm.write_text(text)
|
||||||
(OUT / 'original-addresses.S').write_text(addresses)
|
(OUT / 'original-addresses.S').write_text(addresses)
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -10,101 +10,170 @@ import fnmatch
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
repo = Path(__file__).resolve().parents[1]
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
def file_hash(path):
|
||||||
parser.add_argument('--tree', required=True, type=Path, help='prepared XNU build tree of the OS profile')
|
with path.open('rb') as stream:
|
||||||
parser.add_argument('--lto', action='store_true', help='enable the RELEASE default link-time optimization')
|
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
||||||
options = parser.parse_args()
|
|
||||||
mode = 'lto' if options.lto else 'snapshot'
|
|
||||||
root = options.tree.resolve()
|
def tree_hash(path):
|
||||||
output = root / 'build/xnu/fileset-reference'
|
"""Hash inputs by content; follow SDK header-directory links as the compiler does."""
|
||||||
final = output / 'mach.release.bcm2837.unstripped.noctf'
|
if not path.is_dir():
|
||||||
if final.is_file():
|
raise FileNotFoundError(path)
|
||||||
# An earlier run of this step left the comparison kernel in place.
|
digest = hashlib.sha256()
|
||||||
print(f'reference present: {final}')
|
for directory, dirs, files in os.walk(path, followlinks=True):
|
||||||
raise SystemExit(0)
|
dirs[:] = sorted(d for d in dirs if d != '.git')
|
||||||
output.mkdir(parents=True, exist_ok=True)
|
for name in sorted(files):
|
||||||
source = output / 'source'
|
item = Path(directory) / name
|
||||||
if not source.exists():
|
digest.update(str(item.relative_to(path)).encode() + b'\0')
|
||||||
shutil.copytree(root/'.cache/xnu-work', source, symlinks=True,
|
digest.update(file_hash(item).encode() + b'\0')
|
||||||
ignore=shutil.ignore_patterns('.git'))
|
return digest.hexdigest()
|
||||||
# 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'
|
def reusable(final, record, inputs):
|
||||||
text = sleh.read_text()
|
try:
|
||||||
misplaced = '\n\tboot_progress_user_breakpoint(state, esr);\n#if __has_feature(ptrauth_calls)'
|
saved = json.loads(record.read_text())
|
||||||
if misplaced in text:
|
return saved['inputs'] == inputs and saved['sha256'] == file_hash(final)
|
||||||
sleh.write_text(text.replace(misplaced, '\n#if __has_feature(ptrauth_calls)', 1))
|
except (OSError, ValueError, KeyError):
|
||||||
manifest = {str(p.relative_to(source)): hashlib.sha256(p.read_bytes()).hexdigest()
|
return False
|
||||||
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'
|
def linked_objects(directory, lto):
|
||||||
archives = [root/'build/corecrypto-2023/libcorecrypto_kernel_noasm-namespaced.a']
|
if lto:
|
||||||
forced = [root/f'build/{folder}/{name}' for folder,name in (
|
# XNU splits the final link into non-LTO objects and the code generated
|
||||||
('libpthread-kernel','libpthread_kernel.a'),('armdt-platform','libArmDTPlatform.a'),('armcpu','libArmCpu.a'))]
|
# by its intermediate LTO link. There is no link.filelist in this mode.
|
||||||
assert all(p.is_file() for p in archives+forced)
|
objects = (directory / 'nonltolink.filelist').read_text().splitlines()
|
||||||
env = dict(os.environ, KERNEL_SDKROOT=str(sdk),
|
generated = directory / 'justlto.o'
|
||||||
XNU_AVAILABILITY_PL=str(sdk/'usr/local/libexec/availability.pl'))
|
objects += [str(p) for p in sorted(generated.glob('*.o'))] if generated.is_dir() else [str(generated)]
|
||||||
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'
|
else:
|
||||||
args = ['make','-C',str(source),'build','SDKROOT=iphoneos',
|
objects = (directory / 'link.filelist').read_text().splitlines()
|
||||||
'ARCH_CONFIGS=ARM64','MACHINE_CONFIGS=BCM2837','KERNEL_CONFIGS=RELEASE',
|
objects += [str(directory / name) for name in
|
||||||
'ARCH_STRING_FOR_CURRENT_MACHINE_CONFIG=arm64e','LARGE_MEMORY_DEFINE=-UARM_LARGE_MEMORY',
|
('version.o', 'lastkerneldataconst.o', 'lastkernelconstructor.o')
|
||||||
'ARM64_LINK_ADDR=0xfffffff007004000','ARM64_PLKSEG_ADDR=0xfffffff004004000',
|
if (directory / name).is_file()]
|
||||||
f'DEPLOYMENT_TARGET_FLAGS=-miphoneos-version-min={os.environ.get("UNIXNU_IOS_VERSION", "26.0")} -DXNU_TARGET_OS_IOS',
|
return [str(Path(name) if Path(name).is_absolute() else directory / name)
|
||||||
f'DEPLOYMENT_LINKER_FLAGS=-Wl,-ios_version_min,{os.environ.get("UNIXNU_IOS_VERSION", "26.0")}',
|
for name in objects if name.strip()]
|
||||||
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',
|
def main():
|
||||||
'LDFLAGS_KERNEL_LTO=-miphoneos-version-min=26.0 -Wl,-mllvm,-inline-threshold=100',
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
f'CFLAGS_EXTRA={flags}',
|
parser.add_argument('--tree', required=True, type=Path, help='prepared XNU build tree of the OS profile')
|
||||||
'copyio.o_CFLAGS_ADD=-march=armv8.1-a','pcb.o_CFLAGS_ADD=-march=armv8.1-a',
|
parser.add_argument('--lto', action='store_true', help='enable the RELEASE default link-time optimization')
|
||||||
'LD_KERNEL_ARCHIVES='+' '.join([f'-L{sdk}/usr/local/lib/kernel','-lfirehose_kernel',
|
options = parser.parse_args()
|
||||||
*map(str,archives),*(f'-Wl,-force_load,{p}' for p in forced)]),
|
mode = 'lto' if options.lto else 'snapshot'
|
||||||
'XNU_EXTERNAL_KERNEL_ARCHIVES='+' '.join(map(str,archives+forced)),
|
root = options.tree.resolve()
|
||||||
f'OBJROOT={output}/{mode}-obj',f'SYMROOT={output}/{mode}-sym',f'DSTROOT={output}/{mode}-dst']
|
build_root = root / 'build/xnu/fileset-reference'
|
||||||
(output/f'{mode}-command.json').write_text(json.dumps(args,indent=2)+'\n')
|
final = build_root / 'mach.release.bcm2837.unstripped.noctf'
|
||||||
with (output/f'{mode}-build.log').open('w') as log:
|
cache_record = final.with_suffix('.json')
|
||||||
result = subprocess.run(args,cwd=root,env=env,stdout=log,stderr=subprocess.STDOUT)
|
sdk = root / '.cache/fakeroot'
|
||||||
print('RELEASE reference exit',result.returncode,'log',output/f'{mode}-build.log',flush=True)
|
archives = [root/'build/corecrypto-2023/libcorecrypto_kernel_noasm-namespaced.a']
|
||||||
# The bring-up patch calls native hooks from the kernel core behind weak_import declarations; on arm64e
|
forced = [root/f'build/{folder}/{name}' for folder,name in (
|
||||||
# the authenticated pointer of an undefined weak symbol fails the link. The private copy stubs them: this
|
('libpthread-kernel','libpthread_kernel.a'),('armdt-platform','libArmDTPlatform.a'),('armcpu','libArmCpu.a'))]
|
||||||
# comparison kernel never runs, the prelink step reads its layout.
|
inputs = {
|
||||||
build_log = (output / f'{mode}-build.log').read_text()
|
'mode': mode,
|
||||||
stub_names = sorted(set(re.findall(r'^\s+"(_(?:native_[a-z0-9_]+|boot_log_rounds))"', build_log, re.M)))
|
'script': file_hash(Path(__file__)),
|
||||||
if stub_names:
|
'source': tree_hash(root / '.cache/xnu-work'),
|
||||||
(output / 'native-stubs.c').write_text(''.join(f'long {name[1:]};\n' for name in stub_names)) # the linker names carry the leading underscore
|
'sdk': tree_hash(sdk),
|
||||||
iphoneos = subprocess.check_output(['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'], text=True).strip()
|
'archives': {str(p.relative_to(root)): file_hash(p) for p in archives + forced},
|
||||||
subprocess.run(['xcrun', 'clang', '-arch', 'arm64e', '-mkernel', '-isysroot', iphoneos, '-c',
|
'versions': {name: os.environ.get(name, default) for name, default in (
|
||||||
'-o', str(output / 'native-stubs.o'), str(output / 'native-stubs.c')], check=True)
|
('UNIXNU_IOS_VERSION', '26.0'), ('UNIXNU_XNU_DARWIN', '25.5.0'),
|
||||||
at = next(i for i, a in enumerate(args) if a.startswith('LD_KERNEL_ARCHIVES='))
|
('UNIXNU_XNU_VERSION', '12377.121.6'))},
|
||||||
args[at] += f' -Wl,-force_load,{output}/native-stubs.o'
|
'compiler': subprocess.check_output(['xcrun', 'clang', '--version'], text=True),
|
||||||
# Forced exports the built objects cannot provide: remove them from this private copy and link again
|
'environment': {name: value for name, value in os.environ.items()
|
||||||
# (the symbols are absent from the comparison kernel either way; the first link can stop exactly there,
|
if name.startswith(('XNU_', 'RC_', 'CCC_')) or name in ('CC', 'CXX', 'LD')},
|
||||||
# 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()
|
if reusable(final, cache_record, inputs):
|
||||||
names = set()
|
print(f'reference inputs and digest match ({mode}): {final}')
|
||||||
for i in range(0, len(objects), 100):
|
return
|
||||||
listed = subprocess.run(['xcrun', 'nm', '-gUj', *objects[i:i + 100]], capture_output=True, text=True, check=True)
|
# Separate workspaces prevent non-LTO objects and stale patched sources from
|
||||||
names.update(symbol for symbol in listed.stdout.splitlines() if symbol.startswith('_') and ' ' not in symbol)
|
# being reused after a mode, source, SDK, archive or compiler change.
|
||||||
removed = []
|
key = hashlib.sha256(json.dumps(inputs, sort_keys=True).encode()).hexdigest()
|
||||||
for filename in ('Private.exports', 'Private.arm64.exports'):
|
output = build_root / f'{mode}-{key[:16]}'
|
||||||
path = output / 'source/config' / filename
|
output.mkdir(parents=True, exist_ok=True)
|
||||||
lines = path.read_text().splitlines(keepends=True)
|
(output / 'inputs.json').write_text(json.dumps(inputs, indent=2) + '\n')
|
||||||
rejected = {line.strip() for line in lines
|
source = output / 'source'
|
||||||
if line.strip().startswith('_') and ':' not in line and not any(fnmatch.fnmatchcase(n, line.strip()) for n in names)}
|
if not source.exists():
|
||||||
removed.extend(sorted(rejected))
|
shutil.copytree(root/'.cache/xnu-work', source, symlinks=True,
|
||||||
path.write_text(''.join(line for line in lines if line.strip() not in rejected))
|
ignore=shutil.ignore_patterns('.git'))
|
||||||
import json as _json
|
# The shared tree currently has this observation hook at file scope. The
|
||||||
record = output / 'omitted-unavailable-forced-exports.json'
|
# reference build disables observation hooks; repair only its private copy.
|
||||||
previous = _json.loads(record.read_text()) if record.exists() else []
|
sleh = source / 'osfmk/arm64/sleh.c'
|
||||||
record.write_text(_json.dumps(sorted(set(previous + removed)), indent=2) + chr(10))
|
text = sleh.read_text()
|
||||||
print('removed', len(removed), 'unavailable forced exports; linking again')
|
misplaced = '\n\tboot_progress_user_breakpoint(state, esr);\n#if __has_feature(ptrauth_calls)'
|
||||||
with (output / f'{mode}-build.log').open('a') as log:
|
if misplaced in text:
|
||||||
result = subprocess.run(args, cwd=root, env=env, stdout=log, stderr=subprocess.STDOUT)
|
sleh.write_text(text.replace(misplaced, '\n#if __has_feature(ptrauth_calls)', 1))
|
||||||
print('RELEASE reference exit', result.returncode, 'log', output / f'{mode}-build.log', flush=True)
|
manifest = {str(p.relative_to(source)): hashlib.sha256(p.read_bytes()).hexdigest()
|
||||||
if result.returncode:
|
for p in source.rglob('*') if p.is_file()}
|
||||||
raise SystemExit(result.returncode)
|
(output/'source-sha256.json').write_text(json.dumps(manifest,indent=2)+'\n')
|
||||||
import shutil as _shutil
|
env = dict(os.environ, KERNEL_SDKROOT=str(sdk),
|
||||||
built = output / f'{mode}-obj/RELEASE_ARM64_BCM2837/mach.release.bcm2837.unstripped'
|
XNU_AVAILABILITY_PL=str(sdk/'usr/local/libexec/availability.pl'))
|
||||||
_shutil.copyfile(built, final)
|
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'
|
||||||
print('reference', final)
|
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()
|
||||||
@@ -90,6 +90,23 @@ def main():
|
|||||||
except Error:
|
except Error:
|
||||||
pass
|
pass
|
||||||
expect(payload.check_args(['-v', 'a=1', 'b=x,y']) == '-v a=1 b=x,y', 'kernel arguments')
|
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']):
|
for bad in (['a b'], ['x' * 1100], ['a;b']):
|
||||||
try:
|
try:
|
||||||
payload.check_args(bad)
|
payload.check_args(bad)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -5,12 +5,12 @@ import platform
|
|||||||
import shutil
|
import shutil
|
||||||
import sys
|
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 .config import LOCAL_KINDS, Context, parse_config
|
||||||
from .util import Error, expand, git, log
|
from .util import Error, expand, git, log
|
||||||
|
|
||||||
STEPS = ('kernel', 'package', 'firmware', 'payload')
|
STEPS = ('kernel', 'package', 'firmware', 'payload')
|
||||||
OPTIONAL_STEPS = ('userdata',) # large; built only when named
|
OPTIONAL_STEPS = ('userdata', 'runtime')
|
||||||
|
|
||||||
|
|
||||||
def common():
|
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('--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('--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('--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('--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('--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')
|
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:
|
if 'firmware' in steps:
|
||||||
need_uefi(ctx) # fail before fetching anything
|
need_uefi(ctx) # fail before fetching anything
|
||||||
modules = prepare(ctx, steps + (['kernel'] if 'prelink' in steps else []))
|
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:
|
if 'kernel' in steps:
|
||||||
kernel.build(ctx, modules)
|
kernel.build(ctx, modules)
|
||||||
kernel.prelink(ctx)
|
kernel.prelink(ctx)
|
||||||
|
|||||||
@@ -236,6 +236,21 @@ def stage_native(kernel, output):
|
|||||||
return 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):
|
def prelink(ctx):
|
||||||
"""Produce out/<device>/kernel/kernel.macho from the XNU build and the original kernelcache."""
|
"""Produce out/<device>/kernel/kernel.macho from the XNU build and the original kernelcache."""
|
||||||
spec = ctx.board.get('kernel', {}).get('prelink')
|
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')
|
run(command + ['--output', candidate], cwd=ctx.tree, log_file=ctx.logs / 'prelink.log')
|
||||||
if not candidate.is_file():
|
if not candidate.is_file():
|
||||||
raise Error('prelink completed without writing a kernel candidate')
|
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
|
success = True
|
||||||
finally:
|
finally:
|
||||||
if candidate_map.is_file():
|
if candidate_map.is_file():
|
||||||
|
|||||||
@@ -38,6 +38,21 @@ def check_args(arguments):
|
|||||||
return line
|
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):
|
def check_native_root(kernel, arguments, vmm_present=False):
|
||||||
"""Refuse the known bare-metal AMFI panic before the volume is written.
|
"""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:
|
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)')
|
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
|
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():
|
if data and not data.is_file():
|
||||||
raise Error(f'Data ramdisk not found: {data}')
|
raise Error(f'Data ramdisk not found: {data}')
|
||||||
if getattr(args, 'skip_setup', False):
|
if getattr(args, 'skip_setup', False):
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ def write_manifest(ctx, modules, patch_order, files):
|
|||||||
'revisions': revisions, 'patches': [str(path) for path in patch_order],
|
'revisions': revisions, 'patches': [str(path) for path in patch_order],
|
||||||
'kernel_command_line_bytes': len(command_line(ctx)),
|
'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()}}
|
'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 = ctx.work / 'package.manifest.json'
|
||||||
target.write_text(json.dumps(manifest, indent=2, sort_keys=True) + '\n')
|
target.write_text(json.dumps(manifest, indent=2, sort_keys=True) + '\n')
|
||||||
return target
|
return target
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in new issue
Block a user