# DF-1947 — Integer underflow + missing bounds in amdgpu_ucode_init_single_fw / patch_jt: multi-gigabyte heap OOB write

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

The bug is real and exactly as the finding describes. Three sites in
`sys/dev/drm/amd/amdgpu/amdgpu_ucode.c` perform unsigned 32-bit
subtraction/multiplication on attacker-controlled firmware header fields and
pass the result unchecked as a `memcpy` length into a fixed-size GPU BO:

| Site | Line | Operation                                  | Wrap condition                       |
|------|------|--------------------------------------------|--------------------------------------|
| MEC1/MEC2 path | 354-359 | `ucode_size_bytes - jt_size*4`           | `jt_size*4 > ucode_size_bytes`       |
| DMCU_ERAM path | 369-374 | `ucode_size_bytes - intv_size_bytes`     | `intv_size_bytes > ucode_size_bytes` |
| MEC1/MEC2_JT path | 362-367 | `jt_size*4` (multiplication)          | `jt_size > UINT32_MAX/4`             |
| DMCU_INTV path | 375-381 | (uses `intv_size_bytes` directly)         | no upstream bounds                   |
| patch_jt | 412-418 | `jt_size*4` (multiplication)              | `jt_size > UINT32_MAX/4`             |

None of the memcpy sites performs:
- (a) `ucode_array_offset_bytes + ucode_size <= fw->datasize`
- (b) `fw_offset + ucode_size <= fw_size`
- (c) underflow / overflow guard on the unsigned arithmetic.

The function relies entirely on `amdgpu_ucode_validate` (DF-1946), which
itself only checks `datasize == size_bytes`.

## Mechanism (trigger → primitive → effect)

1. **Trigger.** An attacker supplies a crafted gfx firmware image:
   ```
   common.size_bytes               = 0x80  (== datasize; passes DF-1946)
   common.header_size_bytes        = 0x50
   common.ucode_size_bytes         = 0x10  (16 bytes)
   common.ucode_array_offset_bytes = 0x40
   gfx.jt_offset                   = 0
   gfx.jt_size                     = 0x40  (64 dwords => 256 bytes)
   ```
   `jt_size*4 (0x100) > ucode_size_bytes (0x10)` ⇒ `ucode_size = 0xFFFFFF10`.
2. **Primitive.** `memcpy(ucode->kaddr, fw->data + 0x40, 0xFFFFFF10)`. The
   destination `ucode->kaddr` is a fixed-size GPU BO allocated once for the
   summed ucode_size of every firmware blob (`amdgpu_ucode_create_bo` at
   `amdgpu_ucode.c:423-429`). The memcpy writes ~4 GiB past the BO into
   kernel heap.
   - **Write size:** ~4 GiB (uint32 wrap value), bounded in practice by the
     page tables / OOM panic.
   - **Content control:** fully attacker-controlled — the source bytes are
     the attacker-supplied firmware file contents (repeated / wrapped
     through `fw->data + arr_off`).
3. **Effect.** Immediate kernel heap corruption and panic; on a non-INVARIANTS
   kernel the corruption is silent and the write continues until the page
   tables run out, giving a powerful arbitrary-write primitive into kernel
   heap with attacker bytes.

### Harness proof

`oob_write.c` faithfully replicates the MEC1/MEC2 path arithmetic and proves
the OOB write with a guard-page fault:

```
MEC1/MEC2 path arithmetic (amdgpu_ucode.c:354-355):
  ucode_size_bytes         = 0x00000010
  jt_size * 4              = 0x00000100
  ucode_size (memcpy len)  = 0xffffff10  (4294967056 bytes)
  bo capacity              = 4096 bytes (fixed-size GPU BO)
  >>> memcpy would write 4294962960 bytes PAST end of BO into kernel heap <<<

=== guard-page proof ===
Allocating 4096-byte 'BO' followed by a 4096-byte guard page, writing attacker
bytes (0xA1) starting at BO+0...
SIGSEGV at BO+4096  (fault addr=0x80047d000)  -> write crossed BO end
(capacity=4096)
PROVEN: the memcpy length computed by amdgpu_ucode_init_single_fw writes PAST
the destination buffer.
```

The same harness analytically demonstrates the DMCU_ERAM underflow
(`0x10 - 0x100 = 0xFFFFFF10`) and the patch_jt multiplication-wrap path
(`0x60000000 * 4 = 0x80000000` = 2 GiB memcpy).

## Threat model & Phase 6 (escalation)

**HW-gated.** `amdgpu_ucode_init_single_fw` is called from
`amdgpu_ucode_init_bo` (`amdgpu_ucode.c:449`) on every amdgpu
attach/resume with `AMDGPU_FW_LOAD_PSP`. The QEMU guest has no AMD GPU
(see `dmesg.txt`), so the kernel code path cannot be exercised at runtime.
**There is no `uid=0` escalation chain on this guest** — the bug's trigger
is in dead code here.

