summaryrefslogtreecommitdiffstats
path: root/tools/guest-diag/guest_pin_stream.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/guest-diag/guest_pin_stream.py')
-rw-r--r--tools/guest-diag/guest_pin_stream.py798
1 files changed, 777 insertions, 21 deletions
diff --git a/tools/guest-diag/guest_pin_stream.py b/tools/guest-diag/guest_pin_stream.py
index 3db61ba..a645a86 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,612 @@ 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 _strict_kvm_sweep(kc, ent_va, off, ln, out):
+ """Kernel-vmap sweep for an anchor with no known-family page index.
+
+ SESSION 153-CONTINUED-74: mirrors the k == 0 known-family path (the
+ resolver that found the r1126 0x3200000 macro window) but works for ANY
+ anchor outside PB_GL_FAMILIES. Every candidate window passes the host's
+ own full parse (`_window_accept`) before a single pin is emitted, so a
+ false positive is impossible by construction; the cheap per-page prefix
+ walk rejects arbitrary data after a few words. Bounded: at most
+ 8192 candidate windows and at most 64 pages of a window (the same caps
+ as resolve_va_client_mem).
+
+ Areas swept: the nv_vm_map_pages vmap set (the historical resolver) plus
+ the nvidia os_alloc_mem vmalloc areas (`vpages`), which the r1156 probe
+ showed can be large (571 pages in one area) and are completely outside
+ _nv_vmap_areas' filter. A window is only emitted for an area whose pages
+ are all present in the CPU alias.
+ """
+ import re as _re
+
+ page = ent_va & ~0xFFF
+ need = (off + ln + 0xFFF) // 0x1000
+ if ln <= 0 or ln > 0x40000 or need > 64:
+ return False
+ areas = list(_nv_vmap_areas(kc))
+ try:
+ for line in open("/proc/vmallocinfo"):
+ if "[nvidia]" not in line or "vmalloc" not in line:
+ continue
+ if "nv_vm_map_pages" in line or "phys=" in line:
+ continue
+ if "os_map_kernel_space" in line or "ioremap" in line:
+ continue
+ m = _re.match(r'0x([0-9a-f]+)-0x([0-9a-f]+)\s+(\d+)', line)
+ if not m:
+ continue
+ lo = int(m.group(1), 16)
+ hi = int(m.group(2), 16)
+ pages = (hi - lo) >> 12
+ if pages >= need and lo < hi:
+ areas.append((lo, hi, pages))
+ except OSError:
+ pass
+ tried = 0
+ for (lo, _hi, pages) in areas:
+ for adj in range(0, max(1, pages - need + 1)):
+ tried += 1
+ if tried > 8192:
+ return False
+ cpu = lo + adj * 0x1000
+ first = kc.read(cpu, 0x1000)
+ if not first:
+ continue
+ 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))
+ dbg("demand-va: 0x%x -> pa=0x%x (%s vmap 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 vmap sweep failed (%u B, %u windows)"
+ % (page, ln, tried))
+ return False
+
+
+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.
+ #
+ # SESSION 153-CONTINUED-74: the unknown-family branch skipped the
+ # kernel-vmap sweep entirely - the known-family path has always tried
+ # it, and it is the resolver that found the r1126 0x3200000 macro
+ # window (a KERNEL-mapped allocation no process pagemap covers). The
+ # r1156 held push (va=0x3e4ccccc, 123916 B) failed here after only
+ # the client-mem attempt ("strict unknown-family client-mem failed"),
+ # so a kernel-resident window could never be found for any anchor
+ # outside PB_GL_FAMILIES. Try the vmap sweep first (same full-parse
+ # gate; strictly additive - a failed sweep costs the bounded reads
+ # and falls through to the client-mem path unchanged).
+ try:
+ if _strict_kvm_sweep(kc, ent_va, off, ln, out):
+ return True
+ except Exception:
+ pass
+ 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 (vmap+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 +1450,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 +2327,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 +2442,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 +2589,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,