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_FWis 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.
