aboutsummaryrefslogtreecommitdiffstats

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.