~james/split

d902931be6c5fc32d850933c43265a84f976be3a — james 14 hours ago main
chore(import): snapshot james.dad.town/split at f044a95a6ba7
A  => .gitignore +3 -0
@@ 1,3 @@
/build/

/staging.txt

A  => Makefile +62 -0
@@ 1,62 @@
SDK ?= $(HOME)/.cache/split/microkit-sdk-2.3.1
TOOL ?= $(HOME)/.cache/split/microkit-2.3.1/target/release/microkit
BOARD := $(SDK)/board/qemu_virt_aarch64/debug
CC := aarch64-linux-gnu-gcc
LD := aarch64-linux-gnu-ld
CFLAGS := -std=gnu11 -O2 -g -Wall -Wextra -Werror -Wno-unused-function -ffreestanding -nostdlib -mstrict-align -I$(BOARD)/include
QEMU := qemu-system-aarch64 -machine virt,virtualization=on -cpu cortex-a53 -m 2G -nographic -monitor none -serial stdio

.PHONY: all proof model work demo check run clean
all: build/loader.img

build:
	mkdir -p build

build/pager.o: guest/pager.c guest/split.h | build
	$(CC) $(CFLAGS) -c $< -o $@

build/alpha.o: guest/client.c guest/split.h | build
	$(CC) $(CFLAGS) -DCLIENT=0 -c $< -o $@

build/beta.o: guest/client.c guest/split.h | build
	$(CC) $(CFLAGS) -DCLIENT=1 -c $< -o $@

build/unsafe.o: guest/pager.c guest/split.h | build
	$(CC) $(CFLAGS) -DSHARED_WRITE=1 -c $< -o $@

build/%.elf: build/%.o
	$(LD) -L$(BOARD)/lib $< -lmicrokit -Tmicrokit.ld -o $@

build/loader.img: build/pager.elf build/alpha.elf build/beta.elf guest/split.system
	MICROKIT_SDK=$(SDK) $(TOOL) --board qemu_virt_aarch64 --config debug --search-path build -o $@ -r build/report.txt guest/split.system

build/unsafe.img: build/unsafe.elf build/alpha.elf build/beta.elf guest/split.system
	sed 's/pager.elf/unsafe.elf/' guest/split.system > build/unsafe.system
	MICROKIT_SDK=$(SDK) $(TOOL) --board qemu_virt_aarch64 --config debug --search-path build -o $@ -r build/unsafe.txt build/unsafe.system

proof:
	isabelle build -D proof

build/split.ml: proof/Export.thy proof/Reuse.thy proof/Snapshot.thy proof/Execution.thy proof/Protocol.thy proof/Isolation.thy proof/Model.thy proof/ROOT | build
	isabelle build -D proof
	isabelle export -n -d proof -O build/generated -x 'Split.Export:code/**' Split
	cp build/generated/Split.Export/code/split.ocaml $@

build/check: build/split.ml host/check.ml Makefile
	ocamlopt -w -a -I build -c build/split.ml
	ocamlopt -w +a-4-42-70 -I +unix -I build -c -o build/check.cmx host/check.ml
	ocamlopt -I +unix -I build unix.cmxa build/split.cmx build/check.cmx -o $@

model: build/check
	build/check --model

work: all build/check
	build/check work example/document

demo run: work

check: all build/unsafe.img model
	build/check --test

clean:
	rm -rf build

A  => README.md +5 -0
@@ 1,5 @@
Split gives two seL4 workers private configuration snapshots through a userspace pager that copies shared pages on first write and scrubs discarded frames before reuse, with an Isabelle model exported to OCaml for checking guest memory operations.

<video controls src="https://codesperg.org/wmxiel/split/raw/branch/main/media/split.mp4">
  <a href="media/split.mp4">Watch the demonstration</a>
</video>

A  => example/document +4 -0
@@ 1,4 @@
host = db.internal
port = 5432
workers = 8
log = info

A  => guest/client.c +19 -0
@@ 1,19 @@
#include "split.h"

void init(void) {}

void notified(microkit_channel channel) {
  require(channel == 0, "unknown notification");
  volatile struct job *job = (void *)TASK;
  unsigned p = job->page, length = job->length;
  require(p < PAGES && length <= LIMIT, "edit bounds");
  volatile unsigned char *page = (void *)(BASE + p * BYTES);
  for (unsigned i = 0; i <= length; ++i) {
    page[i] = job->text[i];
    microkit_mr_set(0, p);
    microkit_mr_set(1, i);
    microkit_mr_set(2, page[i]);
    (void)microkit_ppcall(0, microkit_msginfo_new(1, 3));
  }
  (void)microkit_ppcall(0, microkit_msginfo_new(2, 0));
}

A  => guest/pager.c +249 -0
@@ 1,249 @@
#include "split.h"

#ifndef SHARED_WRITE
#define SHARED_WRITE 0
#endif

static unsigned mapping[SLOTS], next = PAGES;
static unsigned available[FRAMES - PAGES], returned;
static seL4_CPtr caps[SLOTS];
static int started, busy = -1;
static char line[384];
static unsigned used;
static volatile uint32_t *uart = (void *)0x70000000ul;

static unsigned char *frame(unsigned f) {
  return (unsigned char *)(BACKING + f * BYTES);
}

static void done(void) { microkit_dbg_puts("@done\n"); }

static seL4_Word physical(seL4_CPtr cap) {
  seL4_ARM_Page_GetAddress_t result = seL4_ARM_Page_GetAddress(cap);
  require(result.error == seL4_NoError, "frame address query");
  return result.paddr;
}

static int length(const char *text, unsigned *size) {
  unsigned n = 0;
  while (text[n]) if (++n > LIMIT) return 0;
  *size = n;
  return 1;
}

static void map(unsigned k, seL4_CPtr page, int write) {
  seL4_CapRights_t rights = seL4_CapRights_new(0, 0, 1, write);
  require(seL4_ARM_Page_Map(page, microkit_cspace_root_slot_to_cptr(1 + k / PAGES),
    BASE + (k % PAGES) * BYTES, rights,
    seL4_ARM_Default_VMAttributes | seL4_ARM_ExecuteNever) == seL4_NoError,
    "page map");
}

static void table(void) {
  microkit_dbg_puts("SPLIT TABLE");
  for (unsigned k = 0; k < SLOTS; ++k) {
    microkit_dbg_puts(" "); number(mapping[k]);
  }
  microkit_dbg_puts("\n");
}

void init(void) {
  for (unsigned f = 0; f < FRAMES; ++f)
    for (unsigned b = 0; b < BYTES; ++b) frame(f)[b] = 0;
  for (unsigned k = 0; k < SLOTS; ++k) {
    mapping[k] = k % PAGES;
    caps[k] = microkit_cspace_root_slot_to_cptr(10 + k);
    map(k, caps[k], SHARED_WRITE);
  }
  uart[0x30 / 4] |= 0x301;
  uart[0x2c / 4] = 0x60;
  uart[0x44 / 4] = 0x7ff;
  uart[0x38 / 4] = 0x10;
  microkit_irq_ack(5);
  table();
  microkit_dbg_puts("@ready\n");
}

seL4_Bool fault(microkit_child child, microkit_msginfo info, microkit_msginfo *reply) {
  require(child < CLIENTS && microkit_msginfo_get_label(info) == seL4_Fault_VMFault,
    "unexpected fault");
  seL4_Word address = microkit_mr_get(seL4_VMFault_Addr);
  seL4_Word fsr = microkit_mr_get(seL4_VMFault_FSR);
  seL4_Word ip = microkit_mr_get(seL4_VMFault_IP);
  require(address >= BASE && address < BASE + PAGES * BYTES && (fsr & (1ul << 6)),
    "not a managed write fault");
  unsigned k = child * PAGES + (address - BASE) / BYTES;
  unsigned old = mapping[k], fresh = returned ? available[--returned] : next++;
  require(old < PAGES && fresh < FRAMES, "frame pool exhausted");
  seL4_Word source = physical(caps[k]);
  seL4_CPtr destination = microkit_cspace_root_slot_to_cptr(30 + fresh - PAGES);
  seL4_Word target = physical(destination);
  for (unsigned b = 0; b < BYTES; ++b) frame(fresh)[b] = frame(old)[b];
  require(seL4_ARM_Page_Unmap(caps[k]) == seL4_NoError, "page unmap");
  caps[k] = microkit_cspace_root_slot_to_cptr(30 + fresh - PAGES);
  map(k, caps[k], 1);
  mapping[k] = fresh;
  microkit_dbg_puts("SPLIT COPY "); number(k);
  microkit_dbg_puts(" "); number(old);
  microkit_dbg_puts(" "); number(fresh); microkit_dbg_puts("\n");
  table();
  microkit_dbg_puts("SPLIT FAULT "); number(k);
  microkit_dbg_puts(" "); number(address);
  microkit_dbg_puts(" "); number(ip);
  microkit_dbg_puts(" "); number(fsr);
  microkit_dbg_puts(" "); number(source);
  microkit_dbg_puts(" "); number(target); microkit_dbg_puts("\n");
  *reply = microkit_msginfo_new(0, 0);
  return seL4_True;
}

microkit_msginfo protected(microkit_channel channel, microkit_msginfo info) {
  require(channel < CLIENTS && (int)channel == busy, "unexpected worker");
  if (microkit_msginfo_get_label(info) == 1) {
    unsigned p = microkit_mr_get(0), offset = microkit_mr_get(1), byte = microkit_mr_get(2);
    require(p < PAGES && offset <= LIMIT && byte < 256, "write bounds");
    unsigned k = channel * PAGES + p;
    require(frame(mapping[k])[offset] == byte, "write observation");
    microkit_dbg_puts("SPLIT W "); number(k);
    microkit_dbg_puts(" "); number(offset);
    microkit_dbg_puts(" "); number(byte); microkit_dbg_puts("\n");
  } else {
    require(microkit_msginfo_get_label(info) == 2, "unknown worker request");
    busy = -1;
    done();
  }
  return microkit_msginfo_new(0, 0);
}

