summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-04 18:42:36 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-04 18:42:36 -0400
commit1f2073d4fbb1d412d9bbf373312e3c51b83db92a (patch)
treea661537d7ec9510324e0a40a5a65e4b8c853f01b /debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py
parentc1adf2727983f5cace8ca0f5d1119ac2256c348e (diff)
downloadopen-iov-1f2073d4fbb1d412d9bbf373312e3c51b83db92a.tar.gz
open-iov-1f2073d4fbb1d412d9bbf373312e3c51b83db92a.zip
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.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py988
1 files changed, 988 insertions, 0 deletions
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())