From 1f2073d4fbb1d412d9bbf373312e3c51b83db92a Mon Sep 17 00:00:00 2001 From: Arthur Hanson Rasmusson Date: Sun, 4 Oct 2026 18:42:36 -0400 Subject: greeneye: GMMU/FBIF observability, PTE decoder and oracle, MPU/libos simulator wave Simulator development wave (docs 42-44): GMMU page-table entry decode with its oracle/workflow harness, FBIF observability, and MPU/LIBOS device surfaces in the device model, plus refreshed boot/machine/profile data (tu104-570.144), the qemu-line results, and the new unit tests. Docs 00/06/35-41 updated to match; harness helpers and .gitignore included. --- debug/risc-v-gsp-rm-vm/sim/devices/__init__.py | 26 + debug/risc-v-gsp-rm-vm/sim/devices/base.py | 16 +- debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py | 21 +- debug/risc-v-gsp-rm-vm/sim/devices/observe.py | 372 ++++++++ debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py | 988 +++++++++++++++++++++ debug/risc-v-gsp-rm-vm/sim/gsp_boot.py | 179 +++- debug/risc-v-gsp-rm-vm/sim/libos.py | 217 +++++ debug/risc-v-gsp-rm-vm/sim/machine.py | 80 +- debug/risc-v-gsp-rm-vm/sim/mpu.py | 154 ++++ .../sim/profiles/tu104-570.144.json | 815 ++++++++++++++++- 10 files changed, 2841 insertions(+), 27 deletions(-) create mode 100644 debug/risc-v-gsp-rm-vm/sim/devices/observe.py create mode 100644 debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py create mode 100644 debug/risc-v-gsp-rm-vm/sim/libos.py create mode 100644 debug/risc-v-gsp-rm-vm/sim/mpu.py (limited to 'debug/risc-v-gsp-rm-vm/sim') diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py b/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py index caf22b3..6fdce2e 100644 --- a/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py +++ b/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py @@ -26,6 +26,14 @@ from .dma import DmaTranslator from .falcon import FalconBlock from .fwsec import Wpr2Block from .irq import IrqRouter +from .observe import ( + TRACE_SCHEMA, + ObservedWindow, + build_trace, + collect_observations, + parse_window_spec, + windows_from_section, +) from .pmc import PmcBlock from .pmu import PmuStub from .priscv import PriscvBlock @@ -33,12 +41,16 @@ from .timer import PtimerBlock PROFILE_SCHEMA = "greeneye-sim-profile/1" +#: Profile device sections that declare observe-only/log-only windows. +OBSERVE_SECTIONS = ("gmmu", "fbif") + __all__ = [ "Device", "DeviceRegistry", "Window", "SNAPSHOT_SCHEMA", "PROFILE_SCHEMA", + "TRACE_SCHEMA", "PmcBlock", "FalconBlock", "PriscvBlock", @@ -48,8 +60,13 @@ __all__ = [ "PmuStub", "DmaTranslator", "IrqRouter", + "ObservedWindow", "load_profile", "registry_from_profile", + "windows_from_section", + "parse_window_spec", + "collect_observations", + "build_trace", ] @@ -153,6 +170,15 @@ def registry_from_profile(profile: dict, *, machine=None, ) elif name == "pmu": dev = PmuStub() + elif name in OBSERVE_SECTIONS: + for win in windows_from_section(section): + reg.add(ObservedWindow( + base=win["base"], size=win["size"], label=win["label"], + mode=win["mode"], registers=win["registers"], + evidence=section.get("evidence", ""), + clock=(machine.insn_count if machine is not None else None), + )) + continue else: raise ValueError(f"unknown device section {name!r}") for base, size in _windows(section): diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/base.py b/debug/risc-v-gsp-rm-vm/sim/devices/base.py index 877109c..7254334 100644 --- a/debug/risc-v-gsp-rm-vm/sim/devices/base.py +++ b/debug/risc-v-gsp-rm-vm/sim/devices/base.py @@ -44,6 +44,11 @@ class Device: name = "device" + #: Opt-in hook: when True the registry forwards the faulting PC to + #: ``read``/``write`` as ``pc=`` (observer windows record it; every other + #: device keeps the original two-/three-argument call unchanged). + wants_pc = False + def __init__(self, base: int = 0, size: int = 0): self.base = base self.size = size @@ -184,7 +189,10 @@ class DeviceRegistry: matched = False for win in self._candidates(addr, size): matched = True - value = win.device.read(addr - win.base, size) + if win.device.wants_pc: + value = win.device.read(addr - win.base, size, pc=pc) + else: + value = win.device.read(addr - win.base, size) if value is not None: self._record(addr, size, False, value, True, pc) return True, value @@ -202,7 +210,11 @@ class DeviceRegistry: matched = False for win in self._candidates(addr, size): matched = True - if win.device.write(addr - win.base, size, value): + if win.device.wants_pc: + handled = win.device.write(addr - win.base, size, value, pc=pc) + else: + handled = win.device.write(addr - win.base, size, value) + if handled: self._record(addr, size, True, value, True, pc) return True self.unhandled += 1 diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py b/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py index 8dcf633..b278ec8 100644 --- a/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py +++ b/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py @@ -25,6 +25,17 @@ WPR2_ADDR_HI = 0x1FA828 WPR2_ALIGNMENT = 12 WPR2_BLOCK = 0x20000 +#: Block-relative offsets (a profile window that starts at ``+0x1FA000`` +#: reaches the pair at ``0x824``/``0x828``; the device accepts both forms). +WPR2_ADDR_LO_REL = WPR2_ADDR_LO & 0xFFF +WPR2_ADDR_HI_REL = WPR2_ADDR_HI & 0xFFF +_KEYS = { + WPR2_ADDR_LO: WPR2_ADDR_LO, + WPR2_ADDR_HI: WPR2_ADDR_HI, + WPR2_ADDR_LO_REL: WPR2_ADDR_LO, + WPR2_ADDR_HI_REL: WPR2_ADDR_HI, +} + class Wpr2Block(Device): """WPR2 address pair as seen by the firmware (host/booter-written).""" @@ -60,13 +71,15 @@ class Wpr2Block(Device): self.regs: dict[int, int] = {} def read(self, offset: int, size: int) -> int | None: - if size == 4 and offset in self.regs: - return self.regs[offset] + key = _KEYS.get(offset) + if size == 4 and key is not None and key in self.regs: + return self.regs[key] return None def write(self, offset: int, size: int, value: int) -> bool: - if size == 4 and offset in (WPR2_ADDR_LO, WPR2_ADDR_HI): - self.regs[offset] = value & 0xFFFFFFFF + key = _KEYS.get(offset) + if size == 4 and key is not None: + self.regs[key] = value & 0xFFFFFFFF return True return False diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/observe.py b/debug/risc-v-gsp-rm-vm/sim/devices/observe.py new file mode 100644 index 0000000..d43cfe6 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/devices/observe.py @@ -0,0 +1,372 @@ +"""Observer-only MMIO windows: record accesses, change no semantics. + +Evidence rule (see ``sim/devices/base.py`` and ``docs/36``): this module +carries **no modelled register behaviour** beyond optional read-back of +values written to registers the header proves plain read/write. Every access +is recorded -- address, window-relative offset, size, direction, +value-if-known, PC, virtual time, window label and register name -- and the +observer then **declines** the access, so the machine's existing +device-map/lenient/unknown path serves it exactly as before. A read value is +never invented: an observe-only read records ``value: null`` and returns +``None``; a storage read before any write is declined the same way. + +Modes +===== + +``observe-only`` (default) + Read and write are recorded and declined. Nothing changes. + +``storage`` + A declared register whose header access type is plain ``rw`` (or ``wo``) + stores the last written value; a later read of a register that has been + written returns that value. Undeclared offsets, ``ro`` registers and + never-written storage registers are still declined. + +Register specs come from the profile (or a runtime window list):: + + "0x24": {"name": "NV_PFB_PRI_MMU_TLB_CTRL", + "width": 4, "access": "rw", + "fields": {"L2_SIZE": [1, 0], ...}} + +``fields`` is optional and only used to decode known values in the trace +(``hi``/``lo`` bit positions, header field names); no field semantics are +implied. + +The access record is the unit of evidence. It lives in +:attr:`ObservedWindow.accesses` for the live run and is exported through the +``greeneye-gmmu-fbif-trace/1`` document built by :func:`build_trace`; +:meth:`ObservedWindow.state` deliberately carries only counters and storage +values so a device snapshot stays small. + +Nothing here is a silicon claim and nothing here authorises hardware: these +windows are simulator-only observability for the GMMU/FBIF configuration +surface (``docs/42-GMMU-FBIF-OBSERVABILITY.md``). +""" +from __future__ import annotations + +from .base import Device + +#: Accepted observer modes. +MODES = ("observe-only", "storage") +#: Accepted per-register access kinds (header access type, plain meaning). +ACCESS_KINDS = ("rw", "ro", "wo") +#: Schema tag for the GMMU/FBIF trace document. +TRACE_SCHEMA = "greeneye-gmmu-fbif-trace/1" + +DEFAULT_MAX_ACCESS_LOG = 65536 + + +def _int(value, default=None): + if value is None: + return default + if isinstance(value, str): + return int(value, 0) + return int(value) + + +def _normalise_spec(offset, spec) -> dict: + """Accept ``"NAME"`` or ``{name, width, access, fields}`` register specs.""" + if isinstance(spec, str): + spec = {"name": spec} + if not isinstance(spec, dict) or not spec.get("name"): + raise ValueError(f"observer register {offset:#x}: spec needs a name") + access = spec.get("access", "rw") + if access not in ACCESS_KINDS: + raise ValueError(f"observer register {offset:#x}: unknown access {access!r}") + width = int(spec.get("width", 4)) + if width not in (1, 2, 4, 8): + raise ValueError(f"observer register {offset:#x}: unsupported width {width}") + fields = {} + for name, span in (spec.get("fields") or {}).items(): + if isinstance(span, str): + hi, lo = span.split(":") + hi, lo = int(hi, 0), int(lo, 0) + else: + hi, lo = int(span[0]), int(span[1]) + if not 0 <= lo <= hi <= 8 * width - 1: + raise ValueError( + f"observer register {offset:#x}: bad field {name} {hi}:{lo}") + fields[str(name)] = (hi, lo) + return {"name": str(spec["name"]), "width": width, + "access": access, "fields": fields} + + +class ObservedWindow(Device): + """One observe-only (or storage) MMIO window; a :class:`~sim.devices.base.Device`. + + ``clock`` is an optional zero-argument callable returning virtual time + (``Machine.insn_count`` follows the existing ``PtimerBlock`` pattern). + The counter advances at :meth:`sim.machine.Machine.run` boundaries, so all + accesses inside one bounded run share the run-start value; that granularity + is documented, not hidden. + """ + + name = "observe" + #: Opt-in: the registry forwards ``pc=`` to devices that set this. + wants_pc = True + + def __init__(self, *, base: int = 0, size: int = 0x1000, label: str | None = None, + mode: str = "observe-only", registers=None, clock=None, + evidence: str = "", max_access_log: int = DEFAULT_MAX_ACCESS_LOG): + super().__init__(base=base, size=size) + self.label = str(label or "observe") + if mode not in MODES: + raise ValueError(f"observer window {self.label}: unknown mode {mode!r}") + self.mode = mode + self.evidence = str(evidence) + self.clock = clock + self.registers: dict[int, dict] = {} + for off, spec in (registers or {}).items(): + self.registers[_int(off)] = _normalise_spec(_int(off), spec) + self.max_access_log = max(0, int(max_access_log)) + self.accesses: list[dict] = [] + self.dropped = 0 + self.counts = {"r": 0, "w": 0, "served": 0, "declined": 0} + self.written: dict[int, int] = {} + + # -- recording --------------------------------------------------------- + + def _record(self, offset: int, size: int, rw: str, value, served: bool, + pc: int) -> None: + spec = self.registers.get(offset) + entry = { + "addr": self.base + offset, + "offset": offset, + "size": size, + "rw": rw, + "value": value, + "pc": pc, + "vt": self.clock() if self.clock is not None else None, + "window": self.label, + "register": spec["name"] if spec is not None else None, + "served": bool(served), + } + if value is not None and spec is not None and spec["fields"]: + entry["decoded"] = { + name: (value >> lo) & ((1 << (hi - lo + 1)) - 1) + for name, (hi, lo) in spec["fields"].items() + } + self.counts[rw] += 1 + self.counts["served" if served else "declined"] += 1 + if len(self.accesses) < self.max_access_log: + self.accesses.append(entry) + else: + self.dropped += 1 + + # -- guest-visible semantics ------------------------------------------ + + def read(self, offset: int, size: int, pc: int = 0) -> int | None: + spec = self.registers.get(offset) + value = None + served = False + if (self.mode == "storage" and spec is not None + and spec["width"] == size and spec["access"] == "rw" + and offset in self.written): + value = self.written[offset] + served = True + self._record(offset, size, "r", value, served, pc) + return value + + def write(self, offset: int, size: int, value: int, pc: int = 0) -> bool: + spec = self.registers.get(offset) + served = False + if (self.mode == "storage" and spec is not None + and spec["width"] == size and spec["access"] in ("rw", "wo")): + mask = (1 << (8 * size)) - 1 + self.written[offset] = value & mask + served = True + self._record(offset, size, "w", value & ((1 << (8 * size)) - 1), served, pc) + return served + + def peek(self, offset: int, size: int) -> int | None: + spec = self.registers.get(offset) + if (self.mode == "storage" and spec is not None + and spec["width"] == size and spec["access"] == "rw" + and offset in self.written): + return self.written[offset] + return None + + # -- state ------------------------------------------------------------- + + def state(self) -> dict: + return { + "label": self.label, + "mode": self.mode, + "counts": dict(self.counts), + "written": {str(k): v for k, v in sorted(self.written.items())}, + "dropped_accesses": self.dropped, + } + + def load_state(self, data: dict) -> None: + counts = data.get("counts", {}) + self.counts = {k: int(counts.get(k, 0)) for k in ("r", "w", "served", "declined")} + self.written = {int(k): int(v) for k, v in data.get("written", {}).items()} + self.dropped = int(data.get("dropped_accesses", 0)) + + def reset(self) -> None: + self.accesses.clear() + self.dropped = 0 + self.counts = {"r": 0, "w": 0, "served": 0, "declined": 0} + self.written.clear() + + def __repr__(self) -> str: # pragma: no cover - cosmetic + return (f"ObservedWindow({self.label} @ {self.base:#x}+{self.size:#x}, " + f"{self.mode}, {len(self.registers)} registers)") + + +def windows_from_section(section: dict) -> list[dict]: + """Expand a profile observer section into per-window declarations. + + A window entry may carry ``count`` and ``stride`` (header-proven register + instances, e.g. the 16 FBPA partitions); labels get an ``-`` suffix + when more than one instance is declared. Window-level ``mode`` and + ``registers`` override the section-level values. + """ + section_regs = section.get("registers") or {} + section_mode = section.get("mode", "observe-only") + section_label = section.get("label", "observe") + out: list[dict] = [] + for win in section.get("windows", ()): + base = _int(win["base"]) + size = _int(win.get("size", 0x1000)) + count = int(win.get("count", 1)) + stride = _int(win.get("stride", size)) + label = str(win.get("label", section_label)) + mode = win.get("mode", section_mode) + regs = win.get("registers", section_regs) + if count < 1: + raise ValueError(f"observer window {label}: count must be >= 1") + for i in range(count): + out.append({ + "base": base + i * stride, + "size": size, + "label": label if count == 1 else f"{label}-{i}", + "mode": mode, + "registers": regs, + }) + return out + + +def parse_window_spec(text: str) -> dict: + """Parse ``BASE:SIZE[:LABEL[:MODE]]`` for a runtime observer window. + + Bases/sizes accept ``0x`` forms. This is the escape hatch for windows + whose base cannot be established from evidence yet (docs/42); it never + invents a value, it only records accesses. + """ + parts = str(text).split(":") + if len(parts) < 2: + raise ValueError(f"observer window spec {text!r}: expected BASE:SIZE[:LABEL[:MODE]]") + base, size = _int(parts[0]), _int(parts[1]) + if base < 0 or size <= 0: + raise ValueError(f"observer window spec {text!r}: bad base/size") + label = parts[2] if len(parts) > 2 and parts[2] else "runtime-observe" + mode = parts[3] if len(parts) > 3 and parts[3] else "observe-only" + if mode not in MODES: + raise ValueError(f"observer window spec {text!r}: unknown mode {mode!r}") + return {"base": base, "size": size, "label": label, "mode": mode, "registers": {}} + + +def collect_observations(registry) -> list[ObservedWindow]: + """All observer windows in a device registry, in registration order.""" + return [d for d in getattr(registry, "devices", ()) if isinstance(d, ObservedWindow)] + + +def _counts(entries) -> dict: + out = {"r": 0, "w": 0, "served": 0} + for entry in entries: + out[entry["rw"]] += 1 + if entry["served"]: + out["served"] += 1 + return out + + +def _decorate(entry: dict, resolve) -> dict: + out = dict(entry) + out["addr"] = hex(entry["addr"]) + out["offset"] = hex(entry["offset"]) + if entry["value"] is not None: + out["value"] = hex(entry["value"]) + if entry["pc"] is not None: + out["pc"] = hex(entry["pc"]) + out["pc_symbol"] = None + if resolve is not None and entry["pc"] is not None: + out["pc_symbol"] = resolve(entry["pc"]) + return out + + +def build_trace(registry, *, image_sha256: str | None = None, + profile: str | None = None, profile_name: str | None = None, + symbols_path: str | None = None, resolve=None) -> dict: + """Build the ``greeneye-gmmu-fbif-trace/1`` document. + + Accesses are grouped per window and per declared register; offsets inside + an observed window that the profile does not name go to ``unclassified`` + (recorded, never guessed). ``resolve`` is an optional ``pc -> str`` + callable (``SymbolDB``-backed in :mod:`sim.gsp_boot`). + + Truncation is never silent: a window's access log is capped at + ``ObservedWindow.max_access_log`` entries (default 65536); + ``dropped_accesses`` reports the accesses beyond the cap in each window + and in the summary, and the summary separates ``accesses`` (total + observed) from ``retained_accesses`` (present in this document). + """ + windows = [] + total = {"windows": 0, "windows_touched": 0, "accesses": 0, + "retained_accesses": 0, "dropped_accesses": 0, + "reads": 0, "writes": 0, "served": 0, "registers_touched": 0} + for dev in collect_observations(registry): + total["windows"] += 1 + win = { + "label": dev.label, + "base": hex(dev.base), + "size": hex(dev.size), + "mode": dev.mode, + "evidence": dev.evidence, + "counts": dict(dev.counts), + "dropped_accesses": dev.dropped, + "retained_accesses": len(dev.accesses), + "registers": [], + "unclassified": {"counts": {"r": 0, "w": 0, "served": 0}, + "accesses": []}, + } + grouped: dict[int, list] = {} + unclassified: list = [] + for entry in dev.accesses: + if entry["offset"] in dev.registers: + grouped.setdefault(entry["offset"], []).append(entry) + else: + unclassified.append(_decorate(entry, resolve)) + for offset in sorted(grouped): + spec = dev.registers[offset] + entries = [_decorate(e, resolve) for e in grouped[offset]] + win["registers"].append({ + "offset": hex(offset), + "name": spec["name"], + "access": spec["access"], + "counts": _counts(entries), + "accesses": entries, + }) + win["unclassified"]["accesses"] = unclassified + win["unclassified"]["counts"] = _counts(unclassified) + observed = dev.counts["r"] + dev.counts["w"] + total["dropped_accesses"] += dev.dropped + if observed: + total["windows_touched"] += 1 + total["accesses"] += observed + total["retained_accesses"] += len(dev.accesses) + total["reads"] += dev.counts["r"] + total["writes"] += dev.counts["w"] + total["served"] += dev.counts["served"] + total["registers_touched"] += len(grouped) + windows.append(win) + doc = { + "schema": TRACE_SCHEMA, + "image_sha256": image_sha256, + "profile": profile, + "profile_name": profile_name, + "symbols": symbols_path, + "summary": total, + "windows": windows, + } + return doc diff --git a/debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py b/debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py new file mode 100644 index 0000000..24381eb --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py @@ -0,0 +1,988 @@ +"""Clean-room decoder for the Turing (TU104) GMMU page-table entry formats. + +This module decodes *captured* GMMU page-table data. It is a decoder, not a +page-table walker: it never dereferences a decoded address and it never fills +a gap with a guess. Every named field below has an exact bit position taken +from the Turing GMMU format definitions; the page-tree geometry (depth -> VA +bits, index width, entry size, page sizes) is taken from the driver's own +page-tree HAL for Turing, which uses the VER2 entry encoding. + +The write-up, the field tables and the uncertainty notes live in +``docs/43-GMMU-PTE-DECODER.md``. + +Formats shipped (evidence: the Turing GMMU register-field definitions and the +driver's page-table encoder behaviour): + +* ``ver2`` -- the 8-byte VER2 leaf PTE and directory PDE, the encoding the + Turing driver programs (``NV_MMU_VER2_PTE`` / ``NV_MMU_VER2_PDE``). +* ``ver2_dual`` -- the 16-byte dual PDE0 slot used for mixed 64K/4K page + tables (``NV_MMU_VER2_DUAL_PDE``): a BIG half (64K) and a SMALL half (4K) + in one 16-byte entry. The BIG address field spans bits 53:4 and stores + the address shifted right by 8; the format also names bit 5 NO_ATS. + Depth-3 page-table allocations are 4 KB aligned, so address bit 9 (the + overlapping bit) is zero in a valid pointer; the decoder reports + ``no_ats`` from bit 5 and excludes that bit from the BIG address. + +Known unsupported encodings (explicit ``E_UNSUPPORTED``, never guessed): the +older non-VER2 PTE/PDE encoding and the "NEW"/VER1 encodings that are also +described by the same register definitions but are not used by the Turing +driver's page-tree HAL. See ``docs/43-GMMU-PTE-DECODER.md`` section 6. + +Reserved-bit policy: any set bit that no named field of the decoded role +covers is reported in ``reserved_bits`` / ``reserved_mask`` -- never silently +dropped and never given an invented meaning. + +Rule: decode captured data only; validate against a capture before trusting. +""" +from __future__ import annotations + +import argparse +import json +import sys +from typing import Any + +# --------------------------------------------------------------------------- +# Errors +# --------------------------------------------------------------------------- + +E_UNSUPPORTED = "E_UNSUPPORTED" +E_TRUNCATED = "E_TRUNCATED" +E_BAD_VALUE = "E_BAD_VALUE" + + +class GmmuDecodeError(ValueError): + """A decode request that the format evidence cannot answer.""" + + def __init__(self, code: str, message: str): + super().__init__(f"{code}: {message}") + self.code = code + self.message = message + + +# --------------------------------------------------------------------------- +# Encoding families +# --------------------------------------------------------------------------- + +KIND_VER2 = "ver2" +KIND_VER2_DUAL = "ver2_dual" + +#: Entry-encoding families this decoder ships. +SUPPORTED_KINDS = (KIND_VER2, KIND_VER2_DUAL) + +#: Page-tree depths the evidence supports, root (0) -> leaf (4). +SUPPORTED_LEVELS = (0, 1, 2, 3, 4) + +#: Depth -> hardware level, VA bits, index width and entry size. +#: +#: Evidence: the driver's page-tree HAL for Turing inherits the Pascal/Volta +#: geometry; the comments in that HAL state the mapping below. Depths 0-2 +#: are pointer directories, depth 3 is the PDE0 slot (a dual 64K/4K entry, +#: or a 2 MB leaf PTE), depth 4 is the 4K/64K leaf page table. +LEVELS = ( + {"depth": 0, "hw": "pde3", "va_bits": (48, 47), "index_bits": 2, + "entry_size": 8, "role": "pde"}, + {"depth": 1, "hw": "pde2", "va_bits": (46, 38), "index_bits": 9, + "entry_size": 8, "role": "pde"}, + {"depth": 2, "hw": "pde1", "va_bits": (37, 29), "index_bits": 9, + "entry_size": 8, "role": "pde"}, + {"depth": 3, "hw": "pde0", "va_bits": (28, 21), "index_bits": 8, + "entry_size": 16, "role": "pde_or_2m_pte"}, + {"depth": 4, "hw": "pte", "va_bits": (20, 12), "index_bits": 9, + "entry_size": 8, "role": "pte"}, +) + +#: Page sizes the format evidence supports, and the depth their entries live at. +PAGE_SIZES = ("4k", "64k", "2m") +PAGE_TABLE_DEPTH = {"2m": 3, "4k": 4, "64k": 4} + +#: PTE aperture field (bits 2:1). +APERTURE_NAMES_PTE = { + 0: "video_memory", + 1: "peer_memory", + 2: "system_coherent_memory", + 3: "system_non_coherent_memory", +} + +#: PDE aperture field (bits 2:1). Note aperture 0 is INVALID here, while for +#: a leaf PTE aperture 0 is video memory -- the two entry roles differ. +APERTURE_NAMES_PDE = { + 0: "invalid", + 1: "video_memory", + 2: "system_coherent_memory", + 3: "system_non_coherent_memory", +} + +#: PTE KIND field (bits 63:56), numeric IDs from the Turing GMMU format +#: definitions. Values not listed are reported as unknown, never guessed. +PTE_KIND_NAMES = { + 0x00: 'PITCH', + 0x01: 'Z16', + 0x02: 'S8', + 0x03: 'S8Z24', + 0x04: 'ZF32_X24S8', + 0x05: 'Z24S8', + 0x06: 'GENERIC_MEMORY', + 0x07: 'INVALID', + 0x08: 'GENERIC_MEMORY_COMPRESSIBLE', + 0x09: 'GENERIC_MEMORY_COMPRESSIBLE_DISABLE_PLC', + 0x0A: 'S8_COMPRESSIBLE_DISABLE_PLC', + 0x0B: 'Z16_COMPRESSIBLE_DISABLE_PLC', + 0x0C: 'S8Z24_COMPRESSIBLE_DISABLE_PLC', + 0x0D: 'ZF32_X24S8_COMPRESSIBLE_DISABLE_PLC', + 0x0E: 'Z24S8_COMPRESSIBLE_DISABLE_PLC', + 0x0F: 'SMSKED_MESSAGE', + 0x10: 'Z16_MS2_2Z', + 0x11: 'Z16_MS2_2C', + 0x12: 'S8Z24_1Z', + 0x13: 'S8Z24_MS2_1Z', + 0x14: 'S8Z24_MS4_1Z', + 0x15: 'S8Z24_MS8_1Z', + 0x16: 'S8Z24_MS16_1Z', + 0x17: 'S8Z24_2CZ', + 0x18: 'S8Z24_MS2_2CZ', + 0x19: 'S8Z24_MS4_2CZ', + 0x1A: 'S8Z24_MS8_2CZ', + 0x1B: 'S8Z24_MS16_2CZ', + 0x1C: 'S8Z24_2CS', + 0x1D: 'S8Z24_MS2_2CS', + 0x1E: 'S8Z24_MS4_2CS', + 0x1F: 'S8Z24_MS8_2CS', + 0x20: 'S8Z24_MS16_2CS', + 0x21: 'S8Z24_4CSZV', + 0x22: 'S8Z24_MS2_4CSZV', + 0x23: 'S8Z24_MS4_4CSZV', + 0x24: 'S8Z24_MS8_4CSZV', + 0x25: 'S8Z24_MS16_4CSZV', + 0x26: 'V8Z24_MS4_VC12', + 0x27: 'V8Z24_MS4_VC4', + 0x28: 'V8Z24_MS8_VC8', + 0x29: 'V8Z24_MS8_VC24', + 0x2A: 'Z16_2C', + 0x2B: 'S8_2S', + 0x2C: 'C32_MS4_4CBRA', + 0x2D: 'C64_MS4_4CBRA', + 0x2E: 'V8Z24_MS4_VC12_1ZV', + 0x2F: 'V8Z24_MS4_VC4_1ZV', + 0x30: 'V8Z24_MS8_VC8_1ZV', + 0x31: 'V8Z24_MS8_VC24_1ZV', + 0x32: 'V8Z24_MS4_VC12_2CS', + 0x33: 'V8Z24_MS4_VC4_2CS', + 0x34: 'V8Z24_MS8_VC8_2CS', + 0x35: 'V8Z24_MS8_VC24_2CS', + 0x36: 'Z16_2CZ', + 0x37: 'Z16_MS2_2CZ', + 0x38: 'Z16_MS4_2CZ', + 0x39: 'Z16_MS8_2CZ', + 0x3A: 'V8Z24_MS4_VC12_2CZV', + 0x3B: 'V8Z24_MS4_VC4_2CZV', + 0x3C: 'V8Z24_MS8_VC8_2CZV', + 0x3D: 'V8Z24_MS8_VC24_2CZV', + 0x3E: 'V8Z24_MS4_VC12_2ZV', + 0x3F: 'V8Z24_MS4_VC4_2ZV', + 0x40: 'V8Z24_MS8_VC8_2ZV', + 0x41: 'V8Z24_MS8_VC24_2ZV', + 0x42: 'V8Z24_MS4_VC12_4CSZV', + 0x43: 'V8Z24_MS4_VC4_4CSZV', + 0x44: 'V8Z24_MS8_VC8_4CSZV', + 0x45: 'V8Z24_MS8_VC24_4CSZV', + 0x46: 'Z16_MS8_2C', + 0x47: 'Z24S8_1Z', + 0x48: 'Z24S8_MS2_1Z', + 0x49: 'Z24S8_MS4_1Z', + 0x4A: 'Z24S8_MS8_1Z', + 0x4B: 'Z24S8_MS16_1Z', + 0x4C: 'Z24S8_2CS', + 0x4D: 'Z24S8_MS2_2CS', + 0x4E: 'Z24S8_MS4_2CS', + 0x4F: 'Z24S8_MS8_2CS', + 0x50: 'Z24S8_MS16_2CS', + 0x51: 'Z24S8_2CZ', + 0x52: 'Z24S8_MS2_2CZ', + 0x53: 'Z24S8_MS4_2CZ', + 0x54: 'Z24S8_MS8_2CZ', + 0x55: 'Z24S8_MS16_2CZ', + 0x56: 'Z24S8_4CSZV', + 0x57: 'Z24S8_MS2_4CSZV', + 0x58: 'Z24S8_MS4_4CSZV', + 0x59: 'Z24S8_MS8_4CSZV', + 0x5A: 'Z24S8_MS16_4CSZV', + 0x5B: 'Z24V8_MS4_VC12', + 0x5C: 'Z24V8_MS4_VC4', + 0x5D: 'Z24V8_MS8_VC8', + 0x5E: 'Z24V8_MS8_VC24', + 0x5F: 'Z16_MS16_2CZ', + 0x60: 'Z16_MS4_2Z', + 0x61: 'Z16_MS8_2Z', + 0x62: 'Z16_MS16_2Z', + 0x63: 'Z24V8_MS4_VC12_1ZV', + 0x64: 'Z24V8_MS4_VC4_1ZV', + 0x65: 'Z24V8_MS8_VC8_1ZV', + 0x66: 'Z24V8_MS8_VC24_1ZV', + 0x67: 'Z24V8_MS4_VC12_2CS', + 0x68: 'Z24V8_MS4_VC4_2CS', + 0x69: 'Z24V8_MS8_VC8_2CS', + 0x6A: 'Z24V8_MS8_VC24_2CS', + 0x6B: 'Z16_2Z', + 0x6C: 'Z16_MS16_2C', + 0x6F: 'Z24V8_MS4_VC12_2CZV', + 0x70: 'Z24V8_MS4_VC4_2CZV', + 0x71: 'Z24V8_MS8_VC8_2CZV', + 0x72: 'Z24V8_MS8_VC24_2CZV', + 0x73: 'Z24V8_MS4_VC12_2ZV', + 0x74: 'Z24V8_MS4_VC4_2ZV', + 0x75: 'Z24V8_MS8_VC8_2ZV', + 0x76: 'Z24V8_MS8_VC24_2ZV', + 0x77: 'Z24V8_MS4_VC12_4CSZV', + 0x78: 'Z24V8_MS4_VC4_4CSZV', + 0x79: 'Z24V8_MS8_VC8_4CSZV', + 0x7A: 'Z24V8_MS8_VC24_4CSZV', + 0x7B: 'ZF32', + 0x7C: 'ZF32_1Z', + 0x7D: 'ZF32_MS2_1Z', + 0x7E: 'ZF32_MS4_1Z', + 0x7F: 'ZF32_MS8_1Z', + 0x80: 'ZF32_MS16_1Z', + 0x81: 'ZF32_2CS', + 0x82: 'ZF32_MS2_2CS', + 0x83: 'ZF32_MS4_2CS', + 0x84: 'ZF32_MS8_2CS', + 0x85: 'ZF32_MS16_2CS', + 0x86: 'ZF32_2CZ', + 0x87: 'ZF32_MS2_2CZ', + 0x88: 'ZF32_MS4_2CZ', + 0x89: 'ZF32_MS8_2CZ', + 0x8A: 'ZF32_MS16_2CZ', + 0x8B: 'X8Z24_X16V8S8_MS4_VC12', + 0x8C: 'X8Z24_X16V8S8_MS4_VC4', + 0x8D: 'X8Z24_X16V8S8_MS8_VC8', + 0x8E: 'X8Z24_X16V8S8_MS8_VC24', + 0x8F: 'X8Z24_X16V8S8_MS4_VC12_1CS', + 0x90: 'X8Z24_X16V8S8_MS4_VC4_1CS', + 0x91: 'X8Z24_X16V8S8_MS8_VC8_1CS', + 0x92: 'X8Z24_X16V8S8_MS8_VC24_1CS', + 0x97: 'X8Z24_X16V8S8_MS4_VC12_1ZV', + 0x98: 'X8Z24_X16V8S8_MS4_VC4_1ZV', + 0x99: 'X8Z24_X16V8S8_MS8_VC8_1ZV', + 0x9A: 'X8Z24_X16V8S8_MS8_VC24_1ZV', + 0x9B: 'X8Z24_X16V8S8_MS4_VC12_1CZV', + 0x9C: 'X8Z24_X16V8S8_MS4_VC4_1CZV', + 0x9D: 'X8Z24_X16V8S8_MS8_VC8_1CZV', + 0x9E: 'X8Z24_X16V8S8_MS8_VC24_1CZV', + 0x9F: 'X8Z24_X16V8S8_MS4_VC12_2CS', + 0xA0: 'X8Z24_X16V8S8_MS4_VC4_2CS', + 0xA1: 'X8Z24_X16V8S8_MS8_VC8_2CS', + 0xA2: 'X8Z24_X16V8S8_MS8_VC24_2CS', + 0xA3: 'X8Z24_X16V8S8_MS4_VC12_2CSZV', + 0xA4: 'X8Z24_X16V8S8_MS4_VC4_2CSZV', + 0xA5: 'X8Z24_X16V8S8_MS8_VC8_2CSZV', + 0xA6: 'X8Z24_X16V8S8_MS8_VC24_2CSZV', + 0xA7: 'ZF32_X16V8S8_MS4_VC12', + 0xA8: 'ZF32_X16V8S8_MS4_VC4', + 0xA9: 'ZF32_X16V8S8_MS8_VC8', + 0xAA: 'ZF32_X16V8S8_MS8_VC24', + 0xAB: 'ZF32_X16V8S8_MS4_VC12_1CS', + 0xAC: 'ZF32_X16V8S8_MS4_VC4_1CS', + 0xAD: 'ZF32_X16V8S8_MS8_VC8_1CS', + 0xAE: 'ZF32_X16V8S8_MS8_VC24_1CS', + 0xB3: 'ZF32_X16V8S8_MS4_VC12_1ZV', + 0xB4: 'ZF32_X16V8S8_MS4_VC4_1ZV', + 0xB5: 'ZF32_X16V8S8_MS8_VC8_1ZV', + 0xB6: 'ZF32_X16V8S8_MS8_VC24_1ZV', + 0xB7: 'ZF32_X16V8S8_MS4_VC12_1CZV', + 0xB8: 'ZF32_X16V8S8_MS4_VC4_1CZV', + 0xB9: 'ZF32_X16V8S8_MS8_VC8_1CZV', + 0xBA: 'ZF32_X16V8S8_MS8_VC24_1CZV', + 0xBB: 'ZF32_X16V8S8_MS4_VC12_2CS', + 0xBC: 'ZF32_X16V8S8_MS4_VC4_2CS', + 0xBD: 'ZF32_X16V8S8_MS8_VC8_2CS', + 0xBE: 'ZF32_X16V8S8_MS8_VC24_2CS', + 0xBF: 'ZF32_X16V8S8_MS4_VC12_2CSZV', + 0xC0: 'ZF32_X16V8S8_MS4_VC4_2CSZV', + 0xC1: 'ZF32_X16V8S8_MS8_VC8_2CSZV', + 0xC2: 'ZF32_X16V8S8_MS8_VC24_2CSZV', + 0xC3: 'Z16_MS4_2C', + 0xC4: 'ZF32_X24S8_1CS', + 0xC5: 'ZF32_X24S8_MS2_1CS', + 0xC6: 'ZF32_X24S8_MS4_1CS', + 0xC7: 'ZF32_X24S8_MS8_1CS', + 0xC8: 'ZF32_X24S8_MS16_1CS', + 0xCB: 'SMHOST_MESSAGE', + 0xCC: 'C32_MS2_4CBRA', + 0xCD: 'C64_MS2_4CBRA', + 0xCE: 'ZF32_X24S8_2CSZV', + 0xCF: 'ZF32_X24S8_MS2_2CSZV', + 0xD0: 'ZF32_X24S8_MS4_2CSZV', + 0xD1: 'ZF32_X24S8_MS8_2CSZV', + 0xD2: 'ZF32_X24S8_MS16_2CSZV', + 0xD3: 'ZF32_X24S8_2CS', + 0xD4: 'ZF32_X24S8_MS2_2CS', + 0xD5: 'ZF32_X24S8_MS4_2CS', + 0xD6: 'ZF32_X24S8_MS8_2CS', + 0xD7: 'ZF32_X24S8_MS16_2CS', + 0xD8: 'C32_2C', + 0xD9: 'C32_2CBR', + 0xDA: 'C32_2CBA', + 0xDB: 'C32_2CRA', + 0xDC: 'C32_2BRA', + 0xDD: 'C32_MS2_2C', + 0xDE: 'C32_MS2_2CBR', + 0xDF: 'C32_MS4_2C', + 0xE0: 'C32_MS4_2CBR', + 0xE1: 'C32_MS4_2CBA', + 0xE2: 'C32_MS4_2CRA', + 0xE3: 'C32_MS4_2BRA', + 0xE4: 'C32_MS8_MS16_2C', + 0xE5: 'C32_MS8_MS16_2CRA', + 0xE6: 'C64_2C', + 0xE7: 'C64_2CBR', + 0xE8: 'C64_2CBA', + 0xE9: 'C64_2CRA', + 0xEA: 'C64_2BRA', + 0xEB: 'C64_MS2_2C', + 0xEC: 'C64_MS2_2CBR', + 0xED: 'C64_MS4_2C', + 0xEE: 'C64_MS4_2CBR', + 0xEF: 'C64_MS4_2CBA', + 0xF0: 'C64_MS4_2CRA', + 0xF1: 'C64_MS4_2BRA', + 0xF2: 'C64_MS8_MS16_2C', + 0xF3: 'C64_MS8_MS16_2CRA', + 0xF4: 'C128_2C', + 0xF5: 'C128_2CR', + 0xF6: 'C128_MS2_2C', + 0xF7: 'C128_MS2_2CR', + 0xF8: 'C128_MS4_2C', + 0xF9: 'C128_MS4_2CR', + 0xFA: 'C128_MS8_MS16_2C', + 0xFB: 'C128_MS8_MS16_2CR', + 0xFC: 'X8C24', + 0xFD: 'PITCH_NO_SWIZZLE', + 0xFE: 'GENERIC_16BX2', +} + +# --------------------------------------------------------------------------- +# Field positions (lo, hi), inclusive. A field is only listed when the +# format definitions prove its position; there are no inferred fields. +# --------------------------------------------------------------------------- + +_VER2_PTE_FIELDS = ( + ("valid", 0, 0), + ("aperture", 1, 2), + ("volatile", 3, 3), + ("encrypted", 4, 4), + ("privilege", 5, 5), + ("read_only", 6, 6), + ("atomic_disable", 7, 7), + ("address_sys", 8, 53), + ("address_vid", 8, 32), + ("peer_id", 33, 35), + ("comptagline", 36, 55), + ("kind", 56, 63), +) + +_VER2_PDE_FIELDS = ( + ("is_pde_pointer", 0, 0), + ("aperture", 1, 2), + ("volatile", 3, 3), + ("no_ats", 5, 5), + ("address_sys", 8, 53), + ("address_vid", 8, 32), + ("peer_id", 33, 35), +) + +# The dual PDE is a 16-byte (128-bit) entry: the BIG half is the low 64 bits, +# the SMALL half is the high 64 bits. Bit 0 (IS_PDE) applies to the whole +# entry and is stored in the low bits. +_VER2_DUAL_PDE_FIELDS = ( + ("is_pde_pointer", 0, 0), + ("aperture_big", 1, 2), + ("volatile_big", 3, 3), + ("no_ats", 5, 5), + ("address_big_sys", 4, 53), + ("address_big_vid", 4, 32), + ("peer_id_big", 33, 35), + ("aperture_small", 65, 66), + ("volatile_small", 67, 67), + ("address_small_sys", 72, 117), + ("address_small_vid", 72, 96), + ("peer_id_small", 97, 99), +) + +# Address field shifts from the format definitions. +_PTE_ADDRESS_SHIFT = 12 +_PDE_ADDRESS_SHIFT = 12 +_DUAL_ADDRESS_SHIFT = 12 +_DUAL_ADDRESS_BIG_SHIFT = 8 + + +def _field(value: int, lo: int, hi: int) -> int: + return (value >> lo) & ((1 << (hi - lo + 1)) - 1) + + +def _named_mask(fields) -> int: + mask = 0 + for _name, lo, hi in fields: + mask |= ((1 << (hi - lo + 1)) - 1) << lo + return mask + + +def _reserved(value: int, fields, width: int) -> tuple[list[int], int]: + """Set bits not covered by any named field: (bit list, mask).""" + mask = _named_mask(fields) + leftover = value & ~mask + bits = [b for b in range(width) if (leftover >> b) & 1] + return bits, leftover + + +def _check_value(value: int, width: int, what: str) -> None: + if not isinstance(value, int) or isinstance(value, bool): + raise GmmuDecodeError(E_BAD_VALUE, f"{what} must be an int") + if value < 0 or value >= (1 << width): + raise GmmuDecodeError( + E_BAD_VALUE, f"{what} {value:#x} does not fit in {width} bits") + + +def _check_kind(kind: str, allowed, what: str) -> None: + if kind not in allowed: + raise GmmuDecodeError( + E_UNSUPPORTED, + f"{what} {kind!r} is not supported (supported: {', '.join(allowed)})") + + +def _check_level(level, allowed, what: str) -> None: + if level is None: + return + if not isinstance(level, int) or isinstance(level, bool): + raise GmmuDecodeError(E_BAD_VALUE, f"{what} level must be an int or None") + if level not in allowed: + raise GmmuDecodeError( + E_UNSUPPORTED, + f"{what} level {level} is not supported " + f"(supported: {', '.join(str(x) for x in allowed)})") + + +def _page_size_for_level(level) -> str | None: + if level == 3: + return "2m" + if level == 4: + return "4k_or_64k" + return None + + +# --------------------------------------------------------------------------- +# Leaf PTE (VER2, 8 bytes) +# --------------------------------------------------------------------------- + +def decode_pte(value: int, *, pte_kind: str = KIND_VER2, + level: int | None = None) -> dict: + """Decode one 8-byte VER2 leaf PTE. + + ``pte_kind`` selects the entry encoding; only ``ver2`` is a leaf encoding. + ``level``, when given, pins the page-tree depth (3 = 2 MB leaf, 4 = + 4 KB/64 KB leaf) and is otherwise reported as undetermined. + """ + _check_kind(pte_kind, SUPPORTED_KINDS, "pte_kind") + if pte_kind != KIND_VER2: + raise GmmuDecodeError( + E_UNSUPPORTED, f"{pte_kind} is a directory entry, not a leaf PTE") + _check_value(value, 64, "PTE value") + _check_level(level, (3, 4), "PTE") + + valid = bool(_field(value, 0, 0)) + aperture = _field(value, 1, 2) + aperture_name = APERTURE_NAMES_PTE[aperture] + address_sys = _field(value, 8, 53) + address_vid = _field(value, 8, 32) + peer_id = _field(value, 33, 35) + comptagline = _field(value, 36, 55) + kind = _field(value, 56, 63) + + if aperture_name == "system_coherent_memory" or \ + aperture_name == "system_non_coherent_memory": + address = address_sys << _PTE_ADDRESS_SHIFT + address_complete = True + elif aperture_name in ("video_memory", "peer_memory"): + # Bits 36:12 of the physical address are in address_vid. The high + # address bits are overloaded into comptagline only for the + # documented 47-bit peer case; otherwise comptagline is a + # compression tag, so a full address is not claimed here. + address = address_vid << _PTE_ADDRESS_SHIFT + address_complete = False + else: # pragma: no cover - all 4 aperture values are named + address = None + address_complete = False + + reserved_bits, reserved_mask = _reserved(value, _VER2_PTE_FIELDS, 64) + return { + "role": "pte", + "level": level, + "level_candidates": [3, 4] if level is None else [level], + "page_size": _page_size_for_level(level), + "valid": valid, + "aperture": aperture, + "aperture_name": aperture_name, + "volatile": bool(_field(value, 3, 3)), + "encrypted": bool(_field(value, 4, 4)), + "privilege": bool(_field(value, 5, 5)), + "read_only": bool(_field(value, 6, 6)), + "writable": not bool(_field(value, 6, 6)), + "atomic_disable": bool(_field(value, 7, 7)), + "address": address, + "address_complete": address_complete, + "address_sys": address_sys, + "address_vid": address_vid, + "peer_id": peer_id, + "comptagline": comptagline, + "kind": kind, + "kind_name": PTE_KIND_NAMES.get(kind), + "kind_known": kind in PTE_KIND_NAMES, + "reserved_bits": reserved_bits, + "reserved_mask": reserved_mask, + } + + +# --------------------------------------------------------------------------- +# Directory PDE (VER2, 8 bytes) +# --------------------------------------------------------------------------- + +def decode_pde(value: int, *, pde_kind: str = KIND_VER2, + level: int | None = None) -> dict: + """Decode one 8-byte VER2 directory entry. + + Bit 0 is the leaf/pointer discriminator: the driver's encoder writes 0 + for a pointer to a lower table and 1 for a 2 MB leaf PTE. A value whose + bit 0 is 1 is a leaf, not a directory entry, and is rejected here; use + :func:`decode_pte` (or :func:`decode_entry` with ``role='auto'``). + """ + _check_kind(pde_kind, SUPPORTED_KINDS, "pde_kind") + if pde_kind != KIND_VER2: + raise GmmuDecodeError( + E_UNSUPPORTED, f"{pde_kind} is a dual entry, not a single PDE") + _check_value(value, 64, "PDE value") + _check_level(level, (0, 1, 2, 3), "PDE") + + if _field(value, 0, 0): + raise GmmuDecodeError( + E_UNSUPPORTED, + "PDE bit 0 is set: this is a leaf (2 MB) PTE, not a directory entry") + + aperture = _field(value, 1, 2) + aperture_name = APERTURE_NAMES_PDE[aperture] + address_sys = _field(value, 8, 53) + address_vid = _field(value, 8, 32) + peer_id = _field(value, 33, 35) + + if aperture_name in ("system_coherent_memory", "system_non_coherent_memory"): + address = address_sys << _PDE_ADDRESS_SHIFT + address_complete = True + elif aperture_name in ("video_memory", "peer_memory"): + address = address_vid << _PDE_ADDRESS_SHIFT + address_complete = False + else: # aperture 0 = invalid + address = None + address_complete = False + + reserved_bits, reserved_mask = _reserved(value, _VER2_PDE_FIELDS, 64) + return { + "role": "pde", + "level": level, + "level_candidates": [0, 1, 2, 3] if level is None else [level], + "is_pde_pointer": True, + "is_leaf": False, + "aperture": aperture, + "aperture_name": aperture_name, + "volatile": bool(_field(value, 3, 3)), + "no_ats": bool(_field(value, 5, 5)), + "address": address, + "address_complete": address_complete, + "address_sys": address_sys, + "address_vid": address_vid, + "peer_id": peer_id, + "reserved_bits": reserved_bits, + "reserved_mask": reserved_mask, + } + + +# --------------------------------------------------------------------------- +# Dual PDE0 slot (VER2, 16 bytes) +# --------------------------------------------------------------------------- + +def decode_dual_pde(value: int, *, pde_kind: str = KIND_VER2_DUAL, + level: int | None = None) -> dict: + """Decode one 16-byte VER2 dual PDE0 slot (BIG + SMALL halves). + + ``value`` is the 128-bit little-endian slot value: the BIG half is the + low 64 bits, the SMALL half the high 64 bits. Bit 0 belongs to the whole + slot. The BIG half carries 64K-page-table state, the SMALL half 4K. + """ + _check_kind(pde_kind, SUPPORTED_KINDS, "pde_kind") + if pde_kind != KIND_VER2_DUAL: + raise GmmuDecodeError( + E_UNSUPPORTED, f"{pde_kind} is a single entry, not a dual PDE") + _check_value(value, 128, "dual PDE value") + _check_level(level, (3,), "dual PDE") + + aperture_big = _field(value, 1, 2) + aperture_small = _field(value, 65, 66) + name_big = APERTURE_NAMES_PDE[aperture_big] + name_small = APERTURE_NAMES_PDE[aperture_small] + + address_big_sys = _field(value, 4, 53) + address_big_vid = _field(value, 4, 32) + peer_id_big = _field(value, 33, 35) + address_small_sys = _field(value, 72, 117) + address_small_vid = _field(value, 72, 96) + peer_id_small = _field(value, 97, 99) + + # Bit 5 of the entry is named NO_ATS and also lies inside the BIG + # address field (bits 53:4), i.e. at field bit 1 (entry bit 5). A valid + # depth-3 allocation is 4 KB aligned, so address bit 9 (the overlapping + # bit) is zero; exclude it from the reconstructed address and report it + # as the flag the definitions name. + _NO_ATS_FIELD_MASK = 1 << (5 - 4) + if name_big in ("system_coherent_memory", "system_non_coherent_memory"): + address_big = (address_big_sys & ~_NO_ATS_FIELD_MASK) << _DUAL_ADDRESS_BIG_SHIFT + complete_big = True + elif name_big in ("video_memory", "peer_memory"): + address_big = (address_big_vid & ~_NO_ATS_FIELD_MASK) << _DUAL_ADDRESS_BIG_SHIFT + complete_big = False + else: + address_big = None + complete_big = False + + if name_small in ("system_coherent_memory", "system_non_coherent_memory"): + address_small = address_small_sys << _DUAL_ADDRESS_SHIFT + complete_small = True + elif name_small in ("video_memory", "peer_memory"): + address_small = address_small_vid << _DUAL_ADDRESS_SHIFT + complete_small = False + else: + address_small = None + complete_small = False + + reserved_bits, reserved_mask = _reserved(value, _VER2_DUAL_PDE_FIELDS, 128) + return { + "role": "dual_pde", + "level": level, + "level_candidates": [3] if level is None else [level], + "is_pde_pointer": not bool(_field(value, 0, 0)), + "is_leaf": bool(_field(value, 0, 0)), + "no_ats": bool(_field(value, 5, 5)), + "big": { + "aperture": aperture_big, + "aperture_name": name_big, + "volatile": bool(_field(value, 3, 3)), + "address": address_big, + "address_complete": complete_big, + "address_sys": address_big_sys, + "address_vid": address_big_vid, + "peer_id": peer_id_big, + }, + "small": { + "aperture": aperture_small, + "aperture_name": name_small, + "volatile": bool(_field(value, 67, 67)), + "address": address_small, + "address_complete": complete_small, + "address_sys": address_small_sys, + "address_vid": address_small_vid, + "peer_id": peer_id_small, + }, + "reserved_bits": reserved_bits, + "reserved_mask": reserved_mask, + } + + +# --------------------------------------------------------------------------- +# Role dispatch +# --------------------------------------------------------------------------- + +def decode_entry(value: int, *, kind: str = KIND_VER2, role: str = "auto", + level: int | None = None) -> dict: + """Decode one entry with an explicit or inferred role. + + ``role`` is one of ``auto``, ``pte``, ``pde``, ``dual_pde``. ``auto`` + uses the format's own bit-0 discriminator for 8-byte VER2 entries + (bit 0 set = leaf PTE, clear = directory PDE); the inferred role is + reported with ``role_inferred=True`` and the level stays undetermined + unless the caller pins it. + """ + if role == "auto": + if kind == KIND_VER2_DUAL: + decoded = decode_dual_pde(value, pde_kind=kind, level=level) + decoded["role_inferred"] = level is None + return decoded + _check_value(value, 64, "entry value") + if _field(value, 0, 0): + decoded = decode_pte(value, pte_kind=kind, level=level) + else: + decoded = decode_pde(value, pde_kind=kind, level=level) + decoded["role_inferred"] = level is None + return decoded + if role == "pte": + return decode_pte(value, pte_kind=kind, level=level) + if role == "pde": + return decode_pde(value, pde_kind=kind, level=level) + if role == "dual_pde": + return decode_dual_pde(value, pde_kind=kind, level=level) + raise GmmuDecodeError( + E_UNSUPPORTED, + f"role {role!r} is not supported (auto, pte, pde, dual_pde)") + + +# --------------------------------------------------------------------------- +# Table decode +# --------------------------------------------------------------------------- + +def _normalize_levels(levels) -> tuple[tuple[int, ...], int | None]: + """Return (allowed levels, pinned level or None) from ``levels``.""" + if levels is None: + return tuple(SUPPORTED_LEVELS), None + requested: tuple[int, ...] + if isinstance(levels, int) and not isinstance(levels, bool): + requested = (levels,) + else: + try: + requested = tuple(int(x) for x in levels) + except (TypeError, ValueError): + raise GmmuDecodeError( + E_BAD_VALUE, "levels must be an int, a sequence of ints, or None") + if not requested: + raise GmmuDecodeError(E_BAD_VALUE, "levels must not be empty") + for level in requested: + if level not in SUPPORTED_LEVELS: + raise GmmuDecodeError( + E_UNSUPPORTED, + f"level {level} is not supported " + f"(supported: {', '.join(str(x) for x in SUPPORTED_LEVELS)})") + pinned = requested[0] if len(set(requested)) == 1 else None + return tuple(sorted(set(requested))), pinned + + +def _infer_role(bit0: int, allowed: tuple[int, ...]) -> str: + plausible_leaf = [lv for lv in allowed if lv in (3, 4)] + plausible_pde = [lv for lv in allowed if lv in (0, 1, 2, 3)] + if bit0: + if not plausible_leaf: + raise GmmuDecodeError( + E_UNSUPPORTED, + f"entry bit 0 is set (leaf) but levels {allowed} have no leaf level") + return "pte" + if not plausible_pde: + raise GmmuDecodeError( + E_UNSUPPORTED, + f"entry bit 0 is clear (directory) but levels {allowed} have no directory level") + return "pde" + + +def _candidates_for(role: str, allowed: tuple[int, ...]) -> list[int]: + if role == "pte": + return [lv for lv in allowed if lv in (3, 4)] + if role == "pde": + return [lv for lv in allowed if lv in (0, 1, 2, 3)] + return list(allowed) + + +def decode_table(blob: bytes, *, base_pa: int, entry_size: int = 8, + levels=None, kind: str = KIND_VER2) -> list[dict]: + """Decode a captured page-table blob into a list of entry dicts. + + ``blob`` is read little-endian, ``entry_size`` bytes per entry. + ``base_pa`` is the GPU physical address of the blob's first byte and is + reported per entry as ``entry_pa`` (it is not dereferenced). + + ``kind`` selects the entry encoding: ``ver2`` (8-byte PTE/PDE) or + ``ver2_dual`` (16-byte dual PDE0). ``entry_size`` must match the kind. + + ``levels`` optionally pins the page-tree depth of this blob (0-4) or a + sequence of acceptable depths; ``None`` (the default) uses the full + supported set and lets each entry's bit-0 discriminator choose between + the leaf-PTE and directory-PDE views, reporting the level as + undetermined (``level=None`` plus ``level_candidates``) rather than + guessing one. Any depth outside the supported set raises + ``E_UNSUPPORTED``. + + An empty blob decodes to ``[]``; a blob whose length is not a multiple + of ``entry_size`` raises ``E_TRUNCATED`` (trailing bytes are never + silently dropped). + """ + _check_kind(kind, SUPPORTED_KINDS, "kind") + expected = 8 if kind == KIND_VER2 else 16 + if entry_size != expected: + raise GmmuDecodeError( + E_UNSUPPORTED, + f"kind {kind!r} uses {expected}-byte entries, got entry_size={entry_size}") + if not isinstance(base_pa, int) or isinstance(base_pa, bool) or base_pa < 0: + raise GmmuDecodeError(E_BAD_VALUE, "base_pa must be a non-negative int") + if not isinstance(blob, (bytes, bytearray, memoryview)): + raise GmmuDecodeError(E_BAD_VALUE, "blob must be bytes-like") + blob = bytes(blob) + allowed, pinned = _normalize_levels(levels) + + if len(blob) == 0: + return [] + if len(blob) % entry_size != 0: + raise GmmuDecodeError( + E_TRUNCATED, + f"blob length {len(blob)} is not a multiple of entry_size={entry_size}") + + entries = [] + for index in range(len(blob) // entry_size): + offset = index * entry_size + value = int.from_bytes(blob[offset:offset + entry_size], "little") + + if kind == KIND_VER2_DUAL: + if pinned is not None and pinned != 3: + raise GmmuDecodeError( + E_UNSUPPORTED, + f"dual PDE entries live at level 3, got levels={levels}") + decoded = decode_dual_pde(value, level=3 if pinned == 3 else None) + level_reported = 3 if pinned == 3 else None + decoded["role_inferred"] = False + else: + bit0 = value & 1 + if pinned is None: + role = _infer_role(bit0, allowed) + decoded = decode_entry(value, kind=kind, role=role, level=None) + level_reported = None + else: + level = pinned + if level == 4: + role = "pte" + elif level in (0, 1, 2): + role = "pde" + else: # level 3 + role = "pte" if bit0 else "pde" + decoded = decode_entry(value, kind=kind, role=role, level=level) + level_reported = level + decoded["role_inferred"] = level_reported is None + if level_reported is None: + decoded["level_candidates"] = _candidates_for( + decoded["role"], allowed) + + entry: dict[str, Any] = { + "index": index, + "entry_pa": base_pa + offset, + "value": value, + } + entry.update(decoded) + entry["encoding"] = kind + entries.append(entry) + return entries + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def _parse_int(text: str) -> int: + try: + return int(text, 0) + except ValueError: + raise argparse.ArgumentTypeError(f"not an integer: {text!r}") + + +def _parse_levels(text: str): + if text is None: + return None + try: + return [int(part, 0) for part in text.split(",") if part != ""] + except ValueError: + raise argparse.ArgumentTypeError(f"not a level list: {text!r}") + + +def _render_entry(entry: dict) -> str: + role = entry["role"] + head = (f"index={entry['index']} pa={entry['entry_pa']:#x} " + f"role={role} level={entry['level']} value={entry['value']:#x}") + if role == "pte": + fields = (f"valid={entry['valid']} aperture={entry['aperture_name']} " + f"volatile={entry['volatile']} encrypted={entry['encrypted']} " + f"privilege={entry['privilege']} read_only={entry['read_only']} " + f"atomic_disable={entry['atomic_disable']} " + f"address={entry['address'] if entry['address'] is None else hex(entry['address'])} " + f"address_complete={entry['address_complete']} " + f"peer_id={entry['peer_id']} comptagline={entry['comptagline']:#x} " + f"kind={entry['kind']:#x} kind_name={entry['kind_name']}") + elif role == "pde": + fields = (f"aperture={entry['aperture_name']} volatile={entry['volatile']} " + f"no_ats={entry['no_ats']} " + f"address={entry['address'] if entry['address'] is None else hex(entry['address'])} " + f"address_complete={entry['address_complete']}") + else: + big, small = entry["big"], entry["small"] + fields = (f"big=({big['aperture_name']}, " + f"{big['address'] if big['address'] is None else hex(big['address'])}) " + f"small=({small['aperture_name']}, " + f"{small['address'] if small['address'] is None else hex(small['address'])}) " + f"no_ats={entry['no_ats']}") + tail = (f"reserved_bits={entry['reserved_bits']} " + f"reserved_mask={entry['reserved_mask']:#x}") + return f"{head} {fields} {tail}" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="python3 -m sim.gmmu_pte", + description=("Decode captured TU104 GMMU page-table entries (read-only, " + "stdlib-only; decode captured data only and validate " + "against a capture before trusting).")) + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument("--value", type=_parse_int, + help="single entry value, e.g. --value 0x...") + source.add_argument("--blob", help="file containing a captured table blob") + parser.add_argument("--base-pa", type=_parse_int, default=0, + help="GPU physical address of the blob's first byte") + parser.add_argument("--kind", default=KIND_VER2, choices=SUPPORTED_KINDS, + help="entry encoding family (default: ver2)") + parser.add_argument("--entry-size", type=int, default=8, + help="bytes per entry (default: 8; ver2_dual uses 16)") + parser.add_argument("--levels", type=_parse_levels, default=None, + help="page-tree depth or comma-separated depths (0-4)") + parser.add_argument("--role", default="auto", + choices=("auto", "pte", "pde", "dual_pde"), + help="entry role for --value (default: auto)") + parser.add_argument("--json", action="store_true", + help="emit JSON (sorted keys; deterministic)") + args = parser.parse_args(argv) + + try: + if args.value is not None: + level = None + if args.levels is not None: + if len(args.levels) != 1: + raise GmmuDecodeError( + E_BAD_VALUE, "--value accepts exactly one level") + level = args.levels[0] + entry = decode_entry(args.value, kind=args.kind, role=args.role, + level=level) + entry.setdefault("index", 0) + entry.setdefault("entry_pa", args.base_pa) + entry["value"] = args.value + entry["encoding"] = args.kind + entries = [entry] + else: + with open(args.blob, "rb") as handle: + blob = handle.read() + entries = decode_table(blob, base_pa=args.base_pa, + entry_size=args.entry_size, + levels=args.levels, kind=args.kind) + except OSError as exc: + print(f"gmmu_pte: cannot read {args.blob}: {exc}", file=sys.stderr) + return 2 + except GmmuDecodeError as exc: + print(f"gmmu_pte: {exc}", file=sys.stderr) + return 2 + + if args.json: + print(json.dumps(entries, indent=2, sort_keys=True)) + else: + for entry in entries: + print(_render_entry(entry)) + print(f"# {len(entries)} entr{'y' if len(entries) == 1 else 'ies'} " + f"(kind={args.kind}, entry_size={args.entry_size})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py b/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py index c586ab3..a6f2545 100644 --- a/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py +++ b/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py @@ -22,6 +22,16 @@ The boot shim supplies reset-time state that the real SEC2 booter/host would provide (bootargs and the register arguments); every shim value is an explicit, documented assumption in the profile and the session metadata. The device model itself never fakes a register read. + +GMMU/FBIF observability (``--trace-out``, default off): the profile's +``gmmu``/``fbif`` sections declare observe-only register windows +(:mod:`sim.devices.observe`); every access in them is recorded (never served, +never faked) and, when requested, exported as a +``greeneye-gmmu-fbif-trace/1`` document. ``--symbols`` optionally resolves +recorded PCs through a ``greeneye-symbols/1`` JSON database, and +``--observe-window BASE:SIZE[:LABEL[:MODE]]`` adds a runtime window list for +surfaces whose base is not established yet. The count summary is always part +of the boot report; see ``docs/42-GMMU-FBIF-OBSERVABILITY.md``. """ from __future__ import annotations @@ -35,7 +45,14 @@ from dataclasses import dataclass, field from greeneye.device_map import firmware_sha256 from greeneye.elf import load_elf from greeneye.errors import GreenEyeError +from greeneye.symbols import SymbolDB from sim.devices import load_profile, registry_from_profile +from sim.libos import LibosSyscalls, PROCESSOR_SUSPEND, TASK_PORT_SEND_RECV_WAIT +from sim.devices.observe import ( + ObservedWindow, + build_trace, + parse_window_spec, +) from sim.machine import ( R_BP, R_BUDGET, @@ -89,6 +106,10 @@ LIBOS_MEMORY_REGION_LOC_SYSMEM = 1 WPRM_ID8 = 0x5750524D # 'WPRM' +#: ``libos_bootstrap_args_t``: IMEM then DMEM ``base/size/bs`` u64 fields. +BOOTSTRAP_ARGS_SIZE = 6 * 8 + + def _int(value, default=None): if value is None: return default @@ -131,6 +152,25 @@ def build_bootargs(machine, *, bootargs_pa: int, core_dump_pa: int, # initArgs[1..2] stay zero (id8 == 0 terminates the array). +def build_bootstrap_args(machine, *, pa: int, imem_base_pa: int, imem_size: int, + imem_bs_size: int, dmem_base_pa: int, dmem_size: int, + dmem_bs_size: int) -> None: + """Write the ``libos_bootstrap_args_t`` the bootloader passes in ``a0``. + + Assumption (profile-recorded): the real libos bootloader builds this + structure (IMEM/DMEM base+size+bs) from its TCM configuration and calls + the firmware entry with a pointer to it; we reconstruct the documented + layout. Values are declared in the profile, never derived from silicon. + """ + machine.write_mem(pa, bytes(BOOTSTRAP_ARGS_SIZE)) + _write_u64(machine, pa + 0x00, imem_base_pa) + _write_u64(machine, pa + 0x08, imem_size) + _write_u64(machine, pa + 0x10, imem_bs_size) + _write_u64(machine, pa + 0x18, dmem_base_pa) + _write_u64(machine, pa + 0x20, dmem_size) + _write_u64(machine, pa + 0x28, dmem_bs_size) + + def build_wpr_meta(machine, *, wpr_meta_pa: int, elf_pa: int, elf_size: int, wpr_start: int, wpr_end: int, boot_bin_offset: int, gsp_fw_offset: int, @@ -206,7 +246,10 @@ class GspBootResult: device_state_sha256: str | None = None registers: dict = field(default_factory=dict) shim: dict = field(default_factory=dict) + libos: dict = field(default_factory=dict) milestones: list = field(default_factory=list) + trace_summary: dict | None = None + trace_path: str | None = None device_map: object | None = field(default=None, repr=False) def to_dict(self) -> dict: @@ -232,7 +275,12 @@ class GspBootResult: "device_state_sha256": self.device_state_sha256, "registers": self.registers, "boot_shim": self.shim, + "libos": self.libos, "milestones": list(self.milestones), + "gmmu_fbif_trace": { + "path": self.trace_path, + "summary": self.trace_summary, + }, } @@ -263,8 +311,18 @@ def run_boot( name: str = "gsp-boot", use_shim: bool = True, shim_overrides: dict | None = None, + trace_out: str | None = None, + symbols_path: str | None = None, + observe_windows: list[str] | None = None, ) -> GspBootResult: - """Run one bounded boot attempt and record a session.""" + """Run one bounded boot attempt and record a session. + + ``trace_out`` (default off) writes the ``greeneye-gmmu-fbif-trace/1`` + observer document to that path (a directory gets ``gmmu-fbif-trace.json`` + inside); ``symbols_path`` resolves recorded PCs through a + ``greeneye-symbols/1`` JSON database; ``observe_windows`` adds runtime + observer windows (``BASE:SIZE[:LABEL[:MODE]]``, repeatable). + """ if instructions <= 0 or instructions > MAX_ATTEMPT_INSTRUCTIONS: raise GreenEyeError( f"instructions must be in 1..{MAX_ATTEMPT_INSTRUCTIONS}, got {instructions}") @@ -274,6 +332,15 @@ def run_boot( return GspBootResult(ran=False, reason=f"profile absent: {profile_path}", image=image) + resolve_pc = None + if symbols_path: + db = SymbolDB() + db.load_json(symbols_path) + + def resolve_pc(pc, _db=db): + sym = _db.resolve(pc) + return sym.label(pc) if sym is not None else None + profile = load_profile(profile_path) image_obj = load_elf(image) if not image_obj.is_riscv: @@ -299,6 +366,16 @@ def run_boot( machine = session.target.machine machine.devices = registry_from_profile(profile, machine=machine) machine.translator = getattr(machine.devices, "dma", None) + libos = LibosSyscalls() + machine.syscall_handler = libos.handle + for spec in observe_windows or (): + win = parse_window_spec(spec) + machine.devices.add(ObservedWindow( + base=win["base"], size=win["size"], label=win["label"], + mode=win["mode"], registers=win["registers"], + evidence="runtime --observe-window", + clock=machine.insn_count, + )) shim_meta = {"enabled": bool(use_shim), "assumptions": []} if use_shim: @@ -309,6 +386,7 @@ def run_boot( _apply_boot_shim(machine, shim, image_obj, shim_meta) milestones_hit: list[str] = [] + trace_path: str | None = None try: reason, pc, a, b = _run_with_milestones(machine, instructions, milestones_hit) model = machine.cycle_model @@ -320,8 +398,19 @@ def run_boot( state_sha = hashlib.sha256(state_json.encode()).hexdigest() if state else None registers = {f"x{i}": machine.x[i] for i in range(32) if i and machine.x[i]} registers["pc"] = pc - rung = _rung_from_run(machine, shim_meta, milestones_hit) + rung = _rung_from_run(machine, shim_meta, milestones_hit, libos) shim_meta["rung"] = rung + trace_doc = build_trace( + reg, + image_sha256=sha, + profile=os.path.basename(profile_path), + profile_name=profile.get("name"), + symbols_path=symbols_path, + resolve=resolve_pc, + ) if reg is not None else None + trace_summary = trace_doc["summary"] if trace_doc is not None else None + if trace_out and trace_doc is not None: + trace_path = _write_trace_file(trace_out, trace_doc) meta = { "kind": "gsp-boot", "profile": os.path.basename(profile_path), @@ -341,7 +430,13 @@ def run_boot( "device_accesses": len(getattr(reg, "accesses", ())), "device_state_sha256": state_sha, "boot_shim": shim_meta, + "mpu": machine.mpu.state(), + "libos": libos.state(), } + if trace_summary is not None: + meta["gmmu_fbif"] = trace_summary + if trace_path is not None: + meta["gmmu_fbif_trace"] = trace_path if getattr(session.recorder, "dir", None) is not None: if state is not None: rel = session.recorder.blob( @@ -349,6 +444,12 @@ def run_boot( json.dumps(state, indent=1, sort_keys=True, default=str).encode(), ) meta["device_state_blob"] = rel + if trace_doc is not None and trace_out: + rel = session.recorder.blob( + "gmmu-fbif-trace.json", + json.dumps(trace_doc, indent=1, sort_keys=True).encode(), + ) + meta["gmmu_fbif_trace_blob"] = rel session.recorder.meta.update(meta) session.note( "gsp_boot", @@ -383,7 +484,10 @@ def run_boot( device_state_sha256=state_sha, registers=registers, shim=shim_meta, + libos=libos.state(), milestones=list(milestones_hit), + trace_summary=trace_summary, + trace_path=trace_path, ) @@ -408,8 +512,8 @@ def _run_with_milestones(machine, budget: int, hit: list[str]): return R_BUDGET, machine.pc, 0, 0 -def _rung_from_run(machine, shim_meta: dict, milestones_hit) -> str: - """Observed rung from milestones and the unknown-MMIO surface. +def _rung_from_run(machine, shim_meta: dict, milestones_hit, libos=None) -> str: + """Observed rung from milestones, libos services and the unknown-MMIO surface. B0: the first unmodelled access is the PMC ``BOOT_42`` chip-ID read. B1: the device model served the whole run (no unknown MMIO) and the boot @@ -417,10 +521,17 @@ def _rung_from_run(machine, shim_meta: dict, milestones_hit) -> str: B2: the boot shim was consumed and the firmware reached its relocation stage (milestone ``relocation_copy``) -- boot-stage progress beyond the bootargs consumer. + B3: the libos init path completed its services (TCM/pagetable + registration, DMA flush) and the message-loop task blocks in the + port wait -- the CSB/TRANSCOM poll itself is host-traffic-triggered + and remains owed to the message-injection rung (docs/35 §3.3 rev). """ first_unknown = machine.mmio_log[0] if machine.mmio_log else None if first_unknown is not None and first_unknown["addr"] == 0x2000000000000A00: return "B0" + stopped = getattr(libos, "stopped", None) if libos is not None else None + if stopped and stopped.get("num") in (TASK_PORT_SEND_RECV_WAIT, PROCESSOR_SUSPEND): + return "B3" if "relocation_copy" in milestones_hit: return "B2" return "B1" @@ -431,6 +542,8 @@ def _rung_note(rung: str) -> str: "B0": "first MMIO is the unmodelled PMC BOOT_42 chip-ID read", "B1": "device model serves the boot path; bootargs consumer reached", "B2": "boot-stage progress past the bootargs consumer (relocation)", + "B3": "init complete; message-loop task blocks in the port wait " + "(CSB poll awaits host traffic)", }.get(rung, rung) @@ -472,6 +585,24 @@ def _apply_boot_shim(machine, shim: dict, image_obj, shim_meta: dict) -> None: shim_meta["assumptions"].append( "GspFwWprMeta reconstructed from the documented layout (header); " "real value is booter-provided") + bootstrap = shim.get("bootstrap_args") + if bootstrap: + args_pa = _int(bootstrap.get("pa", 0x81000)) + build_bootstrap_args( + machine, + pa=args_pa, + imem_base_pa=_int(bootstrap.get("imem_base_pa", 0x0)), + imem_size=_int(bootstrap.get("imem_size", 0x10000)), + imem_bs_size=_int(bootstrap.get("imem_bs_size", 0x0)), + dmem_base_pa=_int(bootstrap.get("dmem_base_pa", 0x100000000)), + dmem_size=_int(bootstrap.get("dmem_size", 0x10000)), + dmem_bs_size=_int(bootstrap.get("dmem_bs_size", 0x0)), + ) + shim_meta["assumptions"].append( + "libos_bootstrap_args_t (IMEM/DMEM base+size+bs) reconstructed at " + "0x%x from the documented layout; real value is bootloader-built " + "and passed in a0" % args_pa) + shim_meta["bootstrap_args_pa"] = args_pa shim_meta["applied"] = True shim_meta["registers"] = {k: registers[k] for k in sorted(registers)} image_at = shim.get("image_at") @@ -481,13 +612,27 @@ def _apply_boot_shim(machine, shim: dict, image_obj, shim_meta: dict) -> None: blob = fh.read() machine.write_mem(addr, blob) shim_meta["assumptions"].append( - "raw firmware image mirrored at 0x%x (DMEM) so the app's own " - "self-relocation copy is self-consistent; real value is " - "booter-DMA-provided" % addr) + "raw firmware image mirrored at 0x%x (DMEM); legacy fallback for " + "profiles without a bootstrap-args record (no longer used by the " + "tu104-570.144 profile: the MMU model resolves the copy source)" + % addr) shim_meta["image_at"] = addr shim_meta["image_bytes"] = len(blob) +def _write_trace_file(path: str, doc: dict) -> str: + """Write the observer trace document; a directory gets the canonical name.""" + target = path + if os.path.isdir(path) or path.endswith(os.sep): + target = os.path.join(path, "gmmu-fbif-trace.json") + parent = os.path.dirname(os.path.abspath(target)) + os.makedirs(parent, exist_ok=True) + with open(target, "w", encoding="utf-8") as fh: + json.dump(doc, fh, indent=1, sort_keys=True) + fh.write("\n") + return target + + def _print_result(result: GspBootResult, out=sys.stdout) -> None: print(f"image: {result.image}", file=out) print(f"profile: {result.profile_name} ({result.profile})", file=out) @@ -504,6 +649,14 @@ def _print_result(result: GspBootResult, out=sys.stdout) -> None: first = result.first_unknown_mmio print(f"first unknown MMIO: pc={first['pc']:#x} addr={first['addr']:#x} " f"size={first['size']} rw={first['rw']}", file=out) + if result.trace_summary is not None: + summary = result.trace_summary + print(f"GMMU/FBIF observers: {summary['windows']} windows, " + f"{summary['accesses']} accesses ({summary['reads']} r / " + f"{summary['writes']} w, {summary['served']} served, " + f"{summary['windows_touched']} windows touched)", file=out) + if result.trace_path is not None: + print(f"GMMU/FBIF trace: {result.trace_path}", file=out) print("note: device semantics carry their evidence in docs/36; " "boot-shim values are declared assumptions", file=out) @@ -520,6 +673,15 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--name", default="gsp-boot") parser.add_argument("--no-shim", action="store_true", help="run without the boot shim (pure device model)") + parser.add_argument("--trace-out", default=None, + help="write the greeneye-gmmu-fbif-trace/1 observer " + "document to this file (or directory); default off") + parser.add_argument("--symbols", default=None, + help="greeneye-symbols/1 JSON database for PC resolution") + parser.add_argument("--observe-window", action="append", default=None, + metavar="BASE:SIZE[:LABEL[:MODE]]", + help="extra runtime observer window (repeatable); " + "observe-only unless MODE=storage") parser.add_argument("--json", action="store_true") args = parser.parse_args(argv) try: @@ -530,6 +692,9 @@ def main(argv: list[str] | None = None) -> int: record=args.record, name=args.name, use_shim=not args.no_shim, + trace_out=args.trace_out, + symbols_path=args.symbols, + observe_windows=args.observe_window, ) except GreenEyeError as exc: print(f"[gsp-boot] error: {exc}", file=sys.stderr) diff --git a/debug/risc-v-gsp-rm-vm/sim/libos.py b/debug/risc-v-gsp-rm-vm/sim/libos.py new file mode 100644 index 0000000..e8280fd --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/libos.py @@ -0,0 +1,217 @@ +"""Minimal, honest libos syscall surface for the native GSP boot. + +Evidence kind: **firmware use + RTOS syscall table.** The GSP application is +a libos task that requests kernel services with ``ecall`` (the syscall number +travels in ``t0``); the native ``Machine`` stops on ``ecall`` by default, so +booting the real image past kernel init needs a deterministic service model. + +Rules: + +* every requested syscall is recorded (number, name, arguments, pc, + instruction count) so the session shows exactly what the firmware asked for; +* only *no-ops and modelled* services are acknowledged: task yield, cache + invalidation, external-interrupt arming, profiler enable/query, and the + timer read (returned from the modeled instruction count, labelled + modeled); +* **waits are never faked**: ``PROCESSOR_SUSPEND``, + ``TASK_PORT_SEND_RECV_WAIT``, ``TASK_PANIC`` and every unimplemented + syscall return *not handled*, which stops the run with ``R_ECALL`` so the + caller can classify the stop (message-loop wait / panic / unsupported + service) instead of inventing progress; +* task switching, DMA, memory queries, pagetable registration, partition + switch and interrupt delivery are not modelled. + +No timing, scheduling or security claim is made; this is development +tooling for the boot ladder (docs/35 §3.3). +""" +from __future__ import annotations + +# Syscall numbers (RTOS ``libos_syscalls.h``). +TASK_PORT_SEND_RECV_WAIT = 1 +TASK_YIELD = 2 +PROCESSOR_SUSPEND = 3 +DCACHE_INVALIDATE = 4 +MEMORY_QUERY = 5 +TASK_STATE_QUERY = 6 +SHUTTLE_RESET = 7 +TASK_DMA_REGISTER_TCM = 8 +TASK_DMA_COPY = 9 +TASK_DMA_FLUSH = 10 +TASK_MEMORY_READ = 11 +INIT_REGISTER_PAGETABLES = 12 +PROFILER_ENABLE = 13 +TASK_EXIT = 14 +TASK_PANIC = 15 +INTERRUPT_ARM_EXTERNAL = 16 +TIMER_GET_NS = 17 +DCACHE_INVALIDATE_ALL = 18 +PROFILER_GET_COUNTER_MASK = 19 +PARTITION_SWITCH = 20 +LIMIT_POW2 = 32 + +NAMES = { + TASK_PORT_SEND_RECV_WAIT: "task_port_send_recv_wait", + TASK_YIELD: "task_yield", + PROCESSOR_SUSPEND: "processor_suspend", + DCACHE_INVALIDATE: "dcache_invalidate", + MEMORY_QUERY: "memory_query", + TASK_STATE_QUERY: "task_state_query", + SHUTTLE_RESET: "shuttle_reset", + TASK_DMA_REGISTER_TCM: "task_dma_register_tcm", + TASK_DMA_COPY: "task_dma_copy", + TASK_DMA_FLUSH: "task_dma_flush", + TASK_MEMORY_READ: "task_memory_read", + INIT_REGISTER_PAGETABLES: "init_register_pagetables", + PROFILER_ENABLE: "profiler_enable", + TASK_EXIT: "task_exit", + TASK_PANIC: "task_panic", + INTERRUPT_ARM_EXTERNAL: "interrupt_arm_external", + TIMER_GET_NS: "timer_get_ns", + DCACHE_INVALIDATE_ALL: "dcache_invalidate_all", + PROFILER_GET_COUNTER_MASK: "profiler_get_counter_mask", + PARTITION_SWITCH: "partition_switch", + LIMIT_POW2: "limit_pow2", +} + +#: Services acknowledged as no-ops (result 0); everything else stops the run. +_NOOP_SYSCALLS = frozenset({ + TASK_YIELD, DCACHE_INVALIDATE, DCACHE_INVALIDATE_ALL, PROFILER_ENABLE, +}) + + +class LibosSyscalls: + """Deterministic libos service model: record, acknowledge, or stop.""" + + name = "libos-syscalls" + + def __init__(self) -> None: + self.calls: list[dict] = [] + self.counts: dict[int, int] = {} + self.armed_mask = 0 + #: Last registered TCM bounce buffer (``task_dma_register_tcm``). + self.tcm: dict | None = None + #: Last inverted-pagetable registration (``init_register_pagetables``). + self.pagetables: dict | None = None + self.dma_copies = 0 + self.dma_bytes = 0 + #: (number, a0, a1, pc, insns) of the last request. + self.last: tuple | None = None + #: The request that stopped the run (not handled), if any. + self.stopped: dict | None = None + + def handle(self, machine, pc: int = 0): + """``Machine`` ecall hook: ``(handled, result)``. + + Unhandled requests leave ``machine.run`` stopping with ``R_ECALL``; + the number is still recorded here and in ``stopped``. + """ + num = machine.x[5] + a0, a1 = machine.x[10], machine.x[11] + entry = { + "num": num, + "name": NAMES.get(num, f"unknown_{num}"), + "a0": a0, + "a1": a1, + "pc": pc, + "insns": machine.insns, + } + self.calls.append(entry) + self.counts[num] = self.counts.get(num, 0) + 1 + self.last = (num, a0, a1, pc, machine.insns) + if num == INTERRUPT_ARM_EXTERNAL: + self.armed_mask |= a0 + return True, 0 + if num == TIMER_GET_NS: + # Modeled time: instruction count * 1 ns (docs/36: modeled, not + # calibrated; no timing claim). + return True, machine.insns + if num == TASK_DMA_REGISTER_TCM: + # The kernel validates the VA against its pagetable and stores it + # as the DMA bounce buffer; without a paging model the region is + # recorded and the observed result (LIBOS_OK) returned. + self.tcm = {"va": a0, "size": a1} + return True, 0 + if num == TASK_DMA_COPY: + return self._dma_copy(machine, a0, a1, machine.x[12], pc) + if num == INIT_REGISTER_PAGETABLES: + # The kernel builds its inverted page table from this registration + # and returns LIBOS_OK. The simulator does not model the + # inverted table (DMA resolution goes through the flat RAM + MPU + # + aperture seam); the registration is recorded as evidence. + self.pagetables = {"base": a0, "entries": a1, "probe_count": machine.x[12]} + return True, 0 + if num == TASK_DMA_FLUSH: + return True, 0 + if num in _NOOP_SYSCALLS: + return True, 0 + if num == PROFILER_GET_COUNTER_MASK: + return True, 0 + self.stopped = entry + return False, None + + # -- DMA service model ------------------------------------------------- + + def _resolve(self, machine, va: int): + """VA -> simulator RAM offset through the MPU and aperture seam.""" + if machine.mpu.active: + translated = machine.mpu.translate(va) + if translated is not None: + va = translated + off = va - machine.ram_base + if 0 <= off < machine.ram_size: + return off + translator = machine.translator + if translator is not None: + sim = translator.translate(va) + if sim is not None: + off = sim - machine.ram_base + if 0 <= off < machine.ram_size: + return off + return None + + def _dma_copy(self, machine, dst_va: int, src_va: int, size: int, pc: int = 0): + """Modeled DMA copy: equivalent to a RAM memcpy in the flat model. + + The real kernel drives the falcon DMA engine through TCM; the + simulator keeps all regions in one RAM, so the copy effect is the + same. Both sides must resolve or the run stops (never a faked copy). + """ + if size == 0: + return True, 0 + dst = self._resolve(machine, dst_va) + src = self._resolve(machine, src_va) + if dst is None or src is None: + self.stopped = {"num": TASK_DMA_COPY, "name": "task_dma_copy", + "a0": dst_va, "a1": src_va, "size": size, + "reason": "unresolved", "pc": pc} + return False, None + ram = machine.ram + if (0 <= dst <= machine.ram_size - size + and 0 <= src <= machine.ram_size - size): + ram[dst:dst + size] = bytes(ram[src:src + size]) + else: + # Straddling or out-of-range: byte-wise through the bounds. + for i in range(size): + if dst + i >= machine.ram_size or src + i >= machine.ram_size: + self.stopped = {"num": TASK_DMA_COPY, "name": "task_dma_copy", + "a0": dst_va, "a1": src_va, "size": size, + "reason": "out-of-range", "pc": pc} + return False, None + ram[dst + i] = ram[src + i] + machine.invalidate() + self.dma_copies += 1 + self.dma_bytes += size + return True, 0 + + def state(self) -> dict: + return { + "calls": len(self.calls), + "counts": {str(k): v for k, v in sorted(self.counts.items())}, + "armed_mask": self.armed_mask, + "tcm": self.tcm, + "pagetables": self.pagetables, + "dma_copies": self.dma_copies, + "dma_bytes": self.dma_bytes, + "last": list(self.last) if self.last else None, + "stopped": self.stopped, + } diff --git a/debug/risc-v-gsp-rm-vm/sim/machine.py b/debug/risc-v-gsp-rm-vm/sim/machine.py index 9908753..7634ce1 100644 --- a/debug/risc-v-gsp-rm-vm/sim/machine.py +++ b/debug/risc-v-gsp-rm-vm/sim/machine.py @@ -19,6 +19,7 @@ import mmap import struct from .cycles import DEFAULT_MODEL +from .mpu import MSTATUS, WATCHED_CSRS, RiscvMpu from .decode import ( A_ADD, A_AND, @@ -84,6 +85,9 @@ R_ECALL = 6 #: Accepted ``unknown_mmio`` policies for accesses outside RAM/UART/the map. UNKNOWN_MMIO_MODES = ("fault", "lenient") +#: CSRs whose writes update the MMU model (M-mode protection unit + mstatus). +_MPU_CSRS = frozenset(WATCHED_CSRS) | {MSTATUS} + UART_BASE = 0x10000000 UART_SIZE = 0x100 @@ -232,6 +236,10 @@ class Machine: self.device_map = device_map self.devices = devices self.translator = translator # aperture -> RAM seam (sim.devices.dma) + self.mpu = RiscvMpu() # M-mode protection unit (sim.mpu) + #: Optional guest-ECALL service (``handle(machine) -> (handled, ret)``); + #: when absent (or unhandled) ``run`` stops with ``R_ECALL`` as before. + self.syscall_handler = None self.unknown_mmio = unknown_mmio self.learn_mmio = bool(learn_mmio) self.bias = bias # PC relocation for learned-map firmware addresses @@ -506,6 +514,10 @@ class Machine: a = b = 0 traced = trace is not None active = False + # M-mode protection unit (sim.mpu): translation is consulted only when + # an enabled entry exists; no match is treated as identity. + mpu = self.mpu + mpu_on = mpu.active # Modeled cycle accounting (sim.cycles): pure table lookups, no timing. cycle_base = self._cycle_base cycle_op = self._cycle_op @@ -520,7 +532,12 @@ class Machine: a = pc break - off = pc - base + fetch_pc = pc + if mpu_on: + translated = mpu.translate(pc) + if translated is not None: + fetch_pc = translated + off = fetch_pc - base if off < 0 or off + 4 > limit: reason = R_FAULT a = 1 @@ -558,6 +575,12 @@ class Machine: b = hit break o = ea - base + mem_ea = ea + if mpu_on: + translated = mpu.translate(ea) + if translated is not None: + mem_ea = translated + o = mem_ea - base if 0 <= o <= limit - size: if active: trace.mem(insn_pc, ea, size, False, self.insns + n) @@ -587,7 +610,7 @@ class Machine: pc = nxt else: try: - v = self._mmio_load(ea, size, pc) + v = self._mmio_load(mem_ea, size, pc) except Fault as f: reason = R_FAULT a = f.cause @@ -613,6 +636,12 @@ class Machine: b = hit break o = ea - base + mem_ea = ea + if mpu_on: + translated = mpu.translate(ea) + if translated is not None: + mem_ea = translated + o = mem_ea - base value = x[r2] if 0 <= o <= limit - size: if active: @@ -627,17 +656,20 @@ class Machine: ram[o] = value & 0xFF # Self-modifying code: drop any cached decode overlapping # the written range (firmware patches code at runtime). - p = ea & ~1 - end = ea + size - while p < end: - cache.pop(p, None) - p += 2 + # Both the virtual address and its translated backing are + # dropped so a fetch through either mapping re-decodes. + for target in ((ea, mem_ea) if mem_ea != ea else (ea,)): + p = target & ~1 + end = target + size + while p < end: + cache.pop(p, None) + p += 2 if counting: cycle_sum += cycle_mem pc = nxt else: try: - self._mmio_store(ea, size, value, pc) + self._mmio_store(mem_ea, size, value, pc) except Fault as f: reason = R_FAULT a = f.cause @@ -864,6 +896,9 @@ class Machine: a = 2 b = csr break + if csr in _MPU_CSRS: + mpu.on_csr_write(csr, self.csrs.get(csr, 0)) + mpu_on = mpu.active pc = nxt elif kind == K_AMO: @@ -938,9 +973,20 @@ class Machine: elif kind == K_ECALL: if single_step: pc = nxt + elif self.syscall_handler is not None: + handled, result = self.syscall_handler(self, pc) + if not handled: + reason = R_ECALL + a = pc + b = x[5] # syscall number (t0), for diagnostics + break + if result is not None: + x[10] = result & MASK64 + pc = nxt else: reason = R_ECALL a = pc + b = x[5] break elif kind == K_MRET: @@ -975,15 +1021,25 @@ class Machine: # -- guest memory helpers (AMO) ---------------------------------------- def _guest_load(self, addr: int, width: int, pc: int = 0) -> int: - off = addr - self.ram_base + mem = addr + if self.mpu.active: + translated = self.mpu.translate(addr) + if translated is not None: + mem = translated + off = mem - self.ram_base if 0 <= off <= self.ram_size - width: if width == 8: return self._u64.unpack_from(self.ram, off)[0] return self._u32.unpack_from(self.ram, off)[0] - return self._mmio_load(addr, width, pc) + return self._mmio_load(mem, width, pc) def _guest_store(self, addr: int, width: int, value: int, pc: int = 0) -> None: - off = addr - self.ram_base + mem = addr + if self.mpu.active: + translated = self.mpu.translate(addr) + if translated is not None: + mem = translated + off = mem - self.ram_base if 0 <= off <= self.ram_size - width: if width == 8: self._u64p(self.ram, off, value) @@ -995,7 +1051,7 @@ class Machine: self._cache.pop(p, None) p += 2 return - self._mmio_store(addr, width, value, pc) + self._mmio_store(mem, width, value, pc) #: Task-facing alias: the native simulator machine is :class:`Machine`. diff --git a/debug/risc-v-gsp-rm-vm/sim/mpu.py b/debug/risc-v-gsp-rm-vm/sim/mpu.py new file mode 100644 index 0000000..3f24358 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/mpu.py @@ -0,0 +1,154 @@ +"""RISC-V MMU (NVMPU) CSR model for the GSP core. + +Evidence kind: **hardware CSR/field header + firmware use + observed access +sequence.** + +The GSP RISC-V core boots in *bare* mode and then enables a small protection +unit through CSRs; the firmware maps resident code/data into TCM and uses a +scratch window to copy it there. Without this model the flat-RAM simulator +serves those accesses at their virtual addresses and the relocated copy reads +the wrong backing (the B2 ceiling in ``docs/36-GSP-DEVICE-MODEL.md``). + +Modelled CSRs (M-mode values from the hardware CSR header; the firmware writes +exactly these - observed at the 570.144 bootstrap ``0x4007000``..``0x4007C00``): + +============================================ ====== ========================= +CSR value semantics +============================================ ====== ========================= +``MMPUIDX`` (0x7C8) entry INDEX field 5:0 +``MMPUVA`` (0x7C9) VA BASE 63:12, VLD bit 0 +``MMPUPA`` (0x7CA) PA BASE 63:12 +``MMPURNG`` (0x7CB) range RANGE 63:12 (a size) +``MMPUATTR`` (0x7CC) attr recorded, not enforced +``mstatus.VM`` (0x300) mode 0x1E = NVMPU (enabled) +============================================ ====== ========================= + +Semantics modelled: + +* an entry is *valid* when ``MMPUVA.VLD`` is set; writes to ``MMPUVA`` with + VLD clear disable the selected entry (the firmware's clear sequence); +* translation requires ``mstatus.VM == NVMPU`` (``0x1E``, field bits 28:24); + before the enable write the CSR entries exist but no access is translated; +* lookup is **lowest-index-first** (the firmware's own bootstrap comment: + "the MPU gives precedence to lower mappings"); a matching entry maps + ``addr`` to ``pa + (addr - va_base)`` when + ``va_base <= addr < va_base + range``; +* **no match is not a fault here**: the machine treats "no matching entry" as + identity. This is deliberately more lenient than silicon (which would + fault under the kernel's management) because the simulator keeps local + memory flat and the boot path's identity mappings are self-consistent; the + choice is recorded in the session and in ``docs/36`` §6. + +Permissions and cache/coherence attributes are recorded for evidence but not +enforced; no timing or side-channel claim is made. This module has no device +or host dependency and is deterministic. +""" +from __future__ import annotations + +#: CSR numbers (M-mode protection unit; hardware CSR header values). +MMPUIDX = 0x7C8 +MMPUVA = 0x7C9 +MMPUPA = 0x7CA +MMPURNG = 0x7CB +MMPUATTR = 0x7CC + +#: CSRs this model consumes when written by the guest. +WATCHED_CSRS = (MMPUIDX, MMPUVA, MMPUPA, MMPURNG, MMPUATTR) + +MSTATUS = 0x300 +MSTATUS_VM_SHIFT = 24 +MSTATUS_VM_MASK = 0x1F << MSTATUS_VM_SHIFT +#: mstatus.VM value that enables the protection unit (header addendum). +MSTATUS_VM_NVMPU = 0x1E + +#: Field masks (header: BASE/RANGE fields start at bit 12, 4 KiB granularity). +_PAGE_MASK = ~0xFFF & ((1 << 64) - 1) +_INDEX_MASK = 0x3F +_MAX_ENTRIES = 64 + + +class RiscvMpu: + """MMPU entry table + mstatus enable state (deterministic, serialisable).""" + + def __init__(self) -> None: + self.index = 0 + self.mstatus = 0 + self.entries: dict[int, dict] = {} + #: Ascending [(va_base, pa, range)] of valid entries; kept in sync on + #: every CSR write so translation is a short loop. + self._active: list[tuple[int, int, int]] = [] + self.enabled = False + + # -- guest CSR writes -------------------------------------------------- + + def on_csr_write(self, csr: int, value: int) -> None: + """Apply one guest CSR write (``value`` already masked to 64 bits).""" + if csr == MMPUIDX: + self.index = value & _INDEX_MASK + elif csr == MMPUVA: + entry = self.entries.setdefault(self.index, self._blank()) + entry["va"] = value & _PAGE_MASK + entry["valid"] = bool(value & 1) + self._refresh() + elif csr == MMPUPA: + entry = self.entries.setdefault(self.index, self._blank()) + entry["pa"] = value & _PAGE_MASK + elif csr == MMPURNG: + entry = self.entries.setdefault(self.index, self._blank()) + entry["range"] = value & _PAGE_MASK + elif csr == MMPUATTR: + entry = self.entries.setdefault(self.index, self._blank()) + entry["attr"] = value + elif csr == MSTATUS: + self.mstatus = value & ((1 << 64) - 1) + self.enabled = ((self.mstatus & MSTATUS_VM_MASK) >> MSTATUS_VM_SHIFT + == MSTATUS_VM_NVMPU) + + @staticmethod + def _blank() -> dict: + return {"va": 0, "pa": 0, "range": 0, "attr": 0, "valid": False} + + def _refresh(self) -> None: + self._active = [ + (e["va"], e["pa"], e["range"]) + for _, e in sorted(self.entries.items()) + if e.get("valid") and e.get("range") + ] + + # -- translation ------------------------------------------------------- + + @property + def active(self) -> bool: + """True when a matching entry could translate (fast-path switch).""" + return self.enabled and bool(self._active) + + def translate(self, addr: int) -> int | None: + """Virtual -> physical for ``addr``, or ``None`` when unmatched.""" + if not self.enabled: + return None + for va, pa, rng in self._active: + off = addr - va + if 0 <= off < rng: + return pa + off + return None + + # -- evidence / snapshot ---------------------------------------------- + + def state(self) -> dict: + return { + "enabled": self.enabled, + "mstatus": self.mstatus, + "index": self.index, + "entries": [ + {"index": i, **{k: e[k] for k in ("va", "pa", "range", "attr", "valid")}} + for i, e in sorted(self.entries.items()) + if e.get("valid") or e.get("pa") or e.get("range") or e.get("attr") + ], + } + + def reset(self) -> None: + self.index = 0 + self.mstatus = 0 + self.entries.clear() + self._active = [] + self.enabled = False diff --git a/debug/risc-v-gsp-rm-vm/sim/profiles/tu104-570.144.json b/debug/risc-v-gsp-rm-vm/sim/profiles/tu104-570.144.json index 919dffe..91f4ed5 100644 --- a/debug/risc-v-gsp-rm-vm/sim/profiles/tu104-570.144.json +++ b/debug/risc-v-gsp-rm-vm/sim/profiles/tu104-570.144.json @@ -69,6 +69,808 @@ "evidence": "PMU stub: no PMU register has been observed; every access stays unhandled and logged.", "windows": [] }, + "gmmu": { + "evidence": "GMMU/PRI-MMU register surface: NV_PFB_PRI_MMU_* offsets inside the PFB block (block range 0x00100000-0x00100FFF), the WPR/VPR/lock cluster (0x1FA7D8-0x1FA830) and the HSMMU block (0x001FAC00-0x001FAFFF) from the hardware register/field headers. Observe-only: no register semantics are modelled and no read value is invented; the B0-B2 runs did not touch these windows; the B3 wave's WPR2 read pair (0x1FA824/0x1FA828) falls in the wpr-lock window and is recorded observe-only (0 served).", + "mode": "observe-only", + "windows": [ + {"base": "0x2000000000100C80", "size": "0x280", "label": "gmmu-pri-mmu", "registers": {"0x0": {"name": "NV_PFB_PRI_MMU_CTRL", "fields": {"VM_PG_SIZE": [0, 0], "VOL_FAULT": [1, 1], "COMP_FAULT": [2, 2], "MISS_GRAN": [4, 3], "CACHE_MODE": [6, 5], "PDE_CACHE_MODE": [8, 7], "L2_SEND_MODE": [9, 9], "WORK_CREATION_DISABLE": [10, 10], "USE_PDB_BIG_PAGE_SIZE": [11, 11], "USE_FULL_COMP_TAG_LINE": [12, 12], "PRI_FIFO_ERROR_CLEAR": [13, 13], "PRI_FIFO_ERROR": [14, 14], "PRI_FIFO_EMPTY": [15, 15], "PRI_FIFO_SPACE": [23, 16], "ATOMIC_CAPABILITY_MODE": [25, 24], "STRONG_BIT_ATOMIC_CYA": [26, 26], "STRONG_BIT_OVERRIDE_ATOMIC_DISABLE": [26, 26], "ATOMIC_CAPABILITY_SYS_NCOH_MODE": [27, 27], "MMU_APERTURE": [29, 28], "MMU_VOL": [30, 30], "MMU_DISABLE": [31, 31]}}, "0x4": {"name": "NV_PFB_PRI_MMU_PM"}, "0x8": {"name": "NV_PFB_PRI_MMU_PM_ADDR_LOW"}, "0xc": {"name": "NV_PFB_PRI_MMU_PM_ADDR_HIGH"}, "0x10": {"name": "NV_PFB_PRI_MMU_PM_UNIT_MASK"}, "0x14": {"name": "NV_PFB_PRI_MMU_PM_REQ_MASK"}, "0x18": {"name": "NV_PFB_PRI_MMU_CG"}, "0x1c": {"name": "NV_PFB_PRI_MMU_CG1"}, "0x20": {"name": "NV_PFB_PRI_MMU_PG"}, "0x24": {"name": "NV_PFB_PRI_MMU_TLB_CTRL", "fields": {"L2_SIZE": [1, 0], "L2_FA_SIZE": [3, 2], "L2_RB_SIZE": [5, 4], "HUB_SIZE": [7, 6], "HUB_FA_SIZE": [9, 8], "HUB_RB_SIZE": [11, 10], "FILL_SIZE_PTE": [13, 12], "FILL_SIZE_PDE": [15, 14], "FILL_SIZE_RB": [17, 16], "L2_SET_INDEX_BIG_PG": [19, 18], "HUB_SET_INDEX_BIG_PG": [21, 20], "L2_SET_INDEX_SMALL_PG": [23, 22], "HUB_SET_INDEX_SMALL_PG": [25, 24], "HUB_LOCKED_LIMIT": [28, 26], "ATSD_PROP_FLUSH": [26, 26], "INVAL_PROP_FLUSH": [27, 27], "BIND_PROP_FLUSH": [28, 28], "HUB_RSRVD_VM": [31, 29]}}, "0x28": {"name": "NV_PFB_PRI_MMU_RSRVD"}, "0x2c": {"name": "NV_PFB_PRI_MMU_BIND_IMB"}, "0x30": {"name": "NV_PFB_PRI_MMU_BIND"}, "0x34": {"name": "NV_PFB_PRI_MMU_INVALIDATE_VADDR"}, "0x38": {"name": "NV_PFB_PRI_MMU_INVALIDATE_PDB"}, "0x3c": {"name": "NV_PFB_PRI_MMU_INVALIDATE", "fields": {"VADDR_BITS": [31, 4], "UPPER_VADDR_BITS": [19, 0], "PDB_APERTURE": [1, 1], "PDB_ADDR": [31, 4], "UPPER_PDB_ADDR": [19, 0], "PASID_VAL": [19, 0], "SIZE_VAL": [5, 0], "SIZE_GFID": [31, 26], "ALL_VA": [0, 0], "ALL_PDB": [1, 1], "HUBTLB_ONLY": [2, 2], "REPLAY": [5, 3], "SYS_MEMBAR": [6, 6], "ACK": [8, 7], "CANCEL_CLIENT_ID": [14, 9], "CANCEL_GPC_ID": [19, 15], "CANCEL_CLIENT_TYPE": [20, 20], "USE_PASID": [21, 21], "USE_SIZE": [22, 22], "PROP_FLUSH": [23, 23], "CACHE_LEVEL": [26, 24], "TRIGGER": [31, 31], "VADDR_VIRT_BITS": [31, 4], "UPPER_VADDR_VIRT_BITS": [19, 0], "PDB_VIRT_APERTURE": [1, 1], "PDB_VIRT_ADDR": [31, 4], "UPPER_PDB_VIRT_ADDR": [19, 0], "PASID_VIRT_VAL": [19, 0], "SIZE_VIRT_VAL": [5, 0], "SIZE_VIRT_GFID": [31, 26], "VIRT_ALL_VA": [0, 0], "VIRT_ALL_PDB": [1, 1], "VIRT_HUBTLB_ONLY": [2, 2], "VIRT_REPLAY": [5, 3], "VIRT_SYS_MEMBAR": [6, 6], "VIRT_ACK": [8, 7], "VIRT_CANCEL_CLIENT_ID": [14, 9], "VIRT_CANCEL_GPC_ID": [19, 15], "VIRT_CANCEL_CLIENT_TYPE": [20, 20], "VIRT_USE_PASID": [21, 21], "VIRT_USE_SIZE": [22, 22], "VIRT_PROP_FLUSH": [23, 23], "VIRT_CACHE_LEVEL": [26, 24], "VIRT_TRIGGER": [31, 31]}}, "0x40": {"name": "NV_PFB_PRI_MMU_PREFETCH"}, "0x44": {"name": "NV_PFB_PRI_MMU_DEBUG_CTRL"}, "0x48": {"name": "NV_PFB_PRI_MMU_DEBUG_WR"}, "0x4c": {"name": "NV_PFB_PRI_MMU_DEBUG_RD"}, "0x50": {"name": "NV_PFB_PRI_MMU_VPR_INFO"}, "0x54": {"name": "NV_PFB_PRI_MMU_WPR_INFO"}, "0x58": {"name": "NV_PFB_PRI_MMU_VPR_WPR_WRITE"}, "0x5c": {"name": "NV_PFB_PRI_MMU_PRIV_LEVEL_MASK", "fields": {"READ_PROTECTION": [3, 0], "READ_PROTECTION_LEVEL0": [0, 0], "READ_PROTECTION_LEVEL1": [1, 1], "READ_PROTECTION_LEVEL2": [2, 2], "READ_PROTECTION_LEVEL3": [3, 3], "WRITE_PROTECTION": [7, 4], "WRITE_PROTECTION_LEVEL0": [4, 4], "WRITE_PROTECTION_LEVEL1": [5, 5], "WRITE_PROTECTION_LEVEL2": [6, 6], "WRITE_PROTECTION_LEVEL3": [7, 7], "READ_VIOLATION": [8, 8], "WRITE_VIOLATION": [9, 9], "SOURCE_READ_CONTROL": [10, 10], "SOURCE_WRITE_CONTROL": [11, 11], "SOURCE_ENABLE": [31, 12]}}, "0x60": {"name": "NV_PFB_PRI_MMU_LOCAL_MEMORY_RANGE"}, "0x64": {"name": "NV_PFB_PRI_MMU_PHYS_SECURE"}, "0x68": {"name": "NV_PFB_PRI_MMU_INVALIDATE_UPPER_VADDR"}, "0x6c": {"name": "NV_PFB_PRI_MMU_INVALIDATE_UPPER_PDB"}, "0x70": {"name": "NV_PFB_PRI_MMU_PM_UPPER_ADDR_LOW"}, "0x74": {"name": "NV_PFB_PRI_MMU_PM_UPPER_ADDR_HIGH"}, "0x78": {"name": "NV_PFB_PRI_MMU_PAGE_FAULT_CTRL", "fields": {"PRF_FILTER": [1, 0], "VIRT_PRF_FILTER": [1, 0]}}, "0x7c": {"name": "NV_PFB_PRI_MMU_PG1"}, "0x180": {"name": "NV_PFB_PRI_MMU_ATS_ADDR_RANGE_LOCAL"}, "0x1cc": {"name": "NV_PFB_PRI_MMU_FAULT_ADDR_LO"}, "0x1d0": {"name": "NV_PFB_PRI_MMU_FAULT_ADDR_HI"}, "0x1d4": {"name": "NV_PFB_PRI_MMU_FAULT_INST_LO"}, "0x1d8": {"name": "NV_PFB_PRI_MMU_FAULT_INST_HI"}, "0x1dc": {"name": "NV_PFB_PRI_MMU_FAULT_INFO"}, "0x1e0": {"name": "NV_PFB_PRI_MMU_FAULT_STATUS", "fields": {"DROPPED_BAR1_PHYS": [0, 0], "DROPPED_BAR1_VIRT": [1, 1], "DROPPED_BAR2_PHYS": [2, 2], "DROPPED_BAR2_VIRT": [3, 3], "DROPPED_IFB_PHYS": [4, 4], "DROPPED_IFB_VIRT": [5, 5], "DROPPED_OTHER_PHYS": [6, 6], "DROPPED_OTHER_VIRT": [7, 7], "REPLAYABLE": [8, 8], "NON_REPLAYABLE": [9, 9], "REPLAYABLE_ERROR": [10, 10], "NON_REPLAYABLE_ERROR": [11, 11], "REPLAYABLE_OVERFLOW": [12, 12], "NON_REPLAYABLE_OVERFLOW": [13, 13], "REPLAYABLE_GETPTR_CORRUPTED": [14, 14], "NON_REPLAYABLE_GETPTR_CORRUPTED": [15, 15], "BUSY_TYPE": [29, 28], "BUSY": [30, 30], "VALID": [31, 31], "VIRT_DROPPED_BAR1_PHYS": [0, 0], "VIRT_DROPPED_BAR1_VIRT": [1, 1], "VIRT_DROPPED_BAR2_PHYS": [2, 2], "VIRT_DROPPED_BAR2_VIRT": [3, 3], "VIRT_DROPPED_IFB_PHYS": [4, 4], "VIRT_DROPPED_IFB_VIRT": [5, 5], "VIRT_DROPPED_OTHER_PHYS": [6, 6], "VIRT_DROPPED_OTHER_VIRT": [7, 7], "VIRT_REPLAYABLE": [8, 8], "VIRT_NON_REPLAYABLE": [9, 9], "VIRT_REPLAYABLE_ERROR": [10, 10], "VIRT_NON_REPLAYABLE_ERROR": [11, 11], "VIRT_REPLAYABLE_OVERFLOW": [12, 12], "VIRT_NON_REPLAYABLE_OVERFLOW": [13, 13], "VIRT_REPLAYABLE_GETPTR_CORRUPTED": [14, 14], "VIRT_NON_REPLAYABLE_GETPTR_CORRUPTED": [15, 15], "VIRT_BUSY_TYPE": [29, 28], "VIRT_BUSY": [30, 30], "VIRT_VALID": [31, 31]}}, "0x1e4": {"name": "NV_PFB_PRI_MMU_INVALIDATE_PASID"}, "0x1e8": {"name": "NV_PFB_PRI_MMU_INVALIDATE_SIZE"}, "0x1ec": {"name": "NV_PFB_PRI_MMU_L2TLB_ECC_CONTROL"}, "0x1f0": {"name": "NV_PFB_PRI_MMU_L2TLB_ECC_STATUS"}, "0x1f4": {"name": "NV_PFB_PRI_MMU_L2TLB_ECC_CORRECTED_ERR_COUNT"}, "0x1f8": {"name": "NV_PFB_PRI_MMU_L2TLB_ECC_UNCORRECTED_ERR_COUNT"}, "0x1fc": {"name": "NV_PFB_PRI_MMU_L2TLB_ECC_ADDRESS"}, "0x200": {"name": "NV_PFB_PRI_MMU_HUBTLB_ECC_CONTROL"}, "0x204": {"name": "NV_PFB_PRI_MMU_HUBTLB_ECC_STATUS"}, "0x208": {"name": "NV_PFB_PRI_MMU_HUBTLB_ECC_CORRECTED_ERR_COUNT"}, "0x20c": {"name": "NV_PFB_PRI_MMU_HUBTLB_ECC_UNCORRECTED_ERR_COUNT"}, "0x210": {"name": "NV_PFB_PRI_MMU_HUBTLB_ECC_ADDRESS"}, "0x214": {"name": "NV_PFB_PRI_MMU_FILLUNIT_ECC_CONTROL"}, "0x218": {"name": "NV_PFB_PRI_MMU_FILLUNIT_ECC_STATUS"}, "0x21c": {"name": "NV_PFB_PRI_MMU_FILLUNIT_ECC_CORRECTED_ERR_COUNT"}, "0x220": {"name": "NV_PFB_PRI_MMU_FILLUNIT_ECC_UNCORRECTED_ERR_COUNT"}, "0x224": {"name": "NV_PFB_PRI_MMU_FILLUNIT_ECC_ADDRESS"}, "0x228": {"name": "NV_PFB_PRI_MMU_HUBMMU_GLOBAL_ESR"}, "0x22c": {"name": "NV_PFB_PRI_MMU_ECC_PRIV_LEVEL_MASK"}, "0x234": {"name": "NV_PFB_PRI_MMU_CBC_ADJ"}, "0x238": {"name": "NV_PFB_PRI_MMU_CBC_ADJ_PRIV_LEVEL_MASK"}, "0x23c": {"name": "NV_PFB_PRI_MMU_CBC_PRIV_LEVEL_MASK"}, "0x240": {"name": "NV_PFB_PRI_MMU_NUM_ACTIVE_LTCS"}, "0x244": {"name": "NV_PFB_PRI_MMU_CBC_BASE"}, "0x248": {"name": "NV_PFB_PRI_MMU_CBC_TOP"}, "0x24c": {"name": "NV_PFB_PRI_MMU_CBC_MAX"}, "0x250": {"name": "NV_PFB_PRI_MMU_HYPERVISOR_CTL"}, "0x254": {"name": "NV_PFB_PRI_MMU_PEER_SUPPORT"}, "0x258": {"name": "NV_PFB_PRI_MMU_BIND_EXT"}, "0x25c": {"name": "NV_PFB_PRI_MMU_INT_VECTOR_ECC_ERROR"}, "0x260": {"name": "NV_PFB_PRI_MMU_INT_VECTOR_INFO_FAULT"}, "0x27c": {"name": "NV_PFB_PRI_MMU_PHYS_SECURE_PRIV_LEVEL_MASK"}}}, + {"base": "0x20000000001FA7D8", "size": "0x5C", "label": "gmmu-wpr-lock", "registers": {"0x0": {"name": "NV_PFB_PRI_MMU_PMU_PRIV_LEVEL_MASK"}, "0x4": {"name": "NV_PFB_PRI_MMU_SEC_PRIV_LEVEL_MASK"}, "0x8": {"name": "NV_PFB_PRI_MMU_NVDEC_PRIV_LEVEL_MASK"}, "0xc": {"name": "NV_PFB_PRI_MMU_NVENC_PRIV_LEVEL_MASK"}, "0x10": {"name": "NV_PFB_PRI_MMU_NVJPG_PRIV_LEVEL_MASK"}, "0x14": {"name": "NV_PFB_PRI_MMU_GSP_PRIV_LEVEL_MASK"}, "0x18": {"name": "NV_PFB_PRI_MMU_FECS_PRIV_LEVEL_MASK"}, "0x1c": {"name": "NV_PFB_PRI_MMU_GPCCS_PRIV_LEVEL_MASK"}, "0x20": {"name": "NV_PFB_PRI_MMU_FBFALCON_PRIV_LEVEL_MASK"}, "0x28": {"name": "NV_PFB_PRI_MMU_VPR_MODE", "fields": {"VPR_PROTECTED_MODE": [4, 4], "MEMORY_LOCKED": [5, 5], "VPR_NON_COMPRESSIBLE": [6, 6], "SYSWR_WHEN_MEMORY_LOCKED": [7, 7], "MEMORY_LOCK_FUSE": [8, 8], "WPR_COMPRESSIBLE": [9, 9], "SECURE_LOCK": [12, 12], "WPR_WITH_SECURE_LOCK": [13, 13], "FORCE_SPA_EQUAL_GPA": [20, 20], "VPR_FAULT_REPORT": [29, 29], "VPR_LOCK_HWPM": [30, 30], "VPR_LOCK_MISC": [28, 28]}}, "0x2c": {"name": "NV_PFB_PRI_MMU_VPR_ADDR_LO"}, "0x30": {"name": "NV_PFB_PRI_MMU_VPR_ADDR_HI"}, "0x34": {"name": "NV_PFB_PRI_MMU_VPR_CYA_LO"}, "0x38": {"name": "NV_PFB_PRI_MMU_VPR_CYA_HI"}, "0x3c": {"name": "NV_PFB_PRI_MMU_WPR_ALLOW_READ", "fields": {"ALL": [11, 4], "WPR1": [7, 4], "WPR1_SECURE0": [4, 4], "WPR1_SECURE1": [5, 5], "WPR1_SECURE2": [6, 6], "WPR1_SECURE3": [7, 7], "WPR2": [11, 8], "WPR2_SECURE0": [8, 8], "WPR2_SECURE1": [9, 9], "WPR2_SECURE2": [10, 10], "WPR2_SECURE3": [11, 11], "WPR_OK": [27, 27], "MIS_MATCH": [31, 28], "MIS_MATCH0": [28, 28], "MIS_MATCH1": [29, 29], "MIS_MATCH2": [30, 30], "MIS_MATCH3": [31, 31]}}, "0x40": {"name": "NV_PFB_PRI_MMU_WPR_ALLOW_WRITE", "fields": {"ALL": [11, 4], "WPR1": [7, 4], "WPR1_SECURE0": [4, 4], "WPR1_SECURE1": [5, 5], "WPR1_SECURE2": [6, 6], "WPR1_SECURE3": [7, 7], "WPR2": [11, 8], "WPR2_SECURE0": [8, 8], "WPR2_SECURE1": [9, 9], "WPR2_SECURE2": [10, 10], "WPR2_SECURE3": [11, 11], "WPR_ENABLE_FUSE": [27, 27], "MIS_MATCH": [31, 28], "MIS_MATCH0": [28, 28], "MIS_MATCH1": [29, 29], "MIS_MATCH2": [30, 30], "MIS_MATCH3": [31, 31]}}, "0x44": {"name": "NV_PFB_PRI_MMU_WPR1_ADDR_LO"}, "0x48": {"name": "NV_PFB_PRI_MMU_WPR1_ADDR_HI"}, "0x4c": {"name": "NV_PFB_PRI_MMU_WPR2_ADDR_LO"}, "0x50": {"name": "NV_PFB_PRI_MMU_WPR2_ADDR_HI"}, "0x54": {"name": "NV_PFB_PRI_MMU_LOCK_ADDR_LO"}, "0x58": {"name": "NV_PFB_PRI_MMU_LOCK_ADDR_HI"}}}, + {"base": "0x20000000001FAC00", "size": "0x400", "label": "gmmu-hsmmu", "registers": {"0x80": {"name": "NV_PFB_HSMMU_PRI_MMU_CTRL", "fields": {"VM_PG_SIZE": [0, 0], "VOL_FAULT": [1, 1], "COMP_FAULT": [2, 2], "MISS_GRAN": [4, 3], "CACHE_MODE": [6, 5], "L2_SEND_MODE": [9, 9], "USE_FULL_COMP_TAG_LINE": [12, 12], "ATOMIC_CAPABILITY_MODE": [25, 24], "STRONG_BIT_ATOMIC_CYA": [26, 26], "STRONG_BIT_OVERRIDE_ATOMIC_DISABLE": [26, 26]}}, "0x84": {"name": "NV_PFB_HSMMU_PRI_MMU_PM"}, "0x88": {"name": "NV_PFB_HSMMU_PRI_MMU_PM_ADDR_LOW"}, "0x8c": {"name": "NV_PFB_HSMMU_PRI_MMU_PM_UPPER_ADDR_LOW"}, "0x90": {"name": "NV_PFB_HSMMU_PRI_MMU_PM_ADDR_HIGH"}, "0x94": {"name": "NV_PFB_HSMMU_PRI_MMU_PM_UPPER_ADDR_HIGH"}, "0x98": {"name": "NV_PFB_HSMMU_PRI_MMU_PM_UNIT_MASK"}, "0x9c": {"name": "NV_PFB_HSMMU_PRI_MMU_PM_REQ_MASK"}, "0xa0": {"name": "NV_PFB_HSMMU_PRI_MMU_TLB_CTRL", "fields": {"UTLB_SIZE": [1, 0], "UTLB_RB_SIZE": [3, 2], "UTLB_FAST_PATH_DISABLE": [4, 4], "L1TLB_SIZE": [17, 16], "L1TLB_FA_SIZE": [19, 18], "L1TLB_RB_SIZE": [21, 20], "L1TLB_SET_INDEX_BIG_PG": [23, 22], "L1TLB_SET_INDEX_SMALL_PG": [25, 24], "L1TLB_FAST_PATH_DISABLE": [26, 26], "SECURE_LOCK": [30, 30], "FAST_BIG_PG_FAULT_DISABLE": [31, 31]}}, "0xa4": {"name": "NV_PFB_HSMMU_PRI_MMU_PREFETCH"}, "0xb0": {"name": "NV_PFB_HSMMU_PRI_MMU_CG"}, "0xb4": {"name": "NV_PFB_HSMMU_PRI_MMU_CG1"}, "0xb8": {"name": "NV_PFB_HSMMU_PRI_MMU_PG"}, "0xc0": {"name": "NV_PFB_HSMMU_PRI_MMU_PG1"}, "0xc4": {"name": "NV_PFB_HSMMU_PRI_MMU_DEBUG_CTRL"}, "0xc8": {"name": "NV_PFB_HSMMU_PRI_MMU_DEBUG_WR"}, "0xcc": {"name": "NV_PFB_HSMMU_PRI_MMU_DEBUG_RD"}}} + ], + "note": "gmmu-pri-mmu spans the NV_PFB_PRI_MMU_* cluster 0x100C80-0x100EFC (interleaved PFB PRI_FB/NISO offsets inside the span stay unclassified); gmmu-wpr-lock overlaps the fwsec WPR2 window at 0x1FA824/0x1FA828 and just declines, so the access follows exactly the same underlying path as a run without the observer (observed by tests, not an assertion that the WPR2 path serves)." + }, + "fbif": { + "evidence": "FBIF/FBPA register surface: NV_PFB_FBPA block (0x009A0000-0x009A3FFF), the 16 per-partition FBPA instances (0x00900000 + i*0x4000) and the 3 MC instances (0x00980000 + i*0x4000) from the hardware register/field headers. Observe-only: FB access-path, compression and ECC semantics are not modelled; no read value is invented.", + "mode": "observe-only", + "registers": { + "0x0": { + "name": "NV_PFB_FBPA_PRI_FBPA_CG" + }, + "0x4": { + "name": "NV_PFB_FBPA_PRI_FBPA_CG1" + }, + "0x8": { + "name": "NV_PFB_FBPA_PRI_FBPA_PG" + }, + "0xc": { + "name": "NV_PFB_FBPA_PRI_FBPA_PG1" + }, + "0x50": { + "name": "NV_PFB_FBPA_SPARE" + }, + "0x54": { + "name": "NV_PFB_FBPA_FALCON_MONITOR" + }, + "0x60": { + "name": "NV_PFB_FBPA_CFG_PWRD" + }, + "0x64": { + "name": "NV_PFB_FBPA_FALCON_CTRL_VER" + }, + "0x80": { + "name": "NV_PFB_FBPA_DEBUG_0" + }, + "0x84": { + "name": "NV_PFB_FBPA_DEBUG_1" + }, + "0x90": { + "name": "NV_PFB_FBPA_TESTCMD", + "fields": { + "CKE": [ + 0, + 0 + ], + "CS0": [ + 1, + 1 + ], + "CS1": [ + 2, + 2 + ], + "RAS": [ + 3, + 3 + ], + "CAS": [ + 4, + 4 + ], + "WE": [ + 5, + 5 + ], + "RES": [ + 6, + 6 + ], + "BANK_4": [ + 7, + 7 + ], + "BANK": [ + 11, + 8 + ], + "ADR": [ + 25, + 12 + ], + "ADR_UPPER": [ + 28, + 26 + ], + "GO": [ + 30, + 30 + ], + "HOLD": [ + 31, + 31 + ], + "EXT_ACT": [ + 0, + 0 + ], + "EXT_VLD_DRAM0": [ + 1, + 1 + ], + "EXT_VLD_DRAM1": [ + 2, + 2 + ], + "EXT_ADR_DRAM1": [ + 10, + 3 + ], + "EXT_RW_CMD": [ + 16, + 16 + ], + "EXT_MRS_CMD": [ + 17, + 17 + ], + "EXT_DR_CMD": [ + 18, + 18 + ], + "EXT_4CK_CMD": [ + 19, + 19 + ], + "EXT_LAM_CMD": [ + 20, + 20 + ], + "EXT_1_ADR": [ + 23, + 0 + ], + "EXT_1_CSB0": [ + 27, + 27 + ], + "EXT_1_CSB1": [ + 28, + 28 + ], + "EXT_1_USE_ADR": [ + 29, + 29 + ] + } + }, + "0x94": { + "name": "NV_PFB_FBPA_TESTCMD_EXT" + }, + "0xa0": { + "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP0BYTE0" + }, + "0xa4": { + "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP0BYTE1" + }, + "0xa8": { + "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP0BYTE2" + }, + "0xac": { + "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP0BYTE3" + }, + "0xb0": { + "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP1BYTE0" + }, + "0xb4": { + "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP1BYTE1" + }, + "0xb8": { + "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP1BYTE2" + }, + "0xbc": { + "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP1BYTE3" + }, + "0xc0": { + "name": "NV_PFB_FBPA_SWIZZLE_DBI" + }, + "0xc4": { + "name": "NV_PFB_FBPA_SWIZZLE_BYTE" + }, + "0xc8": { + "name": "NV_PFB_FBPA_SWIZZLE_ECC" + }, + "0xcc": { + "name": "NV_PFB_FBPA_SPARE2" + }, + "0xd0": { + "name": "NV_PFB_FBPA_REPLAY_RETRY_SUBP0" + }, + "0xd4": { + "name": "NV_PFB_FBPA_REPLAY_RETRY_SUBP1" + }, + "0xd8": { + "name": "NV_PFB_FBPA_REPLAY_CYCLES_SUBP0" + }, + "0xdc": { + "name": "NV_PFB_FBPA_REPLAY_CYCLES_SUBP1" + }, + "0x100": { + "name": "NV_PFB_FBPA_PM" + }, + "0x148": { + "name": "NV_PFB_FBPA_HISEC_PRIV_LEVEL_MASK", + "fields": { + "READ_PROTECTION": [ + 3, + 0 + ], + "READ_PROTECTION_LEVEL0": [ + 0, + 0 + ], + "READ_PROTECTION_LEVEL1": [ + 1, + 1 + ], + "READ_PROTECTION_LEVEL2": [ + 2, + 2 + ], + "READ_PROTECTION_LEVEL3": [ + 3, + 3 + ], + "WRITE_PROTECTION": [ + 7, + 4 + ], + "WRITE_PROTECTION_LEVEL0": [ + 4, + 4 + ], + "WRITE_PROTECTION_LEVEL1": [ + 5, + 5 + ], + "WRITE_PROTECTION_LEVEL2": [ + 6, + 6 + ], + "WRITE_PROTECTION_LEVEL3": [ + 7, + 7 + ], + "READ_VIOLATION": [ + 8, + 8 + ], + "WRITE_VIOLATION": [ + 9, + 9 + ], + "SOURCE_READ_CONTROL": [ + 10, + 10 + ], + "SOURCE_WRITE_CONTROL": [ + 11, + 11 + ], + "SOURCE_ENABLE": [ + 31, + 12 + ] + } + }, + "0x150": { + "name": "NV_PFB_FBPA_ASRCTRL_NONISO" + }, + "0x154": { + "name": "NV_PFB_FBPA_TESTCMD_EXT_1" + }, + "0x158": { + "name": "NV_PFB_FBPA_PTR_RESET" + }, + "0x15c": { + "name": "NV_PFB_FBPA_CONFIG12" + }, + "0x160": { + "name": "NV_PFB_FBPA_MISC_CTRL", + "fields": { + "INTERVAL_TIMER_PERIOD": [ + 2, + 0 + ], + "INTERVAL_TIMER_EN": [ + 4, + 4 + ] + } + }, + "0x164": { + "name": "NV_PFB_FBPA_NUM_ACTIVE_FBPS" + }, + "0x168": { + "name": "NV_PFB_FBPA_MEM_PRIV_LEVEL_MASK", + "fields": { + "READ_PROTECTION": [ + 3, + 0 + ], + "READ_PROTECTION_LEVEL0": [ + 0, + 0 + ], + "READ_PROTECTION_LEVEL1": [ + 1, + 1 + ], + "READ_PROTECTION_LEVEL2": [ + 2, + 2 + ], + "READ_PROTECTION_LEVEL3": [ + 3, + 3 + ], + "WRITE_PROTECTION": [ + 7, + 4 + ], + "WRITE_PROTECTION_LEVEL0": [ + 4, + 4 + ], + "WRITE_PROTECTION_LEVEL1": [ + 5, + 5 + ], + "WRITE_PROTECTION_LEVEL2": [ + 6, + 6 + ], + "WRITE_PROTECTION_LEVEL3": [ + 7, + 7 + ], + "READ_VIOLATION": [ + 8, + 8 + ], + "WRITE_VIOLATION": [ + 9, + 9 + ], + "SOURCE_READ_CONTROL": [ + 10, + 10 + ], + "SOURCE_WRITE_CONTROL": [ + 11, + 11 + ], + "SOURCE_ENABLE": [ + 31, + 12 + ] + } + }, + "0x16c": { + "name": "NV_PFB_FBPA_NUM_ACTIVE_LTCS" + }, + "0x170": { + "name": "NV_PFB_FBPA_ECC_PRIV_LEVEL_MASK", + "fields": { + "READ_PROTECTION": [ + 3, + 0 + ], + "READ_PROTECTION_LEVEL0": [ + 0, + 0 + ], + "READ_PROTECTION_LEVEL1": [ + 1, + 1 + ], + "READ_PROTECTION_LEVEL2": [ + 2, + 2 + ], + "READ_PROTECTION_LEVEL3": [ + 3, + 3 + ], + "WRITE_PROTECTION": [ + 7, + 4 + ], + "WRITE_PROTECTION_LEVEL0": [ + 4, + 4 + ], + "WRITE_PROTECTION_LEVEL1": [ + 5, + 5 + ], + "WRITE_PROTECTION_LEVEL2": [ + 6, + 6 + ], + "WRITE_PROTECTION_LEVEL3": [ + 7, + 7 + ], + "READ_VIOLATION": [ + 8, + 8 + ], + "WRITE_VIOLATION": [ + 9, + 9 + ], + "SOURCE_READ_CONTROL": [ + 10, + 10 + ], + "SOURCE_WRITE_CONTROL": [ + 11, + 11 + ], + "SOURCE_ENABLE": [ + 31, + 12 + ] + } + }, + "0x174": { + "name": "NV_PFB_FBPA_WPRE0" + }, + "0x178": { + "name": "NV_PFB_FBPA_WPRE1" + }, + "0x17c": { + "name": "NV_PFB_FBPA_WPRE2" + }, + "0x1d8": { + "name": "NV_PFB_FBPA_PM_CAPTURE_CTL" + }, + "0x200": { + "name": "NV_PFB_FBPA_CFG0", + "fields": { + "BL": [ + 1, + 0 + ], + "EXTBANK": [ + 2, + 2 + ], + "X8": [ + 4, + 4 + ], + "CA_RETURN_TO_ZERO": [ + 5, + 5 + ], + "HBM_CMD_PARITY_OVERRIDE": [ + 6, + 6 + ], + "EDC_RATE": [ + 7, + 7 + ], + "GAPLESS_WTR": [ + 8, + 8 + ], + "DRAM_ACPD": [ + 11, + 11 + ], + "AUTO_ATR": [ + 12, + 12 + ], + "DATA_SCRAMBLE": [ + 13, + 13 + ], + "AUTO_SYNC": [ + 14, + 14 + ], + "LOW_POWER": [ + 15, + 15 + ], + "ABI": [ + 16, + 16 + ], + "BURST_LENGTH": [ + 17, + 17 + ], + "DM_ON_ADR": [ + 18, + 18 + ], + "TIMING_UNIT": [ + 19, + 19 + ], + "CMD_PARITY": [ + 20, + 20 + ], + "AUTO_ODT": [ + 21, + 21 + ], + "CLAMSHELL": [ + 22, + 22 + ], + "EDC_EARLY_VALUE": [ + 23, + 23 + ], + "BG_8": [ + 24, + 24 + ], + "AUTO_PRE": [ + 25, + 25 + ], + "SAME_QUSE_TWO_RANKS": [ + 26, + 26 + ], + "RDEDC": [ + 27, + 27 + ], + "WREDC": [ + 28, + 28 + ], + "REPLAY": [ + 29, + 29 + ], + "MS01_ECC": [ + 30, + 30 + ], + "DRAM_ACK": [ + 31, + 31 + ] + } + }, + "0x204": { + "name": "NV_PFB_FBPA_CFG1", + "fields": { + "SUBP": [ + 1, + 0 + ], + "AP": [ + 6, + 4 + ], + "COL": [ + 15, + 12 + ], + "ROWA": [ + 19, + 16 + ], + "ROWB": [ + 23, + 20 + ], + "BANK": [ + 25, + 24 + ], + "BG_8_OVR": [ + 26, + 26 + ], + "NUM_BANKS_PER_BG": [ + 28, + 27 + ], + "NUM_BANKS_PER_ROW_BG": [ + 30, + 29 + ] + } + }, + "0x20c": { + "name": "NV_PFB_FBPA_CSTATUS" + }, + "0x210": { + "name": "NV_PFB_FBPA_REFCTRL", + "fields": { + "PUT": [ + 6, + 0 + ], + "GET": [ + 14, + 8 + ], + "RESET": [ + 16, + 16 + ], + "VALID": [ + 31, + 31 + ] + } + }, + "0x214": { + "name": "NV_PFB_FBPA_ZQ" + }, + "0x218": { + "name": "NV_PFB_FBPA_PIN" + }, + "0x21c": { + "name": "NV_PFB_FBPA_PAD" + }, + "0x220": { + "name": "NV_PFB_FBPA_TIMING0" + }, + "0x224": { + "name": "NV_PFB_FBPA_TIMING1" + }, + "0x228": { + "name": "NV_PFB_FBPA_TIMING2" + }, + "0x22c": { + "name": "NV_PFB_FBPA_TIMING3" + }, + "0x230": { + "name": "NV_PFB_FBPA_TIMING4" + }, + "0x234": { + "name": "NV_PFB_FBPA_TIMING5" + }, + "0x238": { + "name": "NV_PFB_FBPA_TIMING6" + }, + "0x23c": { + "name": "NV_PFB_FBPA_TIMING7" + }, + "0x240": { + "name": "NV_PFB_FBPA_TIMING8" + }, + "0x244": { + "name": "NV_PFB_FBPA_TIMING9" + }, + "0x248": { + "name": "NV_PFB_FBPA_TIMING10" + }, + "0x24c": { + "name": "NV_PFB_FBPA_TIMING11" + }, + "0x250": { + "name": "NV_PFB_FBPA_TIMING12" + }, + "0x254": { + "name": "NV_PFB_FBPA_TIMING13" + }, + "0x258": { + "name": "NV_PFB_FBPA_TIMING14" + }, + "0x25c": { + "name": "NV_PFB_FBPA_TIMING15" + }, + "0x260": { + "name": "NV_PFB_FBPA_TIMING16" + }, + "0x264": { + "name": "NV_PFB_FBPA_TIMING17" + }, + "0x268": { + "name": "NV_PFB_FBPA_TIMING18" + }, + "0x26c": { + "name": "NV_PFB_FBPA_TIMING19" + }, + "0x280": { + "name": "NV_PFB_FBPA_CG1" + }, + "0x284": { + "name": "NV_PFB_FBPA_CG2" + }, + "0x288": { + "name": "NV_PFB_FBPA_TIMING20_GEN" + }, + "0x28c": { + "name": "NV_PFB_FBPA_TIMING20" + }, + "0x290": { + "name": "NV_PFB_FBPA_CONFIG0" + }, + "0x294": { + "name": "NV_PFB_FBPA_CONFIG1" + }, + "0x298": { + "name": "NV_PFB_FBPA_CONFIG2" + }, + "0x29c": { + "name": "NV_PFB_FBPA_CONFIG3" + }, + "0x2a0": { + "name": "NV_PFB_FBPA_CONFIG4" + }, + "0x2a4": { + "name": "NV_PFB_FBPA_CONFIG5" + }, + "0x2a8": { + "name": "NV_PFB_FBPA_CONFIG6" + }, + "0x2ac": { + "name": "NV_PFB_FBPA_CONFIG7" + }, + "0x2b0": { + "name": "NV_PFB_FBPA_TIMING0_GEN" + }, + "0x2b4": { + "name": "NV_PFB_FBPA_TIMING1_GEN" + }, + "0x2b8": { + "name": "NV_PFB_FBPA_TIMING2_GEN" + }, + "0x2bc": { + "name": "NV_PFB_FBPA_TIMING3_GEN" + }, + "0x2c0": { + "name": "NV_PFB_FBPA_TIMING4_GEN" + }, + "0x2c4": { + "name": "NV_PFB_FBPA_TIMING5_GEN" + }, + "0x2c8": { + "name": "NV_PFB_FBPA_TIMING6_GEN" + }, + "0x2cc": { + "name": "NV_PFB_FBPA_CONFIG8" + }, + "0x2d0": { + "name": "NV_PFB_FBPA_TIMING7_GEN" + }, + "0x2d4": { + "name": "NV_PFB_FBPA_TIMING8_GEN" + }, + "0x2d8": { + "name": "NV_PFB_FBPA_TIMING9_GEN" + }, + "0x2dc": { + "name": "NV_PFB_FBPA_TIMING15_GEN" + }, + "0x2e0": { + "name": "NV_PFB_FBPA_TIMING16_GEN" + }, + "0x2e4": { + "name": "NV_PFB_FBPA_TIMING17_GEN" + }, + "0x2e8": { + "name": "NV_PFB_FBPA_CONFIG9" + }, + "0x2ec": { + "name": "NV_PFB_FBPA_TIMING18_GEN" + }, + "0x2f0": { + "name": "NV_PFB_FBPA_TIMING19_GEN" + }, + "0x2f4": { + "name": "NV_PFB_FBPA_CONFIG10" + }, + "0x2f8": { + "name": "NV_PFB_FBPA_ASR_WAKEUP" + }, + "0x2fc": { + "name": "NV_PFB_FBPA_DRAM_ASR" + }, + "0x300": { + "name": "NV_PFB_FBPA_GENERIC_MRS" + } + }, + "windows": [ + {"base": "0x20000000009A0000", "size": "0x4000", "label": "fbpa"}, + {"base": "0x2000000000900000", "size": "0x4000", "label": "fbpa-partition", "count": 16, "stride": "0x4000"}, + {"base": "0x2000000000980000", "size": "0x4000", "label": "fbpa-mc", "count": 3, "stride": "0x4000"} + ] + }, "dma": { "evidence": "GSP RISC-V address map: SYSGPA/FBGPA apertures. In this flat model both apertures alias simulator RAM (documented assumption) so booter-style DMA and firmware aperture reads resolve; unmapped transfers are refused and recorded.", "windows": [ @@ -80,7 +882,7 @@ "boot_shim": { "note": "Reset-time state the real SEC2 booter/host provides. Every value here is an assumption recorded in the session; the device model never fakes a register read.", "registers": { - "a0": "0x80000", + "a0": "0x81000", "a1": "0x80000", "a2": "0x4000000", "a3": "0x1b34ea0", @@ -92,7 +894,16 @@ "core_dump_pa": "0x40000", "core_dump_size": "0x10000", "wpr_meta_pa": "0x90000", - "image_at": "0x100000000", + "bootstrap_args": { + "pa": "0x81000", + "imem_base_pa": "0x0", + "imem_size": "0x10000", + "imem_bs_size": "0x0", + "dmem_base_pa": "0x100000000", + "dmem_size": "0x10000", + "dmem_bs_size": "0x0", + "evidence": "libos bootstrap-args layout (IMEM/DMEM base+size+bs, passed in a0 by the bootloader); the M-mode boot path (mstatus.VM=NVMPU, observed) uses the non-sep-kern TCM configuration (offsets 0, 64 KiB). Values are declared assumptions, not silicon reads." + }, "wpr": { "elf_pa": "0x4000000", "elf_size": "0x1b34ea0", -- cgit v1.2.3