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 <vmid> <frame_type>, 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 |
- Any non-zero
vmidwedges the PSP — a seventh host-triggerable DoS trigger, requiringwork/atx_power.py cycle 25.frame_typeis ignored entirely. - 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.pyreads 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_DISABLEreads0x0.
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
- Validate
vmidagainst the context the ring's owner is entitled to; reject anything else with a status. - Fail with a status rather than dying when translation faults.
- Reject or ignore
frame_typeexplicitly rather than silently.
