aboutsummaryrefslogtreecommitdiffstats
path: root/dmcub/hw/README.md
blob: fba321ac65b461b46c0a37ac04321f02e7ac7317 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# 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

```sh
# 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.