aboutsummaryrefslogtreecommitdiffstats

AMD PSP firmware audit — authoritative consolidated findings

Not the authoritative document for this topic. See VULN-AUDIT-WHAT-WE-ACTUALLY-HAVE.md, and INDEX.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: * DMCUB ABM_INIT_CONFIG unchecked u16 -> ~32 KB OOB write [HW] * DMCUB type-8 unbounded panel_inst -> 8-byte leak [HW] * DMCUB REG_REG_WAIT arbitrary-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-firmware that 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: * 0xFE0D decrements the session refcount and triggers the ASD release on a rejected handle — its sibling FE0B is correct, which is what makes it a defect; * 0xFE0F returns handles for crypto slots that are not live.

Also: GCP reaches ASD 0x202 and OPM reaches ASD 0x301, 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.md and the two fw/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 (PC 0x40433c, FAR 0xdeadbeef) — [HW] Impact: DoS-grade memory corruption. Escalation is canary-gated: the stack guard is loader-randomised (decisive HW test — N=56 with a 0x33 fill 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 the err_state latch) — 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):

  1. every site loads its opcode from an immediate — no TA ever forwards a host-controlled opcode; and
  2. no 0x31xx immediate exists anywhere in any TA image. Correction (later round): the ACL capability check (sub_E05A24 entry [4], mask 0x000ffff0 via SVC 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 opcode 0x3127, inside the same ACL range, and returns real data). The verdict survives on two stronger grounds:

  3. Emission — no host path selects a SYS_DRV opcode (GFX commands map to fixed rpc stubs), and no TA issues 0x31xx or forwards a host opcode.

  4. 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) hands 0x3103 the very buffer 0x3102 filled from that same field. It is a fixed point at zero, so the copy is invariantly 76 bytes. The same argument closes the 0x3102 direction (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 0x8000000a NAK)" 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: |= 0x3C0 on the permission word in tsu_mailbox_task (0xE1CAA4) and irq_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:

  1. "ASD 0x308 gate fails open → controlled OOB write." RETRACTED. The gate sub_D134C4 performs a real check (accepts only 0 or 0xFFFF000A). A 480-combination header fuzz on hardware produced zero write-branch executions (*a3 stayed 0xFFFF, *a4 stayed 0 in every case). Reaching the write needs a valid RXID MAC, which is unforgeable. 0x308 is a read fault (WnR=0) → DoS.
  2. "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.
  3. "ASD 0x308 is an RCE primitive." RETRACTED — follows from 1 and 2.
  4. "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.
  5. "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.
  6. "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: 0x300 gets it right (if (*v9 > a2) return 2) and 0x308 does 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:

  1. Re-validate any length that is re-derived from attacker data before use (ASD 0x308; HDCP subcmd12).
  2. Do not commit session state before authentication succeeds; add rollback (HDCP cmd 0x0E), and provide a reset path for err_state.
  3. Replace the MSVC LCG feeding stack-pointer ASLR with the hardware TRNG.
  4. Bound/ratelimit buf_list descriptor faults and reject a null LOAD_TOC address so a malformed command cannot wedge the device.