Files
Snapintosh/tools/test-kernel-pperm-variants.c
T
Acelogic 1510d8c6ce Snapintosh public release
An EL2 monitor/hypervisor and toolchain that boots macOS 27 on the ASUS A16
(Snapdragon X2 Elite), with an Adreno Metal plug-in and IOGPU kext, and
air2ir3, an AIR to Mesa ir3 shader compiler.

MIT, with GPL-2.0 components listed in LICENSE and THIRD_PARTY_NOTICES.md.
No Apple, Qualcomm, ASUS or Microsoft binaries are included.
2026-10-21 21:26:36 -04:00

422 lines
19 KiB
C

/* SPDX-License-Identifier: MIT */
/* SNAP_KERNEL_PPERM_VARIANTS (docs/KERNEL-PPERM-VARIANTS-20261016.md): a
* PPERM change that flips SPRR_KERNEL_VIEW_PPERM exchanges the CPU's kernel
* copy column with its variant column and installs the new root, instead of
* rewriting the copy in place.
*
* Two machines run the same random operations. The reference is the unchanged
* monitor (snapshot reuse, the bit-8-unchanged skip, in-place sync); the
* candidate adds variants. After every step:
* - every CPU's installed translation tree (from its TTBR1) is identical on
* both machines, descriptor by descriptor (failure mode 1);
* - no page reachable from an installed root holds unpublished stores (2);
* - a TTBR1 change is invalidated before the step ends (3);
* - a valid snapshot describes all six installed columns exactly (4);
* - every unlinked variant page is byte-identical to when it was unlinked (5).
* Every exact swap is also verified against the translation (the monitor
* samples 1 in 64). -DVARIANTS_PRE_CHANGE=1 drops the snapshot-generation
* check from the exact predicate and must fail.
*
* 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
*/
#define main baseline_view_tests
#include "test-sprr-view.c"
#undef main
#include "sprr-variants.h"
#ifndef VARIANTS_PRE_CHANGE
#define VARIANTS_PRE_CHANGE 0
#endif
#define PAGES (BYTES/4096)
struct machine {
struct sprr_views views;
struct sprr_reuse_state reuse;
struct sprr_publish_state publish;
struct sprr_view_variants var;
uint64_t pp[6],ttbr1[6],next;
unsigned budget,tlbi_due[6];
unsigned char ram[BYTES],dirty[PAGES];
uint64_t skipped,inplace,exact,synced,fallbacks,verified,mismatches,hits,misses;
};
static struct machine ref,cand,*active;
static uint64_t vhash[SPRR_VIEW_TABLES][6],vpage[SPRR_VIEW_TABLES][6];
static uint64_t random_state=UINT64_C(0xbb67ae8584caa73b);
static unsigned rnd(unsigned n)
{
random_state^=random_state<<13;random_state^=random_state>>7;random_state^=random_state<<17;
return (unsigned)(random_state%n);
}
static void mark(uint64_t pa,int dirty)
{if(active && pa>=BASE && pa<BASE+BYTES)active->dirty[(pa-BASE)/4096]=(unsigned char)dirty;}
/* kernel_table_write: identical stores elided, changed stores mark the copy
* page pending (SNAP_KERNEL_DIRTY_PUBLISH) and unpublished. */
static int kwrite(void *ctx,uint64_t pa,uint64_t value)
{
uint64_t old;
if(rd(ctx,pa,&old))return -1;
if(old==value)return 0;
if(!sprr_publish_note(&active->publish,&views,pa))return -1;
mark(pa,1);return wr(ctx,pa,value);
}
/* table_alloc zeroes with plain stores: unpublished until a publish. */
static uint64_t kalloc(void *ctx)
{
uint64_t pa=alloc(ctx);
if(pa) {memset(ram+(pa-BASE),0,4096);mark(pa,1);}
return pa;
}
/* The monitor's variant allocator: private_kernel_alloc, then the page is
* cleaned at once. */
static uint64_t valloc(void *ctx)
{
uint64_t pa=alloc(ctx);
if(pa) {memset(ram+(pa-BASE),0,4096);mark(pa,0);}
return pa;
}
static struct shadow kshadow(void)
{return (struct shadow){.txsz=17,.root=BASE,.alloc=kalloc,.read=rd,.write=kwrite};}
static struct shadow vshadow(void)
{return (struct shadow){.txsz=17,.root=BASE,.alloc=valloc,.read=rd,.write=kwrite};}
static int publish_cpu(unsigned cpu)
{
struct sprr_publish_state *p=&active->publish;
if(!sprr_publish_prepare(p,&views))return 0;
for(unsigned level=4;level--;)for(unsigned n=0;n<views.count;n++) {
if(views.tables[n].level!=level)continue;
p->pages++;
if(!sprr_publish_needed(p,cpu,n)) {p->skipped++;continue;}
mark(views.tables[n].copy[cpu],0);
if(!sprr_publish_complete(p,cpu,n))return 0;
}
p->publishes++;return 1;
}
static void invalidate(unsigned cpu) {active->tlbi_due[cpu]=0;}
static int refresh(void)
{
struct shadow s=kshadow();struct machine *m=active;
int match=sprr_reuse_match(&m->reuse,&views,&s,m->pp);
if(match<0)return 0;
if(match) {m->hits++;return 1;}
m->misses++;m->reuse.valid=0;
for(unsigned cpu=0;cpu<6;cpu++)
if(!sprr_views_sync_cpu(&views,&s,cpu,m->pp[cpu]) || !publish_cpu(cpu))return 0;
for(unsigned cpu=0;cpu<6;cpu++)invalidate(cpu); /* shadow_tlb_sync's ALLE1IS */
return sprr_reuse_save(&m->reuse,&views,&s,m->pp);
}
static int reuse_skip(struct machine *m,unsigned cpu,int current,uint64_t before,uint64_t pperm)
{
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];
}
/* The unchanged monitor: kernel_change_pperm_impl with SNAP_KERNEL_PPERM_REUSE. */
static int pperm_reference(unsigned cpu,uint64_t pperm)
{
struct machine *m=active;struct shadow s=kshadow();
int current=views.count && sprr_reuse_current(&m->reuse,&views,&s,m->pp)==1;
sprr_reuse_invalidate(&m->reuse);
uint64_t before=m->pp[cpu];
int skip=reuse_skip(m,cpu,current,before,pperm);
if(skip)m->skipped++;
else if(!sprr_views_sync_cpu(&views,&s,cpu,pperm) || !publish_cpu(cpu))return 0;
else {m->inplace++;invalidate(cpu);}
m->pp[cpu]=pperm;
return !current || sprr_reuse_repin(&m->reuse,&views,cpu,pperm);
}
static uint64_t page_hash(uint64_t pa)
{
uint64_t h=UINT64_C(0xcbf29ce484222325);
for(unsigned i=0;i<4096;i++)h=(h^ram[pa-BASE+i])*UINT64_C(0x100000001b3);
return h;
}
/* A page that just became (or moved within) a variant column. */
static void record_variants(unsigned cpu)
{
for(unsigned n=0;n<SPRR_VIEW_TABLES;n++)if(cand.var.copy[n][cpu] && vpage[n][cpu]!=cand.var.copy[n][cpu]) {
vpage[n][cpu]=cand.var.copy[n][cpu];vhash[n][cpu]=page_hash(vpage[n][cpu]);
}
}
static int exact_predicate(unsigned cpu,uint64_t pperm)
{
#if VARIANTS_PRE_CHANGE
struct sprr_reuse_state r=cand.reuse;r.saves=cand.var.saves[cpu];
return sprr_variants_exact(&cand.var,&views,&r,cpu,pperm);
#else
return sprr_variants_exact(&cand.var,&views,&cand.reuse,cpu,pperm);
#endif
}
/* Mirrors kernel_change_pperm_impl with SNAP_KERNEL_PPERM_VARIANTS. */
static int pperm_candidate(unsigned cpu,uint64_t pperm)
{
struct machine *m=&cand;struct shadow s=kshadow(),vs=vshadow();
int current=views.count && sprr_reuse_current(&m->reuse,&views,&s,m->pp)==1;
sprr_reuse_invalidate(&m->reuse);
uint64_t before=m->pp[cpu];
int skip=reuse_skip(m,cpu,current,before,pperm),swapped=0,exact=0;
if(skip)m->skipped++;
else {
if(current && ((before^pperm)&SPRR_KERNEL_VIEW_PPERM)) {
exact=exact_predicate(cpu,pperm);
int reserved=exact || sprr_variants_reserve(&m->var,&views,&vs,cpu);
record_variants(cpu);
if(reserved)
swapped=sprr_variants_swap(&m->var,&views,&m->publish,cpu,before,m->reuse.saves);
if(swapped)record_variants(cpu);
else {exact=0;m->fallbacks++;}
}
if(exact) {
/* Every exact swap here; the monitor samples 1 in 64. */
m->exact++;m->verified++;
if(!sprr_views_verify_cpu(&views,&s,cpu,pperm)) {m->mismatches++;return 0;}
} else if(!sprr_views_sync_cpu(&views,&s,cpu,pperm))return 0;
else if(swapped)m->synced++;
else m->inplace++;
if(!publish_cpu(cpu))return 0;
if(swapped) {m->ttbr1[cpu]=sprr_views_root(&views,cpu);m->tlbi_due[cpu]=1;}
invalidate(cpu);
}
m->pp[cpu]=pperm;
if(!current)return 1;
return swapped ? sprr_variants_repin(&m->reuse,&views,cpu,pperm) :
sprr_reuse_repin(&m->reuse,&views,cpu,pperm);
}
static void enter(struct machine *m)
{
active=m;memcpy(ram,m->ram,sizeof ram);views=m->views;next=m->next;alloc_budget=m->budget;
}
static void leave(void)
{
struct machine *m=active;
memcpy(m->ram,ram,sizeof ram);m->views=views;m->next=next;m->budget=alloc_budget;active=0;
}
static int word(const struct machine *m,uint64_t pa,uint64_t *out)
{
if(pa<BASE || pa>BASE+BYTES-8 || (pa&7))return 0;
memcpy(out,m->ram+(pa-BASE),8);return 1;
}
/* Descriptor-by-descriptor equality of two installed trees; table addresses
* may differ, everything else may not. */
static int same_tree(uint64_t a,uint64_t b,unsigned level)
{
for(unsigned i=0;i<512;i++) {
uint64_t x,y;
if(!word(&ref,a+8UL*i,&x) || !word(&cand,b+8UL*i,&y))return 0;
if(level<3 && (x&3)==3) {
if((y&3)!=3 || (x&~PT_OA_MASK)!=(y&~PT_OA_MASK) ||
!same_tree(x&PT_OA_MASK,y&PT_OA_MASK,level+1))return 0;
} else if(x!=y)return 0;
}
return 1;
}
/* Every page a walk from root can reach holds no unpublished store. */
static int published(const struct machine *m,uint64_t table,unsigned level)
{
if(table<BASE || table>=BASE+BYTES || m->dirty[(table-BASE)/4096])return 0;
for(unsigned i=0;i<512 && level<3;i++) {
uint64_t d;if(!word(m,table+8UL*i,&d))return 0;
if((d&3)==3 && !published(m,d&PT_OA_MASK,level+1))return 0;
}
return 1;
}
static void invariants(void)
{
for(unsigned cpu=0;cpu<6;cpu++) {
CHECK(ref.ttbr1[cpu]==sprr_views_root(&ref.views,cpu));
CHECK(cand.ttbr1[cpu]==sprr_views_root(&cand.views,cpu));
CHECK(same_tree(ref.ttbr1[cpu],cand.ttbr1[cpu],0));
CHECK(published(&ref,ref.ttbr1[cpu],0) && published(&cand,cand.ttbr1[cpu],0));
CHECK(!ref.tlbi_due[cpu] && !cand.tlbi_due[cpu]);
CHECK(ref.pp[cpu]==cand.pp[cpu]);
}
enter(&cand);
struct shadow s=kshadow();
if(cand.reuse.valid) {
CHECK(sprr_reuse_current(&cand.reuse,&views,&s,cand.pp)==1);
for(unsigned cpu=0;cpu<6;cpu++)
CHECK(sprr_views_verify_cpu(&views,&s,cpu,cand.reuse.pperm[cpu]));
}
for(unsigned cpu=0;cpu<6;cpu++)for(unsigned n=0;n<SPRR_VIEW_TABLES;n++)
if(cand.var.copy[n][cpu]) {
CHECK(vpage[n][cpu]==cand.var.copy[n][cpu]);
CHECK(page_hash(cand.var.copy[n][cpu])==vhash[n][cpu]);
}
leave();
}
static void mutate(uint64_t table,unsigned index,uint64_t desc)
{
enter(&ref);put(table,index,desc);leave();
enter(&cand);put(table,index,desc);leave();
}
static void both_refresh(void)
{
enter(&ref);CHECK(refresh());leave();
enter(&cand);CHECK(refresh());leave();
invariants();
}
static void both_pperm(unsigned cpu,uint64_t pperm)
{
enter(&ref);CHECK(pperm_reference(cpu,pperm));leave();
enter(&cand);CHECK(pperm_candidate(cpu,pperm));leave();
invariants();
}
static void both_prepare(uint64_t va)
{
struct shadow s;
enter(&ref);s=kshadow();CHECK(sprr_views_prepare(&views,&s,va));leave();
enter(&cand);s=kshadow();CHECK(sprr_views_prepare(&views,&s,va));leave();
}
static const uint64_t deltas[4]={0,0x100,SPRR_UPERM_TPRO_WRITE,0x100|SPRR_UPERM_TPRO_WRITE};
static void canonical_change(void)
{
unsigned which=rnd(4);
uint64_t table=which==0 ? BASE+0x3000 : which==1 ? BASE+0x4000 :
which==2 ? BASE+0x2000 : BASE+0x3000;
unsigned index=which==2 ? 5+rnd(3) : 4+rnd(8);
uint64_t desc=rnd(3) ? DATA+0x10000+4096UL*rnd(64)+3+ATTR+PT_UXN+PT_PXN : 0;
if(which==2)desc=rnd(2) ? DATA+0x200000UL*(1+rnd(8))+1+ATTR+PT_UXN+PT_PXN : 0;
if(which==3)desc=rnd(2) ? DATA+0x20000+4096UL*rnd(16)+3+ATTR+PT_UXN : 0;
mutate(table,index,desc);
}
static void unit_checks(unsigned cpu)
{
enter(&cand);
struct shadow s=kshadow(),vs=vshadow();
struct sprr_view_variants a=cand.var;struct sprr_publish_state p=cand.publish;
/* exact: each identity element alone makes it false. */
uint64_t pperm=cand.pp[cpu]^0x100;
if(sprr_variants_exact(&a,&views,&cand.reuse,cpu,pperm)) {
struct sprr_view_variants b=a;b.saves[cpu]++;
CHECK(!sprr_variants_exact(&b,&views,&cand.reuse,cpu,pperm));
b=a;b.count[cpu]--;CHECK(!sprr_variants_exact(&b,&views,&cand.reuse,cpu,pperm));
b=a;b.source[1][cpu]+=4096;CHECK(!sprr_variants_exact(&b,&views,&cand.reuse,cpu,pperm));
b=a;b.copy[2][cpu]=0;CHECK(!sprr_variants_exact(&b,&views,&cand.reuse,cpu,pperm));
CHECK(!sprr_variants_exact(&a,&views,&cand.reuse,cpu,pperm^0x100));
CHECK(!sprr_variants_exact(&a,&views,&cand.reuse,cpu,1));
CHECK(!sprr_variants_exact(&a,&views,&cand.reuse,6,pperm));
CHECK(sprr_variants_exact(&a,&views,&cand.reuse,cpu,pperm^SPRR_UPERM_TPRO_WRITE));
} else CHECK(!"no exact variant to probe");
/* swap refuses: pending row, stale publication state, missing page, bad cpu. */
struct sprr_views v=views;
p.pending[cpu][1]|=UINT64_C(1)<<3;
CHECK(!sprr_variants_swap(&a,&v,&p,cpu,cand.pp[cpu],cand.reuse.saves));
p=cand.publish;p.count--;
CHECK(!sprr_variants_swap(&a,&v,&p,cpu,cand.pp[cpu],cand.reuse.saves));
p=cand.publish;a.copy[0][cpu]=0;
CHECK(!sprr_variants_swap(&a,&v,&p,cpu,cand.pp[cpu],cand.reuse.saves));
a=cand.var;
CHECK(!sprr_variants_swap(&a,&v,&p,6,cand.pp[cpu],cand.reuse.saves));
CHECK(!sprr_variants_swap(&a,&v,&p,cpu,1,cand.reuse.saves));
CHECK(!sprr_variants_swap(&a,&v,&p,cpu,cand.pp[cpu],0));
CHECK(!memcmp(&v,&views,sizeof v));
/* A swap and its inverse restore both columns exactly. */
CHECK(sprr_variants_swap(&a,&v,&p,cpu,cand.pp[cpu],cand.reuse.saves));
CHECK(sprr_views_root(&v,cpu)!=sprr_views_root(&views,cpu));
CHECK(sprr_variants_swap(&a,&v,&p,cpu,cand.pp[cpu],cand.reuse.saves));
CHECK(!memcmp(v.tables,views.tables,sizeof v.tables));
for(unsigned n=0;n<views.count;n++)CHECK(a.copy[n][cpu]==cand.var.copy[n][cpu]);
/* reserve refuses a page that aliases a source, a copy or a variant, and
* records nothing for it. */
a=cand.var;a.copy[views.count-1][cpu]=0;
const uint64_t aliases[3]={views.tables[1].source,views.tables[0].copy[(cpu+1)%6],
cand.var.copy[0][(cpu+2)%6]};
for(unsigned i=0;i<3;i++) {
forced_alloc=aliases[i];uint64_t errors=a.errors;
CHECK(!sprr_variants_reserve(&a,&views,&vs,cpu) && a.errors==errors+1 &&
!a.copy[views.count-1][cpu]);
}
forced_alloc=0;unsigned budget=alloc_budget;alloc_budget=0;
CHECK(!sprr_variants_reserve(&a,&views,&vs,cpu) && !a.copy[views.count-1][cpu]);
alloc_budget=budget;
CHECK(sprr_variants_reserve(&a,&views,&vs,cpu) && a.copy[views.count-1][cpu]);
CHECK(!sprr_variants_reserve(&a,&views,&vs,6));
/* repin refuses mismatched identity and malformed values. */
struct sprr_reuse_state r=cand.reuse;r.valid=0;
CHECK(!sprr_variants_repin(&r,&views,6,SPRR_PPERM_INITIAL) && !r.valid);
CHECK(!sprr_variants_repin(&r,&views,0,1) && !r.valid);
r.count--;CHECK(!sprr_variants_repin(&r,&views,0,SPRR_PPERM_INITIAL) && !r.valid);r.count++;
r.tables[1].source+=4096;CHECK(!sprr_variants_repin(&r,&views,0,SPRR_PPERM_INITIAL) && !r.valid);
r=cand.reuse;r.valid=0;
CHECK(sprr_variants_repin(&r,&views,2,cand.pp[2]) && r.valid);
for(unsigned n=0;n<views.count;n++)CHECK(r.tables[n].copy[2]==views.tables[n].copy[2]);
(void)s;leave();
}
int main(void)
{
CHECK(!baseline_view_tests());
struct shadow s;setup(&s);s=kshadow();
CHECK(sprr_views_init(&views,&s));CHECK(sprr_views_prepare(&views,&s,0));
for(unsigned i=0;i<4;i++)put(BASE+0x3000,i,DATA+4096UL*i+3+ATTR+PT_UXN);
for(struct machine *m=&ref;m;m=m==&ref ? &cand : 0) {
memcpy(m->ram,ram,sizeof ram);m->views=views;m->next=next;m->budget=alloc_budget;
for(unsigned cpu=0;cpu<6;cpu++)m->pp[cpu]=SPRR_PPERM_INITIAL;
}
for(unsigned cpu=0;cpu<6;cpu++)ref.ttbr1[cpu]=cand.ttbr1[cpu]=sprr_views_root(&views,cpu);
both_refresh();
/* Allocation failure on a CPU's first flip takes the in-place path. */
enter(&cand);unsigned budget=alloc_budget;alloc_budget=0;leave();cand.budget=0;
both_pperm(0,cand.pp[0]^0x100);
cand.budget=budget;
CHECK(cand.fallbacks==1 && !cand.synced && !cand.exact);
unsigned prepared=0;
/* Flips dominate, as on hardware (v6: 135,838 bit-8 flips, 208 canonical
* re-saves), but canonical changes stay frequent enough to stale every
* CPU's variant many times. */
for(unsigned round=0;round<6000;round++) {
unsigned op=rnd(40),cpu=rnd(6);
if(op<30)both_pperm(cpu,ref.pp[cpu]^deltas[rnd(4)]);
else if(op<32) {canonical_change();both_refresh();}
else if(op<33) {
/* A-B-A: a canonical change with PPERM transactions before its
* refresh, then restored. No stale column may come back. */
uint64_t old;CHECK(word(&ref,BASE+0x3000+8*5,&old));
mutate(BASE+0x3000,5,DATA+0x70000+3+ATTR+PT_UXN+PT_PXN);
enter(&ref);CHECK(pperm_reference(cpu,ref.pp[cpu]^0x100));leave();
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(prepared==1) {
mutate(BASE+0x2000,2,BASE+0x5000+3);
mutate(BASE+0x5000,1,DATA+3+ATTR+PT_UXN+PT_PXN);
}
if(prepared==2) {
mutate(BASE+0x2000,3,BASE+0x6000+3);
mutate(BASE+0x6000,0,DATA+0x3000+3+ATTR+PT_UXN+PT_PXN);
}
both_prepare(vas[prepared++]);both_refresh();
} else if(op<35) {
/* Pending publication for this CPU: no swap, in-place sync. */
uint64_t exact=cand.exact,synced=cand.synced,fallbacks=cand.fallbacks;
cand.publish.pending[cpu][0]|=1;
both_pperm(cpu,ref.pp[cpu]^0x100);
CHECK(!cand.publish.pending[cpu][0]);
CHECK(cand.exact==exact && cand.synced==synced && cand.fallbacks<=fallbacks+1);
} else both_refresh();
if(failed>20)break;
}
CHECK(prepared==3);
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. */
CHECK(cand.synced<=6*cand.reuse.saves+cand.inplace+6);
CHECK(cand.verified==cand.exact && cand.exact+cand.synced+cand.inplace==ref.inplace);
CHECK(cand.skipped==ref.skipped);
/* Two flips with no canonical change between: cpu's variant is exact. */
unsigned cpu=rnd(6);
both_pperm(cpu,ref.pp[cpu]^0x100);both_pperm(cpu,ref.pp[cpu]^0x100);
unit_checks(cpu);
printf("model: reference in-place %llu skipped %llu; candidate exact swaps %llu, synced swaps %llu, "
"in-place %llu (fallbacks %llu), skipped %llu, variant pages %llu\n",
(unsigned long long)ref.inplace,(unsigned long long)ref.skipped,
(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);
printf("%s: %u kernel PPERM variant checks, %u failures\n",failed ? "FAIL" : "PASS",checks,failed);
return failed ? 1 : 0;
}