Files
UniXNU/scripts/audit-target-kext-bindings.py
rpone 02f8cdd377 fix(scripts): use mach.development.bcm2837.unstripped (no .noctf suffix)
The kernel object produced by the ipados27 build tree is
mach.development.bcm2837.unstripped; the .noctf variant is not emitted.
Fix the reference in build-sheng-source-drivers.py and all other scripts
that contained the wrong suffix.

Also update submodule pointers for Adreno740 (preemption A1) and
AdrenoMetal (priority forwarding + pass reporter), and regenerate
turnip-gmem-mutable-swap.patch to include the NativeAdrenoAbiPriority
method-17 call in tu_knl_darwin.cc.
2026-11-01 00:20:19 +08:00

195 lines
9.0 KiB
Python

#!/usr/bin/env python3
"""Audit exact prelink slide sites against a source-built kernel, without loading.
The upstream KASLR list identifies absolute pointers, not original link symbols
or a complete relocation recipe. PC-relative references and ABI compatibility
remain separate requirements. No target bytes or signatures are changed.
"""
import argparse
from collections import Counter
import hashlib
import json
from pathlib import Path
import struct
import subprocess
ROOT = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--bundle', default='com.apple.driver.AppleMobileFileIntegrity')
args = parser.parse_args()
folder = ROOT / 'build/target-kexts' / args.bundle
metadata = json.loads((folder / 'segments.json').read_text())
stock = ROOT / '.cache/firmware/kernelcache.macho/kernelcache.release.ipad7.decompressed'
baseline = ROOT / 'build/xnu/ios-obj/DEVELOPMENT_ARM64_BCM2837/mach.development.bcm2837.unstripped'
data = stock.read_bytes()
if hashlib.sha256(data).hexdigest() != 'f18b4df865405f2a287b84977d7aca745e056d656d9fc9df1662970ce2969de7':
raise SystemExit('Wrong target kernelcache')
def segments(at):
magic, cpu, subtype, _, count, size = struct.unpack_from('<6I', data, at)
if (magic, cpu, subtype) != (0xfeedfacf, 0x100000c, 0):
raise ValueError('Expected supplied ordinary ARM64 Mach-O')
offset = at + 32
result = []
for _ in range(count):
command, length = struct.unpack_from('<II', data, offset)
if length < 8 or offset + length > at + 32 + size or at + 32 + size > len(data):
raise ValueError(f'Malformed load command extent at {at:#x}: command={command:#x}, offset={offset:#x}, length={length:#x}, count={count}, size={size:#x}')
if command == 0x19:
name, va, vmsize, fileoff, filesize = struct.unpack_from('<16s4Q', data, offset + 8)
result.append({'name': name.rstrip(b'\0').decode(), 'va': va, 'size': vmsize,
'fileoff': fileoff, 'filesize': filesize})
offset += length
if offset != at + 32 + size:
raise ValueError('Load command size mismatch')
return result
parent = segments(0)
def file_offset(va, length):
matches = [s['fileoff'] + va - s['va'] for s in parent
if s['va'] <= va and va + length <= s['va'] + s['filesize']]
if len(matches) != 1 or matches[0] + length > len(data):
raise ValueError(f'Ambiguous/unbacked cache extent {va:#x}+{length:#x}')
return matches[0]
owners = []
interface_records = []
for entry in json.loads((ROOT / 'build/target-kexts/inventory.json').read_text()):
if entry['CFBundleIdentifier'].startswith('com.apple.kpi.'):
# These packed KPI symbol-set records contain no executable mappings.
interface_records.append(entry['CFBundleIdentifier'])
continue
va = entry.get('_PrelinkExecutableLoadAddr')
if va is None or not entry.get('_PrelinkExecutableSize'):
continue
try:
module_segments = segments(file_offset(va, 32))
except ValueError as error:
raise ValueError(f'{entry["CFBundleIdentifier"]}: {error}') from error
for segment in module_segments:
if segment['size'] and segment['name'] != '__LINKEDIT':
owners.append((segment, entry['CFBundleIdentifier']))
def ownership(va):
return sorted({name for s, name in owners if s['va'] <= va < s['va'] + s['size']})
def symbols(path):
result = {}
for line in subprocess.check_output(['xcrun', 'nm', '-g', '-n', path], text=True).splitlines():
parts = line.split()
if len(parts) == 3 and parts[1].upper() != 'U':
result.setdefault(int(parts[0], 16), []).append(parts[2])
return result
old_symbols = symbols(stock)
new_symbols = {}
for address, names in symbols(baseline).items():
for name in names:
new_symbols.setdefault(name, set()).add(address)
relocations = (ROOT / 'build/target-kexts/prelink-kaslr.bin').read_bytes()
count, = struct.unpack_from('<I', relocations)
if len(relocations) != 4 + count * 4:
raise SystemExit('Malformed exact KASLR pointer table')
offsets = struct.unpack_from(f'<{count}I', relocations, 4)
if len(set(offsets)) != count:
raise SystemExit('Duplicate exact KASLR sites')
base = next(s['va'] for s in parent if s['name'] == '__PRELINK_TEXT')
records = []
for offset in offsets:
site = base + offset
if not any(s['va'] <= site < s['va'] + s['size'] for s in metadata['segments']):
continue
if ownership(site) != [args.bundle] or ownership(site + 7) != [args.bundle]:
raise SystemExit(f'Overlapping module ownership at {site:#x}')
target, = struct.unpack_from('<Q', data, file_offset(site, 8))
target_owners = ownership(target)
names = old_symbols.get(target, [])
candidates = sorted({address for name in names for address in new_symbols.get(name, set())})
if target_owners == [args.bundle]:
kind = 'internal-pointer'
elif target_owners:
kind = 'cross-kext-pointer'
elif len(candidates) == 1:
kind = 'kernel-symbol-candidate'
elif candidates:
kind = 'ambiguous-kernel-symbol'
elif names:
kind = 'missing-kernel-symbol'
else:
kind = 'unresolved-external-pointer'
records.append({'site': hex(site), 'target': hex(target), 'kind': kind,
'target_owners': target_owners, 'stock_export_names': names,
'baseline_address_candidates': [hex(value) for value in candidates]})
def signed(value, bits):
return value - (1 << bits) if value & (1 << (bits - 1)) else value
# Encoding inventory, not control-flow recovery: executable ranges may include
# literal data. An ADRP page match alone does not resolve its later consumers.
pc_relative = []
for segment in metadata['segments']:
if segment['name'] != '__TEXT_EXEC':
continue
blob = (folder / '__TEXT_EXEC.bin').read_bytes()
if hashlib.sha256(blob).hexdigest() != segment['sha256']:
raise SystemExit('Extracted executable segment digest mismatch')
for offset in range(0, len(blob) - 3, 4):
word, = struct.unpack_from('<I', blob, offset)
pc = segment['va'] + offset
if word & 0x9f000000 in (0x10000000, 0x90000000):
immediate = signed(((word >> 5) & 0x7ffff) << 2 | ((word >> 29) & 3), 21)
page = bool(word & 0x80000000)
target = (pc & ~4095) + (immediate << 12) if page else pc + immediate
kind = 'adrp-page' if page else 'adr'
elif word & 0x7c000000 == 0x14000000:
target = pc + (signed(word & 0x3ffffff, 26) << 2)
kind = 'b-or-bl'
elif word & 0xff000010 == 0x54000000:
target = pc + (signed((word >> 5) & 0x7ffff, 19) << 2)
kind = 'conditional-branch'
elif word & 0x7e000000 == 0x34000000:
target = pc + (signed((word >> 5) & 0x7ffff, 19) << 2)
kind = 'compare-branch'
elif word & 0x7e000000 == 0x36000000:
target = pc + (signed((word >> 5) & 0x3fff, 14) << 2)
kind = 'test-bit-branch'
elif word & 0x3b000000 == 0x18000000:
target = pc + (signed((word >> 5) & 0x7ffff, 19) << 2)
kind = 'literal-load-or-prefetch'
else:
continue
if kind == 'adrp-page':
internal = any((s['va'] & ~4095) <= target < ((s['va'] + s['size'] + 4095) & ~4095)
for s in metadata['segments'])
else:
internal = any(s['va'] <= target < s['va'] + s['size'] for s in metadata['segments'])
pc_relative.append({'site': hex(pc), 'target': hex(target), 'kind': kind,
'within_module_extent_or_adrp_page': internal})
summary = dict(Counter(record['kind'] for record in records))
report = {'bundle_id': args.bundle, 'stock_sha256': hashlib.sha256(data).hexdigest(),
'baseline_sha256': hashlib.sha256(baseline.read_bytes()).hexdigest(),
'kaslr_table_sha256': hashlib.sha256(relocations).hexdigest(),
'excluded_kernel_kpi_symbol_sets': interface_records,
'total_cache_pointer_sites': count, 'module_pointer_sites': len(records),
'summary': summary, 'pointers': records,
'pc_relative_encoding_counts': dict(Counter(record['kind'] for record in pc_relative)),
'pc_relative_encodings_outside_module': [r for r in pc_relative if not r['within_module_extent_or_adrp_page']],
'pc_relative_consumer_dataflow_proven': False,
'complete_relocation_recipe': False, 'abi_compatibility_proven': False,
'pc_relative_references_audited': False, 'target_bytes_modified': False}
(folder / 'exact-pointer-audit.json').write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps({key: report[key] for key in ('bundle_id', 'module_pointer_sites', 'summary')}, indent=2))
print('PC-relative encoding counts:', report['pc_relative_encoding_counts'])
print('Encodings outside module extents/pages:', len(report['pc_relative_encodings_outside_module']))
print('Static pointer inventory only: no module loaded, no ABI/PC-relative binding proof')