feat(scripts): iPadOS 27 kernel, driver, GPU addon and seed build steps

Source-driver and source-storage builds, the arm64e PAC rewrite for the Rust bridge, the IOAccelerator base ABI of 24A446, the bridge with mimalloc and link-time optimisation, the plugin with the prime list, addon options for a prime list, a pipeline pack and cache export, wallpaper overlay and seed changes, and payload, kernel and userdata steps for the new profile.
This commit is contained in:
rpone authored and Sharky Wang committed 2026-10-28 09:20:55 +08:00
1 parent 7b0219043e
commit ab11a8cb68
26 files changed
+1198 -151

No files matched your search

+2 -14
View File
@@ -1,12 +1,10 @@
#!/usr/bin/env python3
"""Stage the owner-built Adreno Metal plugin into a copy of a packed Data seed.
add-gpu-plugin-data.py [--publish] [--clients NAME,...] BASE.dbr OUTPUT.dbr
add-gpu-plugin-data.py [--publish] BASE.dbr OUTPUT.dbr
With --publish the seed also carries /private/var/ngpu/addons/NativeAdrenoMetal.publish: the plugin then returns the native Metal device to the original
discovery in backboardd (NativeAdrenoMetalPlugin.mm), so the render server composites through it. Without the file the plugin only runs its tests.
With --clients the seed also carries /private/var/ngpu/addons/NativeAdrenoMetal.clients: the processes besides backboardd that get their own native
Metal device ("*" = every process); any other process keeps the software renderer.
Adds /private/var/ngpu/addons/NativeAdrenoMetal.bundle, /private/var/ngpu/bin/adrenoprobed and /private/var/ngpu/daemons/*.plist (from build/adreno-metal-plugin, see build-adreno-metal-plugin.py). The
kernel's boot-time addon mount (sheng-gpu-addon=1) presents that directory as a read-only union view over
@@ -31,13 +29,6 @@ BUNDLE = ROOT / 'build/adreno-metal-plugin/NativeAdrenoMetal.bundle'
def main():
args = [a for a in sys.argv[1:] if a != '--publish']
publish = len(args) != len(sys.argv) - 1
clients = None
if '--clients' in args:
at = args.index('--clients')
clients = [name for name in args[at + 1].split(',') if name]
del args[at:at + 2]
if not clients or any(not all(c.isalnum() or c in '._-*' for c in name) for name in clients):
sys.exit('--clients takes process names separated by commas')
if len(args) != 2:
sys.exit(__doc__)
base, output = Path(args[0]).resolve(), Path(args[1]).resolve()
@@ -70,9 +61,6 @@ def main():
for directory in (mount / 'ngpu', mount / 'ngpu/addons'):
os.chmod(directory, 0o755)
shutil.copytree(BUNDLE, target, symlinks=False)
if clients:
(mount / 'ngpu/addons/NativeAdrenoMetal.clients').write_text('\n'.join(clients) + '\n')
os.chmod(mount / 'ngpu/addons/NativeAdrenoMetal.clients', 0o644)
marker = mount / 'ngpu/addons/NativeAdrenoMetal.publish'
if publish:
marker.write_text('1\n')
@@ -111,7 +99,7 @@ def main():
if raw.read_bytes()[32:36] != b'NXSB':
sys.exit('The edited image is no APFS container any more')
metadata = packed_ramdisk.pack(raw, output)
report = dict(base_sha256=packed_ramdisk.inspect(base)[0]['sha256'], output=metadata, publish=publish, clients=clients,
report = dict(base_sha256=packed_ramdisk.inspect(base)[0]['sha256'], output=metadata, publish=publish,
plugin=json.loads((ROOT / 'build/adreno-metal-plugin/trust.json').read_text()))
output.with_suffix('.json').write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps(report, indent=2))
+23 -7
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Build the native Metal bridge (drivers/AdrenoMetal/bridge/NativeMetalBridge.rs): the pinned metal2vulkan runtime behind a C ABI, as a static library.
--target ios (default) cross-builds for arm64-apple-ios17.0, --target host for the macOS host simulation. The runtime's own example library
--target ios (default) cross-builds for arm64-apple-ios17.0, --target ios27 for arm64e-apple-ios27.0 (nightly, std built from source), --target host for the macOS host simulation. The runtime's own example library
(runtime_examples.ll) and its function-constant fixture are assembled to AIR with llvm-as and embedded for the self-tests. Only generated build files hold local paths; nothing here
is committed. Output: build/adreno-metal-bridge/<target>/libadreno_metal_bridge.a"""
import argparse
@@ -16,7 +16,7 @@ ROOT = Path(__file__).resolve().parents[1]
PIN = json.loads((ROOT / 'scripts/data/gpu-metal-vulkan-inputs.json').read_text())['translator']['commit']
SOURCE = ROOT / '.cache/gpu-reference/metal2vulkan'
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--target', choices=['ios', 'host'], default='ios')
parser.add_argument('--target', choices=['ios', 'ios27', 'host'], default='ios')
parser.add_argument('--manifest-only', action='store_true',
help='write build/adreno-metal-bridge/Cargo.toml (patched source, fixtures) and stop before cargo, for `cargo fetch`')
args = parser.parse_args()
@@ -30,7 +30,12 @@ PATCHES = [ROOT / 'drivers/AdrenoMetal/bridge/patches' / name for name in [
# The rest stack in order, each on the source the ones before it leave.
'0009-keep-final-pass.patch', '0010-fewer-allocations-per-draw.patch', '0011-keep-draw-commands.patch',
'0012-keep-draw-lists.patch', '0013-skip-unchanged-rebinds.patch', '0014-map-tracker-keys.patch',
'0015-two-plane-shadow-as-memory-lies.patch', '0016-tile-stores-within-rows.patch']]
'0015-two-plane-shadow-as-memory-lies.patch', '0016-tile-stores-within-rows.patch',
# From the DroidVM board's translator patches (its 0006 to 0010), checked against this tree: a cache directory a sandboxed process
# may not write, the Astronomy wallpaper's extended-range sRGB blends, llvm.bitreverse, pooled blocks that start at 4 MiB and grow
# (the render server alone held 1 GiB of wired memory), and a patch function with no control points.
'0017-optional-shader-cache-directory.patch', '0018-srgb-extended-range-blend-rounded-model.patch', '0019-llvm-bitreverse.patch',
'0020-growing-memory-blocks.patch', '0021-patch-with-no-control-points.patch']]
def git_tree(*steps):
@@ -63,6 +68,9 @@ air = out / 'runtime_examples.air'
subprocess.run([llvm_as, '-o', air, SOURCE / 'crates/metal2vulkan-validation/fixtures/public/examples/runtime_examples.ll'], check=True)
fc_air = out / 'function_constant.air'
subprocess.run([llvm_as, '-o', fc_air, SOURCE / 'crates/metal2vulkan-validation/fixtures/public/kernel_function_constant_gate_fold.ll'], check=True)
# The GPU scanout's half-float conversion kernel (GpuScanoutPresenter.h), hand-written AIR.
scanout_air = out / 'scanout_convert.air'
subprocess.run([llvm_as, '-o', scanout_air, ROOT / 'drivers/AdrenoMetal/shaders/scanout_convert.ll'], check=True)
manifest = out / 'Cargo.toml'
manifest.write_text('\n'.join([
'[package]', 'name = "adreno-metal-bridge"', 'version = "0.1.0"', 'edition = "2024"', 'rust-version = "1.98"', '[workspace]',
@@ -74,20 +82,28 @@ manifest.write_text('\n'.join([
# The runtime hashes every shader-cache key and entry; without asm, sha2 0.10 on aarch64 is the portable software rounds, not the
# ARMv8 SHA-256 instructions every target CPU has (cpufeatures answers sha2 true on Apple platforms).
'sha2 = { version = "0.10", features = ["asm"] }',
'[profile.release]', 'panic = "unwind"', 'debug = 1', '',
# The bridge's global allocator (NativeMetalBridge.rs).
'mimalloc = { version = "=0.1.43", default-features = false }',
# The render server's display thread spends most of a frame in the runtime, much of it in small functions across the
# translator's crates: one optimisation unit with link-time optimisation lets them inline across crate boundaries.
'[profile.release]', 'panic = "unwind"', 'debug = 1', 'lto = "fat"', 'codegen-units = 1', '',
]))
if args.manifest_only:
print(manifest)
raise SystemExit(0)
triple = 'aarch64-apple-ios' if args.target == 'ios' else 'aarch64-apple-darwin'
triple = {'ios': 'aarch64-apple-ios', 'ios27': 'arm64e-apple-ios', 'host': 'aarch64-apple-darwin'}[args.target]
# arm64e has no prebuilt std: it is built from rust-src (RUSTC_BOOTSTRAP for -Z); the iPadOS 27 processes that load the plugin are arm64e.
cargo = ['cargo', '+1.98.0'] + (['-Z', 'build-std=std,panic_unwind'] if args.target == 'ios27' else [])
# The translator source the runtime is built from (its pin and recorded patches), which keys the shader cache: shader priming keeps what it
# finished while this stays the same (PrimeShaders.h).
runtime_id = hashlib.sha256((PIN + '\n' + states[-1]).encode()).hexdigest()[:32]
env = dict(os.environ, NMB_RUNTIME_ID=runtime_id, NMB_FIXTURE_AIR=str(air), NMB_FC_AIR=str(fc_air), IPHONEOS_DEPLOYMENT_TARGET='17.0', MACOSX_DEPLOYMENT_TARGET='14.0')
env = dict(os.environ, NMB_RUNTIME_ID=runtime_id, NMB_FIXTURE_AIR=str(air), NMB_FC_AIR=str(fc_air), NMB_SCANOUT_AIR=str(scanout_air), IPHONEOS_DEPLOYMENT_TARGET='27.0' if args.target == 'ios27' else '17.0', MACOSX_DEPLOYMENT_TARGET='14.0')
if args.target == 'ios27':
env['RUSTC_BOOTSTRAP'] = '1'
target_dir = ROOT / 'build/m2v-target'
log = out / ('build-%s.log' % args.target)
with log.open('w') as stream:
result = subprocess.run(['cargo', '+1.98.0', 'build', '--release', '--manifest-path', manifest, '--target', triple,
result = subprocess.run(cargo + ['build', '--release', '--manifest-path', manifest, '--target', triple,
'--target-dir', target_dir], env=env, stdout=stream, stderr=subprocess.STDOUT, timeout=1800)
if result.returncode:
raise SystemExit(log.read_text()[-6000:])
+30 -9
View File
@@ -10,26 +10,44 @@ import plistlib
import re
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / 'build/adreno-metal-plugin'
# --target ios27: the arm64e build for iPadOS 27 (24A446), whose render server is an arm64e process.
IOS27 = sys.argv[1:] == ['--target', 'ios27']
OUT = ROOT / ('build/adreno-metal-plugin27' if IOS27 else 'build/adreno-metal-plugin')
bundle = OUT / 'NativeAdrenoMetal.bundle'
if OUT.exists():
shutil.rmtree(OUT)
bundle.mkdir(parents=True)
sdk = subprocess.check_output(['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'], text=True).strip()
base = ['xcrun', 'clang', '-target', 'arm64-apple-ios17.0', '-isysroot', sdk, '-O2', '-g', '-Wall', '-Wextra', '-Werror',
base = ['xcrun', 'clang', '-target', 'arm64e-apple-ios27.0' if IOS27 else 'arm64-apple-ios17.0', '-isysroot', sdk, '-O2', '-g', '-Wall', '-Wextra', '-Werror',
'-fstack-protector-strong']
VULKAN_INCLUDE = ROOT / '.cache/mesa-work/include'
TURNIP = ROOT / 'build/mesa-tu-ios/src/freedreno/vulkan/libvulkan_freedreno.dylib'
TURNIP = ROOT / ('build/mesa-tu-ios27' if IOS27 else 'build/mesa-tu-ios') / 'src/freedreno/vulkan/libvulkan_freedreno.dylib'
if not TURNIP.is_file():
raise SystemExit('Build Turnip first: python3 scripts/build-turnip-darwin.py --target 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)
# 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)
subprocess.run(['python3', ROOT / 'scripts/build-adreno-metal-bridge.py', '--target', 'ios'], check=True)
BRIDGE = ROOT / 'build/adreno-metal-bridge/ios/libadreno_metal_bridge.a'
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'
# arm64e: the Rust objects' imports that take a callback go through the signing wrappers of RustPacBoundary.c, and their indirect branches
# are routed through its stripping trampolines after the link (scripts/rust-pac-rewrite.py).
link_extra, PAC_RENAMES = [], {'_pthread_create': '_nmb_pac_pthread_create', '__tlv_atexit': '_nmb_pac_tlv_atexit',
'_atexit': '_nmb_pac_atexit', '__dyld_register_func_for_add_image': '_nmb_pac_dyld_register_func_for_add_image',
'__Unwind_Backtrace': '_nmb_pac_Unwind_Backtrace', '_dlsym': '_nmb_pac_dlsym'}
if IOS27:
objcopy = next(p for p in ['/opt/homebrew/opt/llvm/bin/llvm-objcopy', shutil.which('llvm-objcopy') or ''] if p and Path(p).exists())
pac_bridge = OUT / 'libadreno_metal_bridge_pac.a'
subprocess.run([objcopy, *[f'--redefine-sym={a}={b}' for a, b in PAC_RENAMES.items()], BRIDGE, pac_bridge], check=True)
BRIDGE = pac_bridge
boundary = OUT / 'RustPacBoundary.o'
subprocess.run([*base, '-std=c11', '-I', VULKAN_INCLUDE, '-c', ROOT / 'drivers/AdrenoMetal/bridge/RustPacBoundary.c', '-o', boundary],
check=True)
link_extra = [boundary, '-Wl,-map,' + str(OUT / 'NativeAdrenoMetal.map')]
pm4 = OUT / 'AdrenoPM4.o'
subprocess.run([*base, '-std=c11', '-c', ROOT / 'kits/AdrenoPM4/AdrenoPM4.c', '-o', pm4], check=True)
exe = bundle / 'NativeAdrenoMetal'
@@ -41,8 +59,11 @@ subprocess.run([*(['xcrun', 'clang++'] + base[2:]), '-std=c++17', '-fobjc-arc',
'-I', ROOT / 'drivers/Adreno740', '-I', ROOT / 'kits/AdrenoPM4', '-I', VULKAN_INCLUDE, '-I', ROOT / 'build/turnip-smoke',
'-x', 'objective-c++', ROOT / 'drivers/AdrenoMetal/NativeAdrenoMetalPlugin.mm',
ROOT / 'drivers/AdrenoMetal/NativeBridgeMetalFrontend.mm', '-x', 'none', pm4, BRIDGE,
'-Wl,-dead_strip', *prime_section, '-lobjc', '-framework', 'Foundation', '-framework', 'Metal', '-framework', 'IOSurface', '-o', exe],
*link_extra, '-Wl,-dead_strip', *prime_section, '-lobjc', '-framework', 'Foundation', '-framework', 'Metal', '-framework', 'IOSurface', '-o', exe],
check=True)
if IOS27:
subprocess.run(['python3', ROOT / 'scripts/rust-pac-rewrite.py', exe, OUT / 'NativeAdrenoMetal.map', 'libadreno_metal_bridge_pac.a'],
check=True)
subprocess.run(['strip', '-x', exe], check=True)
# The linker writes the debug symbols of the Rust bridge next to the executable; keep them beside the bundle, not in the image.
symbols = OUT / 'NativeAdrenoMetal.dSYM'
@@ -61,7 +82,7 @@ assert re.search(r'^Page size=4096$', turnip_detail, re.M)
(bundle / 'Info.plist').write_bytes(plistlib.dumps(dict(
CFBundleIdentifier='org.blu-sharky.NativeAdrenoMetal', CFBundleExecutable='NativeAdrenoMetal', CFBundlePackageType='BNDL',
CFBundleVersion='1', CFBundleShortVersionString='1', NSPrincipalClass='NativeAdrenoMetalDevice',
MinimumOSVersion='17.0'), fmt=plistlib.FMT_XML))
MinimumOSVersion='27.0' if IOS27 else '17.0'), fmt=plistlib.FMT_XML))
subprocess.run(['codesign', '--force', '--sign', '-', '--pagesize', '4096', '--identifier', 'org.blu-sharky.NativeAdrenoMetal',
bundle], check=True)
subprocess.run(['codesign', '--verify', '--strict', bundle], check=True)
@@ -70,7 +91,7 @@ cdhash = re.search(r'^CDHash=([a-f0-9]{40})$', detail, re.M)[1]
assert re.search(r'^Page size=4096$', detail, re.M), 'the signature must use 4 KiB pages'
header = exe.read_bytes()[:16]
magic, cputype, subtype = int.from_bytes(header[0:4], 'little'), int.from_bytes(header[4:8], 'little'), int.from_bytes(header[8:12], 'little')
assert (magic, cputype, subtype & 0xffffff) == (0xfeedfacf, 0x100000c, 0), 'expected an ordinary ARM64 (subtype 0) Mach-O'
assert (magic, cputype, subtype & 0xffffff) == (0xfeedfacf, 0x100000c, 2 if IOS27 else 0), 'expected an arm64e (27) or ARM64 (17) Mach-O'
# The probe daemon (launchd starts it as an owner job). The original platform sandbox policy refuses IOServiceOpen for any user-client class
# a process has not been granted (the same refusal an unentitled consumer got in the offline owner tests), so it carries exactly the grant that
# worked there: com.apple.security.iokit-user-client-class naming the one class it opens, plus no-container.
+41 -8
View File
@@ -14,9 +14,16 @@ ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / 'build/adreno740-accelerator'
OUT.mkdir(exist_ok=True)
sys.path.insert(0, str(ROOT / 'kits/XnuPrelink'))
from native_prelink import macho
from native_prelink import macho as ordinary_macho
def macho(data):
# The layout of an arm64e object is that of arm64; only the CPU subtype (2, with the pointer-authentication ABI flags) differs.
if len(data) >= 12 and struct.unpack_from('<I', data, 8)[0] & 0xff == 2:
data = data[:8] + bytes(4) + data[12:]
return ordinary_macho(data)
sys.path.insert(0, str(Path(__file__).resolve().parent))
from kernel_target import TRIPLE
from kernel_target import ARCH, IOS_VERSION, TRIPLE
def run(cmd, **kw):
@@ -27,10 +34,12 @@ def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
plan_path = ROOT / 'build/gpu-analysis/ioaccelerator-base-abi.json'
# iPadOS 27 (arm64e fileset): the reviewed 24A446 plan, original addresses before the collection slide.
FILESET = ARCH == 'arm64e' and IOS_VERSION.split('.')[0] == '27'
plan_path = ROOT / ('build/gpu-analysis/ioaccelerator-base-abi-24A446.json' if FILESET else 'build/gpu-analysis/ioaccelerator-base-abi.json')
# The audit of the original base class needs the hardware-observed parent evidence of the audited kernel; its recorded result
# ships with the scripts and seeds a fresh tree.
recorded = ROOT / 'scripts/data/ioaccelerator-base-abi.json'
recorded = ROOT / ('scripts/data/ioaccelerator-base-abi-24A446.json' if FILESET else 'scripts/data/ioaccelerator-base-abi.json')
if not plan_path.exists() and recorded.exists():
plan_path.parent.mkdir(parents=True, exist_ok=True)
plan_path.write_bytes(recorded.read_bytes())
@@ -79,8 +88,30 @@ extra = undefined - set(bindings)
if extra or not undefined & set(bindings):
sys.exit('Unexpected original-base imports: %s' % sorted(extra))
asm = OUT / 'original-bindings.S'
asm.write_text(''.join(f'.globl {n}\n.set {n}, {a:#x}\n' for n, a in sorted(bindings.items()) if n in undefined))
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.
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:
continue
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'
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)
run(['xcrun', 'clang', '-target', TRIPLE, '-isysroot', sdk, '-c', OUT / 'original-addresses.S', '-o', OUT / 'original-addresses.o'])
binding_objects.append(OUT / 'original-addresses.o')
else:
asm.write_text(''.join(f'.globl {n}\n.set {n}, {a:#x}\n' for n, a in sorted(bindings.items()) if n in undefined))
run(['xcrun', 'clang', '-target', TRIPLE, '-isysroot', sdk, '-c', asm, '-o', OUT / 'original-bindings.o'])
llvm = Path(run(['brew', '--prefix', 'llvm'], text=True).strip()) / 'bin'
@@ -99,13 +130,15 @@ if code(obj) != code(late):
sys.exit('Only the owned initializer registration may change')
archive = OUT / 'libAdreno740Accelerator.late.a'
archive.unlink(missing_ok=True)
run(['xcrun', 'ar', 'rcs', archive, late, OUT / 'original-bindings.o'])
run(['xcrun', 'ar', 'rcs', archive, late, *binding_objects])
# The kernel links libAdreno740.a; the staged class rides in it (its initializer is not in __mod_init_func).
native = ROOT / 'build/adreno740/libAdreno740.a'
if native.exists():
run(['xcrun', 'ar', 'rcs', native, late, OUT / 'original-bindings.o'])
run(['xcrun', 'ar', 'rcs', native, late, *binding_objects])
(OUT / 'build-report.json').write_text(json.dumps(dict(
source_sha256={str(p.relative_to(ROOT)): sha(p) for p in (source, ROOT / 'drivers/AdrenoMetal/OriginalAcceleratorABI.h')},
# The header the source includes (#include "../NativeMetal/OriginalAcceleratorABI.h", beside the driver's own checkout).
source_sha256={str(source.relative_to(ROOT)): sha(source),
'OriginalAcceleratorABI.h': sha(source.resolve().parent.parent / 'NativeMetal/OriginalAcceleratorABI.h')},
archive_sha256=sha(archive), original_abi_sha256=sha(plan_path), bindings={n: hex(a) for n, a in bindings.items()},
original_instructions_modified=False, runtime_qualified=False), indent=2) + '\n')
print('PASS: staged AGXAccelerator builds against the audited original IOAcceleratorES imports')
+6 -1
View File
@@ -5,8 +5,9 @@ import json
import tomllib
from pathlib import Path
import subprocess
import sys
from kernel_target import TRIPLE, check_object
from kernel_target import ARCH, TRIPLE, check_object
ROOT = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
@@ -21,6 +22,10 @@ parser.add_argument('--verified-root-bundle', action='store_true', help='Select
parser.add_argument('--negative-verified-block', action='store_true', help='Negative own-buffer control: corrupt only the completed DMA snapshot')
args = parser.parse_args()
identifier = aliases.get(args.component, args.component)
if identifier == 'ShengUfs' and ARCH == 'arm64e':
if any((args.negative_aes_kat, args.verified_root, args.verified_root_bundle, args.negative_verified_block)):
parser.error('The source UFS runtime always uses the authenticated policy bundle of the OS profile')
raise SystemExit(subprocess.call([sys.executable, ROOT / 'scripts/build-sheng-source-storage.py', '--tree', ROOT]))
metadata = catalog.get(identifier, {})
runtime = metadata.get('legacy_name', identifier)
args.component = runtime
+5
View File
@@ -2,6 +2,11 @@
"""Qualify an owned offline HID source against the unchanged target IOHIDDevice."""
import hashlib,json,struct,subprocess,sys
from pathlib import Path
from kernel_target import ARCH, IOS_VERSION
if ARCH != 'arm64' or IOS_VERSION.split('.')[0] != '17':
raise SystemExit('The original HID ABI builder only supports arm64 iOS 17. '
'Use build-sheng-source-drivers.py for a source-interface compile audit; '
'its output still requires target-family linking and runtime qualification.')
ROOT=Path(__file__).resolve().parents[1];OUT=ROOT/'build/sheng-hid-input';OUT.mkdir(exist_ok=True)
sys.path.insert(0,str(ROOT/'kits/XnuPrelink'))
from native_prelink import macho,backed_offset,exports
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Compile sheng storage/input against pinned public source interfaces.
Produces relocatable objects and archives only. It does not bind an original
IOKit family, emit a boot image, or qualify the family's private binary ABI.
"""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import tomllib
ROOT = Path(__file__).resolve().parents[1]
def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def load_helper(name, path):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def compile_command(record, source, output, includes, extra):
arguments = record['arguments']
command = [arguments[0], *includes]
index = 1
replaced = False
while index < len(arguments):
value = arguments[index]
if value == record['file']:
command.append(str(source))
replaced = True
elif value == '-o':
command.extend(['-o', str(output)])
index += 1
elif value in ('-MF', '-MT', '-MQ', '-MJ'):
index += 1
elif value in ('-MD', '-MMD', '-MP'):
pass
elif value == '-Xclang' and arguments[index + 1].startswith('-W'):
index += 1
elif not value.startswith('-W'):
command.append(value)
index += 1
if not replaced:
raise SystemExit('Kernel compilation record does not contain its source argument')
command += ['-Wall', '-Wextra', '-Werror', '-Wno-unused-parameter']
if source.suffix != '.c':
command += ['-Wno-c99-designator', '-Wno-reorder-init-list',
'-Wno-deprecated-enum-enum-conversion', '-Wno-unnecessary-virtual-specifier']
command += ['-mgeneral-regs-only', '-MD', '-MF', str(output.with_suffix('.d')), *extra]
return command
def nm(path, mode):
return set(subprocess.check_output(['xcrun', 'nm', '-j', mode, str(path)], text=True).splitlines())
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--tree', required=True, type=Path, help='Prepared XNU build tree')
parser.add_argument('--device', required=True, type=Path, help='UniXNU-sheng checkout')
parser.add_argument('--profile', type=Path, help='OS profile.toml (default: the os_profile of the device board.toml)')
parser.add_argument('--storage-source', type=Path)
parser.add_argument('--hid-source', type=Path)
parser.add_argument('--touch-firmware', type=Path, help='Extracted Novatek firmware; defaults to the device cache')
parser.add_argument('--with-storage-family', action='store_true',
help='Also compile the public IOStorageFamily kernel sources and generated IIG implementation')
args = parser.parse_args()
tree, device = args.tree.resolve(), args.device.resolve()
if args.profile is None:
board = tomllib.loads((device / 'board.toml').read_text())
args.profile = ROOT / 'profiles/os' / board['board']['os_profile'] / 'profile.toml'
profile = tomllib.loads(args.profile.read_text())
pins = {entry['name']: entry for entry in profile.get('source', [])}
for family in ('IOStorageFamily', 'IOHIDFamily'):
if not pins.get(family, {}).get('commit'):
parser.error(f'{args.profile}: missing explicit {family} source pin')
os_profile = profile['os']
os.environ['UNIXNU_XNU_ARCH'] = os_profile['arch']
os.environ['UNIXNU_IOS_VERSION'] = os_profile['ios_version']
target = load_helper('source_driver_target', ROOT / 'scripts/kernel_target.py')
storage_helper = load_helper('source_storage_headers', ROOT / 'scripts/prepare-storage-headers.py')
storage = (args.storage_source or tree / 'refer/IOStorageFamily').resolve()
hid = (args.hid_source or tree / 'refer/IOHIDFamily').resolve()
storage_helper.pinned_source(hid, pins['IOHIDFamily']['commit'])
storage_helper.pinned_source(storage, pins['IOStorageFamily']['commit'])
recipe = tree / 'build/xnu/ios-obj/DEVELOPMENT_ARM64_BCM2837/iokit/DEVELOPMENT/IOPlatformExpert.cpo.json'
record = json.loads(recipe.read_text().strip().rstrip(','))
c_recipe = tree / 'build/xnu/ios-obj/DEVELOPMENT_ARM64_BCM2837/osfmk/DEVELOPMENT/startup.o.json'
c_record = json.loads(c_recipe.read_text().strip().rstrip(','))
# Audit the existing object too, before compiling with a possibly stale recipe.
kernel_object = Path(record['directory']) / 'IOPlatformExpert.cpo'
target.check_object(kernel_object.read_bytes(), 'Kernel compilation recipe')
target.check_object((Path(c_record['directory']) / 'startup.o').read_bytes(), 'Kernel C compilation recipe')
kernel = tree / 'build/xnu/ios-obj/DEVELOPMENT_ARM64_BCM2837/mach.development.bcm2837.unstripped.noctf'
out = tree / 'build/sheng-source-drivers'
out.mkdir(parents=True, exist_ok=True)
manifest = out / 'report.json'
manifest.unlink(missing_ok=True)
header_report = storage_helper.prepare(storage, pins['IOStorageFamily']['commit'], out / 'headers')
include = out / 'headers/include'
for source_dir, relative in ((hid / 'IOHIDFamily', 'IOKit/hid'),
(hid / 'IOHIDSystem/IOKit/hidsystem', 'IOKit/hidsystem')):
destination = include / relative
destination.mkdir(parents=True, exist_ok=True)
for path in sorted(source_dir.glob('*.h')):
shutil.copyfile(path, destination / path.name)
sdk = Path(header_report['sdk'])
sdk_kernel = sdk / 'System/Library/Frameworks/Kernel.framework/Versions/A/Headers'
# XNU's own headers retain precedence. The SDK supplies generated HIDDriverKit
# declarations missing from the source checkout; used inputs are hashed below.
includes = ['-I', str(include), '-idirafter', str(sdk_kernel)]
extra = ['-I', str(tree / '.cache/xnu-work/osfmk')]
firmware = (args.touch_firmware or device / '.cache/firmware/novatek/novatek_nt36532_n81a_fw_csot.bin').resolve()
data = firmware.read_bytes()
if not data:
raise SystemExit(f'{firmware}: empty touch firmware')
firmware_source = out / 'NvtFirmware.c'
firmware_source.write_text('/* Generated vendor firmware; build output only. */\n#include <stdint.h>\n'
f'const uint32_t native_touch_firmware_size = {len(data)};\n'
f'const uint8_t native_touch_firmware[{len(data)}] = {{' +
','.join(str(value) for value in data) + '};\n')
components = {
'ShengUfs': [(device / 'drivers/ShengUfs/NativeUfs.cpp', [
'-DNATIVE_VERIFIED_BLOCK=1', '-DNATIVE_VERIFIED_BLOCK_MULTI_POLICY=1',
'-I', str(ROOT / 'kits/VerifiedRoot/include'), '-I', str(ROOT / 'drivers/VirtioMmioBlock')])],
'ShengHidInput': [(device / 'drivers/ShengHidInput/NativeHidInput.cpp', ['-DNATIVE_HID_SOURCE_API=1'])],
'ShengInputTransport': [(device / 'package/TouchProbe.cpp', [
'-I', str(ROOT / 'drivers/QcomGeniSpi'), '-I', str(device / 'drivers/ShengNovatekTouch')]),
(device / 'package/ButtonProbe.cpp', []),
(ROOT / 'drivers/QcomGeniSpi/GeniSpi.c', []),
(device / 'drivers/ShengNovatekTouch/NvtProtocol.c', []),
(device / 'drivers/ShengNovatekTouch/NvtTouchCore.c', []),
(firmware_source, [])],
}
generation = None
if args.with_storage_family:
generated = out / 'storage-generated'
generated.mkdir(exist_ok=True)
driver_sdk = Path(subprocess.check_output(['xcrun', '--sdk', 'driverkit', '--show-sdk-path'], text=True).strip())
definition = storage / 'BlockStorageDeviceDriverKit/IOUserBlockStorageDevice.iig'
header, implementation = generated / 'IOUserBlockStorageDevice.h', generated / 'IOUserBlockStorageDevice.iig.cpp'
generate = ['xcrun', 'iig', '--def', str(definition), '--header', str(header),
'--impl', str(implementation), '--', '-x', 'c++', '-std=gnu++17',
'-fno-exceptions', '-fno-rtti', '-D__IIG=1', '-DPLATFORM_DriverKit=1',
'-isysroot', str(driver_sdk), '-iframework',
str(driver_sdk / 'System/DriverKit/System/Library/Frameworks'),
'-idirafter', str(driver_sdk / 'System/DriverKit/usr/include')]
(generated / 'command.json').write_text(json.dumps(generate, indent=2) + '\n')
result = subprocess.run(generate, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
(generated / 'generate.log').write_text(result.stdout)
if result.returncode:
raise SystemExit(f'IIG generation failed:\n{result.stdout}')
generation = dict(definition_sha256=sha(definition), header_sha256=sha(header),
implementation_sha256=sha(implementation), sdk=str(driver_sdk),
sdk_iig_inputs={str(p): sha(p) for p in sorted(driver_sdk.rglob('*.iig'))})
# PBXSourcesBuildPhase of Apple's IOStorageFamily kernel target, plus
# the implementation generated from its IIG input. Keep Apple source
# warnings visible without treating them as errors in owned code.
names = ['IOAppleLabelScheme', 'IOApplePartitionScheme', 'IOBlockStorageDevice',
'IOBlockStorageDriver', 'IOFDiskPartitionScheme', 'IOFilterScheme',
'IOGUIDPartitionScheme', 'IOMedia', 'IOMediaBSDClient', 'IOPartitionScheme',
'IORequest', 'IORequestsPool', 'IOStorage', 'IOUserBlockStorageDevice_kext']
family_flags = ['-I', str(storage), '-I', str(generated), '-Wno-error']
components['IOStorageFamily'] = [(storage / (name + '.cpp'), family_flags) for name in names]
components['IOStorageFamily'].append((implementation, family_flags))
kernel_exports = nm(kernel, '-gU')
entries = {}
for component, sources in components.items():
objects, details = [], []
archive = out / ('lib' + component + '.a')
archive.unlink(missing_ok=True)
for source, flags in sources:
output = out / (source.stem + '.o')
output.unlink(missing_ok=True)
source_record = c_record if source.suffix == '.c' else record
command = compile_command(source_record, source, output, includes, extra + flags)
(out / (source.stem + '.command.json')).write_text(json.dumps(command, indent=2) + '\n')
result = subprocess.run(command, cwd=source_record['directory'], text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
(out / (source.stem + '.log')).write_text(result.stdout)
if result.returncode:
raise SystemExit(f'{source.name}: compile failed\n{result.stdout[-5000:]}')
target.check_object(output.read_bytes(), source.name)
dependencies = {}
deptext = output.with_suffix('.d').read_text().replace('\\\n', '')
for name in shlex.split(deptext.split(':', 1)[1]):
path = Path(name)
if not path.is_absolute():
path = Path(source_record['directory']) / path
dependencies[str(path.resolve())] = sha(path)
(out / (source.stem + '.inputs.json')).write_text(json.dumps(dependencies, indent=2) + '\n')
imports = nm(output, '-u')
details.append(dict(source=str(source), source_sha256=sha(source), object=str(output),
object_sha256=sha(output), undefined_symbols=sorted(imports),
warnings=sum('warning:' in line for line in result.stdout.splitlines())))
objects.append(output)
subprocess.run(['xcrun', 'ar', 'rcs', str(archive), *map(str, objects)], check=True)
entries[component] = dict(archive=str(archive), archive_sha256=sha(archive), objects=details)
print(f'PASS: {component}: {target.TRIPLE} relocatable objects')
partial = out / 'source-drivers.partial.o'
partial.unlink(missing_ok=True)
link = ['xcrun', 'ld', '-r', '-arch', target.ARCH, '-platform_version', 'ios',
target.IOS_VERSION, target.IOS_VERSION, '-o', str(partial), '-all_load',
*[entry['archive'] for entry in entries.values()]]
(out / 'partial-link-command.json').write_text(json.dumps(link, indent=2) + '\n')
result = subprocess.run(link, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
(out / 'partial-link.log').write_text(result.stdout)
if result.returncode:
raise SystemExit(f'Relocatable link failed:\n{result.stdout}')
target.check_object(partial.read_bytes(), 'Combined source drivers')
report = dict(profile=str(args.profile.resolve()), profile_sha256=sha(args.profile),
build_scripts={str(path): sha(path) for path in (
Path(__file__), ROOT / 'scripts/prepare-storage-headers.py', ROOT / 'scripts/kernel_target.py')},
target=target.TRIPLE, recipe=str(recipe), recipe_sha256=sha(recipe),
c_recipe=str(c_recipe), c_recipe_sha256=sha(c_recipe),
kernel=str(kernel), kernel_sha256=sha(kernel),
source_pins={name: pins[name] for name in ('IOStorageFamily', 'IOHIDFamily')},
sdk=str(sdk), components=entries,
storage_family_iig=generation,
touch_firmware=dict(path=str(firmware), sha256=sha(firmware), size=len(data)),
partial_link=dict(object=str(partial), sha256=sha(partial), passed=True),
unresolved_after_native_kernel=sorted(nm(partial, '-u') - kernel_exports),
compile_pass=True, link_qualified=False, runtime_qualified=False,
edge_gestures_qualified=False,
limitations=['Family imports have no target binding in this build.',
'HID diagnostics/startup and verified-root policy/crypto objects are not bundled.',
'Source HID mode does not install the Darwin 23 edge-event vtable patch.',
'macOS public source headers and generated SDK headers do not prove the iPadOS binary ABI.'])
manifest.write_text(json.dumps(report, indent=2) + '\n')
print(f'Compile audit: {manifest}; linking and runtime remain unqualified')
if __name__ == '__main__':
main()
+226
View File
@@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""Build the arm64e source UFS runtime and its authenticated policy handoff.
The original IOStorageFamily and APFS stay in the kernel collection. Only the
public IOBlockStorageDevice implementation is compiled here; its metaclass is
registered late below the original class so the original storage driver can
match the source-backed device. Binary interface qualification is separate.
"""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import shutil
import struct
import subprocess
ROOT = Path(__file__).resolve().parents[1]
def helper(name, path):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def weak_defaults(source):
"""Zero weak definitions for the weak imports of a diagnostic source.
The kernel link cannot leave weak imports undefined, so components that are
not built in get these defaults; a component's own definition wins. Data is
zero, functions report absence: int returns -1, strings "", other scalars 0, pointers NULL.
Hidden visibility keeps the static kernel link from binding them at run time.
"""
import re
lines = []
for line in source.read_text().splitlines():
line = line.strip()
if not (line.startswith('extern ') and line.endswith('__attribute__((weak_import));')):
continue
decl = line[len('extern '):-len('__attribute__((weak_import));')].replace('__attribute__((weak_import))', '').strip()
call = re.match(r'^(.*?[\s*])([A-Za-z_]\w*)\s*\((.*)\)$', decl)
if call and '(*' not in decl.split(call.group(2))[0]:
ret = call.group(1).strip()
body = ('' if ret == 'void' else ' return -1;' if ret == 'int' else
' return "";' if ret.replace(' ', '') in ('constchar*', 'char*') else ' return 0;')
lines.append(f'__attribute__((weak, visibility("hidden"))) {decl} {{{body} }}')
else:
lines.append(f'__attribute__((weak, visibility("hidden"))) {decl};')
# Quoted headers are resolved next to the source, which the generated file is not.
includes = [l.replace('#include "', f'#include "{source.parent}/') for l in source.read_text().splitlines()
if l.startswith('#include')]
# Element types the declarations name are defined in the source itself.
named = set(re.findall(r'struct (\w+)', '\n'.join(lines)))
includes += [m.group(0) for m in re.finditer(r'^struct (\w+) \{[^;{}]*(?:;[^;{}]*)*\};', source.read_text(), re.M)
if m.group(1) in named]
# The defaults keep the declarations' unnamed parameters.
pragma = ['#pragma clang diagnostic ignored "-Wc23-extensions"']
return '\n'.join(includes + pragma + lines) + '\n'
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--tree', type=Path, default=ROOT)
parser.add_argument('--device', type=Path)
parser.add_argument('--storage-source', type=Path)
parser.add_argument('--policy-dir', type=Path,
help='Directory containing OS/ and SystemOS/ signed policies and verification.json')
args = parser.parse_args()
tree = args.tree.resolve()
device = (args.device or tree).resolve()
storage = (args.storage_source or tree / 'refer/IOStorageFamily').resolve()
# The OS profile names the root images ([root]); kernel.py passes them in the environment.
build_id = os.environ.get('UNIXNU_IPSW_BUILD')
if not build_id:
parser.error('UNIXNU_IPSW_BUILD is not set; run through unixnu build kernel')
policies = (args.policy_dir or tree / f'build/verified-root-{build_id}').resolve()
# The resolved build tree links the device package as kits/sheng, while an
# explicit device repository keeps package/ and drivers/ side by side.
package_dirs = [device / 'package']
if device != tree:
package_dirs += [device / 'kits/sheng', tree / 'kits/sheng']
else:
package_dirs += [tree / 'kits/sheng']
def device_source(relative, *extra):
for directory in list(extra) + package_dirs + [device, tree]:
candidate = directory / relative
if candidate.is_file():
return candidate
parser.error(f'{relative} not found under the device or build tree')
if os.environ.get('UNIXNU_XNU_ARCH', 'arm64e') != 'arm64e':
parser.error('This adapter requires an arm64e iPadOS profile')
os.environ['UNIXNU_XNU_ARCH'] = 'arm64e'
if int(os.environ.get('UNIXNU_IOS_VERSION', '0').split('.')[0]) < 26:
parser.error('This adapter requires iPadOS 26 or later')
build = helper('source_compile', ROOT / 'scripts/build-sheng-source-drivers.py')
headers = helper('source_headers', ROOT / 'scripts/prepare-storage-headers.py')
target = helper('source_target', ROOT / 'scripts/kernel_target.py')
out = tree / 'build/sheng-source-storage'
out.mkdir(parents=True, exist_ok=True)
header_report = headers.prepare(storage, '7edb88fbae296fb7c8ce2f64e115e116e566d51c', out / 'headers')
# A bring-up image can carry one policy: the boot image must stay under the
# 192 MiB fastboot limit, and the second table does not fit yet.
tables_in_userdata = os.environ.get('XNU_SOURCE_STORAGE_TABLES', '') == 'userdata'
selected = [name.strip() for name in (os.environ.get('UNIXNU_SOURCE_STORAGE_POLICIES') or os.environ.get('XNU_SOURCE_STORAGE_POLICIES', 'OS,SystemOS')).split(',')
if name.strip() in ('OS', 'SystemOS')]
if not selected:
parser.error('UNIXNU_SOURCE_STORAGE_POLICIES names no policy')
manifest_paths = [policies / name / 'manifest.bin' for name in selected]
key = (policies / f'{selected[0]}/trusted-key.bin').read_bytes()
if len(key) != 395 or any(key != (policies / f'{name}/trusted-key.bin').read_bytes() for name in selected):
parser.error('All selected policies must share the same complete explicit owner anchor')
manifests = []
expected = {}
for name, prefix in (('OS', 'UNIXNU_ROOT_OS'), ('SystemOS', 'UNIXNU_ROOT_SYSTEMOS')):
digest, identity = os.environ.get(prefix + '_SHA256'), os.environ.get(prefix + '_UUID')
if name in selected and not (digest and identity):
parser.error(f'the OS profile has no [root] {name.lower()}_image_sha256 and {name.lower()}_volume_uuid')
expected[name] = (digest, identity and identity.upper())
for path in manifest_paths:
verification = json.loads(path.with_name('verification.json').read_text())
digest, identity = expected[path.parent.name]
if (not verification['real_c_signature_and_block_verification'] or
verification['image_sha256'] != digest or verification['volume_uuid'] != identity or
verification['trusted_key_sha256'] != hashlib.sha256(key).hexdigest()):
parser.error(f'{path.parent.name}: require complete verification of the actual {build_id} image')
data = path.read_bytes()
if len(data) < 544 or data[56:88].hex() != digest:
parser.error(f'{path}: manifest does not describe the selected image')
# With the tables in userdata only the 544-byte signed header goes into the boot image;
# NativeUfs reads "<window>.vroot" from the root image and vr_open checks it against the header.
manifests.append(data[:544] if tables_in_userdata else data)
cursor = 32 + 16 * len(manifests)
table = bytearray()
for data in manifests:
table += struct.pack('<QQ', cursor, len(data))
cursor += len(data)
bundle = struct.pack('<8sQQQ', b'VROOTB01', 1, len(manifests), cursor) + table + b''.join(manifests)
(out / 'root-policies.bin').write_bytes(bundle)
# Only the independent trust anchor is linked into the kernel. The large
# signed hash tables are a separate firmware handoff, authenticated in XNU.
boot_log_defaults = out / 'BootLogDefaults.c'
boot_log_defaults.write_text(weak_defaults(device_source('BootLog.c')))
anchor = out / 'RootAnchor.c'
anchor.write_text('#include <stdint.h>\nconst uint8_t verified_root_key[395] = {' +
','.join(str(b) for b in key) + '};\n')
base = out / 'IOBlockStorageDevice.cpp'
original = (storage / 'IOBlockStorageDevice.cpp').read_text()
macro = 'OSDefineMetaClassAndAbstractStructors(IOBlockStorageDevice,IOService)'
if original.count(macro) != 1:
parser.error('Pinned IOBlockStorageDevice registration macro changed')
base.write_text(original.replace(macro, '''
extern "C" const OSMetaClass *native_source_storage_parent(void);
OSDefineMetaClassWithInit(IOBlockStorageDevice, IOService, )
IOBlockStorageDevice::MetaClass::MetaClass()
: OSMetaClass("UniXNUBlockStorageDevice", native_source_storage_parent(), sizeof(IOBlockStorageDevice)) {}
OSDefineAbstractStructors(IOBlockStorageDevice, IOService)
OSDefineOperatorMethods(IOBlockStorageDevice)
'''))
obj = tree / 'build/xnu/ios-obj/DEVELOPMENT_ARM64_BCM2837'
cpp = json.loads((obj / 'iokit/DEVELOPMENT/IOPlatformExpert.cpo.json').read_text().strip().rstrip(','))
c = json.loads((obj / 'osfmk/DEVELOPMENT/startup.o.json').read_text().strip().rstrip(','))
# BSD sources (sys/proc.h, the system calls) take kern_exec's flags.
bsd = json.loads((obj / 'bsd/DEVELOPMENT/kern_exec.o.json').read_text().strip().rstrip(','))
bsd_sources = {'BootWatchdogPolicy.c'}
includes = ['-I', str(out / 'headers/include'), '-I', str(ROOT / 'kits/VerifiedRoot/include'),
'-I', str(ROOT / 'drivers/VirtioMmioBlock'), '-I', str(tree / '.cache/xnu-work/osfmk')]
flags = ['-DNATIVE_UFS_SOURCE_API=1', '-DNATIVE_VERIFIED_BLOCK=1',
'-DNATIVE_VERIFIED_BLOCK_MULTI_POLICY=1', '-DNATIVE_VERIFIED_POLICY_HANDOFF=1']
# The RAM disk carries the Data seed; like NativeUfs its class registers with the source storage classes.
sources = [base, device / 'drivers/ShengUfs/NativeUfs.cpp', ROOT / 'drivers/RamBlock/NativeRamBlock.cpp',
device_source('SourceStorage.cpp'), device_source('SourceRootPolicies.cpp'),
device_source('SourceRootBinding.cpp'), device_source('BootLog.c'), boot_log_defaults, device_source('BootReset.c'),
ROOT / 'kits/VerifiedRoot/src/verified_root.c',
ROOT / 'kits/VerifiedRoot/adapters/xnu_crypto.c', anchor]
if int(os.environ['UNIXNU_IOS_VERSION'].split('.')[0]) >= 27:
# The source path does not run BootProgress's process worker. Reuse its
# native capture code in a separate worker owned by the UFS log.
flags.append('-DNATIVE_BOOT_PROCESS_CAPTURE=1')
sources += [device_source(name) for name in
('BootProcessProbe.c', 'BootUiTraceProbe.c', 'BootProcessWorker.c')]
# The launchd job policy (sheng-disable-watchdogd, sheng-lite, sheng-disable-jobs, sheng-setup) that BootProgress carries on
# iPadOS 17; the kernel runs it after the Data mount.
sources.append(device_source('BootWatchdogPolicy.c'))
objects, inputs = [], {}
objcopy = shutil.which('llvm-objcopy') or '/opt/homebrew/opt/llvm/bin/llvm-objcopy'
for source in sources:
record = (bsd if source.name in bsd_sources else c) if source.suffix == '.c' else cpp
output = out / (source.stem + '.o')
command = build.compile_command(record, source, output, includes, flags)
if source == base:
command.append('-Wno-error')
result = subprocess.run(command, cwd=record['directory'], text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
(out / (source.stem + '.command.json')).write_text(json.dumps(command, indent=2) + '\n')
(out / (source.stem + '.log')).write_text(result.stdout)
if result.returncode:
raise SystemExit(result.stdout[-6000:])
target.check_object(output.read_bytes(), source.name)
if source.name in ('IOBlockStorageDevice.cpp', 'NativeUfs.cpp', 'NativeRamBlock.cpp'):
late = output.with_suffix('.late.o')
subprocess.run([objcopy, '--remove-section=__DATA,__mod_init_func',
f'--globalize-symbol=__GLOBAL__sub_I_{source.name}', output, late], check=True)
output = late
objects.append(output)
inputs[str(source)] = build.sha(source)
archive = out / 'libShengSourceStorage.a'
archive.unlink(missing_ok=True)
subprocess.run(['xcrun', 'ar', 'rcs', archive, *objects], check=True)
# Keep the catalog's archive output stable for the resolved-tree copier.
catalog_output = tree / 'build/sheng-ufs/libShengUfs.a'
catalog_output.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(archive, catalog_output)
report = dict(archive=str(archive), archive_sha256=build.sha(archive), sources=inputs,
headers=header_report, target=target.TRIPLE,
policy_bundle_sha256=build.sha(out / 'root-policies.bin'),
trusted_key_sha256=hashlib.sha256(key).hexdigest(), runtime_qualified=False)
(out / 'report.json').write_text(json.dumps(report, indent=2) + '\n')
print(archive)
if __name__ == '__main__':
main()
+7 -6
View File
@@ -2,7 +2,7 @@
"""Build Turnip (Mesa freedreno Vulkan) with the native Darwin kernel backend (patches/mesa/turnip-darwin-kmd.patch).
--target host builds for the Mac only to prove the backend compiles and links against the real Turnip; --target ios
cross-builds the ARM64/iOS 17 library. Neither runs a GPU: the backend talks to the NativeAdreno IOKit service, which
cross-builds the ARM64/iOS 17 library, --target ios27 the arm64e/iOS 27 one. Neither runs a GPU: the backend talks to the NativeAdreno IOKit service, which
exists only on the tablet. Output stays under build/ and is never committed."""
import argparse
import hashlib
@@ -16,12 +16,12 @@ from mesa_patch_identity import validate_recorded_patches
ROOT = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--target', choices=('host', 'ios'), default='host')
parser.add_argument('--target', choices=('host', 'ios', 'ios27'), default='host')
parser.add_argument('--configure-only', action='store_true')
parser.add_argument('--builtin-pack', type=Path, help='Embed an exact-source/profile/GPU-bound own builtin pack; no runtime compiler fallback')
args = parser.parse_args()
SOURCE = ROOT / '.cache/mesa-work'
OUT = ROOT / ('build/mesa-tu' if args.target == 'host' else 'build/mesa-tu-ios')
OUT = ROOT / {'host': 'build/mesa-tu', 'ios': 'build/mesa-tu-ios', 'ios27': 'build/mesa-tu-ios27'}[args.target]
LOGS = ROOT / '.cache'
@@ -44,11 +44,12 @@ options = ['-Dgallium-drivers=', '-Dvulkan-drivers=freedreno', '-Dfreedreno-kmds
'-Dllvm=disabled', '-Dshared-glapi=disabled', '-Dxmlconfig=disabled', '-Dshader-cache=disabled',
'-Dtools=', '-Dbuild-tests=false', '-Dvideo-codecs=', '-Dvulkan-layers=']
setup = [ROOT / '.cache/mesa-venv/bin/meson', 'setup', OUT, SOURCE, '--wrap-mode=nodownload']
if args.target == 'ios':
if args.target in ('ios', 'ios27'):
sdk = read(['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'])
find = lambda tool: read(['xcrun', '--sdk', 'iphoneos', '--find', tool])
flags = ['-target', 'arm64-apple-ios17.0', '-isysroot', sdk, '-fstack-protector-strong']
cross = ROOT / 'build/mesa-tu-ios.cross'
triple = 'arm64e-apple-ios27.0' if args.target == 'ios27' else 'arm64-apple-ios17.0'
flags = ['-target', triple, '-isysroot', sdk, '-fstack-protector-strong']
cross = ROOT / ('build/mesa-tu-%s.cross' % args.target)
cross.parent.mkdir(exist_ok=True)
cross.write_text(
'[binaries]\nc = %r\ncpp = %r\nobjc = %r\nar = %r\nstrip = %r\npkg-config = \'/usr/bin/false\'\n'
+24 -9
View File
@@ -80,9 +80,10 @@ if [ "$BOOT_PROGRESS" != 0 ] && [ "$BOOT_PROGRESS" != 1 ]; then
exit 1
fi
if [ "$BOOT_PROGRESS" = 1 ]; then
# The board diagnostics use the UFS/owner-root paths; they do not touch the Virtio block driver.
if [ "${XNU_NATIVE_UFS:-0}" != 1 ] || [ "${XNU_OWNER_ROOT_AUDIT:-0}" != 1 ] || \
[ "${XNU_NATIVE_VIRTIO_BLOCK:-0}" != 1 ] || [ "${XNU_SOURCE_CORECRYPTO14:-0}" != 1 ]; then
echo 'Boot progress requires the audited UFS/owner-root, Virtio and source-corecrypto build' >&2
[ "${XNU_SOURCE_CORECRYPTO14:-0}" != 1 ]; then
echo 'Boot progress requires the audited UFS/owner-root and source-corecrypto build' >&2
exit 1
fi
python3 scripts/build-boot-progress.py
@@ -150,6 +151,9 @@ if [ "$(uname -s)" = Linux ]; then
fi
# The baseline builds the kernel; `all` also checks the Apple kext export ABI.
EXTRA_MAKE_ARGS=()
# A fileset boot replaces com.apple.kernel inside a kernel collection: the
# native executable must carry chained fixups and split-segment records.
EXTRA_MAKE_ARGS+=("XNU_KERNEL_CHAINED_FIXUPS=${XNU_KERNEL_CHAINED_FIXUPS:-0}")
ARM_DT_PLATFORM=${XNU_ARM_DT_PLATFORM:-0}
if [ "$ARM_DT_PLATFORM" != 0 ] && [ "$ARM_DT_PLATFORM" != 1 ]; then
echo 'XNU_ARM_DT_PLATFORM must be 0 or 1' >&2
@@ -205,7 +209,8 @@ if [ "$NATIVE_RAM_BLOCK" != 0 ] && [ "$NATIVE_RAM_BLOCK" != 1 ]; then
echo 'XNU_NATIVE_RAM_BLOCK must be 0 or 1' >&2
exit 1
fi
if [ "$NATIVE_RAM_BLOCK" = 1 ]; then
# With source storage the RAM disk is part of the source storage archive, which also starts it.
if [ "$NATIVE_RAM_BLOCK" = 1 ] && [ "${XNU_SOURCE_STORAGE:-0}" != 1 ]; then
if [ "${XNU_NATIVE_AES:-0}" != 1 ]; then
echo 'NativeRamBlock requires the qualified original storage/AES prelink closure' >&2
exit 1
@@ -280,7 +285,8 @@ if [ "$OWNER_ROOT_AUDIT" != 0 ] && [ "$OWNER_ROOT_AUDIT" != 1 ]; then
echo 'XNU_OWNER_ROOT_AUDIT must be 0 or 1' >&2
exit 1
fi
if [ "$OWNER_ROOT_AUDIT" = 1 ] && [ "$OWNER_ROOT_PROBE" != 1 ]; then
# Source storage verifies its root windows with the source corecrypto adapter instead of the probe.
if [ "$OWNER_ROOT_AUDIT" = 1 ] && [ "$OWNER_ROOT_PROBE" != 1 ] && [ "${XNU_SOURCE_STORAGE:-0}" != 1 ]; then
echo 'The root-binding audit requires real owner-verified block crypto' >&2
exit 1
fi
@@ -289,7 +295,8 @@ if [ "$OWNER_ROOT_TRUST" != 0 ] && [ "$OWNER_ROOT_TRUST" != 1 ]; then
echo 'XNU_OWNER_ROOT_TRUST must be 0 or 1' >&2
exit 1
fi
if [ "$OWNER_ROOT_TRUST" = 1 ] && { [ "$OWNER_ROOT_AUDIT" != 1 ] || [ "${XNU_NATIVE_VIRTIO_BLOCK:-0}" != 1 ]; }; then
if [ "$OWNER_ROOT_TRUST" = 1 ] && { [ "$OWNER_ROOT_AUDIT" != 1 ] || \
{ [ "${XNU_NATIVE_VIRTIO_BLOCK:-0}" != 1 ] && [ "${XNU_SOURCE_STORAGE:-0}" != 1 ]; }; }; then
echo 'Owner root acceptance requires actual mounted-media binding and verified block storage' >&2
exit 1
fi
@@ -418,7 +425,7 @@ if [ "$OWNER_DATA_MOUNT" = 1 ] && [ "$OWNER_BOOT_MANIFEST" != 1 ]; then
exit 1
fi
OWNER_DATA_HEADER=$ROOT/build/xnu/owner-data-volume.h
python3 - "$OWNER_DATA_HEADER" "$OWNER_DATA_MOUNT" "$NATIVE_RAM_BLOCK" "${XNU_OWNER_DATA_VOLUME_UUID:-}" "${XNU_OWNER_CACHE_VOLUME_UUID:-}" <<'PY_DATA_UUID'
python3 - "$OWNER_DATA_HEADER" "$OWNER_DATA_MOUNT" "$NATIVE_RAM_BLOCK" "${XNU_OWNER_DATA_VOLUME_UUID:-}" "${XNU_OWNER_CACHE_VOLUME_UUID:-}" "${XNU_OWNER_ROOT_VOLUME_UUID:-}" "$OWNER_BOOT_MANIFEST" <<'PY_DATA_UUID'
from pathlib import Path
import sys
import uuid
@@ -438,14 +445,22 @@ if len(sys.argv) > 5 and sys.argv[5]:
except ValueError:
raise SystemExit('Set XNU_OWNER_CACHE_VOLUME_UUID to the SystemOS volume UUID of the signed policy')
text += '#define XNU_OWNER_CACHE_VOLUME_UUID "' + cache + '"\n'
# The OS root volume of the boot manifest comes from the OS profile ([root] os_volume_uuid).
if len(sys.argv) > 6 and sys.argv[6]:
try:
text += '#define XNU_OWNER_ROOT_VOLUME_UUID "' + str(uuid.UUID(sys.argv[6])).upper() + '"\n'
except ValueError:
raise SystemExit('Set XNU_OWNER_ROOT_VOLUME_UUID to the OS volume UUID of the signed policy')
elif len(sys.argv) > 7 and sys.argv[7] == '1':
raise SystemExit('The owner boot manifest needs XNU_OWNER_ROOT_VOLUME_UUID (OS profile [root] os_volume_uuid)')
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists() or path.read_text() != text:
path.write_text(text)
PY_DATA_UUID
OWNER_DATA_STAMP=$ROOT/build/xnu/ios-obj/DEVELOPMENT_ARM64_BCM2837/.owner-data-mount-mode
if [ "$(cat "$OWNER_DATA_STAMP" 2>/dev/null || echo 0)" != "$OWNER_DATA_MOUNT:$NATIVE_RAM_BLOCK:${XNU_OWNER_CACHE_VOLUME_UUID:-}" ]; then
if [ "$(cat "$OWNER_DATA_STAMP" 2>/dev/null || echo 0)" != "$OWNER_DATA_MOUNT:$NATIVE_RAM_BLOCK:${XNU_OWNER_CACHE_VOLUME_UUID:-}:${XNU_OWNER_ROOT_VOLUME_UUID:-}" ]; then
rm -f "$ROOT/build/xnu/ios-obj/DEVELOPMENT_ARM64_BCM2837/bsd/DEVELOPMENT/kern_exec.o"
printf '%s\n' "$OWNER_DATA_MOUNT:$NATIVE_RAM_BLOCK:${XNU_OWNER_CACHE_VOLUME_UUID:-}" > "$OWNER_DATA_STAMP"
printf '%s\n' "$OWNER_DATA_MOUNT:$NATIVE_RAM_BLOCK:${XNU_OWNER_CACHE_VOLUME_UUID:-}:${XNU_OWNER_ROOT_VOLUME_UUID:-}" > "$OWNER_DATA_STAMP"
fi
OWNER_CRYPTEX_FILES=${XNU_OWNER_CRYPTEX_FILES:-0}
if [ "$OWNER_CRYPTEX_FILES" != 0 ] && [ "$OWNER_CRYPTEX_FILES" != 1 ]; then
@@ -700,7 +715,7 @@ PY_GPU_BROKER
KERNEL_EXTRA_ARCHIVES="$KERNEL_EXTRA_ARCHIVES -Wl,-force_load,$CONSOLE_ARCHIVE"
KERNEL_ARCHIVE_DEPENDENCIES="$KERNEL_ARCHIVE_DEPENDENCIES $CONSOLE_ARCHIVE"
fi
if [ "$NATIVE_RAM_BLOCK" = 1 ]; then
if [ "$NATIVE_RAM_BLOCK" = 1 ] && [ "${XNU_SOURCE_STORAGE:-0}" != 1 ]; then
RAM_BLOCK_ARCHIVE=$ROOT/build/ram-block/libRamBlock.a
test -f "$RAM_BLOCK_ARCHIVE"
if [ "${XNU_NATIVE_VIRTIO_BLOCK:-0}" = 1 ]; then
@@ -52,6 +52,16 @@ run = subprocess.run([str(nilargs), loader, str(cache)], capture_output=True, te
if run.returncode or not any(l.startswith('PASS') for l in run.stdout.splitlines()):
sys.exit('the nil-resource blit check failed; see %s' % (OUT / 'nil_args_host.log'))
print([l for l in run.stdout.splitlines() if l.startswith('PASS')][0])
# The GPU scanout: half-float frames converted by the compute kernel store the display driver's bytes; BGRA still copies.
scanout = OUT / 'scanout_host'
subprocess.run(['xcrun', 'clang++', *flags, KIT / 'NativeBridgeMetalFrontend.mm', KIT / 'bridge/scanout_host.mm',
ROOT / 'build/adreno-metal-bridge/host/libadreno_metal_bridge.a', '-framework', 'Foundation', '-framework', 'Metal',
'-framework', 'IOSurface', '-framework', 'CoreFoundation', '-o', scanout], check=True)
run = subprocess.run([str(scanout), loader, str(cache)], capture_output=True, text=True, timeout=600)
(OUT / 'scanout_host.log').write_text(run.stdout + run.stderr)
if run.returncode or not any(l.startswith('PASS') for l in run.stdout.splitlines()):
sys.exit('the GPU scanout check failed; see %s' % (OUT / 'scanout_host.log'))
print([l for l in run.stdout.splitlines() if l.startswith('PASS')][0])
# Interleaved encoding must be attributed to its own command buffer, regardless of commit order.
telemetry = OUT / 'telemetry_host'
subprocess.run(['xcrun', 'clang++', *flags, '-Wno-unused-function', KIT / 'NativeBridgeMetalFrontend.mm', KIT / 'bridge/telemetry_host.mm',
@@ -0,0 +1,13 @@
{
"kernelcache_sha256": "38e6d9fa5f789b8228f4617ef4c6d2a3fd8ab564abdc9b62920f1db53fc1780a",
"collection_slide": "0x1f000000000",
"metaclass": "0xfffffe000bb3f290",
"instance_bytes": 136,
"virtual_entries": 168,
"operational_overrides": [],
"metadata_overrides": [0, 1, 7],
"protected_compatible_constructor_aliases": ["0xfffffe000a14d990", "0xfffffe000a14d9c8"],
"nondeleting_destructor": "0xfffffe000a14da00",
"get_meta_class": "0xfffffe000a14da70",
"evidence": "IOGPUFamily static initializer at 0xfffffe000a14da80 constructs IOAcceleratorES::gMetaClass with size 0x88. vtable 0xfffffe000832f2e8 (+0x10 address point) has 168 IOService entries; only slots 0, 1 (destructors 0xfffffe000a14da08, 0xfffffe000a14da10) and 7 (getMetaClass) differ from IOService's. Both constructors call IOService::IOService(const OSMetaClass *) and store the signed vtable (DA, 0xcda1); the nondeleting destructor tail-calls IOService::~IOService."
}
+37 -16
View File
@@ -1,12 +1,13 @@
#!/usr/bin/env python3
"""Pack the owner-built Adreno Metal plugin into a GPU addon file for the boot payload.
make-gpu-addon.py [--publish] [--clients NAME,...] [--wallpaper DIR] [--prime-list FILE] OUTPUT
make-gpu-addon.py [--publish] [--no-daemon] [--wallpaper DIR] [--wallpaper-floating DIR] [--prime-list FILE] [--pipeline-pack FILE] [--cache-export] OUTPUT
The files are the ones add-gpu-plugin-data.py stages into a Data seed, with the same paths below /private/var/ngpu and the same modes:
addons/NativeAdrenoMetal.bundle/*, addons/NativeAdrenoMetal.publish (--publish), addons/NativeAdrenoMetal.clients (--clients),
addons/NativeAdrenoMetal.bundle/*, addons/NativeAdrenoMetal.publish (--publish),
bin/adrenoprobed and daemons/*.plist. --wallpaper adds the files of DIR (scripts/make-wallpaper-overlay.py) as wallpaper/*, which the
kernel mounts over the default wallpaper's layered animation. --prime-list adds FILE (the absolute paths of the OS image's .metallib
kernel mounts over the default wallpaper's layered animation; --wallpaper-floating DIR as wallpaper-floating/*, mounted over the same
wallpaper's floating layer (iPadOS 27). --prime-list adds FILE (the absolute paths of the OS image's .metallib
files, one per line) as addons/NativeAdrenoMetal.primelist, the libraries the render server compiles ahead (PrimeShaders.h). The payload carries the file next to the kernel (unixnu build payload --gpu-addon), the loader
hands it to the kernel as /chosen/memory-map/GpuAddon, and the kernel writes every file whose content or mode differs into the mounted
Data volume before its addon mounts (sheng-gpu-addon=1). The Data seed then no longer changes with the plugin, so its persistent store
@@ -17,13 +18,15 @@ loader accepts (a 256 MiB ramdisk does not start the firmware), and the kernel a
Layout, little endian: "NGPUADD2", u32 count, u32 0, u64 total bytes; per file u32 path bytes (with its NUL, at most 160), u32 mode,
u64 size, u64 compressed size, the path padded to 8 bytes, the zlib stream padded to 8 bytes."""
import os
import struct
import sys
import zlib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PLUGIN = ROOT / 'build/adreno-metal-plugin'
# ADRENO_PLUGIN_DIR: another plugin build (build/adreno-metal-plugin27 for iPadOS 27).
PLUGIN = Path(os.environ.get('ADRENO_PLUGIN_DIR') or ROOT / 'build/adreno-metal-plugin')
BUNDLE = PLUGIN / 'NativeAdrenoMetal.bundle'
@@ -32,10 +35,17 @@ def pad(data):
def main():
args = [a for a in sys.argv[1:] if a != '--publish']
publish = len(args) != len(sys.argv) - 1
clients = None
no_daemon = '--no-daemon' in sys.argv[1:]
args = [a for a in sys.argv[1:] if a not in ('--publish', '--no-daemon')]
publish = '--publish' in sys.argv[1:]
wallpaper = None
floating = None
if '--wallpaper-floating' in args:
at = args.index('--wallpaper-floating')
floating = Path(args[at + 1])
del args[at:at + 2]
if not floating.is_dir():
sys.exit('--wallpaper-floating takes a directory')
if '--wallpaper' in args:
at = args.index('--wallpaper')
wallpaper = Path(args[at + 1])
@@ -50,12 +60,16 @@ def main():
lines = prime_list.read_text().split()
if not lines or any(not line.startswith('/') or not line.endswith('.metallib') for line in lines):
sys.exit('--prime-list takes a file of absolute .metallib paths')
if '--clients' in args:
at = args.index('--clients')
clients = [name for name in args[at + 1].split(',') if name]
cache_export = '--cache-export' in args
if cache_export:
args.remove('--cache-export')
pipeline_pack = None
if '--pipeline-pack' in args:
at = args.index('--pipeline-pack')
pipeline_pack = Path(args[at + 1])
del args[at:at + 2]
if not clients or any(not all(c.isalnum() or c in '._-*' for c in name) for name in clients):
sys.exit('--clients takes process names separated by commas')
if pipeline_pack.read_bytes()[:8] != b'SHEXP001':
sys.exit('--pipeline-pack takes a SHEXP001 pack (local-tools/make-m2vpack.py)')
if len(args) != 1:
sys.exit(__doc__)
output = Path(args[0]).resolve()
@@ -69,17 +83,24 @@ def main():
files.append(('addons/NativeAdrenoMetal.bundle/' + path.relative_to(BUNDLE).as_posix(), mode, path.read_bytes()))
if publish:
files.append(('addons/NativeAdrenoMetal.publish', 0o644, b'1\n'))
if clients:
files.append(('addons/NativeAdrenoMetal.clients', 0o644, ('\n'.join(clients) + '\n').encode()))
if prime_list:
files.append(('addons/NativeAdrenoMetal.primelist', 0o644, prime_list.read_bytes()))
files.append(('bin/adrenoprobed', 0o755, (PLUGIN / 'adrenoprobed').read_bytes()))
for plist in sorted(PLUGIN.glob('*.plist')):
if cache_export: # the plugin sends its cache directory to the UFS log (CacheExport.h)
files.append(('addons/NativeAdrenoMetal.cacheexport', 0o644, b'1\n'))
if pipeline_pack:
files.append(('addons/NativeAdrenoMetal.m2vpack', 0o644, pipeline_pack.read_bytes()))
# --no-daemon: only the plugin (iPadOS 27 runs no probe daemon and keeps its launchd jobs).
if not no_daemon:
files.append(('bin/adrenoprobed', 0o755, (PLUGIN / 'adrenoprobed').read_bytes()))
for plist in sorted(PLUGIN.glob('*.plist')) if not no_daemon else []:
if not plist.name.endswith('-entitlements.plist'): # build input, not a launchd job
files.append(('daemons/' + plist.name, 0o644, plist.read_bytes()))
if wallpaper:
for path in sorted(p for p in wallpaper.rglob('*') if p.is_file()):
files.append(('wallpaper/' + path.relative_to(wallpaper).as_posix(), 0o644, path.read_bytes()))
if floating:
for path in sorted(p for p in floating.rglob('*') if p.is_file()):
files.append(('wallpaper-floating/' + path.relative_to(floating).as_posix(), 0o644, path.read_bytes()))
body = b''
for name, mode, data in files:
encoded = name.encode() + b'\0'
+39 -12
View File
@@ -1,27 +1,46 @@
#!/usr/bin/env python3
"""Build the overlay for the default iPadOS 17 wallpaper's layered animation with PNG images in place of its HEIC ones.
make-wallpaper-overlay.py SYSTEM_ROOT OUTPUT_DIR
make-wallpaper-overlay.py SYSTEM_ROOT OUTPUT_DIR [ipados27|ipados27-floating]
SYSTEM_ROOT is the mounted OS volume of the IPSW (ipsw mount fs). The iPad7,1 variant of wallpaper 7570 (Flat, two image layers) is the
one PosterBoard picks on this device; its images are HEIC, which ImageIO cannot decode without a hardware HEVC decoder. OUTPUT_DIR gets
the animation's index.xml, assetManifest.caml and a main.caml that points at J120.png and J120-Dark.png next to it (converted with sips,
so this runs on macOS, and scaled to EDGE pixels). make-gpu-addon.py --wallpaper OUTPUT_DIR carries it to the kernel, which mounts it over the sealed animation."""
so this runs on macOS, and scaled to EDGE pixels). make-gpu-addon.py --wallpaper OUTPUT_DIR carries it to the kernel, which mounts it over the sealed animation.
With ipados27 the overlay is for iPadOS 27's variant of the same wallpaper (1032w-1376h, a background and a floating layer): its background
layer, whose two images (Background Light/Dark, 1620 x 1080) become Background-Light.png and Background-Dark.png at their own size.
With ipados27-floating it is the same variant's floating layer: its 13 images (blurred colour shapes with alpha) become PNG images, the
ones wider than 1000 pixels at a third of their size (the layers stretch their contents to their bounds; full size does not fit the boot
payload)."""
import subprocess
import sys
from pathlib import Path
ANIMATION = ('Library/Wallpaper/Collections/iPadOS_17~ipad.wallpaperCollection/Wallpapers/7570.iPadOS_17_Flat-1024w-1366h@2x~ipad.wallpaper/'
'7570.iPadOS_17_Flat_Background-1024w-1366h@2x~ipad.ca')
IMAGES = ('J120', 'J120-Dark')
# The HEIC images are 3416 pixels square; PNG at that size does not fit the boot payload. The layers stretch their contents to their bounds.
EDGE = 2732
PROFILES = {
None: (('Library/Wallpaper/Collections/iPadOS_17~ipad.wallpaperCollection/Wallpapers/7570.iPadOS_17_Flat-1024w-1366h@2x~ipad.wallpaper/'
'7570.iPadOS_17_Flat_Background-1024w-1366h@2x~ipad.ca'), {'J120': 'J120', 'J120-Dark': 'J120-Dark'}),
'ipados27': (('Library/Wallpaper/Collections/iPadOS_17~ipad.wallpaperCollection/Wallpapers/7570.iPadOS_17-1032w-1376h@2x~ipad.wallpaper/'
'7570.iPadOS_17_Background-1032w-1376h@2x~ipad.ca'),
{'Background%20Light': 'Background-Light', 'Background%20Dark': 'Background-Dark'}),
'ipados27-floating': (('Library/Wallpaper/Collections/iPadOS_17~ipad.wallpaperCollection/Wallpapers/7570.iPadOS_17-1032w-1376h@2x~ipad.wallpaper/'
'7570.iPadOS_17_Floating-1032w-1376h@2x~ipad.ca'),
{name.replace(' ', '%20'): name.replace(' ', '-') for name in (
'Aqua Dark', 'Aqua Light', 'Green Dark', 'Green Light', 'Orange Dark', 'Orange Light', 'Red Dark', 'Red Light',
'Red Landscape Patch', 'Red Yellow Edge Patch Dark', 'Red Yellow Edge Patch Light', 'Yellow Dark', 'Yellow Light')}),
}
# Longest edge of each profile's PNG images: None keeps the image's own size, a negative value divides images wider than its absolute
# value by three.
EDGES = {None: 2732, # iPadOS 17's HEIC images are 3416 pixels square; PNG at that size does not fit the boot payload
'ipados27': None, 'ipados27-floating': -1000}
def main():
if len(sys.argv) != 3:
if len(sys.argv) not in (3, 4) or len(sys.argv) == 4 and sys.argv[3] not in PROFILES:
sys.exit(__doc__)
source, output = Path(sys.argv[1]) / ANIMATION, Path(sys.argv[2])
profile = sys.argv[3] if len(sys.argv) == 4 else None
animation, images = PROFILES[profile]
edge = EDGES[profile]
source, output = Path(sys.argv[1]) / animation, Path(sys.argv[2])
if not (source / 'main.caml').is_file():
sys.exit('No wallpaper animation at ' + str(source))
if output.exists():
@@ -30,9 +49,17 @@ def main():
for name in ('index.xml', 'assetManifest.caml'):
(output / name).write_bytes((source / name).read_bytes())
main_caml = (source / 'main.caml').read_text()
for image in IMAGES:
main_caml = main_caml.replace(f'"/assets/{image}.HEIC"', f'"/{image}.png"').replace(f'"assets/{image}.HEIC"', f'"{image}.png"')
subprocess.run(['sips', '-Z', str(EDGE), '-s', 'format', 'png', str(source / 'assets' / f'{image}.HEIC'), '--out', str(output / f'{image}.png')],
for image, png in images.items():
main_caml = main_caml.replace(f'"/assets/{image}.HEIC"', f'"/{png}.png"').replace(f'"assets/{image}.HEIC"', f'"{png}.png"')
heic = source / 'assets' / (image.replace('%20', ' ') + '.HEIC')
size = []
if edge and edge > 0:
size = ['-Z', str(edge)]
elif edge:
width = int(subprocess.check_output(['sips', '-g', 'pixelWidth', str(heic)], text=True).split()[-1])
if width > -edge:
size = ['-Z', str(width // 3)]
subprocess.run(['sips', *size, '-s', 'format', 'png', str(heic), '--out', str(output / f'{png}.png')],
check=True, stdout=subprocess.DEVNULL)
if '.HEIC"/>' in main_caml:
sys.exit('main.caml still points at a HEIC image')
+54 -33
View File
@@ -1,45 +1,66 @@
#!/usr/bin/env python3
"""Prepare the public IOStorageFamily headers the built-in block drivers compile against.
The storage ABI audit (scripts/audit-storage-interface.py) also copies these
headers, but it additionally needs the live guest metadata snapshot and the
original kernel candidate. A bring-up build only needs the headers, so this
step keeps the compile independent of that qualification evidence.
"""
"""Prepare pinned public storage headers; this does not qualify a binary family ABI."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / 'refer/IOStorageFamily'
REVISION = 'e45fbe30a7879e0b16e26dddb78538f1b00e0bff'
OUT = ROOT / 'build/storage-abi'
LEGACY_REVISION = 'e45fbe30a7879e0b16e26dddb78538f1b00e0bff'
if not (SOURCE / '.git').is_dir():
raise SystemExit(f'Missing {SOURCE}; run "unixnu fetch IOStorageFamily" first')
revision = subprocess.check_output(['git', '-C', SOURCE, 'rev-parse', 'HEAD'], text=True).strip()
if revision != REVISION:
raise SystemExit(f'Unexpected public IOStorageFamily revision: {revision}')
headers = OUT / 'include/IOKit/storage'
headers.mkdir(parents=True, exist_ok=True)
copied = {}
for path in sorted(SOURCE.glob('*.h')):
target = headers / path.name
shutil.copyfile(path, target)
copied[path.name] = hashlib.sha256(target.read_bytes()).hexdigest()
if not copied:
raise SystemExit('IOStorageFamily has no public headers')
def pinned_source(source, revision):
"""Require the explicit source commit and unmodified tracked inputs."""
actual = subprocess.check_output(['git', '-C', str(source), 'rev-parse', 'HEAD'], text=True).strip()
if len(revision) != 40 or actual != revision:
raise SystemExit(f'{source}: expected full commit {revision}, found {actual}')
dirty = subprocess.check_output(['git', '-C', str(source), 'status', '--porcelain', '--untracked-files=all'], text=True)
if dirty:
raise SystemExit(f'{source}: source checkout must be clean')
return actual
sdk = Path(subprocess.check_output(['xcrun', '--sdk', 'macosx', '--show-sdk-path'], text=True).strip())
conditions = sdk / 'usr/include/TargetConditionals.h'
target = OUT / 'include/TargetConditionals.h'
shutil.copyfile(conditions, target)
report = dict(source_revision=revision, headers=copied,
target_conditionals_sha256=hashlib.sha256(target.read_bytes()).hexdigest(),
qualification=False)
(OUT / 'headers.json').write_text(json.dumps(report, indent=2) + '\n')
print(f'PASS: {len(copied)} public IOStorageFamily headers prepared')
def prepare(source, revision, out):
pinned_source(source, revision)
sdk = Path(subprocess.check_output(['xcrun', '--sdk', 'macosx', '--show-sdk-path'], text=True).strip())
out.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix='headers-', dir=out) as temporary:
include = Path(temporary) / 'include'
headers = include / 'IOKit/storage'
headers.mkdir(parents=True)
copied = {}
for path in sorted(source.glob('*.h')):
target = headers / path.name
shutil.copyfile(path, target)
copied[path.name] = hashlib.sha256(target.read_bytes()).hexdigest()
if not copied:
raise SystemExit(f'{source}: no public IOStorageFamily headers')
target = include / 'TargetConditionals.h'
shutil.copyfile(sdk / 'usr/include/TargetConditionals.h', target)
report = dict(source=str(source.resolve()), source_revision=revision, headers=copied,
sdk=str(sdk), target_conditionals_sha256=hashlib.sha256(target.read_bytes()).hexdigest(),
qualification=False)
# Only replace this builder's generated include tree after all inputs validate.
if (out / 'include').exists():
shutil.rmtree(out / 'include')
shutil.move(str(include), out / 'include')
(out / 'headers.json').write_text(json.dumps(report, indent=2) + '\n')
return report
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', type=Path, default=ROOT / 'refer/IOStorageFamily')
parser.add_argument('--revision', default=os.environ.get('UNIXNU_IOSTORAGEFAMILY_COMMIT', LEGACY_REVISION))
parser.add_argument('--output', type=Path, default=ROOT / 'build/storage-abi')
args = parser.parse_args()
report = prepare(args.source.resolve(), args.revision, args.output.resolve())
print(f'PASS: {len(report["headers"])} public IOStorageFamily headers prepared at {report["source_revision"]}')
if __name__ == '__main__':
main()
+1 -1
View File
@@ -94,7 +94,7 @@ cpu = node('cpu0', [('state', b'running\0'), ('reg', struct.pack('<I', 0)),
tree = node('device-tree', [('model', b'qemu-entry-fixture\0'),
*([('#address-cells', struct.pack('<I', 2)), ('#size-cells', struct.pack('<I', 2))] if virtio_nodes else []),
*([('compatible', args.platform_compatible.encode() + b'\0')] if args.armvirt else [])], [
node('chosen', [('random-seed', os.urandom(64)),
node('chosen', [('random-seed', os.urandom(256)),
# This fixture's writable Data lives only in the owned
# RAMDisk. Publish its actual nonpersistent lifetime via
# XNU's standard hw.ephemeral_storage device-tree input.
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Route the indirect branches of the linked Rust code of an arm64e image through the stripping trampolines.
rust-pac-rewrite.py IMAGE MAP ARCHIVE_NAME
rustc's arm64e code calls function pointers with a plain blr/br, which faults on the signed pointers the rest of an arm64e process hands
out (drivers/AdrenoMetal/bridge/RustPacBoundary.c). Every blr Xn / br Xn inside a function the link map attributes to an object of
ARCHIVE_NAME becomes bl / b to nmb_pac_call_xN (xpaci; br), in place, before the image is signed. blr/br x30 is left alone and counted."""
import re
import struct
import sys
from pathlib import Path
if len(sys.argv) != 4:
sys.exit(__doc__)
image, link_map, archive = Path(sys.argv[1]), Path(sys.argv[2]).read_text(errors='replace'), sys.argv[3]
data = bytearray(image.read_bytes())
files, text, symbols, part = set(), None, [], None
for line in link_map.splitlines():
if line.startswith('# Object files:'): part = 'files'; continue
if line.startswith('# Sections:'): part = 'sections'; continue
if line.startswith('# Symbols:'): part = 'symbols'; continue
if line.startswith('# Dead Stripped'): part = None; continue
if line.startswith('#') or not line.strip(): continue
if part == 'files':
m = re.match(r'\[\s*(\d+)\]\s+(.*)$', line)
if m and archive in m.group(2): files.add(int(m.group(1)))
elif part == 'sections':
f = line.split()
if len(f) >= 4 and f[2] == '__TEXT' and f[3] == '__text': text = (int(f[0], 16), int(f[1], 16))
elif part == 'symbols':
m = re.match(r'(0x[0-9A-Fa-f]+)\s+(0x[0-9A-Fa-f]+)\s+\[\s*(\d+)\]\s+(.*)$', line)
if m: symbols.append((int(m.group(1), 16), int(m.group(2), 16), int(m.group(3)), m.group(4)))
if not files or not text:
sys.exit('the map names no object of %s or no __TEXT,__text' % archive)
tramp = {}
for address, size, index, name in symbols:
m = re.fullmatch(r'_nmb_pac_call_x(\d+)', name)
if m: tramp[int(m.group(1))] = address
if sorted(tramp) != list(range(30)):
sys.exit('trampolines missing: %s' % sorted(set(range(30)) - set(tramp)))
# __TEXT segment: virtual address to file offset.
ncmds = struct.unpack_from('<I', data, 16)[0]
at, segment = 32, None
for _ in range(ncmds):
cmd, size = struct.unpack_from('<II', data, at)
if cmd == 0x19 and data[at + 8:at + 24].rstrip(b'\0') == b'__TEXT':
vmaddr, vmsize, fileoff = struct.unpack_from('<QQQ', data, at + 24)
segment = (vmaddr, vmsize, fileoff)
at += size
vmaddr, vmsize, fileoff = segment
lo, hi = text[0], text[0] + text[1]
counts = {'blr': 0, 'br': 0, 'x30': 0}
for address, size, index, name in symbols:
if index not in files or not lo <= address < hi:
continue
for pc in range(address, min(address + size, hi), 4):
off = pc - vmaddr + fileoff
word = struct.unpack_from('<I', data, off)[0]
kind = {0xD63F0000: 'blr', 0xD61F0000: 'br'}.get(word & 0xFFFFFC1F)
if not kind:
continue
reg = (word >> 5) & 31
if reg == 30:
counts['x30'] += 1
continue
delta = (tramp[reg] - pc) >> 2
if not -(1 << 25) <= delta < (1 << 25):
sys.exit('trampoline out of branch range at %#x' % pc)
struct.pack_into('<I', data, off, (0x94000000 if kind == 'blr' else 0x14000000) | (delta & 0x3FFFFFF))
counts[kind] += 1
image.write_bytes(data)
print('PASS: Rust indirect branches routed through the PAC trampolines: %d blr, %d br, %d x30 left (%d objects)'
% (counts['blr'], counts['br'], counts['x30'], len(files)))
+86 -5
View File
@@ -4,6 +4,10 @@
Implements the source fipspost_get_hmac/hmacfiletool build contract: HMAC-SHA256
with a one-byte zero key over the first __TEXT[ _EXEC ]/__text section.
No selftest or integrity check is disabled. Only locally compiled input is used.
static_if_init rewrites __text before the POST runs. It stores absolute instructions (b or nop), so the
sites of keys that boot flips are written here in their final state and the boot rewrite leaves the
hashed bytes unchanged. --kernel-args names the boot arguments the kernel starts with.
"""
import argparse
import hashlib
@@ -16,19 +20,47 @@ from kernel_target import ARCH, SUBTYPE
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('kernel', type=Path)
parser.add_argument('--kernel-args', default='', help='boot arguments, to refuse ones that flip more static_if keys')
args = parser.parse_args()
# kern_feature_override_apply flips assert and sched debug keys only for these arguments.
for argument in args.kernel_args.split():
if argument.lstrip('-').split('=', 1)[0] in ('validation_disables', 'serverperfmode', 'lcks'):
raise SystemExit(f'{argument} flips static_if keys at boot that this seal does not model')
data = bytearray(args.kernel.read_bytes())
magic, cpu, subtype, kind = struct.unpack_from('<4I', data)
header = 0
if (magic, cpu, subtype & 0xff) == (0xfeedfacf, 0x100000c, SUBTYPE) and kind == 12:
# A boot kernel collection: seal its com.apple.kernel entry. Section offsets are collection file offsets.
# The emitter moves the native text, so the HMAC is computed here, after emission. The static_if sites
# were prepatched when the native kernel was sealed before emission.
offset = 32
for _ in range(struct.unpack_from('<I', data, 16)[0]):
cmd, length = struct.unpack_from('<II', data, offset)
if cmd == 0x80000035:
name = struct.unpack_from('<I', data, offset + 24)[0]
if data[offset + name:data.index(b'\0', offset + name)] == b'com.apple.kernel':
header = struct.unpack_from('<Q', data, offset + 16)[0]
offset += length
if not header:
raise SystemExit('Kernel collection has no com.apple.kernel entry')
magic, cpu, subtype, kind = struct.unpack_from('<4I', data, header)
if (magic, cpu, subtype & 0xff, kind) != (0xfeedfacf, 0x100000c, SUBTYPE, 2):
raise SystemExit(f'Expected a source-built {ARCH} MH_EXECUTE kernel')
raise SystemExit(f'Expected a source-built {ARCH} MH_EXECUTE kernel or a collection of one')
if b'_source_corecrypto14_bootstrap\0' not in data:
raise SystemExit('Input does not contain the explicit source-provider bootstrap')
offset = 32
offset = header + 32
code = None
integrity = None
segments = []
for _ in range(struct.unpack_from('<I', data, 16)[0]):
sections = {}
symtab = None
chained = False
for _ in range(struct.unpack_from('<I', data, header + 16)[0]):
cmd, length = struct.unpack_from('<II', data, offset)
if cmd == 0x80000034:
chained = True
if cmd == 0x2:
symtab = struct.unpack_from('<4I', data, offset + 8)
if cmd == 0x19:
name, va, _, fileoff, _ = struct.unpack_from('<16s4Q', data, offset + 8)
name = name.rstrip(b'\0')
@@ -36,6 +68,7 @@ for _ in range(struct.unpack_from('<I', data, 16)[0]):
for index in range(struct.unpack_from('<I', data, offset + 64)[0]):
section = offset + 72 + index * 80
section_name, segment_name, address, size, position = struct.unpack_from('<16s16sQQI', data, section)
sections[(segment_name.rstrip(b'\0'), section_name.rstrip(b'\0'))] = (address, size, position)
if section_name.rstrip(b'\0') == b'__text' and segment_name.rstrip(b'\0') in (b'__TEXT', b'__TEXT_EXEC') and code is None:
code = (position, size, address)
if section_name.rstrip(b'\0') == b'__fips_hmacs':
@@ -46,15 +79,63 @@ for _ in range(struct.unpack_from('<I', data, 16)[0]):
if code is None or integrity is None:
raise SystemExit('Missing real provider code or POST HMAC field')
base = next(va for name, va, _ in segments if name == b'__TEXT')
if code[2] - base != code[0]:
if code[2] - base != code[0] - header:
raise SystemExit('Runtime header-relative text mapping differs from the POST file-offset contract')
if code[0] <= integrity < code[0] + code[1]:
raise SystemExit('HMAC field must be outside the hashed instructions')
def symbols(names):
found = {}
if symtab:
symoff, nsyms, stroff, _ = symtab
for index in range(nsyms):
strx, kind, _, _, value = struct.unpack_from('<IBBHQ', data, symoff + index * 16)
name = bytes(data[stroff + strx:data.index(b'\0', stroff + strx)])
if name in names and kind & 0x0e == 0x0e:
found[name] = value
return found
# osfmk/tests/kernel_tests.c static_if_tests_setup: unconditional, one disable and one enable.
flips = [b'_static_if_test_key_true_to_false_jump_key', b'_static_if_test_key_false_to_true_jump_key']
present = symbols({b'_static_if_tests_setup', *flips})
patched = 0
if b'_static_if_tests_setup' in present and not header:
# Threaded rebases keep the key's low address bits; chained kernel-cache pointers keep a 30-bit offset.
target_mask = 0x3fffffff if chained else 0x7ffffffffff
keys = {(present[name] - (base if chained else 0)) & target_mask for name in flips if name in present}
if len(keys) != len(flips):
raise SystemExit('static_if test initializer present without its keys')
entries = sections.get((b'__DATA_CONST', b'__static_if')) or sections.get((b'__BOOTDATA', b'__static_if'))
if entries is None:
raise SystemExit('static_if test initializer present without the entry section')
address, size, position = entries
for entry in range(0, size, 16):
delta, target, link = struct.unpack_from('<iiQ', data, position + entry)
# The link is a rebase of key + flags; bit 0 says the site was emitted as a branch.
if (link & target_mask & ~3) not in keys:
continue
point = address + entry + delta
site = code[0] + point - code[2]
if not code[2] <= point < code[2] + code[1]:
continue
current = struct.unpack_from('<I', data, site)[0]
branch = 0x14000000 | ((target >> 2) & 0x03ffffff)
expected, final = (branch, 0xd503201f) if link & 1 else (0xd503201f, branch)
if current == final:
# Sealed before: the kernel build seals, and packaging seals again with the boot arguments.
patched += 1
continue
if current != expected:
raise SystemExit(f'static_if site {point:#x} holds {current:#010x}, not the emitted instruction')
struct.pack_into('<I', data, site, final)
patched += 1
mac = hmac.digest(b'\0', data[code[0]:code[0] + code[1]], 'sha256')
data[integrity:integrity + 32] = mac
args.kernel.write_bytes(data)
report = {'kernel_sha256': hashlib.sha256(data).hexdigest(), 'code_file_offset': hex(code[0]),
'code_bytes': code[1], 'hmac_file_offset': hex(integrity), 'hmac_sha256': mac.hex(),
'runtime_selftests_executed': False}
'static_if_sites_prepatched': patched, 'runtime_selftests_executed': False}
args.kernel.with_suffix(args.kernel.suffix + '.crypto-hmac.json').write_text(json.dumps(report, indent=2) + '\n')
print('Source kernel POST integrity field populated; runtime selftests are still required')
+8 -2
View File
@@ -2,7 +2,7 @@
"""Make a Data seed that boots to SpringBoard instead of Setup: the preference changes github.com/dipa-studio/ios-sweet
(tools/sweet-root-tweaks.py) established on hardware, applied to a copy of an existing packed Data seed.
seed-data-skip-setup.py BASE.dbr OUTPUT.dbr [--no-auto-dim]
seed-data-skip-setup.py BASE.dbr OUTPUT.dbr [--no-auto-dim] [--build=BUILD]
BASE is a DBRDISK1 packed APFS container (drivers/DarwinBoot/Test/packed_ramdisk.py format). It is expanded to a raw image, attached
read-write on this Mac, the preference files below are merged into /private/var/mobile/Library/Preferences, and the result is packed
@@ -13,6 +13,8 @@ again. The volume UUID is unchanged. Nothing is written to Git or to the OS inpu
com.apple.springboard BKNoWatchdogs, FBLaunchWatchdogExceptions{com.apple.purplebuddy: 100}, MCForceShowCloudConfigurationUI false
com.apple.migration LastSystemVersion (no data migration on every boot)
and the container directories under /private/var/containers that on a device live on the data volume.
--build names the OS build the seed boots (default 21H461): LastSystemVersion must equal it, or the system migrates its data on
the first boot (the progress bar under the Apple logo).
--no-auto-dim also sets com.apple.springboard SBDisableAutoDim, SBDontDimOrLockOnAC and SBDisableIdleSleep (as UniXNU-qemu's
build-os-root.py does): a board without input would otherwise turn its display off about 30 seconds after SpringBoard starts
and put the system to sleep.
@@ -37,8 +39,12 @@ def merge_pref(var, name, values):
print('pref', name, sorted(values))
def main():
global BUILD
no_auto_dim = '--no-auto-dim' in sys.argv[1:]
positional = [arg for arg in sys.argv[1:] if arg != '--no-auto-dim']
for arg in sys.argv[1:]:
if arg.startswith('--build='):
BUILD = arg.split('=', 1)[1]
positional = [arg for arg in sys.argv[1:] if arg != '--no-auto-dim' and not arg.startswith('--build=')]
if len(positional) != 2:
sys.exit(__doc__)
base, output = Path(positional[0]).resolve(), Path(positional[1]).resolve()
+4
View File
@@ -26,6 +26,7 @@ def common():
p.add_argument('--uefi', metavar='DIR', help='Mu-Silicium checkout for the device (uefi-sheng); or UNIXNU_UEFI')
p.add_argument('--edk2', metavar='DIR', help='Mu_Basecore checkout; or UNIXNU_EDK2')
p.add_argument('--kernelcache', metavar='FILE', help='original decompressed kernelcache; or UNIXNU_KERNELCACHE')
p.add_argument('--skip-prelink', action='store_true', help='kernel/prelink: copy the built native kernel to kernel.macho without adding kernelcache kexts')
p.add_argument('--skip-preflight', action='store_true', help='skip the board preflight scripts of the firmware step')
p.add_argument('--kernel', metavar='FILE', help='prelinked kernel Mach-O for the payload (default: the kernel step output)')
p.add_argument('--kernel-arg', action='append', metavar='NAME[=VALUE]', help='extra kernel boot argument (repeatable)')
@@ -37,6 +38,9 @@ def common():
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('--dry-run', action='store_true', help='userdata: check sizes and pins, write nothing')
p.add_argument('--object', action='append', default=[], metavar='NAME=FILE[:OFFSET]',
help='userdata: add a data object the kernel reads itself (policy table, firmware), '
'the bytes of FILE from OFFSET on (repeatable)')
p.add_argument('--target', choices=('DEBUG', 'RELEASE'), help='EDK II build target')
return p
+4 -2
View File
@@ -287,7 +287,7 @@ def platform_node(name, table):
return node(name, properties, children)
def generate(dtb, config, *, unique_chip_id=None, data=False, trust=False, addon=False):
def generate(dtb, config, *, unique_chip_id=None, data=False, trust=False, addon=False, root_policies=False):
root = read_fdt(Path(dtb).read_bytes())
entries = list(root.walk())
handles = {}
@@ -305,7 +305,8 @@ def generate(dtb, config, *, unique_chip_id=None, data=False, trust=False, addon
dram_base, dram_size = registers(memory[0])[0]
if payload.get('dram_size') is not None and int(str(payload['dram_size']), 0) != dram_size:
raise Error('payload.dram_size differs from the hardware DTS')
chosen = [('random-seed', os.urandom(64)), ('dram-base', struct.pack('<Q', dram_base)),
# xnu-12377 takes 4 x 64 seed bytes on ARM and panics with fewer; older kernels read the first 64.
chosen = [('random-seed', os.urandom(256)), ('dram-base', struct.pack('<Q', dram_base)),
('dram-size', struct.pack('<Q', dram_size)), *empty_nvram_proxy()]
if unique_chip_id:
seed = int(str(unique_chip_id), 16)
@@ -335,6 +336,7 @@ def generate(dtb, config, *, unique_chip_id=None, data=False, trust=False, addon
mappings = [('PackedRAMDisk' if options.get('packed_data_ramdisk') else 'RAMDisk', bytes(16))] if data else []
mappings += [('TrustCache', bytes(16))] if trust else []
mappings += [('GpuAddon', bytes(16))] if addon else []
mappings += [('RootPolicies', bytes(16))] if root_policies else []
children = [node('chosen', chosen, [node('memory-map', mappings), node('manifest-properties')])]
cpus = next((entry for entry in root.children if entry.name == 'cpus'), None)
if cpus is None:
+84 -14
View File
@@ -1,10 +1,16 @@
"""XNU kernel: build from the patched Apple source, then prelink against the original kernelcache."""
import os
import hashlib
import json
import platform
import shlex
import shutil
import struct
import tempfile
from pathlib import Path
from . import components, ipsw, patches
from .config import load_toml
from .util import Error, expand, log, run, sha256_file, tool
@@ -23,7 +29,7 @@ def xnu_env(ctx):
def version_env(modules, ctx=None):
"""Source versions of the Apple references, from feeds.toml, for the kernel scripts."""
env = {}
for name, prefix in (('xnu', 'XNU'), ('libplatform', 'LIBPLATFORM'), ('libdispatch', 'LIBDISPATCH'), ('AvailabilityVersions', 'AVAILABILITYVERSIONS'), ('libpthread', 'LIBPTHREAD')):
for name, prefix in (('xnu', 'XNU'), ('libplatform', 'LIBPLATFORM'), ('libdispatch', 'LIBDISPATCH'), ('AvailabilityVersions', 'AVAILABILITYVERSIONS'), ('libpthread', 'LIBPTHREAD'), ('IOStorageFamily', 'IOSTORAGEFAMILY')):
module = modules.get(('refer', name))
entry = module.source if module and module.source else {}
if entry.get('ref'):
@@ -46,7 +52,20 @@ def version_env(modules, ctx=None):
env['UNIXNU_XNU_TAG'] = 'xnu-' + profile['xnu_version']
if profile.get('ios_version'):
env['UNIXNU_IOS_VERSION'] = profile['ios_version']
env['UNIXNU_XNU_ARCH'] = ctx.board.get('arch', {}).get('name', 'arm64')
# The root images of the OS: volume UUIDs and image digests the kernel and the storage builder check.
root = ctx.board.get('root', {})
for key, variable in (('os_volume_uuid', 'XNU_OWNER_ROOT_VOLUME_UUID'),
('systemos_volume_uuid', 'XNU_OWNER_CACHE_VOLUME_UUID'),
('os_image_sha256', 'UNIXNU_ROOT_OS_SHA256'),
('systemos_image_sha256', 'UNIXNU_ROOT_SYSTEMOS_SHA256')):
if root.get(key):
env[variable] = root[key]
if root.get('os_volume_uuid'):
env['UNIXNU_ROOT_OS_UUID'] = root['os_volume_uuid']
if root.get('systemos_volume_uuid'):
env['UNIXNU_ROOT_SYSTEMOS_UUID'] = root['systemos_volume_uuid']
env['UNIXNU_IPSW_BUILD'] = ctx.board.get('ipsw', {}).get('build', '')
env['UNIXNU_XNU_ARCH'] = load_toml(ctx.profile_roots['arch'] / 'profile.toml').get('arch', {}).get('name', 'arm64')
return env
@@ -183,8 +202,9 @@ def build(ctx, modules):
except Error:
if not record.is_file():
raise
storage = ctx.tree / 'build/storage-abi/include/IOKit/storage'
if (ctx.tree / 'refer/IOStorageFamily').is_dir() and not storage.is_dir():
# Revalidate the selected source and regenerate headers on every archive build.
# A pre-existing directory can belong to a different OS profile/revision.
if (ctx.tree / 'refer/IOStorageFamily').is_dir():
log('kernel: prepare storage headers')
run(['python3', ctx.tree / 'scripts/prepare-storage-headers.py'], cwd=ctx.tree, env=env,
log_file=ctx.logs / 'storage-headers.log')
@@ -192,12 +212,42 @@ def build(ctx, modules):
run(['bash', script], cwd=ctx.tree, env=env, log_file=ctx.logs / 'kernel.log')
def stage_native(kernel, output):
"""Stage the native executable unchanged for bring-up without prelinked kexts."""
data = kernel.read_bytes()
if len(data) < 32 or struct.unpack_from('<II', data) != (0xfeedfacf, 0x100000c) or struct.unpack_from('<I', data, 12)[0] != 2:
raise Error(f'{kernel} is not an ARM64 MH_EXECUTE native kernel')
digest = hashlib.sha256(data).hexdigest()
record = {'mode': 'native-only', 'format': 'MH_EXECUTE', 'native': str(kernel.resolve()),
'native_sha256': digest, 'candidate_sha256': digest, 'candidate_bytes': len(data),
'prelink_skipped': True, 'kernelcache_kexts_added': False, 'boot_verified': False}
output.parent.mkdir(parents=True, exist_ok=True)
for path, contents in ((output, data), (output.with_suffix('.json'), (json.dumps(record, indent=2) + '\n').encode())):
name = None
try:
with tempfile.NamedTemporaryFile(dir=path.parent, prefix='.' + path.name + '.', delete=False) as stream:
name = stream.name
stream.write(contents)
os.replace(name, path)
finally:
if name and os.path.exists(name):
os.unlink(name)
log(f'kernel: native only {output} sha256 {digest}')
return output
def prelink(ctx):
"""Produce out/<device>/kernel/kernel.macho from the XNU build and the original kernelcache."""
log('kernel: prelink')
spec = ctx.board.get('kernel', {}).get('prelink')
if not spec:
raise Error('board.toml has no [kernel.prelink]')
kernel = ctx.tree / spec['kernel']
if not kernel.is_file():
raise Error(f'{kernel} not found; run "unixnu build kernel" first')
output = ctx.work / 'kernel/kernel.macho'
if getattr(ctx.args, 'skip_prelink', False):
return stage_native(kernel, output)
log('kernel: prelink')
cache = ctx.option('kernelcache', 'UNIXNU_KERNELCACHE') or (expand(ctx.tree / spec['kernelcache']) if spec.get('kernelcache') else None)
if not cache:
# Not given: take it from the IPSW (--ipsw PATH_OR_URL, or the url of board.toml [ipsw]).
@@ -207,21 +257,41 @@ def prelink(ctx):
wanted = spec.get('kernelcache_sha256')
if wanted and sha256_file(cache) != wanted:
raise Error(f'{cache} does not match the kernelcache this board was qualified against (sha256 {wanted})')
kernel = ctx.tree / spec['kernel']
if not kernel.is_file():
raise Error(f'{kernel} not found; run "unixnu build kernel" first')
output = ctx.work / 'kernel/kernel.macho'
output.parent.mkdir(parents=True, exist_ok=True)
# emit_prelink refuses to overwrite; a rebuild replaces the previous candidate and its record.
for stale in (output, output.with_suffix('.json')):
stale.unlink(missing_ok=True)
command = ['python3', ctx.tree / spec['tool'], '--kernel', kernel, '--prelinked', cache,
f'--slide={spec["slide"]}', '--output', output]
f'--slide={spec["slide"]}']
for root in spec.get('roots', []):
command += ['--root', root]
if spec.get('early_security_init'):
command += ['--early-security-init', spec['early_security_init']]
command += spec.get('extra_args', [])
run(command, cwd=ctx.tree, log_file=ctx.logs / 'prelink.log')
# A failed rebuild must not remove the last candidate or its record.
with tempfile.TemporaryDirectory(dir=output.parent, prefix='.prelink-') as directory:
candidate = Path(directory) / output.name
saved_map = output.with_suffix('.map.json')
candidate_map = candidate.with_suffix('.map.json')
if saved_map.is_file():
shutil.copyfile(saved_map, candidate_map)
success = False
try:
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')
success = True
finally:
if candidate_map.is_file():
os.replace(candidate_map, saved_map)
record_path = candidate.with_suffix('.json')
if record_path.is_file():
record = json.loads(record_path.read_text())
record['output'] = str(output.resolve())
if record.get('map') == str(candidate_map.resolve()):
record['map'] = str(saved_map.resolve())
record_path.write_text(json.dumps(record, indent=2) + '\n')
destination = output.with_suffix('.json' if success else '.failed.json')
os.replace(record_path, destination)
if not success:
log(f'kernel: prelink failed; diagnostic record {destination}')
os.replace(candidate, output)
log(f'kernel: {output} sha256 {sha256_file(output)}')
return output
+32 -5
View File
@@ -16,6 +16,7 @@ import shutil
import tempfile
from pathlib import Path
from . import kernel as kernel_step
from .util import Error, expand, log, run, sha256_file, tool
EPOCH = 315532800 # 1980-01-01, the first FAT timestamp; keeps the volume reproducible
@@ -37,8 +38,12 @@ def check_args(arguments):
return line
def check_native_root(kernel, arguments):
"""Refuse the known bare-metal AMFI panic before the volume is written."""
def check_native_root(kernel, arguments, vmm_present=False):
"""Refuse the known bare-metal AMFI panic before the volume is written.
A board whose device tree declares vmm-present has no PMGR for AMFI to wait for; it needs no PMGR stand-in."""
if vmm_present:
return
values = {}
for argument in arguments:
key, separator, value = argument.partition('=')
@@ -96,7 +101,7 @@ def build(ctx, kernel=None):
arguments = [item for item in spec.get('kernel_args', []) if item.split('=', 1)[0] not in names] + extra
line = check_args(arguments)
log(f'kernel args: {line}')
check_native_root(kernel, arguments)
check_native_root(kernel, arguments, bool(ctx.board.get('adt', {}).get('defaults', {}).get('u32', {}).get('vmm-present')))
owner_trust = getattr(args, 'owner_trust_cache', None)
trust = expand(owner_trust or args.trust_cache) if (owner_trust or getattr(args, 'trust_cache', None)) else None
if trust:
@@ -117,6 +122,19 @@ def build(ctx, kernel=None):
addon = expand(args.gpu_addon) if getattr(args, 'gpu_addon', None) else None
if addon and (not addon.is_file() or addon.read_bytes()[:8] != b'NGPUADD2'):
raise Error(f'GPU addon not found or not made by scripts/make-gpu-addon.py: {addon}')
policy_path = spec.get('root_policies')
policies = expand(policy_path.format(tree=ctx.tree, out=ctx.work)) if policy_path else None
if policies:
if ctx.schema_version != 2:
raise Error('Root-policy handoff requires a format 2 device tree')
if not policies.is_file() or not 32 <= policies.stat().st_size <= 128 * 1024 * 1024:
raise Error(f'Root-policy bundle missing or outside handoff bounds: {policies}')
with policies.open('rb') as stream:
policy_header = stream.read(32)
if (policy_header[:8] != b'VROOTB01' or int.from_bytes(policy_header[8:16], 'little') != 1 or
not 1 <= int.from_bytes(policy_header[16:24], 'little') <= 16 or
int.from_bytes(policy_header[24:32], 'little') != policies.stat().st_size):
raise Error(f'Invalid root-policy bundle header: {policies}')
seed = getattr(args, 'unique_chip_id', None) or os.environ.get('UNIXNU_UNIQUE_CHIP_ID')
adt = spec.get('adt', {})
if adt.get('unique_chip_id') == 'required' and not seed:
@@ -141,17 +159,25 @@ def build(ctx, kernel=None):
probe.append('--reset-before')
run(['python3', ctx.tree / 'scripts/patch-xnu-entry-probe.py', payloads / 'kernel.macho', *screen, *probe],
cwd=ctx.tree, env=dict(os.environ, UNIXNU_UEFI=str(ctx.uefi or '')), log_file=ctx.logs / 'payload-patch.log')
# The probes rewrite instructions inside __text, which the source corecrypto POST hashes.
if b'_source_corecrypto14_bootstrap\0' in Path(kernel).read_bytes():
boot_args = ' '.join([line, *spec.get('install', {}).get('confirm_args', [])])
run(['python3', ctx.tree / 'scripts/seal-source-crypto-kernel.py', payloads / 'kernel.macho', '--kernel-args', boot_args],
cwd=ctx.tree, env=dict(os.environ, **kernel_step.version_env({}, ctx)), log_file=ctx.logs / 'payload-seal.log')
if trust:
shutil.copyfile(trust, payloads / 'kernel.macho.trustcache')
if addon:
shutil.copyfile(addon, payloads / 'kernel.macho.gpuaddon')
if policies:
shutil.copyfile(policies, payloads / 'kernel.macho.vroot')
if data:
shutil.copyfile(data, payloads / 'scratch-data.img')
if ctx.schema_version == 2:
from .devicetree import compile_hardware, generate
dtb = compile_hardware(ctx)
(payloads / 'kernel.macho.adt').write_bytes(generate(
dtb, ctx.board, unique_chip_id=seed, data=bool(data), trust=bool(trust), addon=bool(addon)))
dtb, ctx.board, unique_chip_id=seed, data=bool(data), trust=bool(trust), addon=bool(addon),
root_policies=bool(policies)))
else:
command = ['python3', ctx.tree / adt['tool'], '--output', payloads / 'kernel.macho.adt', '--dram-size', str(spec['dram_size'])]
for key, flag in (('owned_memory_size', '--owned-memory-size'), ('root_match_volume_uuid', '--root-match-volume-uuid'),
@@ -184,7 +210,8 @@ def build(ctx, kernel=None):
install = spec.get('install')
if install:
if not data:
raise Error('board.toml [payload.install] needs a Data seed: pass --data-ramdisk FILE')
# A bring-up OS may have no seed yet: the entry still boots with the confirmation's arguments.
log('payload: [payload.install] without --data-ramdisk; the install entry carries no Data seed')
titles, extra = install.get('confirm_titles', []), install.get('confirm_args', [])
if not install.get('confirm') or not titles or len(titles) != len(extra):
raise Error('board.toml [payload.install] needs confirm and equally long confirm_titles and confirm_args')
+98
View File
@@ -0,0 +1,98 @@
"""Native-only kernel staging must not fetch or prelink the original kernelcache."""
import hashlib
import json
from pathlib import Path
import struct
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from unixnu import kernel
from unixnu.cli import parser
from unixnu.util import Error
class NativeKernelTests(unittest.TestCase):
def test_prelink_failure_preserves_previous_output_and_keeps_diagnostics(self):
self.check_prelink_publication(fail=True)
def test_prelink_success_publishes_image_and_relocates_record_paths(self):
self.check_prelink_publication(fail=False)
def check_prelink_publication(self, fail):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source, cache = root / 'native', root / 'cache'
source.write_bytes(b'native')
cache.write_bytes(b'cache')
ctx = SimpleNamespace(tree=root, work=root, logs=root, args=SimpleNamespace(skip_prelink=False),
option=lambda *args: cache,
board={'kernel': {'prelink': {'kernel': 'native', 'tool': 'emit.py', 'slide': '0'}}})
output = root / 'kernel/kernel.macho'
output.parent.mkdir()
output.write_bytes(b'old image')
output.with_suffix('.json').write_text('{"old": true}')
output.with_suffix('.map.json').write_text('{"cached": true}')
def emit(command, **kwargs):
self.assertEqual(output.read_bytes(), b'old image')
candidate = command[command.index('--output') + 1]
self.assertNotEqual(candidate, output)
self.assertFalse(candidate.exists())
self.assertEqual(json.loads(candidate.with_suffix('.map.json').read_text()), {'cached': True})
candidate.with_suffix('.json').write_text(json.dumps({
'output': str(candidate.resolve()), 'map': str(candidate.with_suffix('.map.json').resolve())}))
candidate.write_bytes(b'new image')
if fail:
raise Error('incomplete pairing')
with patch.object(kernel, 'run', side_effect=emit):
if fail:
with self.assertRaisesRegex(Error, 'incomplete pairing'):
kernel.prelink(ctx)
self.assertEqual(output.read_bytes(), b'old image')
self.assertEqual(json.loads(output.with_suffix('.json').read_text()), {'old': True})
else:
self.assertEqual(kernel.prelink(ctx), output)
self.assertEqual(output.read_bytes(), b'new image')
report = json.loads(output.with_suffix('.failed.json' if fail else '.json').read_text())
self.assertEqual(report['output'], str(output.resolve()))
self.assertEqual(report['map'], str(output.with_suffix('.map.json').resolve()))
self.assertEqual(list(output.parent.glob('.prelink-*')), [])
def test_skip_prelink_keeps_bytes_and_does_not_fetch_ipsw(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
data = struct.pack('<8I', 0xfeedfacf, 0x100000c, 2, 2, 0, 0, 0, 0) + b'native seal'
source = root / 'native'
source.write_bytes(data)
ctx = SimpleNamespace(tree=root, work=root / 'out',
args=parser().parse_args(['prelink', '--skip-prelink']),
board={'kernel': {'prelink': {'kernel': 'native'}}})
with patch.object(kernel.ipsw, 'Ipsw', side_effect=AssertionError('must not fetch')):
output = kernel.prelink(ctx)
self.assertEqual(output.read_bytes(), data)
report = json.loads(output.with_suffix('.json').read_text())
self.assertTrue(report['prelink_skipped'])
self.assertFalse(report['kernelcache_kexts_added'])
self.assertEqual(report['candidate_sha256'], hashlib.sha256(data).hexdigest())
def test_bad_input_preserves_previous_image_and_record(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source, output = root / 'input', root / 'kernel.macho'
output.write_bytes(b'previous')
output.with_suffix('.json').write_text('{}')
for data in (b'bad', struct.pack('<8I', 0xfeedfacf, 0x100000c, 2, 12, 0, 0, 0, 0)):
source.write_bytes(data)
with self.assertRaisesRegex(Error, 'MH_EXECUTE'):
kernel.stage_native(source, output)
self.assertEqual(output.read_bytes(), b'previous')
self.assertEqual(output.with_suffix('.json').read_text(), '{}')
if __name__ == '__main__':
unittest.main()
+39 -6
View File
@@ -7,9 +7,13 @@ It holds the unchanged OS and SystemOS disk images of the IPSW, byte for byte, b
0x100000 window "OS" the whole attached OS disk image, 1 MiB aligned
.... window "SystemOS" the whole attached SystemOS disk image, 1 MiB aligned
Manifest (little endian): "SHNGROOT", u32 version=1, u32 count, u64 image_bytes, then `count` 128-byte entries
{char name[32], u64 offset, u64 size, u32 block_size=512, u32 flags(1=read-only), u8 sha256[32], 40 reserved},
and at byte 4064 the SHA-256 of bytes 0..4063.
.... data objects (--object NAME=FILE[:OFFSET]), each 1 MiB aligned
Manifest (little endian): "SHNGROOT", u32 version, u32 count, u64 image_bytes, then `count` 128-byte entries
{char name[32], u64 offset, u64 size, u32 block_size, u32 flags, u8 sha256[32], 40 reserved},
and at byte 4064 the SHA-256 of bytes 0..4063. A window has block_size 512 and flags 1 (read-only block device).
Version 2 adds data objects with block_size 4096 and flags 3: the kernel reads them itself and never publishes
them as block devices (the hash tables of header-only root policies, firmware). Version 1 holds windows only.
The OS profile lists the windows ([userdata] windows; optional ones are skipped when the IPSW lacks them) and the
device sets the partition geometry. The images come from the IPSW (a local file or a remote URL, see ipsw.py),
@@ -24,6 +28,7 @@ import plistlib
import re
import struct
import subprocess
from pathlib import Path
from . import udif
from .ipsw import Ipsw
@@ -110,6 +115,21 @@ def build(ctx, dry_run=False):
size = image_size(dmg)
layout.append((name, cursor, size))
cursor += (size + MIB - 1) // MIB * MIB
objects = []
for spec_text in getattr(ctx.args, 'object', None) or []:
name, sep, rest = spec_text.partition('=')
file_text, _, skip_text = rest.partition(':')
path = Path(file_text).expanduser()
if not sep or not name or len(name.encode()) > 31 or not path.is_file():
raise Error(f'--object {spec_text}: expected NAME=FILE[:OFFSET] with an existing file and a name under 32 bytes')
skip = int(skip_text, 0) if skip_text else 0
size = path.stat().st_size - skip
if skip < 0 or size <= 0:
raise Error(f'--object {spec_text}: offset outside the file')
objects.append((name, cursor, size, path, skip))
cursor += (size + MIB - 1) // MIB * MIB
if len(layout) + len(objects) > 8 or len(layout) > 4 or len(objects) > 4:
raise Error('the root manifest holds at most 4 windows and 4 data objects')
total = cursor
log(f'userdata: image {total} bytes ({total / 2**30:.2f} GiB) in a partition of {capacity} bytes')
if total > capacity:
@@ -134,16 +154,29 @@ def build(ctx, dry_run=False):
raise Error(f'{dmg.name}: {done} bytes of raw disk, expected {size}')
entries.append((name, offset, size, digest.digest()))
log(f'userdata: {name} {size} bytes at {offset:#x} sha256 {digest.hexdigest()}')
object_entries = []
for name, offset, size, path, skip in objects:
with path.open('rb') as handle:
handle.seek(skip)
data = handle.read(size)
if len(data) != size:
raise Error(f'{path}: short read for object {name}')
target.seek(offset)
target.write(data)
object_entries.append((name, offset, size, hashlib.sha256(data).digest()))
log(f'userdata: object {name} {size} bytes at {offset:#x} from {path.name}')
header = bytearray(4096)
header[0:8] = b'SHNGROOT'
struct.pack_into('<IIQ', header, 8, 1, len(entries), total)
for index, (name, offset, size, digest) in enumerate(entries):
struct.pack_into('<32sQQII32s40s', header, 24 + 128 * index, name.encode(), offset, size, 512, 1, digest, bytes(40))
struct.pack_into('<IIQ', header, 8, 2 if object_entries else 1, len(entries) + len(object_entries), total)
rows = [(e, 512, 1) for e in entries] + [(e, 4096, 3) for e in object_entries]
for index, ((name, offset, size, digest), block, flags) in enumerate(rows):
struct.pack_into('<32sQQII32s40s', header, 24 + 128 * index, name.encode(), offset, size, block, flags, digest, bytes(40))
header[4064:4096] = hashlib.sha256(bytes(header[:4064])).digest()
target.seek(0)
target.write(header)
data = {'image_bytes': total, 'image_sha256': sha256_file(image), 'manifest_sha256': hashlib.sha256(bytes(header)).hexdigest(),
'windows': [{'name': n, 'offset': o, 'size': s, 'sha256': d.hex()} for n, o, s, d in entries],
'objects': [{'name': n, 'offset': o, 'size': s, 'sha256': d.hex()} for n, o, s, d in object_entries],
'userdata_bytes': capacity, 'flash_command': f'fastboot flash userdata {image}',
'source': 'remote' if source.remote else 'local', 'hardware_tested': False}
report.write_text(json.dumps(data, indent=2) + '\n')
+4 -1
View File
@@ -26,7 +26,10 @@ def kernel_names(root, global_only=True):
filelist = root / OBJ / 'link.filelist'
if not filelist.is_file():
raise SystemExit('No XNU symbol names: run a kernel pass first (it writes link.filelist)')
names = defined_names([line.strip() for line in filelist.read_text().splitlines() if line.strip()], global_only)
# A pass whose compile failed leaves its filelist naming objects it did not produce; the next pass rebuilds them and its
# link sees them, so only the objects that exist are read here.
objects = [line.strip() for line in filelist.read_text().splitlines() if line.strip()]
names = defined_names([path for path in objects if Path(path).is_file()], global_only)
# The link also exports aliases (all-alias.exp: "_real _alias") that no object defines under that name.
aliases = root / OBJ / 'all-alias.exp'
if aliases.is_file():