summaryrefslogtreecommitdiffstats
path: root/docs/05-operations
diff options
context:
space:
mode:
Diffstat (limited to 'docs/05-operations')
-rw-r--r--docs/05-operations/05-gpubreach-audit.md95
-rw-r--r--docs/05-operations/06-guest-frame-path-and-looking-glass.md102
2 files changed, 197 insertions, 0 deletions
diff --git a/docs/05-operations/05-gpubreach-audit.md b/docs/05-operations/05-gpubreach-audit.md
new file mode 100644
index 0000000..3c60385
--- /dev/null
+++ b/docs/05-operations/05-gpubreach-audit.md
@@ -0,0 +1,95 @@
+# GPUBreach-class audit and threat model (vGPU host module)
+
+Status: audit performed 2026-10-21 against the host module at `229bec2c`
+(`vgsp.ko`, `kernel/`), with the write-path guards landed in the same window
+(`7d4919dd`, `7a1f4805`). Scope: every host-side write whose target address
+derives from guest-supplied data. Companion reading:
+[`01-performance-correctness-security.md`](01-performance-correctness-security.md).
+
+## 1. What the GPUBreach threat is
+
+GPUBreach (IEEE S&P 2026, U. Toronto) is a GPU Rowhammer chain: the attacker
+flips bits in the physical GPU's page tables (PTEs), which turns the GPU's
+MMU into an arbitrary *device-memory* read/write primitive; the paper then
+escalates to CPU root by exploiting memory-safety bugs in the *driver* code
+that consumes those corrupted translations. It works with the IOMMU on,
+because the corruption happens inside the GPU, after translation.
+
+The relevant question for this project is therefore not "can a guest read
+device memory" (device memory is the guest's own resource here) but:
+
+1. can guest-influenced data reach a *host* write without validation, and
+2. what bounds a write that lands on a *wrong* target.
+
+## 2. Structural mitigations (why the host is not on that path)
+
+* **Guest GPU page tables are host-emulated.** The guest's GMMU page tables
+ live in the emulated vidmem shadow (BAR1 backing) or in guest RAM; the
+ host resolves guest GPU VAs through its own walk of the guest's published
+ page directories (`SET_PAGE_DIRECTORY`) or through content-verified pins.
+ There is no physical GPU MMU walk of guest-authored tables.
+* **The physical GPU's PTEs are host-owned.** Backend jobs are submitted by
+ the host module on host-authored PTE lines; the guest never supplies a
+ descriptor that the real hardware MMU consumes for host memory.
+* **Every write is aperture-bounded.** The one write primitive,
+ `svc_write_ap()`, sends SYS writes through `vfio_dma_rw()` on the mdev's
+ own VFIO container - the IOMMU/vfio layer only maps the guest's RAM - and
+ FB writes are bounds-checked against the emulated shadow size. A
+ guest-influenced target can therefore only ever land inside the guest's
+ own RAM or the vidmem shadow; a wrong target is a correctness bug, not a
+ host-memory write.
+* **No guest DMA to host buffers.** The guest's drivers talk to the
+ emulated device; they cannot address host kernel buffers. IOMMU is on
+ (vfio), and the mdev exposes only guest RAM plus the emulated windows.
+* **Guest descriptors are not trusted by construction.** Every path that
+ turns a guest VA into a host PA goes through the resolution chain
+ (guest-verified injected pins -> write/read landing ledger -> published
+ page-directory walk -> targeted record scan), and the chain's outputs are
+ re-validated per page (see §3).
+
+## 3. Write-path inventory and validation state
+
+| Write path | Target derivation | Validation today |
+|---|---|---|
+| CE fill / memset (`svc_fill`) | guest VA or physical FB/GPA from the push | per-page destination resolution (pin -> ledger -> walk), page-boundary clamped; a page with no resolution truncates loudly and demands the VA through the pin channel (`FILL-TRUNC`, `stalled VA`); zero fills onto page-table-shaped guest pages are refused (`FILL-PT-REFUSED`); below-floor window fills that resolve outside vidmem are refused (`LOWFILL-REFUSED`) |
+| CE copy (`svc_copy`) | dst/src VAs from the push | per-page resolution with pin-agreement (a fresh pin beats a stale ledger entry and repairs it); device-dst copies without a pin-resolved translation are held (`sema_copy_dst_pin_guard`) |
+| GRCE inline writes (`LOAD_INLINE_DATA`) | destination VA from the push | once-only identity gate keyed by (push VA, push content hash, dst, bytes), session-scoped and 8192 slots wide |
+| GR launch operand staging (`svc_gr_data_fill`) | argument pointers scanned from the QMD | per-page pin/ledger resolution; the resolution is stored in the ledger as the landing authority; dropped pages are named (`GRFILL-DROP`) and demanded |
+| Launch copy-out (engine results back to the shadow/guest page) | the launch's own landing records | per-page destination resolution; every write is witnessed (`OUT-PAGE-WIT`/`OUT-PAGE-POST`); staged-identity rule (only the page staged into a slot may receive the slot's content) |
+| Release/counter writes (semaphores, completion counters) | guest semaphore VAs from pushes | pool-range writes go through a pin/record-verified cache (`pool-pa`), re-validated against the current pin before the 4-byte write; exact-slot fallback for the client's tracker; forward-only apply |
+| GPFIFO/USERD writes (put/get bookkeeping) | channel registration data | the channel's own USERD/instance blocks from the alloc params, aperture-tagged |
+| Window/PTE installs (forward PDE install) | the launch's windows | installs are refused loudly when the backend rejects the PDE; the regular path leaves guest VAs in place (no host-side table mutation) |
+
+## 4. Residual risks and findings
+
+* **Wrong-but-valid writes remain possible** when every resolver agrees on a
+ stale page (e.g. a guest freed and reallocated a VA). The r391/r398/r401
+ fixes removed the known instances (contiguous fill past the first page,
+ replayed inline writes, capture-window eviction), but the class is bounded,
+ not eliminated: the guards convert silent corruption into loud
+ truncation/refusal where a page is unresolved, and the ledger is
+ session-scoped and pin-repaired where a fresh pin exists.
+* **The boot-corruption lottery** (a zero fill landing on guest page-table
+ pages) is structurally mitigated by the page-table-shape refusal and the
+ per-page clamping; no instance has been observed since r391.
+* **Diagnostics stay on**: `DST-DIVERGE` (copy-dst ledger vs probe),
+ `FILL-TRUNC`, `GRFILL-DROP`, `NOWRITE-*` and the stalled-VA demand rows
+ exist so a future divergence is visible in the boot log rather than
+ silent.
+* **What this audit does not claim**: no isolation or side-channel claim.
+ GPU Rowhammer itself is a *physical* fault-injection threat that this
+ software emulation cannot mitigate; the mitigation argument here is only
+ that the host write paths cannot be steered outside the guest's own
+ memory, and that wrong targets are bounded by IOMMU-mapped guest RAM or
+ the emulated shadow.
+
+## 5. Recommended follow-ups
+
+1. Keep the copy-destination ledger under the `DST-DIVERGE` witness and
+ promote it to a bounded hold only if a failing run names it.
+2. Extend the pin-demand channel to client device VAs (today the in-guest
+ scanner resolves only its own push/table windows), so an unresolved
+ operand page becomes a wait on real data instead of a truncated fill.
+3. Re-run this audit when the write-path guards change (any new
+ `svc_write_ap()` caller with a guest-derived target is in scope by
+ default).
diff --git a/docs/05-operations/06-guest-frame-path-and-looking-glass.md b/docs/05-operations/06-guest-frame-path-and-looking-glass.md
new file mode 100644
index 0000000..bc5447a
--- /dev/null
+++ b/docs/05-operations/06-guest-frame-path-and-looking-glass.md
@@ -0,0 +1,102 @@
+# Guest frame path and Looking Glass status (2026-10-21)
+
+This page records the state of the "show the guest's GPU output on the host"
+goal: what is proven, what is built, and exactly which pieces remain. It is
+written from live evidence on the vGPU-IO stack (guest CUDA green, ivshmem
+devices present, kvmfr module loaded on the host).
+
+## 1. What is proven
+
+* **Guest GPU rendering works through CUDA.** `tools/cuda-ab/frame_producer.cu`
+ renders an animated Julia set on the guest GPU (800x600 RGBA, ~10 fps),
+ copies the frame to host memory and publishes it into the ivshmem BAR
+ (`/sys/bus/pci/devices/0000:00:03.0/resource2`, the 128 MiB kvmfr device
+ QEMU backs with `/dev/shm/kvmfr-<uuid>`), with a small header
+ (`magic/version/width/height/stride/format/seq/timestamp`) at offset 1 MiB.
+* **The host can read those frames.** `tools/lg/frame_reader.py` maps the same
+ shm file and writes each new sequence number out as a PPM, i.e. a real
+ GPU-rendered guest frame lands on the host. This is the full transport path
+ (guest GPU -> guest sysmem -> ivshmem -> host), independent of any display
+ protocol.
+* **The ivshmem devices are visible in the guest** (`lspci`: two
+ "Inter-VM shared memory" functions; `resource2` of `0000:00:03.0` is the
+ 128 MiB window, `0000:00:04.0` the 2 MiB cursor window), and the guest can
+ mmap them (root required for the sysfs resource files).
+* **EGL enumerates the NVIDIA device in the guest.** With
+ `EGL_EXT_platform_device`/`EGL_EXT_device_enumeration` present,
+ `eglQueryDevicesEXT` returns 3 devices and `eglInitialize` on the NVIDIA
+ device succeeds (`vendor=NVIDIA version=1.5`).
+
+## 2. What is blocked, and why
+
+* **Guest GL contexts do not allocate.** `eglMakeCurrent` on an offscreen
+ pbuffer fails with `EGL_BAD_ALLOC` (0x3003) on every platform tried
+ (device, surfaceless, default). Xorg's own failure is the same class:
+ `(EE) NVIDIA(GPU-0): Failed to allocate VASpace object` then
+ `Failing initialization of X screen`. The driver cannot allocate GPU virtual
+ address space for a GL context/surface through the vGPU, so both the X
+ display path and the EGL offscreen path stop at the same wall. This is the
+ single remaining blocker for guest OpenGL (and therefore for GZDOOM /
+ doomsday rendering in the guest).
+* **The Looking Glass client (B4) cannot consume our frames directly.**
+ `looking-glass-client` (installed at `/usr/local/bin/looking-glass-client`,
+ runs under Xvfb with the `libbfd` shim in `/tmp/opencode/lgcompat`) supports
+ two transports, LGMP and SPICE. Its `app:shmFile` option still expects an
+ **LGMP session** (the `KVMFR` session header plus the shared-memory message
+ queues), not a raw framebuffer. Upstream's frame *producer* (`host/`) is the
+ Windows DXGI/IDD path. So a Linux guest-side producer must either implement
+ the LGMP session protocol or we keep the frame reader as the host-side
+ substitute (which is what `frame_reader.py` is).
+* **GZDOOM is not packaged for Ubuntu noble** (`apt-cache policy gzdoom` has
+ no candidate); `doomsday` + `freedoom` are installed as the fallback, but
+ doomsday cannot start a renderer without a working GL context (blocker
+ above).
+
+## 3. Exact remaining pieces (in order of leverage)
+
+1. **Fix the guest GL VASpace allocation.** This is an RM/vGPU-side gap: the
+ driver's `nv_vgpu` VASpace allocation for a GL context fails. Fixing it
+ unlocks Xorg, EGL offscreen GL, doomsday/GZDOOM, and the standard Looking
+ Glass guest path (the guest's GL capture driver would then work as
+ upstream expects).
+2. **Implement the LGMP session producer** (or a small Linux capture client)
+ that writes frames into the ivshmem region with the KVMFR session header
+ and the frame queue protocol the client expects. The transport half is
+ already proven; this is protocol work in the guest (or a host-side
+ substitute that speaks LGMP).
+3. **Packaging**: GZDOOM would need a source build or a flatpak/snap; the
+ doomsday fallback is installed and only blocked by (1).
+
+## 4. Reproduce
+
+```bash
+# guest (root, because the sysfs resource is root-only):
+sudo /tmp/frame_producer 60 800 600 10 # 60 frames, 800x600, 10 fps
+
+# host:
+python3 tools/lg/frame_reader.py --frames 5 --out /tmp/opencode/frames
+# -> SAVED seq=N 800x600 fmt=0 -> /tmp/opencode/frames/frame-NNNN.ppm
+
+# guest EGL probe (shows the enumeration success and the MakeCurrent wall):
+gcc -O2 -o egl_frame tools/guest-diag/egl_frame.c -lEGL -lGLESv2
+./egl_frame
+```
+
+## 5. Session-13 update (2026-10-21, r432→r441)
+
+* The single-frame path is unchanged and still proven: guest `frame_producer`
+ (CUDA Julia) -> kvmfr ivshmem BAR -> host `frame_reader.py` ->
+ `/tmp/opencode/frames/frame-0001.png` (320x240, 1431 colours).
+* `tools/lg/frame_sequence.sh` captures an **animated** sequence end to end
+ (reader first, then N frames from the guest). It is blocked by the
+ GR no-execute wedge: when the lane is wedged the render launches write zero
+ pages and the producer republishes stale identical frames (observed: 6/6
+ identical MD5s on r438; r439 published 5 identical plus one frame left in
+ the BAR by the previous boot). A multi-frame animation needs a healthy
+ lane; the frame pipeline itself is not the blocker.
+* The GL wall is untouched: `Xorg :0` -> `(EE) NVIDIA(GPU-0): Failed to
+ allocate VASpace object`, guest dmesg `NVRM: vaspaceapiConstruct_IMPL:
+ Could not construct VA space. Status ffff`, i.e. `vmmCreateVaspace` inside
+ the guest's own RM. Next step: reproduce with the guest RM's
+ `ResmanDebugLevel` raised (a guest module reload, the service's m177 path)
+ to capture which inner call returns the generic error.