Files
Acelogic 49ccf6bf9b tests: cover kernel view reclaim and the 256-table pool
tools/test-view-reclaim.c drives a monitor-shaped kernel tree (one root,
one level 1, four level 2, 512 level 3) the way the retypes do: prepare,
detach, commit, refresh, with kernel_views_quiesce's steps as the
quiesce. After every step, independently of the library: each CPU's
walk of all 16,384 leaves gives the canonical descriptor or, for
selector2/4, its translation under that CPU's PPERM; no reachable page
holds an unpublished store; copies are distinct and never sources; no
store reaches a retired page before the TLBI after its unlinking.
Scenarios: fill to 256, a full pool and a one-slot-short path refuse
with no change, release and reclaim (TXM import, the level-2 ancestor),
reuse of the retired pages before any allocation, reclaim down to the
root, the restart of a two-frame change, read failure, nothing to
retire, unconfigured views and a failed quiesce. A quiesce without its
TLBI and a dropped protected table are caught (harness self-checks).

tools/test-view-reclaim.py runs it, nine weakened sprr-view.c copies
(each must fail: all do), test-kernel-pperm-variants.c with and without
the define, the wiring (default off, guarded uses, cycle, report place
and decoder) and, with --build, the monitor identities.
test-kernel-pperm-variants.c gains reclaim steps under the define:
positions move under the reuse snapshot, publication rows and variant
columns while both machines keep agreeing.

Tests that hard-coded the 128-table shape now derive it from
SPRR_VIEW_TABLES / USER_PUBLISH_MAX (test-tpro-view.c's RAM,
test-user-prepare-fused.c, test-user-fused-compare.c,
test-user-publish.c, test-user-tree-memo.c); define-off they check
exactly what they did.

