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:
1 parent
7b0219043e
commit
ab11a8cb68
26 files changed
+1198
-151
No files matched your search
@@ -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))
|
||||
|
||||
@@ -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:])
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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')
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
|
||||
@@ -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)))
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in new issue
Block a user