Files
Snapintosh/tools/initproc-exit.py
T
Acelogic 1510d8c6ce Snapintosh public release
An EL2 monitor/hypervisor and toolchain that boots macOS 27 on the ASUS A16
(Snapdragon X2 Elite), with an Adreno Metal plug-in and IOGPU kext, and
air2ir3, an AIR to Mesa ir3 shader compiler.

MIT, with GPL-2.0 components listed in LICENSE and THIRD_PARTY_NOTICES.md.
No Apple, Qualcomm, ASUS or Microsoft binaries are included.
2026-10-21 21:26:36 -04:00

145 lines
6.3 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Why launchd died: the exception path and instruction behind "initproc exited".
The kernel panics on pid 1's exit on the exiting launchd thread, so the first
"Panicked thread" backtrace after "pid 1 exited" is launchd's own way out:
fleh_synchronous from a user PC, sleh_synchronous, the handler of that EL0
exception class, exception_triage, then the AST that delivers the signal and
exits. This names each kernel frame with tools/kc-symbolicate.py (KDK
26A428), reports the handler and the user PC, and, for a PC in the dyld shared
cache, finds the instructions of the handler's class in this Mac's 26A428
cache at the same 16 KiB page offset (the shared region slides in 16 KiB
pages) and names them with lldb. The host must run the guest's build.
python3 tools/initproc-exit.py CONSOLE.txt [CONSOLE.txt ...]
Read-only; prints one JSON record per console.
"""
import glob
import json
import mmap
import os
import re
import runpy
import struct
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DSC = '/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e'
# What each 26A428 sleh handler is entered for, the signal it raises for an EL0 origin (KDK disassembly;
# compat/user-crash.h) and the instruction words it can be entered on.
HANDLERS = {
'handle_wf_trap': ('EC 0x01 WFI/WFE', 'SIGILL',
lambda w: w in (0xd503205f, 0xd503207f) or (w & ~31) in (0xd5031000, 0xd5031020)),
'handle_simd_trap': ('EC 0x07 SIMD/FP access', 'SIGILL', None),
'handle_uncategorized': ('EC 0x00 unknown (and the default group)', 'SIGILL', None),
'handle_pac_fail': ('EC 0x1c FPAC', 'EXC_BAD_ACCESS|EXC_PTRAUTH', None),
'handle_user_breakpoint': ('EC 0x3c BRK', 'SIGTRAP', lambda w: (w & 0xffe0001f) == 0xd4200000),
'handle_user_abort': ('EC 0x20/0x24 abort', 'SIGSEGV/SIGBUS', None),
'handle_pc_align': ('EC 0x22 PC alignment', 'SIGBUS', None),
'handle_sp_align': ('EC 0x26 SP alignment', 'SIGBUS', None),
'handle_fp_trap': ('EC 0x2c FP exception', 'SIGFPE', None),
}
SIGNALS = {4: 'SIGILL', 5: 'SIGTRAP', 6: 'SIGABRT', 8: 'SIGFPE', 9: 'SIGKILL', 10: 'SIGBUS', 11: 'SIGSEGV'}
def panic_backtrace(text):
"""(exit line, [lr...]) of the first panicked-thread backtrace after pid 1's exit."""
at = text.find('pid 1 exited')
if at < 0:
return None, []
line = text[at:text.find('\n', at)].strip()
block = text.find('Panicked thread', at)
end = text.find('Kernel Extensions', block)
return line, re.findall(r'lr: (0x[0-9a-f]+)', text[block:end if end > 0 else None])
def cache_candidates(pc, accept):
"""Shared-cache addresses with pc's 16 KiB page offset whose word accept() admits."""
found = []
for path in sorted(glob.glob(DSC + '*')):
with open(path, 'rb') as stream, mmap.mmap(stream.fileno(), 0, access=mmap.ACCESS_READ) as m:
if m[:7] != b'dyld_v1':
continue
offset, count = struct.unpack_from('<II', m, 16)
for i in range(count):
address, size, file_offset, _, init_prot = struct.unpack_from('<QQQII', m, offset + 32 * i)
if not init_prot & 4:
continue
first = (pc - address) & 0x3fff
for at in range(first, size - 3, 0x4000):
if accept(struct.unpack_from('<I', m, file_offset + at)[0]):
found.append(address + at)
return found
def lldb_names(addresses, binary='/sbin/launchd'):
"""file-address -> 'image`symbol + n' through lldb's static view of the shared cache."""
if not addresses:
return {}
commands = ['-o', f'target create --arch arm64e {binary}']
for a in addresses:
commands += ['-o', f'image lookup -a {a:#x}']
out = subprocess.run(['lldb', '-b', *commands], capture_output=True, text=True, timeout=300).stdout
names, current = {}, None
for line in out.splitlines():
m = re.search(r'image lookup -a (0x[0-9a-f]+)', line)
if m:
current = int(m.group(1), 16)
m = re.search(r'Summary: (.*)$', line)
if m and current is not None:
names[current] = m.group(1).strip()
return names
def explain(path, symbolicator):
text = Path(path).read_bytes().decode('utf-8', 'replace').replace('\r\n', '\n').replace('\r', '\n')
line, lrs = panic_backtrace(text)
record = dict(console=str(path), exit=line)
if not lrs:
return dict(record, verdict='no initproc backtrace in this console')
m = re.search(r'namespace (\d+) subcode (0x[0-9a-f]+)', line or '')
if m:
record['namespace'] = int(m.group(1))
record['signal'] = SIGNALS.get(int(m.group(2), 16), m.group(2)) if m.group(1) == '2' else m.group(2)
kernel = [int(a, 16) for a in lrs if int(a, 16) >= 0xffff000000000000]
user = [int(a, 16) for a in lrs if int(a, 16) < 0xffff000000000000]
frames = symbolicator.resolve(kernel)
record['kernel_frames'] = frames
# The handler is the innermost handle_* below the last sleh_synchronous (the frame list runs top-down).
last_sleh = max((i for i, f in enumerate(frames) if ' sleh_synchronous ' in f), default=None)
handler = None
if last_sleh is not None:
for f in reversed(frames[:last_sleh]):
name = f.split()[1]
if name.startswith('handle_'):
handler = name
break
record['handler'] = handler
record['user_pc'] = hex(user[-1]) if user else None
if handler in HANDLERS:
record['exception'], record['raises'], accept = HANDLERS[handler]
if user and accept and 0x180000000 <= user[-1] < 0x300000000:
candidates = cache_candidates(user[-1], accept)
names = lldb_names(candidates)
record['shared_cache_candidates'] = [dict(unslid=hex(a), slide=hex(user[-1] - a), symbol=names.get(a))
for a in candidates]
return record
def main(argv):
if not argv:
print(__doc__)
return 2
symbolicator = runpy.run_path(str(ROOT / 'tools/kc-symbolicate.py'))['Symbolicator']()
for path in argv:
print(json.dumps(explain(path, symbolicator), indent=1))
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))