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).
143 lines
7.5 KiB
Python
Executable File
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()
|