1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
# 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 |
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.
|