From 6c3fd5df61ff2f5cb8833ead6c05eaff9fdc92bf Mon Sep 17 00:00:00 2001 From: Arthur Hanson Rasmusson Date: Tue, 6 Oct 2026 19:28:39 -0400 Subject: risc-v-gsp-rm-debugger: land docs 45-53 + harness/sim tool set with tests rm_capture (chunked decode), launch_audit, state_seed/state_decode, valprobe, rehost, contracts+run_contracts, host_traffic, gmmu_walk, msgq_attach, libos_port, log_decode, server_snapshot; doc index updates in the tool overview, operations pages and the repo README. --- debug/risc-v-gsp-rm-vm/tests/test_launch_audit.py | 477 ++++++++++++++++++++++ 1 file changed, 477 insertions(+) create mode 100644 debug/risc-v-gsp-rm-vm/tests/test_launch_audit.py (limited to 'debug/risc-v-gsp-rm-vm/tests/test_launch_audit.py') diff --git a/debug/risc-v-gsp-rm-vm/tests/test_launch_audit.py b/debug/risc-v-gsp-rm-vm/tests/test_launch_audit.py new file mode 100644 index 0000000..55dce3e --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/tests/test_launch_audit.py @@ -0,0 +1,477 @@ +"""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() -- cgit v1.2.3