# DF-5033 VERDICT — UAF/OOB-read race in amdgpu_bo_list_get() (DF idr not RCU-safe for lookups)

**Overall status: `inconclusive` at the kernel-ioctl level (no AMD GPU on the
audit guest), `certain` at the code level — the defect is statically provable
and both bad interleavings were demonstrated deterministically on the DragonFly
guest against a byte-identical copy of the kernel algorithm.**

## Root cause (path:line)

* `amdgpu_bo_list.c:167-176` — `amdgpu_bo_list_get()` wraps a bare
  `idr_find()` in `rcu_read_lock()` only.  Called from `amdgpu_cs.c:591`
  with no `fpriv->bo_list_lock` (that mutex is taken only in
  `amdgpu_bo_list.c` itself, amdgpu_bo_list.c:157/282/304, and in
  amdgpu_kms.c init/destroy).
* `sys/libkern/linux_idr.c:516-529` — `idr_find()` takes **no lock**;
  it performs two unsynchronized memory steps: bound-check
  `idp->idr_count`, then load `idp->idr_nodes` and dereference
  `nodes[id].allocated` / `nodes[id].data`.
* `sys/libkern/linux_idr.c:421-459` — `idr_grow()` (under the token, from
  `idr_alloc()` at :358) does `idp->idr_nodes = newnodes` (:454) and
  `kfree(oldnodes, M_IDR)` (:458) — **synchronously**, no RCU deferral.
  DF's `rcu_read_lock()` cannot protect this (it is a preemption/critical
  section marker; only `call_rcu()` callbacks, deferred ≥1 s in
  sys/dev/drm/linux_rcu.c, are).  The bo-list *object* lifetime is safe
  (`amdgpu_bo_list_free()` → `call_rcu`, amdgpu_bo_list.c:58); the idr
  *array* lifetime is not.
* Trigger: per-fd idr grows past 255/511/1023… live BO lists
  (`IDR_DEFAULT_SIZE` 255, linux_idr.c:132).  An unprivileged render-node
  user creates 255+ BO lists (amdgpu_bo_list.c:276-291) in one fd while
  hammering CS ioctls referencing a list handle in the same fd.

## In-kernel consequences

1. **V1 — UAF read, preemption in window**: reader between
   `nodes` load and `nodes[id]` deref is preempted; grow frees the array;
   reader resumes → reads freed memory → returns garbage/foreign
   `amdgpu_bo_list*` → `kref_get_unless_zero()` (amdgpu_bo_list.c:170)
   atomically increments freed/garbage memory.  M_IDR-zone reuse is limited
   to other idr arrays (idr_init/idr_grow), so the value read is either
   NULL, a foreign live list pointer (balanced get/put — mostly benign
   cross-fd confusion), or — after the owning fd closed inside the window —
   a pointer to an already-kvfree'd bo list: an atomic increment **write**
   on freed memory, with a matched `amdgpu_bo_list_put()` later dropping a
   refcount that was never counted → premature/double free of a bo list and
   its embedded entries array (which pins ttm_bo references) → classic UAF
   chain material (panic → potential privesc with grooming; narrow).
2. **V2 — compiler load reorder**: `idr_find()`'s two independent loads may
   be reordered (no barriers/READ_ONCE); (new count, old nodes) tear →
   bound check passes for id beyond the old array's 255 entries → OOB read
   past the freed 4080-byte chunk → same garbage-pointer sink.

Realistic impact ceiling on real hardware: kernel panic (garbage pointer
kref_get) or, with substantial grooming, refcount corruption of freed
kernel objects.  Classification: Medium (AV:L/AC:H, requires amdgpu render
node access + 255+ lists + winning a ~2-cycle window or the reorder).

## Verification performed (this run)

1. **Guest**: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 2026, X86_64_GENERIC,
   6 vCPU, cc 8.3.  `amdgpu.ko` is present in /boot/kernel but the guest has
   no AMD GPU (vgapci 0:2:0 unattached, **/dev/dri does not exist**) → the
   ioctl path cannot be exercised end-to-end.  This caps the verdict at
   `inconclusive` for the in-kernel chain.
2. **Algorithm-level proof on the guest** (byte-identical copy of
   sys/libkern/linux_idr.c — only the USERLAND_TEST header block replaced,
   verified by diff):
   * `oracle` baseline: **V1 and V2 both deterministically dereference the
     freed idr_nodes[] array** (poison 0xDD… returned as the `data`
     pointer — the exact value the kernel would pass to
     kref_get_unless_zero).  See oracle_baseline.log, exit 1.
   * `oracle_fixed` (idr_find serialized by the token): both variants
     clean.  See oracle_fixed.log, exit 0.  This validates the fix concept
     at the algorithm level.
   * `idr_race`: 400 concurrent grow phases = 1600 grow-kfree events, 0
     spontaneous hits — expected: the unsynchronized window is ~2 cycles
     while the poison write is µs-scale, so a free can only land inside a
     reader's window if the reader is preempted exactly there (the
     real-world path) or via the reorder variant; both are what the oracle
     executes deterministically.  `idr_race_fixed`: same workload, 0
     anomalies.
3. **Fix**: `fix.diff` serializes `amdgpu_bo_list_get()` with
   `fpriv->bo_list_lock` (the mutex already guarding every other
   bo_list_handles idr operation, amdgpu_bo_list.c:157-159/282-284/304-306),
   which closes the race for this path; the systemic alternative (make DF
   `idr_find()` take the lwkt token) is validated by `oracle_fixed` but
   touches every idr user and is left to maintainers.  Kernel-build-level
   fix validation is not possible on this guest (no GPU to bind amdgpu);
   fix_status = not_testable, with the algorithm-level validation above.

## Honest limits

* No end-to-end kernel reproduction (hardware absent) — hence not
  `reproduced`.
* The concurrent harness never caught the race "naturally"; the oracle
  forces the two legal interleavings instead.  The static unsynchronized
  access is, however, indisputable from source.
* Exploitability beyond panic is assessed, not demonstrated: it requires
  the M_IDR-zone reuse dance + a preempted-in-window reader and is
  rated AC:H accordingly.

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