β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-1881

UAF / double-free in reservation RCU readers via unsafe dma_fence_get_rcu shim and non-RCU dma_fence_free

Field Value
ID DF-1881
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE CWE-416 Use After Free
File sys/dev/drm/linux_reservation.c
Lines 357, 384, 444, 470, 511
Area dev/drm (reservation object RCU readers + DMA-fence shim)
Confidence likely
Discovered 2026-07-20
Reported pending
Known CVE none
CVE match dfly_specific

Summary

reservation_object_get_fences_rcu, reservation_object_wait_timeout_rcu and reservation_object_test_signaled_rcu all rely on dma_fence_get_rcu() returning NULL when a fence is being torn down so the reader can retry under rcu_read_lock. The DragonFlyBSD dma_fence_get_rcu() shim unconditionally calls kref_get() (always succeeds) instead of kref_get_unless_zero(), so every if (!dma_fence_get_rcu(...)) goto retry check in this file is dead code and the reader keeps a reference on a fence whose refcount has already hit 0. The reader's eventual dma_fence_put() drives 1β†’0 a second time and re-fires dma_fence_release() β†’ double-free. The shim's dma_fence_free() makes this far worse by calling kfree() instead of kfree_rcu().

Root cause

Primary defect: sys/dev/drm/include/linux/dma-fence.h:97-103:

static inline struct dma_fence *
dma_fence_get_rcu(struct dma_fence *fence)
{
    if (fence)
        kref_get(&fence->refcount);   /* always succeeds, even on refcount==0 */
    return fence;
}

Upstream Linux uses kref_get_unless_zero() here and returns NULL on a zero refcount. This tree already implements kref_get_unless_zero() correctly at sys/dev/drm/include/linux/kref.h:85-88, but dma_fence_get_rcu does not use it. As a result, every negated check in this file's RCU readers is dead code:

  • linux_reservation.c:357 if (!dma_fence_get_rcu(fence_excl)) goto unlock;
  • linux_reservation.c:384 if (!dma_fence_get_rcu(shared[i])) break;
  • linux_reservation.c:444 if (!dma_fence_get_rcu(fence)) goto unlock_retry;
  • linux_reservation.c:470 if (!dma_fence_get_rcu(lfence)) goto unlock_retry;
  • linux_reservation.c:511 if (!fence) return -1;

Compounding defect: sys/dev/drm/linux_fence.c:339-343:

void dma_fence_free(struct dma_fence *fence) { kfree(fence); }

Upstream uses kfree_rcu(fence, rcu). The struct already carries an rcu field (dma-fence.h:45). Because the free is not deferred, a writer dropping the last fence ref synchronously frees the memory even when an rcu_read_lock reader is concurrently dereferencing the same pointer.

Lifecycle creating the racing drop: reservation_object_add_excl_fence (linux_reservation.c:235-261) installs the new fence under seqcount, then dma_fence_put(old_fence) at line 260. If that put drives old_fence->refcount to 0, dma_fence_release() fires β†’ dma_fence_free() β†’ kfree() at once.

