diff options
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/harness/launch_audit.py')
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/harness/launch_audit.py | 691 |
1 files changed, 691 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/harness/launch_audit.py b/debug/risc-v-gsp-rm-vm/harness/launch_audit.py new file mode 100644 index 0000000..ba7a623 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/harness/launch_audit.py @@ -0,0 +1,691 @@ +#!/usr/bin/env python3 +"""Launch memory-contract audit: where would the engine read/write? + +A forwarded submission goes silent when one of the engine-visible memory legs +is wrong: the QMD destination the front-end copies to, the release semaphore +the engine writes, the ring the front-end fetches, the parameter/program +pages the QMD points at. The host's bookkeeping (shadows, guest pages) is not +what the engine sees -- the *page tables* are. This harness walks each +address of a submission's memory contract through a captured/constructed +page-table view (``sim.gmmu_walk``) and reports, per leg, the PA the engine +would use, its access flags, and any divergence from what the host believes. + +It answers the class of question behind "the front-end never wrote the QMD"/ +"the release never fired": *is the mapping the engine uses the mapping we are +checking?* -- offline, deterministically, without hardware. + +``check`` closes the capture loop: run it against a partial or empty +``memory[]`` view and it reports, per contract leg, the FIRST table page the +walk needed but the view does not contain (with the level/index path), plus a +consolidated ``needed_pages[]`` list. Dump exactly those pages, re-run, and +repeat -- one capture iteration per table level. + +Schemas: + +* ``greeneye-launch-audit-spec/1`` -- inputs (page tables + contract). +* ``greeneye-launch-audit/1`` -- the verdict report. +* ``greeneye-launch-audit-check/1`` -- the dump-completeness report. + +Honesty: a verdict is only as good as the captured table view in the spec. +``capture-hints`` prints what to capture for a real audit and never executes +anything. Fidelity is the address view (L1); no timing claim. +""" +from __future__ import annotations + +import argparse +import json +import os +import struct +import sys +from typing import Any + +TOOL_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if TOOL_ROOT not in sys.path: + sys.path.insert(0, TOOL_ROOT) + +from sim.gmmu_pte import KIND_VER2, LEVELS, SUPPORTED_KINDS # noqa: E402 +from sim.gmmu_walk import ( # noqa: E402 + FAULT_TABLE_MISSING, + MemoryView, + WalkError, + channel_pdb_fixture, + encode_dual_slot, + encode_pde_pointer, + encode_pte, + walk, +) + +SPEC_FORMAT = "greeneye-launch-audit-spec/1" +AUDIT_FORMAT = "greeneye-launch-audit/1" +CHECK_FORMAT = "greeneye-launch-audit-check/1" +DEMO_FORMAT = "greeneye-launch-audit-demo/1" + +VERDICT_OK = "ok" +VERDICT_UNMAPPED = "unmapped" +VERDICT_PA_MISMATCH = "pa_mismatch" +VERDICT_ACCESS_DENIED = "access_denied" + +#: Walk depth -> display label (root first, leaf page table last). Mirrors +#: the ``LEVELS`` geometry in ``sim.gmmu_pte``; kept here as display names so +#: check reports read like the capture hints (root/pde2/pde1/pde0/pte). +LEVEL_LABELS = ("root", "pde2", "pde1", "pde0", "pte") + + +class AuditError(ValueError): + """A spec or request the tool cannot honour.""" + + +def _parse_int(value: Any) -> int: + if isinstance(value, bool): + raise AuditError(f"bad integer {value!r}") + if isinstance(value, int): + return value + if isinstance(value, str): + try: + return int(value, 0) + except ValueError as exc: + raise AuditError(f"bad integer {value!r}") from exc + raise AuditError(f"bad integer {value!r}") + + +def _parse_hex_bytes(text: str) -> bytes: + data = "".join(str(text).split()) + if len(data) % 2: + raise AuditError("odd hex length") + try: + return bytes.fromhex(data) + except ValueError as exc: + raise AuditError(f"bad hex payload: {exc}") from exc + + +def load_memory(items) -> MemoryView: + mem = MemoryView() + for item in items or (): + if "pa" not in item or "hex" not in item: + raise AuditError(f"memory item needs pa and hex: {item!r}") + mem.add(_parse_int(item["pa"]), _parse_hex_bytes(item["hex"])) + return mem + + +def audit(spec: dict) -> dict: + """Run one audit spec; return a ``greeneye-launch-audit/1`` report.""" + fmt = spec.get("format", SPEC_FORMAT) + if fmt != SPEC_FORMAT: + raise AuditError(f"unsupported spec format {fmt!r} (want {SPEC_FORMAT})") + if "pdb_pa" not in spec or "contract" not in spec: + raise AuditError("spec needs pdb_pa and contract") + kind = spec.get("gmmu", KIND_VER2) + if kind not in SUPPORTED_KINDS: + raise AuditError(f"unsupported gmmu kind {kind!r} (support: {SUPPORTED_KINDS})") + page_mode = spec.get("page_mode", "auto") + root_pa = _parse_int(spec["pdb_pa"]) + mem = load_memory(spec.get("memory")) + + rows = [] + for item in spec["contract"]: + label = item.get("label") or "unnamed" + if "va" not in item: + raise AuditError(f"contract item needs va: {item!r}") + va = _parse_int(item["va"]) + access = item.get("access", "read") + report = walk(va, root_pa=root_pa, mem=mem, kind=kind, page_mode=page_mode) + row: dict[str, Any] = { + "label": label, + "va": f"{va:#x}", + "access": access, + "note": item.get("note"), + "verdict": None, + "walk": report, + } + if report["status"] != "ok": + row["verdict"] = VERDICT_UNMAPPED + row["detail"] = report.get("fault_message") + else: + expect_pa = item.get("expect_pa") + if access == "write" and not report.get("writable", False): + row["verdict"] = VERDICT_ACCESS_DENIED + row["detail"] = "PTE is read-only but the engine must write here" + elif expect_pa is not None and _parse_int(expect_pa) != _parse_int(report["pa"]): + row["verdict"] = VERDICT_PA_MISMATCH + row["detail"] = (f"engine walk -> {report['pa']}; host believes " + f"{_parse_int(expect_pa):#x}") + else: + row["verdict"] = VERDICT_OK + rows.append(row) + + summary = {v: sum(1 for r in rows if r["verdict"] == v) + for v in (VERDICT_OK, VERDICT_UNMAPPED, VERDICT_PA_MISMATCH, + VERDICT_ACCESS_DENIED)} + report = { + "format": AUDIT_FORMAT, + "name": spec.get("name"), + "firmware": spec.get("firmware"), + "gmmu": kind, + "page_mode": page_mode, + "pdb_pa": f"{root_pa:#x}", + "contract": rows, + "summary": summary, + "ok": summary[VERDICT_UNMAPPED] == 0 and summary[VERDICT_PA_MISMATCH] == 0 + and summary[VERDICT_ACCESS_DENIED] == 0, + "limits": [ + "Address view only (L1): a clean audit proves the engine CAN reach these PAs, not that it runs.", + "Verdicts depend on the captured table view in the spec; an incomplete view reports unmapped, not unreachability.", + ], + } + return report + + +# --------------------------------------------------------------------------- +# Check: which table pages does the walk still need? +# --------------------------------------------------------------------------- + +def _path_from_steps(steps: list[dict]) -> list[dict]: + """The completed walk hops, in walk order, as check ``path`` elements.""" + return [ + { + "level": LEVEL_LABELS[step["depth"]], + "depth": step["depth"], + "table_pa": step["table_pa"], + "index": step["index"], + "entry_pa": step["entry_pa"], + "present": True, + } + for step in steps + ] + + +def _missing_table(report: dict, root_pa: int, va: int) -> dict: + """Describe the first table page a ``table_missing`` walk needed. + + ``report["steps"]`` holds the levels that completed before the failing + read; the missing table is the page that read was addressing. The + returned dict carries the page base (``pa`` and ``table_pa``), the level + label, the walk depth, the index inside the table and the exact entry + address. ``report`` must be a ``table_missing`` fault. + """ + depth = len(report["steps"]) + if depth == 0: + table_pa = root_pa + else: + prev = report["steps"][-1] + if prev.get("role") == "dual_pde": + half = prev.get("page_mode_used") + table_pa = (prev["big"]["address"] if half == "64k" + else prev["small"]["address"]) + else: + table_pa = prev["address"] + if depth not in range(len(LEVEL_LABELS)): + # Cannot happen for a walker fault, but never guess a geometry. + return {"pa": f"{table_pa:#x}", "table_pa": f"{table_pa:#x}", + "depth": depth, "level": f"depth{depth}", "index": None, + "entry_pa": None, "present": False} + if depth == 4: + prev = report["steps"][-1] + shift = 16 if prev.get("page_mode_used") == "64k" else 12 + index = (va >> shift) & 0x1FF + entry_size = 8 + else: + geo = LEVELS[depth] + index = (va >> geo["va_bits"][1]) & ((1 << geo["index_bits"]) - 1) + entry_size = geo["entry_size"] + return { + "pa": f"{table_pa:#x}", + "table_pa": f"{table_pa:#x}", + "depth": depth, + "level": LEVEL_LABELS[depth], + "index": index, + "entry_pa": f"{table_pa + index * entry_size:#x}", + "present": False, + } + + +def check(spec: dict) -> dict: + """Report the table pages a partial ``memory[]`` view still needs. + + Runs the same walks as :func:`audit` but, instead of verdicts, reports per + contract leg the FIRST table page the walk could not read (the page the + dump is missing) with the level/index path that reached it, and a + consolidated ``needed_pages`` list. Iterate: run ``check`` with a partial + or empty dump, dump exactly the named pages, repeat until ``complete`` is + true, then run :func:`audit` for verdicts. A leg whose walk faults for a + non-dump reason (invalid aperture/PTE, unsupported encoding) is reported + as ``fault`` and needs no more pages. + + An empty spec memory is valid: every leg then reports the root page as + its first needed page. + """ + fmt = spec.get("format", SPEC_FORMAT) + if fmt != SPEC_FORMAT: + raise AuditError(f"unsupported spec format {fmt!r} (want {SPEC_FORMAT})") + if "pdb_pa" not in spec or "contract" not in spec: + raise AuditError("spec needs pdb_pa and contract") + kind = spec.get("gmmu", KIND_VER2) + if kind not in SUPPORTED_KINDS: + raise AuditError(f"unsupported gmmu kind {kind!r} (support: {SUPPORTED_KINDS})") + page_mode = spec.get("page_mode", "auto") + root_pa = _parse_int(spec["pdb_pa"]) + mem = load_memory(spec.get("memory")) + + rows = [] + needed: dict[int, dict[str, Any]] = {} + for item in spec["contract"]: + label = item.get("label") or "unnamed" + if "va" not in item: + raise AuditError(f"contract item needs va: {item!r}") + va = _parse_int(item["va"]) + report = walk(va, root_pa=root_pa, mem=mem, kind=kind, page_mode=page_mode) + row: dict[str, Any] = { + "label": label, + "va": f"{va:#x}", + "access": item.get("access", "read"), + "expect_pa": None, + "status": None, + "resolved_pa": None, + "writable": None, + "first_missing": None, + "fault": report.get("fault"), + "detail": report.get("fault_message"), + "path": _path_from_steps(report["steps"]), + } + if item.get("expect_pa") is not None: + row["expect_pa"] = f"{_parse_int(item['expect_pa']):#x}" + if report["status"] == "ok": + row["status"] = "complete" + row["resolved_pa"] = report["pa"] + row["writable"] = report.get("writable") + row["fault"] = None + row["detail"] = None + elif report.get("fault") == FAULT_TABLE_MISSING: + missing = _missing_table(report, root_pa, va) + row["status"] = "missing" + row["first_missing"] = missing + row["path"].append(missing) + entry = needed.setdefault(int(missing["pa"], 16), + {"levels": set(), "legs": []}) + entry["levels"].add(missing["level"]) + entry["legs"].append(label) + else: + row["status"] = "fault" + rows.append(row) + + needed_pages = [ + {"pa": f"{pa:#x}", "levels": sorted(value["levels"]), + "legs": sorted(value["legs"])} + for pa, value in sorted(needed.items()) + ] + return { + "format": CHECK_FORMAT, + "name": spec.get("name"), + "firmware": spec.get("firmware"), + "gmmu": kind, + "page_mode": page_mode, + "pdb_pa": f"{root_pa:#x}", + "memory_pages": len(mem.spans()), + "contract": rows, + "needed_pages": needed_pages, + "complete": all(row["status"] != "missing" for row in rows), + "limits": [ + "Dump-completeness view only: `complete` means every table page the walk needed is in the spec, not that any mapping is correct.", + "A `fault` leg needs no more pages: the walk read its entries and refused them (invalid aperture/PTE or unsupported encoding).", + "`needed_pages` is the FIRST missing page per leg; dumping it can reveal the next one, so iterate.", + ], + } + + +# --------------------------------------------------------------------------- +# Demo: the captured channel-PDB contract + the QMD-destination hypotheses +# --------------------------------------------------------------------------- + +def _tree_for_va(root_pa: int, target_va: int, target_pa: int | None, + *, tables_base: int = 0x50000000) -> MemoryView: + """Build a minimal VER2 tree mapping one VA to one PA (None = unmapped).""" + va = target_va + i0 = (va >> 47) & 0x3 + i1 = (va >> 38) & 0x1FF + i2 = (va >> 29) & 0x1FF + i3 = (va >> 21) & 0xFF + i4 = (va >> 12) & 0x1FF + pd2 = tables_base + 0x1000 + pd1 = tables_base + 0x2000 + pd0 = tables_base + 0x3000 + pt = tables_base + 0x4000 + mem = MemoryView() + root = bytearray(0x1000) + struct.pack_into("<Q", root, i0 * 8, encode_pde_pointer(pd2)) + mem.add(root_pa, bytes(root)) + page = bytearray(0x1000) + struct.pack_into("<Q", page, i1 * 8, encode_pde_pointer(pd1)) + mem.add(pd2, bytes(page)) + page = bytearray(0x1000) + struct.pack_into("<Q", page, i2 * 8, encode_pde_pointer(pd0)) + mem.add(pd1, bytes(page)) + page = bytearray(0x1000) + struct.pack_into("<16s", page, i3 * 16, encode_dual_slot(None, pt)) + mem.add(pd0, bytes(page)) + page = bytearray(0x1000) + if target_pa is not None: + struct.pack_into("<Q", page, i4 * 8, encode_pte(target_pa)) + mem.add(pt, bytes(page)) + return mem + + +def run_demo(out: str | None = None) -> dict: + """Audit the captured channel contract and the fork classes. + + Expected verdicts (all pinned by tests): + + * captured channel contract -- all 5 low-window legs ``ok``; + * QMD destination (VA ``0x204608000``): engine resolves to the host shadow + while the host checks the guest page -> ``pa_mismatch``; bookkeeping + trusting the shadow -> ``ok``; unmapped leaf -> ``unmapped``; + * engine-write through shadow (the next wall class: CUDA output buffers at + high VAs, where Tier-1 refuses 0x1f and the leg resolves through the + guest scratch PT pool): the write leg resolves to a shadow PA while the + host bookkeeping still expects the guest PA -> ``pa_mismatch``; when the + bookkeeping is aligned to the engine-visible shadow PA -> ``ok``. + The shadow/guest PAs in this scenario are synthetic (documented in + docs/48), not m7 captures. + """ + # 1) Captured channel PDB contract (validated environment probe). + root_pa, mem = channel_pdb_fixture() + captured = { + "format": SPEC_FORMAT, + "name": "captured-channel-pdb", + "firmware": "570.144", + "gmmu": KIND_VER2, + "page_mode": "4k", + "pdb_pa": f"{root_pa:#x}", + "memory": [{"pa": f"{pa:#x}", "hex": b.hex()} for pa, b in _memory_items(mem)], + "contract": [ + {"label": "gpfifo-ring", "va": "0x100000", "access": "read", + "expect_pa": "0x2045d9000", "note": "front-end GPFIFO fetch"}, + {"label": "push-buffer", "va": "0x101000", "access": "write", + "expect_pa": "0x121bd9000"}, + {"label": "copy-src", "va": "0x102000", "access": "read", + "expect_pa": "0x152115000"}, + {"label": "copy-dst", "va": "0x103000", "access": "write", + "expect_pa": "0x15a5f5000"}, + {"label": "semaphore", "va": "0x104000", "access": "write", + "expect_pa": "0x10cf27000"}, + ], + } + captured_report = audit(captured) + + # 2) QMD-destination hypotheses, same VA (0x204608000): + # A: engine resolves to the host shadow (0x15e8fa000) -- host checks + # the guest page (0x204608000) -> pa_mismatch (the "wrong page" bug). + # B: no mapping / invalid leaf -> unmapped (the "copy dropped" bug). + qmd_va = 0x204608000 + shadow_pa = 0x15E8FA000 + scenarios = [] + for label, target, expect in ( + ("qmd-dest-shadow-trusted", shadow_pa, shadow_pa), + ("qmd-dest-host-checks-guest", shadow_pa, 0x204608000), + ("qmd-dest-unmapped", None, None), + ): + root = 0x60000000 + tree = _tree_for_va(root, qmd_va, target) + spec = { + "format": SPEC_FORMAT, "name": label, "firmware": "570.144", + "gmmu": KIND_VER2, "page_mode": "4k", "pdb_pa": f"{root:#x}", + "memory": [{"pa": f"{pa:#x}", "hex": b.hex()} for pa, b in _memory_items(tree)], + "contract": [{"label": "inline-qmd-destination", "va": f"{qmd_va:#x}", + "access": "write", "expect_pa": f"{expect:#x}" if expect else None, + "note": "front-end copies the inline QMD here"}], + } + report = audit(spec) + scenarios.append({"name": label, "report": report}) + + # 3) Engine-write through shadow (the next wall class). A high guest VA + # is refused by Tier-1 and resolved through the guest scratch PT pool + # to a shadow page; the host bookkeeping for the output may still track + # the guest page. Same walk, two bookkeeping states: + # A: host expects the guest PA -> pa_mismatch (the write is invisible). + # B: bookkeeping aligned to the engine-visible shadow PA -> ok. + shadow_va = 0x210000 + shadow_target = 0x13D2A0000 + guest_pa = 0x2045C0000 + shadow_scenarios = [] + for label, expect, want in ( + ("engine-write-shadow-host-checks-guest", guest_pa, VERDICT_PA_MISMATCH), + ("engine-write-shadow-bookkeeping-aligned", shadow_target, VERDICT_OK), + ): + root = 0x70000000 + tree = _tree_for_va(root, shadow_va, shadow_target) + spec = { + "format": SPEC_FORMAT, "name": label, "firmware": "570.144", + "gmmu": KIND_VER2, "page_mode": "4k", "pdb_pa": f"{root:#x}", + "memory": [{"pa": f"{pa:#x}", "hex": b.hex()} for pa, b in _memory_items(tree)], + "contract": [{"label": "high-va-output", "va": f"{shadow_va:#x}", + "access": "write", "expect_pa": f"{expect:#x}", + "note": "Tier-2 shadow alias for a high-VA write leg"}], + } + report = audit(spec) + shadow_scenarios.append({"name": label, "expected_verdict": want, + "report": report}) + + demo = { + "format": DEMO_FORMAT, + "ok": (captured_report["ok"] + and scenarios[0]["report"]["ok"] + and scenarios[1]["report"]["contract"][0]["verdict"] == VERDICT_PA_MISMATCH + and scenarios[2]["report"]["contract"][0]["verdict"] == VERDICT_UNMAPPED + and all(s["report"]["contract"][0]["verdict"] == s["expected_verdict"] + for s in shadow_scenarios)), + "captured_channel": captured_report, + "qmd_hypotheses": scenarios, + "engine_write_shadow": shadow_scenarios, + } + if out: + os.makedirs(out, exist_ok=True) + path = os.path.join(out, "launch-audit-demo.json") + with open(path, "w", encoding="utf-8") as fh: + json.dump(demo, fh, indent=2, sort_keys=True) + fh.write("\n") + demo["artifacts"] = {"demo_json": os.path.abspath(path)} + return demo + + +def _memory_items(mem: MemoryView): + return [(int(item["pa"], 16), mem.read(int(item["pa"], 16), item["size"])) + for item in mem.spans()] + + +CAPTURE_HINTS = """\ +CAPTURE HINTS (print-only; nothing is executed by this tool) +============================================================ +For a real launch audit you need the *engine's* table view, not the host's +bookkeeping. Capture, for one failing submission: + +1. The channel's page-directory base PA (PDB) -- from the channel's INSTANCE + dump field, or the SET_PAGE_DIRECTORY / GPU_PDB_ADDRESS your stack programs. +2. The table pages reachable from that root for the contract VAs: root, PDE2, + PDE1, PDE0 (dual slot) and the leaf page table(s). A raw PA->bytes dump of + those 4 KB pages is enough; keep them byte-exact (no pretty-printing). +3. The submitted stream's own pointers: GPFIFO ring, inline-QMD destination + (SET_INLINE_QMD_ADDRESS_A/B), the QMD's parameter/program/release-slot + addresses, and the channel report semaphore. +4. Your shadow/guest bookkeeping map for those same VAs (what you currently + check after a timeout). +5. Put 1-4 into a `greeneye-launch-audit-spec/1` JSON (see docs/48). + +ITERATE WITH `check` (one capture round per table level): + # start with only pdb_pa + contract and `"memory": []` (or a partial dump) + python3 harness/launch_audit.py check --spec m7-launch.json + # -> prints the FIRST missing table page per leg + `needed_pages[]` + # dump exactly those pages, append them to memory[], re-run check + # when check says complete: + python3 harness/launch_audit.py run --spec m7-launch.json + +CURRENT LIVE WALL (m7, 2026-10-06) -- exact dump list: + Host-side chain (the validated 5/5 fixture; dump all five raw pages): + root 0x12f16e000 + pd2 0x12f16f000 + pd1 0x11065e000 + pd0 0x110663000 (dual slot pd0[0].small -> pt) + pt 0x15908d000 (pt entries 256..260 = the low-window legs) + Forward-window PT pages (Tier-1 DMA_UPDATE_PDE_2 windows 4128 / 4131): + 0x1b8706000 (pdeIndex 4128) + 0x1197a6000 (pdeIndex 4131) + Geometry rule: pdeIndex = (pd1_idx << 8) | pd0_idx, so the forward windows + land at pd1 index 16. Their pd1[16] and pd0-level chain entries are + written by the SERVER on DMA_UPDATE_PDE_2 acceptance and are in no host + log -- include them (and any page they point at) if they fall outside the + host pages above; `check` names the exact PA. + Tier-2 shadows (high-VA legs): + * the 16 guest-scratch-PT page DMAs -- module stage-2 chunk list, + built as `guest scratch PT built: pages=16 vaBase=0x200000 + capacity=512`; shadow VA = 0x200000 + (page << 21) + (line << 16); + * the rmcall_gr_shadow chunk DMAs -- engine-write shadow pages for the + high-VA legs (the next wall class after dispatch clears). + Key contract VAs: + qmd-dest 0x204608000 write (QMD destination prewritten by host; + post-mortem reads the same hpa) + release-slot 0x204027f7c write (engine release; relw never fired) + ring 0x100000..0x104000 read/write (GPFIFO ring, push-buffer, + copy src/dst, channel semaphore) + param-alias 0x210000 read (Tier-2 shadow alias; NOT directly + mapped; PTEs come from guest scratch) + program-alias 0x220000 read (same) + indirect-alias 0x260000 read (shadow slot 6; same) + The high-VA param/program guest VAs (0x7e06f0.../0x735aa0...) are NOT + directly mapped: Tier-1 refuses status 0x1f and they resolve through the + shadow aliases above. + +COPY-PASTE SPEC SKELETON (start with empty memory and let `check` drive; fill +each `hex` with the 4096-byte raw dump of the page -- no pretty-printing): +{ + "format": "greeneye-launch-audit-spec/1", + "name": "m7-launch", + "firmware": "570.144", + "gmmu": "ver2", + "page_mode": "4k", + "pdb_pa": "0x12f16e000", + "memory": [], + "contract": [ + {"label": "qmd-dest", "va": "0x204608000", "access": "write", "expect_pa": "0x204608000"}, + {"label": "release-slot", "va": "0x204027f7c", "access": "write"}, + {"label": "gpfifo-ring", "va": "0x100000", "access": "read"}, + {"label": "push-buffer", "va": "0x101000", "access": "write"}, + {"label": "copy-src", "va": "0x102000", "access": "read"}, + {"label": "copy-dst", "va": "0x103000", "access": "write"}, + {"label": "semaphore", "va": "0x104000", "access": "write"}, + {"label": "param-alias", "va": "0x210000", "access": "read"}, + {"label": "program-alias", "va": "0x220000", "access": "read"}, + {"label": "indirect-alias","va": "0x260000", "access": "read"} + ] +} +Then: python3 harness/launch_audit.py check --spec m7-launch.json + +Reading the result: `pa_mismatch` means the engine reads/writes a different PA +than the one you are checking (look at the shadow/copy bookkeeping); +`unmapped` means the walk cannot reach any PA (the engine's access is dropped +or faults); `access_denied` means the leaf is read-only for a write leg. +`ok` means the engine CAN reach every leg -- the silence is downstream of the +address path (front-end/engine/context), not in the mapping. +""" + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="launch_audit.py", + description="Audit a submission's engine-visible memory contract (offline).", + ) + sub = parser.add_subparsers(dest="command") + run = sub.add_parser("run", help="run an audit spec") + run.add_argument("--spec", required=True) + run.add_argument("--out", help="write the report JSON to this directory") + check_cmd = sub.add_parser( + "check", help="report the first table pages a partial/empty dump is missing") + check_cmd.add_argument("--spec", required=True) + check_cmd.add_argument("--out", help="write the report JSON to this directory") + demo = sub.add_parser("demo", help="captured contract + QMD/shadow hypotheses") + demo.add_argument("--out", help="output directory") + sub.add_parser("capture-hints", help="print what to capture for a real audit") + return parser + + +def _write_out(report: dict, out: str, name: str) -> str: + os.makedirs(out, exist_ok=True) + path = os.path.join(out, name) + with open(path, "w", encoding="utf-8") as fh: + json.dump(report, fh, indent=2, sort_keys=True) + fh.write("\n") + return os.path.abspath(path) + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + try: + if args.command == "run": + if not os.path.isfile(args.spec): + raise AuditError(f"spec not found: {args.spec}") + with open(args.spec, "r", encoding="utf-8") as fh: + spec = json.load(fh) + report = audit(spec) + for row in report["contract"]: + print(f"{row['verdict']:<14} {row['label']:<28} {row['va']}" + + (f" {row['detail']}" if row.get("detail") else "")) + summary = {"format": report["format"], "ok": report["ok"], + "summary": report["summary"]} + if args.out: + summary["report_json"] = _write_out(report, args.out, "launch-audit.json") + print(json.dumps(summary, indent=2, sort_keys=True)) + return 0 if report["ok"] else 3 + if args.command == "check": + if not os.path.isfile(args.spec): + raise AuditError(f"spec not found: {args.spec}") + with open(args.spec, "r", encoding="utf-8") as fh: + spec = json.load(fh) + report = check(spec) + for row in report["contract"]: + if row["status"] == "missing": + miss = row["first_missing"] + print(f"missing {row['label']:<28} {row['va']}" + f" need {miss['pa']} ({miss['level']}[{miss['index']}])") + elif row["status"] == "complete": + print(f"complete {row['label']:<28} {row['va']}" + f" -> {row['resolved_pa']}") + else: + print(f"fault {row['label']:<28} {row['va']}" + f" {row['fault']}: {row['detail']}") + if report["needed_pages"]: + print("dump next:") + for page in report["needed_pages"]: + print(f" {page['pa']} {'/'.join(page['levels'])}" + f" legs: {', '.join(page['legs'])}") + summary = { + "format": report["format"], + "complete": report["complete"], + "needed_pages": [page["pa"] for page in report["needed_pages"]], + } + if args.out: + summary["report_json"] = _write_out( + report, args.out, "launch-audit-check.json") + print(json.dumps(summary, indent=2, sort_keys=True)) + return 0 if report["complete"] else 3 + if args.command == "demo": + demo = run_demo(args.out) + cap = demo["captured_channel"] + print(f"captured channel contract: ok={cap['ok']} {cap['summary']}") + for scenario in demo["qmd_hypotheses"]: + row = scenario["report"]["contract"][0] + print(f"{row['verdict']:<14} {scenario['name']} {row.get('detail') or ''}") + for scenario in demo["engine_write_shadow"]: + row = scenario["report"]["contract"][0] + print(f"{row['verdict']:<14} {scenario['name']} {row.get('detail') or ''}") + print(json.dumps({"ok": demo["ok"]}, indent=2)) + return 0 if demo["ok"] else 3 + if args.command == "capture-hints": + print(CAPTURE_HINTS) + return 0 + parser.print_help() + return 2 + except (AuditError, WalkError) as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) |