static void request(void) {
  if (busy >= 0) { microkit_dbg_puts("@error worker busy\n"); return; }
  if (line[0] == 'B' && !started && line[1] == ' ' && line[2] >= '0' && line[2] <= '3' && line[3] == ' ') {
    unsigned p = line[2] - '0', size;
    if (!length(line + 4, &size)) { microkit_dbg_puts("@error field too long\n"); return; }
    for (unsigned b = 0; b < BYTES; ++b) frame(p)[b] = 0;
    for (unsigned i = 0; i < size; ++i) frame(p)[i] = line[4 + i];
    done();
  } else if (line[0] == 'S' && !line[1] && !started) {
    started = 1;
    done();
  } else if (!started) {
    microkit_dbg_puts("@error document not opened\n");
  } else if (line[0] == 'E' && line[1] == ' ' && line[2] >= '0' && line[2] <= '1' && line[3] == ' ' && line[4] >= '0' && line[4] <= '3' && line[5] == ' ') {
    unsigned c = line[2] - '0', p = line[4] - '0', size;
    if (!length(line + 6, &size)) { microkit_dbg_puts("@error field too long\n"); return; }
    volatile struct job *job = (void *)(TASK + c * BYTES);
    job->page = p;
    for (unsigned i = 0; i < size; ++i) job->text[i] = line[6 + i];
    job->text[size] = 0;
    job->length = size;
    busy = c;
    microkit_notify(c);
  } else if (line[0] == 'Q' && line[1] == ' ' && line[2] >= '0' && line[2] <= '2' && !line[3]) {
    unsigned c = line[2] - '0';
    for (unsigned p = 0; p < PAGES; ++p) {
      unsigned k = c * PAGES + p, f = c == 2 ? p : mapping[k];
      if (c < CLIENTS) {
        for (unsigned b = 0; b <= LIMIT; ++b) {
          microkit_dbg_puts("SPLIT R "); number(k);
          microkit_dbg_puts(" "); number(b);
          microkit_dbg_puts(" "); number(frame(f)[b]); microkit_dbg_puts("\n");
          if (!frame(f)[b]) break;
        }
      }
      microkit_dbg_puts("@field "); number(p); microkit_dbg_puts(" ");
      for (unsigned b = 0; b < LIMIT && frame(f)[b]; ++b) {
        char text[2] = {(char)frame(f)[b], 0}; microkit_dbg_puts(text);
      }
      microkit_dbg_puts("\n");
    }
    done();
  } else if (line[0] == 'P' && !line[1]) {
    for (unsigned k = 0; k < SLOTS; ++k) {
      microkit_dbg_puts("@map "); number(k);
      microkit_dbg_puts(" "); number(BASE + (k % PAGES) * BYTES);
      microkit_dbg_puts(" "); number(physical(caps[k]));
      microkit_dbg_puts(" "); number(mapping[k] >= PAGES); microkit_dbg_puts("\n");
    }
    done();
  } else if (line[0] == 'D' && line[1] == ' ' && line[2] >= '0' && line[2] <= '1' && !line[3]) {
    unsigned c = line[2] - '0';
    require(seL4_TCB_Suspend(BASE_TCB_CAP + c) == seL4_NoError, "worker suspend");
    microkit_dbg_puts("SPLIT STOP "); number(c); microkit_dbg_puts("\n");
    for (unsigned p = 0; p < PAGES; ++p) {
      unsigned k = c * PAGES + p, f = mapping[k];
      if (f < PAGES) continue;
      seL4_Word old = physical(caps[k]);
      require(seL4_ARM_Page_Unmap(caps[k]) == seL4_NoError, "private page unmap");
      caps[k] = microkit_cspace_root_slot_to_cptr(10 + k);
      map(k, caps[k], 0);
      mapping[k] = p;
      volatile unsigned char *bytes = frame(f);
      for (unsigned b = 0; b < BYTES; ++b) bytes[b] = 0;
      unsigned cleared = 0;
      for (unsigned b = 0; b < BYTES; ++b) cleared += bytes[b] == 0;
      require(cleared == BYTES && returned < FRAMES - PAGES, "frame scrub");
      available[returned++] = f;
      microkit_dbg_puts("SPLIT RELEASE "); number(k);
      microkit_dbg_puts(" "); number(f);
      microkit_dbg_puts(" "); number(old);
      microkit_dbg_puts(" "); number(physical(caps[k]));
      microkit_dbg_puts(" "); number(cleared); microkit_dbg_puts("\n");
    }
    table();
    require(seL4_TCB_Resume(BASE_TCB_CAP + c) == seL4_NoError, "worker resume");
    microkit_dbg_puts("SPLIT RUN "); number(c); microkit_dbg_puts("\n");
    done();
  } else if (line[0] == 'F' && !line[1]) {
    for (unsigned i = returned; i > 0; --i) {
      unsigned f = available[i - 1], cleared = 0;
      volatile unsigned char *bytes = frame(f);
      for (unsigned b = 0; b < BYTES; ++b) cleared += bytes[b] == 0;
      microkit_dbg_puts("@free "); number(f);
      microkit_dbg_puts(" "); number(physical(microkit_cspace_root_slot_to_cptr(30 + f - PAGES)));
      microkit_dbg_puts(" "); number(cleared); microkit_dbg_puts("\n");
      for (unsigned b = 0; b < 32; ++b) {
        microkit_dbg_puts("@zero "); number(f);
        microkit_dbg_puts(" "); number(b);
        microkit_dbg_puts(" "); number(bytes[b]); microkit_dbg_puts("\n");
      }
    }
    done();
  } else if (line[0] == 'H' && line[1] == ' ' && line[2] >= '0' && line[2] <= '2' && line[3] == ' ' && line[4] >= '0' && line[4] <= '3' && !line[5]) {
    unsigned c = line[2] - '0', p = line[4] - '0', k = c * PAGES + p;
    unsigned f = c == 2 ? p : mapping[k];
    seL4_CPtr cap = c == 2 ? microkit_cspace_root_slot_to_cptr(10 + p) : caps[k];
    microkit_dbg_puts("@addr "); number(c == 2 ? BACKING + p * BYTES : BASE + p * BYTES);
    microkit_dbg_puts(" "); number(physical(cap)); microkit_dbg_puts("\n");
    for (unsigned b = 0; b < 64; ++b) {
      if (c < CLIENTS) {
        microkit_dbg_puts("SPLIT R "); number(k);
        microkit_dbg_puts(" "); number(b);
        microkit_dbg_puts(" "); number(frame(f)[b]); microkit_dbg_puts("\n");
      }
      microkit_dbg_puts("@byte "); number(b);
      microkit_dbg_puts(" "); number(frame(f)[b]); microkit_dbg_puts("\n");
    }
    done();
  } else if (line[0] == 'M' && !line[1]) {
    table();
    done();
  } else {
    microkit_dbg_puts("@error unknown request\n");
  }
}

void notified(microkit_channel channel) {
  require(channel == 5, "unknown interrupt");
  uart[0x44 / 4] = 0x7ff;
  while (!(uart[0x18 / 4] & 0x10)) {
    unsigned ch = uart[0] & 255;
    if (ch == '\n') {
      line[used] = 0;
      request();
      used = 0;
    } else if (ch >= 32 && ch < 127 && used < sizeof(line) - 1) {
      line[used++] = ch;
    }
  }
  microkit_irq_ack(5);
}

A  => guest/split.h +43 -0
@@ 1,43 @@
#ifndef SPLIT_H
#define SPLIT_H
#include <microkit.h>
#include <stdint.h>

#define PAGES 4u
#define CLIENTS 2u
#define SLOTS (PAGES * CLIENTS)
#define FRAMES (PAGES + SLOTS)
#define BYTES 4096u
#define BASE 0x40000000ul
#define BACKING 0x60000000ul
#define TASK 0x50000000ul
#define LIMIT 255u

struct job {
  unsigned page, length;
  char text[LIMIT + 1];
};

static inline void number(unsigned long n) {
  char s[24];
  unsigned i = sizeof(s) - 1;
  s[i] = 0;
  do {
    s[--i] = '0' + n % 10;
    n /= 10;
  } while (n);
  microkit_dbg_puts(s + i);
}

static inline unsigned char seed(unsigned page, unsigned offset) {
  return (page * 17u + offset) % 256u;
}

static inline void require(int ok, const char *message) {
  if (ok) return;
  microkit_dbg_puts("@error ");
  microkit_dbg_puts(message);
  microkit_dbg_puts("\n");
  for (;;) seL4_Yield();
}
#endif

A  => guest/split.system +76 -0
@@ 1,76 @@
<system>
  <memory_region name="frame0" size="0x1000" page_size="0x1000" />
  <memory_region name="frame1" size="0x1000" page_size="0x1000" />
  <memory_region name="frame2" size="0x1000" page_size="0x1000" />
  <memory_region name="frame3" size="0x1000" page_size="0x1000" />
  <memory_region name="frame4" size="0x1000" page_size="0x1000" />
  <memory_region name="frame5" size="0x1000" page_size="0x1000" />
  <memory_region name="frame6" size="0x1000" page_size="0x1000" />
  <memory_region name="frame7" size="0x1000" page_size="0x1000" />
  <memory_region name="frame8" size="0x1000" page_size="0x1000" />
  <memory_region name="frame9" size="0x1000" page_size="0x1000" />
  <memory_region name="frame10" size="0x1000" page_size="0x1000" />
  <memory_region name="frame11" size="0x1000" page_size="0x1000" />
  <memory_region name="reserve0" size="0x1000" page_size="0x1000" />
  <memory_region name="reserve1" size="0x1000" page_size="0x1000" />
  <protection_domain name="pager" priority="254">
    <program_image path="pager.elf" />
    <map mr="frame0" vaddr="0x60000000" perms="rw" />
    <map mr="frame1" vaddr="0x60001000" perms="rw" />
    <map mr="frame2" vaddr="0x60002000" perms="rw" />
    <map mr="frame3" vaddr="0x60003000" perms="rw" />
    <map mr="frame4" vaddr="0x60004000" perms="rw" />
    <map mr="frame5" vaddr="0x60005000" perms="rw" />
    <map mr="frame6" vaddr="0x60006000" perms="rw" />
    <map mr="frame7" vaddr="0x60007000" perms="rw" />
    <map mr="frame8" vaddr="0x60008000" perms="rw" />
    <map mr="frame9" vaddr="0x60009000" perms="rw" />
    <map mr="frame10" vaddr="0x6000a000" perms="rw" />
    <map mr="frame11" vaddr="0x6000b000" perms="rw" />
    <cspace>
      <cap_vspace slot="1" pd="alpha" />
      <cap_frame slot="10" mr="frame0" />
      <cap_frame slot="11" mr="frame1" />
      <cap_frame slot="12" mr="frame2" />
      <cap_frame slot="13" mr="frame3" />
      <cap_vspace slot="2" pd="beta" />
      <cap_frame slot="14" mr="frame0" />
      <cap_frame slot="15" mr="frame1" />
      <cap_frame slot="16" mr="frame2" />
      <cap_frame slot="17" mr="frame3" />
      <cap_frame slot="30" mr="frame4" />
      <cap_frame slot="31" mr="frame5" />
      <cap_frame slot="32" mr="frame6" />
      <cap_frame slot="33" mr="frame7" />
      <cap_frame slot="34" mr="frame8" />
      <cap_frame slot="35" mr="frame9" />
      <cap_frame slot="36" mr="frame10" />
      <cap_frame slot="37" mr="frame11" />
    </cspace>
    <protection_domain name="alpha" priority="100" id="0">
      <program_image path="alpha.elf" />
      <map mr="reserve0" vaddr="0x40010000" perms="r" />
      <map mr="task0" vaddr="0x50000000" perms="r" />
    </protection_domain>
    <protection_domain name="beta" priority="99" id="1">
      <program_image path="beta.elf" />
      <map mr="reserve1" vaddr="0x40010000" perms="r" />
      <map mr="task1" vaddr="0x50000000" perms="r" />
    </protection_domain>
    <map mr="uart" vaddr="0x70000000" perms="rw" cached="false" />
    <irq irq="33" id="5" trigger="level" />
    <map mr="task0" vaddr="0x50000000" perms="rw" />
    <map mr="task1" vaddr="0x50001000" perms="rw" />
  </protection_domain>
  <channel>
    <end pd="pager" id="0" />
    <end pd="alpha" id="0" pp="true" />
  </channel>
  <channel>
    <end pd="pager" id="1" />
    <end pd="beta" id="0" pp="true" />
  </channel>
  <memory_region name="uart" size="0x1000" page_size="0x1000" phys_addr="0x09000000" />
  <memory_region name="task0" size="0x1000" page_size="0x1000" />
  <memory_region name="task1" size="0x1000" page_size="0x1000" />
</system>

A  => host/check.ml +485 -0
@@ 1,485 @@
module S = Split.Split

let numbers =
  let xs = Array.make 4097 S.Zero_nat in
  for i = 1 to 4096 do xs.(i) <- S.Suc xs.(i - 1) done;
  xs

let nat n = numbers.(n)

let integer n =
  let rec count acc = function
    | S.Zero_nat -> acc
    | S.Suc n -> count (acc + 1) n
  in
  count 0 n

let require ok message = if not ok then failwith message
let read s k offset = integer (S.read_byte s (nat k) (nat offset))
let step capacity s a = S.checked (nat 8) (nat 4096) (nat capacity) s a

