~james/split

test(pager): check recycling preserves isolation after remapping

commit
36fd34c5221ae47ee29e89ecfbab8662ab4fd6f5 browse tree
parent
be965f3facbc207656757e5737b829510b66ca9c
author
james <j@cpl0.zip>
signature
Verified ~james signed with a key registered by ~james
date
2026-08-20 20:27 UTC

5 files changed, +121 −1

Makefile modified

@@ -6,7 +6,7 @@ LD := aarch64-linux-gnu-ld
66 CFLAGS := -std=gnu11 -O2 -g -Wall -Wextra -Werror -Wno-unused-function -ffreestanding -nostdlib -mstrict-align -I$(BOARD)/include
77 QEMU := qemu-system-aarch64 -machine virt,virtualization=on -cpu cortex-a53 -m 2G -nographic -monitor none -serial stdio
88
9-.PHONY: all proof work demo run clean
9+.PHONY: all proof model work demo check run clean
1010 all: build/loader.img
1111
1212 build:
@@ -47,10 +47,16 @@ build/check: build/split.ml check.ml Makefile
4747 ocamlopt -w +a-4-42-70 -I +unix -I build -c -o build/check.cmx check.ml
4848 ocamlopt -I +unix -I build unix.cmxa build/split.cmx build/check.cmx -o $@
4949
50+model: build/check
51+ build/check --model
52+
5053 work: all build/check
5154 build/check work document
5255
5356 demo run: work
5457
58+check: all build/unsafe.img model
59+ build/check --test
60+
5561 clean:
5662 rm -rf build

README.md added

@@ -0,0 +1,3 @@
1+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.
2+
3+[![Watch the seL4 showcase](media/poster.png)](media/split.mp4)

check.ml modified

