# DF-4927 — radeon_sa fence-borrow UAF / double-free (pass-2 finding)

**File:** `sys/dev/drm/radeon/radeon_sa.c`
**Class:** kernel heap use-after-free + double-free (memcorrupt), HW-gated
(radeon GPU required at kernel level)
**Upstream fix:** Linux `0381ac3ca2e7` "drm/radeon: Initialize fences array
entries in radeon_sa_bo_next_hole" (2022; amdgpu twin `6a15f3ff19a8`).
DragonFly carries the pre-2022 code: the 2016 fence-ref fix
(`f6ff4f67cdf8`) is present (the ref loop at radeon_sa.c:352-353) but the
2022 `fences[i] = NULL` initialization in `radeon_sa_bo_next_hole` is
missing.

## What the bug is

`radeon_sa_bo_next_hole()` borrows a *non-counted* pointer into the caller's
`fences[]` array (`radeon_sa.c:278`, only when the flist head fence is not
yet signaled). The array is only reset once per **outer** loop iteration
(`radeon_sa.c:335-338`); nothing clears `fences[i]` between **inner** loop
iterations. If the borrowed fence becomes signaled before a later
`next_hole()` call, the *same thread* frees it — via
`radeon_sa_bo_try_free()` (sa.c:341→166→150) or `next_hole()` itself
(sa.c:306) calling `radeon_sa_bo_remove_locked()` → `radeon_fence_unref()`
→ `dma_fence_put()` → refcount 0 → `kfree()` (`sys/dev/drm/linux_fence.c:43-51`;
fences parked on an flist normally hold exactly one reference — see
`radeon_ib_free()`, `sys/dev/drm/radeon/radeon_ib.c:95-98`). If ring *i*
then produces no fresh borrow (its flist is now empty, or its new head is
signaled with `tries[i] > 2`), the stale pointer survives until the inner
loop exits, and:

- `radeon_sa.c:352-353` `radeon_fence_ref(fences[i])` → **UAF write**
  (kref_get on freed slab memory — corrupts allocator freelist metadata
  if the chunk was not yet reused),
- `radeon_sa.c:356` `radeon_fence_wait_any()` reads `fences[i]->seq` →
  **UAF read**,
- `radeon_sa.c:357-358` `radeon_fence_unref(&fences[i])` drops the
  bogus reference → refcount hits 0 again → `dma_fence_release` →
  `kfree()` a second time → **double free**.

Trigger: any local user with DRI access on a radeon system driving enough
IB traffic that the shared `ring_tmp_bo` sub-allocator blocks
(`radeon_sa_bo_new` wait loop) with multi-ring in-flight work — i.e. GPU
workload + timing, no privileges beyond the DRI device. The window is
"fence signals between two inner-loop iterations", which recurs millions
of times under sustained load.

## What can be verified on this audit guest

The QEMU guest has **no radeon GPU**, so the kernel code path cannot be
exercised end-to-end (status `inconclusive` for the kernel path). What *is*
verified, deterministically, on the guest:

1. **`radeon_sa_uaf.c`** — a line-faithful userspace port of the five
   allocator functions (sa.c:142-374; only list plumbing, locks and the
   fence object are substituted, see file header). A "GPU simulator"
   flips the target fence to signaled on its 3rd `radeon_fence_signaled()`
   check — i.e. after it was borrowed by one `next_hole()` call and before
   the next iteration inspects it, exactly the window the upstream commit
   message describes. The model reproduces the full event chain:
   `UAF-REF` (352-353) → `UAF-WAIT` (356) → `DOUBLE-FREE` (357-358).
   100/100 runs, exit 42.
2. **Fix validation on the same port** (`-DFIXED` adds *exactly* the
   upstream one-liner `fences[i] = NULL;`): identical fence-death
   timeline, zero dangling dereferences, 100/100 runs, exit 0.
3. **Source divergence** vs. Linux v6.1 `radeon_sa.c` (fetched during the
   audit) and the upstream fix commit `0381ac3ca2e7.patch` (quoted in
   VERDICT.md).

## Reproduce

```
sh build.sh          # cc -O2 -> rsa_base, cc -O2 -DFIXED -> rsa_fixed
sh run.sh            # expects rsa_base exit 42 (UAF), rsa_fixed exit 0
```

Decisive output (baseline):

```
    [fence F1 refcount 0 -> kfree]      <- same-thread free inside the locked loop
  *** UAF-REF    fence F1 ... at radeon_sa_bo_new:352 fence_ref
  *** UAF-WAIT   fence F1 ... at radeon_sa_bo_new:356 fence_wait_any
  *** DOUBLE-FREE fence F1 ... at radeon_sa_bo_new:358 fence_unref
VERDICT: UAF/double-free REPRODUCED on the ported allocator logic  (exit 42)
```

## Files

- `radeon_sa_uaf.c` — the PoC (port + fence model + GPU simulator)
- `build.sh` / `run.sh`
- `build.log` / `run.log` (baseline) / `run.2.log` (fixed) / `run.3.log`
  (100× stress both ways) — full untrimmed guest output
- `env.txt` — guest uname, compiler, user
- `fix.diff` — the kernel fix (upstream port), validated on the port model
- `VERDICT.md`, `manifest.json`, `verdict.json`