let selftest () =
  let state = ref S.boot in
  let reference = Array.init 8 (fun k -> Array.init 4096 (fun b -> ((k mod 4) * 17 + b) mod 256)) in
  let random = Random.State.make [|2026; 4; 8|] in
  for i = 0 to 1023 do
    let k = Random.State.int random 8 in
    let offset = if i mod 127 = 0 then 4095 else Random.State.int random 128 in
    let byte = Random.State.int random 256 in
    let action = S.Write (nat k, nat offset, nat byte) in
    state := Option.get (step 12 !state action);
    reference.(k).(offset) <- byte;
    for j = 0 to 7 do
      require (read !state j offset = reference.(j).(offset)) "private memory refinement"
    done
  done;
  require (integer (S.frontier !state) = 12) "one copy per written page";
  List.iter (fun action -> require (step 12 !state action = None) "invalid action accepted")
    [S.Write (nat 8, nat 0, nat 1); S.Write (nat 0, nat 4096, nat 1);
     S.Write (nat 0, nat 0, nat 256); S.Read (nat 8, nat 0)];
  require (step 4 S.boot (S.Write (nat 0, nat 0, nat 1)) = None) "exhaustion accepted";
  require (Option.is_some (step 12 !state (S.Write (nat 0, nat 0, nat 1)))) "private write rejected";
  print_endline "checked 1024 model writes with boundary and exhaustion cases"

type document = { fields : string array; values : string array }

type guest = {
  pid : int;
  input : in_channel;
  output : out_channel;
  serial : out_channel;
  original : document;
  mutable state : unit S.pager_ext;
  mutable frames : int array;
  mutable available : S.nat list;
  mutable tracing : bool;
}

let names = [|"draft"; "review"; "original"|]

let index xs name =
  let rec find i =
    if i = Array.length xs then invalid_arg ("unknown name " ^ name)
    else if xs.(i) = name then i else find (i + 1)
  in
  find 0

let text value =
  if String.length value > 255 then invalid_arg "a field can contain at most 255 bytes";
  String.iter (fun c -> if Char.code c < 32 || Char.code c > 126 then
    invalid_arg "fields must contain printable ASCII text") value

let load path =
  let input = open_in path in
  let entries = Fun.protect ~finally:(fun () -> close_in input) (fun () ->
    let rec collect acc =
      match input_line input with
      | exception End_of_file -> List.rev acc
      | line when String.trim line = "" -> collect acc
      | line ->
          let at = match String.index_opt line '=' with
            | Some at -> at
            | None -> invalid_arg "expected a key followed by an equals sign and a value"
          in
          let name = String.trim (String.sub line 0 at) in
          let value = String.trim (String.sub line (at + 1) (String.length line - at - 1)) in
          require (name <> "" && not (String.contains name ' ')) "keys must be single words";
          require (not (List.mem_assoc name acc)) ("duplicate field " ^ name);
          text value;
          collect ((name, value) :: acc)
    in
    collect []) in
  require (List.length entries = 4) "a document requires exactly four fields";
  {fields = Array.of_list (List.map fst entries); values = Array.of_list (List.map snd entries)}

let close g =
  (try Unix.kill g.pid Sys.sigterm with Unix.Unix_error (Unix.ESRCH, _, _) -> ());
  ignore (Unix.waitpid [] g.pid);
  close_in_noerr g.input;
  close_out_noerr g.output;
  close_out_noerr g.serial

let event g line =
  match String.split_on_char ' ' line with
  | ["SPLIT"; operation; c] when operation = "STOP" || operation = "RUN" ->
      let c = int_of_string c in
      require (c >= 0 && c < 2) "invalid worker lifecycle";
      if g.tracing then Printf.printf "%s %s TCB\n"
        (if operation = "STOP" then "Suspend" else "Resume ") (String.capitalize_ascii names.(c))
  | ["SPLIT"; "RELEASE"; k; f; old; source; cleared] ->
      let k, f = int_of_string k, int_of_string f in
      require (k >= 0 && k < 8 && integer (S.ptable g.state (nat k)) = f)
        "released frame mismatch";
      require (int_of_string cleared = 4096) "incomplete frame scrub";
      let state, available = match S.reclaim (nat 4) (nat 8) g.state g.available (nat k) with
        | Some result -> result
        | None -> failwith "model rejected frame reclamation"
      in
      g.state <- state; g.available <- available;
      if g.tracing then begin
        Printf.printf "Unmap   VA 0x%08x  PA 0x%08x\n" (0x40000000 + k mod 4 * 4096) (int_of_string old);
        Printf.printf "Restore VA 0x%08x -> PA 0x%08x  Rights R--\n"
          (0x40000000 + k mod 4 * 4096) (int_of_string source);
        Printf.printf "Scrub   PA 0x%08x  Readback 4096 zero bytes\n" (int_of_string old);
        Printf.printf "Release Frame %d -> Reclaimed pool\n" f
      end
  | ["SPLIT"; "FAULT"; k; address; ip; fsr; source; target] ->
      let k, address, ip, fsr, source, target = int_of_string k, int_of_string address,
        int_of_string ip, int_of_string fsr, int_of_string source, int_of_string target in
      require (k >= 0 && k < 8 && ip > 0 && ip land 3 = 0 && fsr land 64 <> 0)
        "invalid write fault metadata";
      require (address land (lnot 4095) = 0x40000000 + k mod 4 * 4096)
        "fault address does not match its slot";
      require (source <> target && source land 4095 = 0 && target land 4095 = 0)
        "invalid physical copy frames";
      if g.tracing then begin
        Printf.printf "VMFault %-6s VA 0x%08x  IP 0x%08x  FSR 0x%08x\n"
          (String.capitalize_ascii names.(k / 4)) address ip fsr;
        Printf.printf "Copy    PA 0x%08x -> PA 0x%08x  Bytes 4096\n" source target;
        Printf.printf "Remap   VA 0x%08x -> PA 0x%08x  Rights RW-\n" (address land (lnot 4095)) target;
        Printf.printf "Retry   IP 0x%08x\n" ip
      end
  | ["SPLIT"; "COPY"; k; source; fresh] ->
      let k, source, fresh = int_of_string k, int_of_string source, int_of_string fresh in
      require (k >= 0 && k < 8 && fresh < 12) "invalid page copy";
      require (not (S.writable g.state (nat k))) "duplicate page copy";
      require (integer (S.ptable g.state (nat k)) = source) "copy source mismatch";
      let recycled = g.available <> [] in
      let state, available = match S.reserve (nat 4) (nat 8) (nat 12) g.state g.available (nat k) with
        | Some result -> result
        | None -> failwith "model rejected frame allocation"
      in
      require (integer (S.ptable state (nat k)) = fresh) "allocation mismatch";
      g.state <- state; g.available <- available;
      if g.tracing && recycled then Printf.printf "Acquire Frame %d <- Reclaimed pool\n" fresh
  | "SPLIT" :: "TABLE" :: frames ->
      require (List.length frames = 8) "invalid page table";
      g.frames <- Array.of_list (List.map int_of_string frames);
      Array.iteri (fun k f -> require (integer (S.ptable g.state (nat k)) = f)
        "page table differs from model") g.frames
  | ["SPLIT"; operation; k; offset; byte] when operation = "R" || operation = "W" ->
      let k, offset, byte = int_of_string k, int_of_string offset, int_of_string byte in
      require (k >= 0 && k < 8 && offset >= 0 && offset < 4096 && byte >= 0 && byte < 256)
        "invalid memory observation";
      if operation = "W" then begin
        require (S.writable g.state (nat k)) "write reached a shared page";
        g.state <- Option.get (step 12 g.state (S.Write (nat k, nat offset, nat byte)))
      end;
      require (read g.state k offset = byte) "guest memory differs from model";
      if g.tracing && operation = "W" && (offset = 0 || byte = 0) then
        Printf.printf "Store   %-6s VA 0x%08x  Byte 0x%02x\n"
          (String.capitalize_ascii names.(k / 4)) (0x40000000 + k mod 4 * 4096 + offset) byte
  | _ -> ()

let receive g expected =
  let handler = Sys.signal Sys.sigalrm (Sys.Signal_handle (fun _ -> failwith "guest did not respond")) in
  ignore (Unix.alarm 10);
  Fun.protect ~finally:(fun () -> ignore (Unix.alarm 0); Sys.set_signal Sys.sigalrm handler) (fun () ->
    let rec collect acc =
      let raw = input_line g.input in
      let line = if String.ends_with ~suffix:"\r" raw then String.sub raw 0 (String.length raw - 1) else raw in
      output_string g.serial (line ^ "\n");
      event g line;
      if String.starts_with ~prefix:"@error " line then failwith (String.sub line 7 (String.length line - 7));
      if line = expected then List.rev acc
      else collect (if String.starts_with ~prefix:"@" line then line :: acc else acc)
    in
    collect [])

let request g line =
  output_string g.output (line ^ "\n");
  flush g.output;
  receive g "@done"

let launch image original =
  let child_input, writer = Unix.pipe () in
  let reader, child_output = Unix.pipe () in
  Unix.set_close_on_exec reader;
  Unix.set_close_on_exec writer;
  let args = [|"qemu-system-aarch64"; "-machine"; "virt,virtualization=on";
    "-cpu"; "cortex-a53"; "-m"; "2G"; "-nographic"; "-monitor"; "none";
    "-serial"; "stdio"; "-device"; "loader,file=" ^ image ^ ",addr=0x70000000,cpu-num=0"|] in
  let pid = Unix.create_process args.(0) args child_input child_output child_output in
  Unix.close child_input;
  Unix.close child_output;
  let contents f offset =
    let f, offset = integer f, integer offset in
    nat (if f < 4 && offset < String.length original.values.(f) then Char.code original.values.(f).[offset] else 0)
  in
  let g = {pid; input = Unix.in_channel_of_descr reader; output = Unix.out_channel_of_descr writer;
    serial = open_out "build/live.log"; original; state = S.initial (nat 4) contents;
    frames = Array.init 8 (fun k -> k mod 4); available = []; tracing = false} in
  try
    ignore (receive g "@ready");
    Array.iteri (fun p value -> ignore (request g (Printf.sprintf "B %d %s" p value))) original.values;
    ignore (request g "S");
    g
  with exn -> close g; raise exn

let query g target =
  let values = Array.make 4 "" and seen = Array.make 4 false in
  List.iter (fun line ->
    require (String.length line >= 8) "incomplete document response";
    let p = Char.code line.[7] - Char.code '0' in
    require (p >= 0 && p < 4 && not seen.(p)) "invalid document response";
    values.(p) <- if String.length line > 8 then String.sub line 9 (String.length line - 9) else "";
    seen.(p) <- true) (request g (Printf.sprintf "Q %d" target));
  require (Array.for_all Fun.id seen) "incomplete document";
  if target = 2 then require (values = g.original.values) "original document changed";
  values

let edit g target field value =
  if target = 2 then invalid_arg "the original is read-only";
  text value;
  ignore (request g (Printf.sprintf "E %d %d %s" target field value))

let save path document =
  let output = open_out path in
  Fun.protect ~finally:(fun () -> close_out output) (fun () ->
    Array.iteri (fun i value -> Printf.fprintf output "%s = %s\n" document.fields.(i) value) document.values)

let memory g =
  ignore (request g "M");
  print_endline "Original: 4 read-only source pages";
  Printf.printf "%-12s %14s %14s\n" "Snapshot" "Private pages" "Shared pages";
  for c = 0 to 1 do
    let private_pages = ref 0 in
    for p = 0 to 3 do if g.frames.(c * 4 + p) >= 4 then incr private_pages done;
    Printf.printf "%-12s %14d %14d\n" (String.capitalize_ascii names.(c)) !private_pages (4 - !private_pages)
  done;
  let copies = integer (S.frontier g.state) - 4 - List.length g.available in
  Printf.printf "Copy pool: %d used, %d reserved pages available\n" copies (8 - copies)

