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 #2 — the p2m → Stage-2 refinement

The centerpiece of M4 Arc 5, and the first point where the ∀-N model proofs make contact with real hardware isolation. Audit #1 asked whether the metal honors the southbound trait *fence; Audit #2 asks the isolation question: when the metal translates the proven p2m into real AArch64 Stage-2 page tables, does the emitted table deny exactly what the model forbids — no more, no less — for CPU data access? The refinement is realized in hv-metal/src/stage2.rs (the builder + the realized GuestMemory) and exercised by the negative-isolation test in hv-metal/src/guest.rs. QEMU is a sound oracle for exactly this — Stage-2 translation + fault semantics for CPU-initiated accesses (docs/QEMU-AND-METAL.md: the single most valuable test QEMU can run).*

The charter — no more, no less

The p2m proofs (Tiers A–D) say the model enforces isolation: a domain cannot reach another domain’s memory without a grant. That is a claim about the model. On the metal, a guest’s reachable memory is precisely its Stage-2 mappings. So two properties must both hold for the proof to mean anything about running code:

  1. No less (no isolation hole — not under-restrictive). Every access the model forbids must fault. If a frame the guest may not reach were mapped, the hardware would silently permit what the proof forbids — the proof would be a fiction at the seam.
  2. No more (no liveness hole — not over-restrictive). Every access the model permits must succeed. If a frame the guest is authorized to reach were left unmapped, the guest could not run its own authorized workload — a different, equally real bug (the model says “allowed”; the metal says “fault”).

The diamond is the positive + negative pair: the table permits exactly what the model authorizes and denies exactly what it does not.

The refinement relation

The p2m models reachability + permission: a domain G may access machine frame m iff m is a leaf-mapped child in a page table G owns — freely for its own frames, and for a foreign frame only because hv-core’s p2m_link seam already required a matching grant (p2m::System::link_edges surfaces every such edge, with its writable/leaf bits). The Stage-2 image is a pure function of exactly that relation (stage2::build_stage2_from_p2m):

Stage-2(G) maps IPA(m) → PA(m) at S2AP π ⟺ m is a leaf child of a table G owns, at permission π. A writable leaf → S2AP=RW; a read-only leaf → S2AP=RO; a foreign leaf is present only because a grant authorized it (the seam refused it otherwise); a frame that is neither → no descriptor → the access faults.

The metal’s Mfn → IPA convention (named). The model’s Mfn is an abstract frame index with no linear address, and its page-table slots are not linear addresses (the model gives them no address arithmetic — TABLE_SLOTS = 8, abstract). So the refinement is over the model’s reachability + permission relation (which frames, at what access), and the metal assigns each frame a canonical guest IPA (frame_ipa(m) = DATA_IPA_BASE + m·4 KiB) backed at a real host PA (frame_pa(m)), with IPA ≠ PA so the emitted table performs a genuine translation rather than an identity pass-through. There is nothing in the model’s slot indices to “preserve”; the honest object of the refinement is the frame-reachability relation, and that is what is realized and audited.

Honest scope — what the Stage-2 refinement does and does not cover. The model’s leaves are a guest’s Stage-1 page-table entries in the paravirtual worldview. On this HVM/Stage-2 metal we reinterpret the same authorize/deny relation as Stage-2 reachability, because the proven property is layer-agnostic (reachability + permission) and Stage-2 is how the metal enforces it for an unmodified guest. Consequences, stated plainly:

The test configuration (driven through the real model)

setup_model drives the proven Hypervisor::dispatch into this configuration — so the Stage-2 the metal emits is a translation of state the proven transitions produced, not a hand-built table:

Per-dimension verdict — model vs. emitted table vs. QEMU

Each row: what the model authorizes, what the emitted descriptor is, and what QEMU did when the guest probed it. The QEMU column is the running third oracle; the fault values are decoded from ESR_EL2/HPFAR_EL2 and printed only when they match the expected class (a witness produced by the mechanism).

dimension frame / IPA model says emitted descriptor QEMU (running) verdict
write (own) Mfn 2 / 0x8000_2000 reachable RW (writable leaf) S2AP=RW, XN, Normal-WB guest writes 0xBEEF, reads it back; HV confirms via GuestMemory ✅ permits
read (own, RO) Mfn 3 / 0x8000_3000 reachable RO (read-only leaf) S2AP=RO, XN guest reads the HV-seeded 0x5EED ✅ permits
foreign (granted) Mfn 4 / 0x8000_4000 reachable RW (RW grant) S2AP=RW, XN guest writes 0xF00D; HV confirms via GuestMemory ✅ permits
write → RO Mfn 3 / 0x8000_3000 denied (read-only) S2AP=RO guest write faults: EC=0x24, permission fault DFSC=0x0F, WnR=1 ✅ denies
foreign (un-granted) Mfn 5 / 0x8000_5000 denied (no grant) no descriptor guest read faults: EC=0x24, translation fault DFSC=0x07 ✅ denies
unmapped Mfn 6 / 0x8000_6000 denied (not owned/mapped) no descriptor guest read faults: EC=0x24, translation fault DFSC=0x07 ✅ denies
write-xor-pagetable Mfn 1 / 0x8000_1000 denied (typed PageTable, not a leaf) no descriptor guest read faults: EC=0x24, translation fault DFSC=0x07 ✅ denies
execute (XN) data frames data, not code XN=1 (bit 54) on every data leaf audited by construction (runtime fetch-fault deferred) ⏳ by construction
superpage — leaf above L1 = coarser reach block path exercised (guest-image 2 MiB block) model-driven superpage deferred ⏳ by construction

The positive rows are un-forgeable: ro=0x5EED is a value the guest never holds as an immediate (it appears nowhere in the guest program) — the guest can only echo it by reading the frame the hypervisor seeded through the fence, so it proves the read-only Stage-2 mapping resolves to the right machine frame. rw/fgrant are cross-checked by the hypervisor reading the guest’s writes back via the realized GuestMemory, proving the authorized store landed at the frame the model authorized (IPA → PA correctness).

The “no more, no less” analysis

Encoding convergence — the AArch64 Stage-2 values

The descriptor and fault encodings in stage2.rs were derived from the Arm ARM and independently re-derived by a spec-blind auditor (no sight of the code), and they agree on every value:

The two encoding caveats the auditor flagged are satisfied by our configuration, so the classic (non-optional-feature) format is in force: HCR_EL2.FWB = 0 (we set only RW+VM, bit 46 clear — so MemAttr=0b1111 means Normal-WB) and VTCR_EL2.DS = 0 (0x8002_3559, bit 32 clear — so the non-LPA2 descriptor format holds). QEMU’s observed fault codes — permission 0x0F (L3) and translation 0x07 (L3) — match the auditor’s from-spec prediction exactly.

Method — three-way convergence

Per the arc-2–4 discipline (design-lessons #23–#25), the isolation claims were established three independent ways, and they agree:

  1. Spec-derived code — stage2.rs encodes the AArch64 Stage-2 descriptor/fault fields from the Arm ARM; build_stage2_from_p2m emits from the model’s link_edges.
  2. Two spec-blind auditors — (a) an independent re-derivation of the descriptor/fault encodings from the Arm ARM (converged, above); (b) an independent re-derivation of the model→reachability refinement — what the Stage-2 should permit/deny for this configuration, from p2m.rs alone, blind to stage2.rs/guest.rs. It reproduced the whole matrix and its two sharp discriminators — Mfn 5 read → translation fault (not success, not permission) and Mfn 3 write → permission fault (not translation) — which are exactly the fault classes QEMU produced (0x07 vs 0x0F). It also independently named Mfn 1 (G’s page-table frame) as the case that must not be mapped (added as the write-xor-pagetable probe), and flagged three generalization caveats carried below as F4.
  3. The running emulator — QEMU booted the image and produced the full authorize/deny matrix: the authorized accesses succeeded (with un-forgeable readbacks), and the three denials faulted with the exact expected class and faulting IPA. QEMU is architecturally faithful about Stage-2 translation + fault semantics for CPU-initiated accesses, so it is a sound third oracle (docs/QEMU-AND-METAL.md).

Findings

None of these is a soundness defect in the refinement as realized. That the findings are deferred granularity/witness/generalization dimensions plus one pre-existing carried-forward gap is itself the result: the model→page-table refinement denies exactly what the model forbids, and permits exactly what it authorizes, across read / write / foreign(granted) / unmapped / write-xor-pagetable, witnessed on every boot.

Diamond review pass (post-merge)

After the arc landed, a dedicated review pass re-examined the implementation from three adversarial angles — four independent perspectives + empirical mutation testing — in the arc-4 rhythm (design-lesson #26). Verdict: SOUND, no defect.

Verdict

The emitted Stage-2 table realizes the proven p2m for CPU data access — it denies exactly what the model forbids and permits exactly what it authorizes, across read / write / foreign(granted) / unmapped, three-way-converged and witnessed on every boot; a four-perspective diamond review pass plus mutation testing found no soundness defect. GuestMemory is now realized on ARM (the assumption Audit #1 named for this arc, closed), behind the neutral fence — no descriptor bit leaks into a signature. Superpage and execute-never runtime witnesses are named-deferred; the crate-wide EL2-MMU gap is untouched and carried forward. No soundness defect. Arc 5 refines the proof — the first isolation content on the metal — and is QEMU-sound for CPU-initiated Stage-2 faults, the honest scope per the ledger.

The M4 HAL ledger — hv-hal traits, Arc 5 status

Continuing the M4 ledger (docs/ARC-4-TRAP-AND-SERVICE.md). No trait signature changed — the fence stays architecture-neutral (Audit #1) — this records what Arc 5 realizes on ARM.

trait / method neutral? ARM metal (Arc 5) fidelity check verified scope
TimeSource::now ✅ ✅ realized (Arc 3, CNTPCT_EL0) witness_advance every boot refines — honored
VcpuOps::set_entry ✅ ✅ realized (Arc 4, ELR_EL2) guest runs from the set entry every boot refines — honored
VcpuOps::inject_interrupt ✅ ⏳ deferred — no GIC yet when interrupt delivery lands assumption named
GuestMemory::read/write ✅ ✅ realized this arc — IPA→PA via the shared stage2 layout; seeds the RO frame + reads guest writes back the un-forgeable ro=0x5EED readback + the RW/foreign write-backs, every boot refines — honored
global allocator n/a ✅ bump over .bss (Arc 3, heap.rs) constructs the guest Hypervisor + the model config every boot plumbing — no reclaim

Honest deferred-items note