# DF-4604 — `radeon_gem_va_ioctl()` use-after-free on `bo_va` (GEM_VA vs GEM_CLOSE race)

- **File:** `sys/dev/drm/radeon/radeon_gem.c`
- **Class:** kernel heap UAF / type confusion (memcorrupt bucket)
- **Severity:** High — local unprivileged user with DRM access on Cayman-or-newer radeon
- **Status:** source-proven; runtime verification impossible on the audit guest (no radeon GPU — QEMU cannot emulate AMD GPUs)

## TL;DR

`radeon_gem_va_ioctl()` finds `bo_va` under the BO reservation, then calls
`radeon_vm_bo_set_addr()`, which **drops the reservation internally**, and only
afterwards uses `bo_va` in `radeon_gem_va_update_vm()` — with no reservation and
no `vm->mutex` protecting the first dereferences. A concurrent
`DRM_IOCTL_GEM_CLOSE` (or close(2) of a dup'd fd) on the **same file_priv**
runs `radeon_gem_object_close()` → `radeon_vm_bo_rmv()` → `kfree(bo_va)`.
Thread A then executes `radeon_gem_va_update_vm()` on the freed ~128-byte
object: `bo_va->bo`, `bo_va->vm`, `mutex_lock(&bo_va->vm->mutex)`, and
`radeon_vm_bo_update()` on freed (reclaimable) memory.

## The exact interleaving

```
Thread A: DRM_IOCTL_RADEON_GEM_VA (UNMAP)        Thread B: DRM_IOCTL_GEM_CLOSE (same fd!)
------------------------------------------------ ------------------------------------------
radeon_gem.c:665  drm_gem_object_lookup()         (gobj ref held by A — bo itself safe)
radeon_gem.c:671  radeon_bo_reserve(rbo)          → blocks here: rbo reserved by A
radeon_gem.c:677  bo_va = radeon_vm_bo_find()     |  (lifetime of bo_va is guaranteed ONLY
                  |                                |   by the BO reservation — see
                  |                                |   radeon_vm.c:117 "Object has to be
                  |                                |   reserved!")
radeon_gem.c:696  radeon_vm_bo_set_addr(0,0)      |
  radeon_vm.c:538 radeon_bo_unreserve() ------+   |
                  |                           |   +-- wakes, acquires reservation
                  |   *** WINDOW OPEN ***    +----→ radeon_gem.c:193 reserve OK
radeon_gem.c:702  radeon_gem_va_update_vm(bo_va)      radeon_gem.c:199 bo_va = find()
  radeon_gem.c:565   tv.bo = &bo_va->bo->tbo          radeon_gem.c:201 --ref_count==0
  radeon_gem.c:569   radeon_vm_get_bos(bo_va->vm,..)  radeon_gem.c:202 radeon_vm_bo_rmv()
  radeon_gem.c:573   ttm_eu_reserve_buffers()           radeon_vm.c:1127 it.start==0
  radeon_gem.c:585   mutex_lock(&bo_va->vm->mutex)      radeon_vm.c:1137 kfree(bo_va) ←FREE
  radeon_gem.c:590   if (bo_va->it.start)                 |
  radeon_gem.c:591   radeon_vm_bo_update(rdev, bo_va,..) |  ← ALL UAF
```

For the **UNMAP** variant the free is immediate (`it.start == 0` → straight
`kfree`). For the **MAP** variant, `radeon_vm_bo_rmv()` queues `bo_va` itself on
`vm->freed` and thread A's own `radeon_gem_va_update_vm()` calls
`radeon_vm_clear_freed()` (radeon_gem.c:586), which does `kfree(bo_va)`
(radeon_vm.c:1065) and *then* A dereferences it at radeon_gem.c:590-591. Both
variants UAF.

## Why the race is practically hittable

Thread B blocks *inside* `radeon_bo_reserve(rbo, true)` while A holds the
reservation during `set_addr()`. The instant `set_addr` internally unreserves,
the already-blocked B is woken and runs its entire close path (find → rmv →
kfree) while A is still returning from `set_addr` — the window is scheduler
latency wide, not instruction width. Looping `{GEM_CREATE → VA_MAP →
(UNMAP ∥ GEM_CLOSE)}` tens of thousands of times (see `radeon_va_uaf.c`) is
sufficient on real hardware.

## Preconditions

- radeon KMS hardware, `family >= CHIP_CAYMAN`, `accel_working` (gates both
  `vm_manager.enabled` at radeon_gem.c:618 and the close path at
  radeon_gem.c:188). Cayman (HD 69xx) through the last radeon-supported SI/CIK
  parts.
- An authenticated DRM fd (render node access as an unprivileged user, or any
  fd handed out by the display manager) — the ioctls are
  `DRM_AUTH|DRM_RENDER_ALLOW` (radeon_kms.c:912,918).
- Both threads on the same fd (or dup'd fds — same `file_priv`, same `vm`).

## Exploitability sketch (primary objective: uid=0)

`struct radeon_bo_va` is ~96-104 bytes → `kzalloc` 128-size class. After the
free, thread A's `radeon_gem_va_update_vm()`:
1. reads `bo_va->bo` and `bo_va->vm` from reclaimed memory,
2. passes `&bo_va->bo->tbo` into `ttm_eu_reserve_buffers()` (ww-mutex lock
   chains over attacker-influenced pointers),
3. `mutex_lock(&bo_va->vm->mutex)` on a reclaimed pointer,
4. `radeon_vm_bo_update(rdev, bo_va, ...)` walks `bo_va->vm->page_tables[]` and
   emits GPU page-table writes per the reclaimed `vm`.

Grooming the 128-byte slab with a kernel object whose contents the attacker
controls (classic: repeated small `kzalloc` user-trigerable objects) converts
step 4 into "point GPU PTEs at chosen physical pages", which on this
intentionally-unhardened target (no SMAP/SMEP/KASLR equivalents for the GPU
path) is a direct route to arbitrary physical read/write from the GPU command
processor → `uid=0`. Even without full grooming, the immediate symptom on real
hardware is a kernel panic in the reservation/mutex machinery.

## Verification on the audit guest — honest classification

- Guest: DragonFly 6.5-DEVELOPMENT x86_64, QEMU std/Bochs VGA (`chip=0x11111234`).
  **No `/dev/dri`, no radeon device, no drm in the kernel config.** QEMU has no
  AMD-GPU emulation; the ioctl table for radeon never registers.
- The trigger (`radeon_va_uaf.c`) **builds cleanly on the guest** (`build.log`)
  and **runs**, exiting `rc=3` with the documented SKIP (`run.log`).
- Therefore: `status=inconclusive`, `reproduced=0` — the bug is proven by code
  trace (every step cited), but cannot be *executed* on this guest. Runtime
  confirmation requires a physical Cayman-or-newer machine. Fix validation
  likewise `not_testable` here.

## Recommended fix

Re-look-up `bo_va` after `ttm_eu_reserve_buffers()` has re-established the BO
reservation inside `radeon_gem_va_update_vm()` (and derive `bo`/`vm` from
refcounted objects held by the ioctl). See `fix.diff`.

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