summaryrefslogtreecommitdiffstats
path: root/kernel
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-18 08:07:37 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-18 08:07:49 -0400
commit9500c994fde39ae4661a60d24d3fd31553d32af4 (patch)
treefac7f699744bcb10cb954463e1e228a3092934d4 /kernel
parent03d1ddbcbba333ed8bcc834ee9e6623e24f813e1 (diff)
downloadopen-iov-9500c994fde39ae4661a60d24d3fd31553d32af4.tar.gz
open-iov-9500c994fde39ae4661a60d24d3fd31553d32af4.zip
vgsp: copy completeness hardening + COPYAUDIT/COPY-TRUNC instrumentation
Map every guest-data copy path and its caps (CE soft copy/fill/inline, GR fill span, late/gap span, bounce copy-in/out, forwarded CE copy, launch-page pools) and close the two silent partial-data holes: - late/gap span cap (gsp_phys_booter.c): a word cut at the 16-page cap was still rewritten to the partial run; probe one page past the cut and log COPY-TRUNC when it is resolvable (r218 shape: 4 words x 16 pages = all four cut at the cap). - GR fill span walk (gsp_sema_service.c): a mid-span resolution miss after >=1 authored page now logs COPY-TRUNC (was GRFILL-DROP only, launch proceeded with fill_hold=0). Instrumentation (all additive, no new locks/waits, readback uses the same accessor the write just used): - COPYAUDIT path=... va/len/done for every >=32 KB copy (CE soft copy, fill, GRCE inline, GR bounce/gap copy-in and copy-out). - COPY-MISMATCH 16-byte start/end destination readback vs the written bytes right after the copy (CE first/last chunk; fill and inline first 16 B; GR staging first+last 16 B of every page). - COPY-TRUNC on every partial copy: CE loop returns, fill/grce chunk failures, short GR staging/copy-out counts, reloc-fix-span, gr-fill-span; want>done is rate-limited but never silent. make kernel green; only the pre-existing %x warning at gsp_phys_booter.c:14088.
Diffstat (limited to 'kernel')
-rw-r--r--kernel/gsp_phys_booter.c161
-rw-r--r--kernel/gsp_sema_service.c196
2 files changed, 317 insertions, 40 deletions
diff --git a/kernel/gsp_phys_booter.c b/kernel/gsp_phys_booter.c
index 095a0c3..aa011e4 100644
--- a/kernel/gsp_phys_booter.c
+++ b/kernel/gsp_phys_booter.c
@@ -17395,27 +17395,61 @@ static bool gsp_phys_gr_reloc_fix_word(const struct gsp_phys_gr_submit *req,
gsp_phys_gr_gap_stage_log(wb, "region-full", -ENOSPC);
return false;
}
- for (i = 1; i < GSP_PHYS_GR_GAP_SPAN_MAX; i++) {
- u64 va = wb + ((u64)i << 12);
- u64 ignored = 0;
+ {
+ u32 span_done = 1; /* pages authored this word */
+ bool span_capped = false;
+
+ for (i = 1; i < GSP_PHYS_GR_GAP_SPAN_MAX; i++) {
+ u64 va = wb + ((u64)i << 12);
+ u64 ignored = 0;
+ /*
+ * A page with an existing proxy cannot get a
+ * second one (two copy-out writers for one
+ * guest page would clobber each other); stop
+ * the span before it. A merely unresolved
+ * page (the common end-of-buffer case)
+ * truncates the span like the fill's own span
+ * walk does.
+ */
+ if (gsp_phys_gr_page_tracked(req, va)) {
+ overlap = true;
+ break;
+ }
+ if (gsp_sema_gr_reloc_resolve(va, &r))
+ break;
+ if (!gsp_phys_gr_gap_add(req->n_data, va, r.hpa,
+ &ignored)) {
+ /* capacity cut: tail cannot be
+ * authored though more mapping may
+ * exist - reported below. */
+ span_capped = true;
+ break;
+ }
+ span_done++;
+ }
/*
- * A page with an existing proxy cannot get a second
- * one (two copy-out writers for one guest page would
- * clobber each other); stop the span before it. A
- * merely unresolved page (the common end-of-buffer
- * case) truncates the span like the fill's own span
- * walk does.
+ * COPYAUDIT completeness probe (2026-10-18): the
+ * span cap is intentional (r217 containment), but a
+ * word cut at the cap with a still-resolvable next
+ * page means the engine's base+offset reads past
+ * the authored run into later operands' bounce
+ * pages - silent partial data. One probe page past
+ * the cut proves it and makes the truncation loud.
*/
- if (gsp_phys_gr_page_tracked(req, va)) {
- overlap = true;
- break;
+ if (!overlap && span_done == GSP_PHYS_GR_GAP_SPAN_MAX) {
+ struct gsp_sema_gr_reloc r2;
+ u64 next_va = wb +
+ ((u64)GSP_PHYS_GR_GAP_SPAN_MAX << 12);
+
+ if (!gsp_sema_gr_reloc_resolve(next_va, &r2))
+ span_capped = true;
}
- if (gsp_sema_gr_reloc_resolve(va, &r))
- break;
- if (!gsp_phys_gr_gap_add(req->n_data, va, r.hpa,
- &ignored))
- break; /* cap: the tail stays as authored */
+ if (span_capped)
+ pr_info_ratelimited("vgsp: COPY-TRUNC va=0x%llx want>=0x%llx done=0x%llx path=reloc-fix-span\n",
+ wb,
+ ((u64)span_done + 1) << 12,
+ (u64)span_done << 12);
}
if (overlap) {
/*
@@ -17620,17 +17654,39 @@ static bool gsp_phys_gr_qmd_fetched(const u8 *qmd_mem)
}
/*
+ * COPYAUDIT (2026-10-18 copy-completeness audit): per-copy sighting of the
+ * GR operand staging copies. Every staged page is spot-checked (16 bytes
+ * at the start and end of the page, source vs destination, right after the
+ * memcpy) and any difference logs COPY-MISMATCH; the callers log the
+ * per-launch COPYAUDIT aggregate when the staged set exceeds 32 KB, and
+ * COPY-TRUNC whenever fewer pages were staged than the launch declared
+ * (want>done). Purely diagnostic: no control flow, locking or timing
+ * change; the memcmp reads memory the memcpy just touched.
+ */
+#define GSP_PHYS_GR_AUDIT_MIN (32u << 10) /* >32 KB staged sets */
+
+static void gsp_phys_gr_page_spot_check(const u8 *dst, const u8 *src,
+ u64 va, const char *path)
+{
+ if (memcmp(dst, src, 16) ||
+ memcmp(dst + GSP_PHYS_PAGE_SIZE - 16,
+ src + GSP_PHYS_PAGE_SIZE - 16, 16))
+ pr_info_ratelimited("vgsp: COPY-MISMATCH va=0x%llx off=0x%llx path=%s\n",
+ va, 0ull, path);
+}
+
+/*
* m153: fill the bounce window from the request's guest pages and capture a
* per-page checksum baseline. Used before the first publish and again before
* every outcome retry (a mis-executing first attempt can leave stale or
* wrong-window content behind, so a retry must start from the guest inputs
- * again).
+ * again). Returns the number of pages staged.
*/
-static void gsp_phys_gr_fill_bounce(const struct gsp_phys_gr_submit *req,
- u32 *csum_in)
+static u32 gsp_phys_gr_fill_bounce(const struct gsp_phys_gr_submit *req,
+ u32 *csum_in)
{
struct gsp_phys_boot_chunk *dchunk = gsp_phys_gr_data_chunk();
- u32 di;
+ u32 di, copied = 0;
for (di = 0; di < req->n_data; di++) {
u32 bi = gsp_phys_gr_data_bi(req->data[di].reloc_va);
@@ -17645,11 +17701,15 @@ static void gsp_phys_gr_fill_bounce(const struct gsp_phys_gr_submit *req,
if (!dst)
continue;
memcpy(dst, src, GSP_PHYS_PAGE_SIZE);
+ gsp_phys_gr_page_spot_check(dst, src, req->data[di].va,
+ "gr-bounce-copy-in");
for (ci = 0; ci < GSP_PHYS_PAGE_SIZE; ci += 4)
csum += get_unaligned_le32(dst + ci);
csum_in[bi] = csum;
+ copied++;
}
dma_wmb();
+ return copied;
}
/*
@@ -17658,11 +17718,12 @@ static void gsp_phys_gr_fill_bounce(const struct gsp_phys_gr_submit *req,
* gsp_phys_gr_fill_bounce does for the fill's tracked data pages. Called
* before the doorbell (initial copy-in) and again before every outcome
* retry, so a retried launch starts from the guest inputs in both sets.
+ * Returns the number of pages staged.
*/
-static void gsp_phys_gr_fill_gap_bounce(u32 *csum_in)
+static u32 gsp_phys_gr_fill_gap_bounce(u32 *csum_in)
{
struct gsp_phys_boot_chunk *dchunk = gsp_phys_gr_data_chunk();
- u32 gi;
+ u32 gi, copied = 0;
for (gi = 0; gi < gsp_phys_gr_gap_n; gi++) {
const u8 *src = (const u8 *)__va(gsp_phys_gr_gap[gi].hpa);
@@ -17672,11 +17733,15 @@ static void gsp_phys_gr_fill_gap_bounce(u32 *csum_in)
if (!src || !dst)
continue;
memcpy(dst, src, GSP_PHYS_PAGE_SIZE);
+ gsp_phys_gr_page_spot_check(dst, src, gsp_phys_gr_gap[gi].va,
+ "gr-bounce-copy-in");
for (ci = 0; ci < GSP_PHYS_PAGE_SIZE; ci += 4)
csum += get_unaligned_le32(dst + ci);
csum_in[gsp_phys_gr_gap[gi].bi] = csum;
+ copied++;
}
dma_wmb();
+ return copied;
}
/*
@@ -18803,10 +18868,18 @@ gr_page_unmapped:
ret = -EAGAIN;
goto out;
}
- gsp_phys_gr_fill_bounce(req, bounce_csum_in);
- dcopied = req->n_data;
+ dcopied = gsp_phys_gr_fill_bounce(req, bounce_csum_in);
pr_info("vgsp-gr: bounce copy-in: %u pages, %u skipped (pre-publish)\n",
dcopied, dfailed);
+ if ((u64)req->n_data << 12 >= GSP_PHYS_GR_AUDIT_MIN)
+ pr_info("vgsp: COPYAUDIT path=gr-bounce-copy-in va=0x%llx len=0x%llx done=0x%llx\n",
+ req->data[0].va, (u64)req->n_data << 12,
+ (u64)dcopied << 12);
+ if (dcopied < req->n_data)
+ pr_err_ratelimited("vgsp: COPY-TRUNC va=0x%llx want=0x%llx done=0x%llx path=gr-bounce-copy-in\n",
+ req->data[0].va,
+ (u64)req->n_data << 12,
+ (u64)dcopied << 12);
for (di = 0; di < req->n_data && di < 3; di++)
pr_info("vgsp-gr: data[%u] va=0x%llx hpa=0x%llx reloc=0x%llx\n",
di, req->data[di].va, req->data[di].hpa,
@@ -18821,10 +18894,21 @@ gr_page_unmapped:
* like the block above.
*/
if (gsp_phys_gr_gap_n && gsp_phys_gr_data_chunk()->pages) {
- gsp_phys_gr_fill_gap_bounce(bounce_csum_in);
+ u32 gcopied = gsp_phys_gr_fill_gap_bounce(bounce_csum_in);
+
pr_info("vgsp-gr: gap copy-in: %u late-resolved pages, %llu word(s) fixed first=0x%llx\n",
gsp_phys_gr_gap_n, gsp_phys_gr_reloc_fixed,
gsp_phys_gr_reloc_fixed_first);
+ if ((u64)gsp_phys_gr_gap_n << 12 >= GSP_PHYS_GR_AUDIT_MIN)
+ pr_info("vgsp: COPYAUDIT path=gr-gap-copy-in va=0x%llx len=0x%llx done=0x%llx\n",
+ gsp_phys_gr_gap[0].va,
+ (u64)gsp_phys_gr_gap_n << 12,
+ (u64)gcopied << 12);
+ if (gcopied < gsp_phys_gr_gap_n)
+ pr_err_ratelimited("vgsp: COPY-TRUNC va=0x%llx want=0x%llx done=0x%llx path=gr-gap-copy-in\n",
+ gsp_phys_gr_gap[0].va,
+ (u64)gsp_phys_gr_gap_n << 12,
+ (u64)gcopied << 12);
}
ret = gsp_phys_rmcall_publish_locked(bar0, boot_stage.rmcall_va_pb,
@@ -19983,8 +20067,20 @@ gr_page_unmapped:
}
memcpy((u8 *)__va(req->data[wbi].hpa), src,
GSP_PHYS_PAGE_SIZE);
+ gsp_phys_gr_page_spot_check(
+ (const u8 *)__va(req->data[wbi].hpa), src,
+ req->data[wbi].va, "gr-bounce-copy-out");
wrote++;
}
+ if ((u64)req->n_data << 12 >= GSP_PHYS_GR_AUDIT_MIN)
+ pr_info("vgsp: COPYAUDIT path=gr-bounce-copy-out va=0x%llx len=0x%llx done=0x%llx\n",
+ req->data[0].va, (u64)req->n_data << 12,
+ (u64)wrote << 12);
+ if (wrote < req->n_data)
+ pr_err_ratelimited("vgsp: COPY-TRUNC va=0x%llx want=0x%llx done=0x%llx path=gr-bounce-copy-out\n",
+ req->data[0].va,
+ (u64)req->n_data << 12,
+ (u64)wrote << 12);
/* m143b: unconditional first words of the first page of each
* 32-page relocation window after the run, so the engine's
* read/write footprint per window is provable without relying
@@ -20044,10 +20140,23 @@ gr_page_unmapped:
}
memcpy((u8 *)__va(gsp_phys_gr_gap[gi].hpa), src,
GSP_PHYS_PAGE_SIZE);
+ gsp_phys_gr_page_spot_check(
+ (const u8 *)__va(gsp_phys_gr_gap[gi].hpa), src,
+ gsp_phys_gr_gap[gi].va, "gr-gap-copy-out");
gwrote++;
}
pr_info("vgsp-gr: gap copy-out: %u pages written back, %u changed by engine (first bi: %u %u %u %u)\n",
gwrote, gchanged, gchg[0], gchg[1], gchg[2], gchg[3]);
+ if ((u64)gsp_phys_gr_gap_n << 12 >= GSP_PHYS_GR_AUDIT_MIN)
+ pr_info("vgsp: COPYAUDIT path=gr-gap-copy-out va=0x%llx len=0x%llx done=0x%llx\n",
+ gsp_phys_gr_gap[0].va,
+ (u64)gsp_phys_gr_gap_n << 12,
+ (u64)gwrote << 12);
+ if (gwrote < gsp_phys_gr_gap_n)
+ pr_err_ratelimited("vgsp: COPY-TRUNC va=0x%llx want=0x%llx done=0x%llx path=gr-gap-copy-out\n",
+ gsp_phys_gr_gap[0].va,
+ (u64)gsp_phys_gr_gap_n << 12,
+ (u64)gwrote << 12);
}
poll = w1;
if (bar0)
diff --git a/kernel/gsp_sema_service.c b/kernel/gsp_sema_service.c
index 88c519d..91a128f 100644
--- a/kernel/gsp_sema_service.c
+++ b/kernel/gsp_sema_service.c
@@ -4290,6 +4290,54 @@ static bool svc_dst_watched(u64 dst)
}
/*
+ * COPYAUDIT shared budget (see the svc_copy comment for the contract):
+ * every copy at/above SVC_COPY_AUDIT_MIN is logged once with want/done and
+ * gets a first/last-16-bytes destination readback vs the written bytes
+ * (COPY-MISMATCH). One budget covers every copy path in this file so a
+ * whole cycle stays visible.
+ */
+#define SVC_COPY_AUDIT_MIN (32u << 10) /* >32 KB copies audited */
+#define SVC_COPY_AUDIT_MAX 2048u /* per-boot line budget */
+static u64 svc_copy_audit_n;
+
+/* COPYAUDIT for a finished fill: log want/done and check the first 16
+ * destination bytes against the repeated pattern. The readback uses the
+ * accessor matching the aperture the fill wrote through. */
+static void svc_fill_audit(struct nv_vgpu_device *vgpu, u64 va0, u64 addr,
+ u32 aperture, size_t total, const u8 *pat, u32 elem)
+{
+ u8 exp[16], got[16] = {0};
+ size_t i;
+ bool ok = false;
+
+ if (total < SVC_COPY_AUDIT_MIN || svc_copy_audit_n >= SVC_COPY_AUDIT_MAX)
+ return;
+ /*
+ * The fill writes the 8-byte pattern group repeatedly (pat is the
+ * element zero-padded to 8 bytes), so the expectation mirrors that:
+ * byte i of the destination equals pat[i % 8].
+ */
+ for (i = 0; i < sizeof(exp); i++)
+ exp[i] = pat[i % 8];
+ if (aperture == CE_APERTURE_VID) {
+ u8 *base = vgpu->bars[1].vaddr;
+
+ if (base && addr + sizeof(got) <= svc_fb_shadow_size(vgpu)) {
+ memcpy(got, base + addr, sizeof(got));
+ ok = true;
+ }
+ } else if (!svc_read(vgpu, addr, got, sizeof(got))) {
+ ok = true;
+ }
+ svc_copy_audit_n++;
+ pr_info("vgsp: COPYAUDIT path=sema-fill va=0x%llx len=0x%llx done=0x%llx\n",
+ va0, (u64)total, (u64)total);
+ if (ok && memcmp(exp, got, sizeof(exp)))
+ pr_info_ratelimited("vgsp: COPY-MISMATCH va=0x%llx off=0x0 path=sema-fill want=%*phN got=%*phN\n",
+ va0, 16, exp, 16, got);
+}
+
+/*
* Write `count` elements of `value` (element size `elem`) starting at
* `addr`, in vidmem or guest RAM depending on the aperture DST_TYPE
* selected. Returns 0 on success.
@@ -4382,6 +4430,7 @@ static int svc_fill(struct nv_vgpu_device *vgpu, u64 addr, u64 value, u32 elem,
memcpy(base + addr + o, pat, 8);
if (o < total)
memcpy(base + addr + o, pat, total - o);
+ svc_fill_audit(vgpu, va0, addr, aperture, total, pat, elem);
return 0;
}
@@ -4396,10 +4445,15 @@ static int svc_fill(struct nv_vgpu_device *vgpu, u64 addr, u64 value, u32 elem,
memcpy(bounce + i, pat, 8);
if (i < chunk)
memcpy(bounce + i, pat, chunk - i);
- if (svc_write(vgpu, addr + o, bounce, chunk))
+ if (svc_write(vgpu, addr + o, bounce, chunk)) {
+ if (total >= SVC_COPY_AUDIT_MIN)
+ pr_err_ratelimited("vgsp: COPY-TRUNC va=0x%llx want=0x%llx done=0x%llx path=sema-fill rc=%d\n",
+ va0, (u64)total, (u64)o, -EIO);
return -EIO;
+ }
o += chunk;
}
+ svc_fill_audit(vgpu, va0, addr, aperture, total, pat, elem);
return 0;
}
@@ -4920,6 +4974,48 @@ static void svc_ledger_clear(void)
svc_ledger_reloc_diverges = 0;
}
+/*
+ * COPYAUDIT (2026-10-18 copy-completeness audit): every copy at/above
+ * SVC_COPY_AUDIT_MIN is logged once with its want/done byte counts, and
+ * its first/last 16 destination bytes are read back immediately and
+ * compared against the bytes just written (COPY-MISMATCH on difference).
+ * COPY-TRUNC marks a copy that failed after partially writing (want>done),
+ * so a partial transfer can never be silent. Pure diagnostics: no control
+ * flow, locking or timing change; the readback uses the same accessor the
+ * write just used. Definitions live with svc_fill (first user).
+ */
+static void svc_copy_audit_chunk(struct nv_vgpu_device *vgpu, u64 dst_va,
+ u64 o, const u8 *bounce, size_t chunk,
+ bool shadow, u64 pa_eff, u32 aper)
+{
+ size_t n = chunk < 16 ? chunk : 16;
+ u8 rb[16] = {0};
+
+ if (shadow) {
+ if (svc_vas_read(dst_va + o, rb, n))
+ return;
+ } else if (aper == CE_APERTURE_VID) {
+ u8 *base = vgpu->bars[1].vaddr;
+
+ if (!base || pa_eff + n > svc_fb_shadow_size(vgpu))
+ return;
+ memcpy(rb, base + pa_eff, n);
+ } else if (svc_read(vgpu, pa_eff, rb, n)) {
+ return;
+ }
+ if (memcmp(rb, bounce, n))
+ pr_info_ratelimited("vgsp: COPY-MISMATCH va=0x%llx off=0x%llx path=ce-soft-copy want=%*phN got=%*phN\n",
+ dst_va + o, o, (int)n, bounce, (int)n, rb);
+}
+
+#define SVC_COPY_TRUNC_RET(rc_) \
+ do { \
+ if (total >= SVC_COPY_AUDIT_MIN) \
+ pr_err_ratelimited("vgsp: COPY-TRUNC va=0x%llx want=0x%llx done=0x%llx path=ce-soft-copy rc=%d\n", \
+ dst_orig, (u64)total, (u64)o, (rc_)); \
+ return (rc_); \
+ } while (0)
+
static int svc_copy(struct nv_vgpu_device *vgpu, u64 dst, u64 src, size_t total,
u32 dst_phys, u32 dst_aperture, u32 src_phys, u32 src_aperture)
{
@@ -4931,6 +5027,7 @@ static int svc_copy(struct nv_vgpu_device *vgpu, u64 dst, u64 src, size_t total,
u64 dst_orig = dst, src_orig = src;
u32 dst_prov = 0, src_prov = 0;
bool bigcopy_this = false;
+ bool audit = total >= SVC_COPY_AUDIT_MIN;
static u64 bigcopy_log;
if (!total || total > (256ULL << 20))
@@ -5278,7 +5375,7 @@ static int svc_copy(struct nv_vgpu_device *vgpu, u64 dst, u64 src, size_t total,
if (src_shadow) {
if (svc_vas_read(src_orig + o, bounce, chunk))
- return -EIO;
+ SVC_COPY_TRUNC_RET(-EIO);
} else if (src_virt) {
u64 pa = 0;
u32 aper = 0;
@@ -5292,7 +5389,7 @@ static int svc_copy(struct nv_vgpu_device *vgpu, u64 dst, u64 src, size_t total,
* table knows where this VA's content landed.
*/
if (svc_ledger_lookup(src_orig + o, &lpa, &lap))
- return -ENOENT;
+ SVC_COPY_TRUNC_RET(-ENOENT);
pa = lpa;
aper = (lap == SVC_APERTURE_FB) ?
CE_APERTURE_VID : CE_APERTURE_SYS;
@@ -5320,26 +5417,29 @@ static int svc_copy(struct nv_vgpu_device *vgpu, u64 dst, u64 src, size_t total,
u8 *base = vgpu->bars[1].vaddr;
if (!base || pa + chunk > svc_fb_shadow_size(vgpu))
- return -EINVAL;
+ SVC_COPY_TRUNC_RET(-EINVAL);
memcpy(bounce, base + pa, chunk);
} else if (svc_read(vgpu, pa, bounce, chunk)) {
- return -EIO;
+ SVC_COPY_TRUNC_RET(-EIO);
}
} else if (src_aperture == CE_APERTURE_VID) {
u8 *base = vgpu->bars[1].vaddr;
size_t size = svc_fb_shadow_size(vgpu);
if (!base || src + o + chunk > size)
- return -EINVAL;
+ SVC_COPY_TRUNC_RET(-EINVAL);
memcpy(bounce, base + src + o, chunk);
} else {
if (svc_read(vgpu, src + o, bounce, chunk))
- return -EIO;
+ SVC_COPY_TRUNC_RET(-EIO);
}
if (dst_shadow) {
if (svc_vas_write(dst_orig + o, bounce, chunk))
- return -ENOMEM;
+ SVC_COPY_TRUNC_RET(-ENOMEM);
+ if (audit && (!o || o + chunk == total))
+ svc_copy_audit_chunk(vgpu, dst_orig, o, bounce,
+ chunk, true, 0, 0);
} else if (dst_virt) {
u64 pa = 0;
u32 aper = 0;
@@ -5353,7 +5453,7 @@ static int svc_copy(struct nv_vgpu_device *vgpu, u64 dst, u64 src, size_t total,
* the whole copy (readers already use it).
*/
if (svc_ledger_lookup(dst_orig + o, &lpa, &lap))
- return -ENOENT;
+ SVC_COPY_TRUNC_RET(-ENOENT);
pa = lpa;
aper = (lap == SVC_APERTURE_FB) ?
CE_APERTURE_VID : CE_APERTURE_SYS;
@@ -5382,11 +5482,14 @@ static int svc_copy(struct nv_vgpu_device *vgpu, u64 dst, u64 src, size_t total,
u8 *base = vgpu->bars[1].vaddr;
if (!base || pa + chunk > svc_fb_shadow_size(vgpu))
- return -EINVAL;
+ SVC_COPY_TRUNC_RET(-EINVAL);
memcpy(base + pa, bounce, chunk);
} else if (svc_write(vgpu, pa, bounce, chunk)) {
- return -EIO;
+ SVC_COPY_TRUNC_RET(-EIO);
}
+ if (audit && (!o || o + chunk == total))
+ svc_copy_audit_chunk(vgpu, dst_orig, o, bounce,
+ chunk, false, pa, aper);
/*
* m171/m180: a large device-dest write records its
* landing page - SYS and VID alike, because the
@@ -5406,11 +5509,19 @@ static int svc_copy(struct nv_vgpu_device *vgpu, u64 dst, u64 src, size_t total,
size_t size = svc_fb_shadow_size(vgpu);
if (!base || dst + o + chunk > size)
- return -EINVAL;
+ SVC_COPY_TRUNC_RET(-EINVAL);
memcpy(base + dst + o, bounce, chunk);
+ if (audit && (!o || o + chunk == total))
+ svc_copy_audit_chunk(vgpu, dst_orig, o, bounce,
+ chunk, false, dst + o,
+ CE_APERTURE_VID);
} else {
if (svc_write(vgpu, dst + o, bounce, chunk))
- return -EIO;
+ SVC_COPY_TRUNC_RET(-EIO);
+ if (audit && (!o || o + chunk == total))
+ svc_copy_audit_chunk(vgpu, dst_orig, o, bounce,
+ chunk, false, dst + o,
+ CE_APERTURE_SYS);
}
(void)i;
o += chunk;
@@ -5472,6 +5583,11 @@ static int svc_copy(struct nv_vgpu_device *vgpu, u64 dst, u64 src, size_t total,
pr_info("sema-copy-dst: after dst=0x%llx dst16=%*phN\n",
dst, n, da);
}
+ if (audit && svc_copy_audit_n < SVC_COPY_AUDIT_MAX) {
+ svc_copy_audit_n++;
+ pr_info("vgsp: COPYAUDIT path=ce-soft-copy va=0x%llx len=0x%llx done=0x%llx\n",
+ dst_orig, (u64)total, (u64)o);
+ }
return 0;
}
@@ -5561,11 +5677,42 @@ write_landing:
for (o = 0; o < bytes; o += 4096) {
u32 chunk = bytes - o > 4096 ? 4096 : bytes - o;
- if (svc_write(vgpu, pa + o, (void *)(data + o), chunk))
+ if (svc_write(vgpu, pa + o, (void *)(data + o), chunk)) {
+ if (bytes >= SVC_COPY_AUDIT_MIN)
+ pr_err_ratelimited("vgsp: COPY-TRUNC va=0x%llx want=0x%llx done=0x%llx path=grce-inline rc=%d\n",
+ dst_va, (u64)bytes,
+ (u64)o, -EIO);
return -EIO;
+ }
}
}
/*
+ * COPYAUDIT (see the svc_copy comment): log the big inline write and
+ * read the first 16 destination bytes back through the aperture it
+ * was written to, comparing against the source bytes.
+ */
+ if (bytes >= SVC_COPY_AUDIT_MIN && svc_copy_audit_n < SVC_COPY_AUDIT_MAX) {
+ u8 rb[16] = {0};
+ bool ok = false;
+
+ if (ap == SVC_APERTURE_FB) {
+ u8 *base = vgpu->bars[1].vaddr;
+
+ if (base && pa + sizeof(rb) <= svc_fb_shadow_size(vgpu)) {
+ memcpy(rb, base + pa, sizeof(rb));
+ ok = true;
+ }
+ } else if (!svc_read(vgpu, pa, rb, sizeof(rb))) {
+ ok = true;
+ }
+ svc_copy_audit_n++;
+ pr_info("vgsp: COPYAUDIT path=grce-inline va=0x%llx len=0x%llx done=0x%llx\n",
+ dst_va, (u64)bytes, (u64)bytes);
+ if (ok && memcmp(rb, data, sizeof(rb)))
+ pr_info_ratelimited("vgsp: COPY-MISMATCH va=0x%llx off=0x0 path=grce-inline want=%*phN got=%*phN\n",
+ dst_va, 16, data, 16, rb);
+ }
+ /*
* m180: record the landing so the fill/D2H readers of this VA follow
* the bytes the engine's inline path wrote, not their own resolution.
*/
@@ -14878,6 +15025,27 @@ static void svc_gr_data_fill(struct nv_vgpu_device *vgpu,
if (p == 0 &&
svc_fwd_req.gr_n_data == before)
first_unresolved = true;
+ /*
+ * COPYAUDIT (2026-10-18): a
+ * span cut after at least one
+ * authored page is the
+ * completeness hazard - the
+ * rewritten base pointer is
+ * still handed to the engine,
+ * whose base+offset reads then
+ * run past the authored pages
+ * into later operands' bounce
+ * pages. Make the truncation
+ * loud (want is a lower bound:
+ * the guest asked for >= p+1
+ * pages; done is the authored
+ * page count before the cut).
+ */
+ if (p > 0)
+ pr_info_ratelimited("vgsp: COPY-TRUNC va=0x%llx want>=0x%llx done=0x%llx path=gr-fill-span\n",
+ wins[w],
+ ((u64)p + 1) << 12,
+ (u64)p << 12);
if (dropped_pages < 4) {
drop_va[dropped_pages] = va;
drop_kind[dropped_pages] =