# DF-4927 VERDICT — radeon_sa dangling-fence UAF/double-free

## Bottom line

**The kernel-level bug is certain from source + upstream history; it could
not be exercised on the audit guest because the guest has no radeon GPU.**
The allocator logic was instead extracted verbatim into a userspace port and
run on the guest as uid=1001: the dangling-fence sequence (borrow → same-thread
kfree → ref-after-free → wait-read-after-free → double-free) reproduces
deterministically (100/100), and adding *exactly* the missing upstream
one-liner eliminates all three events (100/100). Kernel-path status is
therefore `inconclusive` (HW absent), with `confidence: certain` on the
code-level claim itself.

## Root cause (path:line)

- `sys/dev/drm/radeon/radeon_sa.c:278` — `fences[i] = sa_bo->fence;`
  borrows an uncounted pointer (only taken when the fence is *not yet*
  signaled).
- `sys/dev/drm/radeon/radeon_sa.c:270-272` — ring scan `continue`s on an
  empty flist **without clearing `fences[i]`**; the array is only reset at
  the top of the *outer* loop (`:335-338`). Upstream Linux v6.1 clears it
  at the top of every scan (the fix added by commit `0381ac3ca2e7`).
- Fence death on the same thread, still under `wq.lock`:
  `radeon_sa_bo_try_free` (`:341`) → `radeon_sa_bo_remove_locked` (`:166`)
  → `radeon_fence_unref` (`:150`) → `dma_fence_put` → `kref` 0 →
  `dma_fence_release` → `kfree` (`sys/dev/drm/linux_fence.c:43-51`,
  radeon `.release` is NULL — `radeon_fence.c:1120`); or `next_hole`'s own
  removal at `:306`. Fences on an flist normally have refcount exactly 1
  because `radeon_ib_free` drops the IB's reference immediately after
  `radeon_sa_bo_free` takes the sa_bo's (`sys/dev/drm/radeon/radeon_ib.c:95-98`).
- Consumption of the dangling pointer:
  `radeon_sa.c:352-353` (ref — UAF write), `:356` (wait reads `->seq` —
  UAF read), `:357-358` (unref → refcount 0 again → second `kfree` —
  double free).

Fence signaling is asynchronous to `wq.lock` (GPU IRQ advances
`fence_drv[ring].last_seq`, `radeon_fence.c:340-353/432-447`), so a fence
borrowed as "not signaled" in scan *k* can be removed as "signaled" in scan
*k+1* — this is precisely the scenario in the upstream commit message
(quoted in full below).

## Upstream evidence

Linux `0381ac3ca2e7` — *"drm/radeon: Initialize fences array entries in
radeon_sa_bo_next_hole"* (Xiaohui Zhang, 2022-06-07):

> The entries were only initialized once in radeon_sa_bo_new. If a fence
> wasn't signalled yet in the first radeon_sa_bo_next_hole call, but then
> got signalled before a later radeon_sa_bo_next_hole call, it could
> destroy the fence but leave its pointer in the array, resulting in
> use-after-free in radeon_sa_bo_new.

The patch adds `fences[i] = NULL;` at the top of the per-ring scan and
removes the now-redundant array reset in `radeon_sa_bo_new`. DragonFly's
copy has neither change; it *does* have the earlier 2016 fix
(`f6ff4f67cdf8`, "hold reference to fences in radeon_sa_bo_new") — the ref
loop at `radeon_sa.c:352-353` — which is what makes the 2022 bug reach a
`radeon_fence_ref()` call on the dangling pointer at all.

## Verification performed (guest, unprivileged user maxx uid=1001)

1. `build.sh`: builds `rsa_base` (DF logic as ported) and `rsa_fixed`
   (adds exactly `fences[i] = NULL;`) with the guest's base gcc 8.3 —
   `build.log`.
2. `run.sh` / `run.log`: baseline prints the kfree of F1 *inside the
   locked loop* and then UAF-REF, UAF-WAIT, DOUBLE-FREE at sa.c:352/356/358;
   exit 42. `run.2.log`: fixed build — same fence-death timeline, zero
   events, exit 0. `run.3.log`: 100× repeat each — 100/100 vs 100/100.
3. PoC port fidelity notes (`poc_changes`): locks dropped (bug is
   single-threaded), linux list.h replaced by an equivalent minimal
   implementation, `dma_fence_get/put` modeled by a refcount with an
   explicit dead flag (NULL-guard semantics of `dma_fence_get` preserved —
   that guard was initially missing from the port and caused a spurious
   NULL-deref crash that was fixed during bring-up; see attempts).

## Exploit chain (kernel, on radeon hardware — analysis, not executed)

1. Unprivileged user with `/dev/dri` render access submits continuous
   command streams on ≥2 rings (graphics + compute/copy) sized to exhaust
   the shared IB sub-allocation pool, entering the `radeon_sa_bo_new`
   blocking loop.
2. A fence borrowed by `next_hole()` completes on the GPU between two
   inner-loop scans; the same thread frees the `radeon_fence`
   (kmalloc'd `M_DRM`, ~96 bytes — `radeon_fence_emit`,
   radeon_fence.c:131) via `try_free`/`next_hole`, leaving the pointer in
   `fences[]`.
3. `radeon_fence_ref()` at sa.c:353 does `kref_get` on the freed chunk:
   +1 to the first word of free slab memory. If the chunk sits on the
   per-CPU freelist this corrupts freelist metadata; if it was reallocated
   (fence churn makes reuse likely) it corrupts an arbitrary victim's
   first field.
4. `radeon_fence_unref()` at :358 drives the refcount back to 0 →
   `kfree()` of the already-free chunk → freelist loop/corruption →
   cross-cache-style attacks on `M_DRM` allocations; with heap grooming
   (fence-sized M_DRM objects are attacker-churnable from DRI) this is a
   plausible path to controlled overlap → code execution in kernel
   context (`uid=0`). On this no-SMAP/SMEP/KASLR guest model the standard
   slab-groom → victim-object → function-pointer pivot chain applies;
   none of it could be exercised without the GPU.

## Fix

`fix.diff` ports the upstream fix verbatim (both hunks of `0381ac3ca2e7`,
lockmgr context unchanged). Validated on the port model: baseline
`reproduced=1`, patched `reproduced=0`. A kernel-level single-fix rebuild
was not performed: without a radeon device the changed code path cannot be
entered on the guest (driver binds only to radeon hardware), so a
kernel-build test would demonstrate nothing beyond compilation — the port
validation is the meaningful one and is recorded as such
(`fix_status: fixed`, scoped in `fix_verdict`).

## Status classification rationale

- Kernel path: **inconclusive** (guest lacks the hardware; not `not_reproduced`
  because it was never runnable, not `skipped` because we substituted the
  strongest available verification).
- Logic-level: reproduced, deterministic, 100/100.
- Impact field `none` = no kernel-level impact was *observed* on this
  guest; the assessed impact on radeon-equipped systems is High
  (kernel heap UAF/double-free driven by unprivileged DRI clients).

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