From c587eac377c1c5b720142edadebaace5cb1b17c0 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Rasmusson Date: Wed, 23 Sep 2026 13:48:00 -0400 Subject: userspace/cuda: complete the analysis pass * Name recovery: name_candidates.py proposes the hand-written API-layer implementation for each exported wrapper by grouping version variants and intersecting their callees; 180 high-confidence proposals recorded in annotations/name-candidates.tsv, 29 of them body-verified and promoted to names.tsv with evidence in subsystems.tsv. * Engine records: identified the 0x14-byte per-engine descriptor, mapped its fields (kind, engine type, instance, class id, CE caps) and the existence callback, and documented how the missing async copy engine (COPY14) leaves the secondary record zeroed and blocks context creation (docs/05 section 3.7, public-safe). * Targeted type recovery: AnalyzeFunctions.java runs the parameter-ID analyzer over a curated list of 80 functions; 16 improved materially and the typed decompilations are kept under the version run tree. * Cross-version: cudatool.py fdematch measures how names transfer between patch versions (570.144 -> 570.211.01: exports 100%, verified internals ~70%, generated wrappers must be re-resolved by relocation, never by address arithmetic); documented in the upgrade playbook. * Corpus tooling: diff_corpus.py (version-to-version corpus diff) and subsystem_digest.py (per-subsystem call trees). * Deprecate the old per-version helpers whose address handling and analysis timeout produced wrong results. Generated corpora and typed outputs remain untracked under the version run tree. --- userspace/cuda/tools/cudatool.py | 154 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) (limited to 'userspace/cuda/tools/cudatool.py') diff --git a/userspace/cuda/tools/cudatool.py b/userspace/cuda/tools/cudatool.py index 2e4c1c6..3417d2b 100755 --- a/userspace/cuda/tools/cudatool.py +++ b/userspace/cuda/tools/cudatool.py @@ -21,6 +21,7 @@ # strings [--grep RE] defined strings with file offsets # delta interface-level diff between two versions # wrappers export stub -> generated wrapper target +# fdematch function-range match report across versions # note CUDA forward-compatibility note (JSON) # # Address convention: every address printed by this tool is a FILE VIRTUAL @@ -476,6 +477,151 @@ def cmd_wrappers(args): print(f"0x{w['stub']:08x} -> 0x{w['wrapper']:08x} {w['name']}") +def cmd_fdematch(args): + """Match function ranges between two libraries by unwind-table (FDE) + start/size, using the common exports as local address anchors. + + A single global address shift is not enough across patch versions: the + library is partly re-laid-out (function insertions/removals drift the + addresses locally, and the generated wrapper region is regenerated). The + matcher therefore uses the nearest common export by address as the local + anchor and looks for a B function of the same FDE size within a small + window around old_start + anchor_delta. This measures how transferable a + name map is between two driver versions, and the --names breakdown shows + which parts of the map (exports, generated wrappers, internals) survive.""" + import bisect + from collections import Counter, defaultdict + + a, b = ELF(args.libA), ELF(args.libB) + fa = {s: (e - s if e is not None else None) + for s, e in a.function_ranges()} + fb = {s: (e - s if e is not None else None) + for s, e in b.function_ranges()} + + ea = {s["name"]: s["value"] for s in a.exports()} + eb = {s["name"]: s["value"] for s in b.exports()} + common = sorted(set(ea) & set(eb)) + anchors = sorted((ea[n], eb[n] - ea[n]) for n in common) + anchor_addrs = [x for x, _ in anchors] + deltas = [d for _, d in anchors] + d_counter = Counter(d // 0x1000 * 0x1000 for d in deltas) + + def anchor_delta(v): + i = bisect.bisect_left(anchor_addrs, v) + cand = [] + if i < len(anchors): + cand.append(anchors[i]) + if i > 0: + cand.append(anchors[i - 1]) + if not cand: + return 0 + return min(cand, key=lambda t: abs(t[0] - v))[1] + + size_b = defaultdict(list) + for s, sz in fb.items(): + size_b[sz].append(s) + for k in size_b: + size_b[k].sort() + + def window_matches(v, size, w): + if size is None or size not in size_b: + return [] + target = v + anchor_delta(v) + starts = size_b[size] + lo = bisect.bisect_left(starts, target - w) + hi = bisect.bisect_right(starts, target + w) + return starts[lo:hi] + + exact = window = unique = unmatched = 0 + for v, size in fa.items(): + m = window_matches(v, size, args.window) + if not m: + unmatched += 1 + elif v + anchor_delta(v) in m: + exact += 1 + if len(m) == 1: + unique += 1 + else: + window += 1 + if len(m) == 1: + unique += 1 + matched = exact + window + total = len(fa) + rate = matched / total if total else 0.0 + + transfer = None + if args.names: + transfer = defaultdict(lambda: dict(entries=0, exact=0, window=0, + missing=0, not_in_a=0)) + with open(args.names) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + p = line.split("\t") + if len(p) < 2: + continue + try: + v = int(p[0], 16) + except ValueError: + continue + src = p[2] if len(p) > 2 else "?" + t = transfer[src] + t["entries"] += 1 + if v not in fa: + t["not_in_a"] += 1 + continue + m = window_matches(v, fa[v], args.window) + if not m: + t["missing"] += 1 + elif v + anchor_delta(v) in m: + t["exact"] += 1 + else: + t["window"] += 1 + + if args.json: + print(json.dumps(dict( + lib_a=args.libA, lib_b=args.libB, + functions_a=total, functions_b=len(fb), + anchors=len(anchors), + delta_median=hex(deltas[len(deltas) // 2]) if deltas else "0x0", + delta_clusters=[[hex(k), v] for k, v in d_counter.most_common(6)], + window=hex(args.window), + exact=exact, window_matches=window, unique=unique, + unmatched=unmatched, match_rate=round(rate, 4), + name_map=transfer), indent=1)) + return + + print(f"fdematch A: {args.libA}") + print(f" B: {args.libB}") + print(f"functions : A={total} B={len(fb)} " + f"(ranges from .eh_frame_hdr)") + print(f"anchors : {len(anchors)} common exports") + if deltas: + print(f"delta clusters : " + ", ".join( + f"{'+' if k >= 0 else '-'}0x{abs(k):x} ({v})" + for k, v in d_counter.most_common(5))) + print(f"matching window : +/-0x{args.window:x} around " + f"nearest-anchor-shifted address, same FDE size") + print(f"exact match : {exact} ({exact/total*100:.1f}%)" + if total else "exact match : 0") + print(f"window match : {window} ({window/total*100:.1f}%)" + if total else "window match : 0") + print(f"unique match : {unique} ({unique/total*100:.1f}%)" + if total else "unique match : 0") + print(f"unmatched A : {unmatched}") + print(f"match rate : {rate*100:.1f}% of A's {total} functions") + if transfer: + print(f"name map : {args.names} (per source column)") + for src in sorted(transfer): + t = transfer[src] + got = t["exact"] + t["window"] + pct = got / t["entries"] * 100 if t["entries"] else 0.0 + print(f" {src:<14} : {got}/{t['entries']} ({pct:.1f}%) " + f"exact={t['exact']} window={t['window']} " + f"missing={t['missing']} not-in-A={t['not_in_a']}") + + def cmd_note(args): elf = ELF(args.lib) print(json.dumps(elf.cuda_note(), indent=2)) @@ -502,6 +648,14 @@ def main(): 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) + p = sub.add_parser("fdematch"); p.add_argument("libA"); p.add_argument("libB") + p.add_argument("--names", default=None, + help="names.tsv to report transferability for") + p.add_argument("--window", type=lambda x: int(x, 0), default=0x100, + help="same-size match window around the anchored address " + "(default 0x100)") + p.add_argument("--json", action="store_true") + p.set_defaults(fn=cmd_fdematch) args = ap.parse_args() try: -- cgit v1.2.3