let discard g target =
  if target = 2 then invalid_arg "the original is read-only";
  ignore (request g (Printf.sprintf "D %d" target));
  require (query g target = g.original.values) "discard did not restore original"

let pool g =
  let remaining = ref g.available and count = ref 0 and samples = ref 0 in
  List.iter (fun line -> match String.split_on_char ' ' line with
    | ["@free"; f; pa; cleared] ->
        let f = int_of_string f in
        require (int_of_string cleared = 4096) "returned frame contains data";
        (match !remaining with
        | head :: tail when integer head = f -> remaining := tail
        | _ -> failwith "returned pool differs from model");
        incr count;
        Printf.printf "Frame %d  PA 0x%08x  Unmapped from clients  4096 zero bytes\n" f (int_of_string pa)
    | ["@zero"; f; offset; byte] ->
        let f, offset, byte = int_of_string f, int_of_string offset, int_of_string byte in
        require (List.exists (fun n -> integer n = f) g.available && offset = !samples mod 32 && byte = 0)
          "returned frame sample mismatch";
        incr samples;
        if offset mod 16 = 0 then Printf.printf "  %04x  " offset;
        Printf.printf "%02x " byte;
        if offset mod 16 = 15 then print_newline ()
    | _ -> failwith "invalid pool response") (request g "F");
  require (!remaining = [] && !samples = !count * 32) "incomplete pool response";
  if !count = 0 then print_endline "Reclaimed pool empty"

let mappings g =
  print_endline "Snapshot  Virtual address  Physical address  Rights";
  List.iter (fun line -> match String.split_on_char ' ' line with
    | ["@map"; k; address; physical; writable] ->
        let k = int_of_string k in
        require ((g.frames.(k) >= 4) = (writable = "1")) "mapping rights mismatch";
        Printf.printf "%-8s  0x%08x       0x%08x        %s\n"
          (String.capitalize_ascii names.(k / 4)) (int_of_string address)
          (int_of_string physical) (if writable = "1" then "RW-" else "R--")
    | _ -> failwith "invalid mapping response") (request g "P")

let peek g target field =
  let address = ref 0 and physical = ref 0 and seen = Array.make 64 false in
  let bytes = Array.make 64 0 in
  List.iter (fun line -> match String.split_on_char ' ' line with
    | ["@addr"; va; pa] -> address := int_of_string va; physical := int_of_string pa
    | ["@byte"; offset; byte] ->
        let offset, byte = int_of_string offset, int_of_string byte in
        require (offset >= 0 && offset < 64 && byte >= 0 && byte < 256) "invalid byte response";
        let expected = if target < 2 then read g.state (target * 4 + field) offset
          else if offset < String.length g.original.values.(field) then Char.code g.original.values.(field).[offset] else 0 in
        require (byte = expected) "memory dump differs from model";
        seen.(offset) <- true; bytes.(offset) <- byte
    | _ -> failwith "invalid memory response") (request g (Printf.sprintf "H %d %d" target field));
  require (Array.for_all Fun.id seen) "incomplete memory response";
  Printf.printf "%s %s  VA 0x%08x  PA 0x%08x\n" (String.capitalize_ascii names.(target))
    (String.capitalize_ascii g.original.fields.(field)) !address !physical;
  for row = 0 to 3 do
    Printf.printf "%08x  " (!address + row * 16);
    for i = 0 to 15 do Printf.printf "%02x " bytes.(row * 16 + i) done;
    print_string " ";
    for i = 0 to 15 do
      let byte = bytes.(row * 16 + i) in
      print_char (if byte >= 32 && byte < 127 then Char.chr byte else '.')
    done;
    print_newline ()
  done

let edit_value line =
  let size = String.length line in
  let rec spaces i = if i < size && line.[i] = ' ' then spaces (i + 1) else i in
  let rec words i remaining =
    if remaining = 0 then String.sub line i (size - i)
    else match String.index_from_opt line (spaces i) ' ' with
      | None -> ""
      | Some at -> words (spaces at) (remaining - 1)
  in
  words 0 3

let work image path =
  let original = load path in
  let g = launch image original in
  Fun.protect ~finally:(fun () -> close g) (fun () ->
    print_endline "Split Configuration Snapshots";
    print_endline "seL4 guest running on AArch64";
    print_endline ("Opened " ^ path ^ ". Draft and Review share the original pages.");
    print_endline "Commands: show, edit, discard, diff, maps, peek, pool, trace, memory, save, quit";
    let running = ref true in
    while !running do
      print_string "$ "; flush stdout;
      match input_line stdin with
      | exception End_of_file -> running := false
      | line ->
          try
            match String.split_on_char ' ' (String.trim line) |> List.filter ((<>) "") with
            | [] -> ()
            | ["quit"] -> running := false
            | ["clear"] -> print_string "\027[2J\027[H"
            | ["trace"; "on"] -> g.tracing <- true; print_endline "Live VM trace enabled"
            | ["trace"; "off"] -> g.tracing <- false; print_endline "Live VM trace disabled"
            | ["maps"] -> mappings g
            | ["pool"] -> pool g
            | ["discard"; name] ->
                discard g (index names name);
                print_endline ("Discarded " ^ String.capitalize_ascii name ^ " edits. Original mappings restored.")
            | ["peek"; name; field] -> peek g (index names name) (index original.fields field)
            | ["show"; name] ->
                let values = query g (index names name) in
                print_endline (String.capitalize_ascii name ^ " configuration");
                Array.iteri (fun i value -> Printf.printf "%-8s %s\n" (String.capitalize_ascii original.fields.(i)) value) values
            | "edit" :: name :: field :: _ ->
                let target = index names name and page = index original.fields field in
                let owned = target < 2 && S.writable g.state (nat (target * 4 + page)) in
                edit g (index names name) (index original.fields field) (edit_value line);
                Printf.printf "Updated %s in %s. %s\n" (String.capitalize_ascii field)
                  (String.capitalize_ascii name)
                  (if owned then "Reused its private page." else "Copied one shared page.")
            | ["diff"; name] ->
                let values = query g (index names name) and baseline = query g 2 in
                print_endline ("Changes in " ^ String.capitalize_ascii name);
                let changed = ref false in
                Array.iteri (fun i value -> if value <> baseline.(i) then begin
                  changed := true;
                  Printf.printf "%s\n  Original  %s\n  %-8s  %s\n" (String.capitalize_ascii original.fields.(i))
                    baseline.(i) (String.capitalize_ascii name) value
                end) values;
                if not !changed then print_endline "No changes"
            | ["memory"] -> memory g
            | ["save"; name; filename] ->
                save filename {original with values = query g (index names name)};
                print_endline ("Saved " ^ filename)
            | _ -> print_endline "Use show NAME, edit NAME FIELD TEXT, diff NAME, memory, save NAME FILE, or quit"
          with Invalid_argument message | Sys_error message -> print_endline (String.capitalize_ascii message)
    done)

let exercise () =
  let original = load "example/document" in
  let g = launch "build/loader.img" original in
  Fun.protect ~finally:(fun () -> close g) (fun () ->
    let physicals () =
      Array.of_list (List.map (fun line -> match String.split_on_char ' ' line with
        | ["@map"; _; _; address; rights] -> int_of_string address, rights
        | _ -> failwith "invalid mapping record") (request g "P"))
    in
    let initial_frames = physicals () in
    require (Array.length initial_frames = 8) "mapping count";
    for p = 0 to 3 do
      require (initial_frames.(p) = initial_frames.(p + 4) && snd initial_frames.(p) = "0")
        "initial physical sharing"
    done;
    require (query g 0 = original.values && query g 1 = original.values && query g 2 = original.values) "initial document";
    edit g 0 3 "x";
    require ((query g 0).(3) = "x") "document edit";
    let copied_frames = physicals () in
    require (fst copied_frames.(3) <> fst initial_frames.(3) && snd copied_frames.(3) = "1"
      && copied_frames.(7) = initial_frames.(7)) "physical copy isolation";
    peek g 0 3;
    peek g 1 3;
    peek g 2 3;
    require (query g 1 = original.values && query g 2 = original.values) "snapshot isolation";
    require (integer (S.frontier g.state) = 5) "first write copy";
    edit g 0 3 "";
    require ((query g 0).(3) = "") "empty field";
    require (integer (S.frontier g.state) = 5) "repeated edit allocated";
    require ((physicals ()).(3) = copied_frames.(3)) "private physical frame changed";
    edit g 0 3 (String.make 255 'x');
    require (String.length (query g 0).(3) = 255) "maximum field";
    let rejected = try edit g 0 0 (String.make 256 'x'); false with Invalid_argument _ -> true in
    require rejected "oversized field accepted";
    let before = query g 0 in
    let rejected = try ignore (request g ("E 0 0 " ^ String.make 256 'x')); false
      with Failure message -> message = "field too long" in
    require (rejected && query g 0 = before) "guest edit bounds";
    for c = 0 to 1 do
      for p = 0 to 3 do edit g c p (Printf.sprintf "snapshot %d field %d" c p) done
    done;
    require (integer (S.frontier g.state) = 12) "copy pool accounting";
    let final_frames = physicals () in
    for k = 0 to 7 do
      for j = k + 1 to 7 do
        require (fst final_frames.(k) <> fst final_frames.(j)) "private physical alias"
      done
    done;
    edit g 1 0 "updated after pool exhausted";
    require ((query g 1).(0) = "updated after pool exhausted") "private edit after exhaustion";
    require (query g 2 = original.values) "original changed";
    let values = query g 0 in
    save "build/export.txt" {original with values};
    require ((load "build/export.txt").values = values) "saved document differs";
    let rejected = try edit g 2 0 "changed"; false with Invalid_argument _ -> true in
    require rejected "original edit accepted";
    let survivor = query g 1 in
    discard g 0;
    require (List.length g.available = 4 && query g 1 = survivor) "discard isolation";
    let reset_frames = physicals () in
    for p = 0 to 3 do require (reset_frames.(p) = initial_frames.(p)) "discard mapping restore" done;
    pool g;
    for cycle = 0 to 15 do
      for p = 0 to 3 do edit g 0 p (Printf.sprintf "cycle %d field %d" cycle p) done;
      require (g.available = [] && integer (S.frontier g.state) = 12) "recycling grew pool";
      require (query g 1 = survivor && query g 2 = original.values) "recycling changed survivor";
      discard g 0
    done;
    discard g 1;
    require (List.length g.available = 8) "complete pool return";
    let recycled = fst final_frames.(7) in
    edit g 0 0 "draft now owns review frame";
    require (fst (physicals ()).(0) = recycled) "cross snapshot physical reuse";
    edit g 1 3 "review writes its old virtual address";
    require (fst (physicals ()).(7) <> recycled && (query g 0).(0) = "draft now owns review frame")
      "stale client mapping survived discard";
    discard g 0;
    discard g 1;
    pool g;
    require (query g 2 = original.values && List.length g.available = 8) "reclamation final state");
  let g = launch "build/unsafe.img" original in
  let detected = Fun.protect ~finally:(fun () -> close g) (fun () ->
    try edit g 0 3 "changed"; false with Failure message -> message = "write reached a shared page") in
  require detected "writable alias was not detected";
  print_endline "exercised edits, exports, full pool recycling and stale mapping isolation"

let () =
  Sys.catch_break true;
  try
    match Array.to_list Sys.argv with
    | [_; "--model"] -> selftest ()
    | [_; "--test"] -> exercise ()
    | [_] | [_; "work"] -> work "build/loader.img" "example/document"
    | [_; "work"; path] -> work "build/loader.img" path
    | _ -> invalid_arg "usage: check work DOCUMENT"
  with
  | Sys.Break -> print_newline ()
  | Failure message | Invalid_argument message | Sys_error message ->
      prerr_endline message; exit 1
  | exn -> prerr_endline (Printexc.to_string exn); exit 1

