# DF-4370 VERDICT — amdgpu placement walk overflow / BUG_ON panic

## Bottom line

**Certain by static proof; not reproduced at runtime because the audit VM has
no GPU (no /dev/dri, only QEMU virtio VGA).** The PoC builds and runs on the
guest and exits `NO_DRM_DEVICES` (2). On any machine with an amdgpu device
node, the first GEM_CREATE with `domains=0x7` provably panics the kernel.

## Reproduction status

| item | result |
|---|---|
| build on guest (DragonFly 6.5-DEVELOPMENT x86_64, gcc) | BUILD_OK |
| run as uid=1001 | `NO_DRM_DEVICES`, exit 2 |
| /dev/dri | does not exist on this VM |
| pciconf | only `vgapci0@pci0:0:2:0` (virtio-gpu 0x1111:1234), no AMD device |
| kldstat drm modules | none loaded |

Status: **not_reproduced** — target device absent (impact `none` *on this
guest*). Real-world impact: unprivileged **kernel panic (local DoS)** on any
system exposing an amdgpu render/card node, plus a deterministic 12-36 byte
constant-data write past `placements[3]` inside the same heap object before
the panic.

## The bug, line by line

1. `sys/dev/drm/amd/amdgpu/amdgpu_object.h:35`
   `#define AMDGPU_BO_MAX_PLACEMENTS 3` and `:81` `struct ttm_place
   placements[3];` inside `struct amdgpu_bo`.

2. `sys/dev/drm/amd/amdgpu/amdgpu_object.c:131-216`
   `amdgpu_bo_placement_from_domain(abo, domain)` appends **one placement per
   set domain bit** among VRAM/GTT/CPU/GDS/GWS/OA (up to 6), writing
   `places[c].fpfn/.lpfn/.flags` **first** and checking the bound **after**:

   ```c
   if (domain & AMDGPU_GEM_DOMAIN_OA) {
       places[c].fpfn = 0;          /* <-- write happens here */
       places[c].lpfn = 0;
       places[c].flags = TTM_PL_FLAG_UNCACHED | AMDGPU_PL_FLAG_OA;
       c++;
   }
   ...
   BUG_ON(c >= AMDGPU_BO_MAX_PLACEMENTS);   /* amdgpu_object.c:209 */
   ```

   * `c == 3` (e.g. VRAM|GTT|CPU): three **in-bounds** writes, then
     `BUG_ON(3 >= 3)` is TRUE → `panic("BUG in ...")`
     (`sys/dev/drm/include/asm/bug.h:33-39`: BUG/BUG_ON = unconditional
     `panic()` in this port).
   * `c >= 4` (any GDS/GWS/OA bit mixed with ≥1 standard domain): `places[3]`
     (and `[4]`, `[5]`) are stored **past the end of the 3-element array**
     before the BUG_ON fires.

3. `struct amdgpu_bo` layout (amdgpu_object.h:77-108), 64-bit:
   `preferred_domains`@0, `allowed_domains`@4, `placements`@8..43,
   `struct ttm_placement placement`@48 (num_placement@48, placement ptr@56,
   num_busy_placement@64, busy_placement ptr@72), `struct ttm_buffer_object
   tbo`@80. So the OOB stores for c=6 scribble:
   * places[3] (GDS) → `placement.num_placement = 0`
   * places[4] (GWS) → `placement.placement = NULL`,
     `num_busy_placement = TTM_PL_FLAG_UNCACHED|AMDGPU_PL_FLAG_GWS`
   * places[5] (OA) → `busy_placement = (flags << 32)` non-canonical garbage
   All values are constants; all land **inside the same kzalloc'd
   `struct amdgpu_bo`** (no cross-object heap corruption), and `BUG_ON`
   panics immediately after, so the write is never dereferenced. Practical
   impact = the panic; the OOB write is the memory-corruption component that
   makes this more than a logic bug (and would become directly dangerous if
   BUG() were ever compiled out).

