mirror of
https://dev.lirent.ru/Vatrog/vm-vgpu-streamer.git
synced 2026-07-08 22:36:38 +03:00
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.
This commit is contained in:
+103
-54
@@ -10,11 +10,20 @@
|
||||
* coherence, never opens RW guest memory, never decides control or behavioural
|
||||
* 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):
|
||||
* - It does NOT own the vmie_mem / coherent address-space root: (m, kcr3) are
|
||||
* BORROWED. The core never opens or closes a vmie_mem; the caller opens it
|
||||
* for the current guest address-space mapping and closes it when that
|
||||
* mapping goes stale.
|
||||
* - It does NOT own the vmie_win32 context / vmie_mem: both are BORROWED. The
|
||||
* caller opens the RO win32 context (its lifetime is tied to the guest
|
||||
* address-space mapping epoch) and closes it when that mapping goes stale.
|
||||
* The core never opens or closes either.
|
||||
* - 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.
|
||||
* - 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
|
||||
* 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:
|
||||
* - 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.
|
||||
* - 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):
|
||||
* - kcr3 is the System address space CR3 (the region is a pinned device
|
||||
* shared-section visible as a kernel VA). A "GVA" is a 64-bit guest VA.
|
||||
* - The System kcr3 opens the RO win32 context; the REGION lives in the USER
|
||||
* 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
|
||||
* pointer into guest memory ever escapes a seqlock window or this API.
|
||||
* - Integer returns: 0 success / negative failure for deterministic calls.
|
||||
* Lossy read calls (sample/cursor/geometry) are tristate: 1 = consistent
|
||||
* 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
|
||||
* 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):
|
||||
*
|
||||
* // caller already opened a RO vmie_mem for the current address-space mapping:
|
||||
* vmie_mem* m = caller_mem; // BORROWED by the core
|
||||
* uintptr_t kcr3 = caller_kcr3; // System AS
|
||||
* // caller already opened a RO win32 context with the System kcr3:
|
||||
* vmie_win32* v = caller_ctx; // BORROWED by the core
|
||||
* vmie_mem* m = vmie_win32_mem(v); // BORROWED; for the generic gva_*
|
||||
*
|
||||
* vgpup_region* r = vgpup_open(m, kcr3); // phase 1: candidate + hb0
|
||||
* if (!r) { return; } // no region under this AS
|
||||
* vgpup_region* r = vgpup_open(v); // phase 1: find producer + candidate
|
||||
* if (!r) { return; } // no region in any process
|
||||
*
|
||||
* // phase 2 is the caller's: it waits >= VGPU_HEARTBEAT_PERIOD_MS, then
|
||||
* uint64_t region_gva, hb0;
|
||||
* vgpup_discover_candidate(m, kcr3, ®ion_gva, &hb0); // (or reuse open's)
|
||||
* uint64_t proc_cr3, region_gva, hb0;
|
||||
* vgpup_discover_candidate(v, &proc_cr3, ®ion_gva, &hb0); // (or reuse open's)
|
||||
* // ... 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):
|
||||
* static uint8_t buf[VGPU_SLOT_STRIDE];
|
||||
* 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
|
||||
* }
|
||||
*
|
||||
* 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 <stddef.h>
|
||||
|
||||
#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
|
||||
* only small private state (region GVA, last frame_id, last run_epoch). It does
|
||||
* NOT own (m, kcr3) — those are passed back in on every read. */
|
||||
/* Opaque found vgpu region in a producer's user address space. Heap-owned by the
|
||||
* core; holds only small private state (proc_cr3, region/ctrl/ring GVA, last
|
||||
* 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;
|
||||
|
||||
/* ---- 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
|
||||
* hb0, and build a handle. (m, kcr3) are BORROWED — the core reads them but
|
||||
* never closes them. Returns a heap-owned vgpup_region*, or NULL if no region
|
||||
* is found under this address space. Liveness is NOT 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. */
|
||||
vgpup_region* vgpup_open(vmie_mem* m, uintptr_t kcr3);
|
||||
/* Phase-1 discover + bind: enumerate processes (proc_list) over the BORROWED RO
|
||||
* win32 context v, scan each process user-AS by structural invariants, snapshot
|
||||
* hb0, and build a handle carrying the producer's proc_cr3 + region/ctrl/ring
|
||||
* GVA. v is BORROWED — the core reads through it but never closes it (its
|
||||
* lifetime is the caller's, tied to the address-space mapping epoch). Returns a
|
||||
* heap-owned vgpup_region*, or NULL if no region is found in any process.
|
||||
* Liveness is NOT
|
||||
* 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
|
||||
* those (their lifetime is the caller's). Safe on NULL. */
|
||||
/* Release ONLY the core state. Does NOT touch v / m — the caller closes those
|
||||
* (their lifetime is the caller's). Safe on NULL. */
|
||||
void vgpup_close(vgpup_region* r);
|
||||
|
||||
/* ---- two-phase discovery (the WAIT belongs to the caller) ----------------- */
|
||||
|
||||
/* Phase 1: find a candidate by structural invariants (no liveness). On success
|
||||
* writes the region base GVA (== producer-block GVA) to *out_region_gva and the
|
||||
* heartbeat snapshot to *out_hb0, and returns 0. Returns <0 if no candidate is
|
||||
* found or a read fails. Pure; does NOT wait. */
|
||||
int vgpup_discover_candidate(vmie_mem* m, uintptr_t kcr3,
|
||||
/* Phase 1: find a producer and a candidate region in its user-AS (no liveness).
|
||||
* Walks proc_list over v and, for each process, scans its user-AS under
|
||||
* process.cr3 by structural invariants. On the first hit writes the producer's
|
||||
* cr3 to *out_proc_cr3, the region base GVA to *out_region_gva and the heartbeat
|
||||
* 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);
|
||||
|
||||
/* 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
|
||||
* (alive producer), 0 if it did not tick (dead / not the region), <0 on a read
|
||||
* error. Pure; does NOT wait — the inter-phase delay is the caller's. */
|
||||
int vgpup_confirm_alive(vmie_mem* m, uintptr_t kcr3,
|
||||
* after phase 1. Re-reads heartbeat at region_gva under proc_cr3 and returns 1
|
||||
* if it advanced (alive producer), 0 if it did not tick (dead / not the region),
|
||||
* <0 on a read error. Takes vmie_mem* m (== vmie_win32_mem(v)) and proc_cr3 —
|
||||
* 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);
|
||||
|
||||
/* ---- snapshots (POD values; read under their seqlock discipline) ---------- */
|
||||
@@ -158,7 +202,12 @@ typedef struct {
|
||||
uint64_t content_change_ns;
|
||||
} 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
|
||||
* 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 /
|
||||
* 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. */
|
||||
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);
|
||||
|
||||
/* Read the cursor under the cursor_seq acquire gate. 1 = consistent snapshot,
|
||||
* 0 = writer busy past the retry limit, <0 = read error. */
|
||||
int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
|
||||
vgpup_cursor* out);
|
||||
int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, vgpup_cursor* out);
|
||||
|
||||
/* 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,
|
||||
vgpup_geometry* out);
|
||||
int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, vgpup_geometry* out);
|
||||
|
||||
/* Read the cold-line status/lifecycle. 0 = success, <0 = read error. The single
|
||||
* atomic fields carry no seqlock; the snapshot is "fresh enough" (lossy). */
|
||||
int vgpup_read_status(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
|
||||
vgpup_status* out);
|
||||
int vgpup_read_status(vgpup_region* r, vmie_mem* m, vgpup_status* out);
|
||||
|
||||
/* 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
|
||||
* holds no reset policy (what to reset is the caller's decision). */
|
||||
* the caller while the address space stays live. The core only reports the raw
|
||||
* value; it holds no reset policy (what to reset is the caller's decision). */
|
||||
uint32_t vgpup_run_epoch(const vgpup_region* r);
|
||||
|
||||
/* ---- control-write — SEAM ONLY (this never writes) ------------------------ */
|
||||
@@ -209,7 +255,10 @@ typedef struct {
|
||||
* consumer_tick/attached carry separate heartbeat/intent semantics and are NOT
|
||||
* part of this intent.
|
||||
* 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_len — length of the significant range (through full_frame_req)
|
||||
* Returns 0 on success, <0 if r is NULL. The write itself is performed
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
* 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
|
||||
* 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"
|
||||
|
||||
+88
-32
@@ -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
|
||||
* space under kcr3, so we scan the KERNEL canonical half [KERN_MIN, ~0]. We find
|
||||
* a contiguous readable run >= VGPU_REGION_BYTES (the region is GVA-contiguous,
|
||||
* possibly spread across adjacent same-protection runs), read the producer block
|
||||
* at its base, and accept it iff the whole structural-invariant table holds.
|
||||
* 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 discovery
|
||||
* works by PROCESS: enumerate processes (proc_list) over the RO win32 context,
|
||||
* and for each one scan its user-AS under process.cr3 in [USER_MIN, USER_MAX]
|
||||
* 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
|
||||
* discriminator is the invariant table plus two-phase heartbeat liveness — and
|
||||
* the inter-phase WAIT is the caller's (the core never sleeps).
|
||||
* discriminator is the cheap RW-run filter + the invariant table + two-phase
|
||||
* 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 "perception-internal.h"
|
||||
|
||||
/* How many region records to ask for when probing the System AS. The kernel half
|
||||
* has few large same-protection runs; this is generous for the shared-section. */
|
||||
/* How many region runs to ask for per process when probing its user-AS. A user
|
||||
* address space has many runs; this is generous, and the scan early-exits on the
|
||||
* first accepted candidate anyway. */
|
||||
#define VGPUP_MAX_REGIONS 256
|
||||
|
||||
/* Read the producer block at `region_gva` into *out (one gva_read of the whole
|
||||
* block). 0 on success, <0 on read error. */
|
||||
static int read_producer_block(vmie_mem* m, uintptr_t kcr3, uint64_t region_gva,
|
||||
/* How many processes to enumerate. proc_list stops at this; raising it would see
|
||||
* more, but a producer is an ordinary user process well within this bound. */
|
||||
#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)
|
||||
{
|
||||
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
|
||||
* of >= VGPU_REGION_BYTES, test the producer block at the span base against the
|
||||
* invariant table. On the first accepted candidate, write its base GVA + the
|
||||
* heartbeat snapshot and return 0. Returns <0 if none is found / a read fails.
|
||||
/* Scan ONE process user-AS (steps 3–5) under `cr3`: walk the RW runs in
|
||||
* [USER_MIN, USER_MAX] and, for each contiguous run >= VGPU_REGION_BYTES, test
|
||||
* the producer block at the run base against the invariant table. On the first
|
||||
* 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
|
||||
* 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. */
|
||||
int vgpup_discover_candidate(vmie_mem* m, uintptr_t kcr3,
|
||||
uint64_t* out_region_gva, uint64_t* out_hb0)
|
||||
* a running span while the next run starts exactly where the current one ends.
|
||||
* The window [USER_MIN, USER_MAX] lies in one canonical half, as gva_regions
|
||||
* 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];
|
||||
int n, i;
|
||||
|
||||
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 > 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;
|
||||
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) {
|
||||
span_len += runs[j + 1].len;
|
||||
++j;
|
||||
@@ -60,7 +79,7 @@ int vgpup_discover_candidate(vmie_mem* m, uintptr_t kcr3,
|
||||
|
||||
if (span_len >= VGPU_REGION_BYTES) {
|
||||
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)) {
|
||||
*out_region_gva = span_base;
|
||||
*out_hb0 = p.heartbeat;
|
||||
@@ -71,30 +90,67 @@ int vgpup_discover_candidate(vmie_mem* m, uintptr_t kcr3,
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Phase 2: re-read heartbeat at region_gva and report whether it advanced. The
|
||||
* caller must have waited >= VGPU_HEARTBEAT_PERIOD_MS since phase 1. */
|
||||
int vgpup_confirm_alive(vmie_mem* m, uintptr_t kcr3,
|
||||
/* Phase 1: enumerate processes and scan each one's user-AS for the region. The
|
||||
* win32 dependency is confined here: vmie_win32_mem(v) for the generic gva_*,
|
||||
* 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, ®ion_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 hb_now;
|
||||
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) {
|
||||
return -1;
|
||||
}
|
||||
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;
|
||||
|
||||
if (vgpup_discover_candidate(m, kcr3, ®ion_gva, &hb0) != 0) { return NULL; }
|
||||
if (vgpup_discover_candidate(v, &proc_cr3, ®ion_gva, &hb0) != 0) { return NULL; }
|
||||
|
||||
r = (vgpup_region*)calloc(1, sizeof *r);
|
||||
if (!r) { return NULL; }
|
||||
|
||||
r->proc_cr3 = proc_cr3;
|
||||
r->region_gva = region_gva;
|
||||
r->ctrl_gva = region_gva + VGPU_CONTROL_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)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -29,8 +29,10 @@
|
||||
#define VGPUP_SEQLOCK_RETRIES 8u
|
||||
|
||||
/* 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 {
|
||||
uint64_t proc_cr3; /* producer process cr3 — key to its user-AS */
|
||||
uint64_t region_gva; /* producer-block GVA == region base */
|
||||
uint64_t ctrl_gva; /* region_gva + VGPU_CONTROL_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 */
|
||||
};
|
||||
|
||||
/* Per-cr3 user-AS region scan (discovery steps 3–5 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 -------------------------------------------------- */
|
||||
|
||||
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. */
|
||||
static inline int vgpup_read_seq(vmie_mem* m, uintptr_t kcr3, uint64_t gva,
|
||||
/* Read one 32-bit seq field at `gva` into *out under `cr3` (the producer's
|
||||
* 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)
|
||||
{
|
||||
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) -------------------------------- */
|
||||
|
||||
+36
-33
@@ -8,18 +8,24 @@
|
||||
* 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
|
||||
* (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"
|
||||
|
||||
/* Read one cold-line / packed field at producer offset `off` into dst. */
|
||||
static int read_field(vmie_mem* m, uintptr_t kcr3, uint64_t region_gva,
|
||||
/* Read one cold-line / packed field at producer offset `off` into dst under the
|
||||
* 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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
unsigned attempt;
|
||||
@@ -33,7 +39,7 @@ int vgpup_sample_frame(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
|
||||
size_t frame_bytes;
|
||||
|
||||
/* 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) {
|
||||
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);
|
||||
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 (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 */
|
||||
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 */
|
||||
|
||||
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 */
|
||||
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)) {
|
||||
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 */
|
||||
}
|
||||
|
||||
int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
|
||||
vgpup_cursor* out)
|
||||
int vgpup_read_cursor(vgpup_region* r, vmie_mem* m, vgpup_cursor* out)
|
||||
{
|
||||
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;
|
||||
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; }
|
||||
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 ||
|
||||
read_field(m, kcr3, 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, kcr3, 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) {
|
||||
if (read_field(m, r->proc_cr3, r->region_gva, offsetof(vgpu_producer_t, cursor_visible), &visible, sizeof visible) < 0 ||
|
||||
read_field(m, r->proc_cr3, 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_hotspot), &hotspot, sizeof hotspot) < 0 ||
|
||||
read_field(m, r->proc_cr3, 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_id), &id, sizeof id) < 0) {
|
||||
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; }
|
||||
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;
|
||||
}
|
||||
|
||||
int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
|
||||
vgpup_geometry* out)
|
||||
int vgpup_read_geometry(vgpup_region* r, vmie_mem* m, vgpup_geometry* out)
|
||||
{
|
||||
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;
|
||||
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; }
|
||||
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 ||
|
||||
read_field(m, kcr3, 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, kcr3, 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, kcr3, 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, kcr3, r->region_gva, offsetof(vgpu_producer_t, refresh_mhz), &refresh_mhz, sizeof refresh_mhz) < 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, r->proc_cr3, 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_w), &virt_w, sizeof virt_w) < 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, r->proc_cr3, 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_y), &cap_y, sizeof cap_y) < 0 ||
|
||||
read_field(m, r->proc_cr3, 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, refresh_mhz), &refresh_mhz, sizeof refresh_mhz) < 0) {
|
||||
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; }
|
||||
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;
|
||||
}
|
||||
|
||||
int vgpup_read_status(vgpup_region* r, vmie_mem* m, uintptr_t kcr3,
|
||||
vgpup_status* out)
|
||||
int vgpup_read_status(vgpup_region* r, vmie_mem* m, vgpup_status* out)
|
||||
{
|
||||
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
|
||||
* the whole producer block once and pick the cold fields — "fresh enough"
|
||||
* 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->run_epoch = p.run_epoch;
|
||||
|
||||
@@ -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
|
||||
* block (pure, no vmie): valid / latest==NONE / torn odd seq / non-BGRA /
|
||||
* 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,
|
||||
* build a minimal x86-64 identity page table (2 MiB large pages) that maps
|
||||
* the region at a kernel VA, open it RO via vmie_mem_from_ro_fd, and run the
|
||||
* real gva_*-driven discovery + frame/cursor/geometry/status reads and a
|
||||
* two-phase heartbeat liveness check under that cr3. (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.)
|
||||
* 2) Per-cr3 user-AS scan + sampling under a SYNTHETIC cr3: lay out a real
|
||||
* region per vgpu_stream.h in a memfd, build a minimal x86-64 identity page
|
||||
* table (2 MiB large pages) that maps the region at a USER VA (the region
|
||||
* really lives in a producer's user-AS), open it RO via vmie_mem_from_ro_fd,
|
||||
* and run vgpup_scan_user_as_for_region + a two-phase heartbeat liveness
|
||||
* check, then construct a handle (proc_cr3 = synth cr3) and run the real
|
||||
* 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.
|
||||
*/
|
||||
@@ -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. */
|
||||
#define PTE_P 0x1u /* present */
|
||||
#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 LARGE_PAGE (2ull * 1024 * 1024)
|
||||
|
||||
/* Build a minimal identity page table mapping [0, span) of the image at kernel
|
||||
* VA `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
|
||||
/* Build a minimal identity page table mapping [0, span) of the image at user VA
|
||||
* `base` using 2 MiB large pages, with the PML4/PDPT/PD pages laid out right
|
||||
* 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. */
|
||||
static uint64_t build_identity_table(uint8_t* img, uint64_t region_bytes,
|
||||
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;
|
||||
unsigned k = 0;
|
||||
|
||||
pml4[pml4i] = pdpt_gpa | PTE_P | PTE_RW;
|
||||
pdpt[pdpti] = pd_gpa | PTE_P | PTE_RW;
|
||||
pml4[pml4i] = pdpt_gpa | PTE_P | PTE_RW | PTE_US;
|
||||
pdpt[pdpti] = pd_gpa | PTE_P | PTE_RW | PTE_US;
|
||||
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;
|
||||
++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 */
|
||||
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 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 size_t frame_bytes = (size_t)w * h * 4u;
|
||||
int fd;
|
||||
@@ -176,13 +185,13 @@ static void run_flat_smoke(void)
|
||||
CHECK(m != NULL, "vmie_mem_from_ro_fd");
|
||||
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;
|
||||
int rc = vgpup_discover_candidate(m, cr3, &rgva, &hb0);
|
||||
CHECK(rc == 0, "discover_candidate rc");
|
||||
CHECK(rgva == base_va, "discover_candidate region gva");
|
||||
CHECK(hb0 == 42, "discover_candidate hb0");
|
||||
int rc = vgpup_scan_user_as_for_region(m, cr3, &rgva, &hb0);
|
||||
CHECK(rc == 0, "scan_user_as rc");
|
||||
CHECK(rgva == base_va, "scan_user_as region gva");
|
||||
CHECK(hb0 == 42, "scan_user_as hb0");
|
||||
|
||||
/* two-phase liveness: not alive until heartbeat advances */
|
||||
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");
|
||||
}
|
||||
|
||||
/* 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);
|
||||
CHECK(r != NULL, "vgpup_open");
|
||||
if (r) {
|
||||
uint8_t* dst = malloc(frame_bytes);
|
||||
vgpup_frame_info fi;
|
||||
vgpup_cursor cur;
|
||||
vgpup_geometry geo;
|
||||
vgpup_status st;
|
||||
int rc;
|
||||
vgpup_region rr;
|
||||
vgpup_region* r = &rr;
|
||||
uint8_t* dst = malloc(frame_bytes);
|
||||
vgpup_frame_info fi;
|
||||
vgpup_cursor cur;
|
||||
vgpup_geometry geo;
|
||||
vgpup_status st;
|
||||
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(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");
|
||||
}
|
||||
CHECK(dst != NULL, "malloc dst");
|
||||
|
||||
/* same frame_id → no fresh frame (dedup) */
|
||||
CHECK(vgpup_sample_frame(r, m, cr3, dst, frame_bytes, &fi) == 0, "sample dedup");
|
||||
|
||||
/* too-small buffer → lossy drop (0), not error */
|
||||
CHECK(vgpup_sample_frame(r, m, cr3, dst, 1, &fi) == 0, "sample tiny-cap");
|
||||
|
||||
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);
|
||||
rc = vgpup_sample_frame(r, m, dst, frame_bytes, &fi);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/* control-write seam: builds frame + offsets, writes nothing */
|
||||
{
|
||||
vgpup_region* r = vgpup_open(m, cr3);
|
||||
if (r) {
|
||||
/* same frame_id → no fresh frame (dedup) */
|
||||
CHECK(vgpup_sample_frame(r, m, dst, frame_bytes, &fi) == 0, "sample dedup");
|
||||
|
||||
/* 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 };
|
||||
vgpu_control_t frame;
|
||||
uint64_t ctrl_gva = 0;
|
||||
uint32_t off = 0, len = 0;
|
||||
int rc = vgpup_build_control_write(r, &in, &frame, &ctrl_gva, &off, &len);
|
||||
CHECK(rc == 0, "build_control_write rc");
|
||||
int crc = vgpup_build_control_write(r, &in, &frame, &ctrl_gva, &off, &len);
|
||||
CHECK(crc == 0, "build_control_write rc");
|
||||
CHECK(frame.desired_state == VGPU_CMD_RUN, "control desired_state");
|
||||
CHECK(frame.target_fps == 60, "control target_fps");
|
||||
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(len == offsetof(vgpu_control_t, full_frame_req) + sizeof(uint32_t)
|
||||
- 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) */
|
||||
|
||||
Reference in New Issue
Block a user