Index — what to read, what is settled, what is next
131 documents accumulated organically. This is the map. Read this first; do not cite a document without checking its row here, because several files have titles that outlived their conclusions.
Evidence classes used throughout: [HW] confirmed on silicon · [EMU] proven executing real firmware under Unicorn · [ST] static only.
1. The spine — four documents that govern the rest
| document | role |
|---|---|
CONSTRAINTS.md |
Standing rules. Read before touching hardware. Includes the flash-write prohibition and the no-blind-SMN-sweep rule. |
VULN-AUDIT-WHAT-WE-ACTUALLY-HAVE.md |
The vulnerability register. Every VULN-* re-checked against its own evidence, tiered by reachability. The authoritative answer to "what do we have". |
ATTACK-SURFACE.md |
The surface map. Host-reachable entry points, layer by layer, with per-handler audit state. |
INDEX.md (this file) |
Status of everything else, duplicate resolution, priorities. |
Three further files overlap as "summaries" and should be treated as historical
narrative, not as the register: DISCLOSURE-SUMMARY.md, FINDINGS-SUMMARY.md,
FINDINGS.md. Where they disagree with VULN-AUDIT-WHAT-WE-ACTUALLY-HAVE.md,
the register wins.
2. RETRACTED — do not cite (10)
These carry retraction banners. The filenames still read like findings; the contents say otherwise.
FINDING-LIVE-VERSION-IS-53-NOT-54.md ·
FINDING-SYSDRV-VCCC-DOUBLEFETCH-UNDERFLOW.md ·
LEAD-SYSDRV-LOADIPFW-DOUBLEFETCH.md ·
FINDING-SYSDRV-CHIPLET-UNBOUNDED-INDEX.md ·
FINDING-SYSDRV-SHAREDMEM-RANGE-FIXED-IN-54.md ·
FINDING-SOS-CCP-WINDOW-FIXED-IN-54.md ·
AUDIT-SOS-SRC-POINTER-TOCTOU.md ·
LATENT-RAS-PARAM-ARRAY-OOB.md ·
VULN-ASD-308-BYPASS.md (partially — see its banner) ·
DISCLOSURE-SUMMARY.md (contains retracted claims inline)
Plus one claim (not a whole file) retracted in ../dmcub/README.md: that
DP_AUX_ACCESS (70) being a stub in 3.2.0 and 4.0.1 means "there is no AUX sink path
via that command in any image we hold". It is implemented in yellow_carp,
dcn_3_1_4, dcn_3_5 and dcn_3_6, and carries FINDING-11 —
../dmcub/AUDIT-DMCUB-07-DPAUX.md. The underlying round-3 bullet was correctly scoped
"in this build"; only the README generalised it.
3. Duplicate clusters — which file is authoritative
| topic | authoritative | also covering it (read only for detail) |
|---|---|---|
| HDCP subcmd12 overflow | SUBCMD12-RESOLVED.md |
WRITE-PRIMITIVE.md, VULN-HDCP-BRANCHB-HANGFREE-OOB.md, HANG-FREE-OVERFLOW-LEAD.md, HANG-ROOT-CAUSE.md, ANALYSIS-HDCP-OVERFLOW-IS-DEFINITIVELY-DoS.md |
| ASD 0x308 | VULN-ASD-308-REACHABLE.md |
VULN-ASD-308-BYPASS.md (banner) |
| ASD SRM walker | VULN-ASD-SRM-WALKER-OOB-READ.md |
VULN-ASD-SRM-V9-WALKER-OOB-READ.md (v9 variant, same gate) |
| Route C / LOAD_IP_FW register writes | VULN-LOADIPFW-UNSIGNED-REGWRITE.md |
RERATE-ROUTEC-TARGET-MAY-BE-VCN.md (refuted, read for the method) |
| VCN type 49/50 write path | FINDING-VCN-GATE-OPENS-VIA-SMU-POWERUP.md |
FINDING-LOADIPFW-VCN-REGWRITE.md (original static analysis) |
| TMR fence | FINDING-TMR-FENCE-IS-TAG-AGNOSTIC.md |
ANGLE-PSP-AS-CONFUSED-DEPUTY-WRITER.md (refuted), VULN-CONFIRMED-TMR-RANGE-ROLLBACK.md (the DoS) |
| Firmware rollback | CONFIRMED-FIRMWARE-ROLLBACK-WORKS.md |
FINDINGS.md, VERSION-AUDIT-STATUS.md |
| External input channels | ANGLE-EXTERNAL-INPUT-CHANNELS-RANKED.md — env-facing SVC census per module (sys_drv 117 reads/191 writes is the largest, TAs touch no hardware); modelling SYS_DRV register replies opens ~1% more blocks; SMU replies, fuses and flash-at-boot untested. |
|
| Display sink end-to-end | psp_emu/harness/display_sink_e2e.py — modelled monitor register data driven through real sOS + ASD + HDCP; read counts differ 21/1021/524 by sink value, both controls pass. Chain: platform-id byte -> ASD registers -> DTM cmd 2 attaches a display -> HDCP cmd 0x01 -> RPC 0x103 -> ASD svc 0x7b reads. |
|
| Display/DDC input surface | ANGLE-DISPLAY-SINK-INPUT-SURFACE.md — 10 display-service RPC ids, 49 call sites in the HDCP TA, many with write-back pointers; two firmware parsers already consume sink-derived data. TA invoke in full_system WORKS (cmd_buf_len > 0x2817); the chain stops earlier, at sub_404626. |
|
| HDCP sink / display surface | VULN-HDCP-SESSION-STATE-BYPASS.md — gate A bypass HW-proven; gate B semantics confirmed by execution; gate B provenance (can a malicious sink choose it?) OPEN, blocked on full_system TA invoke. Harnesses hdcp_malicious_sink.py, hdcp_sink_to_overflow.py are component-level and must NOT be cited as evidence about SYS_DRV's handling. |
|
| External source: AMD SP tree | SOURCE-AMD-SP-TREE-WHAT-IT-GIVES-US.md (constants adopted, verified), ARCH-INSIGHTS-FROM-AMD-SP-SOURCE.md (structure: capability-gated SVCs, indexed fabric windows, PSP as fuse-distribution master, OTP burn sequence) |
|
| SMN / aperture map | SMN-MAP-HEALTHY-CARD.md |
CARD-STATE-ACCESS-INVENTORY.md (historical, card was broken), FINDING-NO-PSP-MEMORY-APERTURE.md, NV22-MEMMAP-MINED.md |
| Emulator bring-up | FINDING-SOS-AND-TAS-BOOT-IN-EMULATOR.md |
FINDING-FULL-SOS-BOOT-IN-EMULATOR.md, FINDING-HOST-MAILBOX-AND-RING-IN-EMULATOR.md (1188 lines, largest) |
| Hardware validation | HW-VALIDATION-RX7900XTX.md |
HW-TA-VERSION-REPRODUCTION.md, HW-RAS-ON-CARD.md |
4. Settled negatives — do NOT re-run these (17)
Each cost real effort and closed cleanly. Re-running them is the most likely way to waste a session.
FINDING-NO-CONSUMABLE-CORRUPTION-ANYWHERE.md ·
FINDING-NO-PSP-MEMORY-APERTURE.md ·
HUNT-HOST-REACHABLE-HEAP-CORRUPTION.md ·
READ-PRIMITIVE-HUNT-HDCP.md ·
AUDIT-OBJECT-LIFETIME-AND-STATE.md (session UAF, [HW]) ·
AUDIT-SMU-I2C-NUMCMDS.md ·
AUDIT-ROUND-NEW-VULN-HUNT.md · AUDIT-ROUND-RAS-DTM-SYSDRV.md ·
AUDIT-TA-KERNEL-BOUNDARY.md · HDCP-TA-AUDIT.md ·
FINDING-HDCP-VERSION-HISTORY-NEGATIVE.md ·
FINDING-SMUIO-AND-UNKNOWN-BASES.md ·
FINDING-CORRECTED-GUARD-SWEEP-COMPLETE.md ·
FINDING-RAS-IS-REACHABLE-AND-AUDITED.md ·
CLOSURES-VERSION-DIFF-ARC.md ·
LEAD-SYSDRV-DMA-TRANSFER-CLIENT-BUFFER.md (closed this session) ·
RERATE-ROUTEC-TARGET-MAY-BE-VCN.md (refuted this session)
Also closed this session and not listed above: loader type confusion
(FINDING-ASD-IS-DRIVER-CLASS-NOT-INVOKABLE.md), TA rollback widening the ASD
opcode surface, and the DF fabric-config port (NV22-MEMMAP-MINED.md).
5. Methodology — worth reading before starting anything
| document | lesson |
|---|---|
TOOLING-EMITTER-SCAN-FIX.md |
a scanner that misses one store form invalidates every "nothing emits X" conclusion built on it |
METHODOLOGY-INLINING-VS-ADDED-GUARDS.md |
version-diff pitfalls |
HARNESS-TA-FUZZING-CORRECTIONS.md |
four harness bugs that invalidated earlier results |
FINDING-ASD-SVC-STUBS-LOAD-BEARING.md |
a fabricated stub return that shaped 48% of coverage |
The recurring failure mode, stated once: a measurement produces a confident result that its own control invalidates. This session alone: a mirror-test read that echoed the address, a monkeypatch that silently never fired, an MMIO window that returned one constant for 2560 registers, an entry-point hook that counted a function as "executing" when its body never ran, and an opcode sweep whose controls were equally unreached. Every negative needs a positive control that the detector can find, established before the measurement is trusted.
6. Priorities — what to look at next
Ranked by (value if it lands) x (probability) / (cost), with the blocker named.
P1 — VCN type 49: get an observation channel
FINDING-VCN-GATE-OPENS-VIA-SMU-POWERUP.md. The only live capability in the
tree: host-reachable, pre-signature, 63 attacker-controlled register writes per
call, ABI measured, parser proven to consume our entries. Blocked on: no
validated read path into the VCN register space — the MMIO window tried returns
a single constant across 2560 registers. Next concrete step: find any VCN
window that passes a decode control (many distinct values, stable across two
no-command snapshots); only then re-attempt write observation. If no such window
exists, the capability stays unobservable and should be reported as such.
P2 — fuse / debug-unlock reachability, round 2
LEAD-FUSE-DEBUG-REACHABILITY-ROUND1.md. fuse_program_bits,
debug_unlock_apply et al appear in no other document. Round 1 established the
method and proved the experiment under-powered; round 2 measured that they are
not on the boot path (only dma_transfer is). Blocked on: per-opcode
valid arguments. Coverage gate: a negative means nothing until
spirom_write_checked is actually reached by the sweep.
P3 — sOS 0xE575B8 — RESOLVED: the harness was at fault, not the firmware
Hardware proved the context is populated on a real boot
(HW-RAS-ON-CARD.md), so this is a missing-state gap with a known destination.
Closing it unblocks RAS command coverage, and there is now a hardware oracle to
validate against: 1 -> 0x0, 2 -> 0xa002, 3/4 -> 0xa010, 5+ rejected.
Resolved. 0xE575B8 is a 32 x 84-byte table (bzero2(0xE575B8, 0xA80) at
init), written by the slot allocation at 0xE1CD76. In full_system.py that
allocation runs 27 times and the 0x000d bound check at 0xE1D3AC
passes every time — r0 = 0x20 requested against r6 = 0x40 capacity. So
the capacity is populated in a real boot, as the hardware result predicted, and
the 0xffff0010 failure was an artefact of sysdrv_bridge.py's synthetic
environment.
Remaining step is a driving problem, not a state problem. Progress:
harness/ras_in_fullsys.py drives RAS over the host ring inside full_system.
LOAD_TA of ras_full.bin executes 39 new RAS blocks (8 -> 47) and the
0x000d bound-check count rises 27 -> 36, all still passing — so RAS-driven
SYS_DRV RPCs now run against populated state.
Both original plumbing faults are now fixed in harness/ras_in_fullsys.py: the
session id is read back from the command buffer's response area, and the gate PA
is latched per address space. The second fix exposed a trap worth keeping —
HDCP, DTM and RAS all live at VA 0x400000 in separate address spaces, so
0x4060E0 exists in all three and resolves to three different PAs. Only RAS's
copy is the init gate; HDCP's reads 0x90, which an earlier version of this
harness reported as "GATE OPEN". Ownership is now attributed by the trampoline
VA that revealed each PA.
Current state: LOAD_TA drives 39 new RAS blocks; INVOKE_CMD executes
zero. Field placement was probed empirically at seven offsets
(0x10..0x28) — none produced RAS execution.
RETRACTED — the oracle was fine; the readback was wrong. The firmware
writes the response into the ALIAS copy of the command buffer
(cmdbuf + 0x04000000), not into cmdbuf. Reading cmdbuf returns a stale 0
for every command, which looks exactly like a flat oracle. Ring.response()
now reads the right address, and the emulated statuses match hardware 5/6:
cmd 0x00/0x55/0x99/0xfe -> 0x100 MATCH (unknown command)
cmd 0x03 INVOKE_CMD -> 0x117 MATCH (bad session)
cmd 0x02 UNLOAD_TA -> 0x11a vs hw 0x117 (state-dependent)
With the correct readback, the RAS picture changes: LOAD_TA returns
0xffff000b, not success. It was only ever reading as 0 because of the stale
address, so "LOAD_TA registers no session" was really "LOAD_TA fails". Finding
why it returns 0xffff000b is the next concrete step.
Layout resolved statically, and the emulator's invoke path is faithful.
Reading tee_cmd_dispatch (VA 0x2027C4): it copies 0x400 bytes of the host
buffer to dst = sp+0x18, takes the command id from dst+8, defaults the
status to 0x100 for an unknown id (matching hardware), and for cmd 3 reads
dst+0x1C before calling the invoke handler at 0x204F28 with dst.
So the 0x1C placement was structurally correct all along.
Instrumenting that handler settles the question: it is entered, and returns
0x117 — precisely the status hardware gives for a bad session
(HW-VALIDATION-RX7900XTX.md). So the emulated invoke path is behaving
correctly; it is the session that is wrong, and the flat response area is what
hid that.
Sweeping session ids 0..7 returns 0x117 for every one, so
LOAD_TA over this path never registers a session even though it drives 39
RAS blocks and reports success.
The gap is now two narrow, concrete defects rather than a mystery:
LOAD_TAthroughfull_system's ring does not create a TA session.full_system's ring response area is never populated (always 0, even for an invalid command id), so the session id cannot be read back — which is also why the status oracle was flat.
Fix (1) and the hardware oracle can finally be applied; (2) is needed for any ring-status comparison in this harness.
Superseded description of the original two faults:
INVOKE_CMDnever reaches RAS (0 new blocks). It is submitted with session id 0;LOAD_TAreturns the real session id in the command buffer's response area (offset 0x360 on hardware) and that is not yet read back.- Gate-byte ambiguity.
0x4060E0's VA is only mapped while RAS's context is live, so the harness latches its PA — but the latched0x41E20E0belongs to the boot-loaded RAS, while the ring load creates a second instance at a different PA. The gate must be read on the instance that was actually invoked, or the reading is meaningless.
P4 — Route C window identity — RESOLVED
Resolved (FINDING-ROUTEC-WINDOW-IDENTIFIED.md): vawin_map_fixed splits
the address into a 1 MB VA-window selector and a 20-bit offset, so the window is
PSP-internal physical space, not host SMN. sos_check_va_window_request
guards exactly [0x3700000,0x3800000) and [0x3900000,0x3A00000) plus an
overflow case — all verified by direct calls with controls — and Route C's
window is outside all of it (returns 0 = allowed). The window holds at least
the bootloader's boot-stage registers. Remaining: enumerate what else the
firmware touches in those 1656 bytes.
Not a priority — a standing exclusion, recorded so it is not mistaken for an oversight
The TSU flash/update mailbox (FINDING-TSU-MAILBOX-HOST-REACHABLE.md) is
arguably the most serious finding in the tree: a confirmed-live, unauthenticated,
host-reachable command interface into the flash/update subsystem, whose
commands 18-23 are gated on a byte that is 0x00 in the shipped image — so
spi_set_bank_protect(0) and bootrec_flush() are reachable, and the implied
chain rewrites the boot-priority record with no signature in the path.
It is dormant by user constraint, not by lack of merit: CONSTRAINTS.md
prohibits issuing flash commands to the card, and the chain has deliberately
never been executed. Only read-only commands (0x01, 0x0B) plus two
unknown-command controls were ever sent.
The same exclusion covers the actual bug in that path, which is more serious
than the unsigned-chain hypothesis: inside the TSU 0x04 update flow,
tsu_write_active_slot calls tsu_write_block with the verify flag hardcoded
0, skipping tsu_verify_region; spi_write_status then bounds the
destination but never bounds the source pointer
(mapped_base + src_off, src_off from the host-supplied image, past only
wrapping checks). The bytes land in flash and are readable back — an arbitrary
read of PSP memory. Traced step by step in
ANALYSIS-PSP-CODE-EXEC-AVENUES.md, independently confirming the other
researcher's §4.1c.
Reachability: negative on all host-reachable paths (round 7). Four checks — init
write-watch, chain write-watch, a single static reference, and a live survey of all
twelve reachable TSU mailbox commands — agree that the table tsu_verify_region consults
is never populated, so the a6 = 0 site is not reachable through tsu_write_active_slot
from the mailbox. The code path and its attacker-controlled source stand; the trigger
does not.
Earlier caveat, kept for history: an earlier step in the same
state machine calls tsu_verify_region (with a6 = 5), and with tsu_write_block's
real body running the chain aborts there rather than reaching the a6 = 0 site. The
abort is a key lookup against an unpopulated SYS_DRV table, not a signature check, so
neither "gated" nor "reachable" is established. See
FINDING-TSU-CHAIN-E2E-IN-EMULATOR.md round 2.
That matters because it is the one thing the whole tree lacks: every escalation path dies on the HDCP overflow being canary-blocked with no disclosure to leak the guard. This is a disclosure — identified, traced, and dropped by constraint rather than by evidence.
If it is ever re-authorised, the prerequisites are already written down: an external programmer attached and a verified flash dump in hand. Nothing about it is ATX-recoverable.
F32 (RLC/SDMA) is readable — the tree's oldest blocker is lifted
../f32/AUDIT-F32-01.md, tool ../f32/f32_audit.py. fail0verflow's radeon-tools
ships an F32 disassembler written for Sea Islands CP/RLC; it works on gfx11 with
container parsing and load-base recovery added.
First SDMA audit pass (../f32/AUDIT-SDMA-01.md, tool ../f32/sdma_audit.py):
the packet dispatch table at the tail of TH0 is recovered and validated against
navi10_sdma_pkt_open.h (key = (op << 8) | (sub_op << 4), 52 packets), and the
taint source is exact — upstream documents r1 as "pulls dwords off the command
queue", so mov rX, r1 is host data. SRBM_WRITE, the arbitrary-register-write
packet, is gated (0x4a14 bit 9 OR 0x5968 bit 31, AND 0x5940 bit 23) with a reject
path raising error 0x3c/0x32. The unpublished op3 sub-ops are just "drain N dwords"
stubs — a clean negative. No finding yet; the blocker is that the gate registers
cannot be named until the F32 register namespace is mapped, which decides whether
that map is a security boundary or mode selection.
[HW] the gate is RB_PRIV, and the confinement holds (../f32/hw/sdma_srbm_probe.py,
evidence EVIDENCE-sdma-srbm-gate.txt). Static naming of the F32 register space failed
honestly (best fit 25/54 vs a null 95th pct of 24 = chance), so the gate was named by
behaviour: RB_PRIV is bit 23 of SDMA*_QUEUE*_RB_CNTL, matching the gate's
lsr #23; and #1 term. On silicon, an SDMA ring packet did write an arbitrary
register (SRBM_WRITE → GRBM_SCRATCH_REG0 = 0x5100d00d) with RB_PRIV=1, and did
not with RB_PRIV=0 — while a NOP control still retired, isolating the difference
to that one bit (a from-scratch unprivileged queue does not fetch at all, so the naive
version of this test proves nothing). So F32 index 0x5940 = the queue's RB_CNTL —
the first F32 register named in this tree — and SDMA's arbitrary-register-write is
properly confined to privileged queues: a confirmed negative on what would otherwise
have been a user→kernel escalation.
[HW] PTEPDE is ungated — and that is correct. The page-table-write packet has no
privilege check (an earlier draft of the SDMA doc called 0x5a6c "PTEPDE's gate"; that
was wrong — the branch picks an implementation, not a permission, and is corrected in
place). On silicon PTEPDE writes with RB_PRIV=0 on the very queue that refuses
SRBM_WRITE. The follow-up decides whether it matters: addressing the same buffer by raw
physical address instead of MC address raises a GCVM L2 protection fault
(walker_error=1, mapping_error=1) and writes nothing. So memory packets are confined
by the queue's VMID page tables while register packets are gated by RB_PRIV — a
coherent split, and a confirmed negative rather than a finding.
RLC first pass — deliberately ranked low (../f32/AUDIT-RLC-01.md). RLC uses a
different addressing convention from SDMA: 67% of its register indices are odd, so
the byte-address model is impossible; a dword-index model fits instead (18/82 and 27/106
named vs nulls maxing at 4 and 3, base ~0xab80). That mapping is partial (~17%), and
an attempt to fit per-block bases for the rest produced 21 mutually inconsistent
"significant" candidates — recorded as overfitting to avoid repeating it. The decisive
column is r1: read once per RLC blob versus 370 times in SDMA TH0, so RLC has
no host packet surface — its inputs are driver-written registers and kernel-built
lists. On this consumer card there is therefore no trust boundary to cross; RLCV's
SR-IOV VF→PF mailbox is the only genuine one and needs VF-capable silicon.
The F32 register namespace is solved: F32_index = 4*host_dword_offset + 0x4980.
Curve-fitting a delta had failed at chance level; the answer came from structure — TH1's
per-queue stride 0x160 over the host's 0x58 is exactly 4, so F32 addresses registers
by byte offset. Validated at 31/54 named against a null whose maximum over 400 trials is
14, with semantically right hits (STATUS_REG x45, CHICKEN_BITS, STATUS2_REG,
CNTL, UTCL1_CNTL). Notably the gate registers are unpublished: 0x5940/0x5968
(SRBM_WRITE gate) and 0x5a6c (PTEPDE switch) map to host offsets 0x3f0-0x43b, inside
the SDMA0 aperture but absent from AMD's header — consistent with 0x5940 being a
current-queue shadow of RB_CNTL rather than a per-queue slot.
TH1 examined too — also clean. It has no packet table (the 64 "entries" f32dis
prints there are the CP-oriented tool misreading the tail; the keys are not
(op<<8)|(sub_op<<4)), and only 11 r1 reads versus TH0's 370 — it is the queue/context
control thread, not a packet processor. Of its 52 computed register accesses, 48 root at
constants and the rest compute 0x4b80 + queue*0x160 inside a bounded 8-queue loop:
host data supplies the restored values, never the register index. By-products: the
per-queue register block in F32 space is base 0x4b80 stride 0x160 (so the
hardware-named 0x5940 = RB_CNTL is a current-queue alias, outside that range), and five
queue registers are deliberately not restored verbatim but shadowed.
TH0's host surface screens clean. A rebuilt, call-clobber-aware taint pass
(clobber sets computed over the call graph to a fixpoint) kills the false lead and
leaves exactly one sink in all 52 packets: POLL_REG_WRITE_MEM, where a host dword
names a register to read and DMA out. On silicon that is gated on RB_PRIV too
(0xfeedface copied out with the bit set, nothing without), so register reads and
writes share one trust model. Caveat recorded in place: this packet lacks the static
gate idiom yet is gated in hardware, so the static gate map under-reports gating and
is not evidence of "unprivileged".
A lead that died on hardware: POLL_MEM_VERIFY appeared to use its ABI-reserved
12th dword as a register index in an ungated handler — which would have been an
unprivileged arbitrary register write. Tested against F32 index 0x5940 (RB_CNTL, the
one index we can read back): no change under either RB_PRIV. Cause: two bl calls
sit between mov r5, r1 and the store, and the screening dataflow is not call-clobber
aware — the same flaw flagged in the MES audit, here manufacturing a false positive.
A measurement I discarded: testing only for the SRBM_WRITE gate idiom reported "PTEPDE (page-table writes) is ungated", which is false — PTEPDE gates on reg 0x5a6c bits 0x101. The tool now reports the per-packet control-register map instead of a binary verdict, and flags that 0x4a14/0x4af8/0x4b58 appear under 49 handlers only because the walk reaches the shared epilogue.
- SDMA TH0/TH1 decode as code (100% of calls resolve in-image, 61/71
ret) — upstream's README says SDMA "will not work", which holds for CI-era SDMA but not gfx11. SDMA is host-packet-driven and loadable on this card, making it the best remaining audit target: unlike FINDING-9 it is testable on silicon we have. - RLC_G, RLCP, RLCV are code; RLC_G is loaded at word base 0x2000, recovered
from 915 branch targets spanning exactly its own size shifted by 0x2000. Upstream
printed those targets as the string
True, hiding the signal (one-hunk fix kept asf32dis-loc.patch). - IRAM/DRAM/SRL_SRM are data — no
retanywhere; stray "calls" are data words decoding as branches, which is whyretand not call count is the discriminator. - Decode percentage is explicitly not the evidence: an RV64 image scores 78% on this
permissive decoder. Controls: CI blobs 100%/57
ret, RV64 and random 0ret.
Two gaps stated in the doc: the microengine register index space is unmapped (6/54
indices match gc_11_0_0_offset.h = coincidence; a ±0x2000 delta search peaks at 9%),
and the CI-era opcode table leaves 2.6-3% of gfx11 words unknown — the listing is a
strong draft, not ground truth.
MEC / PM4 — thread closed: the boundary is hardware/routing, not firmware
Round 10 [ST] took the cheap route instead of more KIQ porting. priv_state is bit 31
of CP_HQD_PQ_CONTROL and kmd_queue bit 30, so a firmware check must extract those
bits. Across the MAP_QUEUES closure from the validated case label 0x3241c — branches
followed, callees to depth 3, 3984 instructions — every candidate idiom resolves to
something else: the srliw 0x1f pair is a ring-slot phase wait, the nine srli 0x1e are
masked index arithmetic, and the eleven lui 0x80000 set the bit into a descriptor
rather than testing it. No test of the submitting queue's privilege exists on the
path.
Combined with the hardware results (round 5: register write refused and the queue
faults; round 6: MAP_QUEUES not serviced on a compute ring at either privilege), the
reading is that PM4's privilege boundary is enforced by hardware and packet routing,
not by checks inside MEC — a weaker kind of safety than SDMA's explicit RB_PRIV
gate. Stated as a conditional for future work: anything that gets a packet to the HWS
path from a queue hardware would not have routed there meets no second line of defence
in firmware.
MEC / PM4 [HW] — the user->kernel boundary exists and holds
Round 5, MEC-PM4-FOOTHOLD.md, evidence ../mes/hw/EVIDENCE-mec-pm4-priv.txt. PM4 is
submitted by unprivileged user queues and WRITE_DATA(dst_sel=register) names a
register, so an ungated path would be a genuine user->kernel primitive. A 2x2 on
silicon (queue privilege x destination):
| dst = memory | dst = register | |
|---|---|---|
| priv_state=0 | wrote | refused, queue faults |
| priv_state=1 | wrote | wrote |
The memory column proves the packet/ring/doorbell path is correct (otherwise "no write"
is just a malformed packet), and the priv_state=1 row proves the refusal is about
privilege rather than WRITE_DATA->register being unsupported on MEC. Privilege was
changed by live-flipping CP_HQD_PQ_CONTROL (0x8509 -> 0xc0008509) on a running
queue, so that bit is the only difference. The boundary is real and enforced.
Across both user-submittable engines the pattern is now identical: memory confined by the VM, registers gated by a per-queue privilege bit, gate holds under test.
Round 6 tried the indirect route — MAP_QUEUES from an unprivileged queue to map a
queue with priv_state=1, which would chain into the register write. Inconclusive and
reported as such: the packet stalls the queue identically at priv_state=0 and 1,
and again with a seeded MQD, so the stall is not a privilege effect — MAP_QUEUES is
an HIQ/KIQ-pipe packet and is not serviced on an ordinary compute ring. Without the
privileged comparison this would have read exactly like the round-5 fault and been
miswritten as "the gate covers MAP_QUEUES too". The chain stays open; testing it
needs HIQ submission, which tinygrad's AM does not expose.
Round 7 attempted the HIQ plumbing needed to settle it. Two obstacles cleared (the MQD
slot shortage, and the KIQ's unrouted doorbell index), one not: CP_HQD_ACTIVE reads
back 0 — the CP rejects the queue. tinygrad's KIQ path is VF-only and hardcodes
me=2, pipe=1, queue=0, whereas amdgpu picks them via amdgpu_gfx_kiq_acquire() and
also needs HPD EOP and MEC pipe enablement. That is an amdgpu KIQ port, not a patch, so
the chain stays open and untested — a queue that never activates cannot refuse
anything.
Round 8 ported amdgpu's KIQ bring-up further: the slot is me=1, pipe=3, queue=0
(gfx11 has one MEC, so me = mec+1 = 1 — tinygrad's hardcoded me=2 is a VF layout and
was the round-7 failure), reached via setup_ring(idx=12); the HIQ pointer is
RLC_CP_SCHEDULERS = (me<<5)|(pipe<<3)|queue|0x80; and gfx11 being RS64 the pipe enables
live in CP_MEC_RS64_CNTL, which had only mec_pipe0_active set. CP_HQD_ACTIVE
now reads 1, but the queue still does not fetch — the remaining gap is the per-pipe
HPD/EOP programming. Register restored; MAP_QUEUES still untested.
Round 9 ported the last two prerequisites (all-pipe enable with MEC_HALT=0, and
CP_MEC_DOORBELL_RANGE_*) and the queue still does not fetch; the remaining
candidate is the MEC's own per-pipe microcode initialisation, a much larger port. Since
each failed attempt leaves an undequeueable HQD needing a cold ATX cycle (three spent),
this thread is stopped on cost/benefit. The cheaper route is static: MAP_QUEUES is
at 0x3241c in the validated table, inside the ~12 KB dispatcher sub_31360, and
whether any path from it consults a privilege bit is a bounded reverse with no hardware
cost.
MEC / PM4 round 4 — table corrected and validated, no finding yet
MEC-PM4-FOOTHOLD.md, tool ../mes/mec_audit.py. Acting on the synthesis: PM4 is the
only user-submittable surface left unaudited. Round 1's table base 0x414d8 was
0x100 low (the nested-$PS1 error again); at the corrected base 0x415d8 all five
of its anchors verify and 5 of 7 populated slots carry published PACKET3_* names
— at the old base, zero did. That table is the queue-management path only, and two
scans found no other dispatch structure, so the remaining packets go through compare
chains.
Round 1's "the per-entry second dword is a length bound" is refuted: MAP_QUEUES
carries 2241 where UNMAP/QUERY/RUN_LIST carry 2 — not dword counts. The parser's length
check is in code, not the table.
Taint from the PM4 data port 0x6F8(tp) finds 75 address-forming sinks, 27 of which
assemble a 64-bit GPU VA from two packet dwords (VM-confined, as proven for SDMA). One
striking lead — a raw packet dword shifted into MMIO-aperture bits and stored through —
was my own artifact: the walker ran through an unconditional j. Making it
block-aware removed it and 16 others.
Consolidated: the GPU-engine work is now in the authoritative audit
VULN-AUDIT-WHAT-WE-ACTUALLY-HAVE.md is the file that wins when others disagree, and
it had covered only the PSP components — an entire campaign against the GPU
microcontrollers sat outside it. It now carries a GPU-engine section, tiered on the
same reachability axis:
- the verdict first — none of the DMCUB findings is exploitable by an unprivileged process, because the inbox is kernel-only and the hostile field is driver-chosen in every case;
- the confirmed set (FINDING-10 with its own sweep bounding it, 1, 2, 6, 8, and the 5/7 negatives that explain why nothing escalates), plus FINDING-9 marked [ST] and not-this-ASIC;
- the boundary results as first-class output: SDMA gated across all three register access shapes, MEC/PM4 gated on a 2x2, MES and SMU clean — and the note that SDMA and MEC are the only user-submittable surfaces here, which is why the campaign produced no escalation;
- the two open items with their blockers named (PM4
MAP_QUEUESneeds an amdgpu KIQ port; DMCUB PSR-SU needs a live OTG).
DISCLOSURE-SUMMARY.md now carries a scope note pointing there, since it predates all
of this and would otherwise read as if no GPU-engine work exists.
Cross-finding synthesis — what compounds, and where it says to look next
SYNTHESIS-CROSS-FINDING-COMPOUNDS.md. A deliberate pass over the whole corpus asking
whether any two findings combine into something neither is alone.
- Tested and falsified: FINDING-10's timing technique transplanted onto SDMA's
user-submittable queues via
POLL_REGMEM— which writes nothing, so the gate covering the other register accesses need not have applied. On silicon the privileged control shows the oracle plainly (match 0.08 ms vs mismatch 3002 ms) and it vanishes atRB_PRIV=0(both hang, indistinguishable). SDMA's register boundary is therefore consistent across all three access shapes — write, read-to-memory, and read-by-timing — a stronger statement than any single probe made. - Untested, concrete: SDMA's register security is one bit (
RB_PRIV), while DMCUB types 1/3 are an intentional arbitrary-register write. Can one engine set another's privilege bit? That would make the gate a convention rather than a boundary. - Systemic pattern: memory is confined by hardware (page tables, CW windows); registers are confined by firmware convention — and firmware convention is exactly what rollback undoes. DMCUB has no register boundary at all; only reachability saves it.
- Amplification: DMUB rollback (FINDING-8) needs no PCIe power cycle, unlike sOS rollback, so it is the quieter way to reintroduce any DMCUB defect fixed since 2024-02.
- Reordered target list: the only user-submittable surfaces on this card are SDMA
queues (audited, gated) and PM4 → MEC, which is unaudited because MEC was
unreadable — and it is RV64 and readable now.
MEC-PM4-FOOTHOLD.mdis the highest-expected-value target left, with the SDMA methodology directly transferable.
Rejected compounds are recorded too, including FINDING-10 × the HDCP canary fence (no PSP memory aperture to read from DMCUB) and FINDING-1 × the PSR state machine (the host owns CW6 anyway — this lowers FINDING-1's severity).
Round 7 [ST] — the newer DMCUB images, a tool false negative, and FINDING-11
../dmcub/AUDIT-DMCUB-07-DPAUX.md, sweep ../dmcub/dmcub_dpaux_audit.py. This
executed round 3's "Next" item 3 (re-run the per-command audit over DCN 3.5/3.6/4.0.1),
which had never been done.
A tool defect first, because it invalidated a whole image's results.
dmcub_cmdaudit.py hardcoded the taint root on a3, the register holding the command
frame in the 3.2.0 dispatcher. The DCN 4.0.1 dispatcher was restructured and uses
a4, so the first run reported 30 of 31 command types as having zero host-data
sinks — including type 66, whose ABM copy is FINDING-1 and must appear. The existing
--selftest could not catch it because it only runs against 3.2.0. Now the frame
register is derived from the dispatch itself (jx → addx4 index → l8ui idx,
aF, 0), and a structural control reports when taint entered none of a case's
handlers, so "0 sinks" can no longer look like a clean result on a future image. After
the fix 4.0.1 yields 17 types with sinks. No 3.2.0 conclusion moves — a3 was
correct there, and the positive control still passes.
FINDING-11 [ST] — DP_AUX_ACCESS (70) is not the stub the audit believed it was
everywhere. On the DPIA leg (type == AUX_CHANNEL_DPIA at frame+0xc) an unchecked
8-bit instance is added with stride 1 to a global base, and a host-controlled
byte (manual_acq_rel_enable, frame+0x5) is stored there at four sites — before the
only bounds check on the path. The sibling called immediately after applies
bgeui instance, 4 three times, so instance < 4 is the firmware's own intended
bound, applied one call too late; the two written arrays being 4 bytes apart
corroborates 4 entries. Result: a repeatable 1-byte write at a host-chosen offset up to
252 bytes past the array, reaching a multiply-referenced neighbouring structure at
instance 8–12. Corruption stays inside the mapped data region, so it is
adjacent-global corruption, not a wild write.
Present in yellow_carp, dcn_3_1_4, dcn_3_5, dcn_3_6; stubbed in 3.2.x and
4.0.1 (and legacy-leg-only, unaffected, in dcn_3_1_6). So it is not a regression —
it predates 3.2.0 and survives in the newest DCN 3.x images. A 3.2.0-anchored audit was
structurally unable to see it. Note FINDING-8: a 3.5 image is accepted by this card's
PSP.
Limits kept explicit: static only, no silicon and no emulator, and Navi 31 exposes
no USB4/DPIA, so reachability on this card is unestablished; base is a runtime
pointer and only the relative layout is fixed in the image; nothing shows a function
pointer or length in the reachable span, so no promotion to code execution is
claimed. Still open from this round: 4.0.1 types 76/78/89 (flagged unreached by the
new control) and the IPS (91) closure, which taint-explodes to 183 functions.
Reachability: none of the DMCUB findings is exploitable by an unprivileged user
../dmcub/AUDIT-DMCUB-06-USER-REACHABLE.md. Stated up front so eleven findings are not
read as ten exploits: every one needs the DMUB inbox, which is kernel-only under
amdgpu, and the hostile field is always driver-chosen (ABM.bytes is a sizeof,
type-8 panel_inst is a link index). Their value is elsewhere — FINDING-10 crosses a
PSP→host confidentiality boundary (and the CW0 sweep bounds what that is worth),
FINDING-8 breaks rollback protection, 1/2/6 are vendor-fixable bugs in firmware meant to
be isolated.
The user-influenced surface does exist and was traced in the kernel: FB_DAMAGE_CLIPS
is settable by any client with a DRM fd, DRM core validates only the blob's element
size, and amdgpu uses the raw blob (not the clipping iterator) computing
x2 - x1 into a uint32_t — so arbitrary coordinates, including negative widths, reach
UPDATE_DIRTY_RECT verbatim via memcpy. Count stays bounded to 3 and the instance
fields stay driver-chosen.
Tested on hardware inside exactly that envelope: nine hostile-coordinate cases, all consumed, firmware alive, no unexpected CW6 write — and input-dependent (8 vs 4 bytes changed), so the handler is not ignoring them. No user-reachable bug demonstrated. Limits kept explicit: the DMCUB is idle with no PSR session, so the deep region-programming path likely never ran; type 68 (cursor) is shown user-influenced but not yet tested.
FINDING-10 [HW] — DMCUB REG_REG_WAIT leaks PSP-protected memory to the host
../dmcub/AUDIT-DMCUB-05-REGSEQ.md, script ../dmcub/hw/dmcub_poc_regseq.py,
evidence ../dmcub/hw/EVIDENCE-finding10-regseq.txt. Confirmed on this card.
Type 4 (REG_REG_WAIT) computes addr = 0x5ffc0000 + 4*u32[frame+4] from a full
32-bit host index — so it loads from any 4-aligned address in DMCUB's space, not just
registers — and spins iters µs when (v & mask) != expected. The host times
DMCUB_INBOX1_RPTR, so each command yields one bit of any word DMCUB can address:
a read oracle running with DMCUB's rights, ~200 bits/s.
Confirmed against three independent sources: controls recover a known register
(0x5a5a5a5a, 0x0000ffff) and live planted CW6 values (0xdeadbeef, 0x12345678)
bit-exactly, and then 6/6 words of CW0 match the signed on-disk image while the
host's own read of the CW0 backing paddr returns all zeros — the image lives in the
PSP TMR. This is the confidentiality half of the boundary FINDING-7 found intact for
writes.
Same experiment produced a negative that extends FINDING-5: CW0/CW1 BASE/TOP and
SEC_CNTL read 0 through the oracle too (while CW6 reads correctly, proving the
oracle can see that register block), so the window config is masked for the DMCUB
requester as well — the hoped-for "remap the code window and defeat the signature
check" escalation does not work.
Swept both host-denied windows (../dmcub/hw/dmcub_regseq_sweep.py): verifying
against the signed image makes one command cover a whole word, so all 70,979 CW0 words
check in 42 s. Result bounds the finding: CW0 is byte-identical to public
linux-firmware except the last 256 bytes, which the PSP leaves unpopulated; CW1 is 80%
zero / 17% all-ones / 3.5% all-ones-with-sparse-cleared-bits, i.e. uninitialised memory
with no structure. Nothing secret is behind the wall on an idle DMCUB. Two jitter traps
were hit and corrected en route (371 phantom "differences", and single-polarity bit
recovery inventing plausible "residue" values) — see the doc.
Caveats kept explicit: reach is limited to what the PSP already maps into DMCUB's windows (not arbitrary physical memory), unmapped addresses were not probed (unbacked access risks wedging the SMU), and the leaked CW0 content is itself a public firmware image — the primitive's significance is that it is address-general and that no bounds-check fixes it, since it is a side channel rather than a memory-safety bug.
FINDING-9 [ST] — DMCUB LSDMA (type 90): unvalidated host-programmed DMA
../dmcub/AUDIT-DMCUB-04-LSDMA.md. On DCN 4.0.1 one inbox command programs the
LSDMA engine with a descriptor built entirely from host bytes:
LSDMA_LINEAR_COPY— 64-bit src, 64-bit dst, 30-bit count. The only validation in the whole handler iscount != 0; a mechanical range-compare count over0x6003790creturns 0. Same forPIO_COPYandPIO_CONSTFILL.LSDMA_POLL_REG_WRITE— 32-bit register address (aligned&= ~3, not bounded) and 32-bit data, packed into an engine packet and enqueued.- The
lsdma_support_in_dmugate is never written by firmware (checked across all 1724 functions) and lives at0x6601a190, i.e. inside CW6 — the host-ownedfw_statewindow that the FINDING-2 PoC already writes and has DMCUB read back [HW] on this card.
Controls: the range-compare counter reports the bound in handlers known to validate
(GET_PSR_STATE, PSR_RESIDENCY, the trace-mask setter); type 90 is absent from
dcn_3_2_0, so this is DCN4-only and was not missed on Navi 31. It is also why
FINDING-7's "no arbitrary write on Navi 31" does not carry over: LSDMA is a different
requester. Nothing here is hardware-confirmed — this card's PSP rejects the DCN4
image (FINDING-8), so it needs Navi 4x. The open question worth the silicon: can the
LSDMA requester read the PSP TMR, which returns 0xffffffff to host reads?
Negatives from the same round (both with working positive controls)
- DMCUB GPINT doorbell is clean (
../dmcub/AUDIT-DMCUB-03-COMMANDS.md): 114-entry table, only 12 commands implemented, and no handler turns the 16-bit host parameter into an unchecked index. Both panel-indexed handlers reject>= 2— the very check the type-8 path (FINDING-2) lacks.BB_COPY/SET_BB_ADDR_WORD*, the best target on that surface, are unimplemented stubs indcn_3_2_0,dcn_3_5anddcn_4_0_1. - SMU host message ABI is bounded (
../smu/AUDIT-SMU-01-HOSTABI.md, [HW]):TransferTableSmu2Dramrejects everytable_id >= TABLE_COUNT(positive control: 6 in-range ids returned data into the host DRAM buffer);GetDpmFreqByIndexbounds both the clock id (>= 13FAIL) and the level, its two "accepted" high args being an 8-bit masking alias rather than an OOB;GetVoltageByDpmis a deterministic quadratic curve evaluation, not an indexed load.
Firmware coverage — what is loadable vs what we have actually examined
FIRMWARE-COVERAGE-MATRIX.md (new, 2026-09) is the answer to "which signed
components can be loaded on this card, and which did we skip?" — the question the
DMCUB round exposed. The component list is enumerated from the live stack
(adev.fw.descs + adev.fw.sos_fw + the gfx11 RLC-autoload set + on-demand
DMUB/TA/VCN), not from memory.
Closed this round:
- MES — the queue scheduler, host-driven with API packets, never examined here.
First pass in
../mes/AUDIT-MES-01.md: RV64 by real decode (92% coverage), code at0xf0005000, data at0xf0100000, 257 functions, and all three API dispatch tables bounds-checked (index ≤ 4 / 10 / 19 over 36/31/20 entries); the one unguarded indirect jump is a compiler computed-goto inside amemset. [HW] the PSP accepts MES ucode asGFX_FW_TYPE_CP_MES(type 76RS64_MESis refused, 0xffff0006). - MES API surface (
../mes/AUDIT-MES-02-API.md) — packet stride recovered as 256 bytes (=MES_API_MAX), and two indirect dispatch sites that are both bounds-checked (≤ 10 over 11 entries, ≤ 19 over 20). An attempt to name those opcodes from the kernel enums was retracted in place — the bounds were exact ABI fits, which felt like confirmation, but the table entries disassemble as mid-block fragments and epilogues, not handler entries; the prologue-scan function boundaries behind that model are unreliable on this image. Copy audit finds 31/33 memcpy sites use constant lengths and none takes a length from a host field, so DMCUB's FINDING-1 pattern is absent in MES. One design note kept as a note: a fixed 172 KB internal-state dump to a packet-supplied 64-bit address with no capacity field (0xf00147a0), opcode unconfirmed. Register access is centralised in locked wrappers taking a dword index (0xf0020074write,0xf0020358/0xf00200f8read, 30 sites);MESAPI_MISC__WRITE_REGis unresolved — amdgpu demonstrably sends it on gfx11, but no wrapper site takes its index from a packet, and that scan is not call-clobber aware so the negative is weak. - PFP and ME — also RV64, container-parsed, load addr
0x3000. - IMU ISA settled: Tensilica Xtensa, correcting the tree's "undetermined" —
210
entry a1/ 225retw.nin the IRAM half vs 0.00 for the RV64/F32/Thumb controls. The old call was made over the whole container, which is dominated by the zero-padded DRAM half. - TA completeness closed as a negative:
psp_13_0_0_ta.binholds exactly four TAs (ASD/HDCP/DTM/RAS), all already audited — there is no SECUREDISPLAY, RAP or XGMI TA for this ASIC, so no unaudited TA exists. - Cross-cutting: every PSP-loaded payload (DMUB, MEC, MES, MES1, PFP, ME, IMU)
carries a nested
$PS1header, so code starts at file 0x200. This is the generalisation of the 0x100 base error corrected in../dmcub/AUDIT-DMCUB-02.md.
Measured, not assumed [HW]: a sweep of every GFX_FW_TYPE_* with a zero blob
separates "unsupported on this ASIC" (0xffff0006, 49 types) from "supported, our
garbage failed authentication" (0xffff0008, 37 types) — so the loadable set is
now measured. Nothing in it is an unexamined image: the only supported types without
a Navi 31 blob are DMCU_ERAM/DMCU_ISR (pre-DMCUB display µC) and UMSCH_*.
That sweep wedged the PSP (sOS ring dead, SMU timeout) and needed a cold ATX
cycle to recover — now a standing hazard in CONSTRAINTS.md; the card was verified
healthy afterwards.
Still open there, and stated as such: SDMA and RLC are characterised but not
readable at instruction level (F32: fixed 32-bit words, NOP 0x90000000, no
capstone arch fits — f32_probe.py makes that blocker a measurement instead of an
inherited claim);
MES API packet fields (host-supplied queue/doorbell/wptr addresses) are the
highest-value remaining target, and unlike DMUB's Xtensa, RV64 can be emulated
under Unicorn/QEMU; MEC/PFP/ME have no handler audit; KDB stays opaque (encrypted,
entropy 7.10).
New surface opened — DMCUB (display microcontroller), one candidate finding
dmcub/AUDIT-DMCUB-02.md is authoritative [ST]; dmcub/AUDIT-DMCUB-01.md
covers provenance/container/ISA and carries a partial-supersession banner (its
VAs are 0x100 low — nested $PS1 header).
The DMCUB/DMUB is a host-programmable Xtensa co-processor in DCN with a
64-byte-frame inbox ring, a 41-command GPINT doorbell, and its own register
aperture. Round 2 recovered the register-address table (aperture 0x5ffcd300,
validated semantically), the inbox1 dispatcher 0x6001c780 and its 129-entry
handler table, and audited the copy and dispatch surface.
FINDING-DMCUB-ABM-INIT-CONFIG-OOB — DMUB_CMD__ABM / sub_type 0
(ABM_INIT_CONFIG) and the DMUB_CMD__CACP twin map a host-supplied 64-bit GPU
address into cache window 7 and memcpy a host-supplied 16-bit bytes from
it into a per-instance state slot with a 0x2c8 (712) byte stride — no bound on
the length anywhere on the path. Up to ~64 KB of attacker-chosen data. Confirmed
in dcn_3_2_0 and dcn_3_2_1. Host→DMCUB only (the inbox is driven by
amdgpu), so it is not a privilege escalation for the host; it is a code-exec
avenue into a die-internal microcontroller that survives host low-power states and
owns the DMUB_CMD__PSP/ASSR path. Static only — no emulator, no repro.
Negatives recorded there, do not re-run: DMUB_CMD__EDID_CEA reassembly is
bounded (offset + length <= 0x80, total_length <= 0x80) and so is its
CEA/VSDB parse — this closes round 1's top-ranked target; all 22 jx
dispatches are bounds-checked with bound ≤ table length (command dispatcher
0x81/129, GPINT 0x72/114); 53 of 57 memcpy sites have constant lengths and the
other two are outbox producers. Also: DMCUB register names cannot be
recovered from l32r literals (the apparent APERTURE = 0x280 fit fails a
random-aperture control and a usage check — those literals are struct offsets;
register addresses live in a table at 0x60018030).
Round 3 — dmcub/AUDIT-DMCUB-03-COMMANDS.md: every implemented DMUB_CMD__*
audited (24 of the 129 dispatch slots are implemented). Two more candidate
findings, both host→DMCUB and both static only:
DMUB_CMD__GET_VISUAL_CONFIRM_COLOR(8) — [HW] CONFIRMED on the card (dmcub/hw/EVIDENCE-type8-poc.txt): indices 2..227 return DMCUB memory past the two-entry array in the reply frame. The DoS leg is retracted — reads whose source leaves every cache window return zeros without faulting. The ABI's 16-bitpanel_instis used unbounded to index state and as the source of `memcpy(reply, ctx + 8*panel_inst- 0xaea0, 8)`: an 8-byte read disclosed to the host anywhere in a 512 KB window, plus stride-1976/304 wild reads (fault/DoS). The whole case is 24 inline instructions with no bound, so this one is locally verifiable.
-
DMUB_CMD__HW_LOCK(69) — 8-bitdig_instindexes a 7-entry table of register pointers (stride 0xb8) with no bound, and the firmware read-modify-writes through whatever it fetches. [HW] partially confirmed and downgraded: the path is host-reachable anddig_instprovably selects the register (1/2/3 →OTG1/2/3_OTG_GLOBAL_CONTROL0, 0 → 0x80007f37), but the pointer table sits inside PSP-owned CW0, so an attacker picks which slot supplies the pointer and never its value; out-of-bounds indices (7, 13, 30, 162) produced no observable write and did not destabilise the firmware. Latent defect, not a write-what-where. -
Instance-index validation is inconsistent across commands (the audit's main structural result):
ABMderives its index from a 2-bitpanel_maskscan andPSRcompares it, butREPLAY(stride 0x130, a plains8iwrite),CACP(0x30c) and aVBIOSsub-command (0xd4, into the register-address table) scale an unchecked byte straight into the address. AMD's own ABI comment on Replay'spanel_instsays it "is only 0 or 1".
Also: types 1/3/4 are the unpublished legacy REG_SEQ_* commands — an
intentional arbitrary register read/write/poll path through aperture base
0x5ffc0000 with no address validation (not an escalation on this ASIC, where the
host can reach DCN registers directly, but it would be on any part where that
aperture is not host-visible).
Negatives added: DMUB_CMD__DP_AUX_ACCESS (70) is a stub in DCN 3.2.0
(memset; return 1), so round 2's top sink-reachable lead does not exist here;
UPDATE_CURSOR_INFO (68) never dereferences the ABI's cursor_copy_src/dst
addresses; all 23 jx and 27/28 callx8 dispatches are bounded.
[HW] DMCUB brought up and FINDING-2 reproduced on silicon (2026-09). Two
hardware results beyond the leak itself: (a) the host cannot program
DMCUB_REGION3_CW0/CW1 or DMCUB_SEC_CNTL — writes dropped, reads 0 — so
dmub_dcn32_backdoor_load is blocked on Navi 31 and DMCUB code execution
requires a PSP-signed image (security-positive; it also kills the idea that the
host can run its own code on the DMCUB); (b) DCN is already powered after ATX-on,
so no display power/clock work was needed, and a hung DMCUB is revived by a fresh
GFX_FW_TYPE_DMUB PSP load with no ATX cycle. A method mistake is recorded in
place: a hand-rolled wreg(14)/rreg(15) register window returned plausible but
entirely wrong values for a session; adev.rreg/wreg already handle indirection,
and the path is now proven with a known-value control plus a scratch
write/readback.
Hardware PoC path (dmcub/hw/). Reproducing
FINDING-2 needs the DMCUB brought up first, because tinygrad initialises no
display IP (the same blocker as the HDCP display-state path). hw/ ports
dmub_srv_hw_init/dmub_dcn32_* onto the AM stack via backdoor load (stock
signed image, no PSP: backdoor_load clears DMCUB_SEC_CNTL.DMCUB_SEC_RESET
itself), then plants markers in cache window 6 — which the host allocates — and
submits type 8 with panel_inst 2..23. Because the leak source stays inside a
host-mapped window, the PoC proves an out-of-range read with attacker-chosen
bytes while never making the DMCUB touch unbacked memory (indices past the window
are refused unless forced). Read-only gate probe decides whether bring-up is even
possible: if DCN is power-gated/SMN-denied, the path stalls on display power and
clocks, which AM never touches.
[HW] FINDING-1 confirmed: a host command overflows DMCUB state by ~32 KB, and a
larger one wedges the firmware. ABM_INIT_CONFIG's unchecked uint16 bytes writes
attacker-supplied data past a 712-byte slot — 32492 bytes changed at bytes=0x8000,
processed cleanly — and from ~0x9c00 the copy reaches the CW6 window tail and the
inbox command loop stops consuming frames (new FINDING-6, display DoS; recovery =
PSP reload). FINDING-4 is not demonstrated: those handlers do run and write state,
but the offsets do not move with the index byte (idx 0..255), and the Replay leg is
caller-bounded anyway.
[HW] DMUB firmware rollback works (FINDING-8). On a card shipping DMUB 7.0.51 the
PSP accepts and boots 7.0.42 and 7.0.38, and accepts a DCN 3.5 image (different IP
generation) which begins executing on DCN 3.2 silicon (SCRATCH0=0x80) before failing
to finish boot; renoir and dcn_4_0_1 are refused (0xffff0008). The signature itself
is enforced — single-bit tampering anywhere is rejected — but there is no version or
ASIC binding, exactly as CONFIRMED-FIRMWARE-ROLLBACK-WORKS.md found for the PSP.
Honest limit: 7.0.38's audit profile is identical to 7.0.51, so rollback buys no extra
exposure today; its value is dating FINDING-1/2/3 to at least 2024-02 and making any
future AMD fix reversible by the host.
Still no arbitrary write, and now a structural reason (FINDING-7 [HW]). The
overflow gives length and contents but not address: the destination follows the CW6
cache window, which can only be pointed at memory the host can already write; booting
with CW6 inside the PSP TMR leaves the core dead (boot=False, SCRATCH0=0), so
VRAM protection covers the DMCUB requester as well as x86; and writes past the
window edge do not land, they wedge the core. The DMCUB's own image is likewise
unreachable -- not in host-readable VRAM, and it survives reset+release with no
PSP reload. With FINDING-5 that is the structural reason this whole surface has
produced no escalation: every primitive stays inside memory the host already owns.
A retraction and the lesson behind it. An earlier pass in the same session
reported all three write findings as "not triggerable" across ~120 field
combinations. That pass was taken after the DMCUB had silently stopped consuming
inbox frames (rptr frozen, wptr advancing) — and DMCUB_SCRATCH0 still read 0x3
with the timer still ticking, so the liveness oracle reported a healthy core.
DMCUB liveness must be measured by ring consumption, not by scratch bits or the
timer; with a per-command consumption check the overflow fired on the first try.
~~The three write findings do not fire on a bare DMCUB.~~ ABM_INIT_CONFIG's
unchecked uint16 bytes (FINDING-1) never reaches its copy site -- verified with two
sensitive detectors (whole-CW6-window diff at zero baseline churn, plus
DMCUB_REGION3_CW7_OFFSET, which the source-mapping helper always reprograms) across
version/panel_mask/sub_type sweeps and with ABM_SET_PIPE prerequisites. FINDING-4's
CACP/REPLAY panel-index writes produced 0 state writes across 69 field
combinations, and the Replay leg turns out to carry a caller-side bgeui idx, 2
bound -- the false-positive class the method section warned about. All three need
display/panel state that only a real DC bring-up establishes, so they stay latent.
The pattern across the whole surface: only the read path (type 8) was reachable on a
bare core, and it can only reach host-readable memory.
Still open there, and stated as such: REPLAY (83), CACP (87) and VBIOS (128)
are screened, not audited (244/144/393 host-data sinks), the taint model has no
global-variable propagation, and nothing is emulated. The sink-reachable
AUX/DPIA surface should be looked for in the DCN 3.5/4.0.1 images instead.
Explicitly deprioritised
- More host-facing aperture hunting. Four angles closed this session; NV22 addresses mostly do not transfer to NV31; hit rate is poor.
- Anything requiring a flash write. Prohibited by
CONSTRAINTS.md, and unlike every other experiment it is not ATX-recoverable. -
fuse_program_bitson hardware. One-way. Emulator only. -
fw/REPORT-SYSDRV-COUNT-UNDERFLOW.md— disclosure-ready: unvalidated count atbuf+0x88in SYS_DRVsub_E04B68/sub_E04DFC(unclamped copy + unsigned underflow). Present in all 31 distinct SYS_DRV builds; verified 14/14 statically and reproduced in emulation including composition through the real dispatcher. Not host-reachable on the parts analysed — every TA passes a 16-48 byte payload where 140 are needed. -
fw/REPORT-PSP-DOS-TRIGGERS.md— disclosure-ready: four host-triggerable PSP hangs (AUC cmd 6 and FP cmd 7 via SYS_DRV opcode0x101D;LOAD_TA cmd_buf_len = 0; FP cmd 2 with an undersized declared window). All [HW]-reproduced with controls, and measured to be hangs rather than memory corruption. Includes the boundary checks that are correct and two rejected root-cause hypotheses. -
RING-FRAME-FIELD-MAP.md— complete [HW] field map of the 64-byte PSP ring frame, the last unexamined structure in the host-to-PSP interface. Records two unvalidated host-controlled fields (fence_addr, a write destination not checked against the trusted region;vmid, a translation context honoured by trusted firmware), three of the seven DoS triggers, andcmd_buf_sizebeing ignored entirely. Limitation: no memory corruption follows — the fence write does not land in the trusted region under a dense functional test, and a host-chosen VM context grants a bare-metal host nothing it did not already have. -
../fw/REPORT-PSP-NO-ANTIROLLBACK.md— disclosure-ready [HW] report: the PSP bootloader validates component signatures but not versions, so an older genuine AMD-signed firmware set (sOS + SYS_DRV + TOC/KDB/SPL/RL) boots on a part shipping 49.0.54, confirmed by the PSP's ownC2PMSG_58reporting the older version after a cold power cycle. Fix = fuse-backed version floor at component load. Limitation: a capability, not itself a memory-safety defect — the two older builds available fuzzed no weaker on the same axes, and the confirmed SYS_DRV count-underflow needs the same unreachable payload size in all three builds, so no exploit chain is demonstrated. -
../fw/FINDING-PSP-SYSDRV-NO-ANTIROLLBACK.md— [HW] the bootloader validates component signatures but not versions. Older genuine AMD-signed SYS_DRV (49.0.37, 49.0.47) boots on a 49.0.54 part; whole-set substitution makes the PSP's ownC2PMSG_58report the older sOS. Positive control (identity re-injection) boots; both the "resident copy" and "refused load" objections closed by a cold power cycle and by TA service succeeding. Limitation: a capability, not a memory-safety defect. Disclosure write-up inREPORT-PSP-NO-ANTIROLLBACK.md. -
../fw/FINDING-PSP-FENCE-ADDR-UNVALIDATED.md— [HW] the ring frame'sfence_addris a write destination chosen by the host, with the host also choosing the value (fence_value), and it is not range-checked against the trusted region, wherecmd_bufis.fence_addr = 0is a DoS trigger. Limitation: the write does not land in the trusted region — host reads of it return0xffffffff, and a dense functional test (16,384 dwords at 64-byte granularity across the first TMR megabyte, where a loaded TA lives) leaves TA behaviour identical, so hardware protection appears to cover the missing software check. -
../fw/FINDING-PSP-RING-FRAME-VMID-UNVALIDATED.md— [HW] the ring frame'svmidbyte (+32) is honoured as the VM context for the PSP's own accesses and is unvalidated: the interrupt log shows an L2 read fault (0x100d33) at the PSP's own command buffer tagged with the host-supplied VMID. Any non-zero value is a DoS trigger;frame_typeis ignored. Limitation: a controlled access under a chosen context was not achieved (configuring MM context 1 identically does not make it work, andMMVM_CONTEXTS_DISABLEis0), and on a bare-metal part it could grant nothing anyway, since the host already owns its own page tables and the frame's addresses. The concerning case is SR-IOV, untestable here. -
../fw/FINDING-PSP-RING-BASE-ALIGNMENT-UNCHECKED.md— [HW] ring creation is otherwise well validated (bogus sizes rejected0x4/0x5, a base inside the trusted region rejected0xa), but a misaligned base is accepted and then wedges on the first submit — an inconsistency, sincecmd_bufand image misalignment are both rejected. Eighth DoS trigger. Limitation: a hang, not corruption. -
../fw/FINDING-PSP-BOOTLOADER-LIVE-AT-RUNTIME.md— [HW] the bootloader mailbox (C2PMSG_35/_36) is still reachable after sOS has booted and answers a bogus component id with a status while sOS runs. Limitation: it refuses to act — valid component loads get the same uniform0xa, so there is no runtime reload, rollback or reinit through this path; recorded because "reachable but gated" differs from "absent" if a future firmware relaxes the gate. -
../fw/LEAD-ROLLBACK-REACHABILITY.md— CLOSED NEGATIVE. The lead that rollback might make the confirmed count-underflow reachable. It does not: the count is read at+0x88in the older builds too, so the required payload is unchanged, and the blocker is TA-side where build choice was already free. Kept for the reasoning and artifacts.