Results on this tree (host, ASan/UBSan):
  python3 tools/test-view-reclaim.py              PASS (model 20,578
      checks; 9/9 mutants caught; consumers; wiring)
  python3 tools/test-view-reclaim.py --build --qualification
      .../installed-fresh-desktop-20261024-next1/qualification.json
                                                  PASS (absent == =0 ==
      86bb02ea9e03...; =1 459,904 bytes, 15,232 headroom)
  the 26 C suites linking sprr-view.c, with and without
      -DSNAP_VIEW_RECLAIM=1                      PASS (define-off check
      counts equal the base commit's)
Hardware: not run.
2026-10-24 18:53:55 -04:00

299 lines
15 KiB
C

/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <string.h>
#include "sprr-view.h"
#define BASE UINT64_C(0x40000000)
#define DATA UINT64_C(0x94050c000)
/* 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;
static uint64_t reads,batch_reads,serial_reads,epoch;
static unsigned writes,budget,checks,failures;
static struct sprr_views views;
#define CHECK(x) do {checks++;if(!(x)){failures++;printf("FAIL %u: %s\n",__LINE__,#x);}} while(0)
static int rd(void *ctx,uint64_t pa,uint64_t *out)
{
reads++;
(void)ctx;if(pa<BASE || pa>BASE+BYTES-8 || pa==fail_read)return -1;
memcpy(out,ram+(pa-BASE),8);return 0;
}
static int wr(void *ctx,uint64_t pa,uint64_t value)
{
(void)ctx;if(pa<BASE || pa>BASE+BYTES-8)return -1;
uint64_t old;memcpy(&old,ram+(pa-BASE),8);
if(old!=value)sprr_view_epoch_advance(&epoch);
memcpy(ram+(pa-BASE),&value,8);writes++;return 0;
}
static uint64_t alloc(void *ctx)
{
(void)ctx;if(!budget || next>BASE+BYTES-4096)return 0;
sprr_view_epoch_advance(&epoch);
budget--;uint64_t p=next;next+=4096;return p;
}
static void put(uint64_t table,unsigned slot,uint64_t d)
{CHECK(!wr(0,table+8UL*slot,d));}
static uint64_t leaf(uint64_t pa,unsigned selector)
{return pa|3|ATTR|((uint64_t)(selector&12)<<4)|((uint64_t)(selector&3)<<53);}
static struct shadow setup(void)
{
memset(ram,0,sizeof ram);memset(&views,0,sizeof views);
next=BASE+0x10000;budget=BYTES/4096;fail_read=0;
put(BASE,0,BASE+0x1000+3);put(BASE+0x1000,0,BASE+0x2000+3);
for(unsigned i=0;i<3;i++)put(BASE+0x2000,i,BASE+0x3000+4096UL*i+3);
for(unsigned i=0;i<4;i++)put(BASE+0x3000,i,leaf(DATA+4096UL*i,9));
put(BASE+0x4000,0,leaf(DATA,7)); /* same PA, ordinary writable alias */
put(BASE+0x5000,0,leaf(DATA+0x8000,13)); /* executable ordinary page */
writes=0;
return (struct shadow){.txsz=17,.root=BASE,.alloc=alloc,.read=rd,.write=wr};
}
static struct mmu_leaf walk(unsigned cpu,uint64_t va)
{
struct mmu_config config={12,17,sprr_views_root(&views,cpu)};
struct mmu_leaf l={0};CHECK(!mmu_walk(&config,va,rd,0,&l));return l;
}
static void rights(unsigned cpu,uint64_t va,uint64_t pa,int write,int execute)
{
struct mmu_leaf l=walk(cpu,va);CHECK(l.valid && l.pa==pa);
CHECK(!!(l.descriptor&(1UL<<6))); /* preserves PAN-visible user access */
CHECK(!!(l.descriptor&(2UL<<6))==!write);
CHECK(!!(l.descriptor&PT_UXN)==!execute);
CHECK(l.descriptor&PT_PXN);
CHECK((l.descriptor&ATTR)==ATTR);
}
static void refused(struct shadow *s,const struct sprr_state *state,int el0)
{
memcpy(before,ram,sizeof ram);unsigned n=writes;
CHECK(!sprr_views_sync_user_cpu(&views,s,0,state,el0));
CHECK(writes==n && !memcmp(before,ram,sizeof ram));
struct sprr_state states[6];int modes[6];
for(unsigned cpu=0;cpu<6;cpu++){states[cpu]=*state;modes[cpu]=el0;}
CHECK(!sprr_views_sync_user_all(&views,s,states,modes));
CHECK(writes==n && !memcmp(before,ram,sizeof ram));
}
static void differential_batch(struct shadow *s,const struct sprr_state states[6],
const int modes[6])
{
struct sprr_views saved=views;
memcpy(before,ram,sizeof ram);uint64_t start=reads;unsigned start_writes=writes;
for(unsigned cpu=0;cpu<6;cpu++)
CHECK(sprr_views_sync_user_cpu(&views,s,cpu,&states[cpu],modes[cpu]));
uint64_t serial=reads-start;unsigned serial_writes=writes-start_writes;
struct sprr_views serial_views=views;memcpy(expected,ram,sizeof ram);
views=saved;memcpy(ram,before,sizeof ram);start=reads;start_writes=writes;
CHECK(sprr_views_sync_user_all(&views,s,states,modes));
uint64_t batch=reads-start;
CHECK(!memcmp(ram,expected,sizeof ram));
CHECK(!memcmp(&views,&serial_views,sizeof views));
CHECK(writes-start_writes==serial_writes);
CHECK(batch<serial);batch_reads+=batch;serial_reads+=serial;
}
static void proof_cases(void)
{
struct shadow s=setup();struct sprr_state state[6]={0};
struct sprr_view_proof proof={0};struct sprr_view_proof_stats stats={0};
for(unsigned cpu=0;cpu<6;cpu++)sprr_root_reset(&state[cpu],0xa);
CHECK(sprr_views_init(&views,&s));CHECK(sprr_views_prepare_user(&views,&s));
uint64_t full_reads=0,cached_reads=0;
for(unsigned step=0;step<256;step++) {
unsigned cpu=step%6;int mode=(int)(step&1);
state[cpu].uperm=SPRR_UPERM_INITIAL|((uint64_t)((step>>1)&1)<<36);
state[cpu].pperm=SPRR_PPERM_INITIAL|((uint64_t)((step>>2)&1)<<36);
if(!(step%17))put(BASE+0x5000,0,leaf(DATA+((uint64_t)(step&1)<<14),13));
struct sprr_views saved=views;
memcpy(before,ram,sizeof ram);uint64_t prior_epoch=epoch,start=reads;
CHECK(sprr_views_sync_user_cpu(&views,&s,cpu,&state[cpu],mode));
full_reads+=reads-start;memcpy(expected,ram,sizeof ram);
struct sprr_views result=views;
views=saved;epoch=prior_epoch;memcpy(ram,before,sizeof ram);start=reads;
CHECK(sprr_views_sync_user_cached(&views,&s,cpu,&state[cpu],mode,&epoch,&proof,&stats,1));
cached_reads+=reads-start;
CHECK(!memcmp(ram,expected,sizeof ram));CHECK(!memcmp(&views,&result,sizeof views));
CHECK(proof.valid==1 && proof.epoch==epoch && proof.root==s.root && proof.count==views.count);
}
CHECK(stats.calls==256 && stats.hits>200 && stats.verified>=4 && !stats.mismatches);
CHECK(cached_reads<full_reads);
printf("Versioned proof descriptor reads: %llu versus %llu full\n",
(unsigned long long)cached_reads,(unsigned long long)full_reads);
/* Epoch/key drift takes the full path, including uncopied source branches. */
uint64_t old;CHECK(!rd(0,BASE+0x5000,&old));
put(BASE+0x5000,0,leaf(DATA,2));unsigned before_writes=writes;uint64_t hits=stats.hits;
CHECK(!sprr_views_sync_user_cached(&views,&s,0,&state[0],0,&epoch,&proof,&stats,0));
CHECK(writes==before_writes && stats.hits==hits && !proof.valid);
put(BASE+0x5000,0,old);
CHECK(sprr_views_sync_user_cached(&views,&s,0,&state[0],0,&epoch,&proof,&stats,1));
hits=stats.hits;proof.root^=4096;
CHECK(sprr_views_sync_user_cached(&views,&s,0,&state[0],0,&epoch,&proof,&stats,1));
CHECK(stats.hits==hits);proof.count++;
CHECK(sprr_views_sync_user_cached(&views,&s,0,&state[0],0,&epoch,&proof,&stats,1));
CHECK(stats.hits==hits);
/* Diagnostic sampling catches an intentionally untracked bad mutation. */
uint64_t bad=leaf(DATA,2);memcpy(ram+0x5000,&bad,8);
stats.hits=(stats.hits+63)&~UINT64_C(63);before_writes=writes;
CHECK(!sprr_views_sync_user_cached(&views,&s,0,&state[0],0,&epoch,&proof,&stats,1));
CHECK(stats.mismatches==1 && !proof.valid && writes==before_writes);
put(BASE+0x5000,0,old);
CHECK(sprr_views_sync_user_cached(&views,&s,0,&state[0],0,&epoch,&proof,&stats,1));
/* New private-path allocation changes the epoch before reuse. */
CHECK(sprr_views_prepare_user_leaf(&views,&s,0x401000));hits=stats.hits;
CHECK(sprr_views_sync_user_cached(&views,&s,0,&state[0],0,&epoch,&proof,&stats,1));
CHECK(stats.hits==hits);
/* Saturation never wraps back into a retained proof. */
epoch=UINT64_MAX-1;sprr_view_epoch_advance(&epoch);sprr_view_epoch_advance(&epoch);
CHECK(epoch==UINT64_MAX);hits=stats.hits;
for(unsigned i=0;i<3;i++) {
CHECK(sprr_views_sync_user_cached(&views,&s,0,&state[0],0,&epoch,&proof,&stats,1));
CHECK(stats.hits==hits && !proof.valid);
}
CHECK(!sprr_views_sync_user_cached(&views,&s,6,&state[0],0,&epoch,&proof,&stats,1));
CHECK(!sprr_views_sync_user_cached(&views,&s,0,&state[0],0,0,&proof,&stats,1));
CHECK(!sprr_views_sync_user_cached(&views,&s,0,&state[0],0,&epoch,0,&stats,1));
CHECK(!sprr_views_sync_user_cached(&views,&s,0,&state[0],0,&epoch,&proof,0,1));
}
int main(void)
{
struct shadow s=setup();struct sprr_state state[6]={0};
for(unsigned cpu=0;cpu<6;cpu++)sprr_root_reset(&state[cpu],0xa);
CHECK(sprr_views_init(&views,&s));
refused(&s,&state[0],0); /* raw selector9 path has no private copies */
CHECK(sprr_views_prepare_user(&views,&s));CHECK(views.count==4);
for(unsigned cpu=0;cpu<6;cpu++) {
CHECK(sprr_views_sync_user_cpu(&views,&s,cpu,&state[cpu],0));
for(unsigned i=0;i<4;i++)rights(cpu,4096UL*i,DATA+4096UL*i,0,0);
rights(cpu,0x200000,DATA,1,0);rights(cpu,0x400000,DATA+0x8000,0,1);
}
/* Same entire memory image and sync counters as six independent calls,
* including distinct CPU permissions and mixed user/kernel regimes. */
int modes[6];
for(unsigned pattern=0;pattern<64;pattern++) {
for(unsigned cpu=0;cpu<6;cpu++) {
sprr_root_reset(&state[cpu],0xa);
state[cpu].uperm|=(uint64_t)((pattern>>cpu)&1)<<36;
state[cpu].pperm|=(uint64_t)((pattern>>(5-cpu))&1)<<36;
state[cpu].pperm|=(uint64_t)(cpu&1)<<8;
modes[cpu]=(int)((pattern+cpu)&1);
}
differential_batch(&s,state,modes);
}
/* A late CPU's invalid state/copy must fail before any CPU is written. */
memcpy(before,ram,sizeof ram);unsigned batch_before=writes;
state[5].uperm^=1;
CHECK(!sprr_views_sync_user_all(&views,&s,state,modes));state[5].uperm^=1;
CHECK(writes==batch_before && !memcmp(before,ram,sizeof ram));
uint64_t copy=views.tables[views.count-1].copy[5];
views.tables[views.count-1].copy[5]=0;
CHECK(!sprr_views_sync_user_all(&views,&s,state,modes));
CHECK(writes==batch_before && !memcmp(before,ram,sizeof ram));
views.tables[views.count-1].copy[5]=copy;
CHECK(!sprr_views_sync_user_all(&views,&s,0,modes));
CHECK(!sprr_views_sync_user_all(&views,&s,state,0));
/* Every UPERM/PPERM pair, independently on all six CPUs, in both ELs.
* The unrelated selector2 grant must not change user permissions. */
for(unsigned bits=0;bits<8;bits++)for(unsigned el0=0;el0<2;el0++) {
for(unsigned cpu=0;cpu<6;cpu++) {
sprr_root_reset(&state[cpu],0xa);
state[cpu].uperm|=(uint64_t)(bits&1)<<36;
state[cpu].pperm|=(uint64_t)((bits>>1)&1)<<36;
state[cpu].pperm|=(uint64_t)((bits>>2)&1)<<8;
CHECK(sprr_views_sync_user_cpu(&views,&s,cpu,&state[cpu],(int)el0));
for(unsigned i=0;i<4;i++)rights(cpu,4096UL*i,DATA+4096UL*i,
el0 ? bits&1 : (bits>>1)&1,0);
rights(cpu,0x200000,DATA,1,0);
}
}
for(unsigned cpu=0;cpu<6;cpu++) {
sprr_root_reset(&state[cpu],0xa);
CHECK(sprr_views_sync_user_cpu(&views,&s,cpu,&state[cpu],0));
}
state[0].uperm|=1UL<<36;
state[1].pperm|=1UL<<36;
CHECK(sprr_views_sync_user_cpu(&views,&s,0,&state[0],1));
CHECK(sprr_views_sync_user_cpu(&views,&s,1,&state[1],0));
for(unsigned cpu=0;cpu<6;cpu++)rights(cpu,0,DATA,cpu<2,0);
CHECK(sprr_views_sync_user_cpu(&views,&s,0,&state[0],0));
CHECK(sprr_views_sync_user_cpu(&views,&s,1,&state[1],1));
for(unsigned cpu=0;cpu<6;cpu++)rights(cpu,0,DATA,0,0);
/* Preparation must also cover a new shared source branch and aliases. */
put(BASE+0x4000,1,leaf(DATA,9));
refused(&s,&state[0],1);
CHECK(sprr_views_prepare_user(&views,&s));CHECK(views.count==5);
CHECK(sprr_views_sync_user_cpu(&views,&s,0,&state[0],1));
rights(0,0x201000,DATA,1,0);rights(0,0x200000,DATA,1,0);
put(BASE+0x2000,3,BASE+0x4000+3); /* repeated source table at another VA */
CHECK(sprr_views_prepare_user(&views,&s));CHECK(views.count==5);
CHECK(sprr_views_sync_user_cpu(&views,&s,0,&state[0],0));
rights(0,0x601000,DATA,0,0);rights(0,0x600000,DATA,1,0);
/* Unmap and replacement refresh all aliases without touching the source. */
put(BASE+0x4000,1,0);
CHECK(sprr_views_prepare_user(&views,&s));
CHECK(sprr_views_sync_user_cpu(&views,&s,0,&state[0],1));
CHECK(!walk(0,0x201000).valid && !walk(0,0x601000).valid);
put(BASE+0x4000,1,leaf(DATA+0xc000,9));
memcpy(before,ram,0x10000);
CHECK(sprr_views_prepare_user(&views,&s));
CHECK(sprr_views_sync_user_cpu(&views,&s,0,&state[0],0));
CHECK(!memcmp(before,ram,0x10000));
rights(0,0x201000,DATA+0xc000,0,0);rights(0,0x601000,DATA+0xc000,0,0);
differential_batch(&s,state,modes); /* repeated table alias and replacement */
/* Invalid control state cannot publish even a partial private view. */
for(unsigned field=0;field<4;field++)for(unsigned bit=0;bit<64;bit++) {
struct sprr_state bad=state[0];uint64_t *p=field==0 ? &bad.uperm :
field==1 ? &bad.pperm : field==2 ? &bad.umprr : &bad.pmprr;
*p^=1UL<<bit;
if(sprr_fixed_state_valid(&bad) && (bad.umprr&0xc0000)==0x40000)continue;
refused(&s,&bad,1);
}
refused(&s,&state[0],2);refused(&s,&state[0],-1);
struct sprr_state bad=state[0];bad.resets=0;refused(&s,&bad,0);
for(unsigned kind=0;kind<7;kind++) {
uint64_t where=BASE+0x2000+32,old=0;
uint64_t d=kind==0 ? leaf(DATA,2) : kind==1 ? DATA+1+ATTR :
kind==2 ? BASE+0x1000+3 : kind==3 ? sprr_views_root(&views,0)+3 :
kind==4 ? 2 : kind==5 ? BASE+0x4000+7 : BASE+0x1000+3;
if(kind==0)where=BASE+0x5000;
if(kind==6)where=BASE+8*256;
CHECK(!rd(0,where,&old));CHECK(!wr(0,where,d));
refused(&s,&state[0],0);
unsigned n=writes;uint64_t count=views.count;
CHECK(!sprr_views_prepare_user(&views,&s));
CHECK(writes==n && views.count==count);
CHECK(!wr(0,where,old));
}
fail_read=BASE+0x5000+4096-8;refused(&s,&state[0],0);fail_read=0;
/* Partial allocation never publishes copies, and can be retried. */
s=setup();CHECK(sprr_views_init(&views,&s));budget=7;
unsigned n=writes;CHECK(!sprr_views_prepare_user(&views,&s));CHECK(writes==n);
refused(&s,&state[0],0);budget=BYTES/4096;
CHECK(sprr_views_prepare_user(&views,&s));
CHECK(sprr_views_sync_user_cpu(&views,&s,0,&state[0],1));rights(0,0,DATA,1,0);
/* The old kernel API must continue refusing raw selector9. */
memcpy(before,ram,sizeof ram);n=writes;
CHECK(!sprr_views_sync_cpu(&views,&s,0,SPRR_PPERM_INITIAL));
CHECK(writes==n && !memcmp(before,ram,sizeof ram));
/* A running CPU must never see a freshly published raw selector9 through
* a shared canonical table. Detach the still-empty path on every CPU. */
s=setup();
for(unsigned i=0;i<4;i++)put(BASE+0x3000,i,0);
CHECK(sprr_views_init(&views,&s));
for(unsigned cpu=0;cpu<6;cpu++)
CHECK(sprr_views_sync_user_cpu(&views,&s,cpu,&state[cpu],1));
CHECK(views.count==1);
CHECK(sprr_views_prepare_user_leaf(&views,&s,0x201000));
for(unsigned cpu=0;cpu<6;cpu++)
CHECK(sprr_views_sync_user_cpu(&views,&s,cpu,&state[cpu],1));
put(BASE+0x4000,1,leaf(DATA,9)); /* raw canonical publication */
for(unsigned cpu=0;cpu<6;cpu++)CHECK(!walk(cpu,0x201000).valid);
CHECK(sprr_views_sync_user_cpu(&views,&s,0,&state[0],1));
rights(0,0x201000,DATA,1,0);
for(unsigned cpu=1;cpu<6;cpu++)CHECK(!walk(cpu,0x201000).valid);
CHECK(!sprr_views_prepare_user_leaf(&views,&s,1UL<<47));
CHECK(!sprr_views_prepare_user_leaf(&views,&s,0x201001));
proof_cases();
printf("TPRO private views: %u checks, %u failures\n",checks,failures);
printf("Batch differential descriptor reads: %llu versus %llu serial\n",
(unsigned long long)batch_reads,(unsigned long long)serial_reads);
return !!failures;
}