# DF-4510 — drm_syncobj: lockless `syncobj->fence` reference race → immediate kfree ⇒ dma_fence UAF (unpriv via render node on i915/amdgpu)

**Status: inconclusive on the audit guest — the guest has no DRM device**
(`no /dev/dri`; its only display PCI device is QEMU virtio-gpu
`chip=0x11111234`, for which this DRM port has no driver — only i915, radeon
and amdgpu exist, and only i915/amdgpu set `DRIVER_SYNCOBJ`).
The race itself is proven from the code below; every element of it was
verified line-by-line in this tree.

## The race (line-accurate)

**Reader (waiter)** — `drm_syncobj_wait_ioctl` → `drm_syncobj_array_wait_timeout`:

```
drm_syncobj.c:715:  entries[i].fence = drm_syncobj_fence_get(syncobjs[i]);
```
```c
/* include/drm/drm_syncobj.h:120-130 */
rcu_read_lock();                                   /* == preempt_disable()  */
fence = dma_fence_get_rcu_safe(&syncobj->fence);
rcu_read_unlock();

/* include/linux/dma-fence.h:189-199 — NO lock, NO retry, NO unless-zero: */
fence = *dfp;               /* (R1) load pointer                    */
if (fence)
        kref_get(&fence->refcount);   /* (R2) unconditional atomic inc */
return fence;
```

**Writer (signaller/resetter/driver CS)** — `drm_syncobj_signal_ioctl` /
`drm_syncobj_reset_ioctl` / `drm_syncobj_replace_fence` (also
`drm_syncobj_free`, and every driver export user):

```c
/* drm_syncobj.c:170-196 */
if (fence) dma_fence_get(fence);
lockmgr(&syncobj->lock, LK_EXCLUSIVE);
old_fence = syncobj->fence;            /* plain deref              */
rcu_assign_pointer(syncobj->fence, fence);  /* mfence + store      */
... run cb_list callbacks under the lock ...
lockmgr(&syncobj->lock, LK_RELEASE);
dma_fence_put(old_fence);              /* (W) OUTSIDE the lock      */
```

For a syncobj last SIGNALed (the only thing an unprivileged user needs to
arrange), `old_fence` is a `drm_syncobj_stub_fence`
(`drm_syncobj.c:199-216`) whose **single** reference is the one the syncobj
holds.  So (W) drives refcount 1→0:

```c
/* linux_fence.c:43-51 */
dma_fence_release(): fence->ops->release ? : kfree(fence);
/* drm_syncobj_stub_fence_ops (drm_syncobj.c:69-72) has NO ->release
   => kfree() fires SYNCHRONOUSLY, no grace period of any kind. */
```

On Linux this is safe because `rcu_read_lock()` is a real RCU critical
section and fence frees go through `kfree_rcu`; a reader inside the section
cannot have the object pulled from under it, and `dma_fence_get_rcu_safe`
retries.  On DragonFly both halves are stubs:

```
sys/dev/drm/include/linux/rcupdate.h:46-56:  rcu_read_lock()  == preempt_disable()
sys/dev/drm/include/linux/rcupdate.h:94:     synchronize_rcu() == <empty>
sys/dev/drm/include/linux/dma-fence.h:97-103: dma_fence_get_rcu() == unconditional kref_get
```

**Interleave:** CPU0 executes (R1) and loads `old_fence`; CPU1 executes (W),
kfree'ing it; CPU0 resumes at (R2) → `atomic_inc` on **freed memory**, then
returns the dangling pointer, which the waiter immediately dereferences
(`dma_fence_is_signaled` reads `fence->flags` and dispatches `fence->ops` at
`dma-fence.h:115-127`, and `dma_fence_add_callback` writes
`cb->node` into the freed object's `cb_list` at `linux_fence.c:310-312`).

No lock is shared between reader and writer, `preempt_disable` only prevents
preemption of CPU0 — it does not stop CPU1 — so this is a genuine cross-CPU
UAF.  The window (load→inc, a few instructions) is small but the attack loop
is free: SIGNAL allocates+frees a stub fence per call (kmalloc churn at ioctl
speed) while the waiter polls WAIT(0) at ioctl speed; run indefinitely on a
2+ core machine.

Same-site readers: `drm_syncobj_find_fence` (`drm_syncobj.c:242`), the wait
path (`:715`), `drm_syncobj_fence_get_or_add_callback` (`:118`); and all
driver-facing exports of `drm_syncobj_replace_fence` drop the old fence the
same way (amdgpu CS out-fences, i915 execbuf out-fences).

## Exploitation sketch (why this is memcorrupt, not a theoretical splat)

1. The freed object is `struct drm_syncobj_stub_fence` (dma_fence + spinlock,
   one kmalloc bucket).  Groom that bucket with attacker-controlled data
   (spray via the usual unprivileged kmalloc consumers).
2. The waiter's `kref_get` is a controlled `atomic_inc` at offset 0 of the
   reused slot (refcount corruption of whatever victim object was sprayed in).
3. If the slot still contains groomed fake-fence bytes, `dma_fence_is_signaled`
   reads fake `flags`/`ops` → `fence->ops->signaled(fence)` is an
   indirect call through a forged ops pointer; `dma_fence_add_callback`
   writes a linked-list node through fake `fence->cb_list` —
   write-what-where-list primitives.  On this audit target (no SMAP/SMEP/
   KASLR by policy) that is a direct code-execution path.
4. Even the benign-looking outcome (refcount bump on a reused victim object)
   is a classic refcount-overflow→UAF seed.

Family (same no-op-RCU class, distinct files/objects — NOT duplicates):
DF-4329 (i915_request), DF-1881 (linux_reservation), DF-4506 (linux_idr
grow), DF-4362 (fd/sync_file shims — that one is the *fd leak*, unrelated).

## Why inconclusive on this guest (not a false positive)

```
open("/dev/dri/renderD128") -> No such file or directory
open("/dev/dri/card0")      -> No such file or directory
pciconf -l: vgapci0@pci0:0:2.0 chip=0x11111234 (QEMU virtio-gpu) — no driver
  in this DRM port (port contains i915/radeon/amdgpu only); radeon lacks
  DRIVER_SYNCOBJ anyway.
```
Every syncobj ioctl is gated on `drm_core_check_feature(dev,
DRIVER_SYNCOBJ)` (`drm_syncobj.c:583` etc.) and no DRM device exists, so the
racing threads cannot even be started.  The C trigger (`syncobj_race.c`)
builds cleanly on the guest (see `build.log`) and would run unmodified on any
i915/amdgpu machine with 2+ cores; `run.log` records the device-absent
diagnosis.

## Recommended fix

Two halves must both land (see `fix.diff`):
1. `dma_fence_get_rcu`/`dma_fence_get_rcu_safe`: use
   `kref_get_unless_zero()` + retry loop (upstream semantics) so a reader can
   never resurrect a fence whose release already fired.
2. Deferred free: `dma_fence_release`'s default path and `dma_fence_free`
   must use `kfree_rcu(fence, rcu)` — the port's `__kfree_rcu`
   (`linux_rcu.c:162-184`) already implements a ≥1 s delay-line, which is a
   sufficient grace period for the tiny load→inc window.

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