A  => media/poster.png +0 -0
A  => media/split.mp4 +0 -0
A  => proof/Execution.thy +131 -0
@@ 1,131 @@
theory Execution
  imports Isolation
begin

datatype action = Read nat nat | Write nat nat nat

fun valid :: "nat \<Rightarrow> nat \<Rightarrow> action \<Rightarrow> bool" where
  "valid slots bytes (Read k offset) = (k < slots \<and> offset < bytes)"
| "valid slots bytes (Write k offset byte) = (k < slots \<and> offset < bytes \<and> byte < 256)"

fun advance :: "pager \<Rightarrow> action \<Rightarrow> pager" where
  "advance s (Read k offset) = s"
| "advance s (Write k offset byte) = store s k offset byte"

fun reference :: "memory \<Rightarrow> action \<Rightarrow> memory" where
  "reference m (Read k offset) = m"
| "reference m (Write k offset byte) = reference_write m k offset byte"

fun observe :: "pager \<Rightarrow> action \<Rightarrow> nat" where
  "observe s (Read k offset) = view s k offset"
| "observe s (Write k offset byte) = byte"

fun reference_observe :: "memory \<Rightarrow> action \<Rightarrow> nat" where
  "reference_observe m (Read k offset) = m k offset"
| "reference_observe m (Write k offset byte) = byte"

fun execute :: "pager \<Rightarrow> action list \<Rightarrow> pager" where
  "execute s [] = s"
| "execute s (a # actions) = execute (advance s a) actions"

fun reference_execute :: "memory \<Rightarrow> action list \<Rightarrow> memory" where
  "reference_execute m [] = m"
| "reference_execute m (a # actions) = reference_execute (reference m a) actions"

fun observations :: "pager \<Rightarrow> action list \<Rightarrow> nat list" where
  "observations s [] = []"
| "observations s (a # actions) = observe s a # observations (advance s a) actions"

fun reference_observations :: "memory \<Rightarrow> action list \<Rightarrow> nat list" where
  "reference_observations m [] = []"
| "reference_observations m (a # actions) =
    reference_observe m a # reference_observations (reference m a) actions"

lemma advance_invariant:
  "invariant n s \<Longrightarrow> invariant n (advance s a)"
  by (cases a; simp add: write_invariant)

lemma advance_simulation:
  "invariant n s \<Longrightarrow> represents n s m \<Longrightarrow> valid n bytes a \<Longrightarrow>
   represents n (advance s a) (reference m a)"
  by (cases a; auto intro: simulation)

lemma observe_simulation:
  "represents n s m \<Longrightarrow> valid n bytes a \<Longrightarrow>
   observe s a = reference_observe m a"
  by (cases a; auto simp: represents_def)

theorem execute_invariant:
  "invariant n s \<Longrightarrow> invariant n (execute s actions)"
  by (induction actions arbitrary: s) (auto intro: advance_invariant)

theorem execute_refinement:
  assumes "invariant n s" "represents n s m" "list_all (valid n bytes) actions"
  shows "represents n (execute s actions) (reference_execute m actions)"
  using assms
  by (induction actions arbitrary: s m)
     (auto intro: advance_invariant advance_simulation)

theorem observable_equivalence:
  assumes "invariant n s" "represents n s m" "list_all (valid n bytes) actions"
  shows "observations s actions = reference_observations m actions"
  using assms
  by (induction actions arbitrary: s m)
     (auto intro: advance_invariant advance_simulation observe_simulation)

definition admitted :: "nat \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> pager \<Rightarrow> action \<Rightarrow> bool" where
  "admitted slots bytes capacity s a \<longleftrightarrow> valid slots bytes a \<and>
    (case a of Read k offset \<Rightarrow> True
     | Write k offset byte \<Rightarrow> writable s k \<or> frontier s < capacity)"

definition checked :: "nat \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> pager \<Rightarrow> action \<Rightarrow> pager option" where
  "checked slots bytes capacity s a =
    (if admitted slots bytes capacity s a then Some (advance s a) else None)"

lemma checked_failure:
  "\<not> admitted slots bytes capacity s a \<Longrightarrow> checked slots bytes capacity s a = None"
  by (simp add: checked_def)

lemma checked_success:
  "checked slots bytes capacity s a = Some t \<Longrightarrow> t = advance s a"
  by (auto simp: checked_def split: if_splits)

theorem checked_invariant:
  "invariant slots s \<Longrightarrow> checked slots bytes capacity s a = Some t \<Longrightarrow>
   invariant slots t"
  using checked_success advance_invariant by blast

theorem checked_capacity:
  "frontier s \<le> capacity \<Longrightarrow> checked slots bytes capacity s a = Some t \<Longrightarrow>
   frontier t \<le> capacity"
  by (cases a; auto simp: checked_def admitted_def prepare_frontier split: if_splits)

theorem checked_simulation:
  "invariant slots s \<Longrightarrow> represents slots s m \<Longrightarrow>
   checked slots bytes capacity s a = Some t \<Longrightarrow>
   represents slots t (reference m a)"
  by (auto simp: checked_def admitted_def split: if_splits intro: advance_simulation)

lemma invalid_slot_rejected:
  "slots \<le> k \<Longrightarrow> checked slots bytes capacity s (Write k offset byte) = None"
  by (simp add: checked_def admitted_def)

lemma invalid_offset_rejected:
  "bytes \<le> offset \<Longrightarrow> checked slots bytes capacity s (Write k offset byte) = None"
  by (simp add: checked_def admitted_def)

lemma invalid_byte_rejected:
  "256 \<le> byte \<Longrightarrow> checked slots bytes capacity s (Write k offset byte) = None"
  by (simp add: checked_def admitted_def)

lemma exhausted_readonly_rejected:
  "\<not> writable s k \<Longrightarrow> capacity \<le> frontier s \<Longrightarrow>
   checked slots bytes capacity s (Write k offset byte) = None"
  by (simp add: checked_def admitted_def)

lemma exhausted_writable_allowed:
  "writable s k \<Longrightarrow> valid slots bytes (Write k offset byte) \<Longrightarrow>
   checked slots bytes capacity s (Write k offset byte) = Some (store s k offset byte)"
  by (simp add: checked_def admitted_def)

end

A  => proof/Export.thy +16 -0
@@ 1,16 @@
theory Export
  imports Reuse
begin

definition seed :: memory where
  "seed frame offset = (frame * 17 + offset) mod 256"

definition boot :: pager where "boot = initial 4 seed"

definition read_byte :: "pager \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> nat" where
  "read_byte s k offset = view s k offset"

export_code boot initial read_byte checked frontier ptable writable prepare reserve reclaim
  Read Write Suc in OCaml module_name Split file_prefix split

end

A  => proof/Isolation.thy +89 -0
@@ 1,89 @@
theory Isolation
  imports Model
begin

lemma write_target:
  assumes "bounded n s" "k < n"
  shows "view (store s k offset byte) k = (view s k)(offset := byte)"
  using prepare_view[OF assms, of k]
  by (simp add: view_def store_def Let_def)

lemma write_other:
  assumes inv: "invariant n s" and valid: "k < n" "j < n" and different: "j \<noteq> k"
  shows "view (store s k offset byte) j = view s j"
proof -
  have prepared: "invariant n (prepare s k)"
    using inv by (rule prepare_invariant)
  have distinct: "ptable (prepare s k) j \<noteq> ptable (prepare s k) k"
    using writable_unique[of n "prepare s k" k j] prepared valid different
    by (auto simp: invariant_def)
  have preserved: "view (prepare s k) j = view s j"
    using prepare_view[of n s j k] inv valid by (auto simp: invariant_def)
  show ?thesis
    using distinct preserved by (simp add: store_def view_def Let_def)
qed

theorem write_refinement:
  assumes "invariant n s" "k < n" "j < n"
  shows "view (store s k offset byte) j =
    (if j = k then (view s k)(offset := byte) else view s j)"
  using assms write_target write_other by (auto simp: invariant_def)

corollary read_after_write:
  "invariant n s \<Longrightarrow> k < n \<Longrightarrow>
   view (store s k offset byte) k offset = byte"
  using write_refinement[of n s k k offset byte] by simp

corollary other_offset:
  "invariant n s \<Longrightarrow> k < n \<Longrightarrow> address \<noteq> offset \<Longrightarrow>
   view (store s k offset byte) k address = view s k address"
  using write_refinement[of n s k k offset byte] by simp

definition reference_write :: "memory \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> memory" where
  "reference_write m k offset byte = m(k := (m k)(offset := byte))"

definition represents :: "nat \<Rightarrow> pager \<Rightarrow> memory \<Rightarrow> bool" where
  "represents n s m \<longleftrightarrow> (\<forall>k<n. view s k = m k)"

lemma represents_initial:
  "represents n (initial pages contents) (\<lambda>k. contents (k mod pages))"
  by (simp add: represents_def)

theorem simulation:
  assumes "invariant n s" "represents n s m" "k < n"
  shows "represents n (store s k offset byte) (reference_write m k offset byte)"
  using assms write_refinement[of n s k]
  by (auto simp: represents_def reference_write_def)

lemma prepared_exclusive:
  "invariant n s \<Longrightarrow> k < n \<Longrightarrow> j < n \<Longrightarrow> j \<noteq> k \<Longrightarrow>
   ptable (prepare s k) j \<noteq> ptable (prepare s k) k"
  using prepare_invariant writable_unique prepare_writable
  unfolding invariant_def by blast

lemma writable_no_copy:
  "writable s k \<Longrightarrow> frontier (store s k offset byte) = frontier s"
  by (simp add: prepare_def)

lemma readonly_one_copy:
  "\<not> writable s k \<Longrightarrow> frontier (store s k offset byte) = Suc (frontier s)"
  by (simp add: prepare_def)

lemma subsequent_write_no_copy:
  "frontier (store (store s k a x) k b y) = frontier (store s k a x)"
  by (rule writable_no_copy) simp

lemma write_writable_target [simp]: "writable (store s k offset byte) k"
  by simp

lemma write_writable_other:
  "j \<noteq> k \<Longrightarrow> writable (store s k offset byte) j = writable s j"
  by (simp add: prepare_def)

lemma write_preserves_unallocated:
  "f > frontier s \<Longrightarrow> physical (store s k offset byte) f = physical s f"
  if "bounded n s" "k < n" for f s k n offset byte
  using that bounded_frame[OF that]
  by (auto simp: store_def prepare_def detach_def Let_def split: if_splits)

end

A  => proof/Model.thy +158 -0
@@ 1,158 @@
theory Model
  imports Main
begin

type_synonym page = "nat \<Rightarrow> nat"
type_synonym memory = "nat \<Rightarrow> page"

record pager =
  ptable :: "nat \<Rightarrow> nat"
  physical :: memory
  writable :: "nat \<Rightarrow> bool"
  frontier :: nat

definition view :: "pager \<Rightarrow> memory" where
  "view s k = physical s (ptable s k)"

definition bounded :: "nat \<Rightarrow> pager \<Rightarrow> bool" where
  "bounded n s \<longleftrightarrow> (\<forall>k<n. ptable s k < frontier s)"

definition exclusive :: "nat \<Rightarrow> pager \<Rightarrow> bool" where
  "exclusive n s \<longleftrightarrow>
    (\<forall>k<n. writable s k \<longrightarrow> (\<forall>j<n. ptable s j = ptable s k \<longrightarrow> j = k))"

definition invariant :: "nat \<Rightarrow> pager \<Rightarrow> bool" where
  "invariant n s \<longleftrightarrow> bounded n s \<and> exclusive n s"

