summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/guest-diag/guest_pin_stream.py700
-rw-r--r--tools/guest-diag/pin_gr_stream.py134
2 files changed, 798 insertions, 36 deletions
diff --git a/tools/guest-diag/guest_pin_stream.py b/tools/guest-diag/guest_pin_stream.py
index 3db61ba..ddc7098 100644
--- a/tools/guest-diag/guest_pin_stream.py
+++ b/tools/guest-diag/guest_pin_stream.py
@@ -211,7 +211,11 @@ def read_u32(kc, va):
return struct.unpack("<I", d)[0] if d else 0xFFFFFFFF
-DBG_BUDGET = int(os.environ.get("STREAM_DBG_BUDGET", "600"))
+DBG_BUDGET = int(os.environ.get("STREAM_DBG_BUDGET", "2400"))
+# SESSION 153-CONTINUED-65: minimum seconds between full-offset sweeps of
+# one strict anchor (the demand file re-sends every ~2 s; the sweep can cost
+# tens of seconds - see the throttle comment at the strict processing loop).
+ANCHOR_RETRY_S = float(os.environ.get("STREAM_ANCHOR_RETRY_S", "20"))
_dbg_count = [0]
@@ -306,6 +310,12 @@ def _nv_vmap_areas(kc):
# instead of walking the whole +/-16 neighbourhood.
_area_off = {}
+# SESSION 153-CONTINUED-55: per-GL-family learned {family_base: (area_lo,
+# adj)}. Once one page of a GL push-buffer family is content-verified the
+# whole family resolves through the learned area/offset (continuation and
+# zero pages included); cleared when the area disappears (context rebuild).
+_gl_fam = {}
+
def resolve_va_by_content(kc, va, out):
"""Demand pin: find the page carrying GPU VA 'va' by content match.
@@ -320,6 +330,12 @@ def resolve_va_by_content(kc, va, out):
lottery. Only the requested VA is pinned, so a wrong candidate cannot
poison other VAs.
+ SESSION 153-CONTINUED-55: the same rule now covers the GL push-buffer
+ families (PB_GL_FAMILIES). The setup-push holds (va=0x4001xxx) and the
+ draw pushes (0x402xxxx) are nv_vm_map_pages allocations exactly like the
+ CE window; without this they were rejected here silently and the held
+ setup push was never pinned, so its 4K pages read zeros at the host.
+
2026-10-16b: the offset search window was +/-1 page. Pushes of a client
whose pushbuffer vmap carries a larger per-allocation header (observed
with multi-MB CE staging: pushes at 0x120000f58+ never resolved and the
@@ -328,11 +344,47 @@ def resolve_va_by_content(kc, va, out):
vmap area, because consecutive pushes advance sequentially inside the
same allocation and share the offset.
"""
- if not (PB_CE_LO <= va < PB_CE_HI):
- return False
- off = (va - PB_CE_LO) & 0xFFFFFF
- k = off >> 12
+ if PB_CE_LO <= va < PB_CE_HI:
+ k = ((va - PB_CE_LO) & 0xFFFFFF) >> 12
+ gl_base = None
+ else:
+ gl_base = next((lo for lo, hi in PB_GL_FAMILIES if lo <= va < hi),
+ None)
+ if gl_base is None:
+ return False
+ k = (va - gl_base) >> 12
areas = _nv_vmap_areas(kc)
+ # SESSION 153-CONTINUED-55: a GL push-buffer family is one allocation
+ # whose vmap offset is IDENTITY with the family offset (verified: the
+ # setup push's bytes at va 0x4001004 read back byte-exact at the vmap
+ # page's +0x1004). Once one page of the family resolves, every page of
+ # the family is known: continuation pages of a stream do NOT start with
+ # a method header (the 60216-byte setup push crosses page boundaries
+ # mid-run, so pages 2/3/5/7 fail the prefix test) and unwritten pages
+ # read all zero (pages 8..15 of the same push) yet are real translations
+ # the host read still needs. Use the learned (area, adj) for the whole
+ # family; only the FIRST learn pays the content test.
+ learned_fam = _gl_fam.get(gl_base) if gl_base else None
+ if learned_fam is not None:
+ lo, adj = learned_fam
+ if any(a_lo == lo for (a_lo, _h, _p) in areas):
+ idx = k + adj
+ pages = next(p for (a_lo, _h, p) in areas if a_lo == lo)
+ if 0 <= idx < pages:
+ cpu = lo + idx * 0x1000
+ pa = kc.va_to_pa(cpu)
+ d = kc.read(cpu, 64) if pa is not None else None
+ if pa is not None and d is not None:
+ if gl_base is not None or (_push_prefix_ok(d) and
+ any(d[:32])):
+ out.append("0x%x,0x%x,1\n" % (va & ~0xFFF, pa))
+ dbg("demand-va: 0x%x -> pa=0x%x (gl family learned"
+ " area=0x%x adj=%+d)"
+ % (va & ~0xFFF, pa, lo, adj))
+ return True
+ # The learned area is gone (context rebuilt): fall through to a
+ # fresh content match and re-learn.
+ _gl_fam.pop(gl_base, None)
for (lo, _hi, pages) in areas:
learned = _area_off.get(lo)
if learned is not None:
@@ -345,24 +397,514 @@ def resolve_va_by_content(kc, va, out):
if idx < 0 or idx >= pages:
continue
cpu = lo + idx * 0x1000
- d = kc.read(cpu, 64)
- if not d or not any(d[:32]):
- continue
- if not _push_prefix_ok(d):
- continue
pa = kc.va_to_pa(cpu)
if pa is None:
continue
+ # 0x200 (not 64): a GL family's page 0 can carry its first
+ # method header at offset ~0x68 behind a zero-filled prefix
+ # (observed on the 60216 B setup push), invisible to a 64-byte
+ # read.
+ d = kc.read(cpu, 0x200)
+ if not d or not any(d):
+ continue
+ if not _push_prefix_ok(d):
+ continue
_area_off[lo] = adj
+ if gl_base is not None:
+ # First content-verified page of this GL family: the
+ # identity mapping (area, adj) now serves every page of
+ # the family, including continuation and zero pages.
+ _gl_fam[gl_base] = (lo, adj)
out.append("0x%x,0x%x,1\n" % (va & ~0xFFF, pa))
- dbg("demand-va: 0x%x -> pa=0x%x (area off=%d adj=%+d)"
- % (va & ~0xFFF, pa, idx, adj))
+ dbg("demand-va: 0x%x -> pa=0x%x (area off=%d adj=%+d%s)"
+ % (va & ~0xFFF, pa, idx, adj,
+ " gl-learn" if gl_base is not None else ""))
return True
dbg("demand-va: 0x%x not found by content (%d areas)"
% (va & ~0xFFF, len(areas)))
return False
+# SESSION 153-CONTINUED-64: candidate adjacencies for the strict window scan.
+_STRICT_ADJ = (0, 1, -1, 2, -2, 3, -3, 4, -4, 6, -6, 8, -8, 14, -14, 16, -16)
+
+
+def _stream_parse_full(buf):
+ """Exact port of the host's svc_stream_parse (gsp_sema_service.c), plus
+ the MME-setup content requirements the bare parse lacks.
+
+ Zero words are NOP padding (skipped anywhere in the stream), ops 1/3/5
+ are INC/NON_INC/ONE_INC runs with a 12-bit count (1..0xfff) and count
+ data words, ops 0/2/4/7 are header-only, anything else fails. The buffer
+ parses only when at least one method was seen and the walk lands exactly
+ on the end (trailing zero padding allowed).
+
+ SESSION 153-CONTINUED-64b: the bare exact-end parse accepted a GMMU
+ page-table window - PTEs are {pfn<<12, flags} u32 pairs whose values sit
+ below 0x20000000, so every word reads as op 0 (header-only) and the walk
+ lands exactly on the end (r1122 pinned pa=0x11dbb0000: all 32 words of
+ every page op 0, zero data runs, host read `sema-bigrun hdr=0x1dbb7000`).
+ A window is now only accepted when it carries at least one data run AND
+ at least one run addressing an MME-setup method (0x114..0x124 / 0x234 /
+ 0x550..0x568 / 0x3400..0x34fc - the same windows the host's
+ svc_mme_setup_run_addr classifies with). A page table can never satisfy
+ either; the held setup push always carries both (the r1116 capture's
+ first run `20020047` = INC run @0x11c).
+ """
+ off = 0
+ methods = 0
+ data_runs = 0
+ mme_hits = 0
+ code_words = 0
+ n = len(buf)
+ while off + 4 <= n:
+ hdr = struct.unpack_from('<I', buf, off)[0]
+ if hdr == 0:
+ off += 4
+ continue
+ op = (hdr >> 29) & 7
+ count = (hdr >> 16) & 0xFFF
+ if op in (1, 3, 5):
+ if not count or off + 4 + count * 4 > n:
+ return False
+ data_runs += 1
+ addr = (hdr & 0x1FFF) << 2
+ if (0x114 <= addr <= 0x124 or addr == 0x234 or
+ 0x550 <= addr <= 0x568 or 0x3400 <= addr <= 0x34FC):
+ mme_hits += 1
+ if addr == 0x114 and count >= 1:
+ code_words += count
+ off += 4 + count * 4
+ elif op in (0, 2, 4, 7):
+ off += 4
+ else:
+ return False
+ methods += 1
+ if methods > 8192:
+ return False
+ while off < n and buf[off] == 0:
+ off += 1
+ return (methods > 0 and off == n and data_runs > 0 and mme_hits > 0
+ and code_words >= 8)
+
+
+def _walk_measure(buf):
+ """Method-shape walk returning (end_ok, data_runs, mme_hits, code_words,
+ pair_runs, parsed_bytes). 'end_ok' mirrors _stream_parse_full's
+ exact-end + zero padding landing; 'pair_runs' counts runs addressing the
+ SA-pair method 0x11c (the client push signature); 'parsed_bytes' is how
+ far the walk got before a shape failure (or the window end)."""
+ off = 0
+ methods = 0
+ data_runs = 0
+ mme_hits = 0
+ code_words = 0
+ pair_runs = 0
+ n = len(buf)
+ end_ok = False
+ while off + 4 <= n:
+ hdr = struct.unpack_from('<I', buf, off)[0]
+ if hdr == 0:
+ off += 4
+ continue
+ op = (hdr >> 29) & 7
+ count = (hdr >> 16) & 0xFFF
+ if op in (1, 3, 5):
+ if not count or off + 4 + count * 4 > n:
+ break
+ data_runs += 1
+ addr = (hdr & 0x1FFF) << 2
+ if (0x114 <= addr <= 0x124 or addr == 0x234 or
+ 0x550 <= addr <= 0x568 or 0x3400 <= addr <= 0x34FC):
+ mme_hits += 1
+ if addr == 0x114:
+ code_words += count
+ if addr == 0x11c and count >= 2:
+ pair_runs += 1
+ off += 4 + count * 4
+ elif op in (0, 2, 4, 7):
+ off += 4
+ else:
+ break
+ methods += 1
+ if methods > 8192:
+ break
+ else:
+ o2 = off
+ while o2 < n and buf[o2] == 0:
+ o2 += 1
+ end_ok = (methods > 0 and o2 == n)
+ return end_ok, data_runs, mme_hits, code_words, pair_runs, off
+
+
+def _window_accept(buf, ln):
+ """Acceptance for a client-mapping candidate window.
+
+ SESSION 153-CONTINUED-65: the full-offset sweep of the client-mem
+ resolver needs a false-positive gate that survives pointer/event data.
+ The probe of the r1125 boot found a 60216-byte window of pointer junk
+ that walked to an exact end with one misinterpreted 0x114 run
+ (mme=1): the old accept (data>0, mme>0, code>=8) would have pinned it.
+ Large windows therefore require the client push signature - at least
+ two MME-setup runs or two SA-pair (0x11c) runs - in addition to the
+ counts; small windows keep the historical strict rule. The relaxed
+ path additionally accepts a window whose tail is unmapped/unwritten
+ (walk parses >= 0x1000) when it carries >= 64 code words (the macro
+ upload signature).
+
+ Returns (ok, mode).
+ """
+ if ln < 0x8000:
+ return _stream_parse_full(buf), "strict"
+ end_ok, data, mme, code, pairs, parsed = _walk_measure(buf)
+ # The client-push signature: SA-pair (0x11c) runs or 0x114 code runs.
+ # The r1125 probe of the live client mappings measured 10k+ pointer/float
+ # windows with data/mme hits but pairs=0, code=0 - MME-setup-family hits
+ # alone (0x124/0x34xx) are NOT a discriminator; the pair/code runs are.
+ sig = (pairs >= 2 or code >= 8)
+ if end_ok and data >= 2 and sig:
+ return True, "strict"
+ if data >= 2 and sig and parsed >= 0x1000:
+ return True, "relaxed"
+ return False, None
+
+
+def _prefix_walk_ok(buf, off):
+ """Cheap first-page pre-filter for a strict window candidate.
+
+ Walks the method shapes from `off` to the end of `buf` WITHOUT requiring
+ the end to land on a boundary (runs may cross into the next page and a
+ run header near the page end declares data we cannot see yet). Fails on
+ the first header no method shape can produce; this kills arbitrary data
+ pages after a few words so the full parse only runs on survivors.
+ """
+ n = len(buf)
+ while off + 4 <= n:
+ hdr = struct.unpack_from('<I', buf, off)[0]
+ if hdr == 0:
+ off += 4
+ continue
+ op = (hdr >> 29) & 7
+ count = (hdr >> 16) & 0xFFF
+ if op in (1, 3, 5):
+ if not count:
+ return False
+ off += 4 + count * 4
+ elif op in (0, 2, 4, 7):
+ off += 4
+ else:
+ return False
+ return True
+
+
+def _read_window(kc, cpu, off, ln):
+ """Read [cpu+off, cpu+off+ln) page-by-page through kcore.
+
+ A vmap area's pages are contiguous in the kernel VA but arbitrary in
+ physical memory, so the window is assembled one page at a time.
+ """
+ out = b""
+ a = cpu + off
+ left = ln
+ while left > 0:
+ n = min(left, 0x1000 - (a & 0xFFF))
+ d = kc.read(a, n)
+ if not d:
+ return None
+ out += d
+ a += n
+ left -= n
+ return out
+
+
+def _mme_run_in_page(d):
+ """True when a 4K page contains a run addressing an MME-setup method.
+
+ The strict-window prefilter for client-mapping candidates (cheap: one
+ page read rejects the ~511/512 wrong shifts before the full parse).
+ """
+ n = len(d)
+ off = 0
+ while off + 4 <= n:
+ hdr = struct.unpack_from('<I', d, off)[0]
+ if hdr == 0:
+ off += 4
+ continue
+ op = (hdr >> 29) & 7
+ count = (hdr >> 16) & 0xFFF
+ if op in (1, 3, 5):
+ if not count:
+ off += 4
+ continue
+ addr = (hdr & 0x1FFF) << 2
+ if (0x114 <= addr <= 0x124 or addr == 0x234 or
+ 0x550 <= addr <= 0x568 or 0x3400 <= addr <= 0x34FC):
+ return True
+ off += 4 + count * 4
+ else:
+ off += 4
+ return False
+
+
+def resolve_va_client_mem(pid, ent_va, off, ln, out):
+ """Strict pin for a setup push whose pages live in CLIENT sysmem.
+
+ SESSION 153-CONTINUED-64c: when the setup push's allocation is UVM
+ sysmem it has NO kernel vmap (r1122: no nv_vm_map_pages area held the
+ `a0100045` 0x114 upload runs anywhere on the boot - the content exists
+ only in the client's `rw-s` /dev/nvidia mappings, and the host's
+ GPU-page-table walk answered an unrelated page-table buffer for the
+ held VA). The client's CPU mappings + /proc/<pid>/pagemap are the only
+ ground truth for those pages. Scan the client's readable+writable
+ mappings: for each candidate page shift, read the exact [va, va+len)
+ window via /proc/<pid>/mem, validate with the host's own full parse,
+ and emit pins for every page of the validated window from the
+ pagemap. The cheap per-page MME-run prefilter rejects wrong shifts.
+ """
+ if not pid:
+ return False
+ try:
+ pfd = os.open('/proc/%d/pagemap' % pid, os.O_RDONLY)
+ mfd = os.open('/proc/%d/mem' % pid, os.O_RDONLY)
+ except OSError:
+ return False
+ try:
+ gl_base = next((lo for lo, hi in PB_GL_FAMILIES
+ if lo <= (ent_va & ~0xFFF) < hi), None)
+ k = ((ent_va & ~0xFFF) - gl_base) >> 12 if gl_base is not None else None
+ if ln <= 0 or ln > 0x40000:
+ return False
+ need = (off + ln + 0xFFF) // 0x1000
+ if need > 64:
+ return False
+ try:
+ maps = open('/proc/%d/maps' % pid).read().splitlines()
+ except OSError:
+ return False
+ tried = 0
+ for line in maps:
+ parts = line.split()
+ if len(parts) < 5 or 'rw' not in parts[1]:
+ continue
+ # SESSION 153-CONTINUED-64e: any rw mapping may carry the
+ # sysmem push (observed as pathless rw-s /dev/nvidia mappings;
+ # memfd-backed buffers cannot be excluded either - the strict
+ # parse + MME prefilter are the safety gates, not the path).
+ try:
+ lo, hi = [int(x, 16) for x in parts[0].split('-')]
+ except ValueError:
+ continue
+ if hi - lo < (need + 4) * 0x1000 or hi - lo > (64 << 20):
+ continue
+ pages = (hi - lo) >> 12
+ if k is None or k == 0:
+ # Unknown family, or the FAMILY-BASE page (k == 0): neither
+ # has an identity page index to key on, so sweep every page
+ # start of the mapping (bounded). SESSION 153-CONTINUED-65:
+ # the 0x3200000 macro-upload push's window sits at an
+ # arbitrary page offset inside the client's mapping (the
+ # r1124 power-of-two neighbourhood never tried it; the
+ # window content measures code=712, exact-end, under the
+ # strict walk).
+ adjs = range(0, max(1, pages - need + 1))
+ else:
+ # SESSION 153-CONTINUED-65: neighbourhood FIRST, then the
+ # FULL page sweep. The r1125 setup push (family pages k=1
+ # and k=33, 60216 B) failed every power-of-two offset while
+ # a matching window could sit at an arbitrary mapping page;
+ # every candidate still passes _window_accept before any
+ # pin is emitted, so the widened sweep cannot pin a window
+ # the parse rejects.
+ near = [0, 1, -1, 2, -2, 4, -4, 8, -8, 16, -16, 32, -32,
+ 64, -64, 128, -128, 256, -256]
+ near_set = set(near)
+ rest = [a for a in range(-k, pages - need + 1 - k)
+ if a not in near_set]
+ adjs = near + rest
+ for adj in adjs:
+ idx = (k + adj) if k is not None else adj
+ if idx < 0 or idx + need > pages:
+ continue
+ win = lo + idx * 0x1000 + off
+ tried += 1
+ if tried > 8192:
+ break
+ try:
+ first = os.pread(mfd, 0x1000 - off, win)
+ except OSError:
+ continue
+ if not first:
+ continue
+ # An all-zero first page is a legitimate push head (the
+ # method stream may start later); only a page with non-zero
+ # content and no MME-setup run is rejected outright.
+ if not _mme_run_in_page(first) and any(first):
+ continue
+ buf = b''
+ left = ln
+ a = win
+ ok = True
+ while left > 0:
+ n = min(left, 0x1000 - (a & 0xFFF))
+ try:
+ d = os.pread(mfd, n, a)
+ except OSError:
+ ok = False
+ break
+ if not d or len(d) != n:
+ ok = False
+ break
+ buf += d
+ a += n
+ left -= n
+ if not ok:
+ continue
+ acc, mode = _window_accept(buf, ln)
+ if not acc:
+ continue
+ pas = []
+ for j in range(need):
+ vpg = (ent_va & ~0xFFF) + j * 0x1000
+ cpg = lo + (idx + j) * 0x1000
+ os.lseek(pfd, (cpg >> 12) * 8, 0)
+ e = int.from_bytes(os.read(pfd, 8), 'little')
+ if not (e >> 63) & 1:
+ pas = None
+ break
+ pas.append(((e & ((1 << 55) - 1)) << 12))
+ if not pas:
+ continue
+ for j, pa in enumerate(pas):
+ out.append("0x%x,0x%x,1\n"
+ % ((ent_va & ~0xFFF) + j * 0x1000, pa))
+ dbg("demand-va: 0x%x -> pa=0x%x (%s client-mem parse"
+ " %u B ok pages=%u map=0x%x idx=%d)"
+ % (ent_va & ~0xFFF, pas[0], mode, ln, need, lo, idx))
+ return True
+ if tried > 8192:
+ break
+ return False
+ finally:
+ os.close(pfd)
+ os.close(mfd)
+
+
+def resolve_va_strict(kc, ent_va, off, ln, out, pid=None):
+ """Demand pin validated by the host's exact full-parse of [ent_va, +ln).
+
+ SESSION 153-CONTINUED-64: the content heuristic learned a GL family from
+ ONE page's weak 0x200-byte prefix test and then served every family page
+ from that (area, adj) with NO content check at all; r1120 pinned
+ 0x4001000 -> a page whose bytes (`00040000 00001000...`) failed the
+ host's stream parse for the whole boot (`plausible=0 unreadable=1`), so
+ the setup push was never captured and the prelude gate deferred every
+ launch. This path runs the HOST'S OWN full parse on the exact bytes the
+ host will read before any pin is emitted, then pins EVERY page of the
+ validated window and learns the family from it. A family is never
+ served from an unvalidated learn.
+ """
+ page = ent_va & ~0xFFF
+ gl_base = next((lo for lo, hi in PB_GL_FAMILIES if lo <= page < hi), None)
+ if ln <= 0 or ln > 0x40000:
+ return False
+ if gl_base is None:
+ # SESSION 153-CONTINUED-64e: an anchor outside the known families
+ # (r1124: the hold moved to a 153600-byte push at 0x3200000) has no
+ # identity page index; only the client-mapping sweep can resolve it.
+ try:
+ if resolve_va_client_mem(pid, ent_va, off, ln, out):
+ return True
+ except Exception:
+ pass
+ dbg("demand-va: 0x%x strict unknown-family client-mem failed" % page)
+ return False
+ gl_hi = next(hi for lo, hi in PB_GL_FAMILIES if lo <= page < hi)
+ k = (page - gl_base) >> 12
+ need = (off + ln + 0xFFF) // 0x1000
+ if need > 64 or page + need * 0x1000 > gl_hi:
+ return False
+ areas = _nv_vmap_areas(kc)
+ cands = []
+ fam = _gl_fam.get(gl_base)
+ if fam is not None and any(a_lo == fam[0] for (a_lo, _h, _p) in areas):
+ cands.append(fam)
+ if k == 0:
+ # SESSION 153-CONTINUED-65: a family-BASE anchor (the 0x3200000
+ # macro-upload push) has no identity page index to key on. The
+ # r1126 macro window lives in a KERNEL-mapped allocation (no
+ # process pagemap maps it: the whole-process reverse scan found
+ # none), so the kcore vmap sweep is the only resolver - and the
+ # power-of-two neighborhood missed the window's arbitrary page
+ # index. Sweep every page of every nv_vm_map_pages area; the
+ # _window_accept parse gates each candidate.
+ for (lo, _hi, pages) in areas:
+ for adj in range(0, max(1, pages - need + 1)):
+ c = (lo, adj)
+ if c not in cands:
+ cands.append(c)
+ else:
+ for (lo, _hi, pages) in areas:
+ for adj in _STRICT_ADJ:
+ c = (lo, adj)
+ if c not in cands:
+ cands.append(c)
+ tried = 0
+ for (lo, adj) in cands:
+ pages = next(p for (a_lo, _h, p) in areas if a_lo == lo)
+ idx = k + adj
+ if idx < 0 or idx + need > pages:
+ continue
+ tried += 1
+ if tried > (8192 if k == 0 else 128):
+ break
+ cpu = lo + idx * 0x1000
+ first = kc.read(cpu, 0x1000)
+ if not first:
+ continue
+ # SESSION 153-CONTINUED-65: an all-zero first page is a legitimate
+ # push head - the r1126 macro-upload window (0x3200000, 153600 B)
+ # starts with a fully zero page (the method stream lands later in
+ # the window); the old `not any(first)` skip threw exactly that
+ # candidate away (the r1128 sweep tried 166 kernel-vmap windows and
+ # rejected the true one on this check). The prefix walk trivially
+ # passes an all-zero page; the full-window accept is the gate.
+ if any(first) and not _prefix_walk_ok(first, off):
+ continue
+ buf = _read_window(kc, cpu, off, ln)
+ if buf is None:
+ continue
+ acc, mode = _window_accept(buf, ln)
+ if not acc:
+ continue
+ pas = []
+ for j in range(need):
+ vpa = kc.va_to_pa(cpu + j * 0x1000)
+ if vpa is None:
+ pas = None
+ break
+ pas.append(vpa)
+ if not pas:
+ continue
+ for j, vpa in enumerate(pas):
+ out.append("0x%x,0x%x,1\n" % (page + j * 0x1000, vpa))
+ _area_off[lo] = adj
+ _gl_fam[gl_base] = (lo, adj)
+ dbg("demand-va: 0x%x -> pa=0x%x (%s parse %u B ok pages=%u"
+ " area=0x%x adj=%+d)" % (page, pas[0], mode, ln, need, lo, adj))
+ return True
+ dbg("demand-va: 0x%x strict parse failed (%u B, %u windows tried)"
+ % (page, ln, tried))
+ # SESSION 153-CONTINUED-64c: no kernel vmap held the content - the
+ # allocation is UVM sysmem visible only in the client's own mappings.
+ try:
+ if resolve_va_client_mem(pid, ent_va, off, ln, out):
+ return True
+ except Exception:
+ pass
+ dbg("demand-va: 0x%x strict client-mem parse failed too" % page)
+ return False
+
+
def resolve_mem_pins(kc, mem, out, default_pages=16, va_lo=0, va_hi=0,
page_off=0):
"""uvm_rm_mem kptr -> GPU-VA/GPA runs (direct map or page-table walk).
@@ -810,6 +1352,19 @@ def resolve_pb_pins(kc, pb, out):
# hold the ring-entry pushes is chosen (content anchor, _mem_content_ok).
PB_CE_LO = 0x120000000
PB_CE_HI = 0x121000000
+# SESSION 153-CONTINUED-55: GL push-buffer VA families. The GL client's
+# setup/MME-prelude push buffers (observed hold va=0x4001004 len=60216) and
+# the draw pushes (observed va=0x4021004) are separate nv_vm_map_pages
+# allocations in two fixed windows; the same family-relative page index
+# rule applies: k = (va - base) >> 12.
+# SESSION 153-CONTINUED-63: the r1117 boot's setup push lives at
+# va=0x4200110 len=12288 - a THIRD window (0x4200000..0x4210000) the two
+# original families did not cover, so the content resolver answered
+# nothing and the read lottery could never be won for it (the
+# 0x4020000-family hold re-armed for its whole 20 s window).
+PB_GL_FAMILIES = ((0x3200000, 0x3300000), (0x4000000, 0x4100000),
+ (0x4200000, 0x4210000),
+ (0x42000000, 0x43000000))
# m165 (D-F1a): UVM semaphore-pool page range. The pool is a 4 KB sysmem
# uvm_rm_mem placed by the UVM arena right after the 16 MB CE push buffer;
# the client's tracker words (0x121000000 + 4*idx) live in its first page.
@@ -1674,7 +2229,8 @@ def main():
# deinit), so the set deliberately persists across client switches.
pool_pages = {}
pool_reemit_next = 0.0
- _want_state = {"mtime": None, "done": set()}
+ _want_state = {"mtime": None, "done": set(), "strict_pending": {},
+ "strict_try": {}}
loop = 0
while True:
pid = cdt_pid()
@@ -1788,17 +2344,119 @@ def main():
# main(), stopping ALL new pins (the observed
# pool-pin latency). Extract every 0x token instead;
# a bad token is skipped, never fatal.
- wvas = []
- for t in re.findall(r"0x[0-9a-fA-F]+",
- fw.read()):
+ text = fw.read()
+ _want_state["mtime"] = st_d.st_mtime
+ # SESSION 153-CONTINUED-64: strict demand anchors first.
+ # A line `S 0x<entry_va> 0x<off> <len>` (written by
+ # pin_gr_stream from the service's anchored debugfs
+ # demand record) asks for the exact [entry_va, +len)
+ # read window to be validated with the host's own full
+ # stream parse before a pin is injected. Loose lines
+ # (plain `0x<va>`) keep the content/pagemap paths, but
+ # a GL family with a pending anchor is deferred so its
+ # learn can only come from the strict validation.
+ strict = []
+ loose_tokens = []
+ for line in text.splitlines():
+ ms = re.match(
+ r"\s*S\s+0x([0-9a-fA-F]+)\s+0x([0-9a-fA-F]+)"
+ r"\s+(\d+)\s*$", line)
+ if ms:
try:
- wvas.append(int(t, 16))
+ strict.append((int(ms.group(1), 16),
+ int(ms.group(2), 16),
+ int(ms.group(3))))
except ValueError:
pass
- _want_state["mtime"] = st_d.st_mtime
- for wv in wvas[-64:]:
+ continue
+ loose_tokens += re.findall(r"0x[0-9a-fA-F]+", line)
+ now_d = time.time()
+ for epage in list(_want_state["strict_pending"]):
+ if now_d - _want_state["strict_pending"][epage] > 300.0:
+ del _want_state["strict_pending"][epage]
+ for (ev, eoff, eln) in reversed(strict):
+ epage = ev & ~0xFFF
+ if epage in _want_state["done"]:
+ continue
+ # SESSION 153-CONTINUED-65: throttle anchor
+ # re-resolution. The full-offset sweep can read
+ # thousands of windows (tens of seconds of CPU);
+ # the demand file re-sends the anchor every ~2 s,
+ # so without this throttle the scanner starves the
+ # client (both measured at ~91% CPU at r1125). One
+ # attempt per anchor per window is enough - content
+ # that lands late is picked up on the next window.
+ lt = _want_state["strict_try"].get(epage, 0.0)
+ if now_d - lt < ANCHOR_RETRY_S:
+ continue
+ _want_state["strict_try"][epage] = now_d
+ got = []
+ try:
+ ok = resolve_va_strict(kc, ev, eoff, eln, got,
+ pid)
+ except Exception:
+ ok = False
+ got = []
+ if not ok and eoff:
+ # The demand's off is the entry va's in-page
+ # offset; the push content can be page-aligned
+ # instead (r1123 vs r1125 parse behaviour). A
+ # second try at page alignment is cheap next to
+ # the completed sweep and the same pins result.
+ got2 = []
+ try:
+ ok = resolve_va_strict(kc, ev & ~0xFFF, 0,
+ eln, got2, pid)
+ except Exception:
+ ok = False
+ if ok:
+ got = got2
+ if ok and got:
+ lines.extend(got)
+ for g in got:
+ try:
+ _want_state["done"].add(
+ int(g.split(",", 1)[0], 16))
+ except ValueError:
+ pass
+ _want_state["done"].add(epage)
+ _want_state["strict_pending"].pop(epage, None)
+ else:
+ _want_state["strict_pending"].setdefault(
+ epage, now_d)
+ # Loose VAs in a family whose strict anchor is still
+ # pending are deferred to the strict path only.
+ pend_bases = set()
+ for epage in _want_state["strict_pending"]:
+ for lo, hi in PB_GL_FAMILIES:
+ if lo <= epage < hi:
+ pend_bases.add((lo, hi))
+ # SESSION 153-CONTINUED-55: process the NEWEST VAs first,
+ # not the deduped head. The file is append-only and grows
+ # to hundreds of lines (every stalled-VA row is appended
+ # each demand cycle, ~48/4 s under load), so both the
+ # literal tail (`wvas[-64:]`) and a first-occurrence
+ # dedupe sorted older pending VAs out of the window (the
+ # 16-page GL setup-push demand fell out between its
+ # appends and the next mtime change). Walk the raw tail
+ # backwards, newest instruction first, skipping resolved
+ # VAs, up to a bounded batch.
+ wvas = []
+ for t in reversed(loose_tokens):
+ try:
+ v = int(t, 16)
+ except ValueError:
+ continue
+ if v not in _want_state["done"] and v not in wvas:
+ wvas.append(v)
+ if len(wvas) >= 256:
+ break
+ for wv in wvas:
if wv in _want_state["done"]:
continue
+ if any(lo <= (wv & ~0xFFF) < hi
+ for lo, hi in pend_bases):
+ continue
got = []
if POOL_SEM_LO <= (wv & ~0xFFF) < POOL_SEM_HI:
# m165 (D-F1a): pool/waiter VAs have no method
@@ -1833,8 +2491,8 @@ def main():
# page: resolve it through the client's own
# pagemap. Without this the demand for those
# VAs was a silent no-op (resolve_va_by_content
- # rejects anything outside PB_CE_LO..PB_CE_HI)
- # and the fills kept dropping them.
+ # rejects anything outside its windows) and the
+ # fills kept dropping them.
if not ok and not got and pid:
try:
pfd = os.open('/proc/%d/pagemap' % pid,
diff --git a/tools/guest-diag/pin_gr_stream.py b/tools/guest-diag/pin_gr_stream.py
index 776b440..35c2ea3 100644
--- a/tools/guest-diag/pin_gr_stream.py
+++ b/tools/guest-diag/pin_gr_stream.py
@@ -131,11 +131,29 @@ def _demand_send(vas):
pass
-def _start_cmd():
+def _push_guest_script():
+ """Push the scanner into the guest in argv-safe chunks.
+
+ SESSION 153-CONTINUED-64: the one-shot `echo <b64> | base64 -d >` form
+ put the whole ~135 KB base64 of the (grown) scanner on vgpu_vm.py's
+ argv, and the host's MAX_ARG_STRLEN (128 KB) rejected the exec before
+ it ever reached the guest ('OSError: Argument list too long'), so no
+ scanner started and the boot streamed zero pins. Push 60 KB chunks and
+ print the guest-side md5 for verification.
+ """
with open(GUEST_SCRIPT) as f:
b64 = subprocess.run(["base64", "-w0"], stdin=f,
capture_output=True, text=True,
- timeout=10).stdout
+ timeout=10).stdout.strip()
+ vexec("rm -f /tmp/gps.b64", 30)
+ for off in range(0, len(b64), 60000):
+ vexec("printf %%s '%s' >> /tmp/gps.b64" % b64[off:off + 60000], 30)
+ return vexec("base64 -d /tmp/gps.b64 > /tmp/guest_pin_stream.py && "
+ "rm -f /tmp/gps.b64 && md5sum /tmp/guest_pin_stream.py "
+ "&& echo PUSH_OK", 60) or ""
+
+
+def _start_cmd():
prefixes = os.environ.get("CLIENT_PREFIXES",
"cdt,va,victory,Xorg,X,glxgears,gzdoom")
vma_every = os.environ.get("STREAM_VMA_EVERY", "1")
@@ -151,14 +169,13 @@ def _start_cmd():
# failed silently (r149).
return ("P=$(printf '^python3 /tmp/guest_pin_%%s.py' stream); "
"sudo pkill -f \"$P\" 2>/dev/null; sleep 1; "
- "echo '%s' | base64 -d > /tmp/guest_pin_stream.py; "
"sudo rm -f /tmp/guest_pin_stream.pid; "
"sudo setsid nohup env CLIENT_PREFIXES='%s' "
"STREAM_VMA_EVERY='%s' "
"python3 /tmp/guest_pin_stream.py %d %d %s %s %s "
">/tmp/guest_pin_stream.log 2>&1 & sleep 2; "
"sudo cat /tmp/guest_pin_stream.pid 2>/dev/null"
- % (b64, prefixes, vma_every, 2, PORT, LO, HI, PAMAX))
+ % (prefixes, vma_every, 2, PORT, LO, HI, PAMAX))
def install_and_start():
@@ -167,6 +184,14 @@ def install_and_start():
# a boot without the scanner streams no pins and the client then hangs.
for attempt in range(5):
_heartbeat()
+ try:
+ out_push = _push_guest_script()
+ if "PUSH_OK" not in (out_push or ""):
+ log("WARN: scanner push attempt %d incomplete: %s"
+ % (attempt + 1, (out_push or "")[-120:]))
+ except Exception as e:
+ log("WARN: scanner push attempt %d failed: %s"
+ % (attempt + 1, e))
out = vexec(_start_cmd(), 60)
if any(ch.isdigit() for ch in out):
return True
@@ -230,13 +255,31 @@ def _demand_loop():
expiry) as the stalled-VA class.
"""
track = {} # va -> [first_seen, last_sent]
+ # SESSION 153-CONTINUED-64: strict demand anchors (entry va -> [first,
+ # last_sent]) from the anchored debugfs lines. The guest validates the
+ # exact [entry_va, +len) read window with the host's full stream parse
+ # before pinning, so the setup-push pin can never land on a page whose
+ # bytes the host's read will reject. Resent fast (2 s) while the hold
+ # is up; the loose cadence (20 s) is too slow for the client's churn.
+ strict_track = {}
+ it = 0
while True:
- time.sleep(3)
- try:
- r = subprocess.run(["sudo", "-n", "dmesg"],
- capture_output=True, text=True, timeout=20)
- except Exception:
- continue
+ # SESSION 153-CONTINUED-57: the 3 s pass was the losing half of the
+ # r1096 race - the beef setup-hold's demanded pages resolved in the
+ # guest ~2-4 s after the arm, while the pump re-read the entry
+ # every ~110 ms and moved on. Poll fast (0.75 s) so a newly armed
+ # demand goes to the guest within a second; the expensive dmesg
+ # harvest stays on a slower cadence (every 4th pass) because the
+ # debugfs setup-demand set is the time-critical source.
+ time.sleep(0.75)
+ it += 1
+ r = None
+ if it % 4 == 1:
+ try:
+ r = subprocess.run(["sudo", "-n", "dmesg"],
+ capture_output=True, text=True, timeout=20)
+ except Exception:
+ continue
now = time.time()
for m in re.finditer(
r"(?:stalled VA #\d+[:=]\s*"
@@ -258,24 +301,85 @@ def _demand_loop():
# cannot, the guest content resolver is the last source -
# harvest the VA so it resolves and injects the real page.
r"|sema-pushzero: h=0x[0-9a-f]+ va=(0x[0-9a-f]+)",
- (r.stdout or "")[-200000:]):
+ (r.stdout or "")[-2000000:] if r else ""):
va = int(m.group(1) or m.group(2) or m.group(3), 16)
track.setdefault(va, [now, 0.0])
+ # SESSION 153-CONTINUED-51: the pushzero line now carries the
+ # push length; a zero-head WINDOW push spans 1-2 pages, and the
+ # demand of a single page leaves the capture zero - expand the
+ # range and demand every page so the guest pagemap resolves the
+ # whole push at once (bulk demand instead of one page per cycle).
+ for m in re.finditer(
+ r"sema-pushzero: h=0x[0-9a-f]+ va=(0x[0-9a-f]+) len=(\d+)",
+ (r.stdout or "")[-2000000:] if r else ""):
+ zva = int(m.group(1), 16)
+ zlen = int(m.group(2))
+ for zoff in range(0, min(zlen, 0x8000), 0x1000):
+ track.setdefault(zva + zoff, [now, 0.0])
+ # SESSION 153-CONTINUED-56: the armed setup-hold's demand VA set is
+ # exposed by the service as STATE (debugfs), not as a dmesg row, so
+ # it cannot race the ring rotation under vgsp-sema spam (r1090 lost
+ # the setup-push pages that way). Read it every pass and feed the
+ # same track/resend machinery: new VAs are sent this pass (last=0),
+ # existing ones keep the 20 s resend / 300 s expiry cadence.
+ try:
+ rr = subprocess.run(
+ ["sudo", "-n", "cat",
+ "/sys/kernel/debug/vgsp/sema_setup_demand"],
+ capture_output=True, text=True, timeout=10)
+ if rr.returncode == 0:
+ for line in (rr.stdout or "").splitlines():
+ ma = re.match(
+ r"\s*0x([0-9a-f]+)\s+0x([0-9a-f]+)\s+(\d+)\s*$",
+ line)
+ if ma:
+ ev = int(ma.group(1), 16)
+ off = int(ma.group(2), 16)
+ ln = int(ma.group(3))
+ if ev and 0 < ln <= 0x40000:
+ strict_track.setdefault(ev, [now, 0.0,
+ off, ln])
+ continue
+ for m in re.finditer(r"0x[0-9a-f]+", line):
+ sva = int(m.group(0), 16) & ~0xfff
+ if sva:
+ track.setdefault(sva, [now, 0.0])
+ except Exception:
+ pass
for va in list(track):
first, last = track[va]
if now - first > 300.0:
del track[va]
+ for ev in list(strict_track):
+ first, last, off, ln = strict_track[ev]
+ if now - first > 300.0:
+ del strict_track[ev]
+ cmds = []
todo = [va for va, (f, l) in track.items() if now - l > 20.0]
- if not todo:
+ if todo:
+ todo = todo[:48]
+ cmds.append(_demand_write_cmd(todo))
+ stodo = [ev for ev, (f, l, o, ln) in strict_track.items()
+ if now - l > 2.0]
+ if stodo:
+ stodo = stodo[:16]
+ cmds.append("; ".join(
+ "echo S 0x%x 0x%x %d >> /tmp/guest_want_vas.txt"
+ % (ev, strict_track[ev][2], strict_track[ev][3])
+ for ev in stodo))
+ if not cmds:
continue
- cmd = _demand_write_cmd(todo)
try:
- vexec(cmd, 30)
+ vexec("; ".join(cmds), 30)
except Exception:
continue
for va in todo:
track[va][1] = now
- log("demand: sent %d stalled VA(s) to guest" % len(todo))
+ for ev in stodo:
+ strict_track[ev][1] = now
+ log("demand: sent %d stalled VA(s)%s to guest" %
+ (len(todo),
+ " + %d strict anchor(s)" % len(stodo) if stodo else ""))
_DEMAND_STARTED = False