@@ -18,6 +18,29 @@ let require ok message = if not ok then failwith message
1818 let read s k offset = integer (S.read_byte s (nat k) (nat offset))
1919 let step capacity s a = S.checked (nat 8) (nat 4096) (nat capacity) s a
2020
21+let selftest () =
22+ let state = ref S.boot in
23+ let reference = Array.init 8 (fun k -> Array.init 4096 (fun b -> ((k mod 4) * 17 + b) mod 256)) in
24+ let random = Random.State.make [|2026; 4; 8|] in
25+ for i = 0 to 1023 do
26+ let k = Random.State.int random 8 in
27+ let offset = if i mod 127 = 0 then 4095 else Random.State.int random 128 in
28+ let byte = Random.State.int random 256 in
29+ let action = S.Write (nat k, nat offset, nat byte) in
30+ state := Option.get (step 12 !state action);
31+ reference.(k).(offset) <- byte;
32+ for j = 0 to 7 do
33+ require (read !state j offset = reference.(j).(offset)) "private memory refinement"
34+ done
35+ done;
36+ require (integer (S.frontier !state) = 12) "one copy per written page";
37+ List.iter (fun action -> require (step 12 !state action = None) "invalid action accepted")
38+ [S.Write (nat 8, nat 0, nat 1); S.Write (nat 0, nat 4096, nat 1);
39+ S.Write (nat 0, nat 0, nat 256); S.Read (nat 8, nat 0)];
40+ require (step 4 S.boot (S.Write (nat 0, nat 0, nat 1)) = None) "exhaustion accepted";
41+ require (Option.is_some (step 12 !state (S.Write (nat 0, nat 0, nat 1)))) "private write rejected";
42+ print_endline "checked 1024 model writes with boundary and exhaustion cases"
43+
2144 type document = { fields : string array; values : string array }
2245
2346 type guest = {
@@ -360,10 +383,98 @@ let work image path =
360383 with Invalid_argument message | Sys_error message -> print_endline (String.capitalize_ascii message)
361384 done)
362385
386+let exercise () =
387+ let original = load "document" in
388+ let g = launch "build/loader.img" original in
389+ Fun.protect ~finally:(fun () -> close g) (fun () ->
390+ let physicals () =
391+ Array.of_list (List.map (fun line -> match String.split_on_char ' ' line with
392+ | ["@map"; _; _; address; rights] -> int_of_string address, rights
393+ | _ -> failwith "invalid mapping record") (request g "P"))
394+ in
395+ let initial_frames = physicals () in
396+ require (Array.length initial_frames = 8) "mapping count";
397+ for p = 0 to 3 do
398+ require (initial_frames.(p) = initial_frames.(p + 4) && snd initial_frames.(p) = "0")
399+ "initial physical sharing"
400+ done;
401+ require (query g 0 = original.values && query g 1 = original.values && query g 2 = original.values) "initial document";
402+ edit g 0 3 "x";
403+ require ((query g 0).(3) = "x") "document edit";
404+ let copied_frames = physicals () in
405+ require (fst copied_frames.(3) <> fst initial_frames.(3) && snd copied_frames.(3) = "1"
406+ && copied_frames.(7) = initial_frames.(7)) "physical copy isolation";
407+ peek g 0 3;
408+ peek g 1 3;
409+ peek g 2 3;
410+ require (query g 1 = original.values && query g 2 = original.values) "snapshot isolation";
411+ require (integer (S.frontier g.state) = 5) "first write copy";
412+ edit g 0 3 "";
413+ require ((query g 0).(3) = "") "empty field";
414+ require (integer (S.frontier g.state) = 5) "repeated edit allocated";
415+ require ((physicals ()).(3) = copied_frames.(3)) "private physical frame changed";
416+ edit g 0 3 (String.make 255 'x');
417+ require (String.length (query g 0).(3) = 255) "maximum field";
418+ let rejected = try edit g 0 0 (String.make 256 'x'); false with Invalid_argument _ -> true in
419+ require rejected "oversized field accepted";
420+ let before = query g 0 in
421+ let rejected = try ignore (request g ("E 0 0 " ^ String.make 256 'x')); false
422+ with Failure message -> message = "field too long" in
423+ require (rejected && query g 0 = before) "guest edit bounds";
424+ for c = 0 to 1 do
425+ for p = 0 to 3 do edit g c p (Printf.sprintf "snapshot %d field %d" c p) done
426+ done;
427+ require (integer (S.frontier g.state) = 12) "copy pool accounting";
428+ let final_frames = physicals () in
429+ for k = 0 to 7 do
430+ for j = k + 1 to 7 do
431+ require (fst final_frames.(k) <> fst final_frames.(j)) "private physical alias"
432+ done
433+ done;
434+ edit g 1 0 "updated after pool exhausted";
435+ require ((query g 1).(0) = "updated after pool exhausted") "private edit after exhaustion";
436+ require (query g 2 = original.values) "original changed";
437+ let values = query g 0 in
438+ save "build/export.txt" {original with values};
439+ require ((load "build/export.txt").values = values) "saved document differs";
440+ let rejected = try edit g 2 0 "changed"; false with Invalid_argument _ -> true in
441+ require rejected "original edit accepted";
442+ let survivor = query g 1 in
443+ discard g 0;
444+ require (List.length g.available = 4 && query g 1 = survivor) "discard isolation";
445+ let reset_frames = physicals () in
446+ for p = 0 to 3 do require (reset_frames.(p) = initial_frames.(p)) "discard mapping restore" done;
447+ pool g;
448+ for cycle = 0 to 15 do
449+ for p = 0 to 3 do edit g 0 p (Printf.sprintf "cycle %d field %d" cycle p) done;
450+ require (g.available = [] && integer (S.frontier g.state) = 12) "recycling grew pool";
451+ require (query g 1 = survivor && query g 2 = original.values) "recycling changed survivor";
452+ discard g 0
453+ done;
454+ discard g 1;
455+ require (List.length g.available = 8) "complete pool return";
456+ let recycled = fst final_frames.(7) in
457+ edit g 0 0 "draft now owns review frame";
458+ require (fst (physicals ()).(0) = recycled) "cross snapshot physical reuse";
459+ edit g 1 3 "review writes its old virtual address";
460+ require (fst (physicals ()).(7) <> recycled && (query g 0).(0) = "draft now owns review frame")
461+ "stale client mapping survived discard";
462+ discard g 0;
463+ discard g 1;
464+ pool g;
465+ require (query g 2 = original.values && List.length g.available = 8) "reclamation final state");
466+ let g = launch "build/unsafe.img" original in
467+ let detected = Fun.protect ~finally:(fun () -> close g) (fun () ->
468+ try edit g 0 3 "changed"; false with Failure message -> message = "write reached a shared page") in
469+ require detected "writable alias was not detected";
470+ print_endline "exercised edits, exports, full pool recycling and stale mapping isolation"
471+
363472 let () =
364473 Sys.catch_break true;
365474 try
366475 match Array.to_list Sys.argv with
476+ | [_; "--model"] -> selftest ()
477+ | [_; "--test"] -> exercise ()
367478 | [_] | [_; "work"] -> work "build/loader.img" "document"
368479 | [_; "work"; path] -> work "build/loader.img" path
369480 | _ -> invalid_arg "usage: check work DOCUMENT"

media/poster.png added

Binary file.

media/split.mp4 added

Binary file.