definition initial :: "nat \<Rightarrow> memory \<Rightarrow> pager" where
  "initial pages contents =
    \<lparr>ptable = (\<lambda>k. k mod pages), physical = contents,
     writable = (\<lambda>_. False), frontier = pages\<rparr>"

definition detach :: "pager \<Rightarrow> nat \<Rightarrow> pager" where
  "detach s k = s\<lparr>ptable := (ptable s)(k := frontier s),
    physical := (physical s)(frontier s := view s k),
    writable := (writable s)(k := True), frontier := Suc (frontier s)\<rparr>"

definition prepare :: "pager \<Rightarrow> nat \<Rightarrow> pager" where
  "prepare s k = (if writable s k then s else detach s k)"

definition store :: "pager \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> pager" where
  "store s k offset byte = (let t = prepare s k in
    t\<lparr>physical := (physical t)(ptable t k := (view t k)(offset := byte))\<rparr>)"

lemma initial_ptable [simp]: "ptable (initial pages contents) k = k mod pages"
  by (simp add: initial_def)

lemma initial_physical [simp]: "physical (initial pages contents) = contents"
  by (simp add: initial_def)

lemma initial_writable [simp]: "\<not> writable (initial pages contents) k"
  by (simp add: initial_def)

lemma initial_frontier [simp]: "frontier (initial pages contents) = pages"
  by (simp add: initial_def)

lemma initial_view [simp]: "view (initial pages contents) k = contents (k mod pages)"
  by (simp add: view_def)

lemma initial_bounded:
  "0 < pages \<Longrightarrow> bounded n (initial pages contents)"
  by (simp add: bounded_def)

lemma initial_exclusive: "exclusive n (initial pages contents)"
  by (simp add: exclusive_def)

theorem initial_invariant:
  "0 < pages \<Longrightarrow> invariant n (initial pages contents)"
  by (simp add: invariant_def initial_bounded initial_exclusive)

lemma bounded_frame:
  "bounded n s \<Longrightarrow> k < n \<Longrightarrow> ptable s k < frontier s"
  by (simp add: bounded_def)

lemma fresh_frame:
  "bounded n s \<Longrightarrow> k < n \<Longrightarrow> ptable s k \<noteq> frontier s"
  using bounded_frame by fastforce

lemma writable_unique:
  "exclusive n s \<Longrightarrow> k < n \<Longrightarrow> j < n \<Longrightarrow>
   writable s k \<Longrightarrow> ptable s j = ptable s k \<Longrightarrow> j = k"
  by (auto simp: exclusive_def)

lemma detach_ptable [simp]:
  "ptable (detach s k) j = (if j = k then frontier s else ptable s j)"
  by (simp add: detach_def)

lemma detach_writable [simp]:
  "writable (detach s k) j = (j = k \<or> writable s j)"
  by (simp add: detach_def)

lemma detach_frontier [simp]: "frontier (detach s k) = Suc (frontier s)"
  by (simp add: detach_def)

lemma detach_copy [simp]: "physical (detach s k) (frontier s) = view s k"
  by (simp add: detach_def)

lemma detach_other_frame:
  "f \<noteq> frontier s \<Longrightarrow> physical (detach s k) f = physical s f"
  by (simp add: detach_def)

lemma detach_target_view [simp]: "view (detach s k) k = view s k"
  by (simp add: view_def detach_def)

lemma detach_view:
  assumes "bounded n s" "j < n"
  shows "view (detach s k) j = view s j"
  using fresh_frame[OF assms] by (auto simp: view_def detach_def)

lemma detach_bounded:
  "bounded n s \<Longrightarrow> bounded n (detach s k)"
  by (auto simp: bounded_def less_Suc_eq_le)

lemma detach_exclusive:
  assumes "bounded n s" "exclusive n s"
  shows "exclusive n (detach s k)"
  using assms by (auto simp: exclusive_def bounded_def split: if_splits)

theorem detach_invariant:
  "invariant n s \<Longrightarrow> invariant n (detach s k)"
  by (simp add: invariant_def detach_bounded detach_exclusive)

lemma prepare_writable [simp]: "writable (prepare s k) k"
  by (simp add: prepare_def)

lemma prepare_invariant:
  "invariant n s \<Longrightarrow> invariant n (prepare s k)"
  by (simp add: prepare_def detach_invariant)

lemma prepare_view:
  "bounded n s \<Longrightarrow> j < n \<Longrightarrow> view (prepare s k) j = view s j"
  by (simp add: prepare_def detach_view)

lemma prepare_frontier:
  "frontier (prepare s k) = frontier s + (if writable s k then 0 else 1)"
  by (simp add: prepare_def)

lemma prepare_ptable_other:
  "j \<noteq> k \<Longrightarrow> ptable (prepare s k) j = ptable s j"
  by (simp add: prepare_def)

lemma prepare_idempotent [simp]: "prepare (prepare s k) k = prepare s k"
  by (simp add: prepare_def detach_def)

lemma write_ptable [simp]: "ptable (store s k offset byte) = ptable (prepare s k)"
  by (simp add: store_def Let_def)

lemma write_writable [simp]: "writable (store s k offset byte) = writable (prepare s k)"
  by (simp add: store_def Let_def)

lemma write_frontier [simp]: "frontier (store s k offset byte) = frontier (prepare s k)"
  by (simp add: store_def Let_def)

theorem write_invariant:
  "invariant n s \<Longrightarrow> invariant n (store s k offset byte)"
  using prepare_invariant[of n s k]
  by (simp add: invariant_def bounded_def exclusive_def)

end

A  => proof/Protocol.thy +118 -0
@@ 1,118 @@
theory Protocol
  imports Execution
begin

definition commit :: "pager \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> pager" where
  "commit s k offset byte =
    s\<lparr>physical := (physical s)(ptable s k := (view s k)(offset := byte))\<rparr>"

definition attempt :: "pager \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> pager option" where
  "attempt s k offset byte = (if writable s k then Some (commit s k offset byte) else None)"

lemma readonly_faults:
  "\<not> writable s k \<Longrightarrow> attempt s k offset byte = None"
  by (simp add: attempt_def)

lemma writable_commits:
  "writable s k \<Longrightarrow> attempt s k offset byte = Some (store s k offset byte)"
  by (simp add: attempt_def commit_def store_def prepare_def)

lemma resolve_preserves_memory:
  "invariant n s \<Longrightarrow> j < n \<Longrightarrow> view (detach s k) j = view s j"
  using detach_view[of n s j k] by (auto simp: invariant_def)

theorem fault_retry:
  "\<not> writable s k \<Longrightarrow>
   attempt (detach s k) k offset byte = Some (store s k offset byte)"
  by (simp add: attempt_def commit_def store_def prepare_def detach_def)

theorem fault_retry_refinement:
  assumes "invariant n s" "represents n s m" "k < n" "\<not> writable s k"
  obtains t where "attempt (detach s k) k offset byte = Some t"
    "invariant n t" "represents n t (reference_write m k offset byte)"
  using assms fault_retry write_invariant simulation by blast

definition dirty :: "nat \<Rightarrow> pager \<Rightarrow> nat set" where
  "dirty n s = {k. k < n \<and> writable s k}"

lemma finite_dirty [simp]: "finite (dirty n s)"
  unfolding dirty_def by (rule finite_subset[of _ "{..<n}"]) auto

lemma dirty_initial [simp]: "dirty n (initial pages contents) = {}"
  by (auto simp: dirty_def)

lemma dirty_member [simp]: "k \<in> dirty n s \<longleftrightarrow> k < n \<and> writable s k"
  by (simp add: dirty_def)

lemma dirty_store:
  "k < n \<Longrightarrow> dirty n (store s k offset byte) = insert k (dirty n s)"
  by (auto simp: dirty_def prepare_def)

definition accounted :: "nat \<Rightarrow> nat \<Rightarrow> pager \<Rightarrow> bool" where
  "accounted pages slots s \<longleftrightarrow> frontier s = pages + card (dirty slots s)"

lemma initial_accounted: "accounted pages slots (initial pages contents)"
  by (simp add: accounted_def)

lemma store_accounted:
  assumes "accounted pages slots s" "k < slots"
  shows "accounted pages slots (store s k offset byte)"
  using assms
  by (auto simp: accounted_def dirty_store prepare_frontier card_insert_if insert_absorb)

lemma advance_accounted:
  "accounted pages slots s \<Longrightarrow> valid slots bytes a \<Longrightarrow>
   accounted pages slots (advance s a)"
  by (cases a; auto intro: store_accounted)

theorem execute_accounted:
  "accounted pages slots s \<Longrightarrow> list_all (valid slots bytes) actions \<Longrightarrow>
   accounted pages slots (execute s actions)"
  by (induction actions arbitrary: s) (auto intro: advance_accounted)

lemma dirty_card_bound: "card (dirty slots s) \<le> slots"
proof -
  have "dirty slots s \<subseteq> {..<slots}" by (auto simp: dirty_def)
  then have "card (dirty slots s) \<le> card {..<slots}"
    by (intro card_mono) auto
  then show ?thesis by simp
qed

theorem allocation_bound:
  assumes "list_all (valid slots bytes) actions"
  shows "frontier (execute (initial pages contents) actions) \<le> pages + slots"
  using execute_accounted[OF initial_accounted assms] dirty_card_bound
  unfolding accounted_def by fastforce

fun written :: "action list \<Rightarrow> nat set" where
  "written [] = {}"
| "written (Read k offset # actions) = written actions"
| "written (Write k offset byte # actions) = insert k (written actions)"

lemma writable_advance:
  "writable (advance s a) k = (writable s k \<or> k \<in> written [a])"
  by (cases a; auto simp: prepare_def)

theorem written_exactly:
  "writable (execute s actions) k = (writable s k \<or> k \<in> written actions)"
proof (induction actions arbitrary: s)
  case Nil
  then show ?case by simp
next
  case (Cons a actions)
  show ?case by (cases a) (auto simp: Cons.IH prepare_def)
qed

theorem copies_exactly:
  assumes "list_all (valid slots bytes) actions"
  shows "frontier (execute (initial pages contents) actions) =
    pages + card ({..<slots} \<inter> written actions)"
proof -
  have "dirty slots (execute (initial pages contents) actions) =
      {..<slots} \<inter> written actions"
    by (auto simp: dirty_def written_exactly)
  then show ?thesis
    using execute_accounted[OF initial_accounted assms] by (simp add: accounted_def)
qed

end

A  => proof/ROOT +10 -0
@@ 1,10 @@
session Split = HOL +
  options [document = false]
  theories
    Model
    Isolation
    Execution
    Protocol
    Snapshot
    Reuse
    Export

A  => proof/Reuse.thy +209 -0
@@ 1,209 @@
theory Reuse
  imports Snapshot
begin

definition vacant :: "nat \<Rightarrow> pager \<Rightarrow> nat \<Rightarrow> bool" where
  "vacant slots s f = list_all (\<lambda>k. ptable s k \<noteq> f) [0..<slots]"

lemma vacant_iff [simp]:
  "vacant slots s f \<longleftrightarrow> (\<forall>k<slots. ptable s k \<noteq> f)"
  by (auto simp: vacant_def list_all_iff)

definition copy_frame :: "pager \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> pager" where
  "copy_frame s k f = s\<lparr>ptable := (ptable s)(k := f),
    physical := (physical s)(f := view s k), writable := (writable s)(k := True),
    frontier := max (frontier s) (Suc f)\<rparr>"

definition clear_slot :: "nat \<Rightarrow> pager \<Rightarrow> nat \<Rightarrow> pager" where
  "clear_slot pages s k = s\<lparr>ptable := (ptable s)(k := k mod pages),
    physical := (physical s)(ptable s k := (\<lambda>_. 0)), writable := (writable s)(k := False)\<rparr>"

