# DF-1946 — `amdgpu_ucode_validate` missing minimum-size / bounds checks

## Verdict: REPRODUCED (harness proof) — fix builds clean (`rc=0`)

The bug is real and exactly as the finding describes. `amdgpu_ucode_validate()`
at `sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260` performs exactly one check:

```c
if (fw->datasize == le32_to_cpu(hdr->size_bytes))
    return 0;
return -EINVAL;
```

It never verifies that `fw->datasize >= sizeof(struct common_firmware_header)`
(32 bytes), nor that the header's declared payload window
`[ucode_array_offset_bytes, ucode_array_offset_bytes + ucode_size_bytes)` lies
inside `fw->data`. A 4-byte firmware with `size_bytes == datasize == 4` passes;
every caller (30+, including `gfx_v7/8/9_0.c`, `psp_v3_1/v10/v11_0.c`,
`gmc_v7/8_0.c`, `amdgpu_uvd/vce.c`) then dereferences fields past offset 4 —
`ucode_size_bytes` at offset 20, `ucode_array_offset_bytes` at offset 24 —
which are out of bounds. The OOB read of those fields then feeds the
DF-1947 OOB write.

## Mechanism (trigger → primitive → effect)

1. **Trigger.** An attacker who can plant or replace a firmware file in the
   kernel firmware search path supplies a 4-byte (or any tiny) image whose
   first 4 bytes equal the file size. `amdgpu_ucode_validate` returns 0.
2. **Primitive.** The caller casts `fw->data` to `struct
   common_firmware_header *` and reads `hdr->ucode_size_bytes` (offset 20)
   and `hdr->ucode_array_offset_bytes` (offset 24). For a 4-byte image, both
   reads are OOB and the values come from whatever kernel heap / slab
   follows the firmware buffer. The caller then issues
   `memcpy(kaddr, fw->data + arr_off, ucode_size)` with attacker-supplied
   `arr_off` and attacker-or-residue `ucode_size`.
3. **Effect.** The OOB read alone is a kernel-info-leak of at least 24 bytes
   of kernel heap. When the resulting sizes are large or attacker-crafted,
   the read feeds the DF-1947 OOB write (cross-finding amplification).

## Threat model & Phase 6 (escalation)

**HW-gated.** `amdgpu_ucode_validate` runs only on the amdgpu attach/resume
path, which requires an AMD GPU to be present. The QEMU guest has no AMD GPU
(`pciconf -lv` shows only Intel 440FX + a QEMU Standard VGA; see `dmesg.txt`),
so the kernel code path cannot be exercised at runtime on this guest. There
is therefore **no `uid=0` escalation chain** to pursue: the bug's trigger is
in dead code on this guest.

This is a **valid hard blocker** (per Phase 6: "vulnerable code path is dead /
unreachable at runtime on this guest"). The primitive is real and is fully
characterized at the harness level:

- **Primitive class:** integer/struct-bounds violation → OOB kernel-heap
  read of ≥24 bytes per call, attacker-controllable when the attacker
  controls the firmware file.
- **Realistic impact ceiling:** on a host with an AMD GPU, this is the entry
  point for the entire DF-1838/1854/1875/1894/1895/1947 family. An attacker
  who can plant a malicious firmware file (root, or any writable path in the
  firmware search dir) crosses the root→kernel boundary into heap corruption.
  This is a defense-in-depth gap: root-should-not-trivially-corrupt-kernel.

The reproduction is therefore a **source+harness proof** (the harness
replicates the validate logic verbatim and shows the malicious inputs pass),
not a kernel runtime trigger.

## PoC changes

`validate_bypass.c` is a self-contained userspace C harness that:
- Replicates `amdgpu_ucode_validate` verbatim.
- Replicates the proposed fixed version.
- Runs 5 cases: 4 malicious (4-byte, 16-byte, OOB-payload-claim,
  wrap-around-offset) and 1 well-formed baseline.

For every malicious case the vanilla function returns 0 ("PASS, caller will
deref OOB"); the fixed function returns `-EINVAL`. The harness also prints
exactly how many bytes past `fw->data` the caller's deref / memcpy would
touch.

## How to reproduce

```sh
./build.sh && ./run.sh
```

Build is `cc -O2 -Wall -Wextra -o validate_bypass validate_bypass.c`.
Expected output: 4 malicious cases each show "vanilla: PASS (rc=0) <- bug"
and "fixed: REJECT (rc=-22)"; the legitimate baseline shows both pass.

## Recommended fix

`fix.diff` is a standalone `git apply`-able unified diff against
`sys/dev/drm/amd/amdgpu/amdgpu_ucode.c`. It tightens
`amdgpu_ucode_validate` to:

1. Reject any firmware with `datasize < sizeof(struct common_firmware_header)`
   (32 bytes) — fixes CASE 1 & 2.
2. Reject if `datasize != size_bytes` (unchanged behavior).
3. Reject if `(uint64_t)ucode_array_offset_bytes + (uint64_t)ucode_size_bytes
   > size_bytes` — overflow-safe — fixes CASE 3 & 5.

This **supersedes** the finding markdown's proposal (the proposal said "add
sizeof check, header_size_bytes bounds, ucode_array_offset+ucode_size<=datasize
overflow-safe"; the implemented diff performs exactly that, in 64-bit math).

## Fix validation (Phase 8)

- Baseline (#0 unpatched, INVARIANTS ON, GENERIC): `amdgpu.ko` builds clean
  with the unfixed source.
- Applied `fix.diff` to `/usr/src`, removed `amdgpu_ucode.o`, rebuilt
  `amdgpu.ko` with `make -j6 KERNCONF=X86_64_GENERIC` from
  `/usr/src/sys/dev/drm/amd/amdgpu` → **`rc=0`**, `amdgpu.ko` rebuilt with
  the fixed `amdgpu_ucode_validate` (symbol present in `.ko`).
- Runtime kernel PoC of the fix is **not feasible** on this guest: the path
  fires only on amdgpu attach (no AMD GPU). The harness's side-by-side
  comparison of vanilla vs fixed `amdgpu_ucode_validate` IS the
  before/after evidence: vanilla returns 0 for all 4 malicious inputs;
  fixed returns `-EINVAL` for the same 4 inputs and `0` for the legitimate
  baseline.

## References

- `sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260` — the vulnerable function.
- `sys/dev/drm/amd/amdgpu/amdgpu_ucode.h:26-37` — `struct common_firmware_header`
  layout (32 bytes; `ucode_size_bytes` at offset 20, `ucode_array_offset_bytes`
  at offset 24).
- Callers (sampling): `gfx_v7_0.c`, `gfx_v8_0.c`, `gfx_v9_0.c`,
  `psp_v3_1.c`, `psp_v10_0.c`, `psp_v11_0.c`, `gmc_v7_0.c`, `gmc_v8_0.c`,
  `amdgpu_uvd.c`, `amdgpu_vce.c`, `smu7_smumgr.c`, `smu8_smumgr.c`.
- Related findings: DF-1947 (the OOB write that this OOB read feeds),
  DF-1838/1854/1875/1894/1895.
