summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-18 07:15:31 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-18 07:16:09 -0400
commit24c806ffe9dbe1b0a13fc0a8e3dab91a1e8bc448 (patch)
tree883a6dcea712040f75e24fab68eddde54f44b89f
parentfdef24a4999f9699a1143d0d73eab226b36c0035 (diff)
downloadopen-iov-24c806ffe9dbe1b0a13fc0a8e3dab91a1e8bc448.tar.gz
open-iov-24c806ffe9dbe1b0a13fc0a8e3dab91a1e8bc448.zip
vgsp: GR relocation completeness - resolve unmatched device-VA words via the landing resolver (RELOC-FIXED), param gsp_phys_gr_reloc_resolve default 1
-rw-r--r--kernel/gsp_phys_booter.c356
-rw-r--r--kernel/gsp_sema_service.c78
-rw-r--r--kernel/gsp_sema_service.h26
3 files changed, 455 insertions, 5 deletions
diff --git a/kernel/gsp_phys_booter.c b/kernel/gsp_phys_booter.c
index b3e095a..60b6ea4 100644
--- a/kernel/gsp_phys_booter.c
+++ b/kernel/gsp_phys_booter.c
@@ -93,6 +93,29 @@ module_param_named(gsp_phys_gr_strict_reloc, gsp_phys_gr_strict_reloc_param,
MODULE_PARM_DESC(gsp_phys_gr_strict_reloc,
"Hold a GR launch fail-closed when the QMD rewrite finds device-VA-shaped words that match no tracked data page (default 0: RELOC-GAP log only; 1: hold the launch)");
+/*
+ * relfix (agent-A FIX A, default 1): the GR QMD rewrite's device-VA-shaped
+ * words that match NO tracked data page (the RELOC-GAP class - the raw guest
+ * pointers the real engine then faults on; r216: MMU FAULT_PDE at a gap VA,
+ * SM warp exceptions) are re-resolved through the sema service's trusted
+ * resolver (gsp_sema_gr_reloc_resolve). A word that resolves is authored
+ * into the bounce window exactly like a late-tracked data page - it and the
+ * contiguous span the resolver also answers are copied in before the
+ * doorbell and copied back after the retire wait, the same m142 ordering as
+ * every tracked page - and the word is rewritten to the page's reloc VA plus
+ * its in-page offset, the exact encoding the matched-word relocation uses.
+ * Unresolved words keep the historical RELOC-GAP behaviour (counted and left
+ * as guest VAs; never guessed). 0 = exact pre-relfix behaviour for one-flip
+ * A/B. The rewrite runs on the single-inflight GR work item with
+ * gsp_phys_rm_lock held; the resolver is read-only w.r.t. the service tables
+ * but may sleep in the same vfio walks the fill's resolver uses.
+ */
+static unsigned int gsp_phys_gr_reloc_resolve_param = 1;
+module_param_named(gsp_phys_gr_reloc_resolve, gsp_phys_gr_reloc_resolve_param,
+ uint, 0644);
+MODULE_PARM_DESC(gsp_phys_gr_reloc_resolve,
+ "Resolve QMD words that match no tracked data page and author them as late data pages (default 1: RELOC-FIXED; 0: RELOC-GAP only)");
+
bool gsp_phys_boot_enabled(void)
{
return gsp_phys_boot_param;
@@ -16770,9 +16793,18 @@ static void gsp_phys_rmcall_gr_gpc_mmu_init(void __iomem *bar0)
* the affine-relocated server-managed VA (the engine cannot walk the
* client-managed guest VAs: UPDATE_PDE_2 refuses them with 0x1f).
*/
+/*
+ * relfix (agent-A FIX A): resolve-and-author one unmatched device-VA word.
+ * Defined after gsp_phys_gr_data_bi (the bounce-index helpers it needs);
+ * returns true when the word was rewritten to a reloc VA (and the pages
+ * queued for copy-in/out), false to leave it to the RELOC-GAP accounting.
+ */
+static bool gsp_phys_gr_reloc_fix_word(const struct gsp_phys_gr_submit *req,
+ u32 *w, u32 k, u32 *mirror);
static u32 gsp_phys_gr_rewrite_data_vars(u32 *w, u32 n_words,
const struct gsp_phys_gr_submit *req,
- u32 bank, u32 *mirror, u64 *gap_first)
+ u32 bank, u32 *mirror, u64 *gap_first,
+ bool fix_enabled)
{
u32 rew = 0, skipped = 0, gap = 0, k;
@@ -16863,10 +16895,16 @@ static u32 gsp_phys_gr_rewrite_data_vars(u32 *w, u32 n_words,
* without matching req->data[x].va - the word passes the
* exact shape heuristic the match test above uses, so it
* will be left as an unrelocated guest VA in this page.
- * Count it; the caller decides (RELOC-GAP log with the
- * parameter off, fail-closed hold with it on).
+ * relfix: first give the sema service's trusted resolver a
+ * chance to translate it; a word that resolves is authored
+ * as a late data page and rewritten to its reloc VA (the
+ * exact matched-word encoding). Only an unresolved word is
+ * counted as a gap for the caller's RELOC-GAP log/hold.
*/
if (x == req->n_data) {
+ if (fix_enabled && gsp_phys_gr_reloc_resolve_param &&
+ gsp_phys_gr_reloc_fix_word(req, w, k, mirror))
+ continue;
gap++;
if (gap_first && !*gap_first)
*gap_first = v;
@@ -17062,6 +17100,211 @@ static u32 gsp_phys_gr_data_bi(u64 reloc_va)
}
/*
+ * relfix: late-resolved data pages. A QMD word that matches no tracked data
+ * page yet resolves at rewrite time becomes a real operand page of this
+ * launch: it gets the next bounce-region index after the fill's tracked set,
+ * is copied in before the doorbell and copied back after the retire wait
+ * (the same m142 ordering as every tracked page), and the word is rewritten
+ * to its reloc VA - so the engine stops walking an untranslatable guest VA
+ * (r216 MMU FAULT_PDE class). Bounds are per-launch and fail-open: at the
+ * caps, or when no source answers, the word simply stays a RELOC-GAP (the
+ * pre-relfix behaviour). Static scratch: the single-inflight ordered
+ * gsp-phys-work item is the only caller, reset per submission exactly like
+ * bounce_csum_in.
+ */
+#define GSP_PHYS_GR_GAP_MAX 4096u /* total late pages per launch */
+#define GSP_PHYS_GR_GAP_SPAN_MAX 2048u /* contiguous pages per word */
+#define GSP_PHYS_GR_GAP_WORD_MAX 32u /* distinct fixed words per launch */
+struct gsp_phys_gr_gap_page {
+ u64 va; /* device VA of the page (unique-proxy key) */
+ u64 hpa; /* guest page copied in (results copied back to) */
+ u64 reloc_va; /* engine-visible bounce-region VA of that page */
+ u32 bi; /* bounce page index (n_data + late index) */
+};
+static struct gsp_phys_gr_gap_page
+ gsp_phys_gr_gap[GSP_PHYS_GR_GAP_MAX];
+static u32 gsp_phys_gr_gap_n;
+struct gsp_phys_gr_gap_word {
+ u64 va; /* device VA of the fixed word (page base) */
+ u64 reloc_va; /* reloc VA of its first page */
+ u64 pa; /* resolved guest PA (logging) */
+};
+static struct gsp_phys_gr_gap_word
+ gsp_phys_gr_gap_word[GSP_PHYS_GR_GAP_WORD_MAX];
+static u32 gsp_phys_gr_gap_word_n;
+static u64 gsp_phys_gr_reloc_fixed; /* words rewritten this launch */
+static u64 gsp_phys_gr_reloc_fixed_first; /* first fixed device VA */
+
+/*
+ * relfix: reloc VA of the i-th bounce-region index, region mode exactly as
+ * gsp_phys_gr_data_bi; the legacy window's small bound fails open.
+ */
+static bool gsp_phys_gr_data_reloc_va(u32 idx, u64 *va_out)
+{
+ if (gsp_phys_gr_data_region_value()) {
+ if (idx >= GSP_PHYS_RMCALL_GR_DATA_BOUNCE_PAGES)
+ return false;
+ *va_out = GSP_PHYS_RMCALL_GR_DATA_BOUNCE_VA0 +
+ ((u64)idx << 12);
+ return true;
+ }
+ if (idx >= GSP_PHYS_RMCALL_GR_MAIL_SLOT)
+ return false;
+ *va_out = GSP_PHYS_RMCALL_GR_BOUNCE_VA0 +
+ ((u64)idx << 12);
+ return true;
+}
+
+/*
+ * relfix: append one late page at the next free bounce index (n_data +
+ * gap_n, the slot right after the fill's tracked set). Fail-open on the
+ * caps or an absent bounce page.
+ */
+static bool gsp_phys_gr_gap_add(u32 n_data, u64 va, u64 hpa, u64 *reloc_out)
+{
+ u32 gi = gsp_phys_gr_gap_n;
+ u32 idx = n_data + gi;
+ struct gsp_phys_boot_chunk *dchunk = gsp_phys_gr_data_chunk();
+ u64 reloc_va;
+
+ if (gi >= GSP_PHYS_GR_GAP_MAX)
+ return false;
+ if (!gsp_phys_gr_data_reloc_va(idx, &reloc_va))
+ return false;
+ if (!dchunk->pages || !gsp_phys_chunk_page_va(dchunk, idx))
+ return false;
+ gsp_phys_gr_gap[gi].va = va;
+ gsp_phys_gr_gap[gi].hpa = hpa;
+ gsp_phys_gr_gap[gi].reloc_va = reloc_va;
+ gsp_phys_gr_gap[gi].bi = idx;
+ gsp_phys_gr_gap_n = gi + 1u;
+ *reloc_out = reloc_va;
+ return true;
+}
+
+/*
+ * relfix: is this device page already covered by a proxy this launch?
+ * Either the fill's tracked set (req->data, each page exactly once - the
+ * fill dedupes) or a late page queued by this fix. A second proxy for one
+ * guest page is unsound: the engine may author results through either
+ * slot, and the two copy-outs would race to write back to the same guest
+ * page, so one would clobber the other's results. The fix therefore never
+ * creates a second proxy (see the span rollback in
+ * gsp_phys_gr_reloc_fix_word).
+ */
+static bool gsp_phys_gr_page_tracked(const struct gsp_phys_gr_submit *req,
+ u64 va)
+{
+ u32 i;
+
+ for (i = 0; i < req->n_data; i++)
+ if (req->data[i].va == va)
+ return true;
+ for (i = 0; i < gsp_phys_gr_gap_n; i++)
+ if (gsp_phys_gr_gap[i].va == va)
+ return true;
+ return false;
+}
+
+/*
+ * relfix: resolve and author one unmatched word (see gsp_phys_gr_gap_page).
+ * The base VA resolves through the sema service's trusted resolver; on a hit
+ * the page is queued and the word rewritten to <reloc VA> | offset - the
+ * exact encoding the matched-word relocation uses - and the contiguous span
+ * the resolver also answers (bounded) is queued behind it so a multi-page
+ * operand's base+offset reads stay contiguous, exactly like a fill-tracked
+ * span. A repeated word (same base VA authored in another QMD page) reuses
+ * its recorded reloc VA. NEVER guesses: only a successful resolve rewrites,
+ * and a page that already has a proxy (the fill's or an earlier fix's) stops
+ * the fix - the queued pages of THIS word are rolled back and the word stays
+ * a RELOC-GAP - so one guest page can never gain a second copy-out writer
+ * (the clobber class gsp_phys_gr_page_tracked describes).
+ */
+static bool gsp_phys_gr_reloc_fix_word(const struct gsp_phys_gr_submit *req,
+ u32 *w, u32 k, u32 *mirror)
+{
+ u64 v = (u64)w[k] | ((u64)w[k + 1] << 32);
+ u64 wb = v & ~(u64)(GSP_PHYS_PAGE_SIZE - 1);
+ struct gsp_sema_gr_reloc r;
+ struct gsp_phys_gr_gap_word *gw = NULL;
+ u32 gap_start = gsp_phys_gr_gap_n;
+ u64 reloc_va = 0;
+ u32 wi, i;
+
+ for (wi = 0; wi < gsp_phys_gr_gap_word_n; wi++) {
+ if (gsp_phys_gr_gap_word[wi].va == wb) {
+ gw = &gsp_phys_gr_gap_word[wi];
+ break;
+ }
+ }
+ if (!gw) {
+ bool overlap = false;
+ u64 base_pa;
+
+ if (gsp_phys_gr_gap_word_n >= GSP_PHYS_GR_GAP_WORD_MAX)
+ return false;
+ if (gsp_phys_gr_page_tracked(req, wb))
+ return false;
+ if (gsp_sema_gr_reloc_resolve(wb, &r))
+ return false;
+ base_pa = r.pa;
+ if (!gsp_phys_gr_gap_add(req->n_data, wb, r.hpa, &reloc_va))
+ return 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))
+ break; /* cap: the tail stays as authored */
+ }
+ if (overlap) {
+ /*
+ * Roll the whole word back (base included) and
+ * leave it a RELOC-GAP: a partial run would make
+ * the engine's next base+offset read land on a
+ * later word's page.
+ */
+ gsp_phys_gr_gap_n = gap_start;
+ return false;
+ }
+ gw = &gsp_phys_gr_gap_word[gsp_phys_gr_gap_word_n];
+ gw->va = wb;
+ gw->reloc_va = gsp_phys_gr_gap[gap_start].reloc_va;
+ gw->pa = base_pa;
+ gsp_phys_gr_gap_word_n++;
+ }
+
+ v = gw->reloc_va | (v & (u64)(GSP_PHYS_PAGE_SIZE - 1));
+ w[k] = (u32)v;
+ w[k + 1] = (u32)(v >> 32);
+ if (mirror) {
+ mirror[k] = (u32)v;
+ mirror[k + 1] = (u32)(v >> 32);
+ }
+ gsp_phys_gr_reloc_fixed++;
+ if (!gsp_phys_gr_reloc_fixed_first)
+ gsp_phys_gr_reloc_fixed_first = wb;
+ pr_info_ratelimited("vgsp-gr: RELOC-FIXED va=0x%llx pa=0x%llx off=%u\n",
+ wb, gw->pa, (u32)(v & (u64)(GSP_PHYS_PAGE_SIZE - 1)));
+ return true;
+}
+
+/*
* m151: output retirement wait. qmd_rel is a schedule/fetch ack (the front
* end latched and fetched the QMD), not a data-path retirement: the SM's
* stores into the bounce pages can still be in flight when the ack lands.
@@ -17115,6 +17358,26 @@ gsp_phys_gr_scan_pages(const struct gsp_phys_gr_submit *req,
if (!dup)
checked[n_all++] = bi;
}
+ /*
+ * relfix: late-resolved pages are tracked exactly like fill-tracked
+ * data pages, so the retire wait, the late-check and the NOWRITE
+ * classification see stores the engine authors into them.
+ */
+ for (di = 0; di < gsp_phys_gr_gap_n; di++) {
+ u32 bi = gsp_phys_gr_gap[di].bi;
+ u32 k;
+ bool dup = false;
+
+ if (bi >= GSP_PHYS_RMCALL_GR_DATA_BOUNCE_PAGES)
+ continue;
+ for (k = 0; k < n_all; k++)
+ if (checked[k] == bi) {
+ dup = true;
+ break;
+ }
+ if (!dup)
+ checked[n_all++] = bi;
+ }
if (n_tracked)
*n_tracked = n_all;
for (j = 0; j < n_all; j++) {
@@ -17244,6 +17507,33 @@ static void gsp_phys_gr_fill_bounce(const struct gsp_phys_gr_submit *req,
}
/*
+ * relfix: copy the late-resolved pages' guest content into their bounce
+ * pages and capture the copy-in checksum baseline, exactly as
+ * 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.
+ */
+static void gsp_phys_gr_fill_gap_bounce(u32 *csum_in)
+{
+ struct gsp_phys_boot_chunk *dchunk = gsp_phys_gr_data_chunk();
+ u32 gi;
+
+ for (gi = 0; gi < gsp_phys_gr_gap_n; gi++) {
+ const u8 *src = (const u8 *)__va(gsp_phys_gr_gap[gi].hpa);
+ u8 *dst = gsp_phys_chunk_page_va(dchunk, gsp_phys_gr_gap[gi].bi);
+ u32 ci, csum = 0;
+
+ if (!src || !dst)
+ continue;
+ memcpy(dst, src, GSP_PHYS_PAGE_SIZE);
+ 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;
+ }
+ dma_wmb();
+}
+
+/*
* F3 retry hygiene: an outcome retry re-publishes the same QMD. If the QMD's
* pointer pairs name the win0 shadow bank the previous attempt used, move the
* already-prepared param/program/bank pages into the alternate bank and
@@ -17467,6 +17757,15 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
reloc_gap = 0;
reloc_gap_first = 0;
/*
+ * relfix: the late-resolved page list is per-launch scratch exactly
+ * like bounce_csum_in; the single-inflight work item is the only
+ * caller.
+ */
+ gsp_phys_gr_gap_n = 0;
+ gsp_phys_gr_gap_word_n = 0;
+ gsp_phys_gr_reloc_fixed = 0;
+ gsp_phys_gr_reloc_fixed_first = 0;
+ /*
* The fault record is static scratch (frame bound): reset it per
* submission so a previous launch's fault can never leak into this
* one's result - the old on-stack `= { }` semantics.
@@ -17794,7 +18093,7 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
(u32 *)sh_page,
GSP_PHYS_PAGE_SIZE / 4, req,
(pg->qmd_word - 32) / 2,
- (u32 *)__va(pg->hpa), NULL);
+ (u32 *)__va(pg->hpa), NULL, false);
if (pg->hpa && pg->qmd_word == 40u &&
gsp_phys_rmcall_gr_mail_patch_value()) {
u32 *w = (u32 *)sh_page;
@@ -17921,7 +18220,7 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
(u32 *)sh_page,
GSP_PHYS_PAGE_SIZE / 4, req,
(pg->qmd_word - 32) / 2,
- (u32 *)__va(pg->hpa), &gf);
+ (u32 *)__va(pg->hpa), &gf, true);
if (gg) {
reloc_gap += gg;
if (!reloc_gap_first)
@@ -18367,6 +18666,20 @@ gr_page_unmapped:
req->data[di].reloc_va);
}
+ /*
+ * relfix: the late-resolved pages copy in exactly like the tracked
+ * data pages (same m142 pre-doorbell ordering). The word rewrites
+ * already point the engine at these bounce pages; filling them here
+ * is what makes those pointers real. Still under gsp_phys_rm_lock,
+ * 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);
+ 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);
+ }
+
ret = gsp_phys_rmcall_publish_locked(bar0, boot_stage.rmcall_va_pb,
words, &sub);
mutex_unlock(&gsp_phys_rm_lock);
@@ -19242,6 +19555,7 @@ gr_page_unmapped:
nw_attempts);
}
gsp_phys_gr_fill_bounce(req, bounce_csum_in);
+ gsp_phys_gr_fill_gap_bounce(bounce_csum_in);
mutex_lock(&gsp_phys_rm_lock);
ret = gsp_phys_rmcall_publish_locked(
bar0, boot_stage.rmcall_va_pb,
@@ -19556,6 +19870,38 @@ gr_page_unmapped:
wrote, changed, chg[0], chg[1], chg[2], chg[3], chg[4],
chg[5], chg[6], chg[7]);
}
+ /*
+ * relfix: copy the late-resolved pages back to the guest pages the
+ * resolver named - the same identity the word rewrite handed the
+ * engine - after the retire wait, exactly like the tracked data
+ * pages above.
+ */
+ if (gsp_phys_gr_gap_n && gsp_phys_gr_data_chunk()->pages) {
+ struct gsp_phys_boot_chunk *dchunk = gsp_phys_gr_data_chunk();
+ u32 gi, gwrote = 0, gchanged = 0;
+ u32 gchg[4] = { };
+
+ for (gi = 0; gi < gsp_phys_gr_gap_n; gi++) {
+ u32 bi = gsp_phys_gr_gap[gi].bi;
+ const u8 *src = gsp_phys_chunk_page_va(dchunk, bi);
+ u32 ci, csum = 0;
+
+ if (!src)
+ continue;
+ for (ci = 0; ci < GSP_PHYS_PAGE_SIZE; ci += 4)
+ csum += get_unaligned_le32(src + ci);
+ if (csum != bounce_csum_in[bi]) {
+ if (gchanged < 4)
+ gchg[gchanged] = bi;
+ gchanged++;
+ }
+ memcpy((u8 *)__va(gsp_phys_gr_gap[gi].hpa), src,
+ GSP_PHYS_PAGE_SIZE);
+ 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]);
+ }
poll = w1;
if (bar0)
pr_info("vgsp-gr: SKED readback (ok): HWW_ESR=0x%08x INFO2=0x%08x CWD_T0=0x%08x CWD_T1=0x%08x CWD_T2=0x%08x\n",
diff --git a/kernel/gsp_sema_service.c b/kernel/gsp_sema_service.c
index a045f07..24ec36f 100644
--- a/kernel/gsp_sema_service.c
+++ b/kernel/gsp_sema_service.c
@@ -14311,6 +14311,84 @@ static const char *svc_gr_src_probe(struct nv_vgpu_device *vgpu, u64 va,
}
/*
+ * relfix (agent-A FIX A): rewrite-time GR relocation resolver (see
+ * gsp_sema_service.h). Mirrors the per-page source order of the fill
+ * (svc_gr_data_fill: svc_ipin_lookup -> svc_resolve_va's trusted chain ->
+ * ledger) and of the read-only attribution probe svc_gr_src_probe above,
+ * then applies the m180 ledger in the fill's two roles: fallback when
+ * nothing above answered (the C1 landing path), and the m171 landing
+ * preference for VAs at/above the ledger floor when the ledger disagrees
+ * with the resolver (the skip-ahead class where a walk result is stale but
+ * the landing is authoritative).
+ *
+ * Deliberately excluded versus svc_resolve_va: pin_store/lat_store writes,
+ * svc_pd_discover and the sliced targeted RAM scan - those mutate
+ * service-thread state or can block for seconds, and this runs on the GR
+ * work item (process context; the page-table walks take svc.vas_lock only
+ * for their publication snapshot and may sleep in vfio_dma_rw, exactly the
+ * discipline the walk comment above svc_walk_vaspaces mandates).
+ *
+ * Returns 0 with out filled on a successful translation, -ENOENT on a miss
+ * (caller must leave the word untouched), or a conversion errno.
+ */
+int gsp_sema_gr_reloc_resolve(u64 va, struct gsp_sema_gr_reloc *out)
+{
+ struct nv_vgpu_device *vgpu = svc_fwd_req.vgpu;
+ u64 page = va & ~(u64)(SEMA_PAGE_SIZE - 1);
+ u64 pa = 0, lpa = 0, hpa = 0;
+ int ap = SVC_APERTURE_SYS;
+ u32 lap = 0, off = 0;
+ bool resolved = false;
+ int ret;
+
+ if (!vgpu || !out)
+ return -EINVAL;
+
+ if (!svc_ipin_lookup(page, &pa, &ap))
+ resolved = true;
+ else if (!svc_pin_lookup(page, &pa, &ap))
+ resolved = true;
+ else if (!svc_lat_lookup(page, &pa, &ap))
+ resolved = true;
+ else if (!svc_map_lookup(page, &pa))
+ resolved = true;
+ else if (!svc_walk_vaspaces(vgpu, page, &pa, &ap))
+ resolved = true;
+ else if (!svc_walk_discovered(vgpu, page, &pa, &ap))
+ resolved = true;
+
+ if (!resolved) {
+ /* C1 fallback: a landing is a page the client already backed
+ * with content even when every resolver missed (r174). */
+ if (svc_ledger_lookup(page, &lpa, &lap))
+ return -ENOENT;
+ pa = lpa;
+ ap = (int)lap;
+ } else if (page >= SVC_VAS_FLOOR) {
+ /* m171: the landing table wins over a disagreeing resolver. */
+ if (!svc_ledger_lookup(page, &lpa, &lap) &&
+ ((lpa & ~(u64)(SEMA_PAGE_SIZE - 1)) !=
+ (pa & ~(u64)(SEMA_PAGE_SIZE - 1)) ||
+ (u32)lap != svc_ap_of(ap))) {
+ pa = lpa;
+ ap = (int)lap;
+ }
+ }
+
+ ret = svc_fwd_operand_hpa(vgpu,
+ svc_ap_of(ap) == SVC_APERTURE_FB ?
+ SVC_APERTURE_FB : SVC_APERTURE_SYS,
+ pa, 0, SEMA_PAGE_SIZE, &hpa, &off);
+ if (ret || off)
+ return ret ? ret : -EFAULT;
+
+ out->pa = pa & ~(u64)(SEMA_PAGE_SIZE - 1);
+ out->ap = svc_ap_of(ap);
+ out->hpa = hpa;
+ return 0;
+}
+
+/*
* m144 (second-client launch-state divergence): collect device-VA-shaped
* candidate pointers from the bytes the ENGINE will actually execute.
*
diff --git a/kernel/gsp_sema_service.h b/kernel/gsp_sema_service.h
index 26249fd..394ce9d 100644
--- a/kernel/gsp_sema_service.h
+++ b/kernel/gsp_sema_service.h
@@ -314,4 +314,30 @@ extern bool gsp_sema_copy_dst_pin_guard;
*/
extern bool gsp_sema_gr_swrel_gate;
+/*
+ * relfix (agent-A FIX A): rewrite-time GR relocation resolver. Resolves one
+ * device VA through the same trusted source chain the GR fill's per-page
+ * resolution uses (ipin -> self pin -> learned cache -> discovered map ->
+ * published vaspaces -> discovered directory), then the m180 ledger in the
+ * fill's two roles (fallback when nothing else answered; m171 landing
+ * preference above the ledger floor when it disagrees). The result is
+ * converted to a host page the caller can copy/map. Deliberately read-only
+ * w.r.t. the resolver tables - no pin or cache stores, no directory
+ * discovery, no targeted RAM scan: the caller is the GR work item, not the
+ * service thread, and must not write service state or block on a full-RAM
+ * scan. The page-table walks may sleep in vfio_dma_rw exactly like the
+ * fill's walks; callers run in process context (the GR work item, holding
+ * gsp_phys_rm_lock) with no spinlocks held. A live forwarded GR request
+ * (svc_fwd_req.vgpu) is required. A successful return (0) means the VA is a
+ * translated page of that request's client - the caller may act on it; a
+ * miss returns -ENOENT and the caller must change nothing.
+ */
+struct gsp_sema_gr_reloc {
+ u64 pa; /* resolved guest physical page base */
+ u32 ap; /* aperture: 1 = SYS (guest RAM), 2 = FB (vidmem) */
+ u64 hpa; /* host physical address of that guest page */
+};
+
+int gsp_sema_gr_reloc_resolve(u64 va, struct gsp_sema_gr_reloc *out);
+
#endif /* _GSP_SEMA_SERVICE_H_ */