Files
Snapintosh/tools/test-fine-locks.py
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

1162 lines
70 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""SNAP_FINE_LOCKS / SNAP_FINE_SWITCH / SNAP_FAIR_TOKEN (docs/FINE-LOCKS-20261017.md).
python3 tools/test-fine-locks.py [--build [--out DIR] [--expect NAME=SHA256]] [--quick]
Offline, no hardware and no proxy.
Unit: tools/test-fine-locks.c, six pthreads running the real compat engines
under the real gate and root stripes, interleaved with token-path calls,
under ThreadSanitizer and under ASan/UBSan, phase 1 and phase 1b, with the
default compat defines and with the installed-fresh (v41) set, and with four
stripes (roots sharing stripes): every converted call's result, the final
guest RAM, shadow arena and cache equal a single-threaded token-path replay
of the log, the reference and type invariants hold, every cached shadow
equals its guest leaves, no CPU's TLB keeps a stale translation of a changed
page, and every shadow store is published before its TLBI.
Mutants: a missing stripe, unsorted stripes (deadlock), a non-atomic count, a
count delta against a lost snapshot, a token holder that does not drain
readers, a reader that does not publish itself, a commit without its TLBI, a
store without its publication, and (phase 1b) a switch outside its old
root's stripe or with a plain activity store: each must fail the harness.
Source: the define-off text of every changed file is free of the new code;
each converted callback in probes/xnuboot.c repeats its token-path original's
calls (publication, TLBI, commits) except the listed differences; nothing a
converted call reaches allocates, flushes, reclaims, invalidates, retypes,
builds or takes the token; global words it writes are atomic or under aux;
perf_counters.py decodes a fine profile a C writer produced.
Fairness: under an adversarial re-acquire loop a waiter is passed by at most
the five CPUs already queued, for the stripe/aux ticket locks and for the
SNAP_FAIR_TOKEN token; readers cannot enter after a token grant; a
test-and-set stripe lock and a token turn ignoring the queue must fail.
Pauth (SNAP_FAST_PAUTH_CALLS): tools/test-fast-pauth-calls.c compiles the
monitor's own converted pointer calls, the define-on SIGN/AUTH/BATCH_SIGN
cases and the default build's original cases (extracted from
probes/xnuboot.c) against the real pauth engines: for random calls, states and
injected native failures, the default case equals the define-on case, a
converted answer equals the case's, a decline changed nothing and the case
refuses it, a converted failure carries the case's record; weakened sources
must fail it. The source binding: SIGN/AUTH/BATCH_SIGN classified and
dispatched to the converted calls, one signing body for both paths, no stripe,
no pv_audit or view-failure write, nothing token-only reached, per-CPU state
only; weakened bindings must be caught. A source without the conversion fails
both.
Phase-fair gate (SNAP_PHASE_FAIR_GATE, batch6 FAILURES-AND-DESIGN.md G1-G16):
the harness above with the announced wait (bounded, withdrawn at the bound)
under TSan and ASan; mutants for each failure mode; fairness: a reader is
passed by at most the one writer present at its arrival, a token holder's
drain sees at most one entry per other CPU; Mix: installed-fresh-20261020-v5's
load on the real gate, old against new (reader and writer waits, drain,
fairness counts), reported.
--build: control, fast and fast+WFI: absent == =0, SNAP_FAIR_TOKEN=1 differs
(SNAP_FINE_LOCKS=1 and SNAP_FAST_PAUTH_CALLS=1 refuse to build outside the
installed flag set); the installed-fresh (v41) set: absent == =0, =1,
=1+SWITCH, =1+SWITCH+FAIR, =1+SWITCH+FAIR+GATE_WAIT+PHASE_FAIR_GATE and
=1+FAST_PAUTH_CALLS differ, the code budget holds; --expect pins defaults;
SNAP_PHASE_FAIR_GATE refuses to build without SNAP_FAIR_TOKEN or SNAP_GATE_WAIT.
"""
from pathlib import Path
import argparse
import hashlib
import importlib.util
import json
import os
import re
import runpy
import shutil
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')
IMPL = sorted(str(p.relative_to(ROOT)) for p in (ROOT / 'compat').glob('sptm*.c'))
IMPL += ['shadow/sprr-view.c', 'shadow/mmu.c', 'shadow/shadow.c', 'compat/service-lock.c']
CC = ['clang', '-std=gnu11', '-O1', '-g', '-pthread', '-ffunction-sections', '-Wl,-dead_strip',
'-Wall', '-Wextra', '-Werror', '-DSNAP_FINE_LOCKS=1', '-Icompat', '-Ishadow']
SAN = {'tsan': ['-fsanitize=thread'], 'asan': ['-fsanitize=address,undefined', '-fno-sanitize-recover=all']}
CHANGED = ['compat/service-lock.h', 'compat/service-lock.c', 'compat/sptm.h', 'compat/sptm-refs.h',
'compat/sptm-refs.c', 'compat/sptm-root.h', 'compat/sptm-root.c', 'shadow/root-publish.h',
'shadow/txn-publish.h', 'shadow/sprr-private-pages.h', 'shadow/sprr-table-memo.h',
'shadow/sprr-view.h', 'probes/xnuboot.c']
def arguments():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--build', action='store_true')
ap.add_argument('--quick', action='store_true', help='fewer calls per CPU and only the key mutants')
ap.add_argument('--out')
ap.add_argument('--expect', action='append', default=[], metavar='NAME=SHA256')
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, quick=False, out=None, expect=[]), [])
WORK = Path(tempfile.mkdtemp(prefix='fine-locks-'))
CALLS = '1500' if ARGS.quick else '3000'
def installed_defines():
"""The installed-fresh (v41) fast flag set's -D words: cycle.py's recipe."""
spec = importlib.util.spec_from_file_location('wfi_resume', ROOT / 'tools/test-wfi-resume.py')
module = importlib.util.module_from_spec(spec)
saved = sys.argv
sys.argv = [saved[0]]
try:
spec.loader.exec_module(module)
finally:
sys.argv = saved
sets, _ = module.flag_sets()
sys.path.insert(0, str(ROOT / 'tools'))
import cycle_lib.installed_fresh as fresh # noqa: E402
args = argparse.Namespace(variant='fast', wfi_resume=True, fast_el0_sysreg=True, service_subclass=True,
dart_wiredcnt=True, panic_serve=True, root_miss_fast=True, update_disjoint_fast=True,
root_cache_wide=True, update_check_sparse=True, input=True, kernel_pperm_fast=True,
ib_budget_1s=True, unmap_mixed=True)
return fresh.variant_flags(sets, args), sets
def harness(tree, san, extra=(), calls=CALLS, seed='5', timeout=900):
"""Build tools/test-fine-locks.c in tree and run it; (returncode, output)."""
binary = WORK / f'h-{abs(hash((str(tree), san, tuple(extra))))}'
build = subprocess.run([*CC, *SAN[san], *extra, 'tools/test-fine-locks.c', *IMPL, '-o', str(binary)],
cwd=tree, capture_output=True, text=True)
if build.returncode:
return build.returncode, 'build: ' + build.stderr[-3000:]
env = dict(os.environ, TSAN_OPTIONS='halt_on_error=1')
try:
run = subprocess.run([str(binary), calls, seed], capture_output=True, text=True, timeout=timeout, env=env)
except subprocess.TimeoutExpired:
return -9, 'timeout'
return run.returncode, run.stdout + run.stderr
class Unit(unittest.TestCase):
def check(self, san, extra=(), seed='5'):
rc, out = harness(ROOT, san, extra, seed=seed)
self.assertEqual(rc, 0, f'{san} {extra}: {out[-3000:]}')
self.assertIn('replay equal', out)
label = ' '.join(e for e in extra if 'FINE' in e or 'FAIR' in e) + \
(' +installed defines' if any(e.startswith('-DSNAP_') and 'FINE' not in e and 'FAIR' not in e
for e in extra) else '')
print(f'{san} {label or "phase 1"}: ' + [l for l in out.splitlines() if 'calls logged' in l][0],
file=sys.stderr)
def test_phase1(self):
for san in ('tsan', 'asan'):
self.check(san)
def test_phase1b(self):
for san in ('tsan', 'asan'):
self.check(san, ['-DSNAP_FINE_SWITCH=1'], seed='6')
def test_shared_stripes(self):
# Four stripes: roots share stripes, so merged locksets and one root's
# holder excluding another's calls are exercised.
for san in ('tsan', 'asan'):
self.check(san, ['-DSNAP_FINE_SWITCH=1', '-DFINE_STRIPES=4u'], seed='7')
def test_fair_token(self):
for san in ('tsan', 'asan'):
self.check(san, ['-DSNAP_FINE_SWITCH=1', '-DSNAP_FAIR_TOKEN=1'], seed='9')
def test_phase_fair_gate(self):
# SNAP_PHASE_FAIR_GATE: announced arrivals, phase entries, withdrawals at the bound.
for san in ('tsan', 'asan'):
self.check(san, PHASE_FAIR, seed='10')
self.check('asan', [*PHASE_FAIR, '-DFINE_STRIPES=4u'], seed='11')
def test_installed_defines(self):
flags, _ = installed_defines()
defines = [f for f in flags if f.startswith('-D') and 'FINE' not in f]
for san in ('tsan', 'asan'):
self.check(san, ['-DSNAP_FINE_SWITCH=1', *defines], seed='8')
PHASE_FAIR = ['-DSNAP_FINE_SWITCH=1', '-DSNAP_FAIR_TOKEN=1', '-DSNAP_PHASE_FAIR_GATE=1']
# Each must fail the harness under at least one sanitizer.
MUTANTS = [
('missing stripe', 'tools/test-fine-locks.c', '#ifndef MUTANT_NO_STRIPES\n fine_lockset_acquire(stripes,set);',
'#ifndef MUTANT_NO_STRIPES\n (void)set;', []),
('unsorted stripes', 'compat/fine-lock.h',
' while (i < s->count && s->stripe[i] < k) i++;\n if (i < s->count && s->stripe[i] == k) return 1;',
' for (; i < s->count; i++) if (s->stripe[i] == k) return 1;', []),
('non-atomic count', 'compat/sptm-refs.c',
' if (!table && ro)\n __atomic_fetch_add((volatile uint32_t *)(e->entry+8),(uint32_t)ro,__ATOMIC_RELAXED);',
' if (!table && ro)\n store(e->entry+8,(uint32_t)e->ro,4);', []),
('lost snapshot', 'compat/sptm-refs.c',
' m->fine->ro[e-b->entries]=e->ro;m->fine->other[e-b->entries]=e->other;',
' m->fine->ro[e-b->entries]=0;m->fine->other[e-b->entries]=0;', []),
('token holder does not drain', 'compat/service-lock.h',
' return !__atomic_load_n(&lock->readers, __ATOMIC_ACQUIRE);\n}', ' (void)lock;return 1;\n}', []),
('reader not published', 'compat/service-lock.c',
' __atomic_fetch_or(&lock->readers,bit,__ATOMIC_SEQ_CST);', ' (void)bit;', []),
('reader ignores a token taken meanwhile', 'compat/service-lock.c',
' if (__atomic_load_n(&lock->owner,__ATOMIC_SEQ_CST)) {\n guest_service_leave_shared(lock,cpu);\n return SERVICE_BUSY;',
' if (0) {\n guest_service_leave_shared(lock,cpu);\n return SERVICE_BUSY;', []),
('a converted call allocates', 'tools/test-fine-locks.c',
'static uint64_t fine_alloc(void *ctx) {(void)ctx;self->alloc_refused=1;return 0;}',
'static uint64_t token_alloc(void *ctx);\nstatic uint64_t fine_alloc(void *ctx) {return token_alloc(ctx);}', []),
('commit without TLBI', 'tools/test-fine-locks.c', '#ifndef MUTANT_NO_TLBI\n tlbi_range(va,size);',
'#ifndef MUTANT_NO_TLBI\n (void)tlbi_range;(void)va;(void)size;', []),
('store without publication', 'tools/test-fine-locks.c', '#ifndef MUTANT_NO_PUBLISH\n',
'#ifdef MUTANT_NO_PUBLISH\n', []),
('switch outside the old root stripe', 'tools/test-fine-locks.c',
' if(old && !fine_lockset_add(set,old))return 0;', ' (void)old;', ['-DSNAP_FINE_SWITCH=1']),
('plain activity store', 'compat/sptm-root.c',
' __atomic_store_n(&s->active[cpu],root,__ATOMIC_RELEASE);\n __atomic_fetch_add(&s->switches,1,__ATOMIC_RELAXED);\n'
' __atomic_fetch_add(&s->user_switches,1,__ATOMIC_RELAXED);',
' s->active[cpu]=root;\n __atomic_fetch_add(&s->switches,1,__ATOMIC_RELAXED);\n'
' __atomic_fetch_add(&s->user_switches,1,__ATOMIC_RELAXED);', ['-DSNAP_FINE_SWITCH=1']),
# SNAP_PHASE_FAIR_GATE (G numbers: batch6 FAILURES-AND-DESIGN.md).
('phase-fair: a writer drains only the free bank (G1, G4)', 'compat/service-lock.h',
' return !(r & (UINT64_C(0xff) | UINT64_C(0xff) << SERVICE_GATE_BANK_SHIFT(!(r & SERVICE_GATE_PHASE))));',
' return !(r & UINT64_C(0xff));', PHASE_FAIR),
('phase-fair: an announced reader enters at once (G4)', 'compat/service-lock.c',
' return gate&SERVICE_GATE_WRITER ? SERVICE_BUSY : SERVICE_OK;', ' return SERVICE_OK;', PHASE_FAIR),
('phase-fair: a reader bank that ignores the phase (G8)', 'compat/service-lock.c',
'SERVICE_GATE_BANK_SHIFT(!!(gate&SERVICE_GATE_PHASE)) : 0));', 'SERVICE_GATE_BANK_SHIFT(0) : 0));', PHASE_FAIR),
('phase-fair: a writer that keeps the phase (G8)', 'compat/service-lock.c',
'(gate|SERVICE_GATE_WRITER)^SERVICE_GATE_PHASE,1,', '(gate|SERVICE_GATE_WRITER),1,', PHASE_FAIR),
('phase-fair: a relaxed phase poll (G9)', 'compat/service-lock.c',
' uint64_t gate=__atomic_load_n(&lock->readers,__ATOMIC_ACQUIRE);\n unsigned phase;',
' uint64_t gate=__atomic_load_n(&lock->readers,__ATOMIC_RELAXED);\n unsigned phase;', PHASE_FAIR),
('phase-fair: a relaxed writer clear (G9)', 'compat/service-lock.c',
' __atomic_fetch_and(&lock->readers, ~SERVICE_GATE_WRITER, __ATOMIC_RELEASE);',
' __atomic_fetch_and(&lock->readers, ~SERVICE_GATE_WRITER, __ATOMIC_RELAXED);', PHASE_FAIR),
('phase-fair: the turn passed before the writer bit is cleared (G9)', 'compat/service-lock.c',
' if (__atomic_load_n(&lock->readers, __ATOMIC_RELAXED) & SERVICE_GATE_WRITER)\n'
' __atomic_fetch_and(&lock->readers, ~SERVICE_GATE_WRITER, __ATOMIC_RELEASE);\n#endif\n'
' /* The holder alone writes serving: pass the turn once the word is free. */\n'
' if (!__atomic_compare_exchange_n(&lock->owner, &expected, 0, 0,\n'
' __ATOMIC_RELEASE, __ATOMIC_RELAXED)) return SERVICE_BAD_OWNER;\n'
' __atomic_store_n(&lock->serving, __atomic_load_n(&lock->serving, __ATOMIC_RELAXED) + 1,\n'
' __ATOMIC_RELEASE);\n',
'#endif\n if (!__atomic_compare_exchange_n(&lock->owner, &expected, 0, 0,\n'
' __ATOMIC_RELEASE, __ATOMIC_RELAXED)) return SERVICE_BAD_OWNER;\n'
' __atomic_store_n(&lock->serving, __atomic_load_n(&lock->serving, __ATOMIC_RELAXED) + 1,\n'
' __ATOMIC_RELEASE);\n'
' if (__atomic_load_n(&lock->readers, __ATOMIC_RELAXED) & SERVICE_GATE_WRITER)\n'
' __atomic_fetch_and(&lock->readers, ~SERVICE_GATE_WRITER, __ATOMIC_RELEASE);\n', PHASE_FAIR),
('phase-fair: an arrival not withdrawn at the bound (G3, G10)', 'tools/test-fine-locks.c',
' guest_service_leave_shared(&gate,t->cpu);\n break;',
' break;', PHASE_FAIR),
]
# Each must fail the fairness run (an adversarial re-acquire loop).
FAIR_MUTANTS = [
('test-and-set stripe lock', [
('compat/fine-lock.h', ' return __atomic_fetch_add(&t->next, 1, __ATOMIC_RELAXED);\n}', ' (void)t;return 0;\n}'),
('compat/fine-lock.h',
' while (__atomic_load_n(&t->serving, __ATOMIC_ACQUIRE) != me) { waited = 1; FINE_RELAX(); }',
' (void)me;while (__atomic_exchange_n(&t->serving, 1, __ATOMIC_ACQUIRE)) { waited = 1; FINE_RELAX(); }'),
('compat/fine-lock.h',
' uint32_t now = __atomic_load_n(&t->serving, __ATOMIC_RELAXED);\n __atomic_store_n(&t->serving, now + 1, __ATOMIC_RELEASE);',
' __atomic_store_n(&t->serving, 0, __ATOMIC_RELEASE);')]),
('a token turn that ignores the queue', [
('compat/service-lock.c',
' if (__atomic_load_n(&lock->serving, __ATOMIC_ACQUIRE) != ticket) return SERVICE_BUSY;\n', '')]),
]
# Each must fail the phase-fair fairness run.
PHASE_FAIR_MUTANTS = [
('phase-fair: a reader that waits for the writer bit, not its phase (G1, G5)', [
('compat/service-lock.c', ' if (writer && (unsigned)!!(gate&SERVICE_GATE_PHASE)==phase) return SERVICE_BUSY;',
' (void)phase;if (writer) return SERVICE_BUSY;')]),
('phase-fair: readers that ignore the writer (G4)', [
('compat/service-lock.c', 'SERVICE_GATE_BANK_SHIFT(!!(gate&SERVICE_GATE_PHASE)) : 0));', '0 : 0));'),
('compat/service-lock.c', ' return gate&SERVICE_GATE_WRITER ? SERVICE_BUSY : SERVICE_OK;', ' return SERVICE_OK;')]),
]
QUICK_MUTANTS = {'missing stripe', 'unsorted stripes', 'non-atomic count', 'token holder does not drain',
'phase-fair: a writer drains only the free bank (G1, G4)'}
class Mutants(unittest.TestCase):
def test_each_mutant_fails(self):
for n, (name, rel, old, new, extra) in enumerate(MUTANTS):
if ARGS.quick and name not in QUICK_MUTANTS:
continue
text = read_source(ROOT / rel)
self.assertEqual(text.count(old), 1, f'mutant {name} anchor')
tree = WORK / f'mutant-{n}'
for d in ('compat', 'shadow', 'tools'):
shutil.copytree(ROOT / d, tree / d)
(tree / rel).write_text(text.replace(old, new))
caught = []
for san in ('tsan', 'asan'):
rc, out = harness(tree, san, extra, timeout=300)
self.assertFalse(out.startswith('build:'), f'mutant {name} does not build: {out}')
if rc:
caught.append(san)
self.assertTrue(caught, f'mutant {name} survived both sanitizers')
print(f'mutant {name}: caught by {", ".join(caught)}', file=sys.stderr)
class Cycle(unittest.TestCase):
"""cycle.py installed-fresh --fine-locks/--fine-switch/--fair-token."""
def test_flags(self):
installed, sets = installed_defines()
import cycle_lib.installed_fresh as fresh # noqa: E402 (installed_defines put tools on the path)
from cycle_lib.common import Refused # noqa: E402
base = dict(variant='fast', root_cache_wide=True, update_disjoint_fast=True, service_subclass=True)
flags = fresh.variant_flags(sets, argparse.Namespace(**base, fine_locks=True, fine_switch=True,
fair_token=True))
self.assertEqual(flags[-3:], ['-DSNAP_FINE_LOCKS=1', '-DSNAP_FINE_SWITCH=1', '-DSNAP_FAIR_TOKEN=1'])
self.assertEqual(fresh.variant_flags(sets, argparse.Namespace(variant='control', fair_token=True))[-1],
'-DSNAP_FAIR_TOKEN=1')
for missing in ('root_cache_wide', 'update_disjoint_fast', 'service_subclass'):
with self.assertRaises(Refused):
fresh.variant_flags(sets, argparse.Namespace(**dict(base, **{missing: False}), fine_locks=True))
with self.assertRaises(Refused):
fresh.variant_flags(sets, argparse.Namespace(**dict(base, variant='control'), fine_switch=True))
# --fast-pauth-calls: last, after every earlier define; needs --fine-locks.
flags = fresh.variant_flags(sets, argparse.Namespace(**base, fine_locks=True, gate_wait=True,
fast_pauth_calls=True))
self.assertEqual(flags[-2:], ['-DSNAP_GATE_WAIT=1', '-DSNAP_FAST_PAUTH_CALLS=1'])
self.assertEqual(flags[:-1], fresh.variant_flags(sets, argparse.Namespace(**base, fine_locks=True,
gate_wait=True)))
with self.assertRaises(Refused):
fresh.variant_flags(sets, argparse.Namespace(**base, fast_pauth_calls=True))
spec = importlib.util.spec_from_file_location('cycle_cli', ROOT / 'tools/cycle.py')
cycle = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cycle)
args = cycle.parse(['installed-fresh', '--prefix', 'x', '--check', '--variant', 'fast', '--fine-locks',
'--fine-switch', '--fair-token', '--fast-pauth-calls'])
self.assertTrue(args.fine_locks and args.fine_switch and args.fair_token and args.fast_pauth_calls)
# --phase-fair-gate: appended last; needs --fine-locks, --fair-token and --gate-wait.
gate = dict(base, fine_locks=True, fine_switch=True, fair_token=True, gate_wait=True)
flags = fresh.variant_flags(sets, argparse.Namespace(**gate, phase_fair_gate=True))
self.assertEqual(flags[-1], '-DSNAP_PHASE_FAIR_GATE=1')
self.assertEqual(flags[:-1], fresh.variant_flags(sets, argparse.Namespace(**gate)))
for missing in ('fine_locks', 'fair_token', 'gate_wait'):
with self.assertRaises(Refused):
fresh.variant_flags(sets, argparse.Namespace(**dict(gate, **{missing: False}), phase_fair_gate=True))
args = cycle.parse(['installed-fresh', '--prefix', 'x', '--check', '--variant', 'fast', '--fine-locks',
'--fair-token', '--gate-wait', '--phase-fair-gate'])
self.assertTrue(args.phase_fair_gate)
args = cycle.parse(['speed-test', '--prefix', 'x', '--check', '--phase-fair-gate'])
self.assertTrue(args.phase_fair_gate)
import cycle_lib.speed_test as speed # noqa: E402
self.assertEqual(speed.options_of(args), {'phase_fair_gate': True})
fast = speed.speed_sets(ROOT, {'phase_fair_gate': True})[0]['fast']
self.assertEqual(fast[-1], '-DSNAP_PHASE_FAIR_GATE=1')
self.assertEqual(fast[:-1], speed.speed_sets(ROOT)[0]['fast'])
class Fairness(unittest.TestCase):
"""compat/fine-lock.h ticket locks and SNAP_FAIR_TOKEN under an adversarial
re-acquire loop: a waiter is passed by at most the five CPUs already
queued; readers cannot enter once a token holder has the word."""
def fair(self, tree, san, fair_token=True, phase=False):
extra = ['-DSNAP_FINE_SWITCH=1', f'-DSNAP_FAIR_TOKEN={int(fair_token)}'] + \
(['-DSNAP_PHASE_FAIR_GATE=1'] if phase else [])
return harness(tree, san, extra, calls='fair', seed='5000' if ARGS.quick else '20000', timeout=600)
def test_bounded_waits(self):
for san in ('tsan', 'asan'):
rc, out = self.fair(ROOT, san)
self.assertEqual(rc, 0, out[-2000:])
self.assertIn('token (fair): at most', out)
print(f'{san}: ' + '; '.join(l for l in out.splitlines() if 'at most' in l), file=sys.stderr)
def test_phase_fair_bounds(self):
# G5: a reader is passed by at most the writer present at its arrival;
# G4: a token holder's drain sees at most one entry per other CPU.
for san in ('tsan', 'asan'):
rc, out = self.fair(ROOT, san, phase=True)
self.assertEqual(rc, 0, out[-2000:])
self.assertIn('gate (phase-fair): at most', out)
print(f'{san} phase-fair: ' + '; '.join(l for l in out.splitlines() if 'at most' in l), file=sys.stderr)
def test_phase_fair_mutants_fail(self):
for n, (name, edits) in enumerate(PHASE_FAIR_MUTANTS):
tree = WORK / f'phase-fair-mutant-{n}'
for d in ('compat', 'shadow', 'tools'):
shutil.copytree(ROOT / d, tree / d)
for rel, old, new in edits:
text = (tree / rel).read_text()
self.assertEqual(text.count(old), 1, f'{name}: {old[:50]}')
(tree / rel).write_text(text.replace(old, new))
rc, out = self.fair(tree, 'asan', phase=True)
self.assertFalse(out.startswith('build:'), f'{name} does not build: {out}')
self.assertNotEqual(rc, 0, f'{name} survived: {out[-1500:]}')
print(f'fairness mutant {name}: caught', file=sys.stderr)
def test_default_token_reported(self):
# Without SNAP_FAIR_TOKEN the token is the CAS word: reported, not bounded.
rc, out = self.fair(ROOT, 'asan', fair_token=False)
self.assertEqual(rc, 0, out[-2000:])
print('default token: ' + [l for l in out.splitlines() if l.startswith('token:')][0], file=sys.stderr)
def test_unfair_mutants_fail(self):
for n, (name, edits) in enumerate(FAIR_MUTANTS):
tree = WORK / f'fair-mutant-{n}'
for d in ('compat', 'shadow', 'tools'):
shutil.copytree(ROOT / d, tree / d)
for rel, old, new in edits:
text = (tree / rel).read_text()
self.assertEqual(text.count(old), 1, f'{name}: {old[:50]}')
(tree / rel).write_text(text.replace(old, new))
rc, out = self.fair(tree, 'asan')
self.assertFalse(out.startswith('build:'), f'{name} does not build: {out}')
self.assertNotEqual(rc, 0, f'{name} survived: {out[-1500:]}')
print(f'fairness mutant {name}: caught', file=sys.stderr)
class Mix(unittest.TestCase):
"""installed-fresh-20261020-v5's load on the real gate (tools/test-fine-locks.c
"mix", plain -O2): the old gate against SNAP_PHASE_FAIR_GATE, same seeds. A
timing model on the host: the waits are reported, only the fairness bound and
a clean run are asserted."""
SECONDS = '3' if ARGS.quick else '10'
SEEDS = ('1',) if ARGS.quick else ('1', '2', '3')
def run_mix(self, extra, seed, *more):
binary = WORK / f'mix-{abs(hash(tuple(extra)))}'
if not binary.exists():
subprocess.run(['clang', '-std=gnu11', '-O2', '-pthread', '-ffunction-sections', '-Wl,-dead_strip',
'-Wall', '-Wextra', '-Werror',
'-DSNAP_FINE_LOCKS=1', '-Icompat', '-Ishadow', '-DSNAP_FINE_SWITCH=1',
'-DSNAP_FAIR_TOKEN=1', *extra, 'tools/test-fine-locks.c', *IMPL, '-o', str(binary)],
cwd=ROOT, check=True)
out = subprocess.run([str(binary), 'mix', self.SECONDS, seed, *more], capture_output=True, text=True,
timeout=600)
self.assertEqual(out.returncode, 0, out.stdout + out.stderr)
line = [l for l in out.stdout.splitlines() if l.startswith('mix ')][0]
return json.loads(line[4:])
def test_old_and_phase_fair(self):
rows = []
for seed in self.SEEDS:
old = self.run_mix([], seed)
new = self.run_mix(['-DSNAP_PHASE_FAIR_GATE=1'], seed)
self.assertEqual((old['gate'], new['gate']), ('base', 'phase-fair'))
self.assertLessEqual(new['passed_max'], 1, 'G5: a reader passed by more than one writer')
rows += [old, new]
for r in rows:
print(f"mix {r['gate']:10} seed {r['seed']}: busy {r['token_busy']:.3f} converted wait "
f"{r['reader_wait']['mean_us']:6.2f} us (p99 {r['reader_wait']['p99_us']:7.1f}, max "
f"{r['reader_wait']['max_us']:7.1f}) token wait {r['writer_wait']['mean_us']:6.2f} us (p99 "
f"{r['writer_wait']['p99_us']:7.1f}) drain {r['writer_drain']['mean_us']:5.2f} us passed max "
f"{r['passed_max']} hist {r['passed_hist']} ahead {r['ahead']} timeouts {r['timeouts']}",
file=sys.stderr)
if os.environ.get('FINE_LOCKS_MIX_JSON'):
Path(os.environ['FINE_LOCKS_MIX_JSON']).write_text(json.dumps(rows, indent=1) + '\n')
def strip(text):
"""The text with every '#if SNAP_FINE_LOCKS' / '#if SNAP_FINE_SWITCH' /
'#if SNAP_FAST_PAUTH_CALLS ...' branch removed and its '#else' kept: what a
define-off build compiles."""
out, stack = [], []
for line in text.splitlines(keepends=True):
s = line.strip()
if s.startswith('#if'):
fine = bool(re.fullmatch(r'#if (SNAP_FINE_LOCKS|SNAP_FINE_SWITCH|SNAP_FAIR_TOKEN|SNAP_PHASE_FAIR_GATE)'
r'( \|\| (SNAP_FINE_LOCKS|SNAP_FINE_SWITCH|SNAP_FAIR_TOKEN))*', s) or
re.match(r'#if SNAP_FAST_PAUTH_CALLS\b', s) or
# agents/sptm-locks: SNAP_SPTM_FINE[_RETYPE|_KERNEL] blocks are fine-lock code too.
re.match(r'#if !?SNAP_SPTM_FINE(_RETYPE|_KERNEL)?\b', s))
stack.append(('fine' if fine else 'other', fine))
if fine:
continue
elif s.startswith('#else') and stack and stack[-1][0] == 'fine':
stack[-1] = ('fine', False)
continue
elif s.startswith('#endif') and stack:
kind, _ = stack.pop()
if kind == 'fine':
continue
if any(kind == 'fine' and skip for kind, skip in stack):
continue
out.append(line)
return ''.join(out)
def effective(defines):
"""probes/xnuboot.c preprocessed with the monitor's flags (macros expanded)."""
return subprocess.run(['clang', '--target=aarch64-unknown-none-elf', '-ffreestanding', '-mgeneral-regs-only',
'-E', '-P', '-Ishadow', '-Icompat', f'-I{ROOT.parent / "q1n1/platform/uefi"}', *defines,
'probes/xnuboot.c'], cwd=ROOT, capture_output=True, text=True, check=True).stdout
def functions(code):
out = {}
for m in re.finditer(r'^(?:static\s+)?(?:inline\s+)?[A-Za-z_][\w \*]*?\b(\w+)\s*\([^;{}]*\)\s*\{', code, re.M):
name, i, depth = m[1], m.end() - 1, 0
start = i
while i < len(code):
depth += code[i] == '{'
depth -= code[i] == '}'
i += 1
if not depth:
break
out.setdefault(name, code[start:i])
return out
IGNORED = {'fine_add', 'fine_sat_add', 'sprr_count', 'fine_view_failed', 'fine_failed', 'fine_aux_lock',
'fine_aux_unlock', 'fine_cpu', 'fine_of', 'detail_begin', 'detail_end', 'sizeof', 'if', 'for',
'while', 'return', 'switch', 'vcpu_of', '__asm__', '__builtin_ctzll', 'int', 'uint64_t'}
RENAMED = {'sprr_table_memo_note_fine': 'sprr_table_memo_note', 'root_publish_note_fine': 'root_publish_note',
'txn_publish_observe_fine': 'txn_publish_observe', 'sprr_private_write_fine': 'sprr_private_write',
'sprr_view_epoch_advance_fine': 'sprr_view_epoch_advance', 'fine_memory': 'sptm_context_memory'}
def calls(body):
"""The ordered calls and asm templates of a function body."""
seq = []
for m in re.finditer(r'__asm__\s+volatile\s*\(\s*"([^"]*)"|\b([A-Za-z_]\w*)\s*\(', body):
if m[1] is not None:
seq.append('asm:' + ' '.join(m[1].split()))
elif not m[2].startswith('__atomic') and m[2] not in IGNORED:
name = RENAMED.get(m[2], m[2])
seq.append(name[5:] if name.startswith('fine_') else name)
return seq
# fine copy -> (token-path original, calls only in the original, calls only in the copy)
MIRRORS = {
'fine_native_tlbi_range': ('native_tlbi_range', [], []),
'fine_native_store_admit': ('native_store_admit', [], []),
'fine_user_private_write': ('user_private_write', [], []),
'fine_kernel_table_write': ('kernel_table_write', ['sprr_publish_note'], ['native_page']),
'fine_native_copy_write': ('native_copy_write', [], []),
'fine_native_live_shadow': ('native_live_shadow', [], []),
'fine_native_store_propagate': ('native_store_propagate', [], []),
'fine_mem_write': ('mem_write', [], ['sprr_class_proof_tracked']),
'fine_user_live_write': ('user_live_write', [], []),
'fine_sptm_publish': ('sptm_publish', [], []),
'fine_user_refresh_all': ('user_refresh_all', [], []),
'fine_user_commit_tlb_range': ('user_commit_tlb_range', [], []),
'fine_user_prepare_leaf': ('user_prepare_leaf', ['native_prepare_tpro'], ['live_shadow']),
'fine_user_commit_leaf': ('user_commit_leaf', [], ['live_shadow']),
'fine_user_prepare_unmap': ('user_prepare_unmap', [], ['live_shadow']),
'fine_user_prepare_update': ('user_prepare_update', ['native_prepare_tpro'], []),
'fine_user_commit_update': ('user_commit_update', ['user_tlb_range'], ['live_shadow', 'native_tlbi_range']),
'fine_user_commit_unmap': ('user_commit_unmap', [], ['live_shadow']),
'fine_user_backend': ('user_backend', [], ['lockset_has']),
'fine_user_install_view': ('user_install_view', ['user_view_shadow', 'native_umprr', 'native_views_sync',
'native_mark', 'publish'], []),
'fine_user_root_install': ('user_root_install', ['kernel_change_pperm_timed', 'root_publish_flush',
'asm:dsb ish'], ['install_publish']),
'fine_install_failed': ('install_failed', [], []),
}
# What no converted call may reach: token-path state changes and the token.
FORBIDDEN = {'transaction_publish', 'root_publish_flush', 'txn_publish_flush', 'arena_reclaim', 'arena_reserve',
'table_alloc_owned', 'table_alloc', 'user_table_alloc', 'private_table_alloc_owned', 'user_root_build',
'kernel_change_pperm', 'kernel_change_pperm_timed', 'kernel_change_pperm_impl', 'shadow_tlb_sync',
'kernel_refresh', 'user_install_view', 'native_prepare_tpro', 'root_slot_release',
'sptm_root_invalidate', 'inactive_map_keep', 'guest_service_try', 'runtime_enter',
'native_mark', 'native_views_sync', 'sptm_mac_retype', 'sptm_mac_switch_root', 'user_root_install',
'mem_write', 'user_live_write', 'kernel_table_write', 'user_private_write', 'sptm_publish',
'native_group_alloc', 'native_shared_create', 'native_root_created'}
def effective_of(text, defines):
"""effective() of a probes/xnuboot.c text, in a scratch tree."""
tree = WORK / f'src-{hashlib.sha256(text.encode()).hexdigest()[:12]}'
if not tree.exists():
(tree / 'probes').mkdir(parents=True)
for d in ('compat', 'shadow', 'gpu'):
if (ROOT / d).exists():
(tree / d).symlink_to(ROOT / d)
for f in (ROOT / 'probes').iterdir():
if f.suffix == '.h':
(tree / 'probes' / f.name).symlink_to(f)
(tree / 'probes/xnuboot.c').write_text(text)
return subprocess.run(['clang', '--target=aarch64-unknown-none-elf', '-ffreestanding', '-mgeneral-regs-only',
'-E', '-P', '-Ishadow', '-Icompat', f'-I{ROOT.parent / "q1n1/platform/uefi"}', *defines,
'probes/xnuboot.c'], cwd=tree, capture_output=True, text=True, check=True).stdout
def source_problems(text):
"""Every reason probes/xnuboot.c's converted calls would differ from the
token path they repeat, or reach token-path state. Empty means none."""
p = []
trap = text[text.index('int xnuboot_trap(uint64_t *frame, uint64_t esr, uint64_t *elr)\n{'):]
trap = trap[:trap.index('\n}\n')]
if not trap.find('fast_idle_poll(c, frame, esr, elr)') < trap.find('fine_sptm_call(') < trap.find('runtime_enter('):
p.append('fine_sptm_call is not between the fast paths and runtime_enter')
enter = text[text.index('static int runtime_enter(struct context *c'):]
enter = enter[:enter.index('\n}\n')]
if 'if (result == SERVICE_OK && !guest_service_drained(&c->services)) {' not in enter or \
not enter.find('guest_service_drained') < enter.find('service_profile_wait_done'):
p.append('runtime_enter does not drain converted calls before its wait accounting')
flags, _ = installed_defines()
fns = functions(effective_of(text, [f for f in flags if f.startswith('-D')] +
['-DSNAP_FINE_LOCKS=1', '-DSNAP_FINE_SWITCH=1']))
for fine, (orig, only_orig, only_fine) in MIRRORS.items():
if fine not in fns or orig not in fns:
p.append(f'{fine} or {orig} missing')
continue
a, b = calls(fns[orig]), calls(fns[fine])
for name in only_orig:
if name in a:
a.remove(name)
else:
p.append(f'{orig}: {name} is no longer there')
for name in only_fine:
if name in b:
b.remove(name)
else:
p.append(f'{fine}: {name} is no longer there')
if a != b:
p.append(f'{fine} does not repeat {orig}: {b} != {a}')
# Direct calls, and the converted callbacks handed to the engines (a
# fine_* name used as a value); fine_fatal takes the token only after
# leaving the gate (checked below).
seen, todo = set(), ['fine_sptm_call']
while todo:
name = todo.pop()
if name in seen or name not in fns:
continue
seen.add(name)
for m in re.finditer(r'\b([A-Za-z_]\w*)\b(\s*\()?', fns[name]):
callee = m[1]
if callee in fns and callee != 'fine_fatal' and (m[2] or callee.startswith('fine_')):
todo.append(callee)
if seen & FORBIDDEN:
p.append(f'converted calls reach {sorted(seen & FORBIDDEN)}')
for need in ('fine_user_commit_update', 'fine_user_commit_leaf', 'fine_user_commit_unmap',
'fine_mem_write', 'fine_user_root_install', 'fine_install_publish'):
if need not in seen:
p.append(f'{need} not reached')
reached = ''.join(fns[n] for n in seen)
for engine in ('sptm_mac_update_user_disjoint', 'sptm_mac_map_user_page', 'sptm_mac_unmap_user_region',
'sptm_mac_switch_root_hit'):
if engine not in reached:
p.append(f'{engine} not reached')
# Every shadow, memory and backend a converted call builds carries the
# converted writers, allocator and publisher (a token-path callback left
# in one would be reached through the engines).
if '.alloc=fine_refuse_alloc,\n .read=mem_read,.write=fine_user_live_write,' not in \
text[text.index('static struct shadow fine_live_shadow('):]:
p.append('fine_live_shadow does not carry the converted writer and allocator')
if 's.alloc=fine_refuse_alloc;s.write=fine_native_copy_write;return s;' not in text:
p.append('fine_native_live_shadow does not carry the converted writer and allocator')
if 'm.publish=fine_sptm_publish;m.refs=&fine_of(c)->refs[vcpu_of(c)->slot];\n' \
' m.audit=&f->audit;m.fine=&f->ref_fine;m.clean_frame=0;' not in text:
p.append('fine_memory does not carry this CPU\'s batch, snapshots, audit and publisher')
for fn in ('fine_map_page', 'fine_unmap_region', 'fine_update_disjoint', 'fine_user_commit_leaf',
'fine_user_prepare_unmap', 'fine_user_commit_update', 'fine_switch_root'):
if 'sptm_context_memory(' in fns.get(fn, ''):
p.append(f'{fn} builds a token-path memory')
backend = fns.get('fine_user_backend', '')
for field in ('prepare_leaf', 'commit_leaf', 'prepare_unmap', 'commit_unmap', 'prepare_update',
'commit_update'):
if f'b->{field}=fine_user_{field};' not in backend:
p.append(f'fine_user_backend lacks fine_user_{field}')
if 'shared' in backend.replace('guest_ttbr1', '') or 'lockset_has' not in backend:
p.append('fine_user_backend admits shared roots or roots outside the lockset')
call = fns.get('fine_sptm_call', '')
if not call.rindex('guest_service_leave_shared') < call.index('fine_fatal('):
p.append('fine_fatal takes the token before the gate is left')
p += phase_fair_problems(text)
# A store to a context word is per CPU, per root (under its stripe) or
# under aux; everything else is atomic.
block = text[text.index('/* ---- SNAP_FINE_LOCKS: converted user address-space calls'):]
block = block[:block.index('int xnuboot_trap(uint64_t *frame, uint64_t esr, uint64_t *elr);')]
for fn, body in functions(block).items():
aux = False
for line in body.splitlines():
if 'fine_aux_lock(c);' in line:
aux = True
for m in re.finditer(r'\bc->((?:\w|\.|->|\[[^\]]*\])+)\s*(\+\+|\+=|-=|(?<![<>!=])=(?!=))', line):
target = m[1]
if aux or re.match(r'\w+\[(v->slot|vcpu_of\(c\)->slot|cpu)\]', target) or \
target.startswith('root_ext->fine'):
continue
p.append(f'{fn}: plain store c->{target}{m[2]}')
if 'fine_aux_unlock(c);' in line:
aux = False
return p
def phase_fair_problems(text):
"""SNAP_PHASE_FAIR_GATE in probes/xnuboot.c (raw text: the define is off in
the effective source above): the bounded wait polls the announced arrival
with guest_service_wait_shared, withdraws it at the bound before declining
(G3, G10), counts phase entries, and the define refuses to build without
SNAP_GATE_WAIT."""
p = []
call = text[text.index('static int fine_sptm_call(struct context *c'):]
call = call[:call.index('\n}\n')]
loop = call[call.index(' for(int waited=0;;waited=1) {'):]
loop = loop[:loop.index(' if(gate!=SERVICE_OK) {f->profile.declined[FINE_NO_GATE]++;return 0;}')]
if '#if SNAP_PHASE_FAIR_GATE\n gate=waited ? guest_service_wait_shared(&c->services,v->slot,&ahead) :\n' \
' guest_service_enter_shared(&c->services,v->slot);\n#else' not in loop:
p.append('the phase-fair wait does not poll its announced arrival with guest_service_wait_shared')
bound = loop[loop.index('if((int64_t)(now-t0-c->params->clock_host_hz/200)>=0'):]
bound = bound[:bound.index('break;')]
if '#if SNAP_PHASE_FAIR_GATE\n guest_service_leave_shared(&c->services,v->slot);' not in bound:
p.append('the phase-fair wait declines at its bound without withdrawing its arrival')
if 'if(ahead)fine_add(&c->root_ext->gate[4],1);' not in loop:
p.append('phase entries are not counted')
if '#if SNAP_PHASE_FAIR_GATE && !SNAP_GATE_WAIT\n#error' not in text:
p.append('SNAP_PHASE_FAIR_GATE builds without SNAP_GATE_WAIT')
return p
# Each weakened source must be caught by source_problems.
SOURCE_MUTANTS = {
'a live store left unpublished': (' int result=fine_mem_write(ctx,pa,value);\n if(!result)publish(pa,pa+8);',
' int result=fine_mem_write(ctx,pa,value);'),
'an update without its TLBI': (' fine_native_tlbi_range(c,va,UINT64_C(1)<<shift);return !fine_failed(c);',
' return !fine_failed(c);'),
'a store without its dirty note': (' if(!root_publish_note_fine(&c->root_publish,c->work_base,c->next_table,pa,8))return -1;\n', ''),
'a converted call flushes globally': (' (void)p;\n fine_add(&c->user_update_skip.skipped,1);',
' (void)p;transaction_publish(c);\n fine_add(&c->user_update_skip.skipped,1);'),
'a plain global counter': ('fine_add(&c->update_calls,1);', 'c->update_calls++;'),
'a token-path writer in a converted shadow': ('.read=mem_read,.write=fine_user_live_write,.ctx=c,',
'.read=mem_read,.write=user_live_write,.ctx=c,'),
'a token-path copy writer': (' return fine_kernel_table_write(c,pa,value);\n}',
' return kernel_table_write(c,pa,value);\n}'),
'a token holder that does not drain': (' if (result == SERVICE_OK && !guest_service_drained(&c->services)) {',
' if (0) {'),
'the backend outside the lockset': (' if(!fine_lockset_has(&fine_cpu(c)->locks,root))return 0;\n', ''),
'a phase-fair arrival not withdrawn at the bound': (
'#if SNAP_PHASE_FAIR_GATE\n guest_service_leave_shared(&c->services,v->slot); /* withdraw the arrival */\n#endif\n',
''),
'a phase-fair wait that re-arrives': (' gate=waited ? guest_service_wait_shared(&c->services,v->slot,&ahead) :\n'
' guest_service_enter_shared(&c->services,v->slot);',
' gate=guest_service_enter_shared(&c->services,v->slot);'),
'a switch that keeps the global flush': (' if(!fine_install_publish(c,owner)) {fine_install_failed(c,deferred);return;}',
' if(!root_publish_flush(&c->root_publish,c->work_base,c->next_table,'
'publish_page_lines,c)) {fine_install_failed(c,deferred);return;}'),
}
class Source(unittest.TestCase):
def test_define_off_text(self):
for rel in CHANGED:
off = strip(read_source(ROOT / rel))
for token in ('SNAP_FINE', 'fine_', 'FINE_', 'fine-lock.h', '_fine', 'SNAP_FAIR', 'try_turn',
'guest_service_queue', 'user_pointer_'):
self.assertNotIn(token, off, f'{rel}: {token} outside the defines')
self.assertIn('#ifndef SNAP_FINE_LOCKS\n#define SNAP_FINE_LOCKS 0\n#endif', (ROOT / 'compat/fine-lock.h').read_text())
def test_phase_fair_define_off_text(self):
# SNAP_PHASE_FAIR_GATE's code only inside its own blocks (checked on its
# tokens alone: test_define_off_text stops at xnuboot.c's pre-existing
# '#elif SNAP_FINE_LOCKS' report-offset chains).
for rel in CHANGED + ['compat/fine-lock.h', 'compat/token-costs.h']:
off = strip(read_source(ROOT / rel))
for token in ('wait_shared', 'SERVICE_GATE_', 'drain_from', 'ahead)', 'gate[4]', 'gate[5]', 'gate[6]',
'UINT64_C(1) << 5'):
self.assertNotIn(token, off, f'{rel}: {token} outside SNAP_PHASE_FAIR_GATE')
self.assertIn('#ifndef SNAP_PHASE_FAIR_GATE\n#define SNAP_PHASE_FAIR_GATE 0\n#endif',
(ROOT / 'compat/fine-lock.h').read_text())
def test_monitor_source(self):
self.assertEqual(source_problems(SOURCE), [])
def test_weakened_sources_are_caught(self):
for name, (old, new) in SOURCE_MUTANTS.items():
self.assertEqual(SOURCE.count(old), 1, name)
self.assertNotEqual(source_problems(SOURCE.replace(old, new)), [], name)
def test_profile_decoder(self):
perf = runpy.run_path(str(ROOT / 'tools/perf_counters.py'))
flags, _ = installed_defines()
defines = [f for f in flags if f.startswith('-D')] + ['-DSNAP_FINE_LOCKS=1', '-DSNAP_FINE_SWITCH=1']
lay = perf['fine_layout'](defines)
writer = WORK / 'writer.c'
writer.write_text(r'''
#include <stdio.h>
#include "fine-lock.h"
static struct fine_profile p[6];
int main(int argc,char **argv)
{
(void)argc;
for(unsigned cpu=0;cpu<6;cpu++) {
for(unsigned k=0;k<FINE_CALLS;k++) {
struct fine_bucket *b=&p[cpu].call[k];
b->entries=100*cpu+k+1;b->gate_wait=2*b->entries;b->stripe_wait=3*b->entries;
b->aux_wait=b->entries;b->hold=10*b->entries;b->contended=k;
}
for(unsigned d=0;d<FINE_DECLINES;d++)p[cpu].declined[d]=cpu+d;
p[cpu].aux_entries=50;p[cpu].aux_contended=5;p[cpu].failures=0;
}
unsigned long long head[2]={FINE_PROFILE_MAGIC,(unsigned long long)sizeof(struct fine_profile)<<32|BITS};
FILE *f=fopen(argv[1],"wb");fwrite(head,sizeof head,1,f);fwrite(p,sizeof p,1,f);fclose(f);
return 0;
}
''')
def blob_of(name, extra, bits):
binary, blob = WORK / f'writer-{name}', WORK / f'fine-{name}.bin'
subprocess.run(['clang', '-std=gnu11', '-DSNAP_FINE_LOCKS=1', f'-DBITS={bits}', *extra, '-Icompat',
str(writer), '-o', str(binary)], cwd=ROOT, check=True)
subprocess.run([str(binary), str(blob)], check=True)
return blob.read_bytes()
record = blob_of('switch', [], 1)
out = perf['decode_fine_profile'](record, lay=lay, hz=19.2e6)
self.assertTrue(out['switch'])
self.assertFalse(out['pauth'])
self.assertEqual(out['calls']['update_disjoint']['entries'], sum(100 * c + 1 for c in range(6)))
self.assertEqual(out['calls']['switch_kernel']['hold'], sum(10 * (100 * c + 5) for c in range(6)))
self.assertEqual(out['declined']['switch'], sum(c + 8 for c in range(6)))
bad = bytearray(record)
bad[0] ^= 1
with self.assertRaises(ValueError):
perf['decode_fine_profile'](bytes(bad), lay=lay)
with self.assertRaises(ValueError):
perf['decode_fine_profile'](record[:-8], lay=lay)
offset, size = perf['fine_profile_spec'](defines)
exit_offset, exit_size = perf['exit_profile_spec'](defines)
self.assertEqual((offset, size), (exit_offset + exit_size, 16 + 6 * lay['size']))
# SNAP_FAST_PAUTH_CALLS: the two pointer buckets after the five, the
# header's bit 1 set; a bit that disagrees with the layout is refused.
pauth = defines + ['-DSNAP_FAST_PAUTH_CALLS=1']
lay7 = perf['fine_layout'](pauth)
self.assertEqual(lay7['calls'][-2:], ('pauth', 'pauth_batch'))
self.assertEqual(lay7['declined'] - lay7['call'], lay['declined'] - lay['call'] + 2 * lay['bucket'])
self.assertEqual((lay7['call'], lay7['bucket']), (lay['call'], lay['bucket']))
record7 = blob_of('pauth', ['-DSNAP_FAST_PAUTH_CALLS=1'], 3)
out = perf['decode_fine_profile'](record7, lay=lay7, hz=19.2e6)
self.assertTrue(out['switch'] and out['pauth'])
self.assertEqual(out['calls']['pauth']['entries'], sum(100 * c + 6 for c in range(6)))
self.assertEqual(out['calls']['pauth_batch']['hold'], sum(10 * (100 * c + 7) for c in range(6)))
self.assertEqual(out['calls']['update_disjoint']['entries'], sum(100 * c + 1 for c in range(6)))
self.assertEqual(out['declined']['switch'], sum(c + 8 for c in range(6)))
for wrong, wrong_lay in ((blob_of('pauth-unmarked', ['-DSNAP_FAST_PAUTH_CALLS=1'], 1), lay7),
(blob_of('marked', [], 3), lay)):
with self.assertRaises(ValueError):
perf['decode_fine_profile'](wrong, lay=wrong_lay)
offset7, size7 = perf['fine_profile_spec'](pauth)
self.assertEqual((offset7, size7), (exit_offset + exit_size, 16 + 6 * lay7['size']))
self.assertEqual(perf['token_costs_spec'](pauth + ['-DSNAP_GATE_WAIT=1'])[0], offset7 + size7)
# ---- SNAP_FAST_PAUTH_CALLS: the converted pointer calls --------------------
PAUTH_C = 'tools/test-fast-pauth-calls.c'
PAUTH_IMPL = ['compat/pauth-pointer.c', 'compat/pauth-batch.c', 'compat/pauth-domain.c', 'compat/pauth-user.c',
'compat/aes.c']
PAUTH_TCR = '__asm__ volatile("mrs %0,tcr_el1":"=r"(tcr));'
PAUTH_CASES = (' case SPTM_FN_SIGN_USER_POINTER:\n case SPTM_FN_AUTH_USER_POINTER: {',
' case SPTM_FN_SWITCH_ROOT: {')
# What the shared signing bodies may name in the context: this CPU's records
# (indexed by its slot) and the boot-fixed domain.
POINTER_STATE = {'pauth_bank', 'user_pauth', 'pauth_derive', 'pauth_pointer', 'pauth_batch'}
POINTER_FIXED = {'pauth_domain', 'aes_hardware'}
def definition(text, name):
"""The whole definition of the static function name in text, signature
through closing brace; LookupError when there is none."""
m = re.search(r'^static\b[^;{}()]*?\b' + re.escape(name) + r'\s*\([^;{}]*\)\s*\{', text, re.M)
if not m:
raise LookupError(f'probes/xnuboot.c has no {name}')
i, depth = m.end() - 1, 0
while True:
depth += text[i] == '{'
depth -= text[i] == '}'
i += 1
if not depth:
return text[m.start():i] + '\n'
def pauth_extract(text):
"""pauth-extract.h for tools/test-fast-pauth-calls.c: the monitor's own code
for every path -- monitor_call_impl's pointer cases twice (as the default
build and as the define-on build compile them), the helpers, the native
sign callbacks and the converted calls -- with TCR_EL1 read from the model.
LookupError when the source has no converted pointer calls."""
defines = re.findall(r'^#define SPTM_FN_(?:BATCH_SIGN|SIGN|AUTH)_USER_POINTER \d+\n', text, re.M)
native = re.search(r'^struct pauth_batch_native \{[^\n]*\};\n', text, re.M)
state = re.search(r'^enum \{FINE_DECLINE,FINE_DONE,FINE_FATAL\};\nstruct fine_call_state \{.*?\n\};\n', text,
re.M | re.S)
if len(defines) != 3 or not native or not state or text.count(PAUTH_CASES[0]) != 1:
raise LookupError('the pointer endpoints, their native context, the converted-call state or the cases')
start = text.index(PAUTH_CASES[0])
cases = text[start:text.index(PAUTH_CASES[1], start)]
def locked(name, on):
return (f'#undef SNAP_FAST_PAUTH_CALLS\n#define SNAP_FAST_PAUTH_CALLS {on}\n'
f'static int {name}(struct context *c,uint64_t *frame,uint64_t *elr,'
'struct guest_cpu_buffer cpu_scratch)\n{\n uint32_t endpoint=(uint32_t)frame[16];\n'
' switch (endpoint) {\n' + cases + ' default: return -1;\n }\n'
' c->calls++;\n *elr += 4;\n return 1;\n}\n')
out = (''.join(defines) + native[0] +
''.join(definition(text, n) for n in ('pauth_batch_native_sign', 'pauth_pointer_native_sign',
'user_pointer_call', 'user_pointer_batch')) +
state[0] + ''.join(definition(text, n) for n in ('fine_pauth_pointer', 'fine_pauth_batch')) +
locked('locked_off', 0) + locked('locked_on', 1) + '#undef SNAP_FAST_PAUTH_CALLS\n'
'#define SNAP_FAST_PAUTH_CALLS 1\n')
if out.count(PAUTH_TCR) != 6:
raise LookupError(f'{out.count(PAUTH_TCR)} TCR_EL1 reads, not the helpers\' two and the cases\' four')
return out.replace(PAUTH_TCR, 'tcr=c->tcr;')
def pauth_differential(text, cases='20000', seed='11'):
"""Build tools/test-fast-pauth-calls.c on text's extracted pointer calls
(ASan/UBSan) and run it: (returncode, output)."""
try:
header = pauth_extract(text)
except LookupError as error:
return 2, f'extract: {error}'
d = WORK / f'pauth-{hashlib.sha256(text.encode()).hexdigest()[:12]}'
d.mkdir(exist_ok=True)
(d / 'pauth-extract.h').write_text(header)
binary = d / 'differential'
build = subprocess.run(['clang', '-std=gnu11', '-O1', '-g', '-Wall', '-Wextra', '-Werror', *SAN['asan'],
'-DSNAP_FINE_LOCKS=1', '-DSNAP_FAST_PAUTH_CALLS=1', '-Icompat', '-Ishadow', f'-I{d}',
PAUTH_C, *PAUTH_IMPL, '-o', str(binary)], cwd=ROOT, capture_output=True, text=True)
if build.returncode:
return build.returncode, 'build: ' + build.stderr[-3000:]
run = subprocess.run([str(binary), cases, seed], capture_output=True, text=True, timeout=600)
return run.returncode, run.stdout + run.stderr
def pauth_problems(text):
"""Every reason probes/xnuboot.c's converted pointer calls would not be the
locked path's, or would reach stripes, shared or token-path state; and the
default build's cases. Empty means none."""
p = []
flags, _ = installed_defines()
base = [f for f in flags if f.startswith('-D')] + ['-DSNAP_FINE_LOCKS=1', '-DSNAP_FINE_SWITCH=1']
fns = functions(effective_of(text, base + ['-DSNAP_FAST_PAUTH_CALLS=1']))
off = effective_of(text, base)
value = dict(re.findall(r'^#define SPTM_FN_(\w+)\s+(\d+)\s*$', text, re.M))
sign, auth, batch, switch = (value.get(n, 'none') for n in ('SIGN_USER_POINTER', 'AUTH_USER_POINTER',
'BATCH_SIGN_USER_POINTER', 'SWITCH_ROOT'))
call = fns.get('fine_sptm_call', '')
# Classified and dispatched.
if not re.search(rf'case\s+{sign}\s*:\s*case\s+{auth}\s*:\s*kind\s*=\s*FINE_CALL_PAUTH\s*;', call) or \
not re.search(rf'case\s+{batch}\s*:\s*kind\s*=\s*FINE_CALL_PAUTH_BATCH\s*;', call):
p.append('fine_sptm_call does not classify SIGN/AUTH as FINE_CALL_PAUTH and BATCH_SIGN as '
'FINE_CALL_PAUTH_BATCH')
if not re.search(r'kind\s*==\s*FINE_CALL_PAUTH\s*\?\s*fine_pauth_pointer\(c,frame,&s\)', call) or \
not re.search(r'kind\s*==\s*FINE_CALL_PAUTH_BATCH\s*\?\s*fine_pauth_batch\(c,frame,&cpu_scratch,&s\)',
call):
p.append('fine_sptm_call does not dispatch the pointer kinds to fine_pauth_pointer and fine_pauth_batch')
# One signing body: the locked cases and the converted calls run the
# helpers, neither an engine of its own.
impl = fns.get('monitor_call_impl', '')
m = re.search(rf'case\s+{sign}\s*:\s*case\s+{auth}\s*:\s*\{{', impl)
n = re.search(rf'case\s+{switch}\s*:', impl[m.end():]) if m else None
cases = impl[m.start():m.end() + n.start()] if m and n else ''
engines = ('pauth_pointer', 'pauth_batch_sign', 'pauth_domain_user_cached')
for body, name, helpers in ((cases, 'the locked cases', ('user_pointer_call', 'user_pointer_batch')),
(fns.get('fine_pauth_pointer', ''), 'fine_pauth_pointer', ('user_pointer_call',)),
(fns.get('fine_pauth_batch', ''), 'fine_pauth_batch', ('user_pointer_batch',))):
seq = calls(body)
if not body or any(h not in seq for h in helpers) or any(e in seq for e in engines):
p.append(f'{name}: not the shared signing body ({seq})')
# The helpers: this CPU's records and the boot-fixed domain, nothing
# recorded, no reference audit.
for name in ('user_pointer_call', 'user_pointer_batch'):
body = fns.get(name, '')
used = re.findall(r'\bc->(\w+)(\[[^\]]*\])?', body)
if not body or any(f not in POINTER_STATE | POINTER_FIXED for f, _ in used) or \
any(f in POINTER_STATE and index != '[v->slot]' for f, index in used):
p.append(f'{name} reaches more than this CPU\'s signing state and the domain: {used}')
if 'record(' in body or 'pv_audit' in body:
p.append(f'{name} records or touches pv_audit')
# No stripe: the pointer kinds leave fine_lockset_for before any root
# lookup or stripe.
lockset = fns.get('fine_lockset_for', '')
first = min((lockset.find(t) for t in ('hw_root(', 'fine_user_root(', 'fine_lockset_add(') if t in lockset),
default=len(lockset))
guard = re.search(r'if\s*\(\s*\(\s*\(s->kind\)\s*==\s*FINE_CALL_PAUTH\s*\|\|\s*\(s->kind\)\s*==\s*'
r'FINE_CALL_PAUTH_BATCH\s*\)\s*\)\s*\{\s*fine_lockset_init\(set\);\s*return 1;\s*\}', lockset)
if not guard or guard.start() > first:
p.append('a pointer call reaches a root lookup or a stripe in fine_lockset_for')
# The epilogue leaves pv_audit.pending alone; a failure latches no view.
pointer = r'\(\s*\({0}\)\s*==\s*FINE_CALL_PAUTH\s*\|\|\s*\({0}\)\s*==\s*FINE_CALL_PAUTH_BATCH\s*\)'
if not re.search(r'if\s*\(\s*!\s*' + pointer.format('kind') + r'\s*\)\s*pv->pending\s*=\s*0\s*;', call):
p.append('the converted epilogue clears pv_audit.pending for a pointer call')
if not re.search(r'if\s*\(\s*!\s*' + pointer.format(r's->kind') + r'\s*\)\s*fine_view_failed\(c\);',
fns.get('fine_fatal', '')):
p.append('a pointer call\'s failure latches a view failure')
batch_body = fns.get('fine_pauth_batch', '')
if 'sptm_context_memory(' in batch_body or 'fine_memory(' not in batch_body:
p.append('fine_pauth_batch builds a token-path memory')
# Nothing token-only reached from the converted pointer calls.
seen, todo = set(), ['fine_pauth_pointer', 'fine_pauth_batch']
while todo:
name = todo.pop()
if name in seen or name not in fns:
continue
seen.add(name)
for found in re.finditer(r'\b([A-Za-z_]\w*)\b(\s*\()?', fns[name]):
if found[1] in fns and (found[2] or found[1].startswith('fine_')):
todo.append(found[1])
if seen & FORBIDDEN:
p.append(f'converted pointer calls reach {sorted(seen & FORBIDDEN)}')
for need in ('user_pointer_call', 'user_pointer_batch', 'fine_memory', 'fine_sptm_publish'):
if need not in seen:
p.append(f'{need} not reached from the converted pointer calls')
if '(uint64_t)SNAP_FAST_PAUTH_CALLS << 1 |' not in text:
p.append('the fine profile header does not say which bucket layout follows')
# The default build: no pointer conversion, its inline cases.
for token in ('FINE_CALL_PAUTH', 'fine_pauth_', 'user_pointer_call', 'user_pointer_batch'):
if token in off:
p.append(f'{token} in a build without SNAP_FAST_PAUTH_CALLS')
inline = calls(functions(off).get('monitor_call_impl', ''))
if 'pauth_pointer' not in inline or 'pauth_batch_sign' not in inline:
p.append('the default build lost its inline signing cases')
return p
# Each weakened binding must be caught by pauth_problems: (old, new).
PAUTH_SOURCE_MUTANTS = {
'SIGN/AUTH not converted':
(' case SPTM_FN_SIGN_USER_POINTER: case SPTM_FN_AUTH_USER_POINTER: kind=FINE_CALL_PAUTH;break;\n', ''),
'BATCH_SIGN not dispatched':
(' kind==FINE_CALL_PAUTH_BATCH ? fine_pauth_batch(c,frame,&cpu_scratch,&s) :\n', ''),
'a converted call with its own signing copy':
(' enum sptm_result r=user_pointer_call(c,frame,(uint32_t)s->x16,&ran);\n',
' enum sptm_result r=pauth_pointer(frame[0],frame[1],frame[2],0,0,0,0,0,'
'&c->pauth_pointer[vcpu_of(c)->slot]);ran=1;\n'),
'a locked case with its own signing copy':
(' enum sptm_result result=user_pointer_call(c,frame,endpoint,&ran);\n',
' enum sptm_result result=pauth_pointer(frame[0],frame[1],frame[2],0,0,0,0,0,'
'&c->pauth_pointer[v->slot]);(void)ran;\n'),
'a pointer call takes a stripe': (' if(FINE_POINTER_CALL(s->kind)) {fine_lockset_init(set);return 1;}\n', ''),
'the epilogue clears pv_audit.pending for a pointer call':
(' if(!FINE_POINTER_CALL(kind))\n#endif\n pv->pending=0;',
'#endif\n pv->pending=0;'),
'a pointer failure latches a view failure':
(' if(!FINE_POINTER_CALL(s->kind))\n#endif\n fine_view_failed(c);', '#endif\n fine_view_failed(c);'),
'the converted batch publishes through the token path':
(' struct sptm_memory memory=fine_memory(c);\n int ran=0;',
' struct sptm_memory memory=sptm_context_memory(c);\n int ran=0;'),
'a pointer call reclaims the arena':
('s->fail_b=(uint32_t)s->x16;return FINE_FATAL;}\n',
's->fail_b=(uint32_t)s->x16;return FINE_FATAL;}\n arena_reserve(c,frame);\n'),
'a helper signs into another CPU\'s record':
(' pauth_pointer_native_sign,&native,&c->pauth_pointer[v->slot]);\n aes_clear(&native,sizeof(native));',
' pauth_pointer_native_sign,&native,&c->pauth_pointer[0]);\n aes_clear(&native,sizeof(native));'),
'the header does not mark the layout':
(' | (uint64_t)SNAP_FAST_PAUTH_CALLS << 1 |', ' |'),
}
# Each must fail the differential (not its build or extraction): (old, new).
PAUTH_DIFF_MUTANTS = {
'a decline after the engine ran':
(' if(!ran) {s->reason=FINE_NO_ENGINE;return FINE_DECLINE;}\n if(r) {s->result=r;s->fail_kind=EV_TABLE_FAILED;'
's->fail_a=s->a0;s->fail_b=(uint32_t)s->x16;',
' if(r) {s->reason=FINE_NO_ENGINE;return FINE_DECLINE;}\n if(r) {s->result=r;s->fail_kind=EV_TABLE_FAILED;'
's->fail_a=s->a0;s->fail_b=(uint32_t)s->x16;'),
'the define-on body inverts the AUTH flag':
(' endpoint==SPTM_FN_AUTH_USER_POINTER,(tcr>>37)&1,(tcr>>51)&1,\n'
' pauth_pointer_native_sign,&native,&c->pauth_pointer[v->slot]);\n aes_clear',
' endpoint==SPTM_FN_SIGN_USER_POINTER,(tcr>>37)&1,(tcr>>51)&1,\n'
' pauth_pointer_native_sign,&native,&c->pauth_pointer[v->slot]);\n aes_clear'),
'the default case drifts (TBID from the wrong bit)':
(' endpoint==SPTM_FN_AUTH_USER_POINTER,(tcr>>37)&1,(tcr>>51)&1,\n pauth_pointer_native_sign',
' endpoint==SPTM_FN_AUTH_USER_POINTER,(tcr>>37)&1,(tcr>>52)&1,\n pauth_pointer_native_sign'),
'a converted batch failure names the endpoint':
(' s->fail_b=c->pauth_batch[vcpu_of(c)->slot].failed_index;return FINE_FATAL;',
' s->fail_b=(uint32_t)s->x16;return FINE_FATAL;'),
'the converted batch publishes through the token path': PAUTH_SOURCE_MUTANTS[
'the converted batch publishes through the token path'],
'the converted answer from the wrong field':
(' frame[0]=c->pauth_pointer[vcpu_of(c)->slot].result;return FINE_DONE;',
' frame[0]=c->pauth_pointer[vcpu_of(c)->slot].computed;return FINE_DONE;'),
}
class Pauth(unittest.TestCase):
"""SNAP_FAST_PAUTH_CALLS: the converted pointer calls against the locked
path on the monitor's own code, and their source binding."""
def test_converted_equals_locked(self):
rc, out = pauth_differential(SOURCE, '6000' if ARGS.quick else '20000')
self.assertEqual(rc, 0, out[-3000:])
print([l for l in out.splitlines() if l.startswith('pauth differential:')][0], file=sys.stderr)
def test_weakened_calls_fail_the_differential(self):
for name, (old, new) in PAUTH_DIFF_MUTANTS.items():
with self.subTest(name):
self.assertEqual(SOURCE.count(old), 1, name)
rc, out = pauth_differential(SOURCE.replace(old, new), '4000')
self.assertFalse(out.startswith(('build:', 'extract:')), f'{name}: {out[-1500:]}')
self.assertNotEqual(rc, 0, f'{name} survived: {out[-800:]}')
print(f'pauth differential mutant {name}: caught ({out.strip().splitlines()[-1][:110]})',
file=sys.stderr)
def test_binding(self):
self.assertEqual(pauth_problems(SOURCE), [])
def test_weakened_bindings_are_caught(self):
for name, (old, new) in PAUTH_SOURCE_MUTANTS.items():
with self.subTest(name):
self.assertEqual(SOURCE.count(old), 1, name)
self.assertNotEqual(pauth_problems(SOURCE.replace(old, new)), [], name)
@unittest.skipUnless(ARGS.build, 'pass --build to compile the monitor')
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
installed, sets = installed_defines()
sets = {'control': sets['control'], 'fast': sets['fast'],
'fast-wfi': sets['fast'] + ['-DSNAP_WFI_RESUME=1'], 'installed': installed}
expect = dict(e.split('=', 1) for e in ARGS.expect)
out = Path(ARGS.out) if ARGS.out else Path(tempfile.mkdtemp(prefix='fine-locks-build-'))
budget = monitor_run.OUT_AT - monitor_run.CODE_AT
saved = list(monitor_run.FLAGS)
results = {}
try:
for name, flags in sets.items():
blobs = {}
labels = [('default', []), ('off', ['-DSNAP_FINE_LOCKS=0', '-DSNAP_FINE_SWITCH=0',
'-DSNAP_FAIR_TOKEN=0', '-DSNAP_FAST_PAUTH_CALLS=0',
'-DSNAP_PHASE_FAIR_GATE=0'])]
labels.append(('fair', ['-DSNAP_FAIR_TOKEN=1']))
if name == 'installed':
labels += [('on', ['-DSNAP_FINE_LOCKS=1']),
('on-off', ['-DSNAP_FINE_LOCKS=1', '-DSNAP_FAST_PAUTH_CALLS=0']),
('on-switch', ['-DSNAP_FINE_LOCKS=1', '-DSNAP_FINE_SWITCH=1']),
('on-switch-fair', ['-DSNAP_FINE_LOCKS=1', '-DSNAP_FINE_SWITCH=1',
'-DSNAP_FAIR_TOKEN=1']),
('on-pauth', ['-DSNAP_FINE_LOCKS=1', '-DSNAP_FAST_PAUTH_CALLS=1']),
('on-switch-fair-wait', ['-DSNAP_FINE_LOCKS=1', '-DSNAP_FINE_SWITCH=1',
'-DSNAP_FAIR_TOKEN=1', '-DSNAP_GATE_WAIT=1']),
('on-switch-fair-wait-phase', ['-DSNAP_FINE_LOCKS=1', '-DSNAP_FINE_SWITCH=1',
'-DSNAP_FAIR_TOKEN=1', '-DSNAP_GATE_WAIT=1',
'-DSNAP_PHASE_FAIR_GATE=1'])]
for label, extra in labels:
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), f'{name}-{label}')
self.assertEqual(blobs['default'], blobs['off'], name)
self.assertNotEqual(blobs['default'], blobs['fair'], name)
if name == 'installed':
self.assertNotEqual(blobs['default'], blobs['on'])
self.assertEqual(blobs['on'], blobs['on-off'])
self.assertNotEqual(blobs['on'], blobs['on-switch'])
self.assertNotEqual(blobs['on-switch'], blobs['on-switch-fair'])
self.assertNotEqual(blobs['on'], blobs['on-pauth'])
self.assertNotEqual(blobs['on-switch-fair-wait'], blobs['on-switch-fair-wait-phase'])
# SNAP_PHASE_FAIR_GATE needs SNAP_FAIR_TOKEN (writers' FIFO) and SNAP_GATE_WAIT.
for refused in (['-DSNAP_FINE_LOCKS=1', '-DSNAP_GATE_WAIT=1', '-DSNAP_PHASE_FAIR_GATE=1'],
['-DSNAP_FINE_LOCKS=1', '-DSNAP_FAIR_TOKEN=1', '-DSNAP_PHASE_FAIR_GATE=1']):
monitor_run.FLAGS[:] = flags + refused
with self.assertRaises(subprocess.CalledProcessError):
monitor_run.build('xnuboot', out / f'{name}-refused-phase-{len(results)}-{refused[1][7:-2]}')
else:
for refused in ('-DSNAP_FINE_LOCKS=1', '-DSNAP_FAST_PAUTH_CALLS=1'):
monitor_run.FLAGS[:] = flags + [refused]
with self.assertRaises(subprocess.CalledProcessError):
monitor_run.build('xnuboot', out / f'{name}-refused-{refused[7:-2].lower()}')
if name in expect:
self.assertEqual(results[f'{name}-default']['sha256'], expect[name], name)
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__':
unittest.main(argv=[sys.argv[0], *REST])