# FINDING: the PSP ring frame's `vmid` byte is host-controlled, propagated into the PSP's own memory accesses, and unvalidated **Evidence class: [HW]** — Navi 31 / RX 7900 XTX, live sOS/SYS_DRV 0.49.0.54. **Status: host-controlled VMID propagation CONFIRMED. Cross-context access NOT achieved.** ## How this was reached The `fence_addr` finding opened the ring frame as a surface no earlier pass had examined. Sweeping all 16 dwords of `psp_gfx_rb_frame` at dword resolution (`hw/frame_sweep.py`) found the dword at **+32** kills the PSP. That offset is not reserved: | offset | field | |---|---| | +24 / +28 | `sid_lo` / `sid_hi` | | **+32 byte 0** | **`vmid`** | | +32 byte 1 | `frame_type` | | +36.. | `reserved2[7]` | ## Confirmed on hardware `hw/vmid_test.py `, one measurement per fresh boot: | frame setting | response | fence lands | heartbeat | |---|---|---|---| | `vmid` default or **0** | `0x0` | yes | advancing | | **`vmid` = 1, 8, 15, 255** | **TIMEOUT** | **no** | **frozen** | | `frame_type` = 1 or 255 | `0x0` | yes | advancing | 1. **Any non-zero `vmid` wedges the PSP** — a **seventh host-triggerable DoS trigger**, requiring `work/atx_power.py cycle 25`. `frame_type` is ignored entirely. 2. **The host-supplied value is propagated into the PSP's own memory accesses.** The interrupt log during the attempt shows VMC protection faults tagged with the host-supplied VMID, at the PSP's *own command buffer* address: ``` IH client=SOC21_IH_CLIENTID_VMC src=(0) vmid=1(1) pasid=0 ctx=[0x8000088, 0x40, 0x0, 0x0] ``` The PSP fetched its command through **VM context 1 because the frame said so**. Nothing validates the field, and the failure mode is death rather than an error status. ## Fault-level characterisation (strengthens point 2) `hw/vmid_fault.py` reads `MMVM_L2_PROTECTION_FAULT_STATUS` after a single attempt: | | control `vmid=0` | `vmid=1` | |---|---|---| | response | `0x0` | TIMEOUT | | `FAULT_STATUS` | **`0x0`** (no fault) | **`0x100d33`** | | `FAULT_ADDR_LO32` | `0x0` | **`0x8000088`** (= `0x8000088000`, the PSP's command buffer) | `0x100d33` decodes as a **read** fault with `MORE_FAULTS`, `WALKER_ERROR` and `MAPPING_ERROR` set. So the PSP issued a **read of its own command buffer through VM context 1**, and the page-table walk for that context failed. The control leaves the status register at zero, so the fault is attributable to the frame byte and nothing else. ## What was NOT achieved Configuring the MM hub's **VM context 1 with the same root page table as context 0** and flushing its TLB does **not** make `vmid=1` work (`hw/vmid_proof.py conf`: `vmid=0` succeeds, `vmid=1` still fails). Two obvious explanations are **ruled out** by measurement: * **the configuration did not apply** — `hw/vmid_ctx_verify.py` reads the registers back: after programming, context 1 matches context 0 exactly (`base=0x1`, `cntl=0x1fffe07`, `start=0`, `end=0xffffffff`) on the single MM hub (`mm_insts=[0]`, `vmhubs=1`); * **contexts 1-15 disabled at L2** — `regMMVM_CONTEXTS_DISABLE` reads **`0x0`**. So either the PSP's accesses are translated through a hub or context set other than the MM contexts, or something further is required that is not visible from these registers. **A controlled access under a host-chosen context was therefore not demonstrated, and no memory-corruption primitive follows from this on a bare-metal part.** A first version of this experiment did before/after inside one boot and reported "not a translation-context selector"; that verdict was **void** because one `vmid=1` attempt kills the PSP, so the "after" control inherited a dead PSP. Restructured to one measurement per boot. ## Why it still matters **Even a success here would not be a bare-metal primitive**, and that is worth stating plainly: the host already owns context 0's page tables and already chooses the addresses in the frame, so directing the PSP through a different host-owned context grants the host nothing it did not have. On a bare-metal part an unvalidated `vmid` is therefore only a DoS. The concerning shape is a **virtualized (SR-IOV) configuration**: a frame field naming the translation context, honoured by trusted firmware and unchecked, is the classic confused deputy — a guest naming another guest's context. **This is a hypothesis about virtualized parts, not a demonstrated result**; SR-IOV is not available here (already recorded as a blocker for the RLCV work). ## Suggested fixes 1. Validate `vmid` against the context the ring's owner is entitled to; reject anything else with a status. 2. Fail with a status rather than dying when translation faults. 3. Reject or ignore `frame_type` explicitly rather than silently.