diff --git a/tools/test-kernel-pperm-variants.c b/tools/test-kernel-pperm-variants.c index 16dbb9b..7e08cd1 100644 --- a/tools/test-kernel-pperm-variants.c +++ b/tools/test-kernel-pperm-variants.c @@ -17,6 +17,12 @@ * samples 1 in 64). -DVARIANTS_PRE_CHANGE=1 drops the snapshot-generation * check from the exact predicate and must fail. * + * With -DSNAP_VIEW_RECLAIM=1 both machines also retire the copies of the three + * ordinary tables now and then (sprr_views_reclaim with kernel_views_quiesce's + * steps: snapshot invalidated, publication rows reset, refresh) and prepare one + * again on a retired set: every check above must keep holding while table + * positions move under the snapshot, the publication rows and the variants. + * * cc -std=gnu11 -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all \ * -Icompat -Ishadow tools/test-kernel-pperm-variants.c shadow/sprr-view.c \ * shadow/shadow.c shadow/mmu.c -o build/test-kernel-pperm-variants @@ -111,7 +117,7 @@ static int reuse_skip(struct machine *m,unsigned cpu,int current,uint64_t before const struct sprr_publish_state *ps=&m->publish; return current && !((before^pperm)&SPRR_KERNEL_VIEW_PPERM) && ps->root==views.canonical_root && ps->count==views.count && - !ps->pending[cpu][0] && !ps->pending[cpu][1]; + sprr_publish_row_empty(ps,cpu); } /* The unchanged monitor: kernel_change_pperm_impl with SNAP_KERNEL_PPERM_REUSE. */ static int pperm_reference(unsigned cpu,uint64_t pperm) @@ -269,6 +275,27 @@ static void both_prepare(uint64_t va) enter(&ref);s=kshadow();CHECK(sprr_views_prepare(&views,&s,va));leave(); enter(&cand);s=kshadow();CHECK(sprr_views_prepare(&views,&s,va));leave(); } +#if SNAP_VIEW_RECLAIM +static uint64_t reclaims,retired; +/* kernel_views_quiesce on the active machine: refresh() is kernel_refresh + * (a miss: full sync, publication, saves++) and its ALLE1IS. */ +static int machine_quiesce(void *ctx) +{ + (void)ctx; + sprr_reuse_invalidate(&active->reuse); + sprr_publish_reset(&active->publish); + return refresh(); +} +static void both_reclaim(void) +{ + struct shadow s;int a,b; + enter(&ref);s=kshadow();a=sprr_views_reclaim(&views,&s,machine_quiesce,0);leave(); + enter(&cand);s=kshadow();b=sprr_views_reclaim(&views,&s,machine_quiesce,0);leave(); + CHECK(a==b && a>=0 && ref.views.count==cand.views.count); + if(a>0) {reclaims++;retired+=(unsigned)a;} + invariants(); +} +#endif static const uint64_t deltas[4]={0,0x100,SPRR_UPERM_TPRO_WRITE,0x100|SPRR_UPERM_TPRO_WRITE}; static void canonical_change(void) { @@ -378,8 +405,15 @@ int main(void) enter(&cand);CHECK(pperm_candidate(cpu,cand.pp[cpu]^0x100));leave(); mutate(BASE+0x3000,5,old);both_refresh(); } else if(op<34) { - if(prepared>=3) {both_refresh();continue;} static const uint64_t vas[3]={0x200000,0x401000,0x600000}; +#if SNAP_VIEW_RECLAIM + if(prepared>=3) { + /* Their leaves are ordinary: the three copies retire; the one + * prepared again takes the first retired set. */ + both_reclaim();both_prepare(vas[rnd(3)]);both_refresh();continue; + } +#endif + if(prepared>=3) {both_refresh();continue;} if(prepared==1) { mutate(BASE+0x2000,2,BASE+0x5000+3); mutate(BASE+0x5000,1,DATA+3+ATTR+PT_UXN+PT_PXN); @@ -400,6 +434,9 @@ int main(void) if(failed>20)break; } CHECK(prepared==3); +#if SNAP_VIEW_RECLAIM + CHECK(reclaims>10 && retired>=reclaims); +#endif CHECK(cand.exact>800 && cand.synced>=30 && cand.fallbacks>10 && !cand.mismatches); /* A synced swap is a CPU's first flip after a re-save (or after an * in-place flip, or its first ever); every other bit-8 flip is exact. */ @@ -416,6 +453,9 @@ int main(void) (unsigned long long)cand.exact,(unsigned long long)cand.synced, (unsigned long long)cand.inplace,(unsigned long long)cand.fallbacks, (unsigned long long)cand.skipped,(unsigned long long)cand.var.reserved); +#if SNAP_VIEW_RECLAIM + printf("reclaim: %llu reclaims retired %llu tables\n",(unsigned long long)reclaims,(unsigned long long)retired); +#endif printf("%s: %u kernel PPERM variant checks, %u failures\n",failed ? "FAIL" : "PASS",checks,failed); return failed ? 1 : 0; } diff --git a/tools/test-tpro-view.c b/tools/test-tpro-view.c index 0ed7e8b..10942ef 100644 --- a/tools/test-tpro-view.c +++ b/tools/test-tpro-view.c @@ -5,7 +5,8 @@ #define BASE UINT64_C(0x40000000) #define DATA UINT64_C(0x94050c000) -#define BYTES (4u<<20) +/* Room for a full pool of six-copy tables (SNAP_VIEW_RECLAIM: 256). */ +#define BYTES (SPRR_VIEW_TABLES>128 ? (8u<<20) : (4u<<20)) #define ATTR (PT_AF|(3UL<<8)|(1UL<<11)) static _Alignas(4096) unsigned char ram[BYTES],before[BYTES],expected[BYTES]; static uint64_t next,fail_read; diff --git a/tools/test-user-fused-compare.c b/tools/test-user-fused-compare.c index 1b0f0b2..dc769d2 100644 --- a/tools/test-user-fused-compare.c +++ b/tools/test-user-fused-compare.c @@ -117,14 +117,15 @@ int main(void) s=setup();CHECK(sprr_views_init(&views,&s));stats.calls=63;blank_table=1; CHECK(!fused_compare(&s,state,modes));blank_table=0; /* Fill both mapping arrays to their supported maximum: three upper - * tables and125distinct TPRO leaf tables, with768private destinations. */ - s=setup();next=BASE+0x100000; - for(unsigned i=0;i<125;i++) { + * tables and SPRR_VIEW_TABLES-3 distinct TPRO leaf tables, with six + * private destinations each. */ + s=setup();next=BASE+0x80000+4096UL*SPRR_VIEW_TABLES; + for(unsigned i=0;i +#include +#include +#include "sprr-view.h" +#include "sprr-publish.h" +#include "sprr.h" +#if !SNAP_VIEW_RECLAIM +#error "build with -DSNAP_VIEW_RECLAIM=1" +#endif + +#define BASE UINT64_C(0x40000000) +#define BYTES (32u<<20) +#define PAGES (BYTES/4096) +#define DATA UINT64_C(0x900000000) +#define ATTR (PT_AF|(3UL<<8)) +#define L1 (BASE+0x1000) +#define L2(k) (BASE+0x2000+0x1000UL*(k)) +#define L3(k,j) (BASE+0x10000+0x1000UL*((k)*128u+(j))) +#define COPIES (BASE+0x400000) +#define SEL3 (ATTR+PT_PXN+PT_UXN+3) /* ordinary RW/NX page: class 3 */ +#define SEL2 (ATTR+PT_UXN+3) /* selector2 (ROZONE): class 2 */ +#define SEL4 (ATTR+(1UL<<6)+3) /* selector4 (TXM aperture): class 4 */ +#define SEL11 (ATTR+(2UL<<6)+PT_UXN+PT_PXN+3) /* fixed RO/NX: class 11 */ + +static unsigned char ram[BYTES],dirty[PAGES],stale[PAGES]; +static struct sprr_views views; +static struct sprr_publish_state pub; +static uint64_t pp[SPRR_VIEW_CPUS],next_copy,read_fail,allocs,alloc_budget; +static unsigned checks,failed,stale_writes,tlbis,quiesces,quiesce_fails,quiesce_no_tlbi; +#define CHECK(x) do {checks++;if(!(x)){failed++;printf("FAIL %u: %s\n",__LINE__,#x);}} while(0) + +static unsigned page(uint64_t pa) {return (unsigned)((pa-BASE)/4096);} +static int rd(void *ctx,uint64_t pa,uint64_t *out) +{ + (void)ctx;if(paBASE+BYTES-8 || (pa&7) || pa==read_fail)return -1; + memcpy(out,ram+(pa-BASE),8);return 0; +} +/* kernel_table_write: identical stores elided; a changed one marks its copy + * pending (SNAP_KERNEL_DIRTY_PUBLISH) and leaves the page unpublished. */ +static int wr(void *ctx,uint64_t pa,uint64_t value) +{ + uint64_t old; + if(rd(ctx,pa,&old))return -1; + if(old==value)return 0; + if(stale[page(pa)])stale_writes++; + if(!sprr_publish_note(&pub,&views,pa))return -1; + memcpy(ram+(pa-BASE),&value,8);dirty[page(pa)]=1;return 0; +} +/* private_kernel_alloc: zeroed with plain stores, unpublished. */ +static uint64_t alloc(void *ctx) +{ + (void)ctx; + if(!alloc_budget || next_copy>BASE+BYTES-4096)return 0; + alloc_budget--;allocs++; + uint64_t pa=next_copy;next_copy+=4096; + memset(ram+(pa-BASE),0,4096);dirty[page(pa)]=1;return pa; +} +static struct shadow shadow_of(void) +{return (struct shadow){.txsz=17,.root=BASE,.alloc=alloc,.read=rd,.write=wr};} +static void put(uint64_t pa,uint64_t value) +{memcpy(ram+(pa-BASE),&value,8);} +static uint64_t get(uint64_t pa) {uint64_t v;memcpy(&v,ram+(pa-BASE),8);return v;} +static uint64_t va_of(unsigned k,unsigned j,unsigned i) +{return ((uint64_t)k<<30)|((uint64_t)j<<21)|((uint64_t)i<<12);} +static uint64_t frame(unsigned k,unsigned j,unsigned i) +{return DATA+(((uint64_t)k*128+j)*512+i)*4096;} + +static void tlbi(void) {memset(stale,0,sizeof stale);tlbis++;} +static int publish_cpu(unsigned cpu) +{ + if(!sprr_publish_prepare(&pub,&views))return 0; + for(unsigned level=4;level--;)for(unsigned n=0;n=BASE+BYTES)return 1; + if(stale_only ? stale[page(table)] : dirty[page(table)])return 1; + for(unsigned i=0;i<512 && level<3;i++) { + uint64_t d=get(table+8UL*i); + if((d&3)==3 && reachable_bad(d&PT_OA_MASK,level+1,stale_only))return 1; + } + return 0; +} +/* kernel_views_quiesce's model. */ +static int quiesce(void *ctx) +{ + (void)ctx;quiesces++; + unsigned retired=0; + for(unsigned n=views.count;nsource>=BASE && t->sourcelevel<=3);} + else CHECK(!t->source && (!t->level || t->level==SPRR_VIEW_RETIRED)); + for(unsigned cpu=0;cpucopy[cpu]; + if(!pa) {CHECK(n>=views.count);continue;} + CHECK(pa>=COPIES && pa=0)CHECK(release(2,j)); + CHECK(refresh());invariants(); + uint64_t order[SPRR_VIEW_TABLES];unsigned kept=0; + unsigned under2=0; + for(unsigned j=0;j<128;j++)under2+=find_source(L3(2,j))>=0; + for(unsigned n=0;n=L3(1,0) && src=L3(2,0) && src=0) { + uint64_t src=views.tables[n].source; + int dead=src==L3(0,5) || src==L3(0,6) || src==L2(2) || (src>=L3(1,0) && src=L3(2,0) && src=0 && find_source(L1)>=0); + for(unsigned n=views.count;n=0 && find_source(L3(3,100))>=0 && find_source(L3(1,5))<0); + CHECK((find_source(L3(0,127))>=0)==!wrong && !view_mismatches()==!wrong); + if(!wrong) {CHECK(views.count==SPRR_VIEW_TABLES);invariants();} + else { + CHECK(view_mismatches()==4*SPRR_VIEW_CPUS); + const unsigned again[1][2]={{0,127}}; + CHECK(prepare_change(again,1) && refresh() && !view_mismatches()); + } + } + /* A protected table dropped from the views is visible to the walk check. */ + build();s=shadow_of(); + CHECK(protect(0,0,SEL2) && protect(0,1,SEL2) && !view_mismatches()); + struct sprr_views good=views; + int victim=find_source(L3(0,1));CHECK(victim==(int)views.count-1); + views.count--;sprr_publish_reset(&pub); + CHECK(sync_publish() && view_mismatches()==4*SPRR_VIEW_CPUS); + views=good;sprr_publish_reset(&pub);CHECK(sync_publish() && !view_mismatches()); + printf("view reclaim: %u quiesces, %u TLBIs, %llu pages allocated\n",quiesces,tlbis, + (unsigned long long)allocs); + printf("%s: %u view reclaim checks, %u failures\n",failed ? "FAIL" : "PASS",checks,failed); + return failed ? 1 : 0; +} diff --git a/tools/test-view-reclaim.py b/tools/test-view-reclaim.py new file mode 100644 index 0000000..e688cfa --- /dev/null +++ b/tools/test-view-reclaim.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""--view-reclaim, offline: the kernel view pool's reclaim against a monitor-shaped model, weakened implementations it +must reject, the consumers that index tables by position, the opt-in wiring and (on request) the monitor's identity +and size. No hardware. + + python3 tools/test-view-reclaim.py # model, mutants, consumers, wiring + python3 tools/test-view-reclaim.py --build --qualification QUAL.json # + monitors: define absent, =0, =1 + +Model: tools/test-view-reclaim.c (ASan/UBSan, -DSNAP_VIEW_RECLAIM=1) fills the 256-table pool, releases, reclaims, +reuses the retired pages and checks every CPU's walk, publication and the no-store-before-TLBI rule (its header lists +them). Each MUTANTS entry weakens one property in a copy of shadow/sprr-view.c; the same program must FAIL on every one. +Consumers: tools/test-kernel-pperm-variants.c (reuse snapshot, publication rows, PPERM variant columns) with and +without the define. + +Wiring: shadow/sprr-view.h defaults SNAP_VIEW_RECLAIM to 0 and every monitor use sits under it; installed-fresh +--view-reclaim appends the define last; the report record's place agrees between probes/xnuboot/report_layout.c and +tools/perf_counters.py, whose decoder refuses an inconsistent record. + +--build: xnuboot with each --qualification's flags, define absent, =0 and =1: absent == =0 == the qualification's +monitor_sha256; =1 differs and fits the code budget (probes/run.py OUT_AT - CODE_AT), its headroom printed. +""" +from pathlib import Path +import argparse +import json +import re +import shutil +import struct +import subprocess +import sys +import tempfile +import types +import unittest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'tools')) + +_ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) +_ap.add_argument('--build', action='store_true') +_ap.add_argument('--qualification', action='append', default=[]) +_ap.add_argument('--out') +ARGS, REST = _ap.parse_known_args() if __name__ == '__main__' else (_ap.parse_args([]), []) +CC = ['cc', '-std=gnu11', '-O1', '-g', '-fsanitize=address,undefined', '-fno-sanitize-recover=all', + '-Wall', '-Wextra', '-Werror', '-Icompat', '-Ishadow'] +VIEW = ['shadow/shadow.c', 'shadow/mmu.c'] +MUTANTS = ( + ('selector2 tables retired', 'need=(UINT32_C(0x214)>>', 'need=(UINT32_C(0x210)>>'), + ('retired pages dropped by the compaction', + ' struct sprr_view_table t=v->tables[w];\n v->tables[w++]=v->tables[n];v->tables[n]=t;', + ' v->tables[w++]=v->tables[n];'), + ('no room check before the adds', ' if(v->count+missing>SPRR_VIEW_TABLES)return 0;\n', ' (void)missing;\n'), + ('a retired set reused before its invalidation', ' if(t->level)return 0;\n', '\n'), + ('pages freed before the quiesce', + ' v->count=w;\n if(!quiesce(ctx))return -1;\n for(unsigned n=w;ntables[n].level=0;', + ' v->count=w;\n for(unsigned n=w;ntables[n].level=0;\n if(!quiesce(ctx))return -1;'), + ('the quiesce before the unlinking', + ' for(unsigned n=w;ntables[n].source=0;v->tables[n].level=SPRR_VIEW_RETIRED;}\n' + ' v->count=w;\n if(!quiesce(ctx))return -1;', + ' if(!quiesce(ctx))return -1;\n' + ' for(unsigned n=w;ntables[n].source=0;v->tables[n].level=SPRR_VIEW_RETIRED;}\n' + ' v->count=w;'), + ('an upper table kept for a retired child', + 'need=child>=0 && (v->tables[child].level!=level+1 || keep[child]);', 'need=child>=0;'), + ('the root retired', ' int need=!level;\n', ' int need=0;\n'), + ('retired sets never reused', ' uint64_t pa=t->copy[cpu] ? t->copy[cpu] : shadow_alloc_page(s);', + ' uint64_t pa=shadow_alloc_page(s);'), +) + + +def work(): + return Path(ARGS.out) if ARGS.out else Path(tempfile.mkdtemp(prefix='view-reclaim-')) + + +def compile_run(test, view, binary, *defines): + b = subprocess.run([*CC, *defines, test, str(view), *VIEW, '-o', str(binary)], cwd=ROOT, + capture_output=True, text=True) + if b.returncode: + return None, b.stderr[-2000:] + r = subprocess.run([str(binary)], cwd=ROOT, capture_output=True, text=True, timeout=1800) + return r.returncode, (r.stdout + r.stderr) + + +class Model(unittest.TestCase): + def test_model_passes(self): + rc, out = compile_run('tools/test-view-reclaim.c', ROOT / 'shadow/sprr-view.c', work() / 'model', + '-DSNAP_VIEW_RECLAIM=1') + self.assertEqual(rc, 0, out[-3000:]) + self.assertIn('PASS:', out) + print('\n' + out.strip().splitlines()[-1], file=sys.stderr) + + def test_mutants_fail(self): + source = (ROOT / 'shadow/sprr-view.c').read_text() + out_dir = work() + for n, (name, old, new) in enumerate(MUTANTS): + self.assertEqual(source.count(old), 1, name) + d = out_dir / f'mutant-{n}' + d.mkdir(parents=True, exist_ok=True) + (d / 'sprr-view.c').write_text(source.replace(old, new)) + rc, out = compile_run('tools/test-view-reclaim.c', (d / 'sprr-view.c').resolve(), (d / 't').resolve(), + '-DSNAP_VIEW_RECLAIM=1') + self.assertIsNotNone(rc, f'{name}: {out}') + self.assertNotEqual(rc, 0, f'{name}: not caught') + + +class Consumers(unittest.TestCase): + def test_variants_snapshot_publication(self): + for defines in ([], ['-DSNAP_VIEW_RECLAIM=1']): + rc, out = compile_run('tools/test-kernel-pperm-variants.c', ROOT / 'shadow/sprr-view.c', + work() / f'variants{len(defines)}', *defines) + self.assertEqual(rc, 0, out[-3000:]) + self.assertIn('PASS:', out) + self.assertEqual('reclaim:' in out, bool(defines)) + + +class Wiring(unittest.TestCase): + def test_default_off(self): + header = (ROOT / 'shadow/sprr-view.h').read_text() + self.assertIn('#ifndef SNAP_VIEW_RECLAIM\n#define SNAP_VIEW_RECLAIM 0\n#endif', header) + self.assertIn('#if SNAP_VIEW_RECLAIM\n#define SPRR_VIEW_TABLES 256u\n#else\n#define SPRR_VIEW_TABLES 128u\n#endif', + header) + for path in ('probes/xnuboot/sptm_retype.c', 'probes/xnuboot/kernel_views.c', 'probes/xnuboot/report.c', + 'probes/xnuboot/context.c'): + stack = [] + for number, line in enumerate((ROOT / path).read_text().splitlines(), 1): + d = line.strip() + if d.startswith('#if'): + stack.append([d == '#if SNAP_VIEW_RECLAIM', False]) + elif d.startswith('#el') and stack: + stack[-1][1] = True + elif d.startswith('#endif'): + stack.pop() + elif re.search(r'kernel_views_prepare_change|kernel_view_reclaim|sprr_views_reclaim', line): + self.assertTrue(any(on and not other for on, other in stack), f'{path}:{number} unguarded') + + def test_cycle(self): + from cycle_lib import installed_fresh as f + import importlib.util + spec = importlib.util.spec_from_file_location('cycle_cli', ROOT / 'tools/cycle.py') + cli = importlib.util.module_from_spec(spec) + spec.loader.exec_module(cli) + base = ['installed-fresh', '--prefix', 'x', '--variant', 'fast', '--check'] + self.assertFalse(cli.parse(base).view_reclaim) + self.assertTrue(cli.parse(base + ['--view-reclaim']).view_reclaim) + self.assertEqual(f.VIEW_RECLAIM_DEFINE, '-DSNAP_VIEW_RECLAIM=1') + src = (ROOT / 'tools/cycle_lib/installed_fresh.py').read_text() + body = src[src.index('def variant_flags('):] + body = body[:body.index('\n return flags\n')] + self.assertTrue(body.rstrip().endswith('flags.append(VIEW_RECLAIM_DEFINE)'), 'the define is not appended last') + self.assertNotIn('view_reclaim', f.boot_addition(types.SimpleNamespace(view_reclaim=True))) + + def test_report_record(self): + import runpy + pc = runpy.run_path(str(ROOT / 'tools/perf_counters.py')) + layout = (ROOT / 'probes/xnuboot/report_layout.c').read_text() + expr = re.search(r'#define OUT_VIEW_RECLAIM \(OUT_THERMAL([^)]*)\)', layout).group(1) + thermal = int(re.search(r'#define A16_THERMAL_REPORT_OFFSET (0x[0-9a-f]+)u', + (ROOT / 'compat/a16-thermal.h').read_text()).group(1), 16) // 8 + terms = re.findall(r'-\s*(\w+)', expr) + self.assertEqual(''.join(re.findall(r'\S', expr)), ''.join('-' + t for t in terms)) + word = thermal - sum(6 if t == 'VIEW_RECLAIM_WORDS' else int(t) for t in terms) + offset, size = pc['view_reclaim_spec']() + self.assertEqual((offset, size), (16 + 8 * word, 64)) + good = struct.pack('<8Q', pc['VIEW_RECLAIM_MAGIC'], 48 << 32, 140, 256, 2, 9, 0, 256) + self.assertEqual(pc['decode_view_reclaim'](good)['free'], 116) + for bad in (dict(magic=0), dict(count=0), dict(count=257), dict(high=139), dict(retired=1), dict(cap=128)): + w = [pc['VIEW_RECLAIM_MAGIC'], 48 << 32, 140, 256, 2, 9, 0, 256] + w[0] = bad.get('magic', w[0]); w[2] = bad.get('count', w[2]); w[3] = bad.get('high', w[3]) + w[5] = bad.get('retired', w[5]); w[7] = bad.get('cap', w[7]) + with self.assertRaises(ValueError, msg=str(bad)): + pc['decode_view_reclaim'](struct.pack('<8Q', *w)) + + +@unittest.skipUnless(ARGS.build, 'pass --build to compile the monitor (minutes)') +class Build(unittest.TestCase): + def test_monitor(self): + import monitor_build_offline as mbo + out = work() / 'monitors' + run = (ROOT / 'probes/run.py').read_text() + budget = int(re.search(r'^OUT_AT = (0x[0-9a-fA-F]+)', run, re.M).group(1), 16) - \ + int(re.search(r'^CODE_AT = (0x[0-9a-fA-F]+)', run, re.M).group(1), 16) + results = {} + for path in ARGS.qualification: + record = json.loads(Path(path).read_text()) + name, blobs = Path(path).parent.name, {} + for label, extra in (('default', []), ('off', ['-DSNAP_VIEW_RECLAIM=0']), ('on', ['-DSNAP_VIEW_RECLAIM=1'])): + d = out / f'{name}-{label}' + if d.exists(): + shutil.rmtree(d) + r = mbo.build(ROOT, d, list(record['flags']) + extra) + blobs[label] = r['sha256'] + results[f'{name}-{label}'] = dict(bytes=r['bytes'], headroom=budget - r['bytes'], sha256=r['sha256']) + self.assertGreater(budget - r['bytes'], 0, f'{name}-{label} exceeds the code budget') + self.assertEqual(blobs['default'], record['monitor_sha256'], name) + self.assertEqual(blobs['off'], blobs['default'], name) + self.assertNotEqual(blobs['on'], blobs['default'], name) + print(json.dumps(results, indent=1), file=sys.stderr) + + +if __name__ == '__main__': + unittest.main(argv=[sys.argv[0], *REST])