aboutsummaryrefslogtreecommitdiffstats
path: root/f32/AUDIT-F32-01.md
blob: 1d877a285bd140c33d4209169420621ffa0beb3b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# 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`.