# 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: 1. `LOAD_TA` through `full_system`'s ring does not create a TA session. 2. `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: 1. **`INVOKE_CMD` never reaches RAS** (0 new blocks). It is submitted with session id 0; `LOAD_TA` returns the real session id in the command buffer's response area (offset 0x360 on hardware) and that is not yet read back. 2. **Gate-byte ambiguity.** `0x4060E0`'s VA is only mapped while RAS's context is live, so the harness latches its PA — but the latched `0x41E20E0` belongs 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 as `f32dis-loc.patch`). * **IRAM/DRAM/SRL_SRM are data** — no `ret` anywhere; stray "calls" are data words decoding as branches, which is why `ret` and 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 0 `ret`. 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_QUEUES` needs 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** at `RB_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.md` is 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 is `count != 0`; a mechanical range-compare count over `0x6003790c` returns **0**. Same for `PIO_COPY` and `PIO_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_dmu` gate is **never written by firmware** (checked across all 1724 functions) and lives at `0x6601a190`, i.e. inside **CW6** — the host-owned `fw_state` window 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 in `dcn_3_2_0`, `dcn_3_5` *and* `dcn_4_0_1`. * **SMU host message ABI is bounded** (`../smu/AUDIT-SMU-01-HOSTABI.md`, **[HW]**): `TransferTableSmu2Dram` rejects every `table_id >= TABLE_COUNT` (positive control: 6 in-range ids returned data into the host DRAM buffer); `GetDpmFreqByIndex` bounds both the clock id (`>= 13` FAIL) and the level, its two "accepted" high args being an 8-bit masking alias rather than an OOB; `GetVoltageByDpm` is 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 at `0xf0005000`, data at `0xf0100000`, 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 a `memset`. **[HW]** the PSP accepts MES ucode as `GFX_FW_TYPE_CP_MES` (type 76 `RS64_MES` is 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 (`0xf0020074` write, `0xf0020358`/`0xf00200f8` read, 30 sites); **`MESAPI_MISC__WRITE_REG` is 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` / 225 `retw.n` in 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.bin` holds 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 `$PS1` header**, 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-bit `panel_inst` is 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-bit `dig_inst` indexes 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 and `dig_inst` provably 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): `ABM` derives its index from a 2-bit `panel_mask` scan and `PSR` compares it, but `REPLAY` (stride 0x130, a plain `s8i` **write**), `CACP` (0x30c) and a `VBIOS` sub-command (0xd4, into the register-address table) scale an unchecked byte straight into the address. AMD's own ABI comment on Replay's `panel_inst` says 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_bits` on hardware.** One-way. Emulator only. - `fw/REPORT-SYSDRV-COUNT-UNDERFLOW.md` — **disclosure-ready**: unvalidated count at `buf+0x88` in SYS_DRV `sub_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 opcode `0x101D`; `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, and `cmd_buf_size` being 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 own `C2PMSG_58` reporting 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 own `C2PMSG_58` report 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 in `REPORT-PSP-NO-ANTIROLLBACK.md`. - `../fw/FINDING-PSP-FENCE-ADDR-UNVALIDATED.md` — [HW] the ring frame's `fence_addr` is 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, where `cmd_buf` is. `fence_addr = 0` is a DoS trigger. **Limitation**: the write does not land in the trusted region — host reads of it return `0xffffffff`, 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's `vmid` byte (+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_type` is ignored. **Limitation**: a controlled access under a chosen context was **not** achieved (configuring MM context 1 identically does not make it work, and `MMVM_CONTEXTS_DISABLE` is `0`), 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 rejected `0x4`/`0x5`, a base inside the trusted region rejected `0xa`), but a **misaligned** base is accepted and then wedges on the first submit — an inconsistency, since `cmd_buf` and 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 uniform `0xa`, 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 `+0x88` in 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.