aboutsummaryrefslogtreecommitdiffstats
path: root/mes/AUDIT-MES-02-API.md
blob: 8dd7bec57bd4f6da9699d680e2987c9fba3883f8 (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
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
# MES — API packet surface: dispatch mapping and copy audit

Evidence class **[ST]** static only. Image `fw/gc_11_0_0_mes_2.bin` (gfx11 / Navi 31).
Follows `AUDIT-MES-01.md`, which established the container, RV64 and dispatch bounds
but explicitly left the *packet fields* unaudited. Reference enums read from a real
kernel tree at `/home/user/src/linux-7.2.5/drivers/gpu/drm/amd/include/mes_v11_api_def.h`
(not from memory).

## Packet plumbing, recovered

The ring walk in the dispatcher gives the packet geometry directly:

```
f001914c  lui  s1, 0xf013b
f0019150  ld   a3, -0x4c0(s1)     ; global context base
f001915c  add  a5, a3, a5         ; + 0x3000
f0019160  lwu  s0, 0x1a8(a5)      ; current slot index
f001916c  slli s0, s0, 8          ; * 256   <-- packet stride
f0019170  addi s0, s0, 8
f0019174  add  s0, a3, s0         ; s0 = API packet
```

A **256-byte packet stride** matches `MES_API_MAX` (64 dwords). `s0` is the packet
base used by every handler, which is what makes the field offsets below meaningful.

## Two dispatchers — bounds solid, opcode mapping RETRACTED

**Verifiable, read straight out of the code:** there are two indirect dispatch sites
that index an adjacent pointer array in the data image at `0xf0100000`:

| site | index source | bound | table base | entries |
| --- | --- | --- | --- | --- |
| `0xf001919c` | `lw 4(s0)` (whole word) | ≤ 10 | `0xf0100014` | 11 |
| `0xf00196b0` | `andi a5, 0xff` | ≤ 19 | `0xf0100040` | 20 |

Every index is bounds-checked before the `jr`. That part is unconditional.

### What I retract

An earlier revision of this document identified these as `MESAPI_MISC_OPCODE`
(≤ 10 = 10 ops + `MAX`) and `MES_API_HEADER.opcode` (≤ 19 = `MES_SCH_API_SET_HW_RSRC_1`)
on the strength of those bounds being exact fits for the kernel enums. **That
identification does not hold up and is withdrawn.** Disassembling each table target
shows they are not handler entry points:

```
idx 3 ("READ_REG")             -> f0016360  jal 0xf0020074   ; a *write* helper, mid-sequence
idx 5 ("SET_SHADER_DEBUGGER")  -> f0016404  and a0,a0,s0 ... addi sp,sp,0x30 ; ret   ; an epilogue
idx 6 ("NOTIFY_WORK_...")      -> f0016448  addi a1,a1,-2 ; seqz a0,a1 ; ret
idx 9 ("LAUNCH_CLEANER_SHADER")-> f00161a0  li s3,1 ; j 0xf0015b10
```

Those are mid-block fragments and function tails, not per-opcode handlers. The
numeric coincidence (11 and 20 entries matching the two enum sizes) is exactly the
kind of thing that makes a wrong model feel confirmed, and it fooled me.

Two structural facts now conflict and I cannot yet resolve them:

* the targets read as continuations that consume `s0/s1/s2/s4` without setting them,
  i.e. they belong to the frame of the large routine at `0xf0015a88` (frame `-0x5c0`);
* the dispatchers sit in code with different frames (`-0x90`, `-0x50`), and a `jr`
  into another function's frame cannot work.

So either the 36-word array is a *different* switch's jump table that the two
dispatchers also index (unlikely), or my function boundaries are wrong. They are
derived from an `addi sp,sp,-N` prologue scan, and **that heuristic is not reliable
on this image** — which also undercuts `AUDIT-MES-01`'s claim that the 36 entries are
"jump-table targets inside one big routine". Sliding the table base over ±0x80 makes
**0 of 20** entries land on a prologue, so the array is not a table of function
pointers under any offset; the mapping question is genuinely open.

## Register access: how it works, and the question left open

Register access is centralised in locked wrappers, which is useful regardless of the
opcode mapping:

```
f0020074  WRITE(a0 = reg dword index, a1 = value, a2 = selector)
            -> lock 0x530 -> slli a0, s2, 2  -> raw write 0xf001f404
f0020358  READ / f00200f8 READ  -> same shape -> raw read 0xf001f500
```

`a0` is a **dword index**, scaled by 4 inside the wrapper. 30 call sites (21 write,
9 read); the raw primitives are reached only through these wrappers.

At the most WRITE_REG-looking site, the register index is *not* host-chosen:

```
f001627c  add  a5, a5, s4          ; a5 = ctx + 0x3000
f0016280  lw   a5, 0x1dc(a5)       ; register segment base, held in context
f0016284  lw   a2, 0x1c(s0)        ; packet field -> third arg (selector), NOT the index
f001628c  addw a0, s1, a5          ; index = segment base + s1, and s1 = lui 1; addiw 0xa0 = 0x10a0
f0016298  jal  0xf0020074
```

So: **firmware-constant offset over a segment base taken from the context**, with the
packet supplying only the selector. Note the segment bases themselves arrive from the
host, via `MESAPI_SET_HW_RESOURCES.gc_base[] / mmhub_base[] / osssys_base[]` — by
design, the driver tells MES where the register apertures are.

**Open, and explicitly not answered:** amdgpu *does* send `MESAPI_MISC__WRITE_REG`
with a driver-chosen `reg_offset` on gfx11 (`mes_v11_0.c:626`), so a host-offset
register write must exist in this firmware, yet none of the 30 wrapper sites takes
its index from a packet field. Either my sink list is incomplete or the offset
arrives through a wrapper argument. My backward-scan dataflow is **not call-clobber
aware** (a `jal` between definition and use silently breaks the chain), so *absence
of evidence here is weak evidence* and must not be read as "the offset is bounded".

Answering it needs one of:
1. recursive-descent disassembly from real entry points plus call-clobber-aware
   dataflow (the DMUB tooling in `../dmcub/dmcub_dis.py` does this; MES has no
   equivalent yet), or
2. emulation — MES is RV64, so Unicorn/QEMU can run it, unlike DMUB's Xtensa.

## Copy audit: the DMCUB FINDING-1 pattern is absent

`mes_audit.py <img> copies` is the MES analogue of `dmcub/dmcub_copyaudit.py`, the
pass that found DMCUB's `ABM_INIT_CONFIG` overflow (an unchecked host `u16` length
reaching `memcpy`). memcpy is `0xf0021b28`, identified structurally (`xor a0,a1`
alignment test, `a7 = dst + len`, word loop with byte tail), 33 call sites.

