summaryrefslogtreecommitdiffstats
path: root/tools/guest-diag/wall2/wall2_livecap.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-25 05:21:29 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-25 05:21:29 -0400
commit45978e9202412fb9fc6088718e9720f22ece7b30 (patch)
tree1c70d4967088965bff39d5863bfef88ace5aa72f /tools/guest-diag/wall2/wall2_livecap.py
parente5eacffa9e4e0577b8fb109610286db83bf80ff3 (diff)
downloadopen-iov-45978e9202412fb9fc6088718e9720f22ece7b30.tar.gz
open-iov-45978e9202412fb9fc6088718e9720f22ece7b30.zip
wall-2 forensics tools: GR channel-init push localization (session 73)
Tools and findings from the live wall-2 campaign (r674/r675): the vk_frame client's GR channel-init push (va=0x4480000 len=1432) cannot be translated by the sema service, so no release is applied and vkCreateDevice fails -4. Captured: the RM window-record/memdesc layout (ring memdesc PA == the service's validated ring pa; push memdesc PA injected live and read by the service without a false-translation line), the CE-only shape of the service rescue matcher, the notrace status of nvidia/modeset kprobes (QEMU gdb hbreak works), and the ring-entry encoding from channelFillGpFifo/nvWriteGpEntry (neither runs for this channel's kick). New tools: wall2_resolver{2..5}.py, wall2_livecap{1..3}.py, RAM structure scanners (scan_r673*.py, trace_push674.py), gdb675*.gdb probes, plus a README with the ranked next steps.
Diffstat (limited to 'tools/guest-diag/wall2/wall2_livecap.py')
-rw-r--r--tools/guest-diag/wall2/wall2_livecap.py112
1 files changed, 112 insertions, 0 deletions
diff --git a/tools/guest-diag/wall2/wall2_livecap.py b/tools/guest-diag/wall2/wall2_livecap.py
new file mode 100644
index 0000000..af68c03
--- /dev/null
+++ b/tools/guest-diag/wall2/wall2_livecap.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+# wall2_livecap.py - live capture of the GR channel-init push page.
+#
+# The release group in the push is {0x20038090, sem_va_hi, sem_va_lo, payload,
+# continuation}: sem_va = (hi<<32)|lo, and for the channel-init entry the sem
+# sits at push_va + entry_len (entry0: len=1432=0x598, sem at +0x598), with the
+# group itself at push_va + entry_len - 24 = +0x580.
+#
+# This watcher scans guest RAM for 0x20038090 groups during the client's stall,
+# classifies them, and injects push_va -> page_pa via sema_map_add when the
+# group's sem VA and offset identify the push page. Prints full evidence.
+import subprocess, struct, time, sys, re, os
+
+RAM = "/dev/shm/vgpu-guest-ram"
+PARAM = "/sys/module/vgsp/parameters/sema_map_add"
+LOG = "/tmp/opencode/wall2_livecap.log"
+MASK = ~0xfff & ((1 << 64) - 1)
+
+ENTRY_LEN = int(os.environ.get("W2_LEN", "1432"))
+WATCH_S = int(os.environ.get("W2_WATCH", "120"))
+
+logf = open(LOG, "a", buffering=1)
+def log(*a):
+ s = " ".join(str(x) for x in a)
+ print(s, flush=True)
+ logf.write("[%s] %s\n" % (time.strftime("%H:%M:%S"), s))
+
+img = open(RAM, "rb", buffering=0)
+def gpa2off(pa):
+ if pa < 0x80000000: return pa
+ if pa < 0x180000000: return 0x80000000 + (pa - 0x100000000)
+ return None
+def off2gpa(off):
+ return off if off < 0x80000000 else 0x100000000 + (off - 0x80000000)
+def rd(pa, n):
+ off = gpa2off(pa)
+ if off is None: return None
+ img.seek(off); return img.read(n)
+
+def cont_ok(v):
+ op = (v >> 29) & 7
+ cnt = (v >> 16) & 0xfff
+ return (op in (1, 3, 5) and cnt != 0) or op in (4, 7)
+
+def scan():
+ img.seek(0, 2); fsz = img.tell()
+ pat = b"\x90\x80\x03\x20"
+ out = []
+ CH = 64 << 20
+ pos = 0
+ while pos < fsz:
+ img.seek(pos); d = img.read(min(CH, fsz - pos))
+ st = 0
+ while True:
+ i = d.find(pat, st)
+ if i < 0: break
+ st = i + 1
+ if i + 24 > len(d): continue
+ g = d[i:i+24]
+ hdr, hi, lo, pay, w5, v5 = struct.unpack("<6I", g)
+ gpa = off2gpa(pos + i)
+ page = gpa & MASK
+ off = gpa - page
+ out.append((page, off, hi, lo, pay, w5, v5, g))
+ pos += CH
+ return out
+
+def main():
+ log("=== livecap start len=%d watch=%ds ===" % (ENTRY_LEN, WATCH_S))
+ injected = {}
+ deadline = time.time() + WATCH_S
+ while time.time() < deadline:
+ hits = scan()
+ cands = []
+ for (page, off, hi, lo, pay, w5, v5, g) in hits:
+ sem = (hi << 32) | lo
+ # classify
+ pool = (hi == 1 and lo < 0x10000 and 0 < pay < (1 << 24)
+ and (w5 >> 29) == 1 and ((w5 >> 13) & 7) == 4
+ and ((w5 & 0x1fff) << 2) == 0x300 and (v5 & 3) == 0)
+ if pool:
+ continue # CE pool release family, not the push
+ if not (0x4000000 <= sem < 0x100000000):
+ continue
+ if not (0 < pay < (1 << 24)):
+ continue
+ if not cont_ok(w5):
+ continue
+ push_va = sem - ENTRY_LEN
+ cands.append((page, off, sem, pay, push_va, w5, v5, g))
+ if cands:
+ # prefer the canonical group offset 0x580 / sem-consistent push VA
+ cands.sort(key=lambda c: (c[1] != (ENTRY_LEN - 24), -c[3]))
+ page, off, sem, pay, push_va, w5, v5, g = cands[0]
+ d = rd(page, 0x100)
+ log("livecap: %d cand(s); page=0x%x off=0x%x sem=0x%x pay=%u push_va=0x%x w5=0x%08x v5=0x%08x"
+ % (len(cands), page, off, sem, pay, push_va, w5, v5))
+ log(" page head: %s" % d[:0x40].hex())
+ if (push_va & 0xfff) == 0 and push_va not in injected:
+ try:
+ with open(PARAM, "w") as f:
+ f.write("0x%x,0x%x,0x%x,1\n" % (push_va, page, 0x1000))
+ log(" INJECT 0x%x -> 0x%x (off=0x%x)" % (push_va, page, off))
+ injected[push_va] = page
+ except OSError as e:
+ log(" inject FAIL", e)
+ time.sleep(1)
+ log("=== livecap done: %d injection(s) ===" % len(injected))
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main())