aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out/FINDINGS.md
blob: 1906a561a64c7832a13aa230f378670cee07a80f (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
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
# PSP firmware rollback finding — AMD Navi 31 (gfx1100), psp_13_0_0

> **Not the authoritative document for this topic.** See **`CONFIRMED-FIRMWARE-ROLLBACK-WORKS.md`**,
> and `INDEX.md` §3 for why. Kept for its detail and history.


## Summary

Six sOS syscalls expose sized MMIO read/write to User-mode callers (TAs). All
funnel through one shared address-validation helper. In firmware **0.49.0.37
through 0.49.0.51** that helper validates against only **two** hardcoded MMIO
windows and performs **no caller-identity check**. In **0.49.0.54** it was
replaced by a centralised validator that adds a **third** protected window and
gates it on the caller's descriptor type.

No anti-rollback prevents loading the older firmware — verified on hardware.

## The syscalls

Callers of the shared helper (`sub_3D8C` in .54, `sub_3E44` in .37):

    svc_a6_impl  svc_7b_9f_impl  svc_a9_impl
    svc_7c_a0_impl  svc_a5_impl  svc_aa_impl

They implement sized load/store (1/2/4/8 byte) plus a masked read-modify-write
at a caller-supplied address.

**Reachability confirmed:** the shipped RAS TA (`psp_13_0_0_ta.bin`, User mode,
caller class 4) issues `SVC 0xA0`, which is `svc_7c_a0_impl` — one of the six.

## The check, before and after

OLD (.37 .. .51) — inline, two windows, no identity check:

    ADD.W R1, R0, #8          ; end = addr + size
    CMP   R1, R0 / BCC fail   ; overflow
    CMP.W R0, #0x3A00000 ┐ deny [0x3900000, 0x3A00000)
    CMP.W R1, #0x3900000 ┘
    CMP.W R0, #0x3800000 ┐ deny [0x3700000, 0x3800000)
    CMP.W R1, #0x3700000 ┘
    -> proceed

NEW (.54) — `sub_6D20`, adds a third window + caller gating:

    ... same two windows + overflow ...
    LDR   R3, =off_80D8 ; LDR R3,[R3]
    LDRB.W R3, [R3,#0x4B]     ; caller id from KERNEL task context (not spoofable)
    ADD.W R3, R3, R3,LSL#1    ; id*3
    ADD.W R3, R4, R3,LSL#5    ; descriptor, stride 96
    LDR   R4, =0xCB10000
    CMP   R0, R4 / BCS allow
    CMP.W R1, #0xCB00000 / BLS allow
    CMP   R3, #1 / BEQ allow  ; caller type 1 allowed
    CMP   R3, #2 / BNE deny   ; type 2 allowed, all others DENIED

`+0x4B` is the same task-context offset used by `svc_f2_rpc_gate`.

## What the windows are

MMIO register regions. SYS_DRV .54 carries a table of (address, mask, flags)
triples at `0xE23340`, including entries inside the protected ranges:

    0x0c810058  0x03a70020  0x0c870020
    0x0cb0403c   <-- inside the newly protected [0xCB00000, 0xCB10000)
    0x03a000c0  0x0c8000c0

## Bisection

Literal `0x0CB10000` present only in .54:

    .37 .41 .47 .48 .49 .50 .51  -> absent (old inline form, 2 windows)
    .54                          -> present (centralised validator)

In .51 two functions still carry the inline checks: a 6-caller helper (88 B)
and a 3-caller function (166 B). Both were converted in .54.

**.51 is the newest publicly tagged linux-firmware release (20250708) and still
lacks the fix.** .54 comes from a newer commit than any dated tag checked.

## Rollback verified on hardware

Booted 0.49.0.37 (Feb 2023) on a live 7900XTX in place of 0.49.0.54:

    is_sos_alive: True   TMR: 0x1300000   compute: OK

SPL (security patch level) payload is byte-identical across all 21 releases
scanned (Jan 2023 - Jul 2025); only its signature changes. The floor was never
raised, so nothing blocks the downgrade.

## Status / caveats

- CANDIDATE, not a demonstrated exploit.
- Not established what `0xCB00000` controls, only that AMD later restricted it.
- Older builds may have compensating checks elsewhere in the chain (untraced).
- Cannot distinguish a fuse-based rollback floor from a table-based one, since
  the floor never moved in the observed range.

---

## Follow-up: what the protected windows are

The register table at `0xE231F8` (46 entries, stride 12, `(mmio_addr, mask, value)`)
shows **mirrored register instances**:

    0x03af000c / 0x0c8f000c        0x03a10058 / 0x0c810058
    0x03af0010 / 0x0c8f0010        0x03a000c0 / 0x0c8000c0
    0x03a70020 / 0x0c870020 / 0x0cb0403c   <- three instances, same mask 0x3f

Buckets: 0x032xxxxx (10), 0x03axxxxx (11), 0x0c8xxxxx (11), 0x0cbxxxxx (1).

Of all 46 entries **exactly one** — `0x0cb0403c` — falls inside any protected
window, and it is the window `.54` added. It appears to be a third instance of
the register mirrored at `0x03a70020` / `0x0c870020`.

**Block identity not determinable from firmware alone**: no symbols, and SYS_DRV
has *zero* direct operand references to any of the three windows (verified by
scanning all functions). The registers are reached only through this data table.

The legacy windows are likewise referenced nowhere except inside the sOS
validator itself — they are pure deny-lists: regions the kernel forbids but
never itself touches via these syscalls.

## SYS_DRV diff (.37 -> .54): 54 modified, 11 removed, 33 added

Cross-referenced against our 12 annotated functions:

    stack_chk_fail                 unchanged
    sysdrv_map_and_copy            unchanged
    psp_map_host_phys_range        unchanged   <-- PF-bypass present since .37
    gfx_cmd_LOAD_IP_FW_handler     unchanged   <-- fw_size>0xFF DoS still present
    gfx_cmd_PROG_REG_regid_switch  unchanged
    progreg_table_lookup           unchanged
    progreg_apply                  unchanged
    vf_phys_range_check            unchanged
    sysdrv_log_emit                unchanged
    sysdrv_mailbox2_cmd_handler    MODIFIED
    stub_ret_FFFF0007              (matcher miss)
    sysdrv_big_dispatch_maybe      (matcher miss: .37 counterpart is e100e4)

**SYS_DRV's host-facing ring surface has been stable for 2.5 years.** The
PF-bypass in `psp_map_host_phys_range` being unchanged across 8 releases
suggests it is intentional design (the PF is trusted for addressing), not an
oversight — this downgrades that earlier audit finding.

The only security-relevant fix located in 2.5 years is the sOS MMIO-window
change, on the TA -> kernel path.

## Tooling caveat

`diff_versions.py` uses an absolute pairing threshold (`bs < 40`), which
wrongly rejects large-function pairs: `e100e4`(.37) <-> `e105a8`(.54) scores
~76 despite being obviously the same function (4318/4372 B, 664/671 bb,
224/227 calls). Scoring must be normalised by function size before this is
trustworthy unsupervised.

---

## CORRECTION: who can actually reach the MMIO syscalls

An earlier claim that "the RAS TA issues SVC 0xA0" was **WRONG**. It came from a
capstone linear sweep that misdecoded data as instructions (phantom SVCs 0xA0
and 0xE8). Proper IDA analysis of all four TAs:

    TA     base       funcs   SVC census                                   MMIO syscalls
    ASD    0xD00100    1551   51 56 57 59 5a 5d 5e 5f 6c 6d 7b(135)        YES: 0x7b, 0x7c, 0xa5
                              7c(98) 7d 8b 8d a5(1) b4 f2
    RAS    0x400100      98   f0, f1, f2(12)                               none
    HDCP   0x400100     102   f0, f1, f2(49)                               none
    DTM    0x400100      14   f0, f1, f2(3)                                none

**A sharp privilege boundary exists between ASD and the ordinary TAs.**

- Ordinary TAs (RAS/HDCP/DTM) share the 0x400000 slot and issue only three
  SVCs — 0xF0, 0xF1 and 0xF2 — i.e. essentially everything goes through the
  `svc_f2_rpc_gate`. They never touch the raw MMIO syscalls.
- ASD loads at its own base (0xD00100) via a distinct command (LOAD_ASD,
  ring cmd 0x04) and has direct access to 18 distinct syscalls, including 234
  call sites into the MMIO read/write primitives.

This is consistent with ASD being caller descriptor type 1 or 2 (permitted by
the `.54` check) and ordinary TAs being type 4 (denied).

### Revised severity

The `.54` change does not protect against any *shipped* TA — none of
RAS/HDCP/DTM can issue those SVCs in their shipped code. It guards against a
**malicious or compromised TA** issuing the SVCs directly, which nothing in the
kernel prevents (dispatch is purely on SVC number).

So this is a **privilege-escalation primitive, not an entry point**:

    precondition: code execution inside ANY TA  (UNSOLVED)
        -> issue SVC 0x7b / 0x7c / 0xa5 directly
        -> on .37-.51: no caller-identity check, [0xCB00000,0xCB10000) reachable
        -> on .54:     denied unless caller type in {1,2}

### The proposed 0xCB00000 read experiment is BLOCKED

Reading the window via those syscalls is not possible from the host:
  - the host cannot issue SVCs at all; they are issued by code on the PSP
  - no TA we can invoke from the host issues them with attacker-controlled args
  - ASD does issue them, but its arguments are not host-controllable

Identifying `0xCB00000` therefore still requires either hardware documentation
or TA code execution.

---

## ASD analysis (11 versions, Jan 2023 - pinned)

    version        size      tags
    33.0.0.154   176,128   20230117..20230515
    33.0.0.167   184,320
    33.0.0.176   184,320
    33.0.0.187   184,320
    33.0.0.210   192,512
    33.0.0.223   192,512
    33.0.0.227   192,512
    33.0.0.231   192,512
    33.0.0.238   196,608
    33.0.0.239   196,608   20250708 (newest public tag)
    33.0.1.21    208,896   PINNED (minor-version bump)

ASD grew **19%** (176 KB -> 209 KB). Contrast sOS, which is byte-for-byte the
same size (98,944) in all 8 versions.

### Churn profile — ASD is actively developed, sOS is maintained

    transition                  unchanged  modified  removed  added
    33.0.0.154 -> 33.0.0.176          805       230        6     41
    33.0.0.176 -> 33.0.0.210         1058       187        2     48
    33.0.0.210 -> 33.0.0.239         1203       128        1     21
    33.0.0.239 -> 33.0.1.21          1290       127        0     24

    154 vs 1.21: 725 unchanged, 309 modified, 6 removed, 163 added

**309/1551 = 20% of ASD changed**, versus **13/505 = 2.6% for sOS**. Raw
function diffing does not isolate anything useful at this churn rate — a
different filter is required.

### Privileged syscall usage shifted substantially

                    33.0.0.154        33.0.1.21
    SVC 0x7b            126              135
    SVC 0x7c            341               98      <-- -71%
    SVC 0xa5              1                1
    distinct SVCs        14               18      (added 0x6c, 0x6d, 0x7d, 0x8b)

ASD cut its use of the raw MMIO read syscall (`svc_7c_a0_impl`) by 71% and
picked up four new syscalls. That is a real architectural shift away from raw
MMIO access, but it is ASD changing its own behaviour — not, by itself, a
vulnerability.

### Status

No specific vulnerability isolated in ASD. The 20% churn rate defeats
signature diffing as a bug-finding filter here; ASD needs to be approached as a
normal audit target (it is the most privileged non-kernel component: 1551
functions, 18 syscalls, 234 direct MMIO call sites) rather than via version
differential.