F32 (RLC / SDMA) — the tree's oldest blocker is lifted
Evidence class [ST]. Tool: f32_audit.py (offline --selftest), built on
fail0verflow's radeon-tools F32 disassembler (fetched, not vendored — see below).
What was blocked, and why this changes it
psp_out/FIRMWARE-COVERAGE-MATRIX.md carried RLC and SDMA as blocked: fixed-width
32-bit words, NOP 0x90000000, and no public disassembler. psp_out/f32_probe.py had
verified that rather than inheriting it — no capstone architecture fits, and the best
scorer (Thumb, 98%) is meaningless because Thumb decodes almost anything.
fail0verflow's radeon-tools contains f32/f32dis.py, a disassembler for the Radeon
microcode processor ("F32", despite being 64-bit capable), written for Sea Islands
(CI) CE/ME/MEC/PFP/RLC. Its README says "SDMA uses a related but incompatible
variant, so will not work".
Two things had to be checked before trusting any of it on gfx11 hardware that is three generations newer, and one of them contradicts upstream.
The ISA matches — but "percentage decoded" is not the evidence
The decoder is permissive, so raw coverage proves nothing:
| blob | decoded | ret |
calls resolving in-image |
|---|---|---|---|
BONAIRE_rlc (CI, tool's own target) — positive control |
100.0% | 57 | 100% |
BONAIRE_pfp (CI) — positive control |
100.0% | 3 | 100% |
gfx11 mec (RV64) — negative control |
78.4% | 0 | 0% |
| random bytes — negative control | 25.9% | 0 | n/a |
An RV64 image scores 78% decoded. What actually separates real F32 from noise is
ret count and whether call targets resolve to in-image labels — both are zero
for every negative and high for every positive.
Results on this card's blobs
f32_audit.py info parses the containers (which upstream cannot: it hardcodes a
0x100 skip) and reports per sub-blob:
SDMA — decodes cleanly, contradicting upstream's caveat
blob offset size $PS1 insns unk ret calls-ok verdict
TH0 0x100 0x4400 True 4096 107 61 100.0% F32 CODE (base 0x0)
TH1 0x4500 0x4200 True 4096 106 71 100.0% F32 CODE (base 0x0)
Upstream's "SDMA will not work" holds for CI-era SDMA; the gfx11 SDMA threads decode with every call target resolving and 61/71 returns. Sample from TH0's entry:
nop
mov r5, #0x107f
_start_0:
ldw r8, reg[r0, #0x5980] ; poll a register
cbz r8, _start_1
bl _PKT_0xf00_44
b _start_0 ; back-edge
_start_1:
ldw r5, reg[r0, #0x4af8]
lsr r6, r5, #30 ; extract a bit field
and r6, r6, #0x1
cbz r6, _start_2
RLC — three code blobs, and a load base that had to be recovered
blob offset size $PS1 insns unk ret calls-ok verdict
RLC_G 0x100 0x6200 True 6144 187 191 99.3% F32 CODE (base 0x2000)
IRAM 0xfc20 0x10200 True 16384 6917 0 100.0% data (no returns; 42% undecodable)
DRAM 0x1fe20 0x8200 True 8192 635 0 100.0% data (no returns; 8% undecodable)
RLCP 0x28020 0x2200 True 2048 43 15 98.8% F32 CODE (base 0x0)
RLCV 0x2a220 0x3200 True 3072 96 98 99.2% F32 CODE (base 0x0)
SRL_GPM 0x6300 0xa00 True 0 0 0 empty
SRL_SRM 0x6d00 0x8f20 True 8192 258 0 81.8% data (no returns; 3% undecodable)
RLC_G is loaded at word address 0x2000. Disassembled at base 0 it looks broken —
915 branches point outside the image. Their targets span 0x2004..0x37ba, which is
exactly the blob's own size shifted by 0x2000, and subtracting that puts 100% of
them in range. Upstream hides this: its loc() prints the literal string True for
an out-of-range target, so the giveaway is invisible. One-hunk fix in
f32dis-loc.patch.
IRAM, DRAM and SRL_SRM have no return instruction anywhere, which is the
discriminator — they are data (register save/restore lists), matching their names.
Stray "calls" there are data words that happen to decode as branches, which is exactly
why call count alone is not used for classification.
What is NOT established
- The register namespace is unmapped.
reg[r0, #0x4a14]and friends are the microengine's own index space, not the driver's dword offsets: only 6 of 54 distinct indices appear ingc_11_0_0_offset.h(11%, i.e. coincidence), and a search over all deltas in ±0x2000 peaks at 9%. Until this is resolved, register accesses in the listing cannot be named — unlike DMCUB, wheredmcub_regmap.pyvalidated the register-address table semantically. - The opcode table is CI-era. 2.6–3% of words in the gfx11 code blobs decode as
unknown (
dw 0x...). Some of that is embedded data/jump tables, but some is probably gfx11 opcodes the CI-era table lacks, and the SDMA variant may differ further. Treat the listing as a strong draft, not ground truth. - No security audit has been done. This establishes readability only. Nothing here looks for bugs yet.
Why this matters next
SDMA is the highest-value target now readable: it is a DMA engine driven by
host-supplied packets from rings the driver writes, i.e. exactly the shape that
produced DMCUB FINDING-1/2 and the DCN4 LSDMA candidate (FINDING-9). It is also
loadable on this card (SDMA_UCODE_TH0/TH1 are 2 of the 37 accepted types), so unlike
FINDING-9 anything found here is testable on the silicon we have. RLC matters for a
different reason: it owns power/clock-gating state and the register save/restore
lists.
Reproducing
./fetch_ref.sh # clones radeon-tools, applies our patch
./f32_audit.py --selftest # controls
./f32_audit.py info ../psp_out/gc_11_0_0_rlc.bin
./f32_audit.py dis ../psp_out/sdma_6_0_0.bin --blob TH0 | less
Upstream is not vendored: the repository has no LICENSE file and f32dis.py no
license header, so it is fetched into the gitignored ref/ like dmcub/ref/, pinned
to commit e2eb366, with our change kept as f32dis-loc.patch.
