# 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_` 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.