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/s4without setting them, i.e. they belong to the frame of the large routine at0xf0015a88(frame-0x5c0); - the dispatchers sit in code with different frames (
-0x90,-0x50), and ajrinto 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:
- recursive-descent disassembly from real entry points plus call-clobber-aware
dataflow (the DMUB tooling in
../dmcub/dmcub_dis.pydoes this; MES has no equivalent yet), or - 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:
0xf0014804first reported as host-derivedlen = a2 + 0x154. It is actually the RISC-Vlui/addiconstant pair0x2a000 + 0x154=0x2a154constant. The detector now foldslui/addi.0xf001aa1cfirst reportedunknownat a 60-instruction lookback; at 200 it resolves to a constant0x80. 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 naivesizeoflayout 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_MESload acceptance was proven inAUDIT-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_REGremains 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.