definition pooled :: "nat \<Rightarrow> nat \<Rightarrow> pager \<Rightarrow> nat list \<Rightarrow> bool" where
  "pooled pages slots s available \<longleftrightarrow>
    invariant slots s \<and> private_frames pages slots s \<and> distinct available \<and>
    (\<forall>f\<in>set available. pages \<le> f \<and> f < frontier s \<and>
      vacant slots s f \<and> physical s f = (\<lambda>_. 0))"

definition reserve :: "nat \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> pager \<Rightarrow> nat list \<Rightarrow> nat \<Rightarrow> (pager \<times> nat list) option" where
  "reserve pages slots capacity s available k =
    (let f = (case available of [] \<Rightarrow> frontier s | x # xs \<Rightarrow> x) in
     if 0 < pages \<and> k < slots \<and> \<not> writable s k \<and> pages \<le> f \<and>
        f < capacity \<and> vacant slots s f
     then Some (copy_frame s k f, tl available) else None)"

definition reclaim :: "nat \<Rightarrow> nat \<Rightarrow> pager \<Rightarrow> nat list \<Rightarrow> nat \<Rightarrow> (pager \<times> nat list) option" where
  "reclaim pages slots s available k =
    (if 0 < pages \<and> k < slots \<and> writable s k \<and> pages \<le> ptable s k \<and>
      ptable s k \<notin> set available \<and>
      list_all (\<lambda>j. j = k \<or> ptable s j \<noteq> ptable s k) [0..<slots]
     then Some (clear_slot pages s k, ptable s k # available) else None)"

lemma initial_pooled:
  "0 < pages \<Longrightarrow> pooled pages slots (initial pages contents) []"
  by (simp add: pooled_def initial_invariant initial_private)

lemma copy_view:
  "vacant slots s f \<Longrightarrow> j < slots \<Longrightarrow> view (copy_frame s k f) j = view s j"
  by (auto simp: view_def copy_frame_def)

lemma copy_bounded:
  "bounded slots s \<Longrightarrow> bounded slots (copy_frame s k f)"
  by (auto simp: bounded_def copy_frame_def max_def)

lemma copy_exclusive:
  "exclusive slots s \<Longrightarrow> vacant slots s f \<Longrightarrow> exclusive slots (copy_frame s k f)"
  by (auto simp: exclusive_def copy_frame_def split: if_splits)

lemma copy_invariant:
  "invariant slots s \<Longrightarrow> vacant slots s f \<Longrightarrow> invariant slots (copy_frame s k f)"
  by (simp add: invariant_def copy_bounded copy_exclusive)

lemma copy_private:
  "private_frames pages slots s \<Longrightarrow> pages \<le> f \<Longrightarrow>
   private_frames pages slots (copy_frame s k f)"
  by (auto simp: private_frames_def copy_frame_def max_def)

lemma copy_original:
  "pages \<le> f \<Longrightarrow> original < pages \<Longrightarrow>
   physical (copy_frame s k f) original = physical s original"
  by (auto simp: copy_frame_def)

lemma clear_bounded:
  "0 < pages \<Longrightarrow> bounded slots s \<Longrightarrow> pages \<le> frontier s \<Longrightarrow>
   bounded slots (clear_slot pages s k)"
  by (auto simp: bounded_def clear_slot_def intro: less_le_trans[OF mod_less_divisor])

lemma clear_exclusive:
  "0 < pages \<Longrightarrow> exclusive slots s \<Longrightarrow> private_frames pages slots s \<Longrightarrow>
   exclusive slots (clear_slot pages s k)"
  by (auto simp: exclusive_def private_frames_def clear_slot_def split: if_splits; metis mod_less_divisor not_le)

lemma clear_private:
  "private_frames pages slots s \<Longrightarrow> private_frames pages slots (clear_slot pages s k)"
  by (auto simp: private_frames_def clear_slot_def)

lemma clear_invariant:
  "0 < pages \<Longrightarrow> invariant slots s \<Longrightarrow> private_frames pages slots s \<Longrightarrow>
   invariant slots (clear_slot pages s k)"
  by (metis invariant_def private_frames_def clear_bounded clear_exclusive)

lemma clear_vacant:
  "0 < pages \<Longrightarrow> exclusive slots s \<Longrightarrow> k < slots \<Longrightarrow>
   writable s k \<Longrightarrow> pages \<le> ptable s k \<Longrightarrow>
   vacant slots (clear_slot pages s k) (ptable s k)"
  by (auto simp: exclusive_def clear_slot_def; metis mod_less_divisor not_le)

lemma clear_other_view:
  "exclusive slots s \<Longrightarrow> k < slots \<Longrightarrow> writable s k \<Longrightarrow>
   j < slots \<Longrightarrow> j \<noteq> k \<Longrightarrow> view (clear_slot pages s k) j = view s j"
  by (auto simp: exclusive_def clear_slot_def view_def)

lemma clear_target_view:
  "0 < pages \<Longrightarrow> pages \<le> ptable s k \<Longrightarrow>
   view (clear_slot pages s k) k = physical s (k mod pages)"
  by (auto simp: clear_slot_def view_def; metis mod_less_divisor not_le)

lemma clear_original:
  "pages \<le> ptable s k \<Longrightarrow> f < pages \<Longrightarrow>
   physical (clear_slot pages s k) f = physical s f"
  by (auto simp: clear_slot_def)

theorem reclaim_zero:
  "reclaim pages slots s available k = Some (t, remaining) \<Longrightarrow>
   physical t (ptable s k) = (\<lambda>_. 0)"
  by (auto simp: reclaim_def clear_slot_def split: if_splits)

theorem reclaim_unmapped:
  assumes pool: "pooled pages slots s available"
    and step: "reclaim pages slots s available k = Some (t, remaining)"
  shows "vacant slots t (ptable s k)"
  using pool step
  by (auto simp: reclaim_def pooled_def invariant_def split: if_splits; metis clear_vacant vacant_iff)

theorem reclaim_views:
  assumes "pooled pages slots s available"
    "reclaim pages slots s available k = Some (t, remaining)" "j < slots"
  shows "view t j = (if j = k then physical s (k mod pages) else view s j)"
  using assms clear_other_view clear_target_view
  by (auto simp: pooled_def reclaim_def invariant_def split: if_splits)

theorem reclaim_pool:
  assumes pool: "pooled pages slots s available"
    and step: "reclaim pages slots s available k = Some (t, remaining)"
  shows "pooled pages slots t remaining"
proof -
  have positive: "0 < pages" and valid: "k < slots" and writable: "writable s k"
    and private_frame: "pages \<le> ptable s k" and fresh: "ptable s k \<notin> set available"
    and result: "t = clear_slot pages s k" "remaining = ptable s k # available"
    using step by (auto simp: reclaim_def split: if_splits)
  have inv: "invariant slots s" and owned: "private_frames pages slots s"
    using pool by (auto simp: pooled_def)
  have new_inv: "invariant slots t" and new_owned: "private_frames pages slots t"
    using clear_invariant[OF positive inv owned] clear_private[OF owned] result by auto
  have unused: "vacant slots t (ptable s k)"
    using reclaim_unmapped[OF pool step] .
  have clean: "physical t (ptable s k) = (\<lambda>_. 0)"
    using reclaim_zero[OF step] .
  show ?thesis
    using pool fresh result positive valid private_frame inv new_inv new_owned unused clean
    by (auto simp: pooled_def clear_slot_def bounded_def invariant_def split: if_splits; metis mod_less_divisor not_le)
qed

theorem reserve_views:
  "reserve pages slots capacity s available k = Some (t, remaining) \<Longrightarrow>
   j < slots \<Longrightarrow> view t j = view s j"
  by (auto simp: reserve_def Let_def split: list.splits if_splits intro: copy_view)

theorem reserve_pool:
  assumes pool: "pooled pages slots s available"
    and step: "reserve pages slots capacity s available k = Some (t, remaining)"
  shows "pooled pages slots t remaining"
proof (cases available)
  case Nil
  then have fresh: "vacant slots s (frontier s)"
    using pool by (auto simp: pooled_def invariant_def bounded_def)
  have owned: "pages \<le> frontier s" using pool by (auto simp: pooled_def private_frames_def)
  show ?thesis
    using step pool fresh owned Nil copy_invariant copy_private
    by (auto simp: reserve_def Let_def pooled_def split: if_splits)
next
  case (Cons f rest)
  have fresh: "vacant slots s f" and owned: "pages \<le> f"
    and distinct: "f \<notin> set rest"
    using pool Cons by (auto simp: pooled_def)
  have result: "t = copy_frame s k f" "remaining = rest"
    using step Cons by (auto simp: reserve_def Let_def split: if_splits)
  have inv: "invariant slots t" and own: "private_frames pages slots t"
    using pool result fresh owned copy_invariant copy_private by (auto simp: pooled_def)
  show ?thesis
    using pool Cons distinct result inv own
    by (auto simp: pooled_def copy_frame_def max_def split: if_splits)
qed

theorem reserve_original:
  "reserve pages slots capacity s available k = Some (t, remaining) \<Longrightarrow>
   f < pages \<Longrightarrow> physical t f = physical s f"
  by (auto simp: reserve_def Let_def split: list.splits if_splits intro: copy_original)

theorem reclaim_original:
  "reclaim pages slots s available k = Some (t, remaining) \<Longrightarrow>
   f < pages \<Longrightarrow> physical t f = physical s f"
  by (auto simp: reclaim_def split: if_splits intro: clear_original)

theorem pooled_unmapped:
  "pooled pages slots s available \<Longrightarrow> f \<in> set available \<Longrightarrow>
   j < slots \<Longrightarrow> ptable s j \<noteq> f"
  by (auto simp: pooled_def)

theorem pooled_clean:
  "pooled pages slots s available \<Longrightarrow> f \<in> set available \<Longrightarrow>
   physical s f offset = 0"
  by (auto simp: pooled_def)

theorem pooled_store:
  assumes "pooled pages slots s available" "k < slots" "writable s k"
  shows "pooled pages slots (store s k offset byte) available"
  using assms write_invariant[of slots s k offset byte] store_private[of pages slots s k offset byte]
  by (auto simp: pooled_def store_def prepare_def Let_def)

end

A  => proof/Snapshot.thy +186 -0
@@ 1,186 @@
theory Snapshot
  imports Protocol
begin

definition private_frames :: "nat \<Rightarrow> nat \<Rightarrow> pager \<Rightarrow> bool" where
  "private_frames pages slots s \<longleftrightarrow> pages \<le> frontier s \<and>
    (\<forall>k<slots. writable s k \<longrightarrow> pages \<le> ptable s k)"

lemma initial_private: "private_frames pages slots (initial pages contents)"
  by (simp add: private_frames_def)

lemma prepare_private:
  "private_frames pages slots s \<Longrightarrow> private_frames pages slots (prepare s k)"
  by (auto simp: private_frames_def prepare_def split: if_splits)

lemma prepared_frame_private:
  "private_frames pages slots s \<Longrightarrow> k < slots \<Longrightarrow>
   pages \<le> ptable (prepare s k) k"
  by (auto simp: private_frames_def prepare_def)

lemma store_private:
  "private_frames pages slots s \<Longrightarrow> private_frames pages slots (store s k offset byte)"
  using prepare_private[of pages slots s k]
  by (simp add: private_frames_def)

lemma prepare_original:
  "private_frames pages slots s \<Longrightarrow> f < pages \<Longrightarrow>
   physical (prepare s k) f = physical s f"
  by (auto simp: private_frames_def prepare_def detach_def)

theorem store_original:
  assumes owned: "private_frames pages slots s" and valid: "k < slots" and original: "f < pages"
  shows "physical (store s k offset byte) f = physical s f"
proof -
  have distinct: "f \<noteq> ptable (prepare s k) k"
    using prepared_frame_private[OF owned valid] original by arith
  have preserved: "physical (prepare s k) f = physical s f"
    using prepare_original[OF owned original] .
  show ?thesis
    using distinct preserved by (simp add: store_def Let_def)
qed

lemma advance_private:
  "private_frames pages slots s \<Longrightarrow> private_frames pages slots (advance s a)"
  by (cases a; simp add: store_private)

lemma advance_original:
  "private_frames pages slots s \<Longrightarrow> valid slots bytes a \<Longrightarrow> f < pages \<Longrightarrow>
   physical (advance s a) f = physical s f"
  by (cases a; auto intro: store_original)

theorem execute_original:
  "private_frames pages slots s \<Longrightarrow> list_all (valid slots bytes) actions \<Longrightarrow>
   f < pages \<Longrightarrow> physical (execute s actions) f = physical s f"
proof (induction actions arbitrary: s)
  case Nil
  then show ?case by simp
next
  case (Cons a actions)
  have state: "private_frames pages slots (advance s a)"
    using Cons.prems by (simp add: advance_private)
  have tail: "physical (execute (advance s a) actions) f = physical (advance s a) f"
    using Cons.IH[OF state] Cons.prems by simp
  show ?case
    using tail advance_original Cons.prems by auto
qed

corollary original_preserved:
  "list_all (valid slots bytes) actions \<Longrightarrow> f < pages \<Longrightarrow>
   physical (execute (initial pages contents) actions) f = contents f"
  using execute_original[OF initial_private] by simp

definition promote :: "nat \<Rightarrow> nat \<Rightarrow> nat \<Rightarrow> pager \<Rightarrow> pager" where
  "promote pages slots source s =
    s\<lparr>ptable := (\<lambda>k. if slots \<le> k \<and> k < slots + pages
      then ptable s (source + (k - slots)) else ptable s k),
      writable := (\<lambda>k. writable s k \<and> \<not> (source \<le> k \<and> k < source + pages)
        \<and> \<not> (slots \<le> k \<and> k < slots + pages))\<rparr>"

lemma promotion_live_view:
  "k < slots \<Longrightarrow> view (promote pages slots source s) k = view s k"
  by (simp add: promote_def view_def)

lemma promotion_baseline_view:
  "p < pages \<Longrightarrow>
   view (promote pages slots source s) (slots + p) = view s (source + p)"
  by (simp add: promote_def view_def)

lemma promotion_baseline_readonly:
  "p < pages \<Longrightarrow> \<not> writable (promote pages slots source s) (slots + p)"
  by (simp add: promote_def)

lemma promotion_source_readonly:
  "p < pages \<Longrightarrow> \<not> writable (promote pages slots source s) (source + p)"
  by (simp add: promote_def)

lemma promotion_bounded:
  assumes bounds: "bounded (slots + pages) s" and source: "source + pages \<le> slots"
  shows "bounded (slots + pages) (promote pages slots source s)"
proof (unfold bounded_def, intro allI impI)
  fix k
  assume live: "k < slots + pages"
  show "ptable (promote pages slots source s) k < frontier (promote pages slots source s)"
  proof (cases "slots \<le> k")
    case True
    have selected: "source + (k - slots) < slots + pages"
      using source live True by arith
    show ?thesis
      using bounded_frame[OF bounds selected] live True by (simp add: promote_def)
  next
    case False
    show ?thesis
      using bounded_frame[OF bounds live] False by (simp add: promote_def)
  qed
qed

lemma promotion_exclusive:
  assumes owned: "exclusive (slots + pages) s" and source: "source + pages \<le> slots"
  shows "exclusive (slots + pages) (promote pages slots source s)"
proof (unfold exclusive_def, intro allI impI)
  fix k j
  assume live: "k < slots + pages" "j < slots + pages"
    and access: "writable (promote pages slots source s) k"
    and sharing: "ptable (promote pages slots source s) j = ptable (promote pages slots source s) k"
  have writable: "writable s k" and outside: "\<not> (source \<le> k \<and> k < source + pages)"
    and old: "ptable (promote pages slots source s) k = ptable s k"
    using access by (auto simp: promote_def)
  show "j = k"
  proof (cases "slots \<le> j")
    case True
    have selected: "source + (j - slots) < slots + pages"
      and inside: "source \<le> source + (j - slots) \<and> source + (j - slots) < source + pages"
      using source live True by arith+
    have frame: "ptable s (source + (j - slots)) = ptable s k"
      using sharing old live True by (simp add: promote_def)
    have "source + (j - slots) = k"
      using writable_unique[OF owned live(1) selected writable frame] .
    then show ?thesis using inside outside by simp
  next
    case False
    have "ptable s j = ptable s k"
      using sharing old False by (simp add: promote_def)
    then show ?thesis
      using writable_unique[OF owned live writable] by simp
  qed
qed

theorem promotion_invariant:
  "invariant (slots + pages) s \<Longrightarrow> source + pages \<le> slots \<Longrightarrow>
   invariant (slots + pages) (promote pages slots source s)"
  by (simp add: invariant_def promotion_bounded promotion_exclusive)

theorem promotion_preserves_unselected:
  assumes "k < slots" "k < source \<or> source + pages \<le> k"
  shows "view (promote pages slots source s) k = view s k \<and>
    writable (promote pages slots source s) k = writable s k"
  using assms by (auto simp: promote_def view_def)

theorem promotion_store_preserves_unselected:
  assumes inv: "invariant (slots + pages) s" and source: "source + pages \<le> slots"
    and page: "p < pages" and live: "k < slots"
    and unselected: "k < source \<or> source + pages \<le> k"
  shows "view (store (promote pages slots source s) (source + p) offset byte) k = view s k"
proof -
  have selected: "source + p < slots + pages" and other: "k \<noteq> source + p"
    using source page live unselected by arith+
  show ?thesis
    using write_other[OF promotion_invariant[OF inv source] selected _ other] live
    by (simp add: promotion_live_view)
qed

theorem promotion_store_preserves_baseline:
  assumes inv: "invariant (slots + pages) s" and source: "source + pages \<le> slots"
    and pages: "p < pages" "q < pages"
  shows "view (store (promote pages slots source s) (source + p) offset byte) (slots + q) =
    view s (source + q)"
proof -
  have selected: "source + p < slots + pages" and baseline: "slots + q < slots + pages"
    and other: "slots + q \<noteq> source + p"
    using source pages by arith+
  show ?thesis
    using write_other[OF promotion_invariant[OF inv source] selected baseline other]
    by (simp add: promotion_baseline_view pages)
qed

end

A  => tools/microkit.patch +73 -0
@@ 1,73 @@
--- a/tool/microkit/src/sdf/cspace.rs
+++ b/tool/microkit/src/sdf/cspace.rs
@@ -17,6 +17,7 @@
     Tcb,
     Sc,
     VSpace,
+    Frame,
 }
 
 #[derive(Debug, PartialEq, Eq)]
@@ -45,9 +46,9 @@
         // At the moment the four cap maps we support all have the 'pd' element,
         // so we can include it here. When that stops being the case we will
         // have to rework this a bit.
-        check_attributes(xml_sdf, node, &["slot", "pd"])?;
-
-        let pd = Rc::from(checked_lookup(xml_sdf, node, "pd")?);
+        let source = if cap_type == CapMapType::Frame { "mr" } else { "pd" };
+        check_attributes(xml_sdf, node, &["slot", source])?;
+        let pd = Rc::from(checked_lookup(xml_sdf, node, source)?);
 
         let slot: u64 = sdf_parse_required_attribute(xml_sdf, node, "slot")?;
 
@@ -91,6 +92,7 @@
                 "cap_tcb" => CapMap::from_xml(CapMapType::Tcb, xml_sdf, &*child)?,
                 "cap_sc" => CapMap::from_xml(CapMapType::Sc, xml_sdf, &*child)?,
                 "cap_vspace" => CapMap::from_xml(CapMapType::VSpace, xml_sdf, &*child)?,
+                "cap_frame" => CapMap::from_xml(CapMapType::Frame, xml_sdf, &*child)?,
                 child_name => {
                     let location = loc_string(xml_sdf, child.range().start);
                     if let Some(type_name) = child_name.strip_prefix("cap_") {
--- a/tool/microkit/src/sdf.rs
+++ b/tool/microkit/src/sdf.rs
@@ -310,7 +310,11 @@
 
     for pd in pds.values() {
         for cap_map in pd.cap_maps.iter() {
-            if !pds.contains_key(&cap_map.pd) {
+            if cap_map.cap_type == CapMapType::Frame {
+                if !mrs.iter().any(|mr| mr.name.as_str() == cap_map.pd.as_ref()) {
+                    return Err(format!("Unknown frame region {}", cap_map.pd));
+                }
+            } else if !pds.contains_key(&cap_map.pd) {
                 return Err(format!(
                     "Error: unknown PD name '{}': {}",
                     cap_map.pd,
--- a/tool/microkit/src/capdl/builder.rs
+++ b/tool/microkit/src/capdl/builder.rs
@@ -1332,12 +1332,23 @@
         for cap_map in pd.cap_maps.iter() {
             // TODO: Once we add more CapMap options, they might not all have
             // the pd_name. But for now, they do.
-            let pd_src_shadow_cspace = &pd_shadow_cspaces[&cap_map.pd];
+            let pd_src_shadow_cspace = if cap_map.cap_type == CapMapType::Frame {
+                &pd_shadow_cspaces[&pd.name]
+            } else {
+                &pd_shadow_cspaces[&cap_map.pd]
+            };
 
             let cap_map_obj = match cap_map.cap_type {
                 CapMapType::Tcb => capdl_util_make_tcb_cap(pd_src_shadow_cspace.tcb),
                 CapMapType::Sc => capdl_util_make_sc_cap(pd_src_shadow_cspace.sched_context),
                 CapMapType::VSpace => capdl_util_make_page_table_cap(pd_src_shadow_cspace.vspace),
+                CapMapType::Frame => {
+                    let frames = &mr_name_to_frames[cap_map.pd.as_ref()];
+                    if frames.len() != 1 {
+                        return Err(format!("Frame region {} must contain one page", cap_map.pd));
+                    }
+                    capdl_util_make_frame_cap(frames[0], true, true, false, true)
+                },
             };
 
             // Map this into the destination pd's cspace and the specified slot.

A  => tools/setup.sh +17 -0
@@ 1,17 @@
#!/bin/sh
set -eu
cache="${SPLIT_CACHE:-$HOME/.cache/split}"
mkdir -p "$cache"
if [ ! -f "$cache/sdk.tar.gz" ]; then
  curl -fL --retry 2 https://github.com/seL4/microkit/releases/download/2.3.1/microkit-sdk-2.3.1-linux-x86-64.tar.gz -o "$cache/sdk.tar.gz"
fi
if [ ! -f "$cache/source.tar.gz" ]; then
  curl -fL --retry 2 https://codeload.github.com/seL4/microkit/tar.gz/refs/tags/2.3.1 -o "$cache/source.tar.gz"
fi
printf '%s  %s\n' b1b285b8785db7557eba6d0665dd4fbf2f265ea5c0b39d6f91ae3dcc43d2c424 "$cache/sdk.tar.gz" 67ae034628465c35169c398dd7d9ea61dbe8bd93c7a9adb05b9bb31bae25c22f "$cache/source.tar.gz" | sha256sum -c -
if [ ! -d "$cache/microkit-sdk-2.3.1" ]; then
  tar -xzf "$cache/sdk.tar.gz" -C "$cache"
fi
tar -xzf "$cache/source.tar.gz" -C "$cache"
patch -s -p1 -d "$cache/microkit-2.3.1" < "$(dirname "$0")/microkit.patch"
cargo build --release --locked -p microkit-tool --manifest-path "$cache/microkit-2.3.1/Cargo.toml"