summaryrefslogtreecommitdiffstats
path: root/kernel
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-18 09:24:05 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-18 09:24:48 -0400
commit9d64e5562be9b6766ea4e7808c0c509bd4252057 (patch)
tree6f196212f753de90e64d2325a28bafe49033f1be /kernel
parentc4ce3eb0c4af80205de7649f49dcfffa62e46ebe (diff)
downloadopen-iov-9d64e5562be9b6766ea4e7808c0c509bd4252057.tar.gz
open-iov-9d64e5562be9b6766ea4e7808c0c509bd4252057.zip
vgsp: refuse PTE-page code sources in the Tier-2 program-pair path (CODESRC-SUSPECT fallback)
r221 evidence: six CODEHASH-suspect launches resolved six different client code VAs to the same page (deterministic guest allocation within one boot) and all carried valid kernel prologue words at the QMD entry offset; the SUSPECT tag was the zero-window false-positive mode of the CODEHASH content test, and no source page observed was PTE-shaped. The class remains real: the program pair (QMD w48/49) is copied verbatim into the page the engine I-fetches from, so a stale landing, walk or pin answering with an empty, PTE-shaped or module page-tree page would execute PTE/filler bytes as SASS. CODESRC, before the Tier-2 program-pair copy (additive, once per launch): - detect (a) empty source (entry+sampled windows zero), (b) PTE-shaped entry run (>=12/16 identically-flagged valid entries, or mostly-zero with only PTE-signed entries), (c) source HPA on one of the engine page-tree's own module chunks (main PT, guest PT pool, shadow pools, qmdwin); - on detection re-resolve through the late resolver (guest-walk order, the alternative to the fill's ledger-first order) and adopt it only if different and not itself suspicious; otherwise withdraw the page from the submission (no copy, no mapping, QMD pair kept at the guest VA - the per-pair ptr_guest behaviour). One rate-limited 'vgsp-gr: CODESRC-SUSPECT va= pa= alt= used= why=' line. Greens-safety: no observed launch (green or suspect) matches any trigger: entry words are non-zero and distinct, entry runs are not PTE-signed, and a guest HPA cannot equal a module chunk DMA; with no match the flow is byte-identical to before (same memcpy, copied++ and dump gate).
Diffstat (limited to 'kernel')
-rw-r--r--kernel/gsp_phys_booter.c182
1 files changed, 180 insertions, 2 deletions
diff --git a/kernel/gsp_phys_booter.c b/kernel/gsp_phys_booter.c
index daacf59..09af326 100644
--- a/kernel/gsp_phys_booter.c
+++ b/kernel/gsp_phys_booter.c
@@ -17973,6 +17973,159 @@ gsp_phys_gr_codehash(const struct gsp_phys_gr_submit *req,
suspect ? "SUSPECT" : "ok");
}
+/*
+ * CODESRC (2026-10-18, r221 follow-up): conservative refusal of a
+ * non-plausible Tier-2 program-pair source. The program page (QMD w48/49)
+ * is copied verbatim into the shadow the engine fetches from (win0 or the
+ * Tier-2 line); a source that is empty, PTE-shaped or one of the engine
+ * page-tree's own module pages would execute PTE/filler bytes as SASS.
+ * Detection is deliberately narrow so it is statically inert on every
+ * observed launch (greens included):
+ * (a) empty: the entry 16 B AND all three sampled windows are zero
+ * (a page that never landed anywhere the engine starts);
+ * (b) PTE page: >=12 of the 16 8-byte entries at the entry offset are
+ * identically-flagged valid PTEs (dense table page), or the run is
+ * >=12 zero entries with only PTE-signed entries (sparse table);
+ * (c) module page: the source HPA is a page of one of our own page-tree
+ * chunks (main PT, guest PT pool, Tier-2 shadow pools, qmdwin) - a
+ * guest page can never legitimately resolve there.
+ * A refused source is re-resolved through the late resolver
+ * (gsp_sema_gr_reloc_resolve: pins/map/walks before the ledger, the
+ * guest-walk-first counterpart of the fill's ledger-first order). Only a
+ * different, plausibly-code page is adopted (mode ALT; the engine-visible
+ * copy is taken from it); anything else leaves the QMD pair at the guest
+ * VA (mode GUEST_VA, the ptr_guest behaviour for this pair). One
+ * rate-limited CODESRC-SUSPECT line per detection.
+ */
+#define GSP_PHYS_GR_CODESRC_OK 0
+#define GSP_PHYS_GR_CODESRC_ALT 1
+#define GSP_PHYS_GR_CODESRC_GUEST_VA 2
+
+static bool
+gsp_phys_gr_codesrc_chunk_has(const struct gsp_phys_boot_chunk *c, u64 hpa)
+{
+ u32 i, n;
+
+ if (!c || !c->pages)
+ return false;
+ n = c->pages < 32u ? c->pages : 32u;
+ for (i = 0; i < n; i++) {
+ u64 d = (u64)gsp_phys_chunk_page_dma(c, i);
+
+ if (d && (d & ~(u64)(GSP_PHYS_PAGE_SIZE - 1)) == hpa)
+ return true;
+ }
+ return false;
+}
+
+static bool gsp_phys_gr_codesrc_modpt(u64 hpa)
+{
+ u32 i;
+
+ if (gsp_phys_gr_codesrc_chunk_has(
+ &boot_stage.rmcall_chunks[GSP_PHYS_RMCALL_BUF_PT], hpa))
+ return true;
+ for (i = 0; i < GSP_PHYS_RMCALL_GR_PT_PAGES; i++)
+ if (gsp_phys_gr_codesrc_chunk_has(&boot_stage.rmcall_gr_pt[i],
+ hpa))
+ return true;
+ return gsp_phys_gr_codesrc_chunk_has(&boot_stage.rmcall_gr_shadow,
+ hpa) ||
+ gsp_phys_gr_codesrc_chunk_has(
+ &boot_stage.rmcall_gr_shadow_win0, hpa) ||
+ gsp_phys_gr_codesrc_chunk_has(&boot_stage.rmcall_gr_qmdwin, hpa);
+}
+
+/* Entry sampling window: 16 8-byte entries starting at the entry offset. */
+static bool gsp_phys_gr_codesrc_ptelike(const u8 *p, u32 off)
+{
+ u32 start = off & ~7u, i, hits = 0, zeros = 0, nz = 0, flag = 0;
+ bool flag_set = false, sig_ok = true;
+
+ if (start + 128u > GSP_PHYS_PAGE_SIZE)
+ start = GSP_PHYS_PAGE_SIZE - 128u;
+ for (i = 0; i < 16u; i++) {
+ u64 v = get_unaligned_le64(p + start + i * 8);
+ u32 lo = (u32)(v & 0xffu);
+ /* VALID, aperture not PEER(1): the PTE shape family. */
+ bool pte = (v & 1u) && (((v >> 1) & 0x3u) != 1u);
+
+ if (!v) {
+ zeros++;
+ continue;
+ }
+ nz++;
+ if (!pte) {
+ sig_ok = false;
+ continue;
+ }
+ if (!flag_set) {
+ flag = lo;
+ flag_set = true;
+ }
+ if (lo == flag)
+ hits++;
+ }
+ if (hits >= 12u) /* dense table page: a uniform valid-PTE run */
+ return true;
+ /* sparse table page: mostly zero, only PTE-signed entries */
+ return sig_ok && nz >= 1u && nz <= 4u && zeros >= 12u;
+}
+
+static bool gsp_phys_gr_codesrc_empty(const u8 *p, u32 off)
+{
+ const u8 *e = p + off;
+
+ return !(get_unaligned_le64(e) | get_unaligned_le64(e + 8)) &&
+ !(get_unaligned_le64(p) | get_unaligned_le64(p + 8) |
+ get_unaligned_le64(p + 0x800) | get_unaligned_le64(p + 0x808) |
+ get_unaligned_le64(p + 0xff0) | get_unaligned_le64(p + 0xff8));
+}
+
+static noinline int
+gsp_phys_gr_code_src(const struct gsp_phys_gr_submit *req,
+ const struct gsp_phys_gr_page *pg, u64 *alt_hpa)
+{
+ u64 page = pg->guest_va & ~(u64)(GSP_PHYS_PAGE_SIZE - 1);
+ u32 off = (u32)(req->qmd[48] & (GSP_PHYS_PAGE_SIZE - 1));
+ const u8 *p = (const u8 *)__va(pg->hpa);
+ const char *why = NULL, *used;
+ struct gsp_sema_gr_reloc r;
+ int mode = GSP_PHYS_GR_CODESRC_GUEST_VA;
+
+ *alt_hpa = 0;
+ if (off > GSP_PHYS_PAGE_SIZE - 16u)
+ off = GSP_PHYS_PAGE_SIZE - 16u; /* keep 16 B in-page */
+ if (gsp_phys_gr_codesrc_empty(p, off))
+ why = "empty-page";
+ else if (gsp_phys_gr_codesrc_ptelike(p, off))
+ why = "pte-page";
+ else if (gsp_phys_gr_codesrc_modpt(pg->hpa))
+ why = "module-pt-page";
+ if (!why)
+ return GSP_PHYS_GR_CODESRC_OK;
+ /*
+ * Alternative resolution order; only a different page that itself
+ * passes the content checks is adopted.
+ */
+ if (!gsp_sema_gr_reloc_resolve(page, &r) && r.hpa &&
+ (r.hpa & ~(u64)(GSP_PHYS_PAGE_SIZE - 1)) !=
+ (pg->hpa & ~(u64)(GSP_PHYS_PAGE_SIZE - 1)) &&
+ !gsp_phys_gr_codesrc_empty((const u8 *)__va(r.hpa), off) &&
+ !gsp_phys_gr_codesrc_ptelike((const u8 *)__va(r.hpa), off)) {
+ *alt_hpa = r.hpa;
+ mode = GSP_PHYS_GR_CODESRC_ALT;
+ used = "alt";
+ } else {
+ used = "guest-va";
+ }
+ pr_info_ratelimited("vgsp-gr: CODESRC-SUSPECT va=0x%llx pa=0x%llx alt=%s used=%s why=%s\n",
+ page, pg->hpa,
+ mode == GSP_PHYS_GR_CODESRC_ALT ? r.stage : "none",
+ used, why);
+ return mode;
+}
+
int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
struct gsp_phys_backend_copy_result *out)
{
@@ -18358,6 +18511,7 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
&boot_stage.rmcall_gr_shadow;
u8 *sh_page;
u32 j, copied = 0;
+ bool code_keep_guest = false;
if (n_shadow >= GSP_PHYS_RMCALL_GR_SHADOW_PAGES) {
gsp_phys_gr_pool_exhausted++;
@@ -18383,8 +18537,32 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
* domain, copy the full guest page into the shadow and
* rewrite the QMD pointer pair, so no inline coverage
* is needed for pages the engine only reads.
+ *
+ * CODESRC: the program page (qmd_word 48) is the page
+ * the engine's I-fetch reads; screen its source
+ * before publishing it. A refused source is either
+ * replaced by the alternative resolver's page or the
+ * pair is left at the guest VA (no copy, no rewrite,
+ * no mapping - the page is fully withdrawn from the
+ * submission).
*/
- if (pg->hpa) {
+ if (pg->hpa && pg->qmd_word == 48u) {
+ u64 alt_hpa = 0;
+ int cs = gsp_phys_gr_code_src(req, pg, &alt_hpa);
+
+ if (cs == GSP_PHYS_GR_CODESRC_GUEST_VA) {
+ code_keep_guest = true;
+ mapped = true;
+ } else {
+ const u8 *csrc = __va(cs ==
+ GSP_PHYS_GR_CODESRC_ALT ?
+ alt_hpa : pg->hpa);
+
+ memcpy(sh_page, csrc,
+ GSP_PHYS_PAGE_SIZE);
+ copied++;
+ }
+ } else if (pg->hpa) {
memcpy(sh_page, (u8 *)__va(pg->hpa),
GSP_PHYS_PAGE_SIZE);
copied++;
@@ -18442,7 +18620,7 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
* data-access instruction and the guest VA arguments it
* fetches can be read from the log.
*/
- if (pg->hpa && pg->qmd_word == 48u) {
+ if (pg->hpa && pg->qmd_word == 48u && !code_keep_guest) {
u32 po = (qmd[48] &
(GSP_PHYS_PAGE_SIZE - 1)) / 4;
const u32 *w = (const u32 *)sh_page + po;