Files
Snapintosh/tools/test-root-miss.py
T
Acelogic 4f4afd1c5c compat: split sptm-root.c into ordered parts
Move compat/sptm-root.c's top-level ranges verbatim into
compat/sptm-root/{views,view_updates,linked_switch}.c and #include them
from sptm-root.c in their original order. Cuts fall only between
top-level definitions; every part has balanced conditionals.

sptm-root.c keeps its includes and MASK, and also its final
#if SNAP_FINE_SWITCH block (93 lines): that block includes
fine-lock.h, which resolves only beside sptm-root.c (the monitor build
has no -Icompat), and fine-lock.h itself carries a guarded
#define SNAP_FINE_SWITCH, so the block is not split into two
conditionals either.

Tools that read sptm-root.c as text now read the whole unit through
read_source(): test-descriptor-stores, test-el0-memo (the four
active-root stores), test-root-miss and test-user-hw-walk (source text
and mutants), and the mutant copies of test-root-miss, test-root-wide
and test-fine-locks.

Verification (base 84dd208 vs this commit):
- clang -c with the qualification flags (no -flto) and with
  probes/run.py FLAGS, each plus -I shadow -I q1n1/platform/uefi:
  compat/sptm-root.c objects byte-identical (cmp). PASS
- Qualification flags with -flto: llvm-dis output identical and the
  bitcode byte-identical. PASS
- probes/run.py build('xnuboot') with the qualification flags:
  xnuboot.bin unchanged (sha256 0d8b9ec3...). PASS
- Reconstruction from the parts reproduces sptm-root.c exactly.
- Host subset (59 commands, 300 s limit), against base 84dd208 with
  base's host-load timeouts rerun: every outcome equal except
  test-fine-locks --quick, which adds test_unfair_mutants_fail. That
  fairness mutant check depends on thread timing: rerun three times
  on base it failed twice the same way (a surviving unfair mutant in
  compat/fine-lock.h or service-lock.c, which this change does not
  touch) and passed once; on this tree it passed once and timed out
  twice. Not a new failure.
- wc -l: sptm-root.c 108, parts 359 to 540 lines.
2026-10-23 04:04:44 -04:00

