aboutsummaryrefslogtreecommitdiffstats
path: root/fw/FINDING-PSP-BOOTLOADER-LIVE-AT-RUNTIME.md
blob: 530dbaddd5d248fb02d3e54dac0e18cf7e7b3a2a (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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# FINDING: the PSP bootloader command interface is still live after sOS has booted

**Evidence class: [HW]** — Navi 31 / RX 7900 XTX, sOS/SYS_DRV 0.49.0.54.
**Status: interface liveness CONFIRMED. Consequence CLOSED NEGATIVE - component loads are refused post-boot.**

## The interface

The PSP bootloader loads the sOS components at boot through a mailbox pair completely
separate from the GFX ring:

* `regMP0_SMN_C2PMSG_35` — component id in, status out, bit 31 = "bootloader ready"
* `regMP0_SMN_C2PMSG_36` — payload address **>> 20** (1 MB-aligned)

Component ids (from the driver headers): `LOAD_SYSDRV 0x10000`, `LOAD_SOSDRV 0x20000`,
`LOAD_KEY_DATABASE 0x80000`, `LOAD_SOCDRV 0xb0000`, `LOAD_DBGDRV/HADDRV 0xc0000`,
`LOAD_INTFDRV 0xd0000`, `LOAD_RASDRV 0xe0000`, `LOAD_IPKEYMGRDRV 0xf0000`,
`LOAD_TOS_SPL_TABLE 0x10000000`, `DRAM_LONG_TRAIN 0x100000`, `DRAM_SHORT_TRAIN 0x200000`.

**Nothing in this audit had ever touched this interface** — all 140 prior passes drove the
GFX ring, the TA command space, or the ring control registers.

## Confirmed on hardware

With sOS alive, the ring created (`_64 = 0x80020000`, `_71 = 0x10000`) and the heartbeat
advancing, the bootloader's ready bit is **asserted**: `_35 = 0x80000000`.

A latched idle bit would prove nothing, so the test used an **invalid component id**
(`0x44000000`) with the payload address pointing at a zeroed page — asking the bootloader to
answer without asking it to load anything (`hw/bl_runtime_probe.py`):

| | `_35` | `_36` | `_64` | sOS |
|---|---|---|---|---|
| before | `0x80000000` | `0x0` | `0x80020000` | alive |
| **after** | **`0x8000000a`** | `0x80016` | `0x80020000` | **alive, heartbeat advancing** |

**The bootloader responded with status `0xa`.** So the bootloader mailbox is **live and
processing host commands at runtime**, concurrently with sOS and the ring, and rejects a bad
component id cleanly rather than hanging.

## The consequence: CLOSED NEGATIVE

The question was whether it will **act** on a *valid* component load post-boot. It will not.

`hw/bl_runtime_load.py` submits the **exact bytes this boot already loaded** for a component,
placed 1 MB-aligned with the address in `_36`, so acceptance would change no content and would
only demonstrate that the path acts. Components are limited to those this boot actually
loaded (SYS_DRV, SOS, TOC, KDB, SPL, RL) - the harness **refuses to run** for anything else
rather than produce a row:

| request | payload | `_35` status | sOS after |
|---|---|---|---|
| invalid id `0x44000000` | zeroed page | **`0xa`** | alive, heartbeat advancing |
| `LOAD_SYSDRV` `0x10000` | 161,680 B, the running build | **`0xa`** | alive, heartbeat advancing |
| `LOAD_KEY_DATABASE` `0x80000` | 5,888 B, the running build | **`0xa`** | alive, heartbeat advancing |
| `LOAD_TOS_SPL_TABLE` `0x10000000` | 672 B, the running build | **`0xa`** | alive, heartbeat advancing |

**Uniform refusal.** A valid component load gets exactly the same status as a nonsense
component id, so `0xa` here means "not permitted in this state" rather than "bad parameter".
The bootloader answers at runtime but is **state-gated closed**: it will not reinitialise a
component once sOS is up.

So there is **no rollback primitive and no in-place reinitialisation** - the interesting case
(loading one of the corpus's 31 older signed SYS_DRV builds over the running one) cannot even
begin, because the *current* build is refused. This is correct behaviour, recorded as a
boundary that holds.

## The anti-rollback question: BLOCKED, not tested

Component loads are refused *after* boot, but the driver itself drives the bootloader during
init, and in that window loads are accepted - that is how the running firmware got there. So
the real question is whether the bootloader enforces **anti-rollback**: would it accept an
*older signed* SYS_DRV during our own boot?

**It cannot be tested with the material available**, and the reason is recorded so nobody
mistakes a confounded result for an answer:

| | length | container | version family |
|---|---|---|---|
| live SYS_DRV (this part) | 161,680 | `$PS1` magic at +16 | - |
| corpus, 5 images | 116,624 | same `$PS1` container | `128-72-1-0` |
| corpus, 17 images | 116,624 | same `$PS1` container | `201-73-2-0` |

The container format matches, so the images are structurally comparable - but **neither
corpus family is an older build of the component this part runs**, and no historical
`psp_*_sos.bin` for this ASIC is present on the test box. Substituting a corpus image would
be rejected for **ASIC/family mismatch**, and that rejection would say nothing about
rollback protection.

Running it anyway would have produced a "rollback blocked" row that the evidence does not
support - the same error class as the vacuous capacity detector (pass 18) and the vacuous
`GBR_IH_SET` probe (pass 140). **What is needed is an older signed SYS_DRV for this ASIC
family**; that is an access problem, not an analysis one.

## Safety notes for whoever continues

* `LOAD_IP_FW` is a known PSP wedge with junk payloads — do not include it in sweeps.
* The two DRAM training ids would retrain memory on a running system.
* Recovery from any PSP wedge here needs `work/atx_power.py cycle 25`; GPU/AM reset is
  insufficient.

## Why it is still worth recording

An interface that can still be *addressed* after boot is surface that secure-boot reasoning
usually assumes is gone, and the distinction matters: the mailbox is **reachable** but
**refuses to act**, which is a state-machine gate doing its job rather than an absence of
surface. That is worth knowing precisely because a future firmware could relax the gate
without anyone noticing the mailbox was reachable all along.

The four-row table above is the control behind that claim: the refusal is uniform across a
nonsense id and three genuine components, and sOS survives every attempt.