"""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())