311 lines
17 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""SNAP_ROOT_MISS_FAST: bind compat/root-miss.h's victim choice and miss-path
verdicts to SWITCH_ROOT, the linked build, the monitor and its defaults.
python3 tools/test-root-miss.py
python3 tools/test-root-miss.py --build [--out DIR]
[--expect control=SHA256 --expect fast=SHA256 --expect fast-wfi=SHA256]
Offline, no hardware and no proxy. It builds and runs (sanitized)
tools/test-root-miss-cache.c (the victim oracle and a 160-root churn against
an independent model of the cache: round robin without stamps, least recently
installed with them; also built without the define) and
tools/test-root-miss-build.c (memo-assisted misses and linked builds through
the real SWITCH_ROOT/NEST/MAP_PAGE fixtures), then requires a weakened copy of
each guard (compat/root-miss.h failure modes E1-E5, M1, M4, M5) to FAIL them.
The existing root, memo, word-walk and linked-nest suites must print the same
results with the define as without it. It checks that probes/xnuboot.c
defaults the define to 0, asserts its prerequisites, passes stamps, linked
builds and the read-only group probe only under it, and that struct context
keeps its size.
--build also compiles xnuboot with cycle.py's installed-fresh/speed-test flag
sets (control and fast) with the define absent, =0 and =1, and fast with
SNAP_WFI_RESUME=1 absent/=1 for the define: -Werror, the code budget, absent ==
=0, =1 differs. --expect NAME=SHA256 (control, fast, fast-wfi) requires the
default build to equal a pre-change monitor byte for byte. It reports the
define-on SHA-256 of the full fast stack (census, WFI resume, EL0 sysreg, service
subclass, DART wiredcnt, panic serve).
Failure inventory: an active root evicted; a valid shadow evicted before an
invalidated slot; no victim though one exists; eviction counters changed; a
retained verdict standing in for a table whose leaves are copied (private,
commpage, 4K-format, or a shared child whose group the build creates); memo
calls counting misses; the sampled reference walk using the memo; a default
build that changed; a layout change visible to host readers.
"""
from pathlib import Path
import argparse
import ast
import hashlib
import importlib
import json
import os
import re
import shutil
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')
ROOT_C = read_source(ROOT / 'compat/sptm-root.c')
HEADER = (ROOT / 'compat/root-miss.h').read_text()
DEFINE = '-DSNAP_ROOT_MISS_FAST=1'
CORE = ['compat/sptm-root.c', 'compat/sptm-user.c', 'compat/sptm.c', 'compat/sptm-refs.c',
'compat/sptm-unmap.c', 'compat/sptm-update.c', 'compat/txm.c', 'compat/sptm-papt.c',
'compat/sptm-slide.c', 'shadow/shadow.c', 'shadow/mmu.c', 'shadow/sprr-view.c']
FULL_STACK = ['-DSNAP_WFI_RESUME=1', '-DSNAP_FAST_EL0_SYSREG=1', '-DSNAP_SERVICE_SUBCLASS=1',
'-DSNAP_DART_WIREDCNT=1', '-DSNAP_PANIC_SERVE=1']
def arguments():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
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', action='append', default=[], metavar='NAME=SHA256',
help='control, fast or fast-wfi: 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=[]), [])
def between(text, start, end, after=0):
begin = text.index(start, after)
return text[begin:text.index(end, begin)]
def compile_c(directory, test, defines, overrides=None, werror=True, extra=()):
"""Build tools/<test> with CORE; overrides maps a compat file name to a
mutated copy placed (with root-miss.h) in a private directory first."""
exe = Path(directory) / Path(test).stem
include, sources = [], [*CORE, *extra]
if overrides:
mut = Path(directory) / 'mut'
mut.mkdir(exist_ok=True)
for name in ('root-miss.h', 'sptm-root.c'):
(mut / name).write_text(overrides.get(name, read_source(ROOT / 'compat' / name)))
include = [f'-I{mut}']
sources = [str(mut / 'sptm-root.c') if s == 'compat/sptm-root.c' else s for s in sources]
flags = ['cc', '-std=gnu11', '-O1', '-g', '-Wall', '-Wextra', *(['-Werror'] if werror else []),
'-fsanitize=address,undefined', '-fno-sanitize-recover=all', *defines, *include,
'-Icompat', '-Ishadow', f'tools/{test}', *sources, '-o', str(exe)]
subprocess.run(flags, cwd=ROOT, check=True, timeout=300, capture_output=True)
return exe
def run(exe):
return subprocess.run([str(exe)], cwd=ROOT, capture_output=True, text=True, timeout=900)
TESTS = {
'cache': ('test-root-miss-cache.c', ()),
'build': ('test-root-miss-build.c', ('shadow/native-shared.c',)),
}
# Each weakening must make the named C test fail: (file, old, new, test).
MUTANTS = {
'E1 an active root is evicted': ('root-miss.h',
' unsigned i=sptm_root_reclaim_pick(s,stamp,SPTM_ROOT_MASK_NONE);\n',
' unsigned i=SPTM_ROOT_CACHE;\n for(unsigned k=0;k<SPTM_ROOT_CACHE;k++)\n'
' if(s->cache[k].root && (i==SPTM_ROOT_CACHE || stamp[k]<stamp[i]))i=k;\n', 'cache'),
'E3 a valid shadow before an invalid slot': ('root-miss.h',
' unsigned i=sptm_root_reclaim_pick(s,stamp,SPTM_ROOT_MASK_NONE);\n',
' unsigned i=SPTM_ROOT_CACHE;\n for(unsigned k=0;k<SPTM_ROOT_CACHE;k++) {\n'
' int a=0;for(unsigned c=0;c<SPTM_ROOT_CPUS;c++)a|=s->active[c]==s->cache[k].root;\n'
' if(s->cache[k].root && !a && (i==SPTM_ROOT_CACHE || stamp[k]<stamp[i]))i=k;\n }\n',
'cache'),
'E4 an eligible slot is skipped': ('root-miss.h',
'sptm_root_reclaim_pick(s,stamp,SPTM_ROOT_MASK_NONE)', 'sptm_root_reclaim_pick(s,stamp,1)', 'cache'),
'E5 eviction not counted': ('sptm-root.c',
'b->stamp,root)))return SPTM_MEMORY_UNAVAILABLE;\n entry->valid=0;entry->root=0;'
's->invalidations++;\n if(b->evictions)(*b->evictions)++;\n',
'b->stamp,root)))return SPTM_MEMORY_UNAVAILABLE;\n entry->valid=0;entry->root=0;'
's->invalidations++;\n', 'cache'),
'E5 eviction not an invalidation': ('sptm-root.c',
'b->stamp,root)))return SPTM_MEMORY_UNAVAILABLE;\n entry->valid=0;entry->root=0;'
's->invalidations++;\n',
'b->stamp,root)))return SPTM_MEMORY_UNAVAILABLE;\n entry->valid=0;entry->root=0;\n',
'cache'),
'M1 a miss answers any level-3 table': ('root-miss.h',
' return !shared_only || type==SPTM_MAC_PAGE_TABLE_SHARED;\n',
' return ((void)shared_only,(void)type,1);\n', 'build'),
'M1 a created group is not scanned': ('root-miss.h',
' return grouped!=0;\n', ' return ((void)grouped,1);\n', 'build'),
'M1 the linked build answers copied tables': ('sptm-root.c',
'struct validation v={m,&g,flags,0,0,root,0,memo,1};',
'struct validation v={m,&g,flags,0,0,root,0,memo,0};', 'build'),
'M1 a 4K-format miss uses verdicts': ('sptm-root.c',
'int shared_only=!hit && b->linked_builds && !format;',
'int shared_only=!hit && b->linked_builds;', 'build'),
'M4 misses counted as memo calls': ('sptm-root.c',
'if(walk && memo && hit)memo->stats.calls++;', 'if(walk && memo)memo->stats.calls++;', 'build'),
'M5 the reference walk uses the memo': ('sptm-root.c',
'struct validation bytes={m,&g,flags,0,0,root,0,0,0};',
'struct validation bytes={m,&g,flags,0,0,root,0,memo,shared_only};', 'build'),
}
class Decision(unittest.TestCase):
def test_cache_and_build(self):
with tempfile.TemporaryDirectory() as d:
for name, (test, extra) in TESTS.items():
for define in ([DEFINE], ['-DSNAP_ROOT_MISS_FAST=0']) if name == 'cache' else ([DEFINE],):
result = run(compile_c(d, test, define, extra=extra))
self.assertEqual(result.returncode, 0, f'{name} {define}: ' +
result.stdout[-3000:] + result.stderr[-3000:])
self.assertTrue(result.stdout.rstrip().splitlines()[-1].startswith('PASS'), name)
for line in result.stdout.strip().splitlines():
if not line.startswith('PASS: 7489') and not line.startswith('PASS: 43497'):
print(f'{name} {define[0]}: {line}', file=sys.stderr)
def test_weakened_guards_fail(self):
for label, (name, old, new, test) in MUTANTS.items():
text = HEADER if name == 'root-miss.h' else ROOT_C
with self.subTest(label), tempfile.TemporaryDirectory() as d:
self.assertEqual(text.count(old), 1, label)
source, extra = TESTS[test]
result = run(compile_c(d, source, [DEFINE], {name: text.replace(old, new)}, extra=extra))
self.assertNotEqual(result.returncode, 0, f'{label} survived')
def test_existing_suites_unchanged_by_the_define(self):
suites = {'test-sptm-root.c': (), 'test-root-validate-memo.c': (),
'test-native-shared-nest.c': ('shadow/native-shared.c',)}
with tempfile.TemporaryDirectory() as d:
for suite, extra in suites.items():
outs = []
for define in ([], [DEFINE]):
sub = Path(d) / ('on' if define else 'off')
sub.mkdir(exist_ok=True)
result = run(compile_c(sub, suite, define, werror=False, extra=extra))
self.assertEqual(result.returncode, 0, suite + result.stdout[-2000:] + result.stderr[-2000:])
outs.append(result.stdout)
self.assertEqual(outs[0], outs[1], suite)
words = []
for define in ([], [DEFINE]):
sub = Path(d) / ('w-on' if define else 'w-off')
sub.mkdir(exist_ok=True)
result = run(compile_c(sub, 'test-root-validate-words.c',
['-DSPTM_ROOT_VALIDATE_SAMPLE=0', *define], werror=False))
self.assertEqual(result.returncode, 0, result.stdout[-2000:] + result.stderr[-2000:])
words.append(result.stdout)
self.assertEqual(words[0], words[1])
class Source(unittest.TestCase):
def test_default_off_and_prerequisites(self):
self.assertIn('#ifndef SNAP_ROOT_MISS_FAST\n#define SNAP_ROOT_MISS_FAST 0\n#endif\n'
'_Static_assert(!SNAP_ROOT_MISS_FAST || (SNAP_ROOT_VALIDATE_MEMO && '
'SNAP_ROOT_VALIDATE_WORDS &&\n SNAP_NATIVE_SHARED),', SOURCE)
header = (ROOT / 'compat/sptm-root.h').read_text()
self.assertIn('#ifndef SNAP_ROOT_MISS_FAST\n#define SNAP_ROOT_MISS_FAST 0\n#endif', header)
self.assertIn('#if SNAP_ROOT_MISS_FAST\n#include "root-miss.h"\n#endif', ROOT_C)
def test_monitor_wiring_only_under_the_define(self):
backend = between(SOURCE, 'case SPTM_FN_SWITCH_ROOT: {', 'enum sptm_result r=')
self.assertIn('#if SNAP_ROOT_MISS_FAST\n ,.stamp=USTAMPS(c),.linked_builds=1\n#endif', backend)
build = between(SOURCE, 'static int user_root_build(void *ctx', '\n}\n')
on = between(build, '#if SNAP_ROOT_MISS_FAST', '#else')
self.assertIn('sptm_root_build_linked_memo(memory,&s,root,format,flags,leaves,&links,\n'
' &c->root_memo,nl_grouped)', on)
off = between(build, '#else', '#endif', build.index('#if SNAP_ROOT_MISS_FAST'))
self.assertEqual([l.strip() for l in off.splitlines()[1:] if l.strip()],
['sptm_root_build_linked(memory,&s,root,format,flags,leaves,&links));'])
# As before: the 4K-format copy and the !SNAP_NATIVE_SHARED copy, nothing added.
self.assertEqual(build.count('sptm_root_build_shadow('), 2)
grouped = between(SOURCE, 'static int nl_grouped(void *ctx,uint64_t child,uint64_t va)\n{', '\n}\n')
self.assertEqual([l.strip() for l in grouped.splitlines()[2:]],
['struct native_links *l=ctx;(void)va;',
'return native_shared_find(&UNS(l->c),child)>=0;'])
# The stamps are the ones every installation writes and reclaim reads.
# USTAMPS(c): root_stamp, or the wide cache's 192 (compat/root-wide.h).
self.assertEqual(SOURCE.count('USTAMPS(c)[entry-c->user_roots.cache]=++c->arena_reclaim.clock;'), 1)
self.assertEqual(SOURCE.count('.stamp=USTAMPS(c)'), 1)
def test_compat_changes_are_define_guarded(self):
# Every SNAP_ROOT_MISS_FAST block in compat/sptm-root.c has an #else
# that restores the original line, or adds code only when on.
blocks = re.findall(r'#if SNAP_ROOT_MISS_FAST\n(.*?)#endif', ROOT_C, re.S)
self.assertGreaterEqual(len(blocks), 8)
off = subprocess.run(['cc', '-E', '-P', '-DSNAP_ROOT_MISS_FAST=0', '-Icompat', '-Ishadow',
'compat/sptm-root.c'], cwd=ROOT, capture_output=True, text=True, check=True).stdout
self.assertNotIn('root_miss_', off)
self.assertNotIn('shared_only', off)
self.assertNotIn('build_linked_memo', off)
class Layout(unittest.TestCase):
def test_context_unchanged(self):
sys.path.insert(0, str(ROOT / 'tools'))
perf = importlib.import_module('perf_counters')
sets, _ = flag_sets()
for name, flags in sets.items():
defines = [f for f in flags if f.startswith('-D')]
off = perf._records(ROOT / 'probes/xnuboot.c', defines)
on = perf._records(ROOT / 'probes/xnuboot.c', defines + [DEFINE])
self.assertEqual(perf._size(off['context']), perf._size(on['context']), name)
self.assertEqual(perf._size(off['sptm_root_state']), perf._size(on['sptm_root_state']), name)
self.assertEqual(perf._offset(on['context'], 'root_stamp'), perf._offset(off['context'], 'root_stamp'))
def flag_sets():
"""cycle_lib/speed_test.py flag_sets(): config/monitor-flags.json + ADDED (+FAST) + CENSUS."""
tree = ast.parse((ROOT / 'tools/build-fast-traps-monitor.py').read_text())
lists = {t.id: ast.literal_eval(n.value) for n in tree.body if isinstance(n, ast.Assign)
for t in n.targets if isinstance(t, ast.Name) and t.id in ('ADDED', 'FAST')}
census = [ast.literal_eval(n.value) for n in ast.parse((ROOT / 'tools/cycle_lib/speed_test.py')
.read_text()).body if isinstance(n, ast.Assign) and
any(isinstance(t, ast.Name) and t.id == 'CENSUS' for t in n.targets)][0]
base = json.loads((ROOT / 'config/monitor-flags.json').read_text())['flags']
return dict(control=[*base, *lists['ADDED'], census],
fast=[*base, *lists['ADDED'], *lists['FAST'], census]), lists['FAST']
@unittest.skipUnless(ARGS.build, 'pass --build to compile the monitor (a few minutes)')
class Build(unittest.TestCase):
def test_monitor_builds(self):
sys.path.insert(0, str(ROOT / 'probes'))
sys.path.insert(0, str(ROOT.parent / 'q1n1' / 'tools'))
import run as monitor_run # noqa: E402 (import only: build() needs no hardware)
sets, _ = flag_sets()
self.assertNotIn(DEFINE, sets['fast'])
sets['fast-wfi'] = sets['fast'] + ['-DSNAP_WFI_RESUME=1']
sets['fast-stack'] = sets['fast'] + FULL_STACK
expect = dict(e.split('=', 1) for e in ARGS.expect)
out = Path(ARGS.out) if ARGS.out else Path(tempfile.mkdtemp(prefix='root-miss-build-'))
budget = monitor_run.OUT_AT - monitor_run.CODE_AT
saved = list(monitor_run.FLAGS)
results = {}
try:
for name, flags in sets.items():
blobs = {}
for label, extra in (('default', []), ('off', ['-DSNAP_ROOT_MISS_FAST=0']), ('on', [DEFINE])):
monitor_run.FLAGS[:] = flags + extra
blob = monitor_run.build('xnuboot', out / f'{name}-{label}')
blobs[label] = blob
results[f'{name}-{label}'] = dict(bytes=len(blob), headroom=budget - len(blob),
sha256=hashlib.sha256(blob).hexdigest())
self.assertGreater(budget - len(blob), 0, f'{name}-{label}')
self.assertEqual(blobs['default'], blobs['off'], name)
self.assertNotEqual(blobs['default'], blobs['on'], name)
if name in expect:
self.assertEqual(results[f'{name}-default']['sha256'], expect[name],
f'{name}: the default build changed')
finally:
monitor_run.FLAGS[:] = saved
(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])