aboutsummaryrefslogtreecommitdiffstats
path: root/dmcub/hw
ModeNameSize
-rw-r--r--EVIDENCE-finding10-regseq.txt1887logstatsplainblame
-rw-r--r--EVIDENCE-finding10-sweep.txt1435logstatsplainblame
-rw-r--r--EVIDENCE-type8-poc.txt9620logstatsplainblame
-rw-r--r--README.md6364logstatsplainblame
-rw-r--r--dmcub_bringup.py19584logstatsplainblame
-rw-r--r--dmcub_hwregs.py15169logstatsplainblame
-rw-r--r--dmcub_poc_dirtyrect.py7937logstatsplainblame
-rw-r--r--dmcub_poc_regseq.py16601logstatsplainblame
-rw-r--r--dmcub_poc_type8.py16444logstatsplainblame
-rw-r--r--dmcub_probe.py5921logstatsplainblame
-rw-r--r--dmcub_psr_bringup.py6441logstatsplainblame
-rw-r--r--dmcub_regseq_sweep.py10000logstatsplainblame

dmcub/hw — hardware PoC path for FINDING-2 (Option A: bring up the DMCUB)

Status: RUN ON HARDWARE, 2026-09 (Navi 31 over USB4, tinygrad AM). The DMCUB was brought up from a cold, uninitialised display block and FINDING-2 was reproduced: see EVIDENCE-type8-poc.txt. Every script also keeps its offline --selftest (header parsing, address arithmetic, window layout, register write order, frame encoding, and a model of the target firmware case).

Results in one line each:

  • dmcub_probe.py — DCN is powered and DMU registers are live; DMCUB was halted.
  • dmcub_bringup.py — backdoor load is blocked on this part (CW0/CW1 and SEC_CNTL writes are dropped); --phase psp_load boots the core (SCRATCH0 = 0x3).
  • dmcub_poc_type8.py — 5/5 out-of-range panel_inst values returned planted bytes; confirmed to k = 227.

Why bring-up is needed at all

The rig drives the card through tinygrad's AM userspace driver, which initialises no display IP (psp_out/ATTACK-SURFACE.md:190 — the same blocker that stalled the HDCP display-state path). So there is no DMCUB firmware loaded, no cache windows and no inbox1 ring to submit a command to. This directory ports dmub_srv_hw_init + dmub_dcn32_* (vendored in ../ref/) onto that stack.

Corrected on hardware: the plan was to avoid the PSP entirely via dmub_dcn32_backdoor_load, on the assumption that the instruction window is fetched from framebuffer with no signature check in our path. That is wrong on Navi 31 — DMCUB_REGION3_CW0_*, CW1_* and DMCUB_SEC_CNTL silently drop host writes and read back 0, while CW3/4/6, INBOX1_* and DMCUB_CNTL retain them. The backdoor sequence therefore runs to completion and the core never boots.

The working path is --phase psp_load: GFX_CMD_ID_LOAD_IP_FW with GFX_FW_TYPE_DMUB and the signed $PS1 payload (status 0), then windows, mailbox, release — after which DMCUB_SCRATCH0 = 0x3 (dal_fw, mailbox_rdy). The stock signed image is used unmodified throughout. A hung DMCUB is revived by re-running psp_load + release; no ATX cycle was needed at any point, and the rest of the GPU stayed healthy.

Order of operations

# 0. offline, no hardware:
python3 dmcub_hwregs.py   --selftest
python3 dmcub_bringup.py  --selftest
python3 dmcub_poc_type8.py --selftest

# 1. READ-ONLY gate: is DCN powered, is the DMCUB already running?
python3 dmcub_probe.py --json probe.json

# 2. review the exact register sequence (writes nothing)
python3 dmcub_bringup.py --state-out bringup.json

# 3. bring the core up
python3 dmcub_bringup.py --state-out bringup.json --apply

# 4. the PoC: plant markers, submit type-8, check the reply
python3 dmcub_poc_type8.py --state bringup.json          # dry run
python3 dmcub_poc_type8.py --state bringup.json --apply

dmcub_probe.py decides whether steps 2-3 are needed at all: if something has already brought the DMCUB up, the PoC can inject into the existing ring.

Observed safety (and one PoC artifact)

Reads whose source leaves every mapped window return zeros and do not fault the core (verified at panel_inst = 300, 1000, 0x4000, 0xFFFF), so the "unbacked access" hazard this PoC was designed around did not materialise. One DMCUB hang was seen, after a sweep that planted gate bytes across the whole fw_state window — i.e. the PoC corrupting live firmware state, not the bug. Plant only what a run needs.

Why this PoC cannot wedge the card

The leak is memcpy(frame + 4, ctx + 8*panel_inst + 0xaea0, 8) with ctx = 0x66000300 — inside cache window 6, whose backing VRAM we allocate. So the PoC:

  • plants known 8-byte markers at ctx + 8*k + 0xaea0 and sets the stride-0x7b8 gate byte, both from the host, for k in the derived deterministic range 2..23 (source and gate mapped);
  • treats k = 0, 1 as the baseline — those are the only legitimate values, since the part has two panels;
  • refuses any k whose source leaves the window (k > 227 for this image) unless --allow-wild is passed. That is the unbacked-access case psp_out/CONSTRAINTS.md warns about ("any probe that makes an on-die engine access unbacked memory can take out the SMU");
  • re-checks liveness (DMCUB_SCRATCH0.dal_fw + DMCUB_TIMER_CURRENT advancing) after every submission and stops at the first anomaly.

Every address touched stays inside a window the host mapped. The evidence is therefore stronger and safer than a wild read: the reply must contain our bytes from an offset the command is never supposed to reach.

Expected result, and what would falsify the finding

panel_inst expectation if FINDING-2 is real
0, 1 firmware's own per-panel confirm colour (no marker planted) — observed 0000000000000000 / 0000000000000100
2..23 the planted marker returned verbatim in frame[4:12] — observed
24..227 leaks too, once a gate is satisfied; the per-index gate is unmapped there, so set [ctx+0xab28] instead — observed
>227 source outside every window: returns zeros, core stays alive — observed

If indices ≥ 2 come back as zeros, or identical to k = 1, or the frame's ret_status bit is never cleared, then the index is bounded somewhere the static audit missed and FINDING-2 must be retracted.

Known unknowns (updated after the run)

  1. ~~DCN power/clock~~ — resolved: DCN is already powered on this card after ATX-on; DENTIST/DCCG read real values and DMU registers are live. No display power work was needed.
  2. Register access. Use adev.rreg/wreg(idx); they pick BAR vs RSMU indirection themselves. Do not hand-roll a wreg(14)/rreg(15) window (see the mistake recorded in dmcub_hwregs.py). validate() now proves the path with a known-value control plus a scratch write/readback, because every DMCUB register reads 0 before initialisation.
  3. ~~Backdoor load may be fused off~~ — resolved: it is blocked (FINDING-5).
  4. VBIOS window is a zeroed 64 KB stand-in; no VBIOS command is exercised, but if boot itself reads VBIOS tables this may need the real image.
  5. DMCUB_SCRATCH15 is written 0 (the field is psp_version & 0x001100FF), which is what a non-PSP load implies; if the firmware gates behaviour on it, expect boot to differ.