# DF-1164 — gfx_v7_0 CP/MEC/RLC firmware loaders: OOB read past ucode blob (source-only verification)

## Verdict: REPRODUCED at source level (amdgpu is a kld module, not in GENERIC; not runtime-triggerable without AMD Sea Islands GPU)

## Mechanism

`amdgpu` is **not** in `X86_64_GENERIC` (no `device drm`/`device amdgpu` in
`sys/config/X86_64_GENERIC`). It exists only as the loadable module
`/boot/kernel/amdgpu.ko` and only attaches to AMD Sea Islands (CIK) GPUs
(BONAIRE/KAVERI/KABINI/HAWAII/MULLINS — `sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:283-289`).

The common firmware validator only checks total blob size:

```c
/* sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260 */
int amdgpu_ucode_validate(const struct firmware *fw)
{
    const struct common_firmware_header *hdr =
        (const struct common_firmware_header *)fw->data;

    if (fw->datasize == le32_to_cpu(hdr->size_bytes))   /* total-size only */
        return 0;
    return -EINVAL;
}
```

The header (`sys/dev/drm/amd/amdgpu/amdgpu_ucode.h:26-37`) declares
`ucode_size_bytes` and `ucode_array_offset_bytes` separately. The validator
does **not** check that `ucode_array_offset_bytes + ucode_size_bytes <=
fw->datasize`.

The CIK gfx_v7_0 firmware loaders then trust both header fields blindly:

```c
/* sys/dev/drm/amd/amdgpu/gfx_v7_0.c:2447-2453 (PFP; identical pattern CE/ME/MEC1/MEC2/RLC) */
fw_data = (const __le32 *)
    (adev->gfx.pfp_fw->data +
     le32_to_cpu(pfp_hdr->header.ucode_array_offset_bytes));
fw_size = le32_to_cpu(pfp_hdr->header.ucode_size_bytes) / 4;
WREG32(mmCP_PFP_UCODE_ADDR, 0);
for (i = 0; i < fw_size; i++)
    WREG32(mmCP_PFP_UCODE_DATA, le32_to_cpup(fw_data++));   /* reads past blob */
```

Same pattern at:
- `gfx_v7_0.c:2457-2463` (CE)
- `gfx_v7_0.c:2467-2473` (ME)
- `gfx_v7_0.c:2708-2715` (MEC1)
- `gfx_v7_0.c:2731-2737` (MEC2)
- `gfx_v7_0.c:3572-3577` (RLC)

**Effect:** A crafted firmware blob with `ucode_size_bytes` (and/or
`ucode_array_offset_bytes`) declaring more data than the blob actually holds
passes `amdgpu_ucode_validate` (only `size_bytes` is checked) and then makes
the loader loop `le32_to_cpup(fw_data++)` read past the blob into adjacent
kernel heap. The leaked heap bytes are then written to GPU microcode RAM
(so they are not directly leaked back to userspace, but the OOB read itself is
unbounded and attacker-shaped by the crafted header). Same class as the
DF-1119/1130/1133 family.

## Trigger reachability on this guest

- No AMD Sea Islands GPU is present in the QEMU guest.
- The amdgpu module does load (`kldload amdgpu` succeeds with the message
  `[drm] amdgpu kernel modesetting enabled.`) but attaches to no device, so
  `gfx_v7_0_cp_gfx_load_microcode` / `gfx_v7_0_cp_compute_load_microcode` /
  `gfx_v7_0_rlc_resume` are never called. (Loading the module on a
  non-AMD-GPU host is also a bad idea — see the unload hang note below.)
- The vulnerable code path is therefore not runtime-reachable here.

> ⚠ Practical note: loading `amdgpu.ko` on this guest (no AMD GPU) succeeded,
> but `kldunload amdgpu`/`kldunload drm` then **wedged the guest** (process
> stuck in state `D1` uninterruptible). After observing that, we used
> `vm.sh reset with-src` to recover. This is unrelated to the cited OOB bug
> (it's a known module-refcount issue), but it confirms there is no way to
> safely exercise amdgpu code paths on this guest.

## Fix

`fix.diff` extends `amdgpu_ucode_validate` to also check that the declared
ucode payload lies within the blob:
```c
int amdgpu_ucode_validate(const struct firmware *fw)
{
    const struct common_firmware_header *hdr = ...;
    uint32_t ucode_size, ucode_offset;

    if (fw->datasize < sizeof(*hdr))            /* header fits */
        return -EINVAL;
    if (fw->datasize != le32_to_cpu(hdr->size_bytes))
        return -EINVAL;

    ucode_size   = le32_to_cpu(hdr->ucode_size_bytes);
    ucode_offset = le32_to_cpu(hdr->ucode_array_offset_bytes);
    if (ucode_offset > fw->datasize ||
        ucode_size > fw->datasize - ucode_offset)   /* payload in-bounds */
        return -EINVAL;

    return 0;
}
```
This is a single-point fix that protects **all** amdgpu/radeon firmware
loaders (not just gfx_v7_0) since they all funnel through
`amdgpu_ucode_validate`. It matches the finding's recommended fix.

## Build verification of the fix

`amdgpu.ko` built successfully from the patched source on the guest
(`cc 8.3 [DragonFly]`, full module build with `-j6`):
```
=== AMDGPU_BUILD_DONE rc=0 ===
```
Full output captured to `/root/amdgpu_build.log` on the guest; key tail in
`build.log`. The fix is build-clean.

## Fix-validation status

`not_testable` — runtime trigger requires an AMD Sea Islands GPU (or an
emulator presenting a crafted firmware blob via the amdgpu firmware loader).
The guest has neither. We confirmed the fix **applies cleanly** and the
patched full `amdgpu.ko` module **compiles**; the change adds three
arithmetic guards with no behavioural side-effect on well-formed firmware
(which already satisfies `ucode_offset + ucode_size <= datasize`).
