# 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 in `gc_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, where `dmcub_regmap.py` validated 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 ```sh ./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`.