fix: supply shared XNU entry probe tools for device payloads
This commit is contained in:
1 parent
6d1fd59e17
commit
c1f6966146
2 files changed
+732
No files matched your search
@@ -0,0 +1,430 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Put boot-progress probes into a COPY of the source XNU kernel (never the original).
|
||||
|
||||
There is no serial console on the device and the kernel cannot draw before its
|
||||
own video console exists, so the early boot stages are reported by side
|
||||
channels. The stage code lives in the zero padding after the kernel entry
|
||||
(_start is one `b`) and displaced instructions are re-executed there:
|
||||
|
||||
stage 1 (kernel entry, MMU off) blue bar, then continues
|
||||
stage 2 (just before the MMU is on) magenta bar, then continues
|
||||
stage 3 (right after the MMU is on) waits --reset-delay seconds, then PSCI
|
||||
SYSTEM_RESET. A reboot by itself after
|
||||
that time shows the CPU got through the
|
||||
MMU enable; the frame buffer is not
|
||||
mapped by then, so time is the signal.
|
||||
panic screen (--panic-screen) panic() and its common panic funnel draw
|
||||
their format string on the
|
||||
kernel's own frame buffer mapping and
|
||||
stops (scripts/xnu-panic-screen.c).
|
||||
--no-reset leaves out stages 3 and 4.
|
||||
stage 4 (--reset-after SYMBOL) the same wait and reset, but placed after
|
||||
the call to SYMBOL in --in FUNCTION (the
|
||||
Nth call, --occurrence). It replaces
|
||||
stage 3, so one run asks one question:
|
||||
did the kernel get past that call?
|
||||
|
||||
The bars are the 1/16th bands of the frame buffer below the green one that
|
||||
DarwinBoot paints (band index = stage). Each patch site is checked against its
|
||||
expected original instruction first, and the tool refuses a kernel that does
|
||||
not match. Offsets are specific to the kernel built by build-xnu-4k-baseline.sh.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ENTRY_VA = 0xFFFFFFF00B9D0488 # LC_UNIXTHREAD pc, `b start_first_cpu`
|
||||
START_FIRST_CPU = 0xFFFFFFF00B9D205C
|
||||
MMU_ENABLE_VA = 0xFFFFFFF00B9D2330 # msr SCTLR_EL1, x0
|
||||
AFTER_MMU_VA = 0xFFFFFFF00B9D2350 # mov x0, #0x300000
|
||||
PANIC_VA = 0xFFFFFFF00BF69B04 # _panic
|
||||
VINFO_VA = 0xFFFFFFF00C0A2970 # _vinfo
|
||||
PANIC_CAVE_END = 0xFFFFFFF00B9D0FF0
|
||||
CAVE_SIZE = 0x60
|
||||
CAVES = {1: 0xFFFFFFF00B9D0500, 2: 0xFFFFFFF00B9D0560, 3: 0xFFFFFFF00B9D05C0, 4: 0xFFFFFFF00B9D0620}
|
||||
TIMEBASE = 19200000
|
||||
|
||||
PAINT = """
|
||||
ldr x1, [{args}, #0x28]
|
||||
ldr x2, [{args}, #0x38]
|
||||
ldr x3, [{args}, #0x48]
|
||||
mul x2, x2, x3
|
||||
lsr x2, x2, #4
|
||||
bic x2, x2, #7
|
||||
mov x4, #{band}
|
||||
mul x5, x2, x4
|
||||
add x1, x1, x5
|
||||
movz w6, #{lo}
|
||||
movk w6, #{hi}, lsl #16
|
||||
1:
|
||||
stp w6, w6, [x1], #8
|
||||
subs x2, x2, #8
|
||||
b.hi 1b
|
||||
dsb sy
|
||||
"""
|
||||
|
||||
SOURCES = {
|
||||
1: ".text\n.global _start\n_start:\n" + PAINT.format(args='x0', band=1, lo='0x00FF', hi='0xFF00') + " b target\n",
|
||||
2: ".text\n.global _start\n_start:\n" + PAINT.format(args='x20', band=2, lo='0x00FF', hi='0xFFFF')
|
||||
+ " msr SCTLR_EL1, x0\n b target\n",
|
||||
3: """.text
|
||||
.global _start
|
||||
_start:
|
||||
mov x0, #0x300000
|
||||
mrs x1, cntpct_el0
|
||||
ldr x2, ={ticks}
|
||||
add x2, x1, x2
|
||||
1:
|
||||
isb
|
||||
mrs x1, cntpct_el0
|
||||
cmp x1, x2
|
||||
b.lo 1b
|
||||
movz w0, #0x0009
|
||||
movk w0, #0x8400, lsl #16
|
||||
mov x1, xzr
|
||||
mov x2, xzr
|
||||
mov x3, xzr
|
||||
smc #0
|
||||
2:
|
||||
wfe
|
||||
b 2b
|
||||
""" + ".ltorg\n",
|
||||
4: """.text
|
||||
.global _start
|
||||
_start:
|
||||
bl callee
|
||||
mrs x1, cntpct_el0
|
||||
ldr x2, ={ticks}
|
||||
add x2, x1, x2
|
||||
1:
|
||||
isb
|
||||
mrs x1, cntpct_el0
|
||||
cmp x1, x2
|
||||
b.lo 1b
|
||||
movz w0, #0x0009
|
||||
movk w0, #0x8400, lsl #16
|
||||
mov x1, xzr
|
||||
mov x2, xzr
|
||||
mov x3, xzr
|
||||
smc #0
|
||||
2:
|
||||
wfe
|
||||
b 2b
|
||||
""" + ".ltorg\n",
|
||||
}
|
||||
|
||||
RESET_BEFORE = """.text
|
||||
.global _start
|
||||
_start:
|
||||
movz w0, #0x0009
|
||||
movk w0, #0x8400, lsl #16
|
||||
mov x1, xzr
|
||||
mov x2, xzr
|
||||
mov x3, xzr
|
||||
smc #0
|
||||
1:
|
||||
wfe
|
||||
b 1b
|
||||
"""
|
||||
|
||||
|
||||
def kernel_symbols(kernel):
|
||||
"""Address -> name for the kernel's symbols, from llvm-nm."""
|
||||
nm = shutil.which('llvm-nm') or '/opt/homebrew/opt/llvm/bin/llvm-nm'
|
||||
table = {}
|
||||
for line in subprocess.run([nm, '-n', kernel], capture_output=True, text=True, check=True).stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) == 3:
|
||||
table.setdefault(int(parts[0], 16), parts[2])
|
||||
return table
|
||||
|
||||
|
||||
def function_range(symbols, name):
|
||||
"""Start and end (next symbol) of a function by its symbol name."""
|
||||
starts = sorted(symbols)
|
||||
for start, symbol in symbols.items():
|
||||
if symbol == name:
|
||||
return start, next(address for address in starts if address > start)
|
||||
raise SystemExit(f'No symbol {name} in the kernel')
|
||||
|
||||
|
||||
def find_call(data, symbols, function, name, occurrence):
|
||||
"""The VA and target of the Nth `bl name` inside FUNCTION."""
|
||||
hits = []
|
||||
low, high = function_range(symbols, function)
|
||||
for va in range(low, high, 4):
|
||||
word = struct.unpack_from('<I', data, file_offset(va))[0]
|
||||
if word >> 26 == 0b100101:
|
||||
delta = word & 0x3FFFFFF
|
||||
delta -= (1 << 26) if delta & (1 << 25) else 0
|
||||
target = va + delta * 4
|
||||
if symbols.get(target) == name:
|
||||
hits.append((va, target))
|
||||
if len(hits) < occurrence:
|
||||
raise SystemExit(f'{function} has {len(hits)} call(s) to {name}, not {occurrence}')
|
||||
return hits[occurrence - 1]
|
||||
|
||||
|
||||
def assemble_c(source, va, workdir, symbols):
|
||||
"""Build a freestanding C file for VA; returns (bytes, load address of the first byte, address of panic_screen).
|
||||
|
||||
The linker may start .text a little after VA (it keeps the ELF headers in front), so the
|
||||
load address is read back from the ELF and is what the caller must write the bytes to."""
|
||||
clang = shutil.which('clang') or '/opt/homebrew/opt/llvm/bin/clang'
|
||||
lld = shutil.which('ld.lld') or '/opt/homebrew/opt/llvm/bin/ld.lld'
|
||||
objcopy = shutil.which('llvm-objcopy') or '/opt/homebrew/opt/llvm/bin/llvm-objcopy'
|
||||
nm = shutil.which('llvm-nm') or '/opt/homebrew/opt/llvm/bin/llvm-nm'
|
||||
# UNIXNU_UEFI is the Mu-Silicium checkout when the unixnu driver runs this script.
|
||||
font_dir = Path(os.environ.get('UNIXNU_UEFI') or ROOT / 'uefi-sheng') / 'Silicon/Silicium/SiliciumPkg/Library/FrameBufferSerialPortLib'
|
||||
if not (font_dir / 'Font.h').is_file():
|
||||
raise SystemExit('Panic screen requires Mu-Silicium FrameBufferSerialPortLib/Font.h; '
|
||||
'set --uefi PATH or [build] uefi in unixnu.local.toml '
|
||||
'(UNIXNU_UEFI for direct script calls)')
|
||||
obj, elf, binary = (workdir / name for name in ('panic.o', 'panic.elf', 'panic.bin'))
|
||||
definitions = []
|
||||
for macro, name, width in (('MAX_MEM', '_max_mem', 8),
|
||||
('PAGE_TOTAL', '_vm_page_pages', 4),
|
||||
('PAGE_FREE', '_vm_page_free_count', 4)):
|
||||
address = symbols.get(name)
|
||||
if address is None or address % width or not any(
|
||||
base <= address and address + width <= base + size
|
||||
for base, size, _, _ in SEGMENTS):
|
||||
raise SystemExit(f'Missing or invalid diagnostic symbol {name}')
|
||||
definitions.append(f'-D{macro}={address:#x}UL')
|
||||
# Optional: the board's banner buffer for the panic message (kits/sheng/BootProgress.c); kernels without it get 0 (no copy).
|
||||
text = symbols.get('_boot_progress_panic_text')
|
||||
if text is not None:
|
||||
if not any(base <= text and text + 256 <= base + size for base, size, _, _ in SEGMENTS):
|
||||
raise SystemExit('Invalid _boot_progress_panic_text')
|
||||
definitions.append(f'-DPANIC_TEXT={text:#x}UL')
|
||||
subprocess.run([clang, '--target=aarch64-none-elf', '-ffreestanding', '-fno-builtin', '-fno-stack-protector',
|
||||
'-mgeneral-regs-only', '-fno-pic', '-mcmodel=tiny', '-Oz', f'-DVINFO={VINFO_VA:#x}UL',
|
||||
*definitions, f'-I{font_dir}', '-c', source, '-o', obj], check=True)
|
||||
script = workdir / 'panic.ld'
|
||||
script.write_text('SECTIONS { . = %#x; .text : { *(.text.a_entry) *(.text.a_common) *(.text*) *(.rodata*) } /DISCARD/ : { *(.comment) *(.note*) *(.eh_frame*) *(.ARM.*) } }\n' % va)
|
||||
size = shutil.which('llvm-size') or '/opt/homebrew/opt/llvm/bin/llvm-size'
|
||||
sizes = subprocess.run([size, '--format=berkeley', obj], capture_output=True, text=True, check=True).stdout.splitlines()[-1].split()
|
||||
if int(sizes[1]) or int(sizes[2]):
|
||||
# The code runs from the kernel's read-only text: a global would fault on its first store.
|
||||
raise SystemExit(f'{source.name} has writable data (data {sizes[1]}, bss {sizes[2]}); use the stack')
|
||||
subprocess.run([lld, '-T', script, '-e', 'panic_screen', obj, '-o', elf], check=True)
|
||||
# Only .text: a plain binary dump would start at the ELF headers kept in front of it.
|
||||
subprocess.run([objcopy, '-O', 'binary', '-j', '.text', elf, binary], check=True)
|
||||
readelf = shutil.which('llvm-readelf') or '/opt/homebrew/opt/llvm/bin/llvm-readelf'
|
||||
load = None
|
||||
for line in subprocess.run([readelf, '-S', elf], capture_output=True, text=True, check=True).stdout.splitlines():
|
||||
parts = line.replace('[', ' ').replace(']', ' ').split()
|
||||
if len(parts) >= 4 and parts[1] == '.text':
|
||||
load = int(parts[3], 16)
|
||||
if load is None or load < va:
|
||||
raise SystemExit('Cannot find where the linker put .text')
|
||||
entry = None
|
||||
for line in subprocess.run([nm, elf], capture_output=True, text=True, check=True).stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) == 3 and parts[2] == 'panic_screen':
|
||||
entry = int(parts[0], 16)
|
||||
if entry != load:
|
||||
raise SystemExit(f'panic_screen at {entry:#x} is not the first byte {load:#x}')
|
||||
return binary.read_bytes(), load, entry
|
||||
|
||||
|
||||
def panic_common_entry(workdir, code, load):
|
||||
"""Resolve the real alternate entry and verify its distinct argument ABI."""
|
||||
nm = shutil.which('llvm-nm') or '/opt/homebrew/opt/llvm/bin/llvm-nm'
|
||||
names = {parts[2]: int(parts[0], 16)
|
||||
for line in subprocess.check_output([nm, workdir / 'panic.elf'], text=True).splitlines()
|
||||
if len(parts := line.split()) == 3}
|
||||
entry = names.get('panic_screen_common')
|
||||
main = names.get('panic_screen_main')
|
||||
if entry is None or main is None or not load <= entry <= load + len(code) - 8:
|
||||
raise SystemExit('Common panic renderer entry is missing or out of bounds')
|
||||
at = entry - load
|
||||
if code[at:at + 8] != struct.pack('<II', 0xf9400021, branch(entry + 4, main)):
|
||||
raise SystemExit('Common panic entry does not dereference the Darwin va_list pointer and branch to the renderer')
|
||||
return entry
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
SEGMENTS = [] # (vmaddr, vmsize, fileoff, filesize) of the kernel being patched
|
||||
|
||||
|
||||
def load_segments(data):
|
||||
"""Read the Mach-O segment table; the file layout differs between kernel builds."""
|
||||
magic, _, _, _, ncmds = struct.unpack_from('<IiiII', data, 0)
|
||||
if magic != 0xFEEDFACF:
|
||||
raise SystemExit('Not a 64-bit Mach-O')
|
||||
offset = 32
|
||||
SEGMENTS.clear()
|
||||
for _ in range(ncmds):
|
||||
cmd, size = struct.unpack_from('<II', data, offset)
|
||||
if cmd == 0x19:
|
||||
_, vmaddr, vmsize, fileoff, filesize = struct.unpack_from('<16sQQQQ', data, offset + 8)
|
||||
if filesize:
|
||||
SEGMENTS.append((vmaddr, vmsize, fileoff, filesize))
|
||||
offset += size
|
||||
|
||||
|
||||
def unixthread_pc(data):
|
||||
"""LC_UNIXTHREAD entry pc (arm64 thread state: flavor, count, x0..x28, fp, lr, sp, pc)."""
|
||||
offset = 32
|
||||
for _ in range(struct.unpack_from('<I', data, 16)[0]):
|
||||
cmd, size = struct.unpack_from('<II', data, offset)
|
||||
if cmd == 0x5:
|
||||
return struct.unpack_from('<Q', data, offset + 16 + 32 * 8)[0]
|
||||
offset += size
|
||||
raise SystemExit('No LC_UNIXTHREAD in kernel')
|
||||
|
||||
|
||||
def rebase(kernel, data):
|
||||
"""The probe sites are fixed offsets from the entry; shift them for another kernel build.
|
||||
|
||||
_panic and _vinfo come from the symbol table when the kernel has one (the plain source
|
||||
kernel does); the retained candidate layout is the unshifted case.
|
||||
"""
|
||||
global ENTRY_VA, START_FIRST_CPU, MMU_ENABLE_VA, AFTER_MMU_VA, PANIC_VA, VINFO_VA, PANIC_CAVE_END, CAVES
|
||||
delta = unixthread_pc(data) - ENTRY_VA
|
||||
if delta:
|
||||
ENTRY_VA += delta
|
||||
START_FIRST_CPU += delta
|
||||
MMU_ENABLE_VA += delta
|
||||
AFTER_MMU_VA += delta
|
||||
PANIC_CAVE_END += delta
|
||||
CAVES = {stage: va + delta for stage, va in CAVES.items()}
|
||||
PANIC_VA += delta
|
||||
VINFO_VA += delta
|
||||
names = {name: va for va, name in kernel_symbols(kernel).items()}
|
||||
if '_start_first_cpu' in names:
|
||||
START_FIRST_CPU = names['_start_first_cpu']
|
||||
# Native bootstrap table-capacity checks can grow before SCTLR setup.
|
||||
# Locate the unique instruction sequence in this bounded entry body;
|
||||
# do not assume a constant offset from the Mach-O entry point.
|
||||
matches = []
|
||||
for va in range(START_FIRST_CPU, START_FIRST_CPU + 0x1000, 4):
|
||||
def instruction(at):
|
||||
return struct.unpack_from('<I', data, file_offset(at))[0]
|
||||
if (instruction(va) == 0xD5181000 and
|
||||
instruction(va + 4) == 0xD5033FDF and
|
||||
instruction(va + 0x20) == 0xD2A00600 and
|
||||
instruction(va + 0x24) == 0xD5181040):
|
||||
matches.append(va)
|
||||
if len(matches) != 1:
|
||||
raise SystemExit('Require one verified SCTLR/ISB/CPACR entry sequence')
|
||||
MMU_ENABLE_VA = matches[0]
|
||||
AFTER_MMU_VA = MMU_ENABLE_VA + 0x20
|
||||
if '_panic' in names and '_vinfo' in names:
|
||||
PANIC_VA = names['_panic']
|
||||
# Board diagnostics seed this ABI-compatible prefix from valid console
|
||||
# mappings, then pin it to their retained GOP mapping. Ordinary source
|
||||
# kernels without the observer retain the original vinfo path.
|
||||
VINFO_VA = names.get('_boot_progress_panic_vinfo', names['_vinfo'])
|
||||
return names
|
||||
|
||||
|
||||
def file_offset(va):
|
||||
for vmaddr, vmsize, fileoff, filesize in SEGMENTS:
|
||||
if vmaddr <= va < vmaddr + min(vmsize, filesize):
|
||||
return fileoff + (va - vmaddr)
|
||||
raise SystemExit(f'{va:#x} is not in a file-backed segment of this kernel')
|
||||
|
||||
|
||||
def branch(from_va, to_va):
|
||||
delta = (to_va - from_va) // 4
|
||||
if (to_va - from_va) % 4 or not -(1 << 25) <= delta < (1 << 25):
|
||||
raise SystemExit('Branch out of range')
|
||||
return 0x14000000 | (delta & 0x3FFFFFF)
|
||||
|
||||
|
||||
def assemble(source, va, symbols, workdir):
|
||||
asm, obj, elf, binary = (workdir / name for name in ('cave.S', 'cave.o', 'cave.elf', 'cave.bin'))
|
||||
asm.write_text(source)
|
||||
clang = shutil.which('clang') or '/opt/homebrew/opt/llvm/bin/clang'
|
||||
lld = shutil.which('ld.lld') or '/opt/homebrew/opt/llvm/bin/ld.lld'
|
||||
objcopy = shutil.which('llvm-objcopy') or '/opt/homebrew/opt/llvm/bin/llvm-objcopy'
|
||||
subprocess.run([clang, '--target=aarch64-none-elf', '-c', asm, '-o', obj], check=True)
|
||||
subprocess.run([lld, f'-Ttext={va:#x}', *[f'--defsym={name}={value:#x}' for name, value in symbols.items()], '-e', '_start', obj, '-o', elf], check=True)
|
||||
subprocess.run([objcopy, '-O', 'binary', elf, binary], check=True)
|
||||
return binary.read_bytes()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument('kernel', type=Path, help='the COPY of the kernel to patch in place')
|
||||
parser.add_argument('--reset-delay', type=int, default=6, help='seconds before the reset')
|
||||
parser.add_argument('--reset-after', metavar='SYMBOL', help='place the reset after this call in arm_init instead of stage 3')
|
||||
parser.add_argument('--reset-before', action='store_true', help='reset at the call site before invoking SYMBOL (requires --reset-after)')
|
||||
parser.add_argument('--panic-screen', action='store_true', help='draw panic() messages on the screen')
|
||||
parser.add_argument('--no-reset', action='store_true', help='no reset probe at all')
|
||||
parser.add_argument('--occurrence', type=int, default=1, help='which call to SYMBOL (1 = first)')
|
||||
parser.add_argument('--in', dest='function', default='_arm_init', help='function that makes the call (default _arm_init)')
|
||||
args = parser.parse_args()
|
||||
if args.reset_before and not args.reset_after:
|
||||
raise SystemExit('--reset-before requires --reset-after')
|
||||
data = bytearray(args.kernel.read_bytes())
|
||||
load_segments(data)
|
||||
kernel_names = rebase(args.kernel, data)
|
||||
|
||||
def word(va):
|
||||
return struct.unpack_from('<I', data, file_offset(va))[0]
|
||||
|
||||
expected = {ENTRY_VA: branch(ENTRY_VA, START_FIRST_CPU), MMU_ENABLE_VA: 0xD5181000, AFTER_MMU_VA: 0xD2A00600}
|
||||
for va, value in expected.items():
|
||||
if word(va) != value:
|
||||
raise SystemExit(f'{va:#x} holds {word(va):#010x}, expected {value:#010x}: not the kernel these probes are for')
|
||||
for va in CAVES.values():
|
||||
if any(data[file_offset(va):file_offset(va) + CAVE_SIZE]):
|
||||
raise SystemExit(f'Padding at {va:#x} is not empty')
|
||||
|
||||
# (site, cave stage, symbols the cave is linked with)
|
||||
sites = [(ENTRY_VA, 1, {'target': START_FIRST_CPU}),
|
||||
(MMU_ENABLE_VA, 2, {'target': MMU_ENABLE_VA + 4})]
|
||||
if args.no_reset:
|
||||
description = 'no reset probe'
|
||||
elif args.reset_after:
|
||||
symbols = kernel_symbols(args.kernel)
|
||||
call_va, callee = find_call(data, symbols, args.function, args.reset_after, args.occurrence)
|
||||
sites.append((call_va, 4, {'callee': callee, 'target': call_va + 4}))
|
||||
description = f'stage 4 reset {"before" if args.reset_before else "after"} call #{args.occurrence} to {args.reset_after} in {args.function} (at {call_va:#x})'
|
||||
else:
|
||||
sites.append((AFTER_MMU_VA, 3, {'target': AFTER_MMU_VA + 4}))
|
||||
description = f'stage 3 reset {args.reset_delay} s after the MMU is on'
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
for site, stage, symbols in sites:
|
||||
source = RESET_BEFORE if (stage == 4 and args.reset_before) else SOURCES[stage].format(ticks=TIMEBASE * args.reset_delay)
|
||||
code = assemble(source, CAVES[stage], symbols, Path(tmp))
|
||||
if len(code) > CAVE_SIZE:
|
||||
raise SystemExit(f'Stage {stage} code is too large')
|
||||
data[file_offset(CAVES[stage]):file_offset(CAVES[stage]) + len(code)] = code
|
||||
for site, stage, _ in sites:
|
||||
struct.pack_into('<I', data, file_offset(site), branch(site, CAVES[stage]))
|
||||
if args.panic_screen:
|
||||
# Right after the last probe cave in use, so the padding is shared as tightly as possible.
|
||||
PANIC_CAVE = max(CAVES[stage] for _, stage, _ in sites) + CAVE_SIZE
|
||||
if word(PANIC_VA) in (0, 0xFFFFFFFF) or any(data[file_offset(PANIC_CAVE):file_offset(PANIC_CAVE_END)]):
|
||||
raise SystemExit('panic() or the panic screen padding is not as expected')
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
code, load, entry = assemble_c(ROOT / 'scripts/xnu-panic-screen.c', PANIC_CAVE, Path(tmp), kernel_names)
|
||||
common_entry = panic_common_entry(Path(tmp), code, load)
|
||||
if load + len(code) > PANIC_CAVE_END:
|
||||
raise SystemExit(f'panic screen is {len(code):#x} bytes at {load:#x}, the padding ends at {PANIC_CAVE_END:#x}')
|
||||
if any(data[file_offset(load):file_offset(load) + len(code)]):
|
||||
raise SystemExit(f'Padding at {load:#x} is not empty')
|
||||
data[file_offset(load):file_offset(load) + len(code)] = code
|
||||
struct.pack_into('<I', data, file_offset(PANIC_VA), branch(PANIC_VA, entry))
|
||||
common = kernel_names.get('_panic_trap_to_debugger')
|
||||
if common is None or word(common) in (0, 0xFFFFFFFF):
|
||||
raise SystemExit('Common panic funnel is missing; refuse a partially covered panic screen')
|
||||
struct.pack_into('<I', data, file_offset(common), branch(common, common_entry))
|
||||
description += f', panic screen ({len(code):#x} bytes at {entry:#x}; common funnel at {common:#x})'
|
||||
args.kernel.write_bytes(data)
|
||||
print(f'patched {args.kernel.name}: stage 1 blue bar, stage 2 magenta bar, {description}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,302 @@
|
||||
/* Draw a kernel panic message, with its arguments, on the frame buffer, then stop.
|
||||
*
|
||||
* Built freestanding by patch-xnu-entry-probe.py and placed in unused padding
|
||||
* of a COPY of the kernel; panic() and its common panic funnel are redirected
|
||||
* here. It runs on the kernel's
|
||||
* own mapping of the frame buffer. With the board observer it uses the retained
|
||||
* GOP mapping prefix; otherwise it uses vinfo.v_baseaddr.
|
||||
*
|
||||
* panic(fmt, ...) is variadic and the Darwin arm64 ABI passes every variadic
|
||||
* argument in an 8-byte stack slot, starting at the stack pointer on entry.
|
||||
* A small printf walks the format string over those slots (%s %x %p, with
|
||||
* 0-flag, width, precision and l/ll/z). Kernel strings are checked with an
|
||||
* active hardware translation query before reading each byte;
|
||||
* unmapped/device/user pointers print as pointers. VINFO names that mapping prefix.
|
||||
*/
|
||||
typedef unsigned long u64;
|
||||
typedef unsigned int u32;
|
||||
typedef unsigned char u8;
|
||||
|
||||
struct vc_info {
|
||||
u32 v_height, v_width, v_depth, v_rowbytes;
|
||||
u64 v_baseaddr;
|
||||
};
|
||||
|
||||
#define SCALE 3
|
||||
|
||||
extern const u64 gFont[];
|
||||
|
||||
/* All state lives on the stack: this code sits in the kernel's read-only text,
|
||||
* so a writable global would fault on its first store. */
|
||||
struct ctx {
|
||||
u8 *base;
|
||||
u32 rb, w, h, x, y;
|
||||
u8 *log; /* kernel buffer that also receives the formatted message (PANIC_TEXT), 0 = none */
|
||||
u32 logn;
|
||||
};
|
||||
|
||||
#ifndef PANIC_TEXT
|
||||
#define PANIC_TEXT 0UL
|
||||
#endif
|
||||
#define PANIC_TEXT_BYTES 2048 /* boot_progress_panic_text: a data abort's message with every register, then the frames */
|
||||
|
||||
__attribute__((noreturn)) static void park (void)
|
||||
{
|
||||
for (;;) {
|
||||
__asm__ volatile ("wfe");
|
||||
}
|
||||
}
|
||||
|
||||
static void put (struct ctx *g, u8 c)
|
||||
{
|
||||
const u32 cell = 6 * SCALE, line = 14 * SCALE;
|
||||
u32 row, col, dy, dx;
|
||||
|
||||
if (g->log && g->logn < PANIC_TEXT_BYTES - 1 && c >= 9 && c <= 126) {
|
||||
g->log[g->logn++] = (c < 32) ? ' ' : c;
|
||||
g->log[g->logn] = 0;
|
||||
}
|
||||
|
||||
if (c == '\n') {
|
||||
g->x = 0;
|
||||
g->y += line;
|
||||
return;
|
||||
}
|
||||
if (c == '\t') {
|
||||
g->x += 4 * cell;
|
||||
return;
|
||||
}
|
||||
if (c < 32 || c > 126) {
|
||||
return;
|
||||
}
|
||||
if (g->x + cell > g->w) {
|
||||
g->x = 0;
|
||||
g->y += line;
|
||||
}
|
||||
if (g->y + line > g->h) {
|
||||
return;
|
||||
}
|
||||
u64 glyph = gFont[c - 32];
|
||||
u32 top = (u32)(glyph >> 32), bottom = (u32)glyph;
|
||||
for (row = 0; row < 12; row++) {
|
||||
u32 data = (row < 6) ? (top >> (row * 5)) : (bottom >> ((row - 6) * 5));
|
||||
for (col = 0; col < 5; col++) {
|
||||
if ((data >> col) & 1) {
|
||||
for (dy = 0; dy < SCALE; dy++) {
|
||||
for (dx = 0; dx < SCALE; dx++) {
|
||||
*(volatile u32 *)(g->base + (u64)(g->y + row * SCALE + dy) * g->rb + (u64)(g->x + col * SCALE + dx) * 4) = 0xFFFFFFFFu;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
g->x += cell;
|
||||
}
|
||||
|
||||
static void number (struct ctx *g, u64 value, u32 base, u32 width, int zero)
|
||||
{
|
||||
u8 digits[24];
|
||||
u32 n = 0, i;
|
||||
|
||||
do {
|
||||
u32 d = (u32)(value % base);
|
||||
digits[n++] = (u8)(d < 10 ? '0' + d : 'a' + d - 10);
|
||||
value /= base;
|
||||
} while (value && n < 24);
|
||||
for (i = n; i < width; i++) {
|
||||
put (g, zero ? '0' : ' ');
|
||||
}
|
||||
while (n) {
|
||||
put (g, digits[--n]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Probe each string byte before dereferencing it. Heap strings (process name
|
||||
* and exit reason) live outside the linked image. AT performs a hardware
|
||||
* translation query only: no table edits, mappings, or exception bypass.
|
||||
* Refuse user pointers, translation faults and device-memory attributes. */
|
||||
static int readable (const char *s, const u64 *args)
|
||||
{
|
||||
u64 a = (u64)s, par;
|
||||
(void)args;
|
||||
if (a < 0xffff000000000000UL) return 0;
|
||||
__asm__ volatile ("at s1e1r, %1; isb; mrs %0, par_el1" : "=r" (par) : "r" (a) : "memory");
|
||||
return !(par & 1) && ((par >> 60) != 0);
|
||||
}
|
||||
|
||||
static void format (struct ctx *g, const char *fmt, const u64 *args)
|
||||
{
|
||||
u32 i, used = 0;
|
||||
|
||||
for (i = 0; i < 2000 && fmt[i]; i++) {
|
||||
u8 c = (u8)fmt[i];
|
||||
if (c != '%') {
|
||||
put (g, c);
|
||||
continue;
|
||||
}
|
||||
u32 width = 0;
|
||||
int zero = 0, longs = 0;
|
||||
c = (u8)fmt[++i];
|
||||
if (c == '0') {
|
||||
zero = 1;
|
||||
c = (u8)fmt[++i];
|
||||
}
|
||||
while (c >= '0' && c <= '9') {
|
||||
width = width * 10 + (c - '0');
|
||||
c = (u8)fmt[++i];
|
||||
}
|
||||
u32 precision = 4000;
|
||||
if (c == '.') {
|
||||
precision = 0;
|
||||
c = (u8)fmt[++i];
|
||||
if (c == '*') {
|
||||
precision = (u32)args[used++];
|
||||
c = (u8)fmt[++i];
|
||||
} else {
|
||||
while (c >= '0' && c <= '9') {
|
||||
if (precision < 4000) precision = precision * 10 + (c - '0');
|
||||
c = (u8)fmt[++i];
|
||||
}
|
||||
}
|
||||
if (precision > 4000) precision = 4000;
|
||||
}
|
||||
if (width > 256) width = 256;
|
||||
while (c == 'l' || c == 'z' || c == 'h') {
|
||||
longs += (c != 'h');
|
||||
c = (u8)fmt[++i];
|
||||
}
|
||||
if (c == '%') {
|
||||
put (g, '%');
|
||||
continue;
|
||||
}
|
||||
if (c == 0) {
|
||||
break;
|
||||
}
|
||||
u64 v = args[used++];
|
||||
if (c == 's') {
|
||||
const char *s = (const char *)v;
|
||||
u32 k;
|
||||
for (k = 0; k < precision; k++) {
|
||||
if (!readable (s + k, args)) {
|
||||
put (g, '<');
|
||||
number (g, (u64)(s + k), 16, 0, 0);
|
||||
put (g, '>');
|
||||
break;
|
||||
}
|
||||
u8 ch = (u8)s[k];
|
||||
if (!ch) break;
|
||||
put (g, ch);
|
||||
}
|
||||
} else if (c == 'x' || c == 'X') {
|
||||
number (g, longs ? v : (v & 0xFFFFFFFFu), 16, width, zero);
|
||||
} else if (c == 'u' || c == 'd') {
|
||||
number (g, longs ? v : (v & 0xFFFFFFFFu), 10, width, zero);
|
||||
} else if (c == 'p') {
|
||||
put (g, '0');
|
||||
put (g, 'x');
|
||||
number (g, v, 16, width, zero);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void text (struct ctx *g, const char *s)
|
||||
{
|
||||
while (*s) {
|
||||
put (g, (u8)*s++);
|
||||
}
|
||||
}
|
||||
|
||||
static void hex (struct ctx *g, const char *label, u64 v)
|
||||
{
|
||||
text (g, label);
|
||||
put (g, '0');
|
||||
put (g, 'x');
|
||||
number (g, v, 16, 16, 1);
|
||||
}
|
||||
|
||||
/* x0 = fmt as for panic(); x1 is set by the stub to the entry stack pointer. */
|
||||
__attribute__((noreturn, used, section(".text.b_main"))) void panic_screen_main (const char *fmt, const u64 *args)
|
||||
{
|
||||
volatile struct vc_info *v = (volatile struct vc_info *)VINFO;
|
||||
struct ctx g;
|
||||
u32 row, col;
|
||||
|
||||
__asm__ volatile ("msr daifset, #15");
|
||||
g.base = (u8 *)v->v_baseaddr;
|
||||
g.rb = v->v_rowbytes;
|
||||
g.w = v->v_width;
|
||||
g.h = v->v_height;
|
||||
if (g.base == 0 || v->v_depth != 32 || fmt == 0) {
|
||||
park ();
|
||||
}
|
||||
|
||||
/* First panic wins: a marker in the last row's first two pixels means a panic already drew
|
||||
* the screen; a later panic (another thread or exception) must not overwrite it. */
|
||||
volatile u32 *mark = (volatile u32 *)(g.base + (u64)(g.h - 1) * g.rb);
|
||||
if (mark[0] == 0xC0FFEE11u && mark[1] == 0x5CEEDA7Au) {
|
||||
park ();
|
||||
}
|
||||
g.x = 0;
|
||||
g.y = g.h / 8;
|
||||
g.log = 0;
|
||||
g.logn = 0;
|
||||
for (row = g.y; row < g.h; row++) {
|
||||
for (col = 0; col < g.w; col++) {
|
||||
*(volatile u32 *)(g.base + (u64)row * g.rb + (u64)col * 4) = 0;
|
||||
}
|
||||
}
|
||||
g.y += 8;
|
||||
/* The board's live banner keeps showing this text after the next swap paints over the screen (BootProgress.c,
|
||||
* boot_progress_panic_text). The first panic's text is kept: a buffer that already holds one is left alone. */
|
||||
if (PANIC_TEXT && *(volatile u8 *)PANIC_TEXT == 0) {
|
||||
g.log = (u8 *)PANIC_TEXT;
|
||||
}
|
||||
format (&g, fmt, args);
|
||||
/* The return addresses up the frame pointer chain: the exception entry links the panic's frames to the code that faulted, so this
|
||||
* names the callers the message leaves out. Stops at a pointer outside the kernel's range, a misaligned one or one that does not
|
||||
* move up the stack. */
|
||||
{
|
||||
const u64 *fp = (const u64 *)__builtin_frame_address (0);
|
||||
text (&g, "\nframes");
|
||||
for (unsigned i = 0; i < 24; i++) {
|
||||
const u64 at = (u64)fp;
|
||||
if (at < 0xfffffff000000000UL || (at & 15)) break;
|
||||
const u64 next = fp[0], lr = fp[1] & 0x0000007fffffffffUL; /* the return address without its pointer authentication bits */
|
||||
put (&g, ' ');
|
||||
number (&g, lr | 0xffffff8000000000UL, 16, 16, 1);
|
||||
if (next <= at) break;
|
||||
fp = (const u64 *)next;
|
||||
}
|
||||
}
|
||||
g.log = 0;
|
||||
__asm__ volatile ("dsb sy");
|
||||
/* Resolved from this candidate's symbols by the patcher, never fixed addresses.
|
||||
* Values are sampled at panic time and need not form an atomic VM snapshot. */
|
||||
(void)MAX_MEM;
|
||||
hex (&g, "\npages=", *(volatile const u32 *)PAGE_TOTAL);
|
||||
hex (&g, "\nfree=", *(volatile const u32 *)PAGE_FREE);
|
||||
/* The raw argument slots are not dumped: the formatted message above already carries them (room for the PANIC_TEXT copy). */
|
||||
mark[0] = 0xC0FFEE11u;
|
||||
mark[1] = 0x5CEEDA7Au;
|
||||
__asm__ volatile ("dsb sy");
|
||||
park ();
|
||||
}
|
||||
|
||||
/* Entry from panic(): keep the stack pointer, which addresses the variadic arguments. */
|
||||
__attribute__((naked, noreturn, section(".text.a_entry"))) void panic_screen (const char *fmt)
|
||||
{
|
||||
__asm__ volatile ("mov x1, sp\n\tb panic_screen_main");
|
||||
}
|
||||
|
||||
/* The real panic_trap_to_debugger signature takes (fmt, va_list *, ...).
|
||||
* Darwin ARM64 va_list is a stack pointer; its address is passed in x1.
|
||||
* The wrapper has already established its frame, so SP is NOT the argument
|
||||
* list here. Follow exactly the one pointer supplied by the real wrapper.
|
||||
*/
|
||||
__attribute__((naked, noreturn, section(".text.a_common"))) void panic_screen_common (void)
|
||||
{
|
||||
__asm__ volatile ("ldr x1, [x1]\n\tb panic_screen_main");
|
||||
}
|
||||
|
||||
#include "Font.h"
|
||||
__attribute__((section(".text.z_font"))) const u64 gFont[] = GLYPH_FONT;
|
||||
Reference in new issue
Block a user