From 17dcf049aca7b5a65d68389a110dc87d915a5393 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Rasmusson Date: Thu, 5 Nov 2026 12:22:00 -0500 Subject: vgsp-gr: re-target the captured render surface per launch so a partial VA-colliding allocation cannot receive the draw The SURF-PRELUDE target pair was patched once per capture generation and then reused; the re-patch test was dead because gsp_sema_gr_surf_recs() only fills in the VA when it returns true and the code negated the call, so 'want' could never be computed. A later draw whose ~10-page SYS allocation sits at the same 0x1204xxxxx VA as the X server's 1280x720 screen inherited the X server's 900-page target; the engine's RT pair then named the client's command/operand buffer, which is why the 'surface' held method words and copy-out saw no engine pixels. Add gsp_phys_gr_surf_launch_target(): only a complete contiguous rec run for the captured surface may use its data-rec bounce lane; a partial run falls back to the boot-time contiguous alias built from the captured bind's VAS pages (the real screen HPAs). Re-patch whenever the cached pair differs from that effective target (SURF-PRELUDE retarget); RT0 copies the same words, so the RT0==RT1 design is unchanged. r1490 (VP_MODE=6 RT0=1, r1476-args): 15 launches, poison=0, retarget have=0xa000000 want=0x15e0000 for the partial lane then 900-page targets, output retirement up to 3/900 with 2 copy-out pages changed by engine, and a non-empty 3.69 MB xwd. New wall: the draw still produces no raster fragments - the only changed pages are the forced MME constant-buffer pages inside the screen (offset pages 768/772/773, 0xff000000 fills). Also harden tools/guest-diag/qga_fetch.py (retry open, long timeout, atomic .part write, clear error exit) for evidence capture. --- kernel/gsp_phys_booter.c | 113 ++++++++++++++++++++++++++++++++---------- tools/guest-diag/qga_fetch.py | 94 ++++++++++++++++++++++++++++------- 2 files changed, 162 insertions(+), 45 deletions(-) diff --git a/kernel/gsp_phys_booter.c b/kernel/gsp_phys_booter.c index 6f2a858..d6ce2c2 100644 --- a/kernel/gsp_phys_booter.c +++ b/kernel/gsp_phys_booter.c @@ -25212,6 +25212,64 @@ struct gsp_phys_gr_surf_keep { static struct gsp_phys_gr_surf_keep gsp_phys_gr_surf[4]; static u32 gsp_phys_gr_surf_n; +/* + * r1490: the effective engine target of the captured surface ON THIS LAUNCH. + * The captured-surface rec set (gsp_sema_gr_surf_recs) is global - it is the + * last C597 colour-target bind a trusted channel emitted - while a launch's + * own data recs only cover it when the fill resolved the WHOLE span through + * the captured bind's VAS. Several clients share the same 0x1200xxxxx VAs: + * the beef client holds a ~10-page SYS allocation at 0x120400000, the X + * server's 1280x720 screen of the same VA is a 900-page FB allocation. A + * partial run must never win the rec-over-alias preference: the engine would + * be pointed at the client's code/operand buffer, its stores would land in + * (and its copy-in would read) method/SASS words instead of the screen, and + * the surface would look like it received the method stream. + * + * Complete rec run (contiguous base..last reloc VA) -> its bounce base, the + * path that lets the retire wait and copy-out see the pixels. Otherwise the + * per-boot contiguous alias built from the captured bind's VAS pages is the + * target: the engine writes the guest screen HPAs directly, exactly as the + * pre-r1483 alias path did. 0 = neither is available (caller keeps the + * per-page path). + */ +static u64 gsp_phys_gr_surf_launch_target(const struct gsp_phys_gr_submit *req) +{ + u64 sva = 0, last_va, base = 0, last = 0; + u32 sn = 0, di; + bool have_base = false, have_last = false; + + if (!req) + return 0; + if (gsp_sema_gr_surf_recs(&sva, &sn) && sva && sn >= 2u) { + for (di = 0; di < req->n_data; di++) { + if (req->data[di].va == sva && + req->data[di].reloc_va) { + base = req->data[di].reloc_va; + have_base = true; + break; + } + } + last_va = sva + ((u64)(sn - 1u) << 12); + for (di = 0; di < req->n_data; di++) { + if (req->data[di].va == last_va && + req->data[di].reloc_va) { + last = req->data[di].reloc_va; + have_last = true; + break; + } + } + if (have_base && have_last && + last == base + ((u64)(sn - 1u) << 12)) + return base; + } + if (gsp_sema_surf_va_get(&sva, &sn)) { + for (di = 0; di < gsp_phys_gr_surf_n; di++) + if (gsp_phys_gr_surf[di].va == sva) + return gsp_phys_gr_surf[di].alias; + } + return 0; +} + /* * SESSION 153-CONTINUED-27: slack tail for the alias. r994 measured the * replay's colour writes landing past the declared w*h*4 footprint @@ -29098,33 +29156,26 @@ gr_page_unmapped: u64 want = 0; /* - * r1483: the rec-over-alias preference below - * rewrites the pair to THIS launch's data-rec - * bounce VA, and the fill alternates its - * relocation half per capture push. A buffer - * patched once per capture generation would - * keep the first launch's half forever, so - * re-patch when the launch's complete surface - * rec names a different target. + * r1483/r1490: the rec-over-alias preference + * below rewrites the pair to THIS launch's + * data-rec bounce VA, and the fill alternates + * its relocation half per capture push. A + * buffer patched once per capture generation + * would keep the first launch's target + * forever - including the X server's 900-page + * rec lane when the next launch is the beef + * client whose partial 10-page allocation sits + * at the same VA. Re-patch whenever this + * launch's EFFECTIVE surface target (complete + * rec run or the contiguous alias, see + * gsp_phys_gr_surf_launch_target) differs + * from the cached words, so a partial launch + * always falls back to the real screen alias + * and never to its own code/operand buffer. */ if (!repatch && gsp_phys_gr_surf_pre_n >= 3) { - u64 sva = 0; - u32 sn = 0, di; - - if (!gsp_sema_gr_surf_recs(&sva, &sn) && - sva) { - for (di = 0; di < req->n_data; - di++) { - if (req->data[di].va == - sva && - req->data[di] - .reloc_va) { - want = req->data[di] - .reloc_va; - break; - } - } - } + want = gsp_phys_gr_surf_launch_target( + req); if (want) { u64 have = ((u64)(gsp_phys_gr_surf_pre[1] & @@ -29132,8 +29183,18 @@ gr_page_unmapped: (u64)(gsp_phys_gr_surf_pre[2] & 0xfffffffcu); - if (have != want) + if (have != want) { + static u32 retarget_log; + repatch = true; + if (retarget_log < 16u) { + retarget_log++; + pr_info("vgsp-gr: SURF-PRELUDE retarget have=0x%llx want=0x%llx h=0x%x push=0x%llx\n", + have, want, + req->h_object, + req->push_va); + } + } } } if (repatch) { diff --git a/tools/guest-diag/qga_fetch.py b/tools/guest-diag/qga_fetch.py index 5a940fc..2fd53fc 100755 --- a/tools/guest-diag/qga_fetch.py +++ b/tools/guest-diag/qga_fetch.py @@ -2,15 +2,29 @@ # Fetch a file from the guest via the QEMU guest agent protocol # (/tmp/qemu-guest-agent.sock, the virtio-serial QGA chardev - the same # socket vgpu_vm.py exec uses). Usage: qga_fetch.py [chunk] +# +# Robustness (r1490): the r1489 evidence capture fetched nothing because the +# guest-side frame was still being written / the agent answered late. Retry +# the open a few times, tolerate error replies with a clear diagnostic, use a +# long socket timeout, and write to a temporary file so a partial transfer is +# never mistaken for a complete capture. import base64 import json +import os import socket import sys +import time + +if len(sys.argv) < 3: + print("usage: qga_fetch.py [chunk]", file=sys.stderr) + sys.exit(2) GUEST = sys.argv[1] HOST = sys.argv[2] CHUNK = int(sys.argv[3]) if len(sys.argv) > 3 else 1 << 20 SOCK = "/tmp/qemu-guest-agent.sock" +TRIES = 6 +TMP = HOST + ".part" def recv_line(s): @@ -29,25 +43,67 @@ def cmd(s, obj): return json.loads(line) -s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) -s.settimeout(30.0) -s.connect(SOCK) -r = cmd(s, {"execute": "guest-file-open", - "arguments": {"path": GUEST, "mode": "r"}}) -h = r["return"] +def open_guest(): + last = None + for i in range(TRIES): + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.settimeout(120.0) + s.connect(SOCK) + r = cmd(s, {"execute": "guest-file-open", + "arguments": {"path": GUEST, "mode": "r"}}) + if "return" in r: + return s, r["return"], None + last = f"guest-file-open error: {r.get('error')}" + except Exception as e: # noqa: BLE001 - diagnostic surface + last = f"guest-file-open exception: {e}" + try: + s.close() + except Exception: + pass + time.sleep(1.5) + if i + 1 < TRIES: + print(f"retry {i + 1}/{TRIES}: {last}", file=sys.stderr) + return None, None, last + + +s, h, err = open_guest() +if s is None: + print(f"FETCH-FAIL {GUEST} -> {HOST}: {err}", file=sys.stderr) + sys.exit(1) + print(f"handle={h}") total = 0 -with open(HOST, "wb") as out: - while True: - r = cmd(s, {"execute": "guest-file-read", - "arguments": {"handle": h, "count": CHUNK}}) - d = r["return"] - buf = base64.b64decode(d.get("buf-b64", "")) - if buf: - out.write(buf) - total += len(buf) - print(f"\r{total}", end="", flush=True) - if d.get("eof"): - break -cmd(s, {"execute": "guest-file-close", "arguments": {"handle": h}}) +try: + with open(TMP, "wb") as out: + while True: + r = cmd(s, {"execute": "guest-file-read", + "arguments": {"handle": h, "count": CHUNK}}) + if "return" not in r: + raise RuntimeError(f"guest-file-read error: {r.get('error')}") + d = r["return"] + buf = base64.b64decode(d.get("buf-b64", "")) + if buf: + out.write(buf) + total += len(buf) + print(f"\r{total}", end="", flush=True) + if d.get("eof"): + break + cmd(s, {"execute": "guest-file-close", "arguments": {"handle": h}}) +except Exception as e: # noqa: BLE001 + print(f"\nFETCH-FAIL {GUEST} -> {HOST}: {e}", file=sys.stderr) + try: + if os.path.exists(TMP): + os.unlink(TMP) + except OSError: + pass + sys.exit(1) +finally: + try: + s.close() + except Exception: + pass + +os.replace(TMP, HOST) print(f"\nFETCHED {total} bytes -> {HOST}") +sys.exit(0) -- cgit v1.2.3