aboutsummaryrefslogtreecommitdiffstats
path: root/mes
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-28 15:47:22 +0200
committeruser <user@clank>2026-09-28 15:47:22 +0200
commit7691d9a3b091eec96ce58bf049955da0f8d6bc65 (patch)
tree29b0b13343934258a1c4a1596f3e4adbf6fe409f /mes
parentAudit DMCUB end to end: ISA, command surface, and ten findings (diff)
downloadamd-psp-re-7691d9a3b091eec96ce58bf049955da0f8d6bc65.tar.gz
amd-psp-re-7691d9a3b091eec96ce58bf049955da0f8d6bc65.tar.xz
amd-psp-re-7691d9a3b091eec96ce58bf049955da0f8d6bc65.zip
Audit MES, PFP and ME; settle the IMU ISA as Xtensa
MES is the queue scheduler and, like the DMCUB inbox, is driven by host API packets -- it had never been read here. Confirm RV64 by real decode (92%), recover the container (code at 0xf0005000, data at 0xf0100000), and audit the packet surface: the 256-byte stride matches MES_API_MAX, both dispatch sites are bounds-checked, and 31 of 33 memcpy sites use constant lengths, so DMCUB's FINDING-1 pattern is absent. Retract, in place, an earlier identification of the two dispatchers against the kernel enums. The bounds were exact ABI fits, which felt like confirmation, but the table entries disassemble as mid-block fragments and epilogues; the prologue-scan function boundaries behind that model are unreliable on this image. MESAPI_MISC__WRITE_REG is left explicitly unresolved because the screening dataflow is not call-clobber aware. PFP and ME are also RV64 (container-parsed, load addr 0x3000). IMU is Tensilica Xtensa, correcting this tree's "undetermined": the old call was made over the whole container, which is dominated by the zero-padded DRAM half.
Diffstat (limited to 'mes')
-rw-r--r--mes/AUDIT-MES-01.md112
-rw-r--r--mes/AUDIT-MES-02-API.md180
-rw-r--r--mes/fw/gc_11_0_0_me.binbin0 -> 315120 bytes
-rw-r--r--mes/fw/gc_11_0_0_mes.binbin0 -> 286336 bytes
-rw-r--r--mes/fw/gc_11_0_0_mes1.binbin0 -> 220480 bytes
-rw-r--r--mes/fw/gc_11_0_0_mes_2.binbin0 -> 260624 bytes
-rw-r--r--mes/fw/gc_11_0_0_pfp.binbin0 -> 232096 bytes
-rw-r--r--mes/mes_audit.py313
8 files changed, 605 insertions, 0 deletions
diff --git a/mes/AUDIT-MES-01.md b/mes/AUDIT-MES-01.md
new file mode 100644
index 0000000..6a250c9
--- /dev/null
+++ b/mes/AUDIT-MES-01.md
@@ -0,0 +1,112 @@
+# MES (Micro Engine Scheduler) — first-pass audit
+
+Evidence class **[ST]** static, plus one **[HW]** loadability check on the card.
+Target: `fw/gc_11_0_0_mes_2.bin` and `gc_11_0_0_mes1.bin` (gfx11 / Navi 31),
+with `gc_11_0_0_pfp.bin`, `gc_11_0_0_me.bin` and the in-tree `mec_11_0_0.bin`
+alongside as controls. Tool: `mes_audit.py` (needs capstone RISC-V;
+`/home/user/.venvs/cs6/bin/python`).
+
+## Why it was a gap
+
+MES is the queue scheduler the driver talks to with API packets
+(`mes_v11_api_def.h`), i.e. it has the same *shape* of host-reachable command
+surface as the DMCUB inbox — and this tree had never examined it. The ISA map in
+`psp_out/FIRMWARE-INVENTORY.md` classified "MEC/MES" as RISC-V from a dword
+histogram, but no MES image was even in the corpus and no code was ever read.
+
+**It is loadable on this card [HW]:** the PSP accepts the MES ucode under
+`GFX_FW_TYPE_CP_MES` (33) — `RS64_MES` (76) is refused with 0xffff0006, i.e. not a
+supported type on this ASIC. Separately, gfx11 pulls PFP/ME/MES in through PSP
+**RLC autoload** from the TOC rather than per-type `LOAD_IP_FW`, which is why the
+tinygrad AM driver's load list shows only MEC.
+
+## Container (verified)
+
+```
+0x000 common_firmware_header + mes_firmware_header_v1_0 (header_size 0x48)
+0x100 payload; its first 0x100 bytes are a nested PSP header ($PS1 at +0x10)
+0x200 ucode image, size mes_ucode_size_bytes
+ -> the first 0x2000 bytes are zero and mes_uc_start_addr = 0xf0005000,
+ so the image base is 0xf0003000 and code starts at file 0x2200
++ data image at mes_ucode_data_offset_bytes -> mes_data_start_addr 0xf0100000
+```
+
+The **nested `$PS1` header is not a DMCUB quirk** — it is present identically in
+`mes`, `mes1`, `mes_2`, `mec`, `pfp`, `me` and `imu`. Any offset derived from a
+payload without skipping it is 0x100 low, which is exactly the error corrected in
+`dmcub/AUDIT-DMCUB-02.md`.
+
+## ISA and inventory
+
+| image | ucode | RV64 decode | fn prologues | indirect sites |
+| --- | --- | --- | --- | --- |
+| `gc_11_0_0_mes_2.bin` | 0x1f910 | 92.1% | 257 | 18 |
+| `gc_11_0_0_mes1.bin` (KIQ) | 0x15c40 | — | 67 | 8 |
+| `gc_11_0_0_pfp.bin` | 0x169a0 | 90.4% | 15 | — |
+| `gc_11_0_0_me.bin` | 0x2adf0 | 95.1% | 7 | — |
+| `mec_11_0_0.bin` | 0x41300 | 89.5% | 46 | — |
+
+Confirmed **RV64** by real decode, not by histogram: the first instruction at
+`mes_uc_start_addr` is a textbook prologue —
+
+```
+f0005000 130101f1 addi sp, sp, -0xf0
+f0005004 233c910c sd s1, 0xd8(sp)
+f0005008 9b042000 addiw s1, zero, 2
+```
+
+PFP/ME/MEC share the load address `0x3000` and are also RV64; their low prologue
+counts reflect flat state-machine style code, not a decode failure (90-95%
+coverage).
+
+## Command dispatch — all bounded
+
+`mes_audit.py <img> dispatch` recovers every indirect jump/call with its table and
+guard. Three are table dispatches of the kind the API packet handler compiles to,
+and each is bounds-checked before the `jr`:
+
+**Superseded in part by `AUDIT-MES-02-API.md`:** the three "tables" are one adjacent
+array read at three bases, and the attempt to map table entries to named opcodes was
+retracted after the entries turned out to be mid-block fragments. The *bounds* below
+are direct reads of the code and stand; the "36 handlers" framing does not.
+
+| site | table | entries | bound |
+| --- | --- | --- | --- |
+| `0xf0008aa4` | `0xf0100000` | 36 | `li a5,4; bltu a5,s1` → index ≤ 4 |
+| `0xf001919c` | `0xf0100014` | 31 | `li a4,0xa; bltu a4,a5` → index ≤ 10 |
+| `0xf00196b0` | `0xf0100040` | 20 | index ≤ 19 |
+
+The canonical one reads the opcode straight out of the host packet and bounds it:
+
+```
+f0019178 lw a5, 4(s0) ; opcode from the API packet
+f001917c li a4, 0xa
+f0019180 bltu a4, a5, 0xf001961c ; opcode > 10 -> default
+f0019188 lui a4, 0xf0100 ; addi a4, a4, 0x14
+f0019190 slli a5, a5, 2 ; add ; lw a5, 0(a5)
+f001919c jr a5
+```
+
+Of the 18 indirect sites, 17 have a guard in the preceding window. The one that
+does not (`0xf0021d10`) is **not** a command dispatch: it is a PC-relative
+computed goto (`auipc t0,0; add a3,a3,t0; jalr -0x68(a3)`) inside an unrolled
+byte-splat/`memset` routine, with the index derived from a length. Recorded as a
+library artefact, not a finding.
+
+## What is *not* audited here
+
+This is a first pass: container, ISA, inventory and dispatch bounds only. The part
+that matters for security is untouched:
+
+* **API packet field handling.** MES API packets carry host-supplied 64-bit
+ addresses (queue base, doorbell, rptr/wptr addresses, HQD state) and counts.
+ Nothing here checks whether those are validated — that is the real equivalent of
+ the DMCUB work and the obvious next step.
+* No copy-length audit (MES's byte-copy routines were not identified).
+* No per-opcode handler audit behind the three dispatch tables.
+* `mes1` (KIQ) only counted, not read.
+* PFP/ME/MEC: classified and container-parsed only.
+
+Emulation is plausible here in a way it was not for DMUB: RV64 is supported by
+both Unicorn and QEMU, so a `psp_emu`-style harness for MES would not need a new
+CPU model.
diff --git a/mes/AUDIT-MES-02-API.md b/mes/AUDIT-MES-02-API.md
new file mode 100644
index 0000000..8dd7bec
--- /dev/null
+++ b/mes/AUDIT-MES-02-API.md
@@ -0,0 +1,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.
diff --git a/mes/fw/gc_11_0_0_me.bin b/mes/fw/gc_11_0_0_me.bin
new file mode 100644
index 0000000..c8abd62
--- /dev/null
+++ b/mes/fw/gc_11_0_0_me.bin
Binary files differ
diff --git a/mes/fw/gc_11_0_0_mes.bin b/mes/fw/gc_11_0_0_mes.bin
new file mode 100644
index 0000000..eb3c840
--- /dev/null
+++ b/mes/fw/gc_11_0_0_mes.bin
Binary files differ
diff --git a/mes/fw/gc_11_0_0_mes1.bin b/mes/fw/gc_11_0_0_mes1.bin
new file mode 100644
index 0000000..89c862e
--- /dev/null
+++ b/mes/fw/gc_11_0_0_mes1.bin
Binary files differ
diff --git a/mes/fw/gc_11_0_0_mes_2.bin b/mes/fw/gc_11_0_0_mes_2.bin
new file mode 100644
index 0000000..37f6625
--- /dev/null
+++ b/mes/fw/gc_11_0_0_mes_2.bin
Binary files differ
diff --git a/mes/fw/gc_11_0_0_pfp.bin b/mes/fw/gc_11_0_0_pfp.bin
new file mode 100644
index 0000000..dbea275
--- /dev/null
+++ b/mes/fw/gc_11_0_0_pfp.bin
Binary files differ
diff --git a/mes/mes_audit.py b/mes/mes_audit.py
new file mode 100644
index 0000000..b4609e5
--- /dev/null
+++ b/mes/mes_audit.py
@@ -0,0 +1,313 @@
+#!/usr/bin/env python3
+"""First-pass audit tooling for the MES (Micro Engine Scheduler) firmware.
+
+MES is the RV64 scheduler microcontroller on gfx11; the driver drives it with API
+packets (`mes_v11_api_def.h`), so it has the same shape of host-reachable command
+surface as the DMCUB inbox that `../dmcub/` audits -- and it had never been looked
+at in this tree beyond an ISA classification.
+
+Container (verified, same nested-header trick as DMUB/MEC):
+ 0x000 common_firmware_header + mes_firmware_header_v1_0 (header_size 0x48)
+ 0x100 payload, whose first 0x100 bytes are a **PSP header** ($PS1 at +0x10)
+ 0x200 ucode image, mapped at `mes_uc_start_addr - 0x2000`
+ (the first 0x2000 bytes are zero, so code begins at file 0x2200 =
+ uc_start_addr, 0xf0005000)
+ +data data image at `mes_ucode_data_offset_bytes`, mapped at
+ `mes_data_start_addr` (0xf0100000)
+
+Requires capstone (RISC-V); the repo venv at /home/user/.venvs/cs6 works.
+
+usage:
+ mes_audit.py <image> info container, ISA evidence, function count
+ mes_audit.py <image> dispatch every indirect jump/call with table + bound
+ mes_audit.py <image> tables code-pointer tables in the data image
+"""
+import argparse
+import collections
+import re
+import struct
+import sys
+
+import capstone as cs
+
+GUARDS = ('bltu', 'bgeu', 'blt', 'bge', 'bne', 'beq', 'bnez', 'beqz', 'bltz', 'bgez')
+INDIRECT = ('jalr', 'jr', 'c.jalr', 'c.jr')
+
+
+class Mes:
+ """Handles both `mes_firmware_header_v1_0` (MES/MES1) and
+ `gfx_firmware_header_v2_0` (PFP/ME/MEC), so the same dispatch audit runs on
+ every RV64 gfx microcontroller on this ASIC."""
+
+ def __init__(self, path):
+ self.raw = open(path, 'rb').read()
+ self.path = path
+ (self.size, self.hdr_size, self.hv_maj, self.hv_min, self.ip_maj, self.ip_min,
+ self.ucode_version, self.ucode_size, self.ucode_array_offset,
+ self.crc32) = struct.unpack_from('<IIHHHHIIII', self.raw, 0)
+ if self.hdr_size >= 0x48:
+ # mes_firmware_header_v1_0
+ self.kind = 'mes'
+ self.mes_ucode_version, = struct.unpack_from('<I', self.raw, 0x20)
+ (self.mes_ucode_size, self.mes_ucode_offset,
+ self.mes_data_version, self.mes_data_size, self.mes_data_offset,
+ self.uc_start_lo, self.uc_start_hi,
+ self.data_start_lo, self.data_start_hi) = struct.unpack_from('<9I', self.raw, 0x24)
+ else:
+ # gfx_firmware_header_v2_0: feature, ucode size/off, data size/off, start addr
+ self.kind = 'gfx2'
+ (_feat, self.mes_ucode_size, self.mes_ucode_offset,
+ self.mes_data_size, self.mes_data_offset,
+ self.uc_start_lo, self.uc_start_hi) = struct.unpack_from('<7I', self.raw, 0x20)
+ self.mes_data_version = 0
+ self.data_start_lo = self.data_start_hi = 0
+ self.psp_hdr = None
+ if self.raw[self.ucode_array_offset + 0x10:self.ucode_array_offset + 0x14] == b'$PS1':
+ self.psp_hdr = self.raw[self.ucode_array_offset:self.ucode_array_offset + 0x100]
+ self.uc_file = self.ucode_array_offset + (0x100 if self.psp_hdr else 0)
+ self.uc = self.raw[self.uc_file:self.uc_file + self.mes_ucode_size]
+ # code starts at uc_start_addr; the leading zero pad tells us the image base
+ pad = 0
+ while pad + 64 <= len(self.uc) and self.uc[pad:pad + 64] == bytes(64):
+ pad += 64
+ self.code_off = pad
+ self.uc_base_va = (self.uc_start_lo - pad) & 0xffffffff
+ self.data_file = self.ucode_array_offset + self.mes_data_offset
+ self.data = self.raw[self.data_file:self.data_file + self.mes_data_size]
+ # gfx v2 images give no data load address; their pointer tables live in the
+ # ucode image itself, so point the data view at the ucode base instead.
+ self.data_va = self.data_start_lo if self.kind == 'mes' else self.uc_base_va
+ if self.kind != 'mes':
+ self.data = self.uc
+ self.md = cs.Cs(cs.CS_ARCH_RISCV, cs.CS_MODE_RISCV64 | cs.CS_MODE_RISCVC)
+ self.md.skipdata = True
+ self._ins = None
+
+ # ---- decode ---------------------------------------------------------
+ def ins(self):
+ if self._ins is None:
+ self._ins = list(self.md.disasm(self.uc, self.uc_base_va))
+ return self._ins
+
+ def real(self):
+ return [x for x in self.ins()
+ if x.mnemonic not in ('.byte', '.db', 'unimp', 'c.unimp')]
+
+ def code_lo(self):
+ return self.uc_base_va + self.code_off
+
+ def code_hi(self):
+ return self.uc_base_va + len(self.uc)
+
+ def word(self, va):
+ """32-bit word at a VA in either image."""
+ if self.data_va <= va < self.data_va + len(self.data) - 3:
+ return struct.unpack_from('<I', self.data, va - self.data_va)[0]
+ if self.uc_base_va <= va < self.uc_base_va + len(self.uc) - 3:
+ return struct.unpack_from('<I', self.uc, va - self.uc_base_va)[0]
+ return None
+
+ def is_code(self, va):
+ return self.code_lo() <= va < self.code_hi()
+
+ # ---- analyses -------------------------------------------------------
+ def functions(self):
+ return [x.address for x in self.real()
+ if x.mnemonic == 'addi' and x.op_str.startswith('sp, sp, -')]
+
+ def pointer_tables(self, min_len=6):
+ """Runs of consecutive dwords that are code addresses, in the data image."""
+ runs, cur = [], []
+ for i in range(0, len(self.data) - 3, 4):
+ w = struct.unpack_from('<I', self.data, i)[0]
+ if self.is_code(w):
+ cur.append((self.data_va + i, w))
+ else:
+ if len(cur) >= min_len:
+ runs.append(cur)
+ cur = []
+ if len(cur) >= min_len:
+ runs.append(cur)
+ return runs
+
+ def dispatches(self):
+ """Every indirect jump/call, with the table base and bound if recoverable.
+
+ Pattern this recognises (what the MES API dispatcher actually compiles to):
+ lw aX, off(pkt) ; opcode out of the host packet
+ li aY, N
+ bltu aY, aX, default ; opcode > N -> default
+ lui aT, hi ; addi aT, lo ; table base
+ slli aX, aX, 2 ; add ; lw ; jr
+ """
+ ins = self.ins()
+ out = []
+ for i, x in enumerate(ins):
+ if x.mnemonic not in INDIRECT:
+ continue
+ lo = max(0, i - 24)
+ win = ins[lo:i + 1]
+ base = bound = None
+ tbl_reg_hi = None
+ for w in win:
+ if w.mnemonic == 'lui':
+ m = re.match(r'(\w+), (0x[0-9a-f]+|\d+)', w.op_str)
+ if m:
+ tbl_reg_hi = (m.group(1), int(m.group(2), 0) << 12)
+ elif w.mnemonic == 'addi' and tbl_reg_hi and w.op_str.startswith(tbl_reg_hi[0] + ','):
+ m = re.match(r'\w+, \w+, (-?0x[0-9a-f]+|-?\d+)', w.op_str)
+ if m:
+ base = tbl_reg_hi[1] + int(m.group(1), 0)
+ elif w.mnemonic in ('li',):
+ m = re.match(r'(\w+), (-?0x[0-9a-f]+|-?\d+)', w.op_str)
+ if m:
+ bound = (m.group(1), int(m.group(2), 0))
+ elif w.mnemonic in GUARDS:
+ if bound and bound[0] in w.op_str.split(',')[0]:
+ bound = ('%s %s' % (w.mnemonic, w.op_str), bound[1])
+ if base is None and tbl_reg_hi:
+ base = tbl_reg_hi[1]
+ n = 0
+ if base is not None:
+ while n < 256:
+ v = self.word(base + 4 * n)
+ if v is None or not self.is_code(v):
+ break
+ n += 1
+ guard = next((w for w in reversed(win) if w.mnemonic in GUARDS), None)
+ out.append(dict(at=x.address, table=base, entries=n,
+ guard=('%s %s' % (guard.mnemonic, guard.op_str)) if guard else None,
+ bound=bound[1] if bound else None))
+ return out
+
+
+MEMCPY_VA = 0xf0021b28 # verified: xor/andi-7 alignment test, a7 = dst + len, word loop + byte tail
+LOAD_MNEM = ('lw', 'lwu', 'lb', 'lbu', 'lh', 'lhu', 'ld', 'c.lw', 'c.ld', 'c.lwu')
+
+
+def _ops(insn):
+ return [o.strip() for o in insn.op_str.split(',')]
+
+
+def provenance(img, ins, addrs, site, reg, back=200, depth=0):
+ """Walk back linearly for the last writer of `reg` before `site`.
+
+ Folds the RISC-V `lui`/`addi` constant pair, which is why site 0xf0014804 is a
+ CONSTANT 0x2a154 and not a variable length -- an earlier version of this pass
+ reported it as host-derived, which was wrong. Linear and intra-block only: it
+ does not follow control flow, so treat 'unknown' as 'needs eyes', not as safe.
+ """
+ if depth > 4:
+ return ('deep', '', None)
+ i = addrs.index(site)
+ for a in reversed(addrs[max(0, i - back):i]):
+ x = ins[a]
+ o = _ops(x)
+ if not o or o[0] != reg:
+ continue
+ if x.mnemonic in ('li', 'c.li'):
+ return ('const', int(o[1], 0), a)
+ if x.mnemonic in ('addi', 'c.addi') and len(o) > 2:
+ if o[1] == 'zero':
+ return ('const', int(o[2], 0), a)
+ base = provenance(img, ins, addrs, a, o[1], back, depth + 1)
+ if base[0] == 'const': # lui/addi pair -> one constant
+ return ('const', (base[1] + int(o[2], 0)) & 0xffffffffffffffff, a)
+ return ('computed', x.op_str, a)
+ if x.mnemonic == 'lui':
+ return ('const', int(o[1], 0) << 12, a)
+ if x.mnemonic in LOAD_MNEM:
+ return ('load', x.op_str, a)
+ if x.mnemonic in ('mv', 'c.mv'):
+ return provenance(img, ins, addrs, a, o[1], back, depth + 1)
+ return ('computed', '%s %s' % (x.mnemonic, x.op_str), a)
+ return ('unknown', '', None)
+
+
+def cmd_copies(img):
+ """Classify every memcpy call site by the provenance of its length argument.
+
+ This is the MES analogue of dmcub/dmcub_copyaudit.py, which is what found the
+ DMCUB ABM_INIT_CONFIG overflow (FINDING-1): a u16 length loaded straight from a
+ host frame into memcpy. Result for gc_11_0_0_mes_2: no such site exists.
+ """
+ ins = {x.address: x for x in img.ins()}
+ addrs = sorted(ins)
+ sites = []
+ for a in addrs:
+ x = ins[a]
+ if x.mnemonic not in ('jal', 'call'):
+ continue
+ tgt = x.op_str.split()[-1]
+ if not tgt.startswith('0x') or int(tgt, 16) != MEMCPY_VA:
+ continue
+ sites.append((a, provenance(img, ins, addrs, a, 'a2'),
+ provenance(img, ins, addrs, a, 'a0'),
+ provenance(img, ins, addrs, a, 'a1')))
+ print('memcpy(0x%08x) call sites: %d' % (MEMCPY_VA, len(sites)))
+ counts = {}
+ for _, ln, _, _ in sites:
+ counts[ln[0]] = counts.get(ln[0], 0) + 1
+ print('length provenance: %s\n' % counts)
+ for site, ln, dst, src in sites:
+ if ln[0] == 'const':
+ desc = 'len=0x%x const' % ln[1]
+ flag = ''
+ elif ln[0] == 'load':
+ desc = 'len<-LOAD %s' % ln[1]
+ flag = ' <== REVIEW: length from memory'
+ else:
+ desc = 'len<-%s %s' % (ln[0], str(ln[1])[:28])
+ flag = ' <== REVIEW: length computed'
+ print(' 0x%08x %-34s dst<-%-9s %s%s' % (
+ site, desc, dst[0], str(dst[1])[:24], flag))
+ var = [s for s in sites if s[1][0] != 'const']
+ print('\n%d/%d sites use a constant length; %d need review' % (
+ len(sites) - len(var), len(sites), len(var)))
+ return sites
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('image')
+ ap.add_argument('cmd', choices=['info', 'dispatch', 'tables', 'copies'])
+ a = ap.parse_args()
+ m = Mes(a.image)
+ if a.cmd == 'info':
+ print('%s' % a.image)
+ print(' header %d.%d ip %d.%d, header_size 0x%x' % (
+ m.hv_maj, m.hv_min, m.ip_maj, m.ip_min, m.hdr_size))
+ print(' nested PSP header: %s' % ('$PS1 present' if m.psp_hdr else 'absent'))
+ print(' ucode: file 0x%x size 0x%x -> image base VA 0x%08x, code at VA 0x%08x' % (
+ m.uc_file, m.mes_ucode_size, m.uc_base_va, m.code_lo()))
+ print(' data : file 0x%x size 0x%x -> VA 0x%08x' % (
+ m.data_file, m.mes_data_size, m.data_va))
+ ins, real = m.ins(), m.real()
+ print(' decode: %d instructions, %d real (%.1f%%)' % (
+ len(ins), len(real), 100.0 * len(real) / max(1, len(ins))))
+ top = collections.Counter(x.mnemonic for x in real).most_common(8)
+ print(' top mnemonics: %s' % ' '.join('%s:%d' % kv for kv in top))
+ print(' function prologues (addi sp,sp,-N): %d' % len(m.functions()))
+ print(' indirect jumps/calls: %d' % len(m.dispatches()))
+ elif a.cmd == 'copies':
+ cmd_copies(m)
+ elif a.cmd == 'tables':
+ for r in m.pointer_tables():
+ print(' table VA 0x%08x n=%-4d first: %s' % (
+ r[0][0], len(r), ' '.join('0x%08x' % v for _, v in r[:4])))
+ else:
+ print('%-11s %-11s %-7s %-7s %s' % ('site', 'table', 'entries', 'bound', 'guard'))
+ unguarded = 0
+ for h in m.dispatches():
+ if h['guard'] is None:
+ unguarded += 1
+ print('0x%08x %-11s %-7s %-7s %s' % (
+ h['at'], ('0x%08x' % h['table']) if h['table'] else '-',
+ h['entries'] or '-', h['bound'] if h['bound'] is not None else '-',
+ h['guard'] or '*** none in window ***'))
+ print('\n%d indirect sites, %d with no guard in the preceding window' % (
+ len(m.dispatches()), unguarded))
+
+
+if __name__ == '__main__':
+ main()