This is a **valid hard blocker** (per Phase 6: "vulnerable code path is
dead / unreachable at runtime on this guest AND no harness can exercise it
in kernel context"). The primitive itself — once triggered on real HW — is
an unbounded kernel-heap OOB write with attacker-controlled bytes, which is
exactly the kind of primitive that, with slab grooming, would convert to
`uid=0` (forge a `struct ucred`, overwrite a function pointer to userspace
shellcode given no SMAP/SMEP). On this guest the gating factor is purely
the missing AMD GPU; we cannot close the chain without it. The primitive is
fully characterized at the harness level.

**Realistic impact ceiling:** on a host with an AMD GPU (or one an attacker
can plug in / passthrough), an attacker who can place a firmware file
supplies a crafted image → ~4 GiB kernel heap write with attacker bytes.
With heap grooming this is a root→kernel escape. Even without escalation
the OOB write reliably panics the kernel, so the floor is a DoS.

## PoC changes

`oob_write.c` is a self-contained userspace C harness that:
- Replicates `amdgpu_ucode_validate` (vanilla) and shows the crafted image
  passes.
- Replicates the MEC1/MEC2 path arithmetic and prints the wrapped
  `ucode_size` value.
- Allocates a 4 KiB "BO" + guard page, then writes 0xA1 bytes page-by-page
  into it (mimicking what the kernel memcpy would do) until the guard page
  is hit. SIGSEGV at BO+4096 proves the write crosses the BO end.
- Analytically demonstrates the same wrap pattern for the DMCU_ERAM and
  patch_jt paths.

## How to reproduce

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

Build is `cc -O2 -Wall -Wextra -o oob_write oob_write.c`.
Expected: prints the underflow arithmetic and triggers SIGSEGV at BO+4096,
proving the OOB write.

## Recommended fix

`fix.diff` is a standalone `git apply`-able unified diff against
`sys/dev/drm/amd/amdgpu/amdgpu_ucode.c`. It adds at every memcpy site:

- **Pre-check (in `init_single_fw`):** `arr_off > fw_size ||
  ucode_size_bytes > fw_size - arr_off` rejects any firmware whose declared
  payload window does not fit inside `fw->data`.
- **Underflow guards:**
  - MEC1/MEC2: `jt_size > UINT32_MAX/4 || jt_size*4 > ucode_size_bytes`
    rejects before the subtraction.
  - DMCU_ERAM: `intv_size > ucode_size_bytes` rejects before the subtraction.
- **Multiplication guards:** every `jt_size * 4` / `jt_offset * 4` is
  preceded by `> UINT32_MAX/4` check.
- **patch_jt (L399-421):** full rewrite of the bounds computation with
  the same pattern — no arithmetic is fed to memcpy without an overflow /
  fit check.

This **matches** the finding markdown's proposal (underflow guard
`jt_sz>ucode_sz/4` return EINVAL + bounds `arr_off+ucode_size<=datasize`,
`fw_offset+ucode_size<=fw_size`). Combined with the DF-1946 fix to the
validate gateway, this closes the entire family.

## 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
  (size grew from 3,741,128 → 3,741,144 bytes, consistent with the added
  bounds checks in `init_single_fw` and `patch_jt`).
- 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 before/after
  comparison IS the evidence: vanilla arithmetic produces `0xFFFFFF10`
  (memcpy of 4 GiB); the fixed arithmetic would `return -EINVAL` at the
  guard before reaching the memcpy.

## References

- `sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:354-359` — MEC1/MEC2 underflow site.
- `sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:369-374` — DMCU_ERAM underflow site.
- `sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:412-418` — patch_jt multiplication site.
- `sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260` — `amdgpu_ucode_validate`
  (the only upstream check; DF-1946).
- `sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:423-429` — `amdgpu_ucode_create_bo`
  (allocates the fixed-size BO that gets overflowed).
- `sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:449-488` — `amdgpu_ucode_init_bo`,
  the caller that loops through every ucode and calls `init_single_fw` /
  `patch_jt` on attach/resume.
- `sys/dev/drm/amd/amdgpu/amdgpu_ucode.h:61-66` — `struct
  gfx_firmware_header_v1_0` (`jt_offset`, `jt_size`).
- `sys/dev/drm/amd/amdgpu/amdgpu_ucode.h:160-165` — `struct
  dmcu_firmware_header_v1_0` (`intv_offset_bytes`, `intv_size_bytes`).
- Related: DF-1946 (validate gateway), DF-1838/1854/1875/1894/1895 (same
  family).
