From f874d709e5a45d63c8ee9ab6b0300195aa5d20a5 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Rasmusson Date: Wed, 23 Sep 2026 12:11:04 -0400 Subject: userspace/cuda: add CUDA userspace driver analysis, tooling and name maps Add the analysis of the NVIDIA CUDA userspace driver used by Open-IOV VMs: * docs/01-08: architecture (layers, object model, threads, sync, memory, channels, launch), build and execution model, the driver/kernel/GPU interface contract, virtualization approaches, the 570.144 decompilation map with the corrected libcuda crash path and the current engine-topology blocker, tooling, and a version-upgrade playbook. * tools/: version-agnostic toolchain - stdlib ELF/interface toolkit (exports, unwind-table function map, wrapper resolution, version delta, crash address decode), callgraph walker, corpus annotator, and Ghidra analysis/decompilation/name-map scripts with resumable parallel drivers. * 570.144/annotations/: our name map (exports + resolved wrappers + verified internals) and subsystem index, tracked so naming work accumulates. * READ-ME-FIRST.md: reading guide with the fast lookup recipes. The full decompiled corpus and annotated corpus stay untracked (vendor- derived, gitignored under the version run/ tree). --- userspace/cuda/tools/cudatool.py | 515 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 515 insertions(+) create mode 100755 userspace/cuda/tools/cudatool.py (limited to 'userspace/cuda/tools/cudatool.py') diff --git a/userspace/cuda/tools/cudatool.py b/userspace/cuda/tools/cudatool.py new file mode 100755 index 0000000..2e4c1c6 --- /dev/null +++ b/userspace/cuda/tools/cudatool.py @@ -0,0 +1,515 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# +# cudatool.py - Open-IOV CUDA userspace driver analysis toolkit. +# +# A dependency-free (stdlib-only) ELF toolkit for the NVIDIA CUDA userspace +# driver, usable on every driver version without a Ghidra project. It gives +# the fast facts that do not require decompilation: exports, function ranges +# derived from the exception-handling tables, the CUDA forward-compatibility +# note, and a dmesg/crash-address decoder. It is also the version-delta tool: +# `delta` compares two driver versions and reports exactly what changed at +# the interface level, which is what keeps the Open-IOV side anti-fragile +# across driver upgrades. +# +# Commands +# info ELF/version summary +# exports [--json] exported symbols (file vaddrs) +# functions [--json] function ranges from .eh_frame_hdr +# nearest nearest preceding function/export for a vaddr +# crash dmesg mapping base + ip -> file vaddr + symbol +# strings [--grep RE] defined strings with file offsets +# delta interface-level diff between two versions +# wrappers export stub -> generated wrapper target +# note CUDA forward-compatibility note (JSON) +# +# Address convention: every address printed by this tool is a FILE VIRTUAL +# ADDRESS, i.e. the `libcuda+0x...` numbers used in the project docs. Ghidra +# may load the same file at a non-zero image base (0x100000 for these builds); +# use tools/ghidra_decompile_at.sh which translates file vaddrs automatically. +# +# License note: this tool only *reads* the vendor binary for interoperability +# analysis; it contains no vendor code and is published under BSD-3-Clause. + +import argparse +import json +import re +import struct +import sys + +ELF_MAGIC = b"\x7fELF" + + +class ELFError(Exception): + pass + + +class ELF: + """Minimal 64-bit little-endian ELF reader.""" + + def __init__(self, path): + self.path = path + with open(path, "rb") as f: + self.data = f.read() + if self.data[:4] != ELF_MAGIC: + raise ELFError(f"{path}: not an ELF file") + if self.data[4] != 2: + raise ELFError(f"{path}: not ELF64") + if self.data[5] != 1: + raise ELFError(f"{path}: not little-endian") + (self.e_type, self.e_machine, _v, self.e_entry, self.e_phoff, self.e_shoff, + self.e_flags, self.e_ehsize, self.e_phentsize, self.e_phnum, + self.e_shentsize, self.e_shnum, self.e_shstrndx) = struct.unpack_from( + "> 4, + shndx=st_shndx, + defined=st_shndx != 0, + )) + return out + + def exports(self): + return [s for s in self.dynamic_symbols() + if s["defined"] and s["type"] == 2 and s["value"]] + + def imports(self): + return [s for s in self.dynamic_symbols() + if not s["defined"] and s["name"]] + + # ---- exception handling function table ------------------------------- + + def function_ranges(self): + """Return [(start, end)] from .eh_frame_hdr. + + .eh_frame_hdr layout (DWARF .eh_frame_hdr, version 1): + u8 version; u8 eh_frame_ptr_enc; u8 fde_count_enc; u8 table_enc; + encoded eh_frame_ptr; encoded fde_count; + table[count] { encoded pc_begin; encoded fde_addr; } + + GCC emits: eh_frame_ptr_enc = DW_EH_PE_pcrel|sdata4 (0x1b), + fde_count_enc = udata4 (0x03), table_enc = DW_EH_PE_datarel|sdata4 + (0x3b). The datarel anchor is the start of .eh_frame_hdr. The parser + below handles the encodings generally (pcrel/datarel + 1/2/4/8-byte + and ULEB/SLEB), so it also works for other toolchains. + """ + hdr = self.section(".eh_frame_hdr") + if not hdr: + return [] + base = hdr["offset"] + anchor = hdr["addr"] + version = self.data[base] + if version != 1: + raise ELFError(f"unsupported .eh_frame_hdr version {version}") + eh_ptr_enc, fde_count_enc, table_enc = self.data[base + 1:base + 4] + + def enc_size(enc): + return {0x0: 0, 0x1: 1, 0x2: 2, 0x3: 4, 0x4: 8, + 0x9: 1, 0xA: 2, 0xB: 4, 0xC: 8}.get(enc & 0x0F, 0) + + def decode(enc, ptr, field_addr): + fmt = enc & 0x0F + if fmt == 0x0: + return 0 + if fmt == 0x1: + val, shift = 0, 0 + while True: + b = self.data[ptr] + ptr += 1 + val |= (b & 0x7F) << shift + if not (b & 0x80): + break + shift += 7 + return val + if fmt == 0x2: + return struct.unpack_from(" generated wrapper --------------------------------- + + def relative_relocs(self): + """Map address -> R_X86_64_RELATIVE addend (the initial slot value).""" + out = {} + s = self.section(".rela.dyn") + if not s: + return out + n = s["size"] // 24 + for i in range(n): + off = s["offset"] + i * 24 + r_offset, r_info, r_addend = struct.unpack_from("8} {s['name']}") + + +def cmd_functions(args): + elf = ELF(args.lib) + fr = elf.function_ranges() + if args.json: + print(json.dumps([dict(start=hex(a), end=hex(b) if b else None) + for a, b in fr], indent=1)) + return + for a, b in fr: + print(f"0x{a:08x} 0x{b:08x}" if b else f"0x{a:08x} -") + + +def cmd_nearest(args): + elf = ELF(args.lib) + target = int(args.addr, 16) + ex = sorted(elf.exports(), key=lambda s: s["value"]) + prev = None + for s in ex: + if s["value"] <= target: + prev = s + else: + break + fr = elf.function_ranges() + fstart = None + for a, b in fr: + if a <= target and (b is None or target < b): + fstart = a + break + print(f"vaddr : 0x{target:x}") + print(f"function start : 0x{fstart:x}" if fstart is not None else + "function start : ") + if prev: + print(f"nearest export : {prev['name']} + 0x{target - prev['value']:x} " + f"(export at 0x{prev['value']:x})") + else: + print("nearest export : ") + + +def cmd_crash(args): + elf = ELF(args.lib) + base = int(args.map_base, 16) + ip = int(args.ip, 16) + vaddr = ip - base + elf.exec_segment_base() + print(f"file vaddr : 0x{vaddr:x}") + args.addr = hex(vaddr) + cmd_nearest(args) + + +def cmd_strings(args): + elf = ELF(args.lib) + pat = re.compile(args.grep) if args.grep else None + rodata = [s for s in elf.sections if s["type"] == 1 and not (s["flags"] & 4)] + seen = set() + for s in rodata: + blob = elf.data[s["offset"]:s["offset"] + s["size"]] + for m in re.finditer(rb"[\x20-\x7e]{5,}", blob): + text = m.group().decode() + if pat and not pat.search(text): + continue + key = (s["name"], m.start()) + if key in seen: + continue + seen.add(key) + print(f"0x{s['addr'] + m.start():08x} {text}") + + +def cmd_delta(args): + a, b = ELF(args.libA), ELF(args.libB) + ea = {s["name"]: s for s in a.exports()} + eb = {s["name"]: s for s in b.exports()} + added = sorted(set(eb) - set(ea)) + removed = sorted(set(ea) - set(eb)) + moved = [] + resized = [] + for n in sorted(set(ea) & set(eb)): + if ea[n]["value"] != eb[n]["value"]: + moved.append((n, ea[n]["value"], eb[n]["value"])) + if ea[n]["size"] != eb[n]["size"]: + resized.append((n, ea[n]["size"], eb[n]["size"])) + print(f"A: {args.libA}") + print(f" build-id {a.build_id()} exports {len(ea)}") + print(f"B: {args.libB}") + print(f" build-id {b.build_id()} exports {len(eb)}") + na, nb = a.cuda_note(), b.cuda_note() + print(f"note A: {na['raw'] if na else None}") + print(f"note B: {nb['raw'] if nb else None}") + tb_a, tb_b = a.text_bounds(), b.text_bounds() + print(f"text A: 0x{tb_a[0]:x}..0x{tb_a[1]:x} ({tb_a[1]-tb_a[0]})") + print(f"text B: 0x{tb_b[0]:x}..0x{tb_b[1]:x} ({tb_b[1]-tb_b[0]})") + print(f"exports added ({len(added)}): " + ", ".join(added[:40]) + + (" ..." if len(added) > 40 else "")) + print(f"exports removed ({len(removed)}): " + ", ".join(removed[:40]) + + (" ..." if len(removed) > 40 else "")) + print(f"exports moved ({len(moved)})" + + (": " + ", ".join(f"{n} 0x{x:x}->0x{y:x}" for n, x, y in moved[:10]) + if moved else "")) + print(f"exports resized ({len(resized)})" + + (": " + ", ".join(f"{n} {x}->{y}" for n, x, y in resized[:10]) + if resized else "")) + print(f"imports A={len(a.imports())} B={len(b.imports())}") + + +def cmd_wrappers(args): + elf = ELF(args.lib) + ws = elf.wrappers() + if args.json: + print(json.dumps(ws, indent=1)) + return + for w in ws: + if w["wrapper"] is None: + continue + print(f"0x{w['stub']:08x} -> 0x{w['wrapper']:08x} {w['name']}") + + +def cmd_note(args): + elf = ELF(args.lib) + print(json.dumps(elf.cuda_note(), indent=2)) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + sub = ap.add_subparsers(dest="cmd", required=True) + + p = sub.add_parser("info"); p.add_argument("lib"); p.set_defaults(fn=cmd_info) + p = sub.add_parser("exports"); p.add_argument("lib") + p.add_argument("--json", action="store_true"); p.set_defaults(fn=cmd_exports) + p = sub.add_parser("functions"); p.add_argument("lib") + p.add_argument("--json", action="store_true"); p.set_defaults(fn=cmd_functions) + p = sub.add_parser("nearest"); p.add_argument("lib"); p.add_argument("addr") + p.set_defaults(fn=cmd_nearest) + p = sub.add_parser("crash"); p.add_argument("lib") + p.add_argument("map_base"); p.add_argument("ip"); p.set_defaults(fn=cmd_crash) + p = sub.add_parser("strings"); p.add_argument("lib") + p.add_argument("--grep"); p.set_defaults(fn=cmd_strings) + p = sub.add_parser("delta"); p.add_argument("libA"); p.add_argument("libB") + p.set_defaults(fn=cmd_delta) + p = sub.add_parser("note"); p.add_argument("lib"); p.set_defaults(fn=cmd_note) + p = sub.add_parser("wrappers"); p.add_argument("lib") + p.add_argument("--json", action="store_true"); p.set_defaults(fn=cmd_wrappers) + + args = ap.parse_args() + try: + args.fn(args) + except ELFError as e: + print(f"error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() -- cgit v1.2.3