4. Reachability (unprivileged, no capabilities needed):
   * `amdgpu_kms.c:1197` — `AMDGPU_GEM_CREATE` is registered
     `DRM_AUTH|DRM_RENDER_ALLOW` → reachable from any render node.
   * `amdgpu_gem.c:233` — domains only validated against
     `AMDGPU_GEM_DOMAIN_MASK` = 0x3f (all six bits accepted).
   * `amdgpu_gem.c:260-262` → `amdgpu_gem_object_create` →
     `amdgpu_bo_create` (amdgpu_object.c:586) → `amdgpu_bo_do_create`
     (amdgpu_object.c:419) → `amdgpu_bo_placement_from_domain(bo, bp->domain)`
     at **amdgpu_object.c:493** — before any TTM allocation, i.e. the bug
     fires on the *first* ioctl, regardless of memory pressure.
   * Size checks pass trivially: `amdgpu_bo_validate_size`
     (amdgpu_object.c:382-417) passes when GTT is in the mask and the size is
     small (for 0x3f, `size <<= PAGE_SHIFT` at :435-437 first — 4 bytes →
     16 KiB — still far below gart size).
   * Secondary path: `amdgpu_gem_op_ioctl` SET_PLACEMENT
     (amdgpu_gem.c:716-732) sets `robj->preferred_domains =
     value & (VRAM|GTT|CPU)` with **no ≤2-domain constraint**; any later
     `amdgpu_bo_validate()` (amdgpu_object.c:674-694, driven by CS validation
     `amdgpu_cs.c:393,514` or eviction) re-runs the walk on
     `bo->preferred_domains == 7` → same panic.

5. Why c==3 is enough: the guard is `>=` not `>`. c==3 writes only indices
   0..2 (legal), yet asserts. So even the *reasonable* request
   "VRAM|GTT|CPU" (a normal "anywhere" allocation) panics. VRAM|GTT (c=2) —
   the combination Mesa actually uses — is unaffected, which is why this
   survived in the wild: no mainstream client sends ≥3 domain bits.

## Exploitability toward uid=0

Not viable: the OOB write targets a fixed offset with constant data inside
the object's own allocation, and `BUG_ON` is an unconditional `panic()`
(sys/dev/drm/include/asm/bug.h:33-37) in this tree — execution never resumes
after the corruption. Ceiling = reliable local DoS (C:N/I:N/A:H).

## Fix validation

`fix.diff` (guard every store with `if (c < AMDGPU_BO_MAX_PLACEMENTS)`,
assert `c > MAX` instead of `>=`) was generated mechanically and verified
with `patch -p1 --dry-run` against a pristine copy — **APPLIES_CLEAN**
(scratch copy only; `sys/` untouched). Runtime fix validation is impossible
on this VM (no amdgpu kernel path to patch into a testable state), hence
`fix_status: not_testable`. Post-fix semantics: a 0x3f request keeps the
first three placements (VRAM, GTT, CPU) and silently drops the on-chip
pools; a 0x7 request succeeds normally instead of panicking.

## kernel_refs

* sys/dev/drm/amd/amdgpu/amdgpu_object.c:139-216 (walk + BUG_ON at :209)
* sys/dev/drm/amd/amdgpu/amdgpu_object.c:493 (call site in do_create)
* sys/dev/drm/amd/amdgpu/amdgpu_object.h:35,77-108 (MAX=3, struct layout)
* sys/dev/drm/amd/amdgpu/amdgpu_gem.c:233,260 (mask + pass-through)
* sys/dev/drm/amd/amdgpu/amdgpu_gem.c:716-732 (SET_PLACEMENT secondary path)
* sys/dev/drm/amd/amdgpu/amdgpu_kms.c:1197 (DRM_RENDER_ALLOW)
* sys/dev/drm/include/asm/bug.h:33-39 (BUG_ON = panic, unconditional)
* sys/dev/drm/include/drm/ttm/ttm_placement.h:85-89 (ttm_place, 12 bytes)

## 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.
