Files
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

333 lines
17 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""SNAP_FAST_IPI (docs/FAST-IPI-20261019.md): the lock-free Apple IPI registers, their
wiring into probes/xnuboot.c and compat/ipi.c, the report record and cycle.py's option.
python3 tools/test-fast-ipi.py
python3 tools/test-fast-ipi.py --build [--out DIR] [--expect SHA256]
Offline, no hardware. It builds and runs tools/test-fast-ipi.c under ASan/UBSan and under
TSan: a differential against compat/ipi.c as it was, the define's locked path against it,
and six threads posting, reading and acknowledging lock-free and under a mutex with
conservation checks. It requires weakened headers to fail that test. Source checks:
- the define defaults to 0 in both files, and compat/ipi.c without it preprocesses to
its original text;
- the fast handler repeats the locked IPI case's runnable mask and tail, and is reached
only for the three IPI registers, after the fast-trap admission;
- the header changes shared words only atomically.
The report record sits just below the thermal block, after every chained record, and
decodes (damaged ones are refused). cycle.py installed-fresh admits --fast-ipi only with
its prerequisites.
--build builds xnuboot with installed-fresh-20261019-v90's options with the define
absent, =0 and =1: absent == =0, =1 differs, all within the code budget; --expect
requires the default build to have that SHA-256.
Failure inventory: divergence from guest_ipi_route; a lost or torn IPI; a changed
refusal; a lock-free path without the admission; a changed locked path or default build;
a report record that overlaps or misdecodes; the option without its prerequisites.
"""
from pathlib import Path
import argparse
import hashlib
import json
import os
import re
import runpy
import struct
import subprocess
import sys
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-ipi.h').read_text()
IPI_C = (ROOT / 'compat/ipi.c').read_text()
DEFINE = '-DSNAP_FAST_IPI=1'
RENAME = ['-Dguest_ipi_access=ref_guest_ipi_access', '-Dguest_ipi_route=ref_guest_ipi_route',
'-Dguest_ipi_poll_delay=ref_guest_ipi_poll_delay', '-Dguest_ipi_fiq=ref_guest_ipi_fiq']
SANITIZERS = {'asan': ['-fsanitize=address,undefined', '-fno-sanitize-recover=all'], 'tsan': ['-fsanitize=thread']}
def arguments():
ap = argparse.ArgumentParser(add_help=False)
ap.add_argument('--build', action='store_true')
ap.add_argument('--out', help='fresh directory for the monitor builds (default: a temporary one)')
ap.add_argument('--expect', metavar='SHA256', help='the default build must have this SHA-256')
args, rest = ap.parse_known_args()
if args.out and Path(args.out).exists():
ap.error(f'--out {args.out} exists: use a fresh directory')
return args, rest
ARGS, REST = arguments() if __name__ == '__main__' else (argparse.Namespace(build=False, out=None, expect=None), [])
def original_ipi_c(text=IPI_C):
"""compat/ipi.c with every SNAP_FAST_IPI block resolved to its #else branch."""
text = text.replace('#ifndef SNAP_FAST_IPI\n#define SNAP_FAST_IPI 0\n#endif\n', '')
text = re.sub(r'#if SNAP_FAST_IPI\n.*?#else\n(.*?)#endif\n', r'\1', text, flags=re.S)
return text
def compile_test(directory, sanitizer='asan', header_dir=None):
directory = Path(directory)
opts = ['-O1', '-g', *SANITIZERS[sanitizer]]
common = ['cc', '-std=gnu11', '-Wall', '-Wextra', '-Werror', *opts]
reference = directory / 'ipi-reference.c'
reference.write_text(original_ipi_c())
ref = directory / 'ref.o'
subprocess.run([*common, f'-I{ROOT / "compat"}', '-c', str(reference), '-o', str(ref), *RENAME],
cwd=ROOT, check=True)
include = [f'-I{header_dir}'] if header_dir else []
exe = directory / 'test-fast-ipi'
r = subprocess.run([*common, DEFINE, *include, '-Icompat', 'tools/test-fast-ipi.c', 'compat/ipi.c',
str(ref), '-lpthread', '-o', str(exe)], cwd=ROOT, capture_output=True, text=True)
return exe if r.returncode == 0 else None
def run_test(exe, *argv):
r = subprocess.run([str(exe), *argv], capture_output=True, text=True, timeout=900)
return r.returncode, r.stdout + r.stderr
# Each replaces exactly one occurrence in compat/fast-ipi.h.
MUTANTS = {
'a post that is not one exchange': (
'uint64_t old = __atomic_exchange_n(&ipi->pending, 1, __ATOMIC_ACQ_REL);',
'uint64_t old = __atomic_load_n(&ipi->pending, __ATOMIC_RELAXED); '
'__atomic_store_n(&ipi->pending, 1, __ATOMIC_RELAXED);'),
'requests counted with a plain add': ('__atomic_fetch_add(&ipi->requests, 1, __ATOMIC_RELAXED);', 'ipi->requests++;'),
'coalesced not counted': ('__atomic_fetch_add(&ipi->coalesced, old, __ATOMIC_RELAXED);', '(void)old;'),
'an acknowledgement that always counts': (
'if (__atomic_exchange_n(&ipi->pending, 0, __ATOMIC_ACQ_REL)) ipi->acks++;',
'__atomic_store_n(&ipi->pending, 0, __ATOMIC_RELAXED); ipi->acks++;'),
'IPI_SR reads not counted': ('self->reads++;', '(void)self;'),
'IPI_SR accepts other bits': ('if (*value & ~UINT64_C(1)) return FAST_IPI_DECLINE_REFUSED;', ''),
'RR read accepted': (' if (read) return FAST_IPI_DECLINE_REFUSED;\n uint64_t allowed', ' uint64_t allowed'),
'RR_LOCAL takes the cluster from the value': (
'(reg == IPI_RR_LOCAL ? (affinities[source] & 0xff00) : ((*value >> 8) & 0xff00))',
'((*value >> 8) & 0xff00)'),
'a non-runnable target posted': ('if (!(runnable & (UINT64_C(1) << i))) return FAST_IPI_DECLINE_TARGET;\n', ''),
'duplicate affinities accepted': ('if (states[i] == states[j] || (affinities[i] & 0xffff) == (affinities[j] & 0xffff))',
'if (states[i] == states[j])'),
'a non-runnable source accepted': ('!(runnable & (UINT64_C(1) << source)) ||', ''),
'pending of 2 accepted': ('__atomic_load_n(&states[i]->pending, __ATOMIC_RELAXED) > 1 ||', ''),
'the wrong kind for RR_LOCAL': ('return reg == IPI_RR_LOCAL ? FAST_IPI_REQUEST_LOCAL : FAST_IPI_REQUEST_GLOBAL;',
'return FAST_IPI_REQUEST_GLOBAL;'),
'an unknown target refused instead of declined as a target': (
' }\n return FAST_IPI_DECLINE_TARGET;', ' }\n return FAST_IPI_DECLINE_REFUSED;'),
}
class Decision(unittest.TestCase):
def test_differential_and_concurrency(self):
for sanitizer in SANITIZERS:
with self.subTest(sanitizer=sanitizer), tempfile.TemporaryDirectory() as d:
exe = compile_test(d, sanitizer)
self.assertIsNotNone(exe, 'the C test does not build')
rc, out = run_test(exe)
self.assertEqual(rc, 0, out)
self.assertIn('PASS', out)
self.assertNotIn('WARNING: ThreadSanitizer', out)
print(out.strip().splitlines()[-1], f'({sanitizer})', file=sys.stderr)
def test_weakened_headers_fail(self):
survivors = []
for name, (old, new) in MUTANTS.items():
self.assertEqual(HEADER.count(old), 1, f'{name}: {old!r} is not unique')
with tempfile.TemporaryDirectory() as d:
(Path(d) / 'fast-ipi.h').write_text(HEADER.replace(old, new, 1))
exe = compile_test(d, 'asan', header_dir=d)
if exe is None:
survivors.append(name + ' (does not build)')
continue
rc, out = run_test(exe, '60000', '200000')
if rc == 0:
survivors.append(name)
self.assertEqual(survivors, [], 'weakened headers passed (or did not build)')
def body(text, signature):
start = text.index(signature)
depth, i = 0, text.index('{', start)
for j in range(i, len(text)):
depth += {'{': 1, '}': -1}.get(text[j], 0)
if depth == 0:
return text[i:j + 1]
raise ValueError(signature)
def squeeze(text):
return re.sub(r'\s+', '', re.sub(r'/\*.*?\*/', '', text, flags=re.S))
class Source(unittest.TestCase):
def test_defaults_and_ipi_c_text(self):
self.assertIn('#ifndef SNAP_FAST_IPI\n#define SNAP_FAST_IPI 0\n#endif', SOURCE)
self.assertIn('#ifndef SNAP_FAST_IPI\n#define SNAP_FAST_IPI 0\n#endif', IPI_C)
with tempfile.TemporaryDirectory() as d:
original = Path(d) / 'ipi.c'
original.write_text(original_ipi_c())
self.assertNotIn('SNAP_FAST_IPI', original.read_text())
def pre(path, *flags):
out = subprocess.run(['cc', '-E', '-P', f'-I{ROOT / "compat"}', *flags, str(path)], cwd=ROOT,
capture_output=True, text=True, check=True).stdout
return re.sub(r'\s+', ' ', out)
self.assertEqual(pre(ROOT / 'compat/ipi.c'), pre(original))
self.assertEqual(pre(ROOT / 'compat/ipi.c', '-DSNAP_FAST_IPI=0'), pre(original))
self.assertNotEqual(pre(ROOT / 'compat/ipi.c', DEFINE), pre(original))
def test_fast_handler_repeats_the_locked_case(self):
fast = squeeze(body(SOURCE, 'static int fast_ipi_trap('))
locked = squeeze(SOURCE[SOURCE.index('enum ipi_result ipi_status=IPI_UNHANDLED;'):
SOURCE.index('if (reg == CLOCK_ARCH_CNTP_CTL)')])
runnable = squeeze('''if(!(life&SERVICE_ADMISSION_CLOSED) && c->vcpus[i].prepared &&
((life>>(i*3))&7)==EXECUTOR_RUNNING) runnable|=UINT64_C(1)<<i;''')
self.assertIn(runnable, locked)
self.assertIn(runnable, fast)
self.assertIn('uint64_tlife=guest_service_lifecycle(&c->services);', locked)
self.assertIn('life=guest_service_lifecycle(&c->services);', fast)
# The locked case's tail, in order, after a handled result.
order = ['if(!fast_admitted(c,v,esr))', 'fast_ipi_access(states,affinities,GUEST_CPU_COUNT,runnable,',
'if(r>=FAST_IPI_DECLINES)return0;', 'if(reg==IPI_SR&&value)v->ipi_ack_pc=*elr;',
'elseif(reg!=IPI_SR)v->ipi_request_pc=*elr;', 'guest_interrupt_sync(c);',
'if(is_read&&rt!=31)frame[rt]=value;', '*elr+=4;', 'c->perf.fast[v->slot].sysreg++;']
at = -1
for token in order:
self.assertIn(token, fast[at + 1:], token)
at = fast.index(token, at + 1)
for token in ('vcpu_of(c)->ipi_ack_pc=*elr;', 'guest_interrupt_sync(c);', 'if(is_read&&rt!=31)frame[rt]=value;'):
self.assertIn(token, locked)
# Nothing else shared: no record, no c->traps, no other vCPU's fields.
for forbidden in ('record(', 'c->traps', 'guest_ipi_route(', 'timer_arm(', 'step_arm('):
self.assertNotIn(forbidden, fast, forbidden)
def test_dispatch(self):
trap = squeeze(body(SOURCE, 'static int fast_vcpu_trap('))
hook = ('#ifSNAP_FAST_IPIif(reg==IPI_RR_LOCAL||reg==IPI_RR_GLOBAL||reg==IPI_SR)'
'returnfast_ipi_trap(c,v,frame,esr,elr);#endif')
self.assertIn(hook, trap)
self.assertLess(trap.index(hook), trap.index('cls=fast_trap_sysreg_class('))
self.assertEqual(SOURCE.count('fast_ipi_trap('), 2)
self.assertIn('_Static_assert(SNAP_FAST_VCPU_TRAPS && SNAP_ROOT_CACHE_WIDE,', SOURCE)
def test_header_writes_shared_words_atomically(self):
code = re.sub(r'/\*.*?\*/', '', HEADER, flags=re.S)
for field in ('pending', 'requests', 'coalesced', 'last_request'):
plain = re.findall(rf'->{field}\s*(?:[-+]?=(?!=)|\+\+|--)', code)
self.assertEqual(plain, [], f'a plain write of {field}')
# Only this vCPU's own counters change with plain increments.
self.assertEqual(sorted(re.findall(r'(\w+)->(\w+)\+\+', code)), [('ipi', 'acks'), ('self', 'reads')])
def record(lay_size=128, results=None, ticks=0, longest=0, damage=None):
words = [0x3149504954534146, lay_size << 32]
for cpu in range(6):
r = results[cpu] if results else [0] * 7
cpu_words = list(r) + [ticks, longest] + [0] * 7
words += cpu_words
data = bytearray(struct.pack(f'<{len(words)}Q', *words))
if damage:
damage(data)
return bytes(data)
class Report(unittest.TestCase):
def test_place_and_decode(self):
pc = runpy.run_path(str(ROOT / 'tools/perf_counters.py'))
offset, size = pc['fast_ipi_spec']([f for f in flags_v90(True) if f.startswith('-D')])
thermal = runpy.run_path(str(ROOT / 'tools/a16_thermal.py'))['REPORT_OFFSET']
self.assertEqual(size, 16 + 6 * 128)
self.assertEqual(offset + size, thermal)
# The C side's OUT_FAST_IPI, from the same headers.
with tempfile.TemporaryDirectory() as d:
probe = Path(d) / 'probe.c'
probe.write_text('#include <stdio.h>\n#include "a16-thermal.h"\n#include "fast-ipi.h"\n'
'int main(void){printf("%lu\\n",(unsigned long)(16+8*(A16_THERMAL_REPORT_OFFSET/8-2-'
'6*(sizeof(struct fast_ipi_cpu)/8))));return 0;}\n')
subprocess.run(['cc', '-std=gnu11', f'-I{ROOT / "compat"}', str(probe), '-o', f'{d}/probe'], check=True)
self.assertEqual(int(subprocess.run([f'{d}/probe'], capture_output=True, text=True).stdout), offset)
self.assertIn('#define OUT_FAST_IPI (OUT_THERMAL - 2 - GUEST_CPU_COUNT*FAST_IPI_WORDS)', SOURCE)
lay = dict(size=128, result=0, ticks=56, max=64, reserved=72)
results = [[3, 4, 5, 6, 1, 0, 2]] * 6
out = pc['decode_fast_ipi'](record(results=results, ticks=100, longest=10), lay=lay, hz=19.2e6)
self.assertEqual(out['summary']['handled'], 6 * 18)
self.assertEqual(out['summary']['declined'], 6 * 3)
self.assertEqual(out['cpus'][0]['result']['request_global'], 6)
for name, damage in (('magic', lambda b: b.__setitem__(0, 0)),
('size word', lambda b: struct.pack_into('<Q', b, 8, 64 << 32)),
('reserved', lambda b: struct.pack_into('<Q', b, 16 + 72, 1))):
with self.subTest(damage=name), self.assertRaises(ValueError):
pc['decode_fast_ipi'](record(results=results, ticks=100, longest=10, damage=damage), lay=lay)
with self.assertRaises(ValueError): # time without a handled access
pc['decode_fast_ipi'](record(ticks=5, longest=5), lay=lay)
def installed(**extra):
sys.path.insert(0, str(ROOT / 'tools'))
import cycle_lib.installed_fresh as fresh # noqa: E402
import cycle_lib.speed_test as st # noqa: E402
sets, _ = st.flag_sets(ROOT)
v90 = runpy.run_path(str(ROOT / 'tools/test-aes-bitslice.py'))['V90']
args = argparse.Namespace(variant='fast', **{n: True for n in v90})
for k, v in extra.items():
setattr(args, k, v)
return fresh, sets, args
def flags_v90(on=False, **extra):
fresh, sets, args = installed(fast_ipi=on, **extra)
return fresh.variant_flags(sets, args)
class Cycle(unittest.TestCase):
def test_option_and_prerequisites(self):
off, on = flags_v90(), flags_v90(True)
self.assertNotIn(DEFINE, off)
self.assertEqual([f for f in on if f != DEFINE], off)
self.assertEqual(on.count(DEFINE), 1)
fresh, sets, _ = installed()
from cycle_lib.common import Refused # noqa: E402
for variant, wide in (('fast', False), ('control', True)):
args = argparse.Namespace(variant=variant, fast_ipi=True, root_cache_wide=wide)
with self.subTest(variant=variant, root_cache_wide=wide), self.assertRaises(Refused) as caught:
fresh.variant_flags(sets, args)
self.assertIn('--fast-ipi needs', str(caught.exception))
self.assertIn("p.add_argument('--fast-ipi', action='store_true',", (ROOT / 'tools/cycle.py').read_text())
@unittest.skipUnless(ARGS.build, 'needs --build')
class Build(unittest.TestCase):
def test_monitor_builds(self):
sys.path.insert(0, str(ROOT / 'probes'))
import run # noqa: E402
out = Path(ARGS.out) if ARGS.out else Path(tempfile.mkdtemp(prefix='fast-ipi-build-'))
budget = run.OUT_AT - run.CODE_AT
saved = list(run.FLAGS)
results, blobs = {}, {}
base = flags_v90()
try:
for label, flags in (('default', base), ('off', base + ['-DSNAP_FAST_IPI=0']), ('on', flags_v90(True))):
run.FLAGS[:] = flags
blob = blobs[label] = run.build('xnuboot', out / label)
results[label] = dict(bytes=len(blob), headroom=budget - len(blob),
sha256=hashlib.sha256(blob).hexdigest())
self.assertGreater(budget - len(blob), 0, label)
finally:
run.FLAGS[:] = saved
self.assertEqual(blobs['default'], blobs['off'])
self.assertNotEqual(blobs['default'], blobs['on'])
if ARGS.expect:
self.assertEqual(results['default']['sha256'], ARGS.expect, 'the default build changed')
(out / 'result.json').write_text(json.dumps(results, indent=1) + '\n')
print(json.dumps(results, indent=1), file=sys.stderr)
if __name__ == '__main__':
os.chdir(ROOT)
unittest.main(argv=[sys.argv[0], *REST])