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