"""Tests for the launch memory-contract audit (harness/launch_audit.py, docs/48). Offline only: synthetic page-table trees plus the captured channel-PDB vector. Run:: python3 -m unittest tests.test_launch_audit -v """ from __future__ import annotations import contextlib import io import json import os import struct import sys import tempfile import unittest HERE = os.path.dirname(os.path.abspath(__file__)) TOOL_ROOT = os.path.abspath(os.path.join(HERE, "..")) if TOOL_ROOT not in sys.path: sys.path.insert(0, TOOL_ROOT) from harness.launch_audit import ( # noqa: E402 AUDIT_FORMAT, CHECK_FORMAT, SPEC_FORMAT, VERDICT_ACCESS_DENIED, VERDICT_OK, VERDICT_PA_MISMATCH, VERDICT_UNMAPPED, AuditError, _tree_for_va, audit, check, main, run_demo, ) from sim.gmmu_walk import ( # noqa: E402 MemoryView, encode_dual_slot, encode_pde_pointer, encode_pte, ) def _spec(root_pa: int, mem: MemoryView, contract: list, **extra) -> dict: spec = { "format": SPEC_FORMAT, "name": "test", "gmmu": "ver2", "page_mode": "4k", "pdb_pa": f"{root_pa:#x}", "memory": [{"pa": f"{pa:#x}", "hex": b.hex()} for pa, b in _blobs(mem)], "contract": contract, } spec.update(extra) return spec def _blobs(mem: MemoryView): return [(int(item["pa"], 16), mem.read(int(item["pa"], 16), item["size"])) for item in mem.spans()] def _drop(mem: MemoryView, drop_pas) -> MemoryView: """Copy a memory view without the given table pages (a partial dump).""" out = MemoryView() for pa, blob in _blobs(mem): if pa not in drop_pas: out.add(pa, blob) return out def _run_main(argv): out, err = io.StringIO(), io.StringIO() with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): rc = main(argv) return rc, out.getvalue(), err.getvalue() class AuditTests(unittest.TestCase): def test_shadow_maps_to_expected_pa_is_ok(self): va, shadow = 0x204608000, 0x15E8FA000 root = 0x60000000 report = audit(_spec(root, _tree_for_va(root, va, shadow), [ {"label": "qmd-dest", "va": f"{va:#x}", "access": "write", "expect_pa": f"{shadow:#x}"}, ])) self.assertEqual(report["format"], AUDIT_FORMAT) row = report["contract"][0] self.assertEqual(row["verdict"], VERDICT_OK) self.assertTrue(report["ok"]) def test_host_checks_guest_page_is_pa_mismatch(self): va, shadow = 0x204608000, 0x15E8FA000 root = 0x60000000 report = audit(_spec(root, _tree_for_va(root, va, shadow), [ {"label": "qmd-dest", "va": f"{va:#x}", "access": "write", "expect_pa": f"{va:#x}"}, ])) row = report["contract"][0] self.assertEqual(row["verdict"], VERDICT_PA_MISMATCH) self.assertFalse(report["ok"]) self.assertIn("0x15e8fa000", row["detail"]) def test_unmapped_leaf_is_unmapped(self): va = 0x204608000 root = 0x60000000 report = audit(_spec(root, _tree_for_va(root, va, None), [ {"label": "qmd-dest", "va": f"{va:#x}", "access": "write"}, ])) self.assertEqual(report["contract"][0]["verdict"], VERDICT_UNMAPPED) self.assertFalse(report["ok"]) def test_read_only_leaf_for_write_is_access_denied(self): va, target = 0x204608000, 0x15E8FA000 # build the same tree as _tree_for_va but with a read-only leaf i0, i1 = (va >> 47) & 0x3, (va >> 38) & 0x1FF i2, i3 = (va >> 29) & 0x1FF, (va >> 21) & 0xFF i4 = (va >> 12) & 0x1FF base = 0x50000000 pd2, pd1, pd0, pt = base + 0x1000, base + 0x2000, base + 0x3000, base + 0x4000 mem = MemoryView() page = bytearray(0x1000) struct.pack_into("> 29) & 0x1FF self.assertEqual(missing["pa"], f"{pde1:#x}") self.assertEqual(missing["table_pa"], f"{pde1:#x}") self.assertEqual(missing["level"], "pde1") self.assertEqual(missing["depth"], 2) self.assertEqual(missing["index"], index) self.assertEqual(missing["entry_pa"], f"{pde1 + index * 8:#x}") self.assertEqual([hop["level"] for hop in row["path"]], ["root", "pde2", "pde1"]) self.assertEqual(row["path"][0]["table_pa"], f"{root:#x}") self.assertTrue(row["path"][0]["present"]) self.assertFalse(row["path"][-1]["present"]) self.assertEqual(report["needed_pages"][0]["pa"], f"{pde1:#x}") self.assertEqual(report["needed_pages"][0]["legs"], ["qmd"]) def test_check_partial_memory_marks_complete_and_missing_legs(self): root = 0x60000000 base = 0x51000000 pd2, pd1 = base + 0x1000, base + 0x2000 pd0_done, pt_done = base + 0x3000, base + 0x4000 pd0_missing = base + 0x5000 target = 0x12345000 va_done, va_miss = 0x100000, 0x204608000 def idx(va): return ((va >> 47) & 3, (va >> 38) & 0x1FF, (va >> 29) & 0x1FF, (va >> 21) & 0xFF, (va >> 12) & 0x1FF) i0d, i1d, i2d, i3d, i4d = idx(va_done) i0m, i1m, i2m, _i3m, _i4m = idx(va_miss) self.assertEqual((i0d, i1d), (i0m, i1m)) # share root[i0] and pd2[i1] self.assertNotEqual(i2d, i2m) mem = MemoryView() page = bytearray(0x1000) struct.pack_into("> 47) & 3, 0) self.assertEqual((va_a2 >> 47) & 3, 0) self.assertEqual((va_b >> 47) & 3, 1) mem = MemoryView() page = bytearray(0x1000) struct.pack_into(" pte_invalid report = check(_spec(root, mem, [ {"label": "qmd", "va": f"{va:#x}", "access": "write"}, ])) row = report["contract"][0] self.assertEqual(row["status"], "fault") self.assertEqual(row["fault"], "pte_invalid") self.assertIsNone(row["first_missing"]) self.assertEqual(report["needed_pages"], []) self.assertTrue(report["complete"]) def test_check_64k_dual_missing_big_table(self): va = 0x204600000 root, pd2, pd1, pd0 = 0x60000000, 0x50001000, 0x50002000, 0x50003000 big_pt, small_pt = 0x80000000, 0x80001000 i0, i1, i2 = (va >> 47) & 3, (va >> 38) & 0x1FF, (va >> 29) & 0x1FF i3 = (va >> 21) & 0xFF mem = MemoryView() page = bytearray(0x1000) struct.pack_into("> 16) & 0x1FF) def test_check_is_deterministic(self): va = 0x204608000 root = 0x60000000 full = _tree_for_va(root, va, 0x12345000) partial = _drop(full, {0x50002000}) spec = _spec(root, partial, [ {"label": "qmd", "va": f"{va:#x}", "access": "write"}, ]) self.assertEqual(json.dumps(check(spec), sort_keys=True), json.dumps(check(spec), sort_keys=True)) def test_check_spec_errors(self): with self.assertRaises(AuditError): check({"format": SPEC_FORMAT}) with self.assertRaises(AuditError): check({"format": "nope", "pdb_pa": "0x1000", "contract": []}) with self.assertRaises(AuditError): check(_spec(0x60000000, MemoryView(), [], gmmu="ver9")) with self.assertRaises(AuditError): check(_spec(0x60000000, MemoryView(), [{"label": "x"}])) def test_cli_check_exit_codes_and_artifact(self): va = 0x204608000 root = 0x60000000 full = _tree_for_va(root, va, 0x12345000) partial = _drop(full, {0x50002000}) spec = _spec(root, partial, [ {"label": "qmd", "va": f"{va:#x}", "access": "write"}, ]) with tempfile.TemporaryDirectory() as tmp: spec_path = os.path.join(tmp, "spec.json") with open(spec_path, "w", encoding="utf-8") as fh: json.dump(spec, fh) rc, out, _ = _run_main(["check", "--spec", spec_path, "--out", tmp]) self.assertEqual(rc, 3) self.assertIn("need 0x50002000", out) self.assertIn("dump next:", out) report_path = os.path.join(tmp, "launch-audit-check.json") self.assertTrue(os.path.isfile(report_path)) with open(report_path, encoding="utf-8") as fh: report = json.load(fh) self.assertEqual(report["format"], CHECK_FORMAT) self.assertEqual(report["needed_pages"][0]["pa"], "0x50002000") # a complete view -> exit 0 spec["memory"] = [{"pa": f"{pa:#x}", "hex": b.hex()} for pa, b in _blobs(full)] with open(spec_path, "w", encoding="utf-8") as fh: json.dump(spec, fh) rc, out, _ = _run_main(["check", "--spec", spec_path]) self.assertEqual(rc, 0) self.assertIn("complete", out) class DemoAndCliTests(unittest.TestCase): def test_demo_is_green_and_deterministic(self): first = run_demo() second = run_demo() self.assertEqual(first["format"], "greeneye-launch-audit-demo/1") self.assertTrue(first["ok"]) self.assertEqual(json.dumps(first, sort_keys=True), json.dumps(second, sort_keys=True)) def test_audit_is_deterministic(self): va, shadow = 0x204608000, 0x15E8FA000 root = 0x60000000 spec = _spec(root, _tree_for_va(root, va, shadow), [ {"label": "qmd", "va": f"{va:#x}", "access": "write"}, ]) self.assertEqual(json.dumps(audit(spec), sort_keys=True), json.dumps(audit(spec), sort_keys=True)) def test_cli_demo_and_hints(self): rc, out, _ = _run_main(["demo"]) self.assertEqual(rc, 0) self.assertIn("captured channel contract: ok=True", out) self.assertIn("engine-write-shadow-host-checks-guest", out) rc, out, _ = _run_main(["capture-hints"]) self.assertEqual(rc, 0) self.assertIn("CAPTURE HINTS", out) def test_engine_write_shadow_demo_verdicts(self): demo = run_demo() scenarios = {s["name"]: s for s in demo["engine_write_shadow"]} mismatch = scenarios["engine-write-shadow-host-checks-guest"] aligned = scenarios["engine-write-shadow-bookkeeping-aligned"] self.assertEqual(mismatch["expected_verdict"], VERDICT_PA_MISMATCH) self.assertEqual( mismatch["report"]["contract"][0]["verdict"], VERDICT_PA_MISMATCH) self.assertEqual(aligned["expected_verdict"], VERDICT_OK) self.assertEqual( aligned["report"]["contract"][0]["verdict"], VERDICT_OK) self.assertTrue(demo["ok"]) # the original demo sections are still present and unchanged in shape self.assertTrue(demo["captured_channel"]["ok"]) self.assertEqual(len(demo["qmd_hypotheses"]), 3) def test_cli_run_writes_report_and_exit_codes(self): va, shadow = 0x204608000, 0x15E8FA000 root = 0x60000000 spec = _spec(root, _tree_for_va(root, va, shadow), [ {"label": "qmd", "va": f"{va:#x}", "access": "write"}, ]) with tempfile.TemporaryDirectory() as tmp: spec_path = os.path.join(tmp, "spec.json") with open(spec_path, "w", encoding="utf-8") as fh: json.dump(spec, fh) rc, out, _ = _run_main(["run", "--spec", spec_path, "--out", tmp]) self.assertEqual(rc, 0) self.assertTrue(os.path.isfile(os.path.join(tmp, "launch-audit.json"))) # mismatch -> exit 3 spec["contract"][0]["expect_pa"] = f"{va:#x}" with open(spec_path, "w", encoding="utf-8") as fh: json.dump(spec, fh) rc, _, _ = _run_main(["run", "--spec", spec_path]) self.assertEqual(rc, 3) if __name__ == "__main__": unittest.main()