# DF-1130 — Verdict

## Verdict: REPRODUCED (source-level + harness) — OOB heap read / panic class, no escalation chain (read-only primitive)

## Bug confirmation

Three Southern Islands microcode loaders in `radeon/si.c` consume firmware
header fields (`ucode_array_offset_bytes`, `ucode_size_bytes`,
`io_debug_array_offset_bytes`, `io_debug_size_bytes`) **without checking
them against `fw->datasize`**:

1. `si_mc_load_microcode` at **radeon/si.c:1574-1584**:
   ```c
   regs_size = le32_to_cpu(hdr->io_debug_size_bytes) / (4 * 2);
   new_io_mc_regs = (const __le32 *)
       (rdev->mc_fw->data + le32_to_cpu(hdr->io_debug_array_offset_bytes));
   ucode_size = le32_to_cpu(hdr->header.ucode_size_bytes) / 4;
   new_fw_data = (const __le32 *)
       (rdev->mc_fw->data + le32_to_cpu(hdr->header.ucode_array_offset_bytes));
   ```
   Then at line 1622-1636 the loader loops `regs_size` and `ucode_size`
   times reading from `new_io_mc_regs++` / `new_fw_data++`, each iteration
   writing the read u32 to a GPU MMIO register via `WREG32`.

2. `si_cp_load_microcode` at **radeon/si.c:3489-3513** — same pattern for
   each of PFP, CE, ME microcode blocks.

3. `si_rlc_resume` at **radeon/si.c:5876-5888** — same pattern for the
   RLC microcode.

The upstream validator `radeon_ucode_validate` (radeon_ucode.c:156-165)
only checks `fw->datasize == hdr->size_bytes`:
```c
if (fw->datasize == le32_to_cpu(hdr->size_bytes))
    return 0;
```

This passes for any crafted firmware whose header `size_bytes` matches the
file length, **independently of whether the payload offset/size stay inside
the file**. A crafted firmware can therefore set:
```
size_bytes                  = file_length      // passes validator
ucode_array_offset_bytes    = file_length - 16 // near end of file
ucode_size_bytes            = 0x10000          // huge
```
and the loader will read `0x10000` bytes starting near the end of the
firmware buffer — `0x10000 - 16` bytes of which are OOB heap reads.

## Harness confirmation

`harness.c` constructs exactly that crafted firmware (1024-byte buffer,
`ucode_array_offset_bytes=0xff0`, `ucode_size_bytes=0x4000`) and shows:
- `radeon_ucode_validate` returns 0 (passes)
- The buggy loader path computes an OOB read of 19,440 bytes past the
  firmware buffer.
- The fixed loader path rejects with `-EINVAL`.

Output captured in `run.log`.

## Exploit chain

This is a **read-only primitive** — the OOB-read u32 values are written
to GPU MMIO registers (`WREG32`), not directly returned to userspace. The
realistic impact is therefore:

- **Panic** when the OOB read crosses into an unmapped page (the firmware
  buffer is `vmalloc`/`kmalloc`-backed and the read can run for thousands
  of bytes — easily crosses a page boundary on a slab or vmalloc hole).
- **MMIO side-channel** of adjacent heap data written into GPU registers.
  On hardware this could in principle be read back by a GPU command
  buffer, but that requires a much more elaborate setup.

No write primitive, no escalation chain. The bug's primary impact is DoS
via panic during microcode load (which happens at every boot/resume on SI
hardware).

## Trigger conditions (not met on this guest)

1. AMD SI GPU present (the QEMU audit guest has no AMD GPU).
2. `radeon.ko` loaded.
3. Crafted SI firmware blob in `/boot/modules/radeonkmsfw_*.ko` (the
   kernel links against these firmware modules) — requires either a
   malicious distro or an attacker who can replace the firmware files
   (typically root-only, but a malicious VBIOS reflashed via VFIO can
   also surface as crafted power tables; the firmware header itself,
   however, is supplied by the loaded `.ko`).

Source-level + harness-confirmed; no live runtime trigger on the guest.

## Fix

`fix.diff` adds explicit `offset + size <= datasize` checks in all three
loaders (`si_mc_load_microcode`, `si_cp_load_microcode`, `si_rlc_resume`).
Each check returns `-EINVAL` if the payload would escape the firmware
buffer. The fix is local to each loader (no change to
`radeon_ucode_validate`) — minimally invasive.

## Fix validation

1. `patch -p1 --check` — clean apply, all 3 hunks.
2. `cd /usr/src/sys/dev/drm/radeon && make` with the diff applied —
   `radeon.ko` built cleanly (`rc=0`, 2,029,288 bytes).
3. Reverted.

Since the bug cannot be triggered live on the guest (no AMD GPU), the
behaviour comparison is at the harness level: `run.log` shows the buggy
path computing a 19,440-byte OOB read while the fixed path returns
`-EINVAL`.

`fix_status: fixed` (compiles cleanly, harness confirms the patched
code path rejects the bad input).