**Result: 31 of 33 sites use a compile-time constant length.** The 2 remaining are
both inside the printf-family helpers `0xf002105c` / `0xf0021144`, where the length
is `s1 >> 38` — a bitfield extract from an internally-built descriptor, reached only
with firmware-owned format data. **No site takes a length from a host packet field.**

Two corrections worth recording, because the first draft of this pass got them wrong:

* `0xf0014804` first reported as host-derived `len = a2 + 0x154`. It is actually the
  RISC-V `lui`/`addi` constant pair `0x2a000 + 0x154` = **`0x2a154` constant**. The
  detector now folds `lui`/`addi`.
* `0xf001aa1c` first reported `unknown` at a 60-instruction lookback; at 200 it
  resolves to a constant `0x80`. The default window is now 200.

Negative control: the same pass on `gc_11_0_0_mes1.bin` finds **0** sites, because
`MEMCPY_VA` is per-image — a control for the address, *not* evidence that MES1 has no
copies. MES1's memcpy has not been located.

## One observation kept as an observation, not a finding

`0xf00147a0` copies **`0x2a154` bytes (172 372) from an internal pointer to a 64-bit
address taken out of the current API packet** (`lwu 4(s0)` | `lwu 8(s0) << 32`),
reached from `0xf0019ba0 → 0xf0019624 → 0xf00147a0` on an event path that logs event
id `0xf` immediately before:

```
f0019b00  li   a0, 0xf
f0019b04  jal  0xf0011400      ; event log
f0019b08  jal  0xf00147a0      ; -> memcpy(packet_addr, internal, 0x2a154)
```

So MES will write 172 KB of its internal state to a GPU address the packet names,
and **the packet carries no capacity field** — the size is an implicit ABI contract
between driver and firmware. That is a design note, not a vulnerability claim:

* The submitter is the kernel driver, so this is not a privilege boundary (same
  framing as every DMCUB finding — see `../dmcub/AUDIT-DMCUB-03-COMMANDS.md`).
* Whether the write is confined by MES's address translation is **untested**. The
  analogous DMCUB question (FINDING-7) resolved *negative* on hardware because the
  window hardware confined it, so the honest prior here is "probably confined".
* The exact opcode is **unconfirmed**. The shape matches `MESAPI__AMD_LOG`
  (`p_buffer_memory` + `p_buffer_size_used`, no capacity), but the field offsets sit
  4 bytes off a naive `sizeof` layout of that struct, so the identification is not
  claimed.

## Not done

* No hardware verification of anything here. MES is **not loaded by the tinygrad AM
  driver** at all, so testing needs either MES bring-up (GFX is live on this card —
  risky) or an amdgpu host. Note `CP_MES` load acceptance was proven in
  `AUDIT-MES-01.md`, so bring-up is at least reachable.
* `ADD_QUEUE`'s address fields (`mqd_addr`, `wptr_addr`, `doorbell_offset`,
  `trap_handler_addr`) are read and stored but their validation was not traced —
  675 sites reassemble 64-bit addresses from structures in this image, and only 2
  reach memcpy, so the remaining sinks are register writes and pointer stores that
  need per-opcode work.
* `MESAPI_MISC__WRITE_REG` remains **unresolved** (see above): the handler was not
  located, and the negative result from the register-sink scan is not trustworthy
  because that pass is not call-clobber aware.
* No audit of the 5-entry group, MES1/KIQ, or the doorbell paths.