Files
UniXNU/scripts/build-sheng-source-storage.py
rpone 627c932a15 fix(storage): find shipped policy headers in the assembled tree
The ShengUfs component recipe runs this builder without arguments, so the
--policy-dir flag of the archive entry never reached it and a clean checkout
still looked for build/verified-root-<build>. Without an explicit directory the
builder now falls back to the single scripts/policies/*<build> directory of the
assembled tree, where the device repository's scripts are copied.
2026-11-01 08:55:14 +08:00

246 lines
14 KiB
Python

#!/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}')
if not args.policy_dir and not policies.is_dir():
# A clean checkout ships the signed headers in the assembled tree (scripts/policies, copied
# from the device repository); the full block tables stay a local input of the userdata image.
shipped = sorted(tree.glob(f'scripts/policies/*{build_id}'))
if len(shipped) != 1:
parser.error(f'no policy directory for {build_id}: pass --policy-dir or ship scripts/policies/*{build_id}')
policies = shipped[0]
policies = policies.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')
# 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.
# A policy directory may ship manifest.header.bin, the header without the block tables: every
# check below still runs on it, and the full tables stay a local input of the userdata image.
header = path.with_name('manifest.header.bin')
if tables_in_userdata and header.is_file():
data = header.read_bytes()
if len(data) != 544 or data[56:88].hex() != digest:
parser.error(f'{header}: policy header does not describe the selected image')
manifests.append(data)
else:
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')
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', 'BootDiagExport.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'))
# The diagnostic file export (sheng-diag-export): the unified log, crash reports and a screenshot, read through the VFS.
sources.append(device_source('BootDiagExport.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()