summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/sim/gmmu_walk.py
diff options
context:
space:
mode:
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/sim/gmmu_walk.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/gmmu_walk.py488
1 files changed, 488 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/sim/gmmu_walk.py b/debug/risc-v-gsp-rm-vm/sim/gmmu_walk.py
new file mode 100644
index 0000000..50960ab
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/sim/gmmu_walk.py
@@ -0,0 +1,488 @@
+#!/usr/bin/env python3
+"""Functional GMMU page-table walker (Turing VER2 geometry).
+
+The decoder in :mod:`sim.gmmu_pte` decodes one captured entry. This module
+follows those entries: given a page-directory base and a sparse physical-memory
+view of the table pages, it walks a virtual address to its leaf mapping and
+reports every level, the page size, the aperture and the access flags -- or the
+exact fault (missing table, invalid aperture, invalid leaf).
+
+Why it exists: the engine sees memory through the GMMU, not through the host's
+bookkeeping. When a forwarded submission goes silent (an engine-side write
+never appears), the first question is *where would the engine read/write this
+address?* A capture of the channel's page tables plus this walker answers it
+offline, deterministically, without hardware.
+
+Evidence and honesty:
+
+* Geometry, field positions and the entry encodings come from the same
+ evidence base as :mod:`sim.gmmu_pte` (see ``docs/43`` and ``docs/48``).
+* The 4K and 2M paths are **validated against a live captured channel PDB**
+ (see :func:`channel_pdb_fixture`); the 64K dual path is modelled from the
+ format definitions and reported with ``evidence: "modeled"`` until a capture
+ validates it.
+* Unsupported encodings fail closed (``E_UNSUPPORTED``); a video-memory leaf
+ address is reported with ``pa_complete: false`` because the decoder cannot
+ claim the high address bits there (never guessed).
+* No timing claim; this is the engine's *address* view (L1), not its behaviour.
+"""
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import struct
+import sys
+
+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 ( # noqa: E402
+ GmmuDecodeError,
+ KIND_VER2,
+ KIND_VER2_DUAL,
+ LEVELS,
+ SUPPORTED_KINDS,
+ decode_dual_pde,
+ decode_entry,
+ decode_pde,
+ decode_pte,
+)
+
+#: Schema tag of a walk report.
+WALK_FORMAT = "greeneye-gmmu-walk/1"
+
+#: Page modes the walker accepts. ``auto`` prefers the 64K dual half for a
+#: 64K-aligned VA when that half is valid, else the 4K half.
+PAGE_MODES = ("4k", "64k", "auto")
+
+VA_MASK = (1 << 49) - 1
+
+#: Fault codes (never a guess; each names a concrete missing/invalid piece).
+FAULT_TABLE_MISSING = "table_missing"
+FAULT_INVALID_APERTURE = "invalid_aperture"
+FAULT_PTE_INVALID = "pte_invalid"
+FAULT_UNSUPPORTED = "unsupported"
+FAULT_OUT_OF_RANGE = "va_out_of_range"
+
+
+class WalkError(ValueError):
+ """A walk request the evidence cannot answer."""
+
+
+class MemoryView:
+ """Sparse physical memory: ``pa -> bytes`` blobs (table pages, targets)."""
+
+ def __init__(self, blobs: dict[int, bytes] | None = None):
+ self._blobs = dict(blobs or {})
+
+ def add(self, pa: int, data: bytes) -> None:
+ if pa < 0 or not isinstance(data, (bytes, bytearray)):
+ raise WalkError("add() needs a non-negative pa and bytes")
+ self._blobs[pa] = bytes(data)
+
+ def has(self, pa: int) -> bool:
+ return pa in self._blobs
+
+ def read(self, pa: int, size: int) -> bytes | None:
+ for base, blob in self._blobs.items():
+ if base <= pa and pa + size <= base + len(blob):
+ off = pa - base
+ return blob[off:off + size]
+ return None
+
+ def spans(self) -> list[dict]:
+ return [{"pa": f"{pa:#x}", "size": len(b)} for pa, b in sorted(self._blobs.items())]
+
+
+# ---------------------------------------------------------------------------
+# Encoders (fixtures/tests; mirror of the decoder's field positions)
+# ---------------------------------------------------------------------------
+
+def encode_pde_pointer(pa: int, *, volatile: bool = True) -> int:
+ """Encode a VER2 directory PDE: bit0=0, aperture 3 (sysmem non-coherent)."""
+ if pa & 0xFFF:
+ raise WalkError(f"table PA must be 4K aligned, got {pa:#x}")
+ return ((pa >> 12) << 8) | (3 << 1) | (8 if volatile else 0)
+
+
+def encode_pte(pa: int, *, page_size: str = "4k", read_only: bool = False,
+ volatile: bool = True) -> int:
+ """Encode a VER2 leaf PTE: bit0=1, aperture 3 (sysmem non-coherent)."""
+ if page_size == "4k":
+ align = 0xFFF
+ elif page_size == "64k":
+ align = 0xFFFF
+ elif page_size == "2m":
+ align = 0x1F_FFFF
+ else:
+ raise WalkError(f"unknown page_size {page_size!r}")
+ if pa & align:
+ raise WalkError(f"PA {pa:#x} is not {page_size}-aligned")
+ return ((pa >> 12) << 8) | (3 << 1) | 1 | (8 if volatile else 0) | (0x40 if read_only else 0)
+
+
+def encode_dual_slot(big_pt_pa: int | None, small_pt_pa: int) -> bytes:
+ """Encode a VER2 dual PDE0 slot; BIG = 64K table, SMALL = 4K table."""
+ big = 0
+ if big_pt_pa is not None:
+ if big_pt_pa & 0xFFF:
+ raise WalkError(f"big table PA must be 4K aligned, got {big_pt_pa:#x}")
+ big = (((big_pt_pa >> 8) & ~1) << 4) | (3 << 1) | 8
+ small = ((small_pt_pa >> 12) << 8) | (3 << 1) | 8
+ return struct.pack("<QQ", big, small)
+
+
+def _fault(code: str, message: str, steps: list, **extra) -> dict:
+ report = {
+ "format": WALK_FORMAT, "status": "fault", "fault": code,
+ "fault_message": message, "steps": steps,
+ }
+ report.update(extra)
+ return report
+
+
+def walk(
+ va: int,
+ *,
+ root_pa: int,
+ mem: MemoryView,
+ kind: str = KIND_VER2,
+ page_mode: str = "4k",
+) -> dict:
+ """Walk ``va`` through the page tables rooted at ``root_pa``.
+
+ Returns a ``greeneye-gmmu-walk/1`` report. ``status`` is ``ok`` with
+ ``pa``/``page_size``/``writable`` on success, otherwise ``fault`` with a
+ concrete ``fault`` code. Every level's raw entry and decode is kept in
+ ``steps`` so the result is auditable.
+ """
+ if kind not in SUPPORTED_KINDS:
+ return _fault(FAULT_UNSUPPORTED, f"kind {kind!r} not in {SUPPORTED_KINDS}",
+ [], va=va, root_pa=root_pa)
+ if page_mode not in PAGE_MODES:
+ raise WalkError(f"page_mode must be one of {PAGE_MODES}, got {page_mode!r}")
+ if not (0 <= va <= VA_MASK):
+ return _fault(FAULT_OUT_OF_RANGE, f"va {va:#x} outside 49-bit space", [],
+ va=va, root_pa=root_pa)
+
+ steps: list[dict] = []
+ pa_complete = True
+ page_used = page_mode
+ evidence = "validated"
+ cur = root_pa
+
+ for depth in range(5):
+ geo = LEVELS[depth]
+ _hi_bit, lo_bit = geo["va_bits"]
+ idx = (va >> lo_bit) & ((1 << geo["index_bits"]) - 1)
+ table_pa = cur
+ if depth == 3:
+ off = idx * 16
+ raw = mem.read(table_pa + off, 16)
+ if raw is None:
+ return _fault(FAULT_TABLE_MISSING,
+ f"depth-3 dual slot at pa={table_pa + off:#x} not in view",
+ steps, va=va, root_pa=root_pa)
+ value128 = int.from_bytes(raw, "little")
+ low = value128 & 0xFFFF_FFFF_FFFF_FFFF
+ step = {"depth": depth, "level": geo["hw"], "table_pa": f"{table_pa:#x}",
+ "index": idx, "entry_pa": f"{table_pa + off:#x}",
+ "raw": f"{value128:#x}"}
+ if low & 1: # 2 MB leaf in the low half
+ try:
+ pte = decode_entry(low, kind=KIND_VER2, role="pte", level=3)
+ except GmmuDecodeError as exc:
+ return _fault(FAULT_UNSUPPORTED, str(exc), steps + [step],
+ va=va, root_pa=root_pa)
+ step.update({"role": "pte", "page_size": "2m",
+ "aperture_name": pte["aperture_name"],
+ "valid": pte["valid"], "address": pte["address"]})
+ steps.append(step)
+ if not pte["valid"] or pte["address"] is None:
+ return _fault(FAULT_PTE_INVALID,
+ f"2M leaf at {table_pa + off:#x} invalid/unsupported",
+ steps, va=va, root_pa=root_pa)
+ if not pte["address_complete"]:
+ pa_complete = False
+ pa = pte["address"] + (va & 0x1F_FFFF)
+ return _ok(va, pa, "2m", pte, steps, root_pa, kind,
+ page_used="2m", evidence=evidence, pa_complete=pa_complete)
+ try:
+ dual = decode_dual_pde(value128, pde_kind=KIND_VER2_DUAL, level=3)
+ except GmmuDecodeError as exc:
+ return _fault(FAULT_UNSUPPORTED, str(exc), steps + [step],
+ va=va, root_pa=root_pa)
+ chosen = None
+ if page_mode == "64k":
+ chosen, page_used, evidence = dual["big"], "64k", "modeled"
+ elif page_mode == "4k":
+ chosen, page_used = dual["small"], "4k"
+ else: # auto: prefer BIG for a 64K-aligned VA when valid
+ if va % 0x10000 == 0 and dual["big"]["address"] is not None:
+ chosen, page_used, evidence = dual["big"], "64k", "modeled"
+ else:
+ chosen, page_used = dual["small"], "4k"
+ step.update({"role": "dual_pde", "page_mode_used": page_used,
+ "big": dual["big"], "small": dual["small"]})
+ steps.append(step)
+ if chosen["address"] is None:
+ return _fault(FAULT_INVALID_APERTURE,
+ f"dual PDE at {table_pa + off:#x} {page_used} half has "
+ f"aperture {chosen['aperture_name']}", steps,
+ va=va, root_pa=root_pa)
+ if not chosen["address_complete"]:
+ pa_complete = False
+ cur = chosen["address"]
+ continue
+
+ off = idx * geo["entry_size"]
+ if depth == 4:
+ # A 64K-granularity leaf table is indexed by VA bits 24:16.
+ idx = (va >> (16 if page_used == "64k" else 12)) & 0x1FF
+ off = idx * geo["entry_size"]
+ raw = mem.read(table_pa + off, geo["entry_size"])
+ if raw is None:
+ return _fault(FAULT_TABLE_MISSING,
+ f"level {depth} entry at pa={table_pa + off:#x} not in view",
+ steps, va=va, root_pa=root_pa)
+ value = int.from_bytes(raw, "little")
+ step = {"depth": depth, "level": geo["hw"], "table_pa": f"{table_pa:#x}",
+ "index": idx, "entry_pa": f"{table_pa + off:#x}", "raw": f"{value:#x}"}
+ if depth < 3:
+ try:
+ pde = decode_pde(value, pde_kind=KIND_VER2, level=depth)
+ except GmmuDecodeError as exc:
+ return _fault(FAULT_UNSUPPORTED, str(exc), steps + [step],
+ va=va, root_pa=root_pa)
+ step.update({"role": "pde", "aperture_name": pde["aperture_name"],
+ "address": pde["address"]})
+ steps.append(step)
+ if pde["address"] is None:
+ return _fault(FAULT_INVALID_APERTURE,
+ f"level {depth} entry at {table_pa + off:#x} has "
+ f"aperture {pde['aperture_name']}", steps,
+ va=va, root_pa=root_pa)
+ if not pde["address_complete"]:
+ pa_complete = False
+ cur = pde["address"]
+ continue
+
+ # depth 4: leaf page table
+ try:
+ pte = decode_pte(value, pte_kind=KIND_VER2, level=4)
+ except GmmuDecodeError as exc:
+ return _fault(FAULT_UNSUPPORTED, str(exc), steps + [step],
+ va=va, root_pa=root_pa)
+ leaf_mask = 0xFFFF if page_used == "64k" else 0xFFF
+ step.update({"role": "pte", "page_size": page_used,
+ "aperture_name": pte["aperture_name"], "valid": pte["valid"],
+ "address": pte["address"], "writable": pte["writable"]})
+ steps.append(step)
+ if not pte["valid"] or pte["address"] is None:
+ return _fault(FAULT_PTE_INVALID,
+ f"leaf PTE at {table_pa + off:#x} invalid/unsupported "
+ f"(raw {value:#x})", steps, va=va, root_pa=root_pa)
+ if not pte["address_complete"]:
+ pa_complete = False
+ pa = pte["address"] + (va & leaf_mask)
+ return _ok(va, pa, page_used, pte, steps, root_pa, kind,
+ page_used=page_used, evidence=evidence, pa_complete=pa_complete)
+
+ return _fault(FAULT_UNSUPPORTED, "walk exceeded 5 levels", steps,
+ va=va, root_pa=root_pa)
+
+
+def _ok(va, pa, page_size, leaf, steps, root_pa, kind, *, page_used, evidence,
+ pa_complete) -> dict:
+ report = {
+ "format": WALK_FORMAT,
+ "status": "ok",
+ "va": f"{va:#x}",
+ "root_pa": f"{root_pa:#x}",
+ "kind": kind,
+ "page_mode_used": page_used,
+ "page_size": page_size,
+ "evidence": evidence,
+ "pa": f"{pa:#x}",
+ "pa_complete": pa_complete,
+ "aperture_name": leaf.get("aperture_name"),
+ "writable": leaf.get("writable", True),
+ "read_only": leaf.get("read_only", False),
+ "kind_name": leaf.get("kind_name"),
+ "steps": steps,
+ "assumptions": [
+ "4K/2M paths are validated against a captured channel PDB; the 64K dual path is modelled and reported with evidence='modeled'.",
+ "A video-memory leaf reports pa_complete=false (high address bits are not claimed by the decoder).",
+ ],
+ }
+ if page_size == "64k":
+ report["assumptions"].append("64K page-mode selection for auto walks is an assumption (64K-aligned VA prefers the BIG half).")
+ return report
+
+
+def walk_many(vas, *, root_pa: int, mem: MemoryView, kind: str = KIND_VER2,
+ page_mode: str = "4k") -> list[dict]:
+ return [walk(va, root_pa=root_pa, mem=mem, kind=kind, page_mode=page_mode)
+ for va in vas]
+
+
+# ---------------------------------------------------------------------------
+# Captured golden vector: a live channel PDB (numeric only)
+# ---------------------------------------------------------------------------
+
+#: Captured channel-PDB entries from a live run (five VAs of one channel
+#: chunk). Used as the walker's regression vector: the walk must reproduce
+#: the DMA addresses the capture logged for these VAs.
+_CHANNEL_PDB = {
+ "root_pa": 0x12F16E000,
+ "root": {0: 0x0000_0000_12F16F0E},
+ "pd2": {0: 0x0000_0000_11065E0E},
+ "pd1": {0: 0x0000_0000_1106630E},
+ "pd0_small": {0: 0x0000_0000_15908D0E},
+ "pt": {
+ 256: 0x0000_0000_2045D90F,
+ 257: 0x0000_0000_121BD90F,
+ 258: 0x0000_0000_1521150F,
+ 259: 0x0000_0000_15A5F50F,
+ 260: 0x0000_0000_10CF270F,
+ },
+}
+
+_CHANNEL_VAS = {
+ 0x100000: ("gpfifo", 0x2045D9000),
+ 0x101000: ("push-buffer", 0x121BD9000),
+ 0x102000: ("copy-src", 0x152115000),
+ 0x103000: ("copy-dst", 0x15A5F5000),
+ 0x104000: ("semaphore", 0x10CF27000),
+}
+
+
+def channel_pdb_fixture() -> tuple[int, MemoryView]:
+ """Rebuild the captured channel PDB as a walkable memory view."""
+ mem = MemoryView()
+ root = bytearray(0x1000)
+ for idx, value in _CHANNEL_PDB["root"].items():
+ struct.pack_into("<Q", root, idx * 8, value)
+ mem.add(0x12F16E000, bytes(root))
+ for name, base in (("pd2", 0x12F16F000), ("pd1", 0x11065E000)):
+ page = bytearray(0x1000)
+ for idx, value in _CHANNEL_PDB[name].items():
+ struct.pack_into("<Q", page, idx * 8, value)
+ mem.add(base, bytes(page))
+ pd0 = bytearray(0x1000)
+ for idx, small in _CHANNEL_PDB["pd0_small"].items():
+ struct.pack_into("<16s", pd0, idx * 16, encode_dual_slot(None, (small >> 8) << 12))
+ mem.add(0x110663000, bytes(pd0))
+ pt = bytearray(0x1000)
+ for idx, value in _CHANNEL_PDB["pt"].items():
+ struct.pack_into("<Q", pt, idx * 8, value)
+ mem.add(0x15908D000, bytes(pt))
+ return _CHANNEL_PDB["root_pa"], mem
+
+
+def run_demo(out: str | None = None) -> dict:
+ """Walk the captured channel-PDB vector; PASS iff every VA matches."""
+ root_pa, mem = channel_pdb_fixture()
+ rows = []
+ ok = True
+ for va, (label, expected) in sorted(_CHANNEL_VAS.items()):
+ report = walk(va, root_pa=root_pa, mem=mem)
+ got = int(report["pa"], 16) if report["status"] == "ok" else None
+ match = got == expected
+ ok = ok and match
+ rows.append({"label": label, "va": f"{va:#x}", "expected_pa": f"{expected:#x}",
+ "walk": report, "match": match})
+ demo = {"format": "greeneye-gmmu-walk-demo/1", "ok": ok, "rows": rows,
+ "root_pa": f"{root_pa:#x}"}
+ if out:
+ os.makedirs(out, exist_ok=True)
+ path = os.path.join(out, "gmmu-walk-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 _parse_int(text: str) -> int:
+ try:
+ return int(text, 0)
+ except ValueError as exc:
+ raise WalkError(f"bad integer {text!r}") from exc
+
+
+def _parse_mem(spec: str) -> tuple[int, bytes]:
+ if "=" not in spec:
+ raise WalkError(f"--mem needs <pa>=<hex>: {spec!r}")
+ pa_text, _, hex_text = spec.partition("=")
+ data = "".join(hex_text.split())
+ if len(data) % 2:
+ raise WalkError(f"odd hex length in {spec!r}")
+ return _parse_int(pa_text), bytes.fromhex(data)
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ prog="gmmu_walk.py",
+ description="Walk a GMMU VA through captured/constructed page tables (offline).",
+ )
+ sub = parser.add_subparsers(dest="command")
+ run = sub.add_parser("run", help="walk VAs through a page-table view")
+ run.add_argument("--pdb-pa", required=True, help="page-directory base PA")
+ run.add_argument("--va", action="append", required=True, help="virtual address (repeatable)")
+ run.add_argument("--mem", action="append", default=[], help="<pa>=<hex> table page (repeatable)")
+ run.add_argument("--kind", default=KIND_VER2, choices=list(SUPPORTED_KINDS))
+ run.add_argument("--page-mode", default="4k", choices=list(PAGE_MODES))
+ run.add_argument("--out", help="write the JSON report here")
+ demo = sub.add_parser("demo", help="walk the captured channel-PDB vector")
+ demo.add_argument("--out", help="output directory")
+ return parser
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = build_parser()
+ args = parser.parse_args(argv)
+ try:
+ if args.command == "run":
+ mem = MemoryView()
+ for spec in args.mem:
+ pa, data = _parse_mem(spec)
+ mem.add(pa, data)
+ reports = [walk(_parse_int(va), root_pa=_parse_int(args.pdb_pa), mem=mem,
+ kind=args.kind, page_mode=args.page_mode) for va in args.va]
+ doc = {"format": "greeneye-gmmu-walk-set/1", "reports": reports}
+ for report in reports:
+ if report["status"] == "ok":
+ print(f"OK {report['va']} -> {report['pa']} "
+ f"{report['page_size']} {report['aperture_name']} "
+ f"writable={report['writable']}")
+ else:
+ print(f"FAULT {report['va']} {report['fault']}: {report['fault_message']}")
+ if args.out:
+ os.makedirs(args.out, exist_ok=True)
+ path = os.path.join(args.out, "gmmu-walk.json")
+ with open(path, "w", encoding="utf-8") as fh:
+ json.dump(doc, fh, indent=2, sort_keys=True)
+ fh.write("\n")
+ print(json.dumps({"report_json": os.path.abspath(path)}, indent=2))
+ return 0 if all(r["status"] == "ok" for r in reports) else 3
+ if args.command == "demo":
+ demo = run_demo(args.out)
+ for row in demo["rows"]:
+ report = row["walk"]
+ got = report.get("pa", report.get("fault_message"))
+ print(f"{'PASS' if row['match'] else 'FAIL'} {row['label']:<12} "
+ f"{row['va']} -> {got} (expected {row['expected_pa']})")
+ print(json.dumps({"ok": demo["ok"]}, indent=2))
+ return 0 if demo["ok"] else 3
+ parser.print_help()
+ return 2
+ except WalkError as exc:
+ print(f"error: {exc}", file=sys.stderr)
+ return 2
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())