# DF-5008 Verification Verdict

**Finding:** amdgpu_vram_mgr_new() nodes[] out-of-bounds write via
non-power-of-two `mem->page_alignment`
**Severity:** High · **Impact class:** kernel heap OOB write (memcorrupt)
**Verification method:** in-kernel harness executing the verbatim driver loop
against the real in-kernel drm_mm + kvmalloc on a custom drm-enabled guest
kernel, plus a userspace arithmetic sweep.  The amdgpu *entry point* is
hardware-gated (no AMD GPU in QEMU; amdgpu binds only on real ASICs), but
every instruction of the defect — the bound computation and both insert loops
— ran unmodified in kernel space.

## Verdict: REPRODUCED (kernel-space primitive; hardware-gated entry)

### How the chain was proven

1. **Reachability of the attacker input (static, path:line-exact):**
   `AMDGPU_GEM_CREATE` is unprivileged on render nodes
   (amdgpu_kms.c:1197 `DRM_AUTH|DRM_RENDER_ALLOW`); `args->in.alignment`
   flows amdgpu_gem.c:260 → amdgpu_gem.c:58 (**no power-of-two check** —
   the only clamp is `< PAGE_SIZE`) → amdgpu_object.c:434
   (`roundup(3MB, PAGE_SIZE) >> PAGE_SHIFT` = 768, non-p2 preserved) →
   ttm_bo.c:1247 → ttm_bo.c:1099 → amdgpu_vram_mgr.c:183-185
   (`pages_per_node = max(512, 768) = 768`, non-p2) → `num_nodes =
   DIV_ROUND_UP(768, 768) = 1` → loop 1 (:201-215) inserts `nodes[0]` with
   `rounddown_pow_of_two(768) = 512` pages; `pages_left = 256` → loop 2
   (:217-235) inserts `&nodes[1]` — out of bounds.

2. **The sink really writes there (kernel, real drm_mm):** S1 — the real
   `drm_mm_insert_node_in_range()` wrote a live node at `nodes[1]` of a
   1-element `kvmalloc_array` (run.log):
   `start=768 size=256 allocated=1 color=0 mm=0xfffff8011845f668
   hole_size=1047552`.  `sizeof(struct drm_mm_node) = 184` on this build, so
   this is a 184-byte write starting at byte offset 184 of a 184-byte
   allocation.

3. **It corrupts foreign objects (kernel, groomed):** S2 — with the exact-size
   array allocated into a hole between sprayed M_DRM neighbors, the OOB node
   clobbered neighboring object #17 across 13 machine words, including kernel
   text/heap pointers (`0xfffff8008da72040`) and partial-word residue
   (`0xaa55aa5500000001`) — cross-object heap corruption in a stock kernel
   allocator zone (run.log).

4. **The arithmetic is general (userspace model):** every non-p2 alignment
   > 2 MB (i.e. exceeding default `amdgpu_vram_page_split = 512` pages) with
   `size == alignment` overflows by exactly one node; every p2 alignment is
   safe; alignments ≤ 2 MB are absorbed by the p2 default split
   (model_run.log).  Loop 2 provably runs ≤ 1 iteration (its body zeroes
   `pages_left`), so the overflow is exactly one node — 184 bytes — past the
   array, linked into the live mm.

5. **Amplitude/trigger characterization:** first allocation on a fresh VRAM mm
   succeeds deterministically (hole starts at page 0; `0 % 768 == 0`; second
   insert lands at 768..1024 — needs only ≥ 4 MB VRAM).  Repeatable per
   ioctl; content semi-controlled (start/size attacker-steerable, kernel
   pointers in the rest); the OOB node stays linked in `mgr->mm` lists until
   `amdgpu_vram_mgr_del()` removes it (a second OOB write of the same
   struct) and `kvfree`s the array.

### Why not uid=0 / panic on this guest

`amdgpu.ko` builds cleanly as a loadable module on DragonFly (confirmed:
`fix_build.log`/DRMTEST build compiled the whole driver, rc=0) but only binds
on AMD ASICs; the QEMU guest has none (std-VGA), so the ioctl→driver path
cannot be entered here.  Escalation to `uid=0` on real hardware is the
expected outcome class for a repeatable 184-byte semi-controlled kernel-heap
overflow with linked-list/rbtree kernel-pointer writes (classic slab-groom →
victim object → credential overwrite), but was **not** demonstrated — stated
explicitly rather than claimed.

### Fix validation

* `fix.diff` clamps `pages_per_node` to a power of two (rounddown), which
  restores the loop-1 ≥ pages_per_node invariant so `num_nodes` is again a
  true upper bound (loop 2 ≤ 1 node, proven).
* Behavioral: harness S3 ran the fixed loop with the real drm_mm — no write
  beyond `num_nodes` (run.log SUMMARY `S3=0`), while the unfixed loop (S1/S2)
  corrupts (`S1=1 S2=1`).
* Compile: fix applied inside the guest's `/usr/src` copy; the amdgpu module
  rebuilt with `-Werror`, rc=0 (`fix_build.log`: `amdgpu_vram_mgr.o` line 149,
  `amdgpu.ko` relink line 781).  The workspace `sys/` tree was not modified.
* Guest reverted to the clean `with-src` snapshot afterwards.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
