AMD PSP firmware audit — authoritative consolidated findings
Not the authoritative document for this topic. See
VULN-AUDIT-WHAT-WE-ACTUALLY-HAVE.md, andINDEX.md§3 for why. Kept for its detail and history.Scope note (2026-09): this file covers the PSP components only. The later campaign against the GPU microcontrollers (DMCUB, SDMA, MEC/PM4, MES, SMU) is consolidated in
VULN-AUDIT-WHAT-WE-ACTUALLY-HAVE.md. Reportable items from it, for a vendor reading only this file: * DMCUBABM_INIT_CONFIGunchecked u16 -> ~32 KB OOB write [HW] * DMCUB type-8 unboundedpanel_inst-> 8-byte leak [HW] * DMCUBREG_REG_WAITarbitrary-address read oracle [HW] (reads the signed image through a timing channel; host reads of the same physical address return zeros) * DMCUB firmware rollback [HW] — authentication is neither version- nor ASIC-bound * DMCUB inbox wedge DoS [HW]Reachability framing, stated up front: the DMCUB inbox is driver-owned, so these are loss-of-isolation / defence-in-depth, not privilege escalation from an unprivileged process. None of the GPU-side findings is reachable by an unprivileged process.
Twelve-TA round (2026-09), also outside this file: harvesting AMD's Windows driver train found twelve TA types absent from
linux-firmwarethat this card nonetheless loads and invokes (GCP, OPM, MERIT, POWERPLAY, XGMI, FP, AUC, WSPE, RAP, OC, CSU, TMZ). Two confirmed defects, both in GCP and both host-reachable: *0xFE0Ddecrements the session refcount and triggers the ASD release on a rejected handle — its siblingFE0Bis correct, which is what makes it a defect; *0xFE0Freturns handles for crypto slots that are not live.Also: GCP reaches ASD
0x202and OPM reaches ASD0x301, both previously dismissed as unreachable on a scan covering only the four then-known TAs — so that negative is false on this part. Full detail in../fw/AUDIT-NEW-TAS-RE.mdand the twofw/FINDING-GCP-*files.
Target: AMD Navi 31 / gfx1100 (Radeon RX 7900 XTX), PSP psp_13_0_0
Components: sOS (sos.elf), SYS_DRV, ASD, HDCP TA (23.0.0.82), DTM, RAS
Method: static RE (IDA 9.4) + Unicorn emulation (psp_emu/) + live hardware
(Mac mini M4 host, card behind an ASM2464PD USB4/PCIe bridge, tinygrad AM driver)
Oracle: PSP crash record via C2PMSG 82–88 (see CRASH-RECORD.md)
THIS FILE IS THE SINGLE SOURCE OF TRUTH. The other ~30 notes in this directory are working logs that contain superseded and retracted claims, sometimes in their titles. Where any file disagrees with this one, this one wins. Retracted claims are listed in section 4 — do not cite them.
Evidence tags: [HW] reproduced on hardware · [EMU] proven in emulator · [ST] static analysis only.
1. Host-triggerable findings (reportable)
1.1 HDCP TA — unauthenticated session-state forgery [HW]
hdcp_cmd0E_open commits state=2 / active=1 before any HDCP handshake or
authentication occurs, and never rolls the state back on failure. An
unauthenticated host can therefore drive a session into "authenticated" state
with a single INVOKE_CMD, with no display and no HDCP sink attached.
Impact: authentication/state bypass; also the enabler that makes 1.2 reachable.
Detail: VULN-HDCP-SESSION-STATE-BYPASS.md
1.2 HDCP TA — subcmd12 out-of-bounds write / stack buffer overflow
Handler hdcp_subcmd12_rxinfo (sub_402A88) copies 5 * msg[2] bytes (where
msg[2] is a full attacker byte) into the 504-byte sess->rx_ids, and a second
copy of 5*rx_count into a stack buffer.
- Write is real, exact extent
5*rx_count+9, boundary N=101 — [ST + EMU] - Trigger path is hang-free and host-reachable (needs 1.1) — [HW]
- N≥53 overruns the stack frame and reliably faults on
__stack_chk_fail(PC0x40433c, FAR0xdeadbeef) — [HW] Impact: DoS-grade memory corruption. Escalation is canary-gated: the stack guard is loader-randomised (decisive HW test — N=56 with a0x33fill landing exactly on the canary still crashes), and no host-reachable guard disclosure exists anywhere in the loadable-TA surface (see 3.2). The 11-byte OOB variant has no hardware-observable effect (corrupted bytes are masked by theerr_statelatch) — do not cite it as a hardware-verified write. Detail:SUBCMD12-RESOLVED.md,VULN-HDCP-TA-STACK-OVERFLOW.md,WRITE-PRIMITIVE.md
1.3 ASD 0x308 — unauthenticated out-of-bounds READ [HW]
Reachable with a single INVOKE_CMD — HDCP cmd 0x12 → sub_403FD0 →
SVC 0xF2 service 2 → ASD sub_D13340. No session, no display, no handshake.
The handler re-derives a copy length a1' from attacker-supplied header bytes
(b5:b6:b7) after the initial bounded copy, then uses it for a copy whose
source runs off the end of the 0x1400-byte stack buffer.
Observed: data abort, DFSR 0x37 (WnR=0 → read), DFAR ≈ 0xd5b000, PC in
sysdrv_memcpy. Card requires a PCIe power-cycle to recover.
Impact: unauthenticated DoS / memory-safety violation.
Not a write: see 4.1.
Detail: VULN-ASD-308-BYPASS.md (read its status banner), VULN-ASD-308-REACHABLE.md
1.4 buf_list bad-descriptor VMC fault storm → card wedge [HW]
INVOKE_CMD carries a host-controlled buf_list of {addr,size} descriptors.
Count (≤64), size (≤0x4000000) and a protected-region exclusion are all correctly
enforced (sub_3A84/sub_3880/sub_6CB0), but a descriptor whose address is
allowed yet unmapped causes a sustained GPU VMC page-fault storm that wedges the
device (SMU stops responding; mode1_reset times out; only a PCIe power-cycle
recovers it).
Impact: DoS. Not memory corruption — descriptor addresses are GPU-VM
translated, so they can only reach host address space, never PSP-internal memory.
1.5 LOAD_TOC null-address hang [HW]
GFX_CMD_ID_LOAD_TOC with toc_phy_addr = 0 hangs the PSP command ring
(fence never signals; crash record stays empty → a hang, not a fault). A valid
address with malformed content is cleanly rejected (0xFFFF0008).
Impact: DoS.
1.10 LOAD_IP_FW type 0x55/0x31/0x32 — unsigned, host-reachable register writes [HW] (NEW; corrects my own refutation)
sysdrv_tee_load_firmware (RPC 0x1007) dispatches fw types 49/50/85 into paths that
return before verify_image_signature. On the consumer ring (0xFFFF, hardcoded by
bl_legacy_mailbox_loop), type 0x55 -> buffer_copy_bounded gives up to 64
attacker-chosen 32-bit register writes per call: per 8-byte element the low dword is the
address (constrained to [0x21500,0x21B78]) and the high dword is the value, entirely
unconstrained. Types 0x31/0x32 -> region_window_open push caller-chosen
(16-bit index, 32-bit value) pairs through an index/data port. Writes are issued by the
PSP (SVC 0x7C), so they carry PSP initiator identity.
Window contents: bl_set_boot_stage (sos2 0x15A0C) drives 0x215C0 and 0x2161C via
svc_A6_reg_rmw — both inside the attacker-writable window; both it and
buffer_copy_bounded resolve addresses through vawin_map_fixed, so it is the same space.
Credit: independently identified as "Route C" in new/ACTIONABLE-FINDINGS.md.
RETRACTION: AUDIT-ROUND-NEW-VULN-HUNT.md previously recorded this path as REFUTED.
That was wrong — I never disassembled the BLS loc_E1008A branch and turned a "?" into a
verdict. This is the only host-reachable, unauthenticated write primitive found in the
project; everything else here is DoS-class, rollback-only, signature-gated, or latent.
Hardware: 6 rejecting controls reproduce exactly (window bounds [0x21500,0x21B78] confirmed
on silicon by the two boundary probes); address 0x21B40 returns 0x0 — the write executed,
sOS alive, card healthy after. probe_routec_regwrite.py.
Capability: fw types are VCN0_RAM/VCN1_RAM/UMSCH_CMD_BUFFER, so much of the window is
multimedia config the host can likely already program over MMIO. But the window is mixed:
an exhaustive scan of all six modules shows it is referenced by exactly two functions —
buffer_copy_bounded (UMSCH bases, 0x21B4x) and sOS's bl_set_boot_stage
(0x215C0/0x2161C). Nothing reads either register in any image, so their consumer is
hardware or encrypted code. bl_set_boot_stage's only caller is the C2P/boot mailbox
dispatcher, which is not host-reachable on this part (hardware-tested uniform NAK) — so
Route C reaches those two registers from a host-reachable path, with an arbitrary value
instead of a fixed RMW. What the bits gate is unknown and not statically resolvable.
VCN half (types 0x31/0x32), bounded by later measurement: the index/data port is
identified from the firmware's own table as regUVD_DPG_LMA_CTL/_DATA — the VCN
DPG shadow-register port, not a general register file. Inverting the kernel's
SOC15_DPG_MODE_OFFSET shows the 8 firmware-overwritten entries are exactly the VCN ucode
window (VCPU_CACHE_OFFSET0/1, CACHE_SIZE0/1, LMI_VCPU_CACHE_64BIT_BAR_LOW/HIGH,
CACHE1_BAR_LOW), and the single bounded entry (index 195) is VCPU_CACHE_SIZE1 capped at
1 MB — i.e. the loader pins where the VCN VCPU fetches its firmware. It leaves the
NC0/NC1 BARs and LMI_VCPU_CACHE_VMID host-controlled. On hardware, with VCN un-gated and
its register file proven readable/writable, an accepted call carrying 56 host-controlled
entries changed nothing (full-aperture diff, drift baseline 1; and direct LMA writes
latch nothing in five variants). So the writes take effect only via a running DPG state
machine — the realistic risk is injection into a legitimate VCN bring-up, not a standalone
write. Cite as "completes a register-write loop over attacker-controlled entries", never
"arbitrary VCN register write".
Detail: VULN-LOADIPFW-UNSIGNED-REGWRITE.md, FINDING-VCN-GATE-OPENS-VIA-SMU-POWERUP.md
1.9 sOS SVC 0xF2 — insufficient headroom in the third msg range check (NEW)
sos_proc_switch (0x22B4) validates the caller-supplied msg pointer against three
windows. Type 5 (TA) and type 1 reserve 0x1000 bytes of headroom (sub.w r2,r2,#0x1000
@0x238E). The third window, [0xD00000,0xE00000) (ASD's space), reserves only 0x28
(add.w r2,r6,#0x28 @0x23C4) — but the callee, SYS_DRV's sysdrv_message_handler
(0xE087E4), reads and writes 0x7C (124) bytes at that pointer.
0x28 < 0x7C, so msg in [0xDFFF85, 0xDFFFD8] passes validation yet is accessed up to
84 bytes past the window end — at 0xE00000, which is SYS_DRV's own load base.
Verification: check_procswitch_headroom.py extracts both constants from the shipped
images (slack from sos2.elf, access size from sys_drv2.elf) — not from the write-up.
Reachability (honest): latent, not host-triggerable. The caller must supply the
crafted msg; ASD's three task stacks top out at 0xD3D338/0xD38330/0xD42340 (read from its own
SVC 0x51 descriptors), all ~786 KB below the 0xDFFF85 danger zone, so normal ASD
frames can never reach it and the region is unmapped in the shipped configuration — the likely outcome is a data
abort inside SYS_DRV (privileged DoS), becoming an 84-byte OOB read/write only if that
region is ever mapped. Defence-in-depth / post-compromise, not a remote primitive.
Novelty: AUDIT-TA-KERNEL-BOUNDARY.md declared SVC 0xF2 "clean" and enumerated
only two of the three windows; this corrects that verdict.
Detail: VULN-SOS-PROCSWITCH-MSG-HEADROOM.md
1.8 ASD SRM v9 walker — OOB read, walk length decoupled from stored length [EMU] (NEW)
asd_srm_sub_12DB0 (0xD12DB0) fetches the stored SRM length, tests it for
nonzero only, then discards it and takes the record count from blob bytes
[8..9] >> 6 (0..1023). The walk covers buf[12 .. 5126], past the end of the
5120-byte asd_srm_buf, and treats never-stored bytes as revocation records.
Distinct root cause from 1.7 (which bounds the offset but not the extent).
Proof: real code under Unicorn from a 48-byte store: 1015 reads beyond the
stored data, 1 past the buffer end, max offset 5122 (poc_srm_v9_walker_oob.py).
Reachability: same call path as 1.7, and equally signature-gated — latent.
Novelty: this function had never been analysed in any prior document.
Detail: VULN-ASD-SRM-V9-WALKER-OOB-READ.md
1.7 ASD SRM revocation walker — out-of-bounds read [EMU] (NEW)
asd_srm_sub_12D4C (0xD12D4C) walks the stored SRM receiver-ID list. The loop
guard v5 < len-43 bounds the record offset only; the body then reads
buf[v5+9 .. v5+9+5*count-1] where count = buf[v5+8] & 0x7F (<=127), i.e. up to
v5+643. With a legitimate, in-bounds, maximum-size SRM (len = 5120, the true
size of asd_srm_buf at 0xD1F038), the walk reads to offset ~5719 — ~600 bytes
past the buffer, into asd_srm_stored_len and following globals. No malformed or
oversized store is needed.
Reachable (path): host INVOKE_CMD -> HDCP cmd 3 or 4 -> disp_srm_query
(SVC 0xF2, opcode 0x302) -> asd_srm_query -> this walker.
CORRECTED: an earlier revision of this entry claimed the blob is planted "via the
already host-reachable 0x308 store". That was overstated. Both writers of
asd_srm_buf (asd_srm_store 0x308 and asd_srm_op_301) require
asd_srm_header_check(...) == 0, i.e. an RSA verify via asd_srm_verify_t8/t9.
Only the 0x308 OOB read (1.3) is unauthenticated; the store is signature-gated.
This finding is therefore latent (content-gated), not freely host-triggerable.
Proof: real ASD code executed under Unicorn — 120 reads at/beyond +5120, max
offset 5715 (596 bytes past), at the expected 5-byte record stride
(poc_srm_walker_oob.py).
Impact: memory-safety violation + weak 1-bit disclosure oracle (the OOB bytes feed
memcmp_be whose result is returned: 13 = match, 0 = no match). Not control-flow:
the region after the buffer is pointer-free (per 4.2).
Corrects a prior assessment: VULN-ASD-308-BYPASS.md recorded this same function
as "v6 bounded by a length cap". The cap bounds the offset, not the read extent.
Detail: VULN-ASD-SRM-WALKER-OOB-READ.md
1.6 err_state latch — permanent per-slot DoS [HW]
Several HDCP error paths set sess->err_state = 7/8 with no reset path, so a
single malformed command permanently disables that session slot for the lifetime
of the loaded TA.
Impact: DoS; also confounds testing (one observation per freshly-opened slot).
2. Latent findings (real bugs, NOT host-reachable)
2.1 SYS_DRV opcode 0x3103 — controlled ~15 KB overflow [EMU]
Two calls to sub_E0F364: the first stores an attacker dword as a slot
copy-length (v3[10]), the second copies v3[10]+76 bytes → 3840 OOB writes,
+0x4048 past the slot table (~15,356 bytes) of attacker-controlled content.
Definitively NOT host-reachable — closed by a whole-image scan of all 89
SVC 0xF2 call sites across all four loadable TAs (HDCP 49, ASD 25, RAS 12,
DTM 3):
- every site loads its opcode from an immediate — no TA ever forwards a host-controlled opcode; and
-
no
0x31xximmediate exists anywhere in any TA image. Correction (later round): the ACL capability check (sub_E05A24entry [4], mask0x000ffff0viaSVC 0x76) was previously cited here as an additional gate. That is wrong — the host-request context demonstrably holds that capability (GFX cmd 15 reaches SYS_DRV opcode0x3127, inside the same ACL range, and returns real data). The verdict survives on two stronger grounds: -
Emission — no host path selects a SYS_DRV opcode (GFX commands map to fixed rpc stubs), and no TA issues
0x31xxor forwards a host opcode. - The length is pinned at zero — the slot allocator zeroes the stored copy length at
+0x28, the enqueuer never writes it, and the only emitter (an sOS service task) hands0x3103the very buffer0x3102filled from that same field. It is a fixed point at zero, so the copy is invariantly 76 bytes. The same argument closes the0x3102direction (sOS task-stack overflow). Arming the overflow requires an actor who can already emit arbitrary SYS_DRV opcodes, i.e. one who has already compromised sOS. Report as latent / defence-in-depth only. Detail:VULN-SYSDRV-3103-OVERFLOW.md
2.2 ASD opcode 0x202 — uninitialised-stack canary disclosure [EMU]
sub_D14948 copies a never-initialised int src[4] to the caller's output
buffer. Priming with op 0x308 first (whose 0x1400-byte frame saves the guard)
deterministically discloses __stack_chk_guard at output+0x18 — proven in the
emulator with distinct sentinels.
Not host-reachable: ASD rejects direct INVOKE_CMD (GFX status 4 — it is an
RPC-only/system TA), and no host-reachable TA forwards opcode 0x202 (confirmed
by the same 89-site scan). Detail: VULN-ASD-202-CANARY-LEAK.md
2.3 Weak stack-pointer ASLR in sOS [ST]
sos_proc_switch (sub_22B4) derives the per-process stack pointer from a
textbook MSVC LCG (state = state*0x343FD + 0x269EC3, state @ 0x80F0):
SP = stackbase + 0x4000 - (LCG & 0xF8) → only 31 distinct 8-byte-granular
offsets (~5 bits), from a trivially predictable PRNG. This is not the stack
canary (that is loader-supplied and unpredictable), but it is a genuine ASLR
weakness. Detail: GUARD-INIT-CHAIN.md
3. Significant negative results (hardening that held)
These matter for a report: they bound the attack surface and show the failures above are isolated rather than systemic.
3.1 Every other host-reachable input path is correctly validated [HW]:
| Surface | Result |
|---|---|
LOAD_TA $PS1 header |
Signature/hash-verified before parse; every mutated size/offset field → clean reject (status 52) |
buf_list structure |
count ≤64 (status 42), size ≤64 MB (status 5), 8 MB protected region excluded, pool-bounded bookkeeping |
| DTM cmds 2/3/4 | Bounded (a1[9] ≤ 0x17, fixed-size fills) |
ASD 0x100/0x101 |
Index-bounded (≤63), source validated, fixed-offset copies |
ASD 0x300 |
Correctly bounded (if (*v9 > a2) return 2) — this is exactly the check 0x308 is missing |
| HDCP all subcommands | Bounded; subcmd17's length is a byte load (LDRB.W), max dst[257] — 11 bytes short of the canary. 400-iteration HW fuzz: clean |
PROG_REG (cmd 0x0B) |
Table-validated (reg_id 3–15 accepted, others rejected); no OOB indexing |
| Session management | Liveness-validated — freed/double-unload/bad/cross session_id all rejected (279). No UAF |
GET_FW_ATTESTATION |
Robust to malformed output buffer addr/size |
| TA image sections | Segment content is AES-decrypted + hash-verified per segment |
3.2 No host-reachable stack-guard disclosure exists. All four loadable TAs
were audited: HDCP handlers write status ints only and ta_memclr their builder
buffers; DTM and RAS are status/telemetry only; every host-reachable ASD op is
length-bounded or writes only specific caller fields. The one real leak (2.2) is
unreachable.
3.3 RAS error injection is non-functional on this SKU. The RAS TA loads and
accepts cmds 0–4, but its command handlers are runtime function pointers
installed by ras_drv — and this card's firmware bundle contains no
PSP_FW_TYPE_PSP_RAS_DRV (sos_fw = SYS_DRV, SOS, TOC, KDB, SPL, RL only).
All RAS invokes return status 15. The classic RAS "inject error at host-supplied
address" write primitive therefore does not exist here.
3a. Coverage corrections (scope of earlier "clean" verdicts)
Two earlier verdicts were narrower than their wording suggested. Neither was wrong for what it tested; both were over-generalised.
- "C2P mailbox NOT host-reachable (uniform
0x8000000aNAK)" tested C2PMSG_35/36 only. The TSU flash/update mailbox is at C2PMSG_115/116 (since probed — see the TSU note in section 2; the mailbox is host-reachable but the disclosure chain behind it is not, on every path tested). Its host-access bits are opened by the firmware itself — verified in IDA:|= 0x3C0on the permission word intsu_mailbox_task(0xE1CAA4) andirq_enable_mask(0xE0CE46), alongside|= 0xF(init_mailbox_state),|= 0xC00,|= 0x3000,|= 0xC000000.tsu_mailbox_dispatch(0xE1C948) is a real command dispatcher (cmd byte in BYTE2, unknown ->0xFFFF000A). - "SMU mailbox surface audited on hardware — CLEAN" drove the driver port (C2PMSG_66/82/90). The test port (72/96/98) and its register-file message ids were never touched.
4. Retracted claims — DO NOT CITE
These appeared in earlier notes (sometimes in file titles) and are wrong:
- "ASD 0x308 gate fails open → controlled OOB write." RETRACTED. The gate
sub_D134C4performs a real check (accepts only0or0xFFFF000A). A 480-combination header fuzz on hardware produced zero write-branch executions (*a3stayed0xFFFF,*a4stayed0in every case). Reaching the write needs a valid RXID MAC, which is unforgeable.0x308is a read fault (WnR=0) → DoS. - "Function pointers live in the ASD 0x308 OOB region." RETRACTED — false
positives from a crude BLX-proximity heuristic. A rigorous load-then-call scan
over
[0xd1f038,0xd2f038)returns zero; the region is a pointer-free receiver-ID data buffer plus counters. - "ASD 0x308 is an RCE primitive." RETRACTED — follows from 1 and 2.
- "subcmd12 branch-B write is hardware-verified." OVERSTATED — the write is [ST+EMU]; only the trigger path is [HW]. The corrupted bytes were never read back and have no hardware-observable effect.
- "The stack guard is the build-time placeholder
0x33333333(→ predictable → RCE)." RETRACTED by decisive HW test (N=56 fill landing exactly on the canary still faults). The guard is loader-randomised. - "SYS_DRV 0x3103 may be host-reachable." CLOSED as not reachable — see 2.1.
5. Overall assessment
The host-facing PSP surface is broadly well-hardened: length/count/size bounds
on essentially every copy, signature-verify-before-parse on TA images,
cryptographic gating on the one sensitive OOB write, table validation on register
programming, and session liveness checks. Three independent full-surface sweeps
found no host-triggerable memory-corruption primitive beyond the canary-gated
subcmd12 overflow. A later targeted pass over the SRM/revocation opcode family
added one further host-reachable memory-safety defect (1.7, an OOB read), which
also corrects an earlier "bounded" assessment of that walker.
The defects that do exist cluster into two classes:
- Missing state rollback (1.1, 1.6) — commit-before-verify and latched error states.
- Length re-derivation after validation (1.2, 1.3) — a length is validated,
then recomputed from attacker-controlled data and used for a second copy.
This is the single recurring root cause and the most useful thing to fix:
0x300gets it right (if (*v9 > a2) return 2) and0x308does not. - Bounding the index instead of the access extent (1.7) — the SRM walker
validates the record offset but not
offset + 9 + 5*count.
Highest-value hardening recommendations, in order:
- Re-validate any length that is re-derived from attacker data before use
(ASD
0x308; HDCPsubcmd12). - Do not commit session state before authentication succeeds; add rollback
(HDCP cmd
0x0E), and provide a reset path forerr_state. - Replace the MSVC LCG feeding stack-pointer ASLR with the hardware TRNG.
- Bound/ratelimit
buf_listdescriptor faults and reject a nullLOAD_TOCaddress so a malformed command cannot wedge the device.
