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