2 Commits

Author SHA1 Message Date
lirent 5b89dabc04 fix: pin the vgpu region resident (raise working set, then VirtualLock)
The fallback path committed the region but left it demand-zero (not resident). VirtualLock can pin only up to the process minimum working set, and the default quota is far below the 96 MiB region, so a bare lock fails with ERROR_WORKING_SET_QUOTA. Raise the minimum first via SetProcessWorkingSetSize, then VirtualLock; pre-fault every page as a last resort. Neither needs SE_LOCK_MEMORY (that is for large pages / AWE).
2026-06-22 15:17:37 +03:00
lirent 13a16975e3 fix: discover the vgpu region in the producer process user space
The region is a shared mapping in a producer process's user address space, not a kernel VA: open a read-only process context, enumerate processes, and locate it by structural invariants under each process cr3. Frames, cursor, geometry and status are read under the producer's cr3.
2026-06-21 00:03:40 +03:00
7 changed files with 350 additions and 190 deletions
+103 -54
View File
@@ -10,11 +10,20 @@
* coherence, never opens RW guest memory, never decides control or behavioural * coherence, never opens RW guest memory, never decides control or behavioural
* timing, never emits events upward. * timing, never emits events upward.
* *
* Where the region lives (the correction that shapes this API): the region is a
* RW shared mapping projected into the USER address space of a producer PROCESS,
* NOT a kernel VA in the System address space. So the core is handed a RO win32
* context (which the caller opened with the System kcr3), enumerates processes
* with proc_list, and finds the region in a process user-AS under that process's
* own cr3 (process.cr3). The System kcr3 is needed ONLY to open the context and
* walk processes; once the region is found, it is always read under the
* producer's process.cr3 (cached in the handle). The handle carries proc_cr3.
*
* What this core does NOT do (by design — those belong to the caller): * What this core does NOT do (by design — those belong to the caller):
* - It does NOT own the vmie_mem / coherent address-space root: (m, kcr3) are * - It does NOT own the vmie_win32 context / vmie_mem: both are BORROWED. The
* BORROWED. The core never opens or closes a vmie_mem; the caller opens it * caller opens the RO win32 context (its lifetime is tied to the guest
* for the current guest address-space mapping and closes it when that * address-space mapping epoch) and closes it when that mapping goes stale.
* mapping goes stale. * The core never opens or closes either.
* - It does NOT sleep / poll / spawn threads / arm timers: the two-phase * - It does NOT sleep / poll / spawn threads / arm timers: the two-phase
* liveness handshake is two calls; the WAIT between them is the caller's. * liveness handshake is two calls; the WAIT between them is the caller's.
* - It does NOT transport frames. Frame transport is the caller's concern; * - It does NOT transport frames. Frame transport is the caller's concern;
@@ -24,88 +33,123 @@
* desired frame + offsets; the actual write is performed elsewhere, by a * desired frame + offsets; the actual write is performed elsewhere, by a
* component that holds read-write access to the region. * component that holds read-write access to the region.
* *
* Two epochs + producer restart (the caller owns the policy; the core only
* reports facts — this is a flat pull model, no polling from below):
* - Address-space invalidation (new kcr3 / new epoch): the caller closes the
* win32 context, drops the old vgpup_region, opens a fresh context on the
* new epoch and re-discovers (vgpup_open). The old handle is invalid (a
* different address space entirely).
* - vgpu run_epoch advance while the context stays live (session break, same
* process): vgpup_read_status records r->run_epoch; vgpup_run_epoch reports
* it. The caller compares and decides whether to reset vgpu state — the
* region/process are unchanged. The core holds no reset policy.
* - Producer process restart (new pid/cr3 under the same live kcr3): the win32
* context is still valid (kernel alive), but the old handle's proc_cr3 /
* region_gva point at a dead process address space. Symptom: a read under
* r->proc_cr3 returns <0 (the process pages are gone). The core only REPORTS
* this (<0 from a read); the DECISION to re-discover is the caller's — it
* calls vgpup_close(old) + vgpup_open(v) so a fresh proc_list finds the
* restarted producer with its new cr3.
*
* Ownership convention: * Ownership convention:
* - vmie_mem* m, uintptr_t kcr3 — BORROWED. The caller owns their lifecycle * - vmie_win32* v, vmie_mem* m — BORROWED. The caller owns their lifecycle
* (tied to the address-space mapping). The core only reads through them. * (tied to the address-space mapping). The core only reads through them.
* - vgpup_region* — heap-owned by the core (small private state). Create with * - vgpup_region* — heap-owned by the core (small private state). Create with
* vgpup_open, release with vgpup_close. Closing it does NOT touch (m, kcr3). * vgpup_open, release with vgpup_close. Closing it does NOT touch v / m.
* *
* Conventions (mirror memmodel.h): * Conventions (mirror memmodel.h):
* - kcr3 is the System address space CR3 (the region is a pinned device * - The System kcr3 opens the RO win32 context; the REGION lives in the USER
* shared-section visible as a kernel VA). A "GVA" is a 64-bit guest VA. * address space of the producer process and is read under its process.cr3
* (cached in the handle as proc_cr3). A "GVA" is a 64-bit guest VA in that
* process address space.
* - All guest reads go through gva_read into a local copy; no borrowed * - All guest reads go through gva_read into a local copy; no borrowed
* pointer into guest memory ever escapes a seqlock window or this API. * pointer into guest memory ever escapes a seqlock window or this API.
* - Integer returns: 0 success / negative failure for deterministic calls. * - Integer returns: 0 success / negative failure for deterministic calls.
* Lossy read calls (sample/cursor/geometry) are tristate: 1 = consistent * Lossy read calls (sample/cursor/geometry) are tristate: 1 = consistent
* snapshot produced, 0 = no fresh data / writer kept it busy past the retry * snapshot produced, 0 = no fresh data / writer kept it busy past the retry
* limit / would not fit (a SKIP, never an error — do not block), <0 = a * limit / would not fit (a SKIP, never an error — do not block), <0 = a
* hard memory-read error (page gone / mapping stale — the caller re-discovers). * hard memory-read error (page gone / process restarted — the caller
* re-discovers; see "Two epochs + producer restart" above).
* *
* Example (the caller drives the two-phase liveness and the read loop): * Example (the caller drives the two-phase liveness and the read loop):
* *
* // caller already opened a RO vmie_mem for the current address-space mapping: * // caller already opened a RO win32 context with the System kcr3:
* vmie_mem* m = caller_mem; // BORROWED by the core * vmie_win32* v = caller_ctx; // BORROWED by the core
* uintptr_t kcr3 = caller_kcr3; // System AS * vmie_mem* m = vmie_win32_mem(v); // BORROWED; for the generic gva_*
* *
* vgpup_region* r = vgpup_open(m, kcr3); // phase 1: candidate + hb0 * vgpup_region* r = vgpup_open(v); // phase 1: find producer + candidate
* if (!r) { return; } // no region under this AS * if (!r) { return; } // no region in any process
* *
* // phase 2 is the caller's: it waits >= VGPU_HEARTBEAT_PERIOD_MS, then * // phase 2 is the caller's: it waits >= VGPU_HEARTBEAT_PERIOD_MS, then
* uint64_t region_gva, hb0; * uint64_t proc_cr3, region_gva, hb0;
* vgpup_discover_candidate(m, kcr3, &region_gva, &hb0); // (or reuse open's) * vgpup_discover_candidate(v, &proc_cr3, &region_gva, &hb0); // (or reuse open's)
* // ... the caller sleeps here, NOT the core ... * // ... the caller sleeps here, NOT the core ...
* int alive = vgpup_confirm_alive(m, kcr3, region_gva, hb0); * int alive = vgpup_confirm_alive(m, proc_cr3, region_gva, hb0);
* *
* // sampling (lossy pull): * // sampling (lossy pull):
* static uint8_t buf[VGPU_SLOT_STRIDE]; * static uint8_t buf[VGPU_SLOT_STRIDE];
* vgpup_frame_info fi; * vgpup_frame_info fi;
* if (vgpup_sample_frame(r, m, kcr3, buf, sizeof buf, &fi) == 1) { * if (vgpup_sample_frame(r, m, buf, sizeof buf, &fi) == 1) {
* // route fi.desc + buf[0..fi.bytes) to the chosen transport * // route fi.desc + buf[0..fi.bytes) to the chosen transport
* } * }
* *
* vgpup_close(r); // frees core state only; (m, kcr3) stay with the caller * vgpup_close(r); // frees core state only; v / m stay with the caller
*/ */
#include <stdint.h> #include <stdint.h>
#include <stddef.h> #include <stddef.h>
#include "vgpu_stream.h" /* region ABI: producer/control types, slot geometry */ #include "vgpu_stream.h" /* region ABI: producer/control types, slot geometry */
#include "memmodel.h" /* vmie_mem, gva_* (BORROWED access primitives) */ #include "win32.h" /* vmie_win32*, proc_list, process, vmie_win32_mem;
* pulls in memmodel.h for vmie_mem / gva_* the
* producer is found via proc_list under the System
* kcr3, then the region is read under process.cr3 */
/* Opaque found vgpu region under (vmie_mem, kcr3). Heap-owned by the core; holds /* Opaque found vgpu region in a producer's user address space. Heap-owned by the
* only small private state (region GVA, last frame_id, last run_epoch). It does * core; holds only small private state (proc_cr3, region/ctrl/ring GVA, last
* NOT own (m, kcr3) — those are passed back in on every read. */ * frame_id, last run_epoch). It does NOT own v / m — those are passed back in on
* every read. */
typedef struct vgpup_region vgpup_region; typedef struct vgpup_region vgpup_region;
/* ---- handle / lifecycle (the core does NOT own vmie_mem) ------------------ */ /* ---- handle / lifecycle (the core does NOT own the win32 context) --------- */
/* Phase-1 discover + bind: find the region by structural invariants, snapshot /* Phase-1 discover + bind: enumerate processes (proc_list) over the BORROWED RO
* hb0, and build a handle. (m, kcr3) are BORROWED — the core reads them but * win32 context v, scan each process user-AS by structural invariants, snapshot
* never closes them. Returns a heap-owned vgpup_region*, or NULL if no region * hb0, and build a handle carrying the producer's proc_cr3 + region/ctrl/ring
* is found under this address space. Liveness is NOT yet proven: the caller must * GVA. v is BORROWED — the core reads through it but never closes it (its
* call vgpup_confirm_alive after waiting >= VGPU_HEARTBEAT_PERIOD_MS. Sampling * lifetime is the caller's, tied to the address-space mapping epoch). Returns a
* before confirmation is allowed (lossy); "producer alive" is true only after a * heap-owned vgpup_region*, or NULL if no region is found in any process.
* positive confirm. */ * Liveness is NOT
vgpup_region* vgpup_open(vmie_mem* m, uintptr_t kcr3); * yet proven: the caller must call vgpup_confirm_alive after waiting
* >= VGPU_HEARTBEAT_PERIOD_MS. Sampling before confirmation is allowed (lossy);
* "producer alive" is true only after a positive confirm.
*
* If a later read returns <0, the producer process may have restarted (its
* pages are gone): the caller re-discovers via vgpup_close(r) + vgpup_open(v). */
vgpup_region* vgpup_open(vmie_win32* v);
/* Release ONLY the core state. Does NOT touch (m, kcr3) — the caller closes /* Release ONLY the core state. Does NOT touch v / m — the caller closes those
* those (their lifetime is the caller's). Safe on NULL. */ * (their lifetime is the caller's). Safe on NULL. */
void vgpup_close(vgpup_region* r); void vgpup_close(vgpup_region* r);
/* ---- two-phase discovery (the WAIT belongs to the caller) ----------------- */ /* ---- two-phase discovery (the WAIT belongs to the caller) ----------------- */
/* Phase 1: find a candidate by structural invariants (no liveness). On success /* Phase 1: find a producer and a candidate region in its user-AS (no liveness).
* writes the region base GVA (== producer-block GVA) to *out_region_gva and the * Walks proc_list over v and, for each process, scans its user-AS under
* heartbeat snapshot to *out_hb0, and returns 0. Returns <0 if no candidate is * process.cr3 by structural invariants. On the first hit writes the producer's
* found or a read fails. Pure; does NOT wait. */ * cr3 to *out_proc_cr3, the region base GVA to *out_region_gva and the heartbeat
int vgpup_discover_candidate(vmie_mem* m, uintptr_t kcr3, * snapshot to *out_hb0, and returns 0. Returns <0 if no candidate is found in
* any process or a read fails. Pure; does NOT wait. Needs v for proc_list. */
int vgpup_discover_candidate(vmie_win32* v, uint64_t* out_proc_cr3,
uint64_t* out_region_gva, uint64_t* out_hb0); uint64_t* out_region_gva, uint64_t* out_hb0);
/* Phase 2: confirm liveness. The caller calls this >= VGPU_HEARTBEAT_PERIOD_MS /* Phase 2: confirm liveness. The caller calls this >= VGPU_HEARTBEAT_PERIOD_MS
* after phase 1. Re-reads heartbeat at region_gva and returns 1 if it advanced * after phase 1. Re-reads heartbeat at region_gva under proc_cr3 and returns 1
* (alive producer), 0 if it did not tick (dead / not the region), <0 on a read * if it advanced (alive producer), 0 if it did not tick (dead / not the region),
* error. Pure; does NOT wait — the inter-phase delay is the caller's. */ * <0 on a read error. Takes vmie_mem* m (== vmie_win32_mem(v)) and proc_cr3 —
int vgpup_confirm_alive(vmie_mem* m, uintptr_t kcr3, * the win32 surface is no longer needed here, only gva_read. Pure; does NOT
* wait — the inter-phase delay is the caller's. */
int vgpup_confirm_alive(vmie_mem* m, uint64_t proc_cr3,
uint64_t region_gva, uint64_t hb0); uint64_t region_gva, uint64_t hb0);
/* ---- snapshots (POD values; read under their seqlock discipline) ---------- */ /* ---- snapshots (POD values; read under their seqlock discipline) ---------- */
@@ -158,7 +202,12 @@ typedef struct {
uint64_t content_change_ns; uint64_t content_change_ns;
} vgpup_status; } vgpup_status;
/* ---- read API (lossy; seqlock discipline lives inside) -------------------- */ /* ---- read API (lossy; seqlock discipline lives inside) -------------------- *
* All read functions read under r->proc_cr3 (the producer's cr3, cached in the
* handle at discovery). m is a BORROWED vmie_mem* (== vmie_win32_mem(v)); the
* cr3 is NOT in the signature — it travels in the handle. A <0 return is a hard
* memory-read error: the producer process may have restarted, so the caller
* re-discovers (see "Two epochs + producer restart" in the file header). */
/* Sample the latest frame. Seqlock-reads latest/seq[slot]/desc, copies the slot /* Sample the latest frame. Seqlock-reads latest/seq[slot]/desc, copies the slot
* bytes out of the RING via gva_read, then re-checks seq[slot] in one window. * bytes out of the RING via gva_read, then re-checks seq[slot] in one window.
@@ -166,26 +215,23 @@ typedef struct {
* copied (info filled), 0 = no new frame / writer busy past the retry limit / * copied (info filled), 0 = no new frame / writer busy past the retry limit /
* frame would not fit cap (lossy SKIP, not an error), <0 = a memory-read error. * frame would not fit cap (lossy SKIP, not an error), <0 = a memory-read error.
* "Fresh" dedups by frame_id: a frame_id <= the last sampled one returns 0. */ * "Fresh" dedups by frame_id: a frame_id <= the last sampled one returns 0. */
int vgpup_sample_frame(vgpup_region* r, vmie_mem* m, uintptr_t kcr3, int vgpup_sample_frame(vgpup_region* r, vmie_mem* m,
uint8_t* dst, size_t cap, vgpup_frame_info* info); uint8_t* dst, size_t cap, vgpup_frame_info* info);
/* Read the cursor under the cursor_seq acquire gate. 1 = consistent snapshot, /* Read the cursor under the cursor_seq acquire gate. 1 = consistent snapshot,
* 0 = writer busy past the retry limit, <0 = read error. */ * 0 = writer busy past the retry limit, <0 = read error. */
int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, uintptr_t kcr3, int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, vgpup_cursor* out);
vgpup_cursor* out);
/* Read display geometry under the geom_seq seqlock. Returns as read_cursor. */ /* Read display geometry under the geom_seq seqlock. Returns as read_cursor. */
int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, uintptr_t kcr3, int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, vgpup_geometry* out);
vgpup_geometry* out);
/* Read the cold-line status/lifecycle. 0 = success, <0 = read error. The single /* Read the cold-line status/lifecycle. 0 = success, <0 = read error. The single
* atomic fields carry no seqlock; the snapshot is "fresh enough" (lossy). */ * atomic fields carry no seqlock; the snapshot is "fresh enough" (lossy). */
int vgpup_read_status(vgpup_region* r, vmie_mem* m, uintptr_t kcr3, int vgpup_read_status(vgpup_region* r, vmie_mem* m, vgpup_status* out);
vgpup_status* out);
/* The run_epoch from the last vgpup_read_status — a session-break detector for /* The run_epoch from the last vgpup_read_status — a session-break detector for
* the caller while kcr3 stays live. The core only reports the raw value; it * the caller while the address space stays live. The core only reports the raw
* holds no reset policy (what to reset is the caller's decision). */ * value; it holds no reset policy (what to reset is the caller's decision). */
uint32_t vgpup_run_epoch(const vgpup_region* r); uint32_t vgpup_run_epoch(const vgpup_region* r);
/* ---- control-write — SEAM ONLY (this never writes) ------------------------ */ /* ---- control-write — SEAM ONLY (this never writes) ------------------------ */
@@ -209,7 +255,10 @@ typedef struct {
* consumer_tick/attached carry separate heartbeat/intent semantics and are NOT * consumer_tick/attached carry separate heartbeat/intent semantics and are NOT
* part of this intent. * part of this intent.
* out_frame — filled vgpu_control_t (significant fields from `in`) * out_frame — filled vgpu_control_t (significant fields from `in`)
* out_ctrl_gva — control-block GVA (region base + VGPU_CONTROL_OFFSET) * out_ctrl_gva — control-block GVA (region base + VGPU_CONTROL_OFFSET). This
* GVA is valid in the PRODUCER's user address space: the
* external write MUST be performed under r->proc_cr3, NOT the
* System kcr3.
* out_off — offset of the first significant field (offsetof desired_state) * out_off — offset of the first significant field (offsetof desired_state)
* out_len — length of the significant range (through full_frame_req) * out_len — length of the significant range (through full_frame_req)
* Returns 0 on success, <0 if r is NULL. The write itself is performed * Returns 0 on success, <0 if r is NULL. The write itself is performed
+4
View File
@@ -5,6 +5,10 @@
* the intent and computes the GVA + offset/length of the significant field range * the intent and computes the GVA + offset/length of the significant field range
* for that atomic write under the ctrl_gen seqlock. There is no gva_write here * for that atomic write under the ctrl_gen seqlock. There is no gva_write here
* and there must not be — the source is a RO fd that would fault on a store anyway. * and there must not be — the source is a RO fd that would fault on a store anyway.
*
* The reported out_ctrl_gva is a GVA in the PRODUCER's user address space
* (region base + VGPU_CONTROL_OFFSET, cached as r->ctrl_gva): the external write
* MUST be performed under r->proc_cr3, NOT the System kcr3.
*/ */
#include "perception-internal.h" #include "perception-internal.h"
+88 -32
View File
@@ -1,49 +1,68 @@
/* discover.c — region discovery by structural invariants (NO magic) + handle. /* discover.c — process discovery + user-AS region scan (NO magic) + handle.
* *
* The region is a pinned device shared-section projected into the System address * The region is a RW shared mapping projected into the USER address space of a
* space under kcr3, so we scan the KERNEL canonical half [KERN_MIN, ~0]. We find * producer PROCESS — NOT a kernel VA in the System address space. So discovery
* a contiguous readable run >= VGPU_REGION_BYTES (the region is GVA-contiguous, * works by PROCESS: enumerate processes (proc_list) over the RO win32 context,
* possibly spread across adjacent same-protection runs), read the producer block * and for each one scan its user-AS under process.cr3 in [USER_MIN, USER_MAX]
* at its base, and accept it iff the whole structural-invariant table holds. * for a contiguous RW run >= VGPU_REGION_BYTES, read the producer block at its
* base, and accept it iff the whole structural-invariant table holds. The System
* kcr3 is needed only to open the context and walk processes (the caller already
* baked it into v); the region itself is always read under the producer's cr3.
* *
* There is NO magic field in the ABI and the owner forbids inventing one. The * There is NO magic field in the ABI and the owner forbids inventing one. The
* discriminator is the invariant table plus two-phase heartbeat liveness — and * discriminator is the cheap RW-run filter + the invariant table + two-phase
* the inter-phase WAIT is the caller's (the core never sleeps). * heartbeat liveness — and the inter-phase WAIT is the caller's (the core never
* sleeps). Discovery is STRUCTURAL: never filtered by process.name.
*
* Layering: the win32 dependency (proc_list, vmie_win32_mem) lives ONLY in this
* file, in the per-process loop. The per-cr3 scan (vgpup_scan_user_as_for_region)
* is pure gva_* so it stays win32-agnostic and unit-testable under a synthetic
* cr3. A <0 read after binding means the producer process may have restarted
* (its pages are gone); the core only reports it — re-discovery is the caller's.
*/ */
#include <stdlib.h> #include <stdlib.h>
#include "perception-internal.h" #include "perception-internal.h"
/* How many region records to ask for when probing the System AS. The kernel half /* How many region runs to ask for per process when probing its user-AS. A user
* has few large same-protection runs; this is generous for the shared-section. */ * address space has many runs; this is generous, and the scan early-exits on the
* first accepted candidate anyway. */
#define VGPUP_MAX_REGIONS 256 #define VGPUP_MAX_REGIONS 256
/* Read the producer block at `region_gva` into *out (one gva_read of the whole /* How many processes to enumerate. proc_list stops at this; raising it would see
* block). 0 on success, <0 on read error. */ * more, but a producer is an ordinary user process well within this bound. */
static int read_producer_block(vmie_mem* m, uintptr_t kcr3, uint64_t region_gva, #define VGPUP_MAX_PROCS 512
/* Read the producer block at `region_gva` under `cr3` into *out (one gva_read of
* the whole block). 0 on success, <0 on read error. */
static int read_producer_block(vmie_mem* m, uint64_t cr3, uint64_t region_gva,
vgpu_producer_t* out) vgpu_producer_t* out)
{ {
return gva_read(m, kcr3, (uintptr_t)region_gva, out, sizeof *out) < 0 ? -1 : 0; return gva_read(m, (uintptr_t)cr3, (uintptr_t)region_gva, out, sizeof *out) < 0 ? -1 : 0;
} }
/* Walk the region runs in the kernel half and, for each contiguous readable span /* Scan ONE process user-AS (steps 35) under `cr3`: walk the RW runs in
* of >= VGPU_REGION_BYTES, test the producer block at the span base against the * [USER_MIN, USER_MAX] and, for each contiguous run >= VGPU_REGION_BYTES, test
* invariant table. On the first accepted candidate, write its base GVA + the * the producer block at the run base against the invariant table. On the first
* heartbeat snapshot and return 0. Returns <0 if none is found / a read fails. * accepted candidate write its base GVA + heartbeat snapshot and return 0;
* <0 if none is found / a read fails. Pure gva_* — no proc_list, no win32.
* *
* Adjacent same-protection runs are coalesced: gva_regions reports VA-contiguous * Adjacent same-protection runs are coalesced: gva_regions reports VA-contiguous
* runs, but a region can land as one run or as touching neighbours, so we extend * runs, but a region can land as one run or as touching neighbours, so we extend
* a running span while the next run starts exactly where the current one ends. */ * a running span while the next run starts exactly where the current one ends.
int vgpup_discover_candidate(vmie_mem* m, uintptr_t kcr3, * The window [USER_MIN, USER_MAX] lies in one canonical half, as gva_regions
uint64_t* out_region_gva, uint64_t* out_hb0) * requires. The RW filter (VR_R|VR_W) matches the shared mapping's protection
* and is cheap — it reads region metadata, not the 98 MiB of region bytes. */
int vgpup_scan_user_as_for_region(vmie_mem* m, uint64_t cr3,
uint64_t* out_region_gva, uint64_t* out_hb0)
{ {
vregion runs[VGPUP_MAX_REGIONS]; vregion runs[VGPUP_MAX_REGIONS];
int n, i; int n, i;
if (!m || !out_region_gva || !out_hb0) { return -1; } if (!m || !out_region_gva || !out_hb0) { return -1; }
n = gva_regions(m, kcr3, KERN_MIN, ~0ull, VR_R, runs, VGPUP_MAX_REGIONS); n = gva_regions(m, (uintptr_t)cr3, USER_MIN, USER_MAX, VR_R | VR_W, runs, VGPUP_MAX_REGIONS);
if (n < 0) { return -1; } if (n < 0) { return -1; }
if (n > VGPUP_MAX_REGIONS) { n = VGPUP_MAX_REGIONS; } /* truncated; probe what we got */ if (n > VGPUP_MAX_REGIONS) { n = VGPUP_MAX_REGIONS; } /* truncated; probe what we got */
@@ -52,7 +71,7 @@ int vgpup_discover_candidate(vmie_mem* m, uintptr_t kcr3,
uint64_t span_len = runs[i].len; uint64_t span_len = runs[i].len;
int j = i; int j = i;
/* coalesce adjacent readable runs into one contiguous span */ /* coalesce adjacent RW runs into one contiguous span */
while (j + 1 < n && runs[j + 1].va == runs[j].va + runs[j].len) { while (j + 1 < n && runs[j + 1].va == runs[j].va + runs[j].len) {
span_len += runs[j + 1].len; span_len += runs[j + 1].len;
++j; ++j;
@@ -60,7 +79,7 @@ int vgpup_discover_candidate(vmie_mem* m, uintptr_t kcr3,
if (span_len >= VGPU_REGION_BYTES) { if (span_len >= VGPU_REGION_BYTES) {
vgpu_producer_t p; vgpu_producer_t p;
if (read_producer_block(m, kcr3, span_base, &p) == 0 && if (read_producer_block(m, cr3, span_base, &p) == 0 &&
vgpup_invariants_hold(&p)) { vgpup_invariants_hold(&p)) {
*out_region_gva = span_base; *out_region_gva = span_base;
*out_hb0 = p.heartbeat; *out_hb0 = p.heartbeat;
@@ -71,30 +90,67 @@ int vgpup_discover_candidate(vmie_mem* m, uintptr_t kcr3,
return -1; return -1;
} }
/* Phase 2: re-read heartbeat at region_gva and report whether it advanced. The /* Phase 1: enumerate processes and scan each one's user-AS for the region. The
* caller must have waited >= VGPU_HEARTBEAT_PERIOD_MS since phase 1. */ * win32 dependency is confined here: vmie_win32_mem(v) for the generic gva_*,
int vgpup_confirm_alive(vmie_mem* m, uintptr_t kcr3, * proc_list(v, skip_system=1, ...) to drop PEB-less System/kernel-only entries
* (a producer is never one). On the first process that yields a candidate write
* its proc_cr3 + region base GVA + heartbeat snapshot and return 0; <0 if no
* process yields one or proc_list / the context is not ready. */
int vgpup_discover_candidate(vmie_win32* v, uint64_t* out_proc_cr3,
uint64_t* out_region_gva, uint64_t* out_hb0)
{
process procs[VGPUP_MAX_PROCS];
vmie_mem* m;
int np, i;
if (!v || !out_proc_cr3 || !out_region_gva || !out_hb0) { return -1; }
m = vmie_win32_mem(v);
if (!m) { return -1; }
np = proc_list(v, /*skip_system=*/1, procs, VGPUP_MAX_PROCS);
if (np < 0) { return -1; }
if (np > VGPUP_MAX_PROCS) { np = VGPUP_MAX_PROCS; } /* truncated; probe what we got */
for (i = 0; i < np; ++i) {
uint64_t region_gva = 0, hb0 = 0;
if (vgpup_scan_user_as_for_region(m, procs[i].cr3, &region_gva, &hb0) == 0) {
*out_proc_cr3 = procs[i].cr3;
*out_region_gva = region_gva;
*out_hb0 = hb0;
return 0;
}
}
return -1;
}
/* Phase 2: re-read heartbeat at region_gva under proc_cr3 and report whether it
* advanced. The caller must have waited >= VGPU_HEARTBEAT_PERIOD_MS since phase
* 1. <0 here can also mean the producer process restarted (pages gone). */
int vgpup_confirm_alive(vmie_mem* m, uint64_t proc_cr3,
uint64_t region_gva, uint64_t hb0) uint64_t region_gva, uint64_t hb0)
{ {
uint64_t hb_now; uint64_t hb_now;
if (!m) { return -1; } if (!m) { return -1; }
if (gva_read(m, kcr3, (uintptr_t)region_gva + offsetof(vgpu_producer_t, heartbeat), if (gva_read(m, (uintptr_t)proc_cr3,
(uintptr_t)region_gva + offsetof(vgpu_producer_t, heartbeat),
&hb_now, sizeof hb_now) < 0) { &hb_now, sizeof hb_now) < 0) {
return -1; return -1;
} }
return (hb_now - hb0) > 0u ? 1 : 0; return (hb_now - hb0) > 0u ? 1 : 0;
} }
vgpup_region* vgpup_open(vmie_mem* m, uintptr_t kcr3) vgpup_region* vgpup_open(vmie_win32* v)
{ {
uint64_t region_gva = 0, hb0 = 0; uint64_t proc_cr3 = 0, region_gva = 0, hb0 = 0;
vgpup_region* r; vgpup_region* r;
if (vgpup_discover_candidate(m, kcr3, &region_gva, &hb0) != 0) { return NULL; } if (vgpup_discover_candidate(v, &proc_cr3, &region_gva, &hb0) != 0) { return NULL; }
r = (vgpup_region*)calloc(1, sizeof *r); r = (vgpup_region*)calloc(1, sizeof *r);
if (!r) { return NULL; } if (!r) { return NULL; }
r->proc_cr3 = proc_cr3;
r->region_gva = region_gva; r->region_gva = region_gva;
r->ctrl_gva = region_gva + VGPU_CONTROL_OFFSET; r->ctrl_gva = region_gva + VGPU_CONTROL_OFFSET;
r->ring_gva = region_gva + VGPU_RING_OFFSET; r->ring_gva = region_gva + VGPU_RING_OFFSET;
@@ -105,7 +161,7 @@ vgpup_region* vgpup_open(vmie_mem* m, uintptr_t kcr3)
void vgpup_close(vgpup_region* r) void vgpup_close(vgpup_region* r)
{ {
free(r); /* core state only; (m, kcr3) belong to the caller */ free(r); /* core state only; v / m belong to the caller */
} }
uint32_t vgpup_run_epoch(const vgpup_region* r) uint32_t vgpup_run_epoch(const vgpup_region* r)
+17 -4
View File
@@ -29,8 +29,10 @@
#define VGPUP_SEQLOCK_RETRIES 8u #define VGPUP_SEQLOCK_RETRIES 8u
/* Private core state. Owns nothing of the address space — only where the region /* Private core state. Owns nothing of the address space — only where the region
* lives and the last-seen monotonic markers for dedup / session-break. */ * lives (in the producer's user-AS, keyed by proc_cr3) and the last-seen
* monotonic markers for dedup / session-break. */
struct vgpup_region { struct vgpup_region {
uint64_t proc_cr3; /* producer process cr3 — key to its user-AS */
uint64_t region_gva; /* producer-block GVA == region base */ uint64_t region_gva; /* producer-block GVA == region base */
uint64_t ctrl_gva; /* region_gva + VGPU_CONTROL_OFFSET (cached) */ uint64_t ctrl_gva; /* region_gva + VGPU_CONTROL_OFFSET (cached) */
uint64_t ring_gva; /* region_gva + VGPU_RING_OFFSET (cached) */ uint64_t ring_gva; /* region_gva + VGPU_RING_OFFSET (cached) */
@@ -38,15 +40,26 @@ struct vgpup_region {
uint32_t run_epoch; /* last run_epoch seen via vgpup_read_status */ uint32_t run_epoch; /* last run_epoch seen via vgpup_read_status */
}; };
/* Per-cr3 user-AS region scan (discovery steps 35 for ONE address space): scan
* gva_regions over [USER_MIN, USER_MAX] under `cr3` for a contiguous RW run of
* >= VGPU_REGION_BYTES, read the producer block at its base, and accept it iff
* the structural-invariant table holds. On the first hit writes the region base
* GVA to *out_region_gva and the heartbeat snapshot to *out_hb0 and returns 0;
* <0 if none is found / a read fails. Pure gva_* (no proc_list / win32) so it is
* testable under a synthetic cr3; vgpup_discover_candidate calls it per process. */
int vgpup_scan_user_as_for_region(vmie_mem* m, uint64_t cr3,
uint64_t* out_region_gva, uint64_t* out_hb0);
/* ---- seqlock primitives -------------------------------------------------- */ /* ---- seqlock primitives -------------------------------------------------- */
static inline int vgpup_seq_is_writing(uint32_t seq) { return (seq & 1u) != 0u; } static inline int vgpup_seq_is_writing(uint32_t seq) { return (seq & 1u) != 0u; }
/* Read one 32-bit seq field at `gva` into *out. 0 on success, <0 on read error. */ /* Read one 32-bit seq field at `gva` into *out under `cr3` (the producer's
static inline int vgpup_read_seq(vmie_mem* m, uintptr_t kcr3, uint64_t gva, * user-AS cr3). 0 on success, <0 on read error. */
static inline int vgpup_read_seq(vmie_mem* m, uintptr_t cr3, uint64_t gva,
uint32_t* out) uint32_t* out)
{ {
return gva_read(m, kcr3, (uintptr_t)gva, out, sizeof *out) < 0 ? -1 : 0; return gva_read(m, cr3, (uintptr_t)gva, out, sizeof *out) < 0 ? -1 : 0;
} }
/* ---- packed-field unpackers (cursor line) -------------------------------- */ /* ---- packed-field unpackers (cursor line) -------------------------------- */
+36 -33
View File
@@ -8,18 +8,24 @@
* Lossy by contract: when a writer keeps a window busy past VGPUP_SEQLOCK_RETRIES * Lossy by contract: when a writer keeps a window busy past VGPUP_SEQLOCK_RETRIES
* we return 0 (skip), never block. Blocking longer would be behavioural timing * we return 0 (skip), never block. Blocking longer would be behavioural timing
* (control's concern), which has no place in the sensor. * (control's concern), which has no place in the sensor.
*
* All reads go under r->proc_cr3 (the producer's user-AS cr3, cached in the
* handle at discovery), NOT the System kcr3. A <0 from any gva_read means a page
* is gone — the producer process may have restarted; we propagate <0 and the
* caller re-discovers (see vgpu_perception.h "Two epochs + producer restart").
*/ */
#include "perception-internal.h" #include "perception-internal.h"
/* Read one cold-line / packed field at producer offset `off` into dst. */ /* Read one cold-line / packed field at producer offset `off` into dst under the
static int read_field(vmie_mem* m, uintptr_t kcr3, uint64_t region_gva, * producer's user-AS cr3. */
static int read_field(vmie_mem* m, uintptr_t cr3, uint64_t region_gva,
size_t off, void* dst, size_t n) size_t off, void* dst, size_t n)
{ {
return gva_read(m, kcr3, (uintptr_t)region_gva + off, dst, n) < 0 ? -1 : 0; return gva_read(m, cr3, (uintptr_t)region_gva + off, dst, n) < 0 ? -1 : 0;
} }
int vgpup_sample_frame(vgpup_region* r, vmie_mem* m, uintptr_t kcr3, int vgpup_sample_frame(vgpup_region* r, vmie_mem* m,
uint8_t* dst, size_t cap, vgpup_frame_info* info) uint8_t* dst, size_t cap, vgpup_frame_info* info)
{ {
unsigned attempt; unsigned attempt;
@@ -33,7 +39,7 @@ int vgpup_sample_frame(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
size_t frame_bytes; size_t frame_bytes;
/* latest (acquire-equivalent: its own read) */ /* latest (acquire-equivalent: its own read) */
if (read_field(m, kcr3, r->region_gva, if (read_field(m, r->proc_cr3, r->region_gva,
offsetof(vgpu_producer_t, latest), &latest, sizeof latest) < 0) { offsetof(vgpu_producer_t, latest), &latest, sizeof latest) < 0) {
return -1; return -1;
} }
@@ -42,10 +48,10 @@ int vgpup_sample_frame(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
seq_gva = r->region_gva + offsetof(vgpu_producer_t, seq) + (uint64_t)latest * sizeof(uint32_t); seq_gva = r->region_gva + offsetof(vgpu_producer_t, seq) + (uint64_t)latest * sizeof(uint32_t);
desc_gva = r->region_gva + offsetof(vgpu_producer_t, desc) + (uint64_t)latest * sizeof(vgpu_desc_t); desc_gva = r->region_gva + offsetof(vgpu_producer_t, desc) + (uint64_t)latest * sizeof(vgpu_desc_t);
if (vgpup_read_seq(m, kcr3, seq_gva, &seq_before) < 0) { return -1; } if (vgpup_read_seq(m, r->proc_cr3, seq_gva, &seq_before) < 0) { return -1; }
if (vgpup_seq_is_writing(seq_before)) { continue; } /* writer in slot */ if (vgpup_seq_is_writing(seq_before)) { continue; } /* writer in slot */
if (gva_read(m, kcr3, (uintptr_t)desc_gva, &d, sizeof d) < 0) { return -1; } if (gva_read(m, (uintptr_t)r->proc_cr3, (uintptr_t)desc_gva, &d, sizeof d) < 0) { return -1; }
/* dedup by frame_id: nothing newer than what we already sampled */ /* dedup by frame_id: nothing newer than what we already sampled */
if (d.frame_id <= r->last_frame_id) { return 0; } if (d.frame_id <= r->last_frame_id) { return 0; }
@@ -62,10 +68,10 @@ int vgpup_sample_frame(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
if (frame_bytes > cap) { return 0; } /* would not fit → lossy drop */ if (frame_bytes > cap) { return 0; } /* would not fit → lossy drop */
slot_gva = r->ring_gva + (uint64_t)latest * VGPU_SLOT_STRIDE; slot_gva = r->ring_gva + (uint64_t)latest * VGPU_SLOT_STRIDE;
if (gva_read(m, kcr3, (uintptr_t)slot_gva, dst, frame_bytes) < 0) { return -1; } if (gva_read(m, (uintptr_t)r->proc_cr3, (uintptr_t)slot_gva, dst, frame_bytes) < 0) { return -1; }
/* re-check the slot seq: unchanged and still even → snapshot consistent */ /* re-check the slot seq: unchanged and still even → snapshot consistent */
if (vgpup_read_seq(m, kcr3, seq_gva, &seq_after) < 0) { return -1; } if (vgpup_read_seq(m, r->proc_cr3, seq_gva, &seq_after) < 0) { return -1; }
if (seq_after != seq_before || vgpup_seq_is_writing(seq_after)) { if (seq_after != seq_before || vgpup_seq_is_writing(seq_after)) {
continue; /* the slot was rewritten under us — retry */ continue; /* the slot was rewritten under us — retry */
} }
@@ -84,8 +90,7 @@ int vgpup_sample_frame(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
return 0; /* writer kept the slot busy past the retry limit — skip */ return 0; /* writer kept the slot busy past the retry limit — skip */
} }
int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, uintptr_t kcr3, int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, vgpup_cursor* out)
vgpup_cursor* out)
{ {
unsigned attempt; unsigned attempt;
@@ -98,19 +103,19 @@ int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
uint32_t visible = 0, hotspot = 0, glyph = 0, id = 0; uint32_t visible = 0, hotspot = 0, glyph = 0, id = 0;
uint64_t pos = 0; uint64_t pos = 0;
if (vgpup_read_seq(m, kcr3, r->region_gva + offsetof(vgpu_producer_t, cursor_seq), if (vgpup_read_seq(m, r->proc_cr3, r->region_gva + offsetof(vgpu_producer_t, cursor_seq),
&seq_before) < 0) { return -1; } &seq_before) < 0) { return -1; }
if (vgpup_seq_is_writing(seq_before)) { continue; } if (vgpup_seq_is_writing(seq_before)) { continue; }
if (read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cursor_visible), &visible, sizeof visible) < 0 || if (read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_visible), &visible, sizeof visible) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cursor_pos), &pos, sizeof pos) < 0 || read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_pos), &pos, sizeof pos) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cursor_hotspot), &hotspot, sizeof hotspot) < 0 || read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_hotspot), &hotspot, sizeof hotspot) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cursor_glyph), &glyph, sizeof glyph) < 0 || read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_glyph), &glyph, sizeof glyph) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cursor_id), &id, sizeof id) < 0) { read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_id), &id, sizeof id) < 0) {
return -1; return -1;
} }
if (vgpup_read_seq(m, kcr3, r->region_gva + offsetof(vgpu_producer_t, cursor_seq), if (vgpup_read_seq(m, r->proc_cr3, r->region_gva + offsetof(vgpu_producer_t, cursor_seq),
&seq_after) < 0) { return -1; } &seq_after) < 0) { return -1; }
if (seq_after != seq_before || vgpup_seq_is_writing(seq_after)) { continue; } if (seq_after != seq_before || vgpup_seq_is_writing(seq_after)) { continue; }
@@ -128,8 +133,7 @@ int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
return 0; return 0;
} }
int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, uintptr_t kcr3, int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, vgpup_geometry* out)
vgpup_geometry* out)
{ {
unsigned attempt; unsigned attempt;
@@ -140,22 +144,22 @@ int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
int32_t virt_x = 0, virt_y = 0, cap_x = 0, cap_y = 0; int32_t virt_x = 0, virt_y = 0, cap_x = 0, cap_y = 0;
uint32_t virt_w = 0, virt_h = 0, dpi = 0, refresh_mhz = 0; uint32_t virt_w = 0, virt_h = 0, dpi = 0, refresh_mhz = 0;
if (vgpup_read_seq(m, kcr3, r->region_gva + offsetof(vgpu_producer_t, geom_seq), if (vgpup_read_seq(m, r->proc_cr3, r->region_gva + offsetof(vgpu_producer_t, geom_seq),
&seq_before) < 0) { return -1; } &seq_before) < 0) { return -1; }
if (vgpup_seq_is_writing(seq_before)) { continue; } if (vgpup_seq_is_writing(seq_before)) { continue; }
if (read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, virt_x), &virt_x, sizeof virt_x) < 0 || if (read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_x), &virt_x, sizeof virt_x) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, virt_y), &virt_y, sizeof virt_y) < 0 || read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_y), &virt_y, sizeof virt_y) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, virt_w), &virt_w, sizeof virt_w) < 0 || read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_w), &virt_w, sizeof virt_w) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, virt_h), &virt_h, sizeof virt_h) < 0 || read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, virt_h), &virt_h, sizeof virt_h) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cap_x), &cap_x, sizeof cap_x) < 0 || read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cap_x), &cap_x, sizeof cap_x) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, cap_y), &cap_y, sizeof cap_y) < 0 || read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cap_y), &cap_y, sizeof cap_y) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, dpi), &dpi, sizeof dpi) < 0 || read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, dpi), &dpi, sizeof dpi) < 0 ||
read_field(m, kcr3, r->region_gva, offsetof(vgpu_producer_t, refresh_mhz), &refresh_mhz, sizeof refresh_mhz) < 0) { read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, refresh_mhz), &refresh_mhz, sizeof refresh_mhz) < 0) {
return -1; return -1;
} }
if (vgpup_read_seq(m, kcr3, r->region_gva + offsetof(vgpu_producer_t, geom_seq), if (vgpup_read_seq(m, r->proc_cr3, r->region_gva + offsetof(vgpu_producer_t, geom_seq),
&seq_after) < 0) { return -1; } &seq_after) < 0) { return -1; }
if (seq_after != seq_before || vgpup_seq_is_writing(seq_after)) { continue; } if (seq_after != seq_before || vgpup_seq_is_writing(seq_after)) { continue; }
@@ -172,8 +176,7 @@ int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
return 0; return 0;
} }
int vgpup_read_status(vgpup_region* r, vmie_mem* m, uintptr_t kcr3, int vgpup_read_status(vgpup_region* r, vmie_mem* m, vgpup_status* out)
vgpup_status* out)
{ {
vgpu_producer_t p; vgpu_producer_t p;
@@ -182,7 +185,7 @@ int vgpup_read_status(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
/* Cold line: single naturally-aligned atomic fields with no seqlock. Read /* Cold line: single naturally-aligned atomic fields with no seqlock. Read
* the whole producer block once and pick the cold fields — "fresh enough" * the whole producer block once and pick the cold fields — "fresh enough"
* by the lossy contract. */ * by the lossy contract. */
if (gva_read(m, kcr3, (uintptr_t)r->region_gva, &p, sizeof p) < 0) { return -1; } if (gva_read(m, (uintptr_t)r->proc_cr3, (uintptr_t)r->region_gva, &p, sizeof p) < 0) { return -1; }
out->heartbeat = p.heartbeat; out->heartbeat = p.heartbeat;
out->run_epoch = p.run_epoch; out->run_epoch = p.run_epoch;
+72 -63
View File
@@ -1,16 +1,21 @@
/* test_perception.c — table-driven invariant predicates + flat sampling smoke. /* test_perception.c — table-driven invariant predicates + per-cr3 user-AS scan.
* *
* Two layers: * Two layers (no proc_list / win32 — that path needs a real Windows kernel
* bring-up and is covered by an out-of-tree integration run, not this unit):
* 1) Invariant predicates as a TABLE of cases over a synthesized producer * 1) Invariant predicates as a TABLE of cases over a synthesized producer
* block (pure, no vmie): valid / latest==NONE / torn odd seq / non-BGRA / * block (pure, no vmie): valid / latest==NONE / torn odd seq / non-BGRA /
* stride!=width*4 / dims out of range — each asserts accept-vs-reject. * stride!=width*4 / dims out of range — each asserts accept-vs-reject.
* 2) Flat sampling smoke: lay out a real region per vgpu_stream.h in a memfd, * 2) Per-cr3 user-AS scan + sampling under a SYNTHETIC cr3: lay out a real
* build a minimal x86-64 identity page table (2 MiB large pages) that maps * region per vgpu_stream.h in a memfd, build a minimal x86-64 identity page
* the region at a kernel VA, open it RO via vmie_mem_from_ro_fd, and run the * table (2 MiB large pages) that maps the region at a USER VA (the region
* real gva_*-driven discovery + frame/cursor/geometry/status reads and a * really lives in a producer's user-AS), open it RO via vmie_mem_from_ro_fd,
* two-phase heartbeat liveness check under that cr3. (cr3 0 over a flat * and run vgpup_scan_user_as_for_region + a two-phase heartbeat liveness
* image cannot translate — gva_* needs real page tables — so we synthesize * check, then construct a handle (proc_cr3 = synth cr3) and run the real
* them; this exercises the actual translation path the caller will use.) * frame/cursor/geometry/status reads and the control-write seam under it.
* (cr3 0 over a flat image cannot translate — gva_* needs real page tables —
* so we synthesize them; this exercises the actual translation path the
* caller will use.) The win32 proc_list wrapper is deliberately NOT exercised
* here: vgpup_scan_user_as_for_region is the pure per-cr3 core it calls.
* *
* Exit 0 on all-pass; nonzero on the first failure. * Exit 0 on all-pass; nonzero on the first failure.
*/ */
@@ -97,17 +102,19 @@ static void run_invariant_table(void)
} }
} }
/* ---- layer 2: flat sampling smoke over a real RO vmie_mem ----------------- */ /* ---- layer 2: per-cr3 user-AS scan + sampling over a real RO vmie_mem ------ */
/* x86-64 paging entry flags for the synthetic identity table. */ /* x86-64 paging entry flags for the synthetic identity table. */
#define PTE_P 0x1u /* present */ #define PTE_P 0x1u /* present */
#define PTE_RW 0x2u /* writable */ #define PTE_RW 0x2u /* writable */
#define PTE_US 0x4u /* user-accessible (the region is in a user-AS) */
#define PTE_PS 0x80u /* page size (2 MiB leaf at PD level) */ #define PTE_PS 0x80u /* page size (2 MiB leaf at PD level) */
#define LARGE_PAGE (2ull * 1024 * 1024) #define LARGE_PAGE (2ull * 1024 * 1024)
/* Build a minimal identity page table mapping [0, span) of the image at kernel /* Build a minimal identity page table mapping [0, span) of the image at user VA
* VA `base` using 2 MiB large pages, with the PML4/PDPT/PD pages laid out right * `base` using 2 MiB large pages, with the PML4/PDPT/PD pages laid out right
* after the region in the same image. Returns the cr3 (PML4 GPA). The mapped VA * after the region in the same image. Every level carries US so the run reports
* VR_W|VR_U (a real user-AS mapping). Returns the cr3 (PML4 GPA). The mapped VA
* range fits one PD (covers up to 1 GiB), which is plenty for the region. */ * range fits one PD (covers up to 1 GiB), which is plenty for the region. */
static uint64_t build_identity_table(uint8_t* img, uint64_t region_bytes, static uint64_t build_identity_table(uint8_t* img, uint64_t region_bytes,
uint64_t base, uint64_t span) uint64_t base, uint64_t span)
@@ -124,10 +131,10 @@ static uint64_t build_identity_table(uint8_t* img, uint64_t region_bytes,
uint64_t mapped = 0; uint64_t mapped = 0;
unsigned k = 0; unsigned k = 0;
pml4[pml4i] = pdpt_gpa | PTE_P | PTE_RW; pml4[pml4i] = pdpt_gpa | PTE_P | PTE_RW | PTE_US;
pdpt[pdpti] = pd_gpa | PTE_P | PTE_RW; pdpt[pdpti] = pd_gpa | PTE_P | PTE_RW | PTE_US;
while (mapped < span) { while (mapped < span) {
pd[pdi0 + k] = mapped | PTE_P | PTE_RW | PTE_PS; /* VA base+k*2M → GPA mapped */ pd[pdi0 + k] = mapped | PTE_P | PTE_RW | PTE_US | PTE_PS; /* VA base+k*2M → GPA mapped */
mapped += LARGE_PAGE; mapped += LARGE_PAGE;
++k; ++k;
} }
@@ -140,7 +147,9 @@ static void run_flat_smoke(void)
/* region rounded up to a 2 MiB boundary for the large-page identity map */ /* region rounded up to a 2 MiB boundary for the large-page identity map */
const uint64_t mapped_span = (region_bytes + LARGE_PAGE - 1) & ~(LARGE_PAGE - 1); const uint64_t mapped_span = (region_bytes + LARGE_PAGE - 1) & ~(LARGE_PAGE - 1);
const size_t total_bytes = (size_t)region_bytes + 0x3000; /* + PML4/PDPT/PD */ const size_t total_bytes = (size_t)region_bytes + 0x3000; /* + PML4/PDPT/PD */
const uint64_t base_va = KERN_MIN; /* kernel VA */ /* a USER VA, 2 MiB-aligned, within [USER_MIN, USER_MAX] — the region lives in
* a producer's user address space, so we map it there (not at a kernel VA). */
const uint64_t base_va = 0x0000000010000000ull;
const uint32_t w = 64, h = 32; const uint32_t w = 64, h = 32;
const size_t frame_bytes = (size_t)w * h * 4u; const size_t frame_bytes = (size_t)w * h * 4u;
int fd; int fd;
@@ -176,13 +185,13 @@ static void run_flat_smoke(void)
CHECK(m != NULL, "vmie_mem_from_ro_fd"); CHECK(m != NULL, "vmie_mem_from_ro_fd");
if (!m) { munmap(img, total_bytes); close(fd); return; } if (!m) { munmap(img, total_bytes); close(fd); return; }
/* discovery: candidate found at the kernel VA with hb0 == 42 */ /* per-cr3 user-AS scan: candidate found at the user VA with hb0 == 42 */
{ {
uint64_t rgva = 0xdead, hb0 = 0; uint64_t rgva = 0xdead, hb0 = 0;
int rc = vgpup_discover_candidate(m, cr3, &rgva, &hb0); int rc = vgpup_scan_user_as_for_region(m, cr3, &rgva, &hb0);
CHECK(rc == 0, "discover_candidate rc"); CHECK(rc == 0, "scan_user_as rc");
CHECK(rgva == base_va, "discover_candidate region gva"); CHECK(rgva == base_va, "scan_user_as region gva");
CHECK(hb0 == 42, "discover_candidate hb0"); CHECK(hb0 == 42, "scan_user_as hb0");
/* two-phase liveness: not alive until heartbeat advances */ /* two-phase liveness: not alive until heartbeat advances */
CHECK(vgpup_confirm_alive(m, cr3, rgva, hb0) == 0, "confirm not-yet-alive"); CHECK(vgpup_confirm_alive(m, cr3, rgva, hb0) == 0, "confirm not-yet-alive");
@@ -190,56 +199,55 @@ static void run_flat_smoke(void)
CHECK(vgpup_confirm_alive(m, cr3, rgva, hb0) == 1, "confirm alive after tick"); CHECK(vgpup_confirm_alive(m, cr3, rgva, hb0) == 1, "confirm alive after tick");
} }
/* open handle + read API */ /* construct a handle directly (the proc_list/win32 path is not unit-testable;
* proc_cr3 is the synthetic cr3 here) and exercise the read API + control seam */
{ {
vgpup_region* r = vgpup_open(m, cr3); vgpup_region rr;
CHECK(r != NULL, "vgpup_open"); vgpup_region* r = &rr;
if (r) { uint8_t* dst = malloc(frame_bytes);
uint8_t* dst = malloc(frame_bytes); vgpup_frame_info fi;
vgpup_frame_info fi; vgpup_cursor cur;
vgpup_cursor cur; vgpup_geometry geo;
vgpup_geometry geo; vgpup_status st;
vgpup_status st; int rc;
int rc;
CHECK(dst != NULL, "malloc dst"); memset(&rr, 0, sizeof rr);
rr.proc_cr3 = cr3;
rr.region_gva = base_va;
rr.ctrl_gva = base_va + VGPU_CONTROL_OFFSET;
rr.ring_gva = base_va + VGPU_RING_OFFSET;
rc = vgpup_sample_frame(r, m, cr3, dst, frame_bytes, &fi); CHECK(dst != NULL, "malloc dst");
CHECK(rc == 1, "sample_frame fresh");
if (rc == 1) {
CHECK(fi.desc.width == w && fi.desc.height == h, "sample dims");
CHECK(fi.bytes == frame_bytes, "sample bytes");
CHECK(dst[0] == marker && dst[frame_bytes - 1] == marker, "sample content");
}
/* same frame_id → no fresh frame (dedup) */ rc = vgpup_sample_frame(r, m, dst, frame_bytes, &fi);
CHECK(vgpup_sample_frame(r, m, cr3, dst, frame_bytes, &fi) == 0, "sample dedup"); CHECK(rc == 1, "sample_frame fresh");
if (rc == 1) {
/* too-small buffer → lossy drop (0), not error */ CHECK(fi.desc.width == w && fi.desc.height == h, "sample dims");
CHECK(vgpup_sample_frame(r, m, cr3, dst, 1, &fi) == 0, "sample tiny-cap"); CHECK(fi.bytes == frame_bytes, "sample bytes");
CHECK(dst[0] == marker && dst[frame_bytes - 1] == marker, "sample content");
CHECK(vgpup_read_cursor(r, m, cr3, &cur) == 1, "read_cursor");
CHECK(vgpup_read_geometry(r, m, cr3, &geo) == 1, "read_geometry");
CHECK(vgpup_read_status(r, m, cr3, &st) == 0, "read_status");
CHECK(st.status == VGPU_ST_CAPTURING, "status value");
CHECK(st.heartbeat == 43, "status heartbeat");
CHECK(vgpup_run_epoch(r) == st.run_epoch, "run_epoch accessor");
free(dst);
vgpup_close(r);
} }
}
/* control-write seam: builds frame + offsets, writes nothing */ /* same frame_id → no fresh frame (dedup) */
{ CHECK(vgpup_sample_frame(r, m, dst, frame_bytes, &fi) == 0, "sample dedup");
vgpup_region* r = vgpup_open(m, cr3);
if (r) { /* too-small buffer → lossy drop (0), not error */
CHECK(vgpup_sample_frame(r, m, dst, 1, &fi) == 0, "sample tiny-cap");
CHECK(vgpup_read_cursor(r, m, &cur) == 1, "read_cursor");
CHECK(vgpup_read_geometry(r, m, &geo) == 1, "read_geometry");
CHECK(vgpup_read_status(r, m, &st) == 0, "read_status");
CHECK(st.status == VGPU_ST_CAPTURING, "status value");
CHECK(st.heartbeat == 43, "status heartbeat");
CHECK(vgpup_run_epoch(r) == st.run_epoch, "run_epoch accessor");
/* control-write seam: builds frame + offsets, writes nothing */
{
vgpup_control_intent in = { VGPU_CMD_RUN, 60, 1, 7 }; vgpup_control_intent in = { VGPU_CMD_RUN, 60, 1, 7 };
vgpu_control_t frame; vgpu_control_t frame;
uint64_t ctrl_gva = 0; uint64_t ctrl_gva = 0;
uint32_t off = 0, len = 0; uint32_t off = 0, len = 0;
int rc = vgpup_build_control_write(r, &in, &frame, &ctrl_gva, &off, &len); int crc = vgpup_build_control_write(r, &in, &frame, &ctrl_gva, &off, &len);
CHECK(rc == 0, "build_control_write rc"); CHECK(crc == 0, "build_control_write rc");
CHECK(frame.desired_state == VGPU_CMD_RUN, "control desired_state"); CHECK(frame.desired_state == VGPU_CMD_RUN, "control desired_state");
CHECK(frame.target_fps == 60, "control target_fps"); CHECK(frame.target_fps == 60, "control target_fps");
CHECK(frame.full_frame_req == 7, "control full_frame_req"); CHECK(frame.full_frame_req == 7, "control full_frame_req");
@@ -248,8 +256,9 @@ static void run_flat_smoke(void)
CHECK(off == offsetof(vgpu_control_t, desired_state), "control off"); CHECK(off == offsetof(vgpu_control_t, desired_state), "control off");
CHECK(len == offsetof(vgpu_control_t, full_frame_req) + sizeof(uint32_t) CHECK(len == offsetof(vgpu_control_t, full_frame_req) + sizeof(uint32_t)
- offsetof(vgpu_control_t, desired_state), "control len"); - offsetof(vgpu_control_t, desired_state), "control len");
vgpup_close(r);
} }
free(dst);
} }
vmie_mem_close(m); /* the TEST owns vmie_mem here (it is the caller) */ vmie_mem_close(m); /* the TEST owns vmie_mem here (it is the caller) */
+30 -4
View File
@@ -106,14 +106,40 @@ int vgpu_region_create(vgpu_region_t* out) {
} }
uintptr_t addr = (uintptr_t)p; uintptr_t addr = (uintptr_t)p;
uintptr_t aligned = (addr + VGPU_2MB - 1) & ~(uintptr_t)(VGPU_2MB - 1); uintptr_t aligned = (addr + VGPU_2MB - 1) & ~(uintptr_t)(VGPU_2MB - 1);
if (!VirtualLock((void*)aligned, (SIZE_T)bytes)) {
fprintf(stderr, "region: VirtualLock failed (%lu) — pages may not be pinned\n", /* The region must be RESIDENT, not merely committed: the host reads it out
GetLastError()); * of guest RAM and only PRESENT pages are visible to it — a committed but
* demand-zero page has no PTE, so it is unreadable from the host. VirtualLock
* pins the pages into the working set, but it can lock at most the process
* MINIMUM working set, and the default quota is far below the region size
* (so a bare VirtualLock fails with ERROR_WORKING_SET_QUOTA). Raise the
* minimum first. NB: VirtualLock / SetProcessWorkingSetSize do NOT need
* SE_LOCK_MEMORY — that privilege is only for large pages / AWE. */
SIZE_T ws_min = (SIZE_T)(bytes + 64ull * 1024 * 1024); /* region + headroom */
SIZE_T ws_max = ws_min + 128ull * 1024 * 1024;
SIZE_T cur_min = 0, cur_max = 0;
if (GetProcessWorkingSetSize(GetCurrentProcess(), &cur_min, &cur_max)) {
if (cur_min > ws_min) ws_min = cur_min; /* never shrink an existing quota */
if (cur_max > ws_max) ws_max = cur_max;
} }
if (!SetProcessWorkingSetSize(GetCurrentProcess(), ws_min, ws_max))
fprintf(stderr, "region: SetProcessWorkingSetSize(%llu MiB) failed (%lu)\n",
(unsigned long long)(ws_min / (1024 * 1024)), GetLastError());
if (!VirtualLock((void*)aligned, (SIZE_T)bytes)) {
fprintf(stderr, "region: VirtualLock failed (%lu) — pre-faulting region\n",
GetLastError());
/* Last resort: fault every page so it is at least PRESENT now. Without
* the lock the trimmer may evict it under pressure, but the raised
* minimum working set above makes eviction far less likely. */
volatile uint8_t* q = (volatile uint8_t*)aligned;
for (uint64_t off = 0; off < bytes; off += 4096u) q[off] = q[off];
}
os_base = p; os_base = p;
base = (uint8_t*)aligned; base = (uint8_t*)aligned;
os_total = total; os_total = total;
fprintf(stderr, "region: fallback VirtualAlloc+VirtualLock %llu MiB, aligned at %p\n", fprintf(stderr, "region: fallback VirtualAlloc+lock %llu MiB, aligned at %p\n",
(unsigned long long)(bytes / (1024 * 1024)), (void*)aligned); (unsigned long long)(bytes / (1024 * 1024)), (void*)aligned);
} }