#!/usr/bin/env python3 """Observe the first exception from an unchanged Mach-O on QEMU virt. This tests the instruction entry boundary, not an XNU platform or OS boot. The fixture supplies skeletal boot arguments and an optional test device tree. """ import argparse import hashlib import json from pathlib import Path import re import shutil import struct import subprocess import time ROOT = Path(__file__).resolve().parent.parent PHYS_BASE = 0x50000000 BOOT_ARGS_PA = 0x5ff00000 def prepare(kernel, output, phys_base=PHYS_BASE, boot_args_pa=BOOT_ARGS_PA, device_tree=None, command_line='-v serial=3 debug=0x14e'): data = kernel.read_bytes() if data[:4] != b'\xcf\xfa\xed\xfe': raise ValueError('Expected decompressed little-endian 64-bit Mach-O') cpu, subtype, kind, count, commands = struct.unpack_from(' len(data) - 32: raise ValueError('Expected AArch64 MH_EXECUTE with complete load commands') offset = 32 segments = [] entry = None for _ in range(count): if offset + 8 > 32 + commands: raise ValueError('Truncated Mach-O command') command, size = struct.unpack_from(' 32 + commands: raise ValueError('Invalid Mach-O command size') if command == 0x19: if size < 72: raise ValueError('Truncated LC_SEGMENT_64') name, address, length, fileoff, filesize = struct.unpack_from('<16sQQQQ', data, offset + 8) if filesize > length or fileoff + filesize > len(data): raise ValueError('Invalid segment file extent') if length: segments.append((name.rstrip(b'\0').decode(), address, length, fileoff, filesize)) elif command == 5: if size != 288 or struct.unpack_from(' right[1]: raise ValueError('Overlapping virtual segments') base = segments[0][1] end = max(s[1] + s[2] for s in segments) if not 0x40000000 <= phys_base < boot_args_pa or phys_base % 4096: raise ValueError('Physical base must be page-aligned in the fixture RAM') if boot_args_pa % 4096 or boot_args_pa + 0x4000 > 0x60000000: raise ValueError('Boot arguments must be page-aligned in fixture RAM') if end - base > boot_args_pa - phys_base or not base <= entry < end: raise ValueError('Kernel does not fit the fixed test memory layout') image = bytearray(end - base) for name, address, length, fileoff, filesize in segments: image[address - base:address - base + filesize] = data[fileoff:fileoff + filesize] (output / 'kernel-flat.bin').write_bytes(image) boot = bytearray(0x4000) struct.pack_into('= 1024: raise ValueError('Command line exceeds boot_args capacity') boot[0x6c:0x6c + len(encoded_command_line)] = encoded_command_line struct.pack_into(' len(boot) - 0x1000: raise ValueError('Test device tree exceeds reserved boot buffer') boot[0x1000:0x1000 + len(tree)] = tree struct.pack_into(' 1024 * 1024): break time.sleep(0.05) finally: if process.poll() is None: process.terminate() try: process.wait(timeout=2) except subprocess.TimeoutExpired: process.kill() process.wait() text = log.read_text(errors='replace') if log.exists() else '' exceptions = trace.read_text(errors='replace') if trace.exists() else '' report.update(cpu=args.cpu, seconds=round(time.monotonic() - started, 3), outcome='exception' if ('exception captured' in text or 'Taking exception' in exceptions) else 'no-exception-captured', os_boot_demonstrated=False) report['qemu_exception_trace'] = exceptions[:4096] fault = re.search(r'with ELR 0x([0-9a-f]+)', exceptions) if fault: pc = int(fault[1], 16) in_virtual_segment = any(int(s['va'], 16) <= pc < int(s['va'], 16) + s['size'] for s in report['segments']) in_physical_image = args.phys_base <= pc < args.phys_base + report['mapped_bytes'] virtual = pc if in_virtual_segment else ( pc - args.phys_base + int(report['virtual_base'], 16) if in_physical_image else None) report['fault_pc'] = hex(pc) report['fault_address_space'] = ('kernel-virtual' if in_virtual_segment else 'physical' if in_physical_image else 'unmapped') if virtual is not None: report['fault_va'] = hex(virtual) binary = args.kernel.read_bytes() offset = 32 for _ in range(struct.unpack_from('> 19) & 3}_{(opcode >> 16) & 7}' f'_C{(opcode >> 12) & 15}_C{(opcode >> 8) & 15}_{(opcode >> 5) & 7}') offset += size for register in ('CurrentEL', 'MMFR0', 'CNTFRQ', 'ESR_EL1', 'ELR_EL1', 'FAR_EL1'): match = re.search(register + r'=0x([0-9a-f]+)', text) if match: report[register] = '0x' + match[1] (output / f'{args.cpu}.json').write_text(json.dumps(report, indent=2) + '\n') print(text, end='') print(json.dumps({k: v for k, v in report.items() if k != 'segments'}, indent=2)) if 'entering unchanged kernel' not in text: raise SystemExit('Probe did not reach the entry boundary; inspect stderr log')