Files
Snapintosh/tools/test-fast-trap.py
Acelogic f47befb905 probes: split xnuboot.c into ordered parts
Phase 1 of decomposing the 15,770-line monitor unit: move contiguous
top-level ranges verbatim into probes/xnuboot/*.c, and have
probes/xnuboot.c #include them in their original order. The compiler
still sees one translation unit. Every #include of a header stays in
probes/xnuboot.c, because quoted includes resolve beside the including
file and the monitor build has no -Icompat.

Cuts fall only between top-level declarations; no function, struct,
initializer or conditional is split, and every part has balanced
#if/#endif. Two exceptions are recorded for phase 2:
- monitor_call_impl is 1,318 lines on its own, so
  probes/xnuboot/sptm_dispatch.c is 1,370 lines.
- The 1,894-line #if SNAP_FINE_LOCKS region is split into three
  consecutive identical #if SNAP_FINE_LOCKS blocks (fine_locks_*.c).
  SNAP_FINE_LOCKS is not defined or undefined inside the region, and
  nothing inside it includes a header.
The parts that carry the m1n1-derived SPTM endpoint numbers and return
codes (report_layout.c, sptm_dispatch.c) keep the file's notice.

Tools that read xnuboot.c as text now read the whole unit through
tools/source_text.py read_source(); tools that hash or pin monitor
sources add every part through source_files() (generic over each .c
in their lists, so later splits are covered the same way).

Verification (base 84dd208 vs this commit, clang 23.1.3):
- clang -c, qualification flags (b12v20 qualification.json, no -flto)
  plus -I shadow -I q1n1/platform/uefi: probes/xnuboot.c objects
  byte-identical (cmp). PASS
- clang -c, probes/run.py FLAGS plus the same -I: byte-identical. PASS
- Qualification flags with -flto: llvm-dis output equal; the full IR
  differs only in 468 inline-asm !srcloc source-location cookies. PASS
- probes/run.py build('xnuboot'), every unit linked: xnuboot.bin and
  xnuboot.elf byte-identical with the qualification flags (LTO,
  bin sha256 0d8b9ec3...) and with run.py FLAGS (2b5d5f6b...). PASS
- Reconstruction: expanding the includes and dropping the part headers
  and the duplicated #if/#endif lines reproduces xnuboot.c exactly.
- Host battery (63 commands: the two gcc SPTM suites, test-idregs,
  test-audit-sptm-refs and every tools/test-*.py that reads these
  sources, plus the edited tools), 300 s limit, base vs split:
  no new failure. 76 of 78 outcomes equal; test-audit-sparse and
  test-monitor-perf timed out (NOT RUN) on split under host load and
  on rerun match base (PASS; FAIL with the same record). Per-class
  reruns of the long suites (21 commands) equal on both trees.
  Pre-existing failures (missing private inputs or stale checks) are
  unchanged.
- wc -l: xnuboot.c 918, every part under 1,000 except sptm_dispatch.c
  (1,370).
2026-10-23 04:04:44 -04:00

143 lines
7.5 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""SNAP_FAST_VCPU_TRAPS: bind the lock-free classes to the actual locked chain.
tools/test-fast-trap.c runs compat/fast-trap.h against a transcription of
xnuboot_trap_locked's EC 0x18 chain. This test reads probes/xnuboot.c and
fails if that transcription no longer matches: a new switch case, a new
`reg ==` handler, or a reordered handler chain means the fast classes must be
re-derived before the fast path may stay enabled. It also builds and runs the
C test (sanitized, thread-sanitized) and checks that the pre-change trap
entry model fails the lock regression.
"""
from pathlib import Path
import os
import re
import subprocess
import tempfile
import unittest
from source_text import read_source
ROOT = Path(__file__).resolve().parents[1]
SOURCE = read_source(ROOT / 'probes/xnuboot.c')
HEADER = (ROOT / 'compat/fast-trap.h').read_text()
ORACLE = (ROOT / 'tools/test-fast-trap.c').read_text()
def sysreg(op0, op1, crn, crm, op2):
return (op0 << 20) | (op2 << 17) | (op1 << 14) | (crn << 10) | (crm << 1)
def chain():
start = SOURCE.index('static int xnuboot_trap_locked(')
body = SOURCE[start:SOURCE.index('\nint xnuboot_trap(', start)]
traps = body.index('c->traps++;')
switch = body.index('switch (reg) {', traps)
end = body.index('default:', switch)
return body, traps, switch, end
class FastTrapSource(unittest.TestCase):
def test_switch_cases_match_the_oracle(self):
body, traps, switch, end = chain()
cases = re.findall(r'case (SR_\w+):', body[switch:end])
defined = {name: sysreg(*map(int, args)) for name, *args in re.findall(
r'#define (SR_\w+)\s+SYSREG\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)\)', SOURCE)}
oracle = {name: sysreg(*map(int, args)) for name, *args in re.findall(
r'#define (SR_\w+) SR\((\d+),(\d+),(\d+),(\d+),(\d+)\)', ORACLE)}
self.assertEqual(sorted(cases), sorted(oracle))
for name in cases:
self.assertEqual(defined[name], oracle[name], name)
# ESR/FAR reads are hardware register reads in the locked path.
for name, reg in (('SR_ESR_EL1', 'esr_el1'), ('SR_FAR_EL1', 'far_el1')):
case = body[body.index(f'case {name}:', switch):]
case = case[:case.index('break;')]
self.assertIn(f'mrs %0, {reg}', case)
def test_handler_chain_order(self):
body, traps, switch, end = chain()
region = body[traps:switch]
order = ['id_forge(', 'pauth_user_reg(', 'guest_ipi_route(', 'guest_timer_idle_access(',
'CLOCK_TIMER_UNSUPPORTED', 'arch_counter_reads', 'guest_timer_access(',
'guest_clock_access(', 'sprr_access_with(']
positions = [region.index(token) for token in order]
self.assertEqual(positions, sorted(positions))
# Every register the chain claims before the switch is a known one.
claimed = set(re.findall(r'reg\s*==\s*(\w+)', region))
self.assertEqual(claimed, {'IPI_RR_LOCAL', 'IPI_RR_GLOBAL', 'IPI_SR', 'CLOCK_ARCH_CNTP_CTL',
'CLOCK_ARCH_CNTVCT', 'CLOCK_ARCH_CNTVCTSS', 'CLOCK_CNTV_CTL',
'CLOCK_CNTV_TVAL', 'SPRR_UPERM'})
# PACGA: the locked EC 0x09 branch precedes the EC 0x18 chain and has
# exactly the operations fast_trap_emulate performs.
pacga = body[body.index('if (ec == 0x09 && generic_pauth_supported())'):traps]
for token in ('guest_pa(*elr)', 'pa >= p->mem_low && pa <= p->mem_high - 4',
'sp_el1', 'sp_el0', 'pauth_pacga(word, frame, sp, generic_sign, c)',
'pacga_calls++', '*elr += 4'):
self.assertIn(token, pacga)
def test_ignored_registers_are_unclaimed(self):
block = HEADER[HEADER.index('fast_trap_ignored[FAST_TRAP_IGNORED] = {'):]
block = block[:block.index('};')]
ignored = [sysreg(*map(int, args)) for args in
re.findall(r'FAST_TRAP_SYSREG\((\d+),(\d+),(\d+),(\d+),(\d+)\)', block)]
self.assertEqual(len(ignored), 20)
claimed = set()
for header in ('compat/clock.h', 'compat/ipi.h', 'compat/sprr.h'):
text = (ROOT / header).read_text()
for args in re.findall(r'_SYSREG\((\d+),(\d+),(\d+),(\d+),(\d+)\)', text):
claimed.add(sysreg(*map(int, args)))
sprr = (ROOT / 'compat/sprr.h').read_text()
for op2, crm in re.findall(r'SPRR_ISS\((\d+)u,(\d+)u\)', sprr):
claimed.add(sysreg(3, 6, 15, int(crm), int(op2)))
claimed |= {sysreg(*map(int, args)) for _, *args in re.findall(
r'#define (SR_\w+)\s+SYSREG\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)\)', SOURCE)}
for reg in ignored:
self.assertNotIn(reg, claimed, hex(reg))
# Not in the TID3 ID group (Op1=0, CRn=0, CRm<8), which answers first.
self.assertFalse(((reg >> 14) & 7) == 0 and ((reg >> 10) & 15) == 0 and
((reg >> 1) & 15) < 8, hex(reg))
def test_zero_class_cases_are_constant_zero(self):
# docs/FAST-TRAP-BATCH-20261016.md failure mode 2: a register is answered
# zero without the lock only where xnuboot_trap_locked's switch case is
# exactly "reads zero" with no record or other effect.
block = HEADER[HEADER.index('static inline int fast_trap_zero_reg('):]
block = block[:block.index('\n}')]
regs = [sysreg(*map(int, a)) for a in
re.findall(r'FAST_TRAP_SYSREG\((\d+),(\d+),(\d+),(\d+),(\d+)\)', block)]
self.assertEqual(len(regs), 1)
names = {sysreg(*map(int, a)): n for n, *a in re.findall(
r'#define (SR_\w+)\s+SYSREG\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)\)', SOURCE)}
body, traps, switch, end = chain()
for reg in regs:
case = body[body.index(f'case {names[reg]}:', switch):]
case = case[len(f'case {names[reg]}:'):case.index('break;')]
case = re.sub(r'/\*.*?\*/', '', case, flags=re.S)
self.assertEqual(' '.join(case.split()), 'if (is_read) value = 0;', names[reg])
def test_native_behaviour(self):
flags = ['-std=gnu11', '-O1', '-g', '-Wall', '-Wextra', '-Icompat', '-Ishadow']
sources = ['tools/test-fast-trap.c', 'compat/pauth.c', 'compat/pauth-user.c',
'compat/clock.c', 'compat/service-lock.c']
with tempfile.TemporaryDirectory() as directory:
d = Path(directory)
builds = {'asan': ['-fsanitize=address,undefined', '-fno-sanitize-recover=all'],
'tsan': ['-fsanitize=thread', '-pthread', '-DFAST_TRAP_THREADS=1'],
'pre': ['-fsanitize=address,undefined', '-DFAST_TRAP_PRE_CHANGE=1']}
for name, extra in builds.items():
subprocess.run(['cc', *flags, *extra, *sources, '-o', str(d / name)],
cwd=ROOT, check=True, timeout=120)
env = dict(os.environ, TSAN_OPTIONS='halt_on_error=1:exitcode=66')
for name in ('asan', 'tsan'):
result = subprocess.run([str(d / name)], capture_output=True, text=True,
timeout=600, env=env)
self.assertEqual(result.returncode, 0, result.stderr[-2000:])
self.assertIn('PASS:', result.stdout)
pre = subprocess.run([str(d / 'pre')], capture_output=True, text=True, timeout=600)
self.assertNotEqual(pre.returncode, 0)
self.assertIn('e.acquisitions<=5', pre.stderr)
if __name__ == '__main__':
unittest.main()