aboutsummaryrefslogtreecommitdiffstats
path: root/fw/FINDING-PSP-RING-FRAME-VMID-UNVALIDATED.md
blob: 447015b5b991edfee9b4f9d1aaa6aee001671d26 (plain) (blame)
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.