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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
|
# DMCUB (display microcontroller) — first-pass audit
> **PARTIALLY SUPERSEDED by `AUDIT-DMCUB-02.md`.** Two things in this document
> are wrong and are corrected there:
> 1. **Every VA here is 0x100 too low.** The payload contains a nested `$PS1`
> PSP header; the loadable body starts at file 0x200, so
> `VA = 0x60000000 + (file_off - 0x200)`. Function counts and the 0x60000000
> window base stand; individual addresses do not.
> 2. The round-2 pass **answers this document's top-ranked target negatively**:
> `DMUB_CMD__EDID_CEA` does bound its 16-bit `offset` (`offset + length <= 0x80`
> and `total_length <= 0x80`). Do not re-open it.
>
> The ISA determination, container format, provenance and the recorded negative
> about `l32r` literals are unaffected.
Evidence class: **[ST]** static only. Nothing in this document has been run on
silicon or in an emulator. Where a claim rests on AMD's published driver source
rather than on the image, it says so.
--------------------------------------------------------------------------------
## What DMCUB is, and why it is in this tree
**DMCUB** ("Display MicroController Unit, rev B"; the driver calls the service
layer **DMUB**) is an embedded microcontroller inside AMD's DCN display IP,
introduced with DCN2.1/Renoir as the replacement for the older DMCU. It runs
PSR/Replay, ABM/backlight, AUX/HPD and DPIA handling, panel init, idle/Z-state
and SubVP transitions — i.e. the display path keeps running while the rest of the
SoC is asleep.
Two reasons it belongs here:
1. It is a **host-programmable co-processor on the same die** as the PSP, with a
ring/mailbox ABI (`DMCUB_INBOX1_*`) that looks structurally like the PSP's
own, and its own VRAM cache windows.
2. Its firmware is **PSP-loaded** (`GFX_FW_TYPE_DMUB` / `AMDGPU_UCODE_ID_DMCUB`),
so it sits behind the same signature-verification machinery this tree has
already reversed, and it can talk *back* to the PSP: the ABI defines
`DMUB_CMD__PSP = 88` with subtype `DMUB_CMD__PSP_ASSR_ENABLE`
(`ref/dmub_cmd.h:7094`).
`psp_out/SMN-MAP-HEALTHY-CARD.md` previously hunted "the DMUB" as an unknown
MP-class SMN block and found nothing; that is consistent — the DMCUB is reached
through the DCN/DMU register aperture, not an `MP*_PUB_*` base.
--------------------------------------------------------------------------------
## Provenance
10 images pulled from `linux-firmware` (GitLab kernel-firmware mirror, `main`),
covering Renoir through DCN 4.0.1. Hashes appended to `data/MANIFEST.sha256`
under `dmcub/fw/`. Navi 31 — this tree's card — is **DCN 3.2.0**, i.e.
`fw/dcn_3_2_0_dmcub.bin`, which is the target for everything below.
| image | payload | ucode ver | meta fw_version | fw_region | trace_buf | funcs | `jx` |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `renoir_dmcub.bin` | 121 352 | 0x0101002b | 1.1.43 | 0x1200 | 0x410 | 489 | 6 |
| `yellow_carp_dmcub.bin` | 235 792 | 0x0400004c | 4.0.76 | 0x7b40 | 0x10010 | 770 | 10 |
| `dcn_3_1_4_dmcub.bin` | 350 240 | 0x08006200 | 8.0.98 | 0xcd00 | 0x10010 | 1102 | 25 |
| `dcn_3_1_5_dmcub.bin` | 239 392 | 0x05003600 | 5.0.54 | 0xc880 | 0x10010 | 805 | 20 |
| `dcn_3_1_6_dmcub.bin` | 215 312 | 0x06001800 | 6.0.24 | 0xae00 | 0x10010 | 641 | 19 |
| **`dcn_3_2_0_dmcub.bin`** | **284 176** | **0x07003300** | **7.0.51** | **0xb8c0** | **0x10010** | **1030** | **21** |
| `dcn_3_2_1_dmcub.bin` | 232 352 | 0x07003300 | 7.0.51 | 0xb8c0 | 0x10010 | 847 | 20 |
| `dcn_3_5_dmcub.bin` | 522 144 | 0x09005400 | 9.0.84 | 0x14b80 | 0x10010 | 1659 | 32 |
| `dcn_3_6_dmcub.bin` | 535 584 | 0x09005400 | 9.0.84 | 0x15cc0 | 0x10010 | 1682 | 32 |
| `dcn_4_0_1_dmcub.bin` | 367 904 | 0x0a004c00 | 10.0.17 | 0x1a240 | 0x10010 | 1425 | 32 |
`funcs` counts `entry a1` prologues inside the code region and is a **floor**:
call0-ABI leaf functions have no `entry`. The code region is located by the
longest-run rule in `dmcub_cfg.Image.code_start`; a naive density window put it
at payload 0x4e8 and inflated every count (1042 instead of 1030 for DCN 3.2.0,
1812 instead of 1425 for DCN 4.0.1) by counting stray `36 41 xx` byte pairs in
the literal pools.
Container caveats found while parsing (`dmcub_parse.py`):
* the header's `crc32` field matches **no** CRC-32 over the payload — do not use
it as an integrity check without first working out the real polynomial/span;
* `ip_version` in the header is unreliable: `dcn_3_2_1_dmcub.bin` reports 3.0 and
`dcn_3_5/3_6` report 3.1;
* header `ucode_version` and meta `fw_version` use **different field shifts**,
and on `dcn_4_0_1` they disagree outright (0x0a004c00 vs 0x0a001100).
--------------------------------------------------------------------------------
## Container layout (verified)
```
0x0000 struct dmcub_firmware_header_v1_0 (header_size_bytes = 0x28)
0x0100 payload ("inst const", = file size - 0x100)
... literal pools + data tables
~0x1c000 code
end-0x148/0x144/0x140 union dmub_fw_meta { struct dmub_fw_meta_info } (64 B, magic 'DMUB')
last 256 B PSP footer (signature-shaped: 256 B, entropy 7.1-7.2)
```
The variable meta position is not a mystery: AMD's own loader subtracts
`PSP_FOOTER_BYTES_256` and then searches 0..15 bytes of alignment slack for the
magic (`ref/dmub_srv.c`, `dmub_get_fw_meta_info`). `bss_data_bytes` is 0 in every
image here, i.e. all of them use the combined-metadata layout.
--------------------------------------------------------------------------------
## Finding 1 — the DMCUB core is Tensilica Xtensa (windowed ABI)
Same family as this tree's SMU and VCN, **not** RISC-V and not ARM.
Evidence, with the in-tree positive/negative controls demanded by the project
rules (`dmcub_isa_evidence.py`, code region 0x1c100..0x44000):
| image | `entry a1` | `retw.n` | op0-hist cosine vs DMCUB |
| --- | --- | --- | --- |
| **dcn_3_2_0_dmcub** | **933** | **1994** | — |
| SMU 13.0.0 (known Xtensa) | 1179 | 1543 | 0.799 |
| VCN (known Xtensa) | 765 | 1705 | **0.984** |
| MEC (known RV64) | 98 | **0** | 0.662 |
| SDMA (F32) | 0 | **0** | 0.760 |
| ASD (known ARM/Thumb) | 15 | **1** | 0.778 |
The op0 histogram alone is *not* discriminative (Thumb scores 0.78); the
load-bearing evidence is the windowed-ABI prologue/epilogue pair, which is
absent from all three negatives. Confirmed by actual decode — capstone 6
`CS_ARCH_XTENSA` produces coherent windowed Xtensa throughout, e.g. at the
hottest call target:
```
6001c0f0 368100 entry a1, 0x40
6001c0f3 7186fc l32r a7, 0x6001b30c
6001c0f9 9807 l32i.n a9, a7, 0
6001c100 82295d l32i a8, a9, 0x174
6001c10c 07684d bbci a8, 0, . +0x51
```
**Tooling correction** (contradicts `psp_out/FIRMWARE-INVENTORY.md`, which says
stock IDA has no Xtensa module and to use Ghidra): both `ida-pro-9.3` and
`ida-pro-9.4` on this box ship `procs/xtensa.so`, and **capstone 6.0.0 has
`CS_ARCH_XTENSA`** (installed here at `/home/user/.venvs/cs6`; the system
capstone 5.0.9 does not). All scripts in this directory use that venv.
--------------------------------------------------------------------------------
## Finding 2 — load base 0x60000000, derived from the image, then confirmed
Derived bottom-up: data-region dword tables hold pointer arrays into
0x6001c000-0x60042000, which is exactly the code span if payload offset 0 maps to
0x60000000; `l32r` literal pools then resolve inside the data region, and
`entry`/`call8` targets line up.
Independently confirmed afterwards in AMD's source: `DMUB_CW0_BASE = 0x60000000`
(`ref/dmub_srv.c:81`), with the driver stripping `PSP_HEADER_BYTES` (0x100)
before the copy — so file 0x100 → VA 0x60000000, as derived.
Window bases the firmware actually references as literals (CW0 code/data aside):
`0x65000000` (CW5 tracebuff) 1 ref, `0x66000000`/`0x6600011c`/`0x66000300`/
`0x6600b840` (CW6 fw state) 14 refs in 5 functions, `0x67000000`/`0x67002000`
(scratch) 4 refs in 3 functions. The image also carries its own version constant
`0x07003300` as a literal — a useful cross-check that the meta block is honest.
Function inventory for `dcn_3_2_0`: **1030** windowed functions in the code
region (VA 0x6001b6fc..0x60045434), median size 72 B, 21 `jx` indirect
dispatches, 12 of which have a jump table in scope (47, 48, 60, 62, 62, 38, 36,
31, 27, 26, 24, 14 entries). None is the ~129-entry shape a dense
`switch (header.type)` would produce, so the top-level command dispatch is
probably a comparison chain or a sparse two-level table.
--------------------------------------------------------------------------------
## Negative recorded — register access is NOT via absolute literals
Attempt: recover the DMCUB's MMIO aperture by voting `literal = APERTURE +
4*dword_offset` against all 12 490 register offsets in AMD's published
`dcn_3_2_0_offset.h`, then name every register the firmware touches.
Result: **no aperture.** A hypothesis of `APERTURE = 0x280` looked convincing —
literals 0x9d4/0x9d8/0x9dc map exactly onto the consecutive
`DMCUB_INBOX1_{SIZE,WPTR,RPTR}` registers, precisely the set a mailbox firmware
must touch — and it is **wrong**:
* a random-aperture control (400 draws) resolves a median of 46 and up to 88
literals to *some* seg-2 register name, versus 74 at 0x280, i.e. no better than
chance; DMCUB-prefixed hits: random median 1, p95 8, max 23, versus 9 at 0x280;
* the decisive check is usage, not arithmetic: **none** of the 26 `l32r` sites
loading those values is followed by a load or store through that register.
The following instructions are `addmi`/`addi`/`l8ui`/`s8i` — the values are
**offsets added to a base pointer**, i.e. fields of a large global state struct
(consistent with the hottest literal `0x6001b1e0`, 545 refs).
So: do not name DMCUB registers from `l32r` literals. Register offsets are small
immediates (`movi`/`movi.n`, 5017 occurrences) passed to an access helper — most
likely the function at **VA 0x6001c0f0**, which is the hottest call target in the
image (773 `call8` sites). Mapping register touches means resolving that helper's
arguments, which is the next mechanical step (see below).
--------------------------------------------------------------------------------
## Attack surface (from the ABI, `ref/dmub_cmd.h`)
The host→DMCUB channels, for the record:
* **inbox1 ring** in VRAM, 64-byte frames (`DMUB_RB_CMD_SIZE`), doorbell via
`DMCUB_INBOX1_WPTR`. `struct dmub_cmd_header` is `type:8, sub_type:8,
ret_status:1, multi_cmd_pending:1, is_reg_based:1, reserved:5,
payload_bytes:6`. The length field is 6 bits, so it **cannot** exceed the
64-byte frame — the PSP-ring class of "host-supplied length larger than the
buffer" bug is structurally excluded here.
* **register inbox0** (`is_reg_based`) — same commands through registers.
* **GPINT** doorbell: 41 commands (`enum dmub_gpint_command`), 16-bit param.
* **outbox1** for notifications back to the host.
* **36 top-level command types** (`enum dmub_cmd_type`, current master), sparse
0..128, including `DMUB_CMD__PSP`, `DMUB_CMD__SECURE_DISPLAY`,
`DMUB_CMD__LSDMA`, `DMUB_CMD__FUSED_IO`, `DMUB_CMD__DP_AUX_ACCESS`.
What the frame carries that is *not* structurally bounded — ranked as targets:
1. **`DMUB_CMD__EDID_CEA`** (`struct dmub_cmd_send_edid_cea`): a `uint16_t
offset` plus an 8-byte `payload[]` chunk, reassembled *inside* the firmware.
Host-controlled 16-bit offset into a firmware-side buffer, and the data being
reassembled is **monitor-supplied EDID/CEA** — the same malicious-sink theme
as the HDCP/ASD work in `psp_out/ANGLE-DISPLAY-SINK-INPUT-SURFACE.md`. If the
firmware does not bound `offset`, this is a write primitive. **Unverified.**
2. **`union dmub_addr` fields** — e.g. `DMUB_CMD__UPDATE_CURSOR_INFO`'s
`cursor_copy_src`/`cursor_copy_dst`, dirty-rect and FAMS2/`DMUB_CMD__LSDMA`
descriptors: host-supplied GPU addresses the DMCUB dereferences or DMAs.
Whether they are checked against the cache windows is unknown.
3. **`DMUB_CMD__PSP_ASSR_ENABLE`** — a host-triggered DMCUB→PSP path; worth
mapping because it crosses into this tree's existing target.
4. **AUX/DPCD and HPD paths** — 2358 `l8ui` in the image is a lot of byte
parsing, and the sink is attacker-controlled on the DP side.
--------------------------------------------------------------------------------
## Honest status
Established: provenance, container format, ISA (with controls), load base and
window usage, function inventory, dispatch-site inventory, the driver-side ABI
surface, and one recorded negative about register naming.
**Not** established, and not claimed: no handler has been identified, no bounds
check has been audited, no vulnerability is claimed, and there is no
DMCUB emulation. The image has **no strings** (no assert/file-name text), so
every function name must come from behaviour, not text.
### Next steps, in order of value per unit work
1. Resolve the register-access helper at `0x6001c0f0` (args of 773 call sites) →
register-name map → locate the inbox1 service loop and the `type` dispatcher.
2. From the dispatcher, walk to the `DMUB_CMD__EDID_CEA` handler and answer one
concrete question: **is the 16-bit `offset` bounded before the 8-byte copy?**
3. Diff `dcn_3_2_0` against `dcn_3_2_1` / `dcn_3_5` / `dcn_4_0_1` for handler
changes — the cross-version pipeline in `psp_out/vdiff/` is the model.
4. Only then consider hardware work; note that everything here is reachable from
an unprivileged-of-the-PSP position (amdgpu already drives this ring), so an
emulator-first approach is cheaper and safer than poking a live display path.
--------------------------------------------------------------------------------
## Files
| file | role |
| --- | --- |
| `fw/*.bin` | 10 `linux-firmware` DMCUB images (hashes in `data/MANIFEST.sha256`) |
| `fetch_ref.sh` | re-fetch the AMD/kernel reference sources into `ref/` (untracked) |
| `dmcub_parse.py` | container header + `dmub_fw_meta_info` parser |
| `dmcub_isa.py` | capstone-5 multi-arch coverage scan (how the ISA question was opened) |
| `dmcub_isa_evidence.py` | Xtensa evidence with in-tree positive/negative controls |
| `dmcub_map.py` | `l32r` literal recovery + in-image ratio as an ISA validator |
| `dmcub_cfg.py` | function inventory, `jx` dispatch sites, jump tables (needs capstone 6) |
| `dmcub_regmap.py` | aperture vote against AMD register offsets — **produced the negative above**; keep for the next attempt via the helper function |
|