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:357if (!dma_fence_get_rcu(fence_excl)) goto unlock;linux_reservation.c:384if (!dma_fence_get_rcu(shared[i])) break;linux_reservation.c:444if (!dma_fence_get_rcu(fence)) goto unlock_retry;linux_reservation.c:470if (!dma_fence_get_rcu(lfence)) goto unlock_retry;linux_reservation.c:511if (!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/cardNaccess (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 i915with/dev/dri/card0accessible. - 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.
Recommended fix
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_zeroalready available: kref.h:85-88.struct dma_fence.rcufield: 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)
PoC verification
Evidence pack
findings/poc/DF-1881 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_zeroindma_fence_get_rcu;kfreeβkfree_rcuindma_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
fixedVALIDATED 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)
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- n
- c
- l
- u
- d
- e
- /
- l
- i
- n
- u
- x
- /
- d
- m
- a
- -
- f
- e
- n
- c
- e
- .
- h
- :
- 9
- 7
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- n
- c
- l
- u
- d
- e
- /
- l
- i
- n
- u
- x
- /
- d
- m
- a
- -
- f
- e
- n
- c
- e
- .
- h
- :
- 1
- 0
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- n
- c
- l
- u
- d
- e
- /
- l
- i
- n
- u
- x
- /
- d
- m
- a
- -
- f
- e
- n
- c
- e
- .
- h
- :
- 1
- 0
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- l
- i
- n
- u
- x
- _
- r
- e
- s
- e
- r
- v
- a
- t
- i
- o
- n
- .
- c
- :
- 3
- 5
- 7
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).
No comments yet.