diff options
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.py | 988 |
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()) |