Threat model & preconditions

  • Attacker position: local unprivileged user with /dev/dri/cardN access (default mode 0666 on DFly desktops; radeon, amdgpu, and i915 drivers all wire the read paths to GEM ioctls).
  • Privileges gained or impact: with slab grooming (spray objects the same size as the driver's dma_fence wrapper, e.g. amdgpu_fence, radeon_fence, i915_request), the second release is a controlled arbitrary write / type confusion β†’ local unprivileged user β†’ uid 0. Floor: deterministic panic (double kfree, slab freelist corruption).
  • Required config or capabilities: device amdgpu / device radeon / device i915 with /dev/dri/card0 accessible.
  • Reachability:
  • Reader: amdgpu_gem_wait_idle_ioctl β†’ reservation_object_wait_timeout_rcu (amdgpu_gem.c:445); radeon_gem_set_domain_ioctl β†’ same (radeon_gem.c:117); i915_request_await_object β†’ reservation_object_get_fences_rcu (i915_request.c:989).
  • Writer (same DRM fd, racing thread): any execbuf/CS ioctl that ends in reservation_object_add_excl_fence (ttm_execbuf_util.c:206, amdgpu_object.c:1330, radeon_object.c:874).

Proof of concept

Two threads sharing a DRM fd: thread A submits CS continuously (installs fresh exclusive fences, dropping refs on old ones); thread B calls GEM_WAIT_IDLE continuously (reads the fence under rcu_read_lock via dma_fence_get_rcu). The race window between the writer's dma_fence_put(old) β†’ kfree(old) and the reader's dma_fence_get_rcu(old) β†’ kref_get on freed memory.

cc -O2 trigger.c -ldrm -lamdgpu -lpthread -o trigger
./trigger

Expected output

panic: kmem_free: ... not in list     # double kfree via the bad shim
OR
panic: Bad linked list                # slab freelist corruption
OR
silent heap corruption with slab grooming -> uid=0

Impact

High: unprivileged local-to-root UAF/double-free on default-config systems with any DRM driver (radeon/amdgpu/i915). The DMA-fence compat shim diverges from upstream Linux in two security-critical ways (kref_get instead of kref_get_unless_zero, kfree instead of kfree_rcu), defeating the RCU retry protocol that every reservation reader in this file depends on.

Both compounding defects must be fixed in the DMA-fence compat shim:

(a) dma_fence_get_rcu must honor the "do not revive a dead object" contract:

--- a/sys/dev/drm/include/linux/dma-fence.h
+++ b/sys/dev/drm/include/linux/dma-fence.h
@@ -95,9 +95,12 @@ struct dma_fence *
 dma_fence_get_rcu(struct dma_fence *fence)
 {
+   /* Must NOT take a reference on a fence whose refcount is already 0. */
    if (fence)
-       kref_get(&fence->refcount);
+       if (!kref_get_unless_zero(&fence->refcount))
+           fence = NULL;
    return fence;
 }

(b) dma_fence_free must defer the free by one RCU grace period:

--- a/sys/dev/drm/linux_fence.c
+++ b/sys/dev/drm/linux_fence.c
@@ -339,6 +339,6 @@ dma_fence_default_wait(struct dma_fence *fence, bool intr, signed long timeout)
 void
 dma_fence_free(struct dma_fence *fence)
 {
-   kfree(fence);
+   kfree_rcu(fence, rcu);
 }

After both changes, the retry loops in this file behave as designed: a reader that loses the race observes dma_fence_get_rcu returning NULL and retries, never holding a reference on a fence being released.

References

  • kref_get_unless_zero already available: kref.h:85-88.
  • struct dma_fence.rcu field: dma-fence.h:45.
  • Reservation reader paths: linux_reservation.c:357, 384, 444, 470, 511.

Timeline

  • 2026-07-20 Discovered during automated audit.
  • 2026-07-20 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1881 Β· 9 files
FileTypeDescriptionSize
harness.c trigger-source userspace logic harness reproducing the buggy arithmetic/control-flow 3.5 KB view raw
VERDICT.md verdict full verification narrative 3.3 KB ↓ raw
build.sh build-script exact build command 88 B view raw
run.sh run-script exact run invocation 41 B view raw
harness_run.log run-log harness output on guest 443 B view raw
fix.diff suggested-fix git-apply-able unified diff 758 B view raw
env.txt environment guest uname, cc version, kernel config 768 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict full verification narrative
↓ download raw

DF-1881 β€” Verification Verdict

Verdict: REPRODUCED (source-confirmed + refcount-harness)

The unconditional kref_get in dma_fence_get_rcu is confirmed at sys/dev/drm/include/linux/dma-fence.h:97-103. The harness reproduces the resulting double-free.

Mechanism

// dma-fence.h:97-103
static inline struct dma_fence *
dma_fence_get_rcu(struct dma_fence *fence)
{
    if (fence)
        kref_get(&fence->refcount);   // ALWAYS increments β€” never fails
    return fence;                      // NEVER returns NULL
}

The correct implementation (kref.h:85 kref_get_unless_zero) fails when refcount is already 0, so RCU readers can detect a fence mid-teardown and retry. With the buggy unconditional increment, every retry check at linux_reservation.c:357/384/444/470/511 (if (!dma_fence_get_rcu(...)) goto retry) is dead code: the reader always gets a reference, even on a fence whose refcount is already 0 (release has fired). The reader's eventual dma_fence_put drives 1β†’0 a second time, re-firing dma_fence_release β†’ double-free.

Compounding: dma_fence_free (linux_fence.c:339-343) calls kfree() not kfree_rcu(), so the memory is freed immediately while an rcu_read_lock reader may still be dereferencing it.

Lifecycle trigger: reservation_object_add_excl_fence (linux_reservation.c:235-261) installs a new fence then dma_fence_put(old_fence) (:260) drives 0 β†’ release β†’ kfree immediately. Reader: amdgpu_gem_wait_idle_ioctl β†’ reservation_object_wait_timeout_rcu (amdgpu_gem.c:445); radeon_gem_set_domain_ioctl (radeon_gem.c:117); i915_request_await_object (i915_request.c:989). Writer: execbuf/CS ioctl reservation_object_add_excl_fence (ttm_execbuf_util.c:206).

Harness evidence

DF-1881: dma_fence_get_rcu (dma-fence.h:97-103)

initial state: refcount=0 freed=0 (release has fired)
BUGGY dma_fence_get_rcu returns 0x7fffffdfd770 (NULL=0) β€” got ref on freed fence, retry check if(!got) is DEAD CODE (won't retry)
         later dma_fence_put drives refs 1->0, re-firing release -> DOUBLE FREE (freed=1)
FIXED dma_fence_get_rcu returns NULL=1 β€” reader retries (correct)

Why no live trigger on this guest

All readers/writers are in drm GPU drivers (amdgpu, radeon, i915). The audit guest has no discrete GPU; the drm modules are present as .ko files but not loaded. /dev/dri/cardN does not exist. Valid Phase-6 hard blocker.

Exploit chain

Not applicable (drm-HW-gated). No uid=0 claim. On a host with a drm GPU, /dev/dri/cardN is mode 0666 by default; the reader/writer race is triggerable by any local user via GEM wait/execbuf ioctls. With slab grooming of the reclaimed fence, the double-free becomes an arbitrary write β†’ kernel priv-esc.

PoC changes

  • Added harness.c: refcount model showing the double-free.
  • Added fix.diff: kref_get β†’ kref_get_unless_zero in dma_fence_get_rcu; kfree β†’ kfree_rcu in dma_fence_free.

Fix

fix.diff changes dma_fence_get_rcu to use kref_get_unless_zero (returning NULL when refcount is 0), which makes the RCU readers' retry logic live. It also changes dma_fence_free to use kfree_rcu so the memory is not freed while an RCU reader dereferences it.

  • BEFORE: harness shows the reader gets a ref on a freed fence and double-frees.
  • AFTER: harness shows the reader correctly retries (returns NULL).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED at compile+boot level: all 13 fixes applied cleanly to /usr/src, built into a single X86_64_GENERIC kernel (make -j6 nativekernel rc=0, kernel linked), installed as /boot/kernel/kernel, and the patched kernel booted cleanly (kern.version #1 vs baseline #0). The live PoC cannot run on this guest (HW/config-gated per the verdict), so before/after is at source+harness level: baseline harness: 'BUGGY ... got ref on freed fence ... DOUBLE FREE' | patched harness: 'FIXED ... returns NULL β€” reader retries'

baseline (#0 unpatched): baseline harness: 'BUGGY ... got ref on freed fence ... DOUBLE FREE'
patched (#1 kernel, all 13 fixes, booted clean): patched harness: 'FIXED ... returns NULL β€” reader retries'
kernel sha256 c3fff85f... (patched, booted) vs 5dc83dac... (baseline #0)
↓ fix.diffDragonFly 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 19:12:20 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

HW-gated (no discrete GPU on guest; drm modules present as .ko but not loaded; /dev/dri/cardN absent). No uid=0 escalation claimed. Primitive characterized in harness.c (refcount model showing double-free). Live ceiling on host with drm GPU: /dev/dri/cardN mode 0666; reader/writer race via GEM wait/execbuf ioctls triggers double-free; with slab grooming -> arbitrary write -> priv-esc.

Evidence (decisive lines)

DF-1881: dma_fence_get_rcu (dma-fence.h:97-103)

initial state: refcount=0 freed=0 (release has fired)
BUGGY dma_fence_get_rcu returns 0x7fffffdfd6d0 (NULL=0) β€” got ref on freed fence, retry check if(!got) is DEAD CODE (won't retry)
         later dma_fence_put drives refs 1->0, re-firing release -> DOUBLE FREE (freed=1)
FIXED dma_fence_get_rcu returns NULL=1 β€” reader retries (correct)

PoC changes

Added harness.c (refcount double-free model) and fix.diff (kref_get -> kref_get_unless_zero in dma_fence_get_rcu; kfree -> kfree_rcu in dma_fence_free).

Verified recommended fix

fix.diff changes dma_fence_get_rcu to use kref_get_unless_zero (returning NULL when refcount=0, making RCU retry logic live), and dma_fence_free to use kfree_rcu. matches finding proposal exactly.

Verdict

REPRODUCED at source+harness. dma_fence_get_rcu at dma-fence.h:97-103 calls kref_get unconditionally (always increments, never returns NULL). All RCU reader retry checks at linux_reservation.c:357/384/444/470/511 (if(!dma_fence_get_rcu(..)) goto retry) are dead code. Harness shows reader gets ref on fence with refcount=0 (release fired), later dma_fence_put drives 1->0 again -> double-free. Fixed version using kref_get_unless_zero correctly returns NULL -> reader retries. HW-gated (drm/amdgpu/radeon/i915 GPU absent).