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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
|
# CONFIRMED (DoS-class): TMR can be mapped over a PSP-protected 16 MB window on sOS/SYS_DRV <= 0.49.0.51
> **TITLE CORRECTION:** the original title said "CONFIRMED EXPLOITABLE". The base
> sweep later in this same file concludes *"the corruption upside does NOT
> materialise"* and that it is **DoS-class**. Title corrected to match the
> document's own evidence.
**End-to-end confirmed on hardware.** Static finding -> version-diff -> firmware
rollback -> differential hardware test. Identical request, two firmware versions,
opposite outcomes.
## The result
GFX ring cmd **5 (SETUP_TMR)** -> sOS `sysdrv_rpc_1006` -> SYS_DRV
`sysdrv_tee_map_shared_mem` (`0xE142A0`). Request is
`buf_phy_addr` (cmd buffer +28, 64-bit) and `buf_size` (+36).
```
identical request: buf_phy_addr = 0x0000FFFD_FB000000, size = 0x1300000
running 0.49.0.54 (C2PMSG_58 = 0x00310036) -> 0xffff0006 REJECTED
running 0.49.0.51 (C2PMSG_58 = 0x00310033) -> 0x0 ACCEPTED
```
The version register was read *at probe time* on each run, so there is no doubt
about which build produced each result.
### The guard, and that it is a real range check
`.54` `sysdrv_tee_map_shared_mem` calls `range_overlap_check` (`0xE212DC`), which
rejects any request overlapping two adjacent 8 MB regions, i.e.
**`[0x0000FFFD_FB000000, 0x0000FFFD_FC000000)` — 16 MB.**
Verified on stock `.54` that it is precisely a range test, not a blanket
rejection of that address space (size is 0x1300000 = 19 MB, so the base must
clear the window by that much):
| base | extent | `.54` result |
|---|---|---|
| `0xFFFD_FB000000` | overlaps window | **`0xffff0006` REJECTED** |
| `0xFFFD_F9000000` | ends `FA300000`, below | **`0x0` ACCEPTED** |
| `0xFFFD_FC000000` | starts at window top | **`0x0` ACCEPTED** |
On `.51` both the overlapping and non-overlapping bases return `0x0`.
## Why it is reachable
Firmware rollback needs no SPI access (see
`CONFIRMED-FIRMWARE-ROLLBACK-WORKS.md`): the host supplies sOS and SYS_DRV over
the PSP bootloader mailbox on every cold boot. Substituting the signed `.51`
container from linux-firmware (`36934f24c008`) and power-cycling is sufficient.
No anti-rollback enforcement was observed.
So an attacker with the ability to load firmware (i.e. the host driver) can
downgrade to `.51` and then map the TMR over a region the current firmware
deliberately protects.
## Reproduction
```bash
python3 psp_out/psp_recover.py # cold the PSP
python3 psp_out/test_tmr_range_guard.py --fw51 \
--addr=0x0000FFFDFB000000 # -> 0x0 (accepted)
python3 psp_out/psp_recover.py
python3 psp_out/test_tmr_range_guard.py --addr=0x0000FFFDFB000000 # -> 0xffff0006
```
`test_tmr_range_guard.py` hooks `AM_PSP._tmr_load_cmd` and issues exactly one
probe before the legitimate TMR is established, then restores normal setup.
### Two traps that produced false readings first
1. **SETUP_TMR is one-shot per boot.** `qword_E264A8 != 0` short-circuits every
later call with `0xffff0007`, so a batch of probes in one boot measures only
the first. One probe per boot.
2. **Size must clear the window.** With `size = 0x1300000`, a base of
`0xFFFD_FA000000` still *overlaps* (it ends at `FB300000`). My first "outside"
control was not outside, which made the guard look like a blanket rejection.
Also `buf_size` must satisfy `size >= dword_E26498` and be 64 KB aligned, or
the request dies at `0xffff0006` long before the guard.
## What the protected window actually is — IDENTIFIED
The two 8 MB regions are not arbitrary constants: they are entries in SYS_DRV's
**protected-DRAM range table** at `dword_E264B0`, laid out as 16-byte
`(base:u64, length:u64)` records:
```
0xE264B0 0xFB000000 0x0000FFFD base = 0x0000FFFD_FB000000
0xE264B8 0x00800000 0x00000000 len = 0x800000 (8 MB)
0xE264C0 0xFB800000 0x0000FFFD base = 0x0000FFFD_FB800000
0xE264C8 0x00800000 0x00000000 len = 0x800000 (8 MB)
0xE264D0 0x00000000 ... (third slot, unused)
```
That table is the one `dram_range_query` (RPC `0x7008`, the read-only address
query) walks to decide whether an address is in a protected DRAM range — our
own annotation for it reads: *"tests the 64-bit address against 3 entries of the
dword_E264B0 range table and 10 platform_state slots."*
The `0x0000FFFD_xxxxxxxx` aperture is the PSP's physical/MMIO view: the same
high dword appears in `reg_write_sized(0xF95FFE2E, hi=0xFFFD)` used by
`acquire_service_lock` / `reg_unlock_sequence`, in `ecc_params_load`
(`0xFFFD_FC0002F8`, UMC/ECC parameters) and in `engine_state_check`
(`0xFFFD_FC000000`, mapped 0x1000 via `SVC 0x84`). The protected window sits
directly below that block.
So the guard's purpose is now clear: **stop the host placing the Trusted Memory
Region on top of memory SYS_DRV itself tracks as protected DRAM.**
## Impact — and the limit of what is proven
Proven: on `.51` the PSP accepts a TMR placement that `.54` refuses, inside a
window `.54` explicitly fences off. The TMR is the PSP's trusted DRAM carve-out,
so its placement determines which physical memory the PSP treats as protected.
Not proven: the **contents** of those 16 MB, and therefore the concrete
consequence of overlapping them. We now know *what the region is designated as*
(SYS_DRV's own protected-DRAM range table, in the PSP's MMIO aperture), but the
memory was never dumped and no secondary effect — leak, corruption, or privilege
gain — was demonstrated. Calling this "arbitrary PSP memory disclosure" would
still be unsupported.
### Impact experiment (`test_tmr_impact.py`) — the placement IS consequential
The earlier "init broke" reading was a harness artifact (the probe consumed the
one-shot TMR, so the restore hit `0xFFFF0007`). Re-done properly: let the PSP
keep running with the TMR pointed at the protected range, then exercise the
things that depend on it.
| FW | TMR placement | SETUP_TMR | device init | compute |
|---|---|---|---|---|
| `.54` | legitimate | `0x0` | OK | OK (`196608`) |
| `.54` | `0xFFFD_FB000000` | **`0xffff0006` REJECTED** | fails (no TMR) | — |
| `.51` | legitimate | `0x0` | OK | OK (`196608`) |
| `.51` | `0xFFFD_FB000000` | **`0x0` ACCEPTED** | **fails at `LOAD_IP_FW`, status `0x80000029`** | — |
Both "evil" rows fail, but for **opposite reasons**, which is the whole point:
* on `.54` the guard rejects the placement, so no TMR is ever established;
* on `.51` the placement **succeeds**, the PSP proceeds to trust that range, and
the failure appears later — GFX ring cmd 6 (`LOAD_IP_FW`) returns
`0x80000029` ("known command, bad parameter" family).
So the PSP does not merely validate-and-ignore the address: it accepts it, uses
it as the trusted region, and subsequent firmware loading breaks. The guard is
preventing a state the firmware genuinely cannot operate in.
**Demonstrated impact: a functional denial of service of PSP firmware loading,
reachable on a rollback-loadable build.** The device cannot complete
initialisation; recovery needs a PCIe power cycle.
**Still NOT demonstrated:** any disclosure, code execution, or corruption of
secret material. The `LOAD_IP_FW` failure is at least as consistent with "that
address is not usable DRAM" as with "we clobbered something valuable" — indeed
the fact that the PSP cannot use it argues it is an MMIO/register aperture rather
than RAM. Whether the PSP's writes into a TMR placed over MMIO produce
attacker-useful register side effects is an open question this test does not
answer.
The honest statement is: **a security check present in current firmware is absent
in a rollback-loadable build, and the difference is demonstrable on the live part
with a single command.**
## Card state
Restored to stock after testing: `C2PMSG_58 = 0x00310036` (`0.49.0.54`),
`is_sos_alive() = True`. No persistent change observed across ~10
downgrade/restore cycles.
================================================================================
## MECHANISM — what SETUP_TMR actually does with the base (deeper analysis)
Re-examined `sysdrv_tee_map_shared_mem` past the range check. It is **not** a
passive "reserve a region" call — on success it makes the PSP **DMA a set of
SPI-ROM firmware regions to `base + fixed_offset`**, where `base` is the
host-supplied 64-bit value.
### The flow after the guard passes
```c
qword_E264A8 = *(_QWORD *)v7; // host base stored
range_scan_entries / range_overlap_check; // the guards (.53+)
read_reg_pair(qword_E264A8, .., &qword_E264A0);// -> qword_E264A0 = host base (2x SVC 0x7B)
spirom_region_read(8, 0x900000, size8); // each: dest = qword_E264A0 + offset
spirom_region_read(34, region_base(34), ..);
spirom_region_read(13, v5, ..);
spirom_region_read(49, v14, ..); spirom_region_read(58, v16, ..);
spirom_region_read(50, v18, ..);
spirom_region_read(84, 0x700000, 0x100000);
spirom_region_read(83, 0x800000, 0x100000);
SVC 0xA1; // commit
```
* `read_reg_pair` (`0xE189C4`): two `SVC 0x7B` (register reads) then
`*a3 = base` — so **`qword_E264A0` = the host-supplied base**, and it is the
DMA destination base.
* `spirom_region_read(id, off, sz)` (`0xE19118`): sets
`v4 = qword_E264A0 + off` and calls `spirom_xfer(&v4, 0)` — reads SPI-ROM
region `id` and writes it to **`base + off`**.
* `spirom_xfer` (`0xE1447C`) validates the destination: rejects bits above 48,
a non-zero low half-word (64K alignment), and lengths past the device size —
but does **not** re-check the base against the protected window.
### So the primitive is
**A host-directed DMA of the resident firmware region set to a host-chosen 64-bit
physical base.** With `base = 0xFFFD_FB000000` the regions land at, e.g.:
region 84 -> 0xFFFD_FB700000
region 83 -> 0xFFFD_FB800000 (== the 2nd protected-DRAM range entry!)
region 8 -> 0xFFFD_FB900000
The confirmed hardware result (`SETUP_TMR` returned `0x0` on `.51`) means **every
`spirom_region_read` succeeded and `SVC 0xA1` committed** — i.e. the PSP *did*
DMA those firmware regions to `0xFFFD_FBxxxxxx`. On error any region returns
non-zero -> LABEL_27 zeroes the base -> non-zero status; we got `0x0`, so it went
through.
### What the guard is really protecting
The `dword_E264B0` protected-DRAM table entries are at `0xFFFD_FB000000` and
`0xFFFD_FB800000` — exactly where region 83 lands at `base+0x800000`. So the guard
stops the host from steering the firmware-DMA onto the PSP's own reserved DRAM.
On `.51` that steering is allowed and confirmed to execute.
### Reframed severity — this is a controlled-destination write, not just DoS
* **Destination:** fully host-controlled 64-bit physical base (64K-aligned,
< 2^48), in the PSP's physical/MMIO aperture (`0xFFFD_xxxxxxxx`).
* **Content:** fixed SPI-ROM firmware regions (signed images — not attacker
bytes), at fixed offsets from the base.
* **Confirmed:** the DMA executes on `.51` for a base inside the protected window.
This is a "write known firmware content to attacker-chosen physical address"
primitive. Its value is a corruption primitive against whatever PSP structure the
chosen `base + offset` lands on, NOT disclosure of the (public) firmware content.
### The concrete open question (this is where to push)
What lives in the PSP `0xFFFD_xxxxxxxx` physical aperture at the reachable
destinations (`base + {0x700000, 0x800000, 0x900000, ...}`)? If any reachable
`base+offset` maps PSP SRAM / a code region / a live control structure, this
becomes a controlled-destination corruption of PSP-internal memory. If it only
maps DRAM carve-out, it stays DoS-class. Determining the aperture's contents
(against the SoC memory map, or by observing PSP behaviour for different bases on
`.51`) is the step that decides corruption vs DoS.
================================================================================
## BASE SWEEP (hardware, .51) — the corruption upside does NOT materialise
Swept the TMR base across the `0xFFFD` aperture, one base per boot (SETUP_TMR is
one-shot), staying below the `FC000000` register block (safety rail; the ~10 MB
firmware footprint would otherwise reach it). `probe_tmr_base_sweep.py`.
| base | status | dt | alive |
|---|---|---|---|
| `0x80_00300000` (legit control) | `0x0` | 0.006s | True (init OK) |
| `0xFFFD_00000000` | `0x0` | 0.006s | True |
| `0xFFFD_80000000` | `0x0` | 0.006s | True |
| `0xFFFD_F0000000` | `0x0` | 0.006s | True |
| `0xFFFD_F4000000` | `0x0` | 0.006s | True |
| `0xFFFD_F8000000` | `0x0` | 0.006s | True |
| `0xFFFD_FA000000` | `0x0` | 0.006s | True |
| `0xFFFD_FB000000` (protected window) | `0x0` | 0.006s | True |
**Completely uniform across a 4 GB span** — identical status, identical 6 ms
timing, PSP always alive. No base produces a distinct signal (error, hang, timing
change) that would mark a live/critical structure at its destination.
### What the uniformity tells us
* The write is **accepted everywhere** (no dest NAKs / faults) and has **no
immediate effect** on the running PSP at any tested address. If any reachable
destination held a live PSP SRAM / code / control structure, DMAing firmware
over it should have perturbed *some* base — it did not.
* The **identical 6 ms** across a 4 GB address span (and equal to the legit
control) is a strong tell: the observable cost is independent of the
destination. Either a DMA engine completes regardless of target semantics, or
writes into the unmapped parts of the aperture are silently absorbed. Either
way, **no reachable-safe destination behaves like live PSP memory.**
### Verdict on the primitive
Within the reachable **and safely testable** range, the TMR-base primitive is
**DoS-class**: a controlled-destination write whose every reachable destination
is inert from the PSP's perspective, breaking only the (now-mislocated) TMR —
the DoS already established. The hoped-for "controlled-destination corruption of a
live PSP structure" **does not materialise** anywhere in the swept range.
The single untested region is the `0xFFFD_FC000000` register block (`ecc_params`
at `FC0002F8`, `engine_state` mapping at `FC000000`). It is deliberately excluded:
DMAing firmware content into a memory-controller / PHY / fuse register aperture is
brick-risk, and the expected payoff is low because the **content is fixed signed
firmware, not attacker bytes** — even a successful write there injects known data,
not chosen data.
### Net
The TMR range guard (`.53+`) is a real fix and the rollback reaches the vulnerable
path, but the primitive it exposes is a **DoS**, not a memory-corruption or
disclosure primitive, across everything reachable without brick-risk. This closes
the "what's the upside" question: there is no demonstrated upside beyond DoS.
================================================================================
## WHY IT NEVER CRASHED — the DMA writes 0xFF, not firmware
Follow-up to the sweep's suspicious uniformity ("an arbitrary write of real data
should crash *something*"). Tested directly: point the TMR at a HOST-READABLE,
sentinel-filled VRAM buffer, run SETUP_TMR on .51, read it back
(`probe_tmr_writes_real.py`, `probe_tmr_footprint.py`).
### The DMA is real — it lands at the host-chosen base
The sentinel (`c3c2c1c0...`) was overwritten at every region destination. So the
firmware-DMA genuinely writes to `base + offset` where `base` is host-controlled.
That part of the primitive is confirmed real, not a no-op.
### ...but every byte written is 0xFF
Full-buffer scan of the 19 MB TMR: the write footprint is **entirely `0xFF`** in a
1 MB-striped pattern (each ~1 MB region DMA writes `0xFF000` of `0xFF` then skips a
4 KB page). No real firmware content anywhere; the only non-`FF`/non-sentinel
bytes are `FF`-meets-sentinel boundary pages.
`0xFF` is erased/empty SPI flash. So **all** the regions the TMR-DMA reads
(8, 13, 34, 49, 50, 58, 83, 84) are unprovisioned on this consumer Navi31 — the
SPI directory points at erased flash. These are optional IP-firmware slots that a
consumer card does not populate.
### This fully explains the non-crash, and further downgrades the primitive
Three confirmed layers:
1. the write lands at the host-chosen base (real);
2. the content is constant `0xFF` (empty flash), not firmware and not
attacker-controlled;
3. `0xFF` written into the unmapped / unused `0xFFFD` DRAM is inert -> no crash,
uniform 6 ms, PSP always alive across the whole 4 GB sweep.
### Final characterisation of the primitive
`SETUP_TMR` on `<= .51` is a **write of constant `0xFF` (1 MB-striped, ~11 MB
total) to a host-chosen 64-bit physical base**, on this card. The host controls
the destination but NOT the content (fixed region IDs -> empty flash -> `0xFF`).
On a card that populates those SPI regions it would write known signed firmware
instead — still not attacker-controlled.
Combined with the sweep (every reachable-safe destination inert), the practical
severity is **DoS only**: the value written is harmless where it can be aimed, and
the aim cannot reach a live structure without entering the brick-risk `FC000000`
register block. There is no disclosure and no useful corruption. The `.53+` guard
is a correctness/defence-in-depth fix, not the mitigation of an exploitable
memory-corruption bug — at least on hardware of this SKU.
================================================================================
## SPI DISCLOSURE IDEA — investigated, no reachable vehicle
Idea: turn the TMR write-back into a SPI-flash read, or steer a base "earlier" to
reach normally-hidden SPI content. Investigated end to end; it does not work,
for concrete reasons.
### The TMR path cannot read chosen SPI
`SETUP_TMR`'s `base` is the memory **destination** (confirmed: our readable VRAM
received the DMA). The SPI **source** is selected by hardcoded region IDs
(8/13/34/49/50/58/83/84), all empty (`0xFF`) on this consumer Navi31. Moving
`base` relocates where the (empty) bytes land, not which SPI offset is read. So
"earlier base" shifts the destination, not the source.
### The RPC that DOES have host-controlled SPI source is not host-reachable
`sysdrv_rpc_dispatch` contains a direct `spirom_xfer(frame, 1)` call and
`spirom_range_check` (opcode `0x1016`). Their request struct is host-shaped:
{ spi_offset:u64, length:u64, ..., region_id:u32 }
with host-controlled SPI offset + length (the device-size length cap at
`dword_E26498` is even bypassed when `a2=1`). BUT the destination is chosen by
`region_id`, mapping to fixed values (`0x200078, 0x70038, 0x3F0038, 0x30038, ...`)
programmed into a PCIe/SPI **DMA descriptor register** (`pcie_port_reg_write`), not
a host pointer. So it moves SPI to fixed internal apertures, not to a chosen
buffer.
**And it is unreachable from the host.** Reachability requires an sOS stub
`sysdrv_rpc_<opcode>` that forwards the opcode over `SVC 0xF2`. Checked sos2 for
the entire flash cluster:
sysdrv_rpc_1010 .. sysdrv_rpc_1017 : NO STUB (none exist)
No sOS stub forwards any SPI-transfer opcode, so the host cannot issue them. They
are internal SYS_DRV operations (boot / flash-recovery / the on-die TSU mailbox,
itself not host-reachable).
### Conclusion
There is **no host-reachable PSP path that reads arbitrary or hidden SPI content
into host-readable memory**:
* TMR path: host-reachable, dest-controllable, but SPI source is fixed empty
regions -> yields only `0xFF`.
* Direct SPI-transfer RPCs: host-controlled SPI source, but not host-reachable
(no forwarding stub) and dest is a fixed internal aperture.
Separately, the normal VBIOS region of the SPI is host-readable via the standard
GPU ROM aperture without any PSP involvement; the PSP would only add value for
*protected* SPI regions, and no host-reachable PSP path to those exists here.
================================================================================
## Q: can the 0xFF DMA hit SRAM/juicier targets? Q: does the PSP trust DRAM?
### The 0xFF write cannot be weaponised beyond DoS
Two hard limits, independent of aim:
* **Content is 0xFF, not attacker-controlled.** Hitting PSP SRAM / a register
block writes 0xFF (~invalid instructions / poison) -> crash at best, never a
chosen value. No controlled corruption is possible from this primitive.
* **PSP SRAM is not in the DMA-addressable fabric.** The swept 0xFFFD aperture
(4 GB) was inert; the only live thing in reach is the FC000000 register block
(brick risk). PSP tightly-coupled SRAM is not exposed at these physical
addresses. So "target SRAM" is not reachable, and would be DoS anyway.
### The PSP does NOT trust host-mutable DRAM — the TMR fence is real (hardware)
`probe_tmr_fence.py`, normal .54 boot. The TMR is PSP-trusted DRAM (TA sessions,
keys, TA working memory) placed in host-allocated VRAM at `tmr_paddr` (mc
`0x8000300000`). Tested host access to it after the PSP claimed it:
```
READ every TMR offset (0 .. size) -> 0xFF (content not visible)
WRITE 0x5A5B.. at TMR+0x2000, read back -> 0xFF (write DROPPED, not stored)
```
The memory-controller fence the PSP programs during SETUP_TMR (read_reg_pair ->
SVC 0x7B) **poisons host accesses**: reads return 0xFF, writes are dropped. So the
host cannot read or write PSP-trusted TMR state. Confirmed on hardware; sOS alive
and compute working after.
### The broader answer
Every piece of DRAM the PSP reads is either fenced or validated before trust:
* **TMR** (persistent trusted state): fenced by the memory controller -> host
reads 0xFF, writes dropped. (This also means the earlier "firmware regions are
empty (0xFF)" reading may partly be fence poison; either way the content is not
host-usable.)
* **Command / ring buffers**: re-validated per use (validate_client_range, the
snapshot-then-parse pattern in tee_cmd_dispatch).
* **TA / IP-FW images**: signature-verified, and TA code is copied to SRAM
(bl_copy_image_to_sram) before execution -> no post-verify TOCTOU on code.
So the PSP's DRAM trust model holds: host-mutable DRAM is either fenced (TMR) or
validated at each use (buffers) or verified-then-copied-to-SRAM (images). There is
no observed path where sOS/a TA reads host-mutable DRAM and trusts its contents
for a security decision without one of those protections.
|