Baleen

A static-partitioning separation kernel for AArch64/EL2, built so that its claims can be checked rather than believed.

View the Project on GitHub via-balaena/baleen

Architecture Audit #6 — the CoW disk keeps writes off the template

Audit #5 established that a virtio backend’s every touch of guest memory is a proven grant. Audit #6 asks the storage question M5 Arc 4 raises: when a virtio-blk backend serves reads and writes against a shared read-only template and per-tenant copy-on-write overlays, does the CoW discipline actually keep a guest’s write off the template and off a peer’s view? Two orthogonal surfaces are audited per dimension: the grant surface (guest-memory access, an Arc-5 regression) and the new CoW surface (the disk). The audited code is hv-metal/src/blk.rs (the BlkDisk store + device model) and hv-metal/src/guest.rs (the block backend + witnesses). hv-core/hv-hal are untouched (this refines).

The charter

(A) template-immutability — no block request path may mutate the shared template after it is seeded; a guest’s write lands only in that guest’s overlay. (B) overlay-isolation — a tenant’s read returns its own overlay (for sectors it has written) or the shared template (for sectors it has not); it never returns another tenant’s overlay, and two tenants’ overlays are distinct storage. (C) the grant surface holds across a descriptor chain — every backend touch of guest memory across the { header, data, status } chain is grant-authorized; an un-granted data buffer is refused.

The refinement — where each property lives

CoW store (BlkDisk, hv-metal/src/blk.rs). Storage is template: [[u8;512]; DISK_SECTORS] plus overlay: [[[u8;512]; DISK_SECTORS]; N_TENANTS] and dirty: [[bool; DISK_SECTORS]; N_TENANTS].

Property (A) holds by construction: no method other than seed_template writes template, and seed_template is called exactly once (in begin_virtio_blk_phase6, before either guest runs; the disk is a persistent static, so it is not re-seeded between tenant 0’s write and tenant 1’s read). Property (B) holds by construction: overlay and dirty are indexed by tenant first, so one tenant’s writes are invisible to another’s reads, and overlay[0] / overlay[1] are distinct storage.

Grant surface (process_blk_request, hv-metal/src/guest.rs). The backend walks the chain head → next → next (each descriptor read via the grant-checked backend_read_desc), then:

Every access reuses Arc-5’s backend_authorize (frame recovery via gpa_to_mfn, single-frame bound, hv.grant().authorizes(guest, dom0, mfn, writable)), so the grant refinement is Audit #5’s, unchanged; Arc 4 adds only the chain walk and the device-writable direction (an RW grant for a read’s buffer).

The seam — the disk is not guest memory

The template and overlays live in a backend-owned static (BLK_DISK), at host PAs that no guest’s Stage-2 ever maps — build_stage2_from_p2m maps only the guest’s own model frames into the guest’s IPA window, and the disk static is not among them. So a guest cannot reach the disk directly; it reaches it only through the grant-mediated DMA descriptors the backend services. This is the same distinct-PA-⇒-disjoint-by-construction argument Audit #4 used for two domains’ Stage-2, applied at the device layer. Correctly, the disk is not grant-gated (it is not guest memory); only the guest-buffer DMA is (it is).

Verdict per dimension

dimension forbidden mechanism witness verdict
(A) template-immutability a write mutates the template write touches only overlay; seed_template is the sole template writer, called once HV-side: after tenant 0’s write, template_sector(0) == seed AND overlay_sector(0,0) == poison (BLK_WRITE_ISOLATED_OK) ✅
(B) overlay-isolation tenant 1 sees tenant 0’s write per-tenant overlay/dirty rows; clean sector falls through to template tenant 1’s round-trip read == template (BLK_READ_TEMPLATE_OK[1]); overlay_ptr(0,0) != overlay_ptr(1,0); POISON forbidden-marker absent ✅
(C) grant across the chain an un-granted buffer is served every chain access via backend_authorize → hv.grant().authorizes un-granted data buffer (Mfn 5) refused (BLK_UNGRANTED_REFUSED); no bytes cross ✅

Verdict: SOUND, no defect. Both isolation properties fall out of the by-construction structure of the CoW store (per-tenant rows, template-write-once) and the reused proven grant; the witnesses are HV-side and un-forgeable (the immutability check reads the backend’s real storage, not a re-seeded copy), and the poison is a forbidden-marker so a leak cannot be silently green.

Review pass — three spec-blind auditors + empirical mutation testing

Three auditors reviewed the committed implementation on orthogonal axes, each spec-blind (told the axis, not the expected conclusion). All three: SOUND, no defect.

Empirical mutation testing (each perturbation reverted; each self-test FAILED as required)

mutation breaks caught by
write-reaches-template — BlkDisk::write mutates template[sector] template-immutability immutability marker absent, write_isolated_ok=false, tenant-1 read mismatch, POISON printed (forbidden fired)
bypass-CoW / grant-bypass — skip backend_authorize on the data buffer the grant surface Mfn 5 refusal marker absent, refused_ok=false, POISON leaked
two-overlays-alias — key read on a fixed tenant 0 overlay-isolation t1_read_ok=false, PASSED marker absent, POISON leaked

Every mutation is caught through multiple independent channels, and the POISON forbidden-marker is load-bearing for all three leak classes — a broken property cannot go silently green.

Below-bar fixes folded into the review-pass commit

Review-pass verdict: SOUND, no soundness defect. Arc 4 is diamond-grade.