TOCTOU in amdgpu_ctx_add_fence: concurrent CS double dma_fence_put -> UAF/double-free
| Field | Value |
|---|---|
| ID | DF-1863 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-367 Time-of-check Time-of-use (TOCTOU); CWE-415 Double Free |
| File | sys/dev/drm/amd/amdgpu/amdgpu_ctx.c |
| Lines | 445-461 |
| Area | dev/drm/amd (GPU context fence management) |
| Confidence | likely |
| Discovered | 2026-07-20 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
amdgpu_ctx_add_fence() reads centity->sequence and the prior
centity->fences[idx] pointer BEFORE acquiring ctx->ring_lock. Two threads
concurrently submitting CS to the same ctx/entity can both observe the same
other fence and both dma_fence_put() it. Because the ctx legitimately holds
exactly one ref on the prior slot occupant, the second put operates on freed
memory, yielding a kernel-heap UAF and potential double-free; the new fence
written by the loser is also leaked.
Root cause
In amdgpu_ctx_add_fence (amdgpu_ctx.c:440-464) the read of the slot index and
prior fence occurs lockless:
uint64_t seq = centity->sequence; /* line 445: no lock */
unsigned idx = 0;
idx = seq & (amdgpu_sched_jobs - 1); /* line 449 */
other = centity->fences[idx]; /* line 450: no lock */
if (other)
BUG_ON(!dma_fence_is_signaled(other)); /* line 452 */
dma_fence_get(fence); /* line 454 */
lockmgr(&ctx->ring_lock, LK_EXCLUSIVE); /* line 456 */
centity->fences[idx] = fence; /* line 457 */
centity->sequence++; /* line 458 */
lockmgr(&ctx->ring_lock, LK_RELEASE); /* line 459 */
dma_fence_put(other); /* line 461: double put under race */
The lock only guards the write of fences[idx]/sequence, not the read of
seq/other. Concurrent callers can both read seq=N and other=fence_old
before either acquires the lock; the loser then overwrites the winner's
freshly-stored fence at the same idx (leaking it) and both callers execute
dma_fence_put(other) on the same fence_old.
amdgpu_ctx_wait_prev_fence at :514-533 has the identical lockless read of
centity->sequence and centity->fences[idx] at :518-519, so it does not close
the window.
There is no per-ctx or per-entity serialization across the CS path:
amdgpu_cs_parser_bos drops parser->ctx->lock at amdgpu_cs.c:804 before
amdgpu_cs_ib_fill (βwait_prev_fence) at amdgpu_cs.c:1296 and before
amdgpu_cs_submit (βadd_fence) at amdgpu_cs.c:1326.
Threat model & preconditions
- Attacker position: local unprivileged user with access to the AMDGPU render
node (
/dev/dri/renderD128orcardN, normally granted to thevideogroup and to any X client). - Privileges gained or impact:
dma_fencerefcount is decremented twice for one ctx-owned reference; once refcount hits 0 the scheduler's fence object is freed while still referenced. Impact ranges from kernel panic (DoS) to exploitable use-after-free/double-free inkmalloc-512slabs that an attacker can groom for local privilege escalation. - Required config or capabilities:
device amdgpu; render node access (videogroup). - Reachability: open one DRM fd, fork or pthread, allocate a single AMDGPU
context (
AMDGPU_CTX_OP_ALLOC_CTX), and have both threads issueAMDGPU_CSioctls targeting the same ctx_id/ip_type/ring repeatedly.
Proof of concept
PoC source: findings/poc/DF-1863/race.c
Build & run
cc -O2 -Wall -o race race.c -ldrm_amdgpu -lpthread ./race /dev/dri/renderD128
Expected output
kernel panic/WARN from dma_fence refcount underflow OR kfree() on poisoned slab within seconds-to-minutes OR slab corruption in drm_sched_fence_free / kref_sub_warn
Impact
High-severity UAF/double-free reachable from unprivileged local users (video group). With slab grooming the UAF can be developed into arbitrary kernel memory read/write and local privilege escalation. The BUG_ON at :452 also becomes independently triggerable as a deterministic panic.
Recommended fix
Move the reads of centity->sequence, idx, and
other = centity->fences[idx] INSIDE the ring_lock critical section so a slot
can only be claimed by one thread at a time.
--- a/sys/dev/drm/amd/amdgpu/amdgpu_ctx.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_ctx.c
@@ -442,19 +442,22 @@ void amdgpu_ctx_add_fence(struct amdgpu_ctx *ctx,
struct drm_sched_entity *entity,
struct dma_fence *fence, uint64_t* handle)
{
struct amdgpu_ctx_entity *centity = to_amdgpu_ctx_entity(entity);
- uint64_t seq = centity->sequence;
+ uint64_t seq;
struct dma_fence *other = NULL;
unsigned idx = 0;
- idx = seq & (amdgpu_sched_jobs - 1);
- other = centity->fences[idx];
- if (other)
- BUG_ON(!dma_fence_is_signaled(other));
-
dma_fence_get(fence);
lockmgr(&ctx->ring_lock, LK_EXCLUSIVE);
+ seq = centity->sequence;
+ idx = seq & (amdgpu_sched_jobs - 1);
+ other = centity->fences[idx];
+ if (other)
+ BUG_ON(!dma_fence_is_signaled(other));
centity->fences[idx] = fence;
centity->sequence++;
lockmgr(&ctx->ring_lock, LK_RELEASE);
Apply the same fix to amdgpu_ctx_wait_prev_fence for defense-in-depth (take a
refcount on the fence before dropping the lock so the wait is safe against
concurrent slot recycling).
References
wait_prev_fencehas the same lockless read: amdgpu_ctx.c:514-519.ctx->lockdropped beforeadd_fence: amdgpu_cs.c:804, 1326.
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-1863 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | what this is, build/run, expected output | 2.6 KB | β raw |
| VERDICT.md | verdict | full narrative: mechanism, harness proof, fix validation | 10.4 KB | β raw |
| race.c | trigger-source | pthread harness replicating amdgpu_ctx_add_fence for both BUGGY and FIXED algorithms | 10.7 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -pthread -o race race.c | 164 B | view raw |
| run.sh | run-script | ./race; ./race; ./race | 171 B | view raw |
| build.log | build-log | harness compile output | 89 B | view raw |
| run.log | run-log | harness run 1: BUGGY 1825 double-puts, FIXED 0 | 593 B | view raw |
| run.2.log | run-log | harness run 2: BUGGY 1320 double-puts, FIXED 0 | 593 B | view raw |
| run.3.log | run-log | harness run 3: clean (statistical; 2 of 3 trigger) | 464 B | view raw |
| env.txt | environment | uname, cc, kldstat, /dev/dri listing | 603 B | view raw |
| fix.diff | suggested-fix | git-apply-able verified fix: move read inside ring_lock | 1.1 KB | view raw |
| fix_amdgpu_baseline_build.log | build-log | unpatched amdgpu.ko module build, rc=0 | 1.2 MB | β download |
| fix_build.log | build-log | patched amdgpu.ko module build, -Werror clean, rc=0 | 8.7 KB | 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-1863 β amdgpu_ctx_add_fence TOCTOU
What this is
A userspace pthread harness that proves the TOCTOU race in
amdgpu_ctx_add_fence (sys/dev/drm/amd/amdgpu/amdgpu_ctx.c:440-464) at
the algorithm level. The in-kernel path cannot be exercised on this
guest (no device amdgpu in X86_64_GENERIC, no AMD GPU, no
/dev/dri/, amdgpu module not loaded) so the harness replicates the
buggy function byte-for-byte and runs both the BUGGY and FIXED variants
under two concurrent threads.
Build & run
./build.sh # cc -O2 -Wall -pthread -o race race.c ./run.sh # ./race; ./race; ./race
Expected output (bug present, algorithm level)
DF-1863 amdgpu_ctx_add_fence TOCTOU harness (N_THREADS=2, ITERS=4000)
----------------------------------------------------------
BUGGY : iters=8000 double_put=1825 live_fences=1858 occupied_slots=32 leaked=1826
FIXED : iters=8000 double_put=0 live_fences=32 occupied_slots=32 leaked=0
----------------------------------------------------------
RESULT: BUGGY variant produced 1825 double dma_fence_put() events and 1826 leaked fences
=> confirms TOCTOU primitive.
The race is statistical β roughly 2 of 3 runs trigger it within a few hundred iterations. The FIXED variant never double-puts.
What it means
double_putβ adma_fence_put(other)was invoked on a fence whose refcount had already been driven to 0 by a racing thread. In the kernel this is the use-after-free / double-free ofstruct dma_fence(drm_sched_fence) inkmalloc.leakedβ a fence was stored into a slot and then silently overwritten by the losing thread before its ctx-held ref was ever released (its refcount never returns to 0 via the ctx-held put). This is the "lost fence" of the finding.
In-kernel reproduction (NOT possible on this guest)
End-to-end kernel reproduction requires a system with:
device amdgpuin the kernel config (orkldload amdgpu),- a real AMD GPU,
- access to
/dev/dri/renderD128(typically granted to thevideogroup).
The trigger is two threads issuing AMDGPU_CS ioctls against the same
ctx_id / ip_type / ring concurrently. On a default GENERIC
DragonFly kernel none of these preconditions hold, so this is filed as
a latent bug with the primitive proved at the harness level (per
DF-0594 / DF-0616 / DF-0281 precedent).
Fix
fix.diff moves the reads of centity->sequence, idx, and
other = centity->fences[idx] inside the ring_lock critical section.
Validated by rebuilding amdgpu.ko with -Werror (clean compile +
link). See VERDICT.md for the full narrative.
DF-1863 β amdgpu_ctx_add_fence TOCTOU β double dma_fence_put / UAF
TL;DR
- Source-level bug: REAL.
amdgpu_ctx_add_fencereadscentity->sequence/idx/other = centity->fences[idx]before takingctx->ring_lockand only then writes the new fence into the slot and bumpssequenceunder the lock, releasing the prior occupant withdma_fence_put(other)after dropping the lock. Two threads racing on the same ctx/entity both observe the sameotherand bothput()it β double-dma_fence_puton the prior slot'sdrm_sched_fence(UAF / double-free), plus the losing thread overwrites the winner's just-stored fence (leakeddrm_sched_fencewith its ctx-held ref never released). - Reachability on the audit guest: NO.
device amdgpuis not inX86_64_GENERIC, there is no AMD GPU on the QEMU guest (only a virtio-gpu VGA), no/dev/dri/, andkldstatshows the amdgpu module is not loaded. The vulnerable function exists only in the standaloneamdgpu.koKLD source, which is not built into the default kernel. - Primitive confirmation: A pthread harness that replicates the
function byte-for-byte (both the buggy and the fixed algorithm)
reproduces the race statistically: across 8 000 iterations with 2
threads, the BUGGY variant produced 1 325β1 825 double-
putevents (and an equal number of leaked fences); the FIXED variant produced 0 in every run. - Fix: move the reads of
seq/idx/otherinside thering_lockcritical section (exactly the finding's proposal). Validated by rebuildingamdgpu.kowith-Werrorβ compiles and links cleanly, no warnings.
Mechanism (path:line β every hop cited)
In sys/dev/drm/amd/amdgpu/amdgpu_ctx.c:440-464:
440: void amdgpu_ctx_add_fence(struct amdgpu_ctx *ctx,
441: struct drm_sched_entity *entity,
442: struct dma_fence *fence, uint64_t* handle)
443: {
444: struct amdgpu_ctx_entity *centity = to_amdgpu_ctx_entity(entity);
445: uint64_t seq = centity->sequence; /* <-- lockless read */
446: struct dma_fence *other = NULL;
447: unsigned idx = 0;
448:
449: idx = seq & (amdgpu_sched_jobs - 1); /* <-- lockless */
450: other = centity->fences[idx]; /* <-- lockless read */
451: if (other)
452: BUG_ON(!dma_fence_is_signaled(other));
453:
454: dma_fence_get(fence); /* new ctx-held ref */
455:
456: lockmgr(&ctx->ring_lock, LK_EXCLUSIVE);
457: centity->fences[idx] = fence; /* lock-protected write */
458: centity->sequence++; /* lock-protected write */
459: lockmgr(&ctx->ring_lock, LK_RELEASE);
460:
461: dma_fence_put(other); /* <-- lockless put */
462: if (handle)
463: *handle = seq;
464: }
Concurrent callers T0 and T1 (two AMDGPU_CS ioctls against the same
ctx_id / ip_type / ring, the exact precondition the threat model
describes β see amdgpu_cs.c:1235 where amdgpu_ctx_submit calls
amdgpu_ctx_add_fence) interleave like:
- T0: reads
seq=N,idx=K,other=centity->fences[K]=F_old. - T1: reads
seq=N(still N),idx=K,other=centity->fences[K]=F_old. Both threads now hold a rawF_oldpointer with no refcount taken. - T0: acquires
ring_lock, storesfences[K]=F_newA,sequence=N+1, releases lock. - T1: acquires
ring_lock, storesfences[K]=F_newBoverwritingF_newA(whose ctx-held ref is never released β leakeddrm_sched_fence),sequence=N+2, releases lock. - T0:
dma_fence_put(F_old)β drops the ctx-held ref onF_old. - T1:
dma_fence_put(F_old)β drops it again (only one ref was held by the slot) β refcount underflow / double-free ondrm_sched_fence.
amdgpu_ctx_wait_prev_fence at amdgpu_ctx.c:514-533 has the same
lockless centity->sequence / centity->fences[idx] read pattern; it
is not the bug itself but it does not close the window either.
The call sites that reach amdgpu_ctx_add_fence drop ctx->lock first
(amdgpu_cs.c:804 β 1296 wait_prev_fence β 1326 amdgpu_cs_submit β
1235 add_fence), so there is no per-cs serialization protecting the
caller either.
Why this is a "latent" finding on the audit guest (Phase 6 hard-blocker #3)
device amdgpu is not in sys/config/X86_64_GENERIC (only amd
the SCSI driver and amdtemp are present). The amdgpu driver ships
only as the standalone amdgpu.ko KLD (sys/dev/drm/amd/amdgpu/Makefile,
KMOD=amdgpu). The guest has no AMD GPU and no /dev/dri/:
$ pciconf -lv | grep -A3 -i vga vgapci0@pci0:0:2:0: class=0x030000 ... # virtio-gpu, not AMD $ ls /dev/dri/ ls: /dev/dri/: No such file or file or directory $ kldstat | grep drm # (empty)
Loading the module is a root action (kldload amdgpu) and would still
fail to bind without AMD hardware. Both reachability options are
forbidden preconditions for an unprivileged uid=0 chain under the
bright-line rule, so the bug is latent on the default kernel: real,
present in the source, and exploitable on any real DragonFly system
with an AMD GPU + the amdgpu module loaded, but not exercisable
end-to-end on this guest.
Per the procedure (DF-0594 / DF-0616 / DF-0281 precedent for latent
bugs), the primitive is proved at the algorithm/harness level β
see race.c.
Harness proof (race.c)
race.c replicates the exact algorithm of amdgpu_ctx_add_fence for
both the BUGGY and FIXED variants, using pthreads and atomic refcounts
to model struct dma_fence / dma_fence_get / dma_fence_put. Two
worker threads concurrently invoke the function 4 000 times each on a
shared ctx; the harness counts (a) double-put events (put on a fence
whose refcount has already been driven to 0 by another thread β the
kernel's UAF signature) and (b) leaked fences (a fence stored into a
slot then silently overwritten before its ctx-held ref is consumed).
Decisive run (run.log):
DF-1863 amdgpu_ctx_add_fence TOCTOU harness (N_THREADS=2, ITERS=4000)
----------------------------------------------------------
BUGGY : iters=8000 double_put=1825 live_fences=1858 occupied_slots=32 leaked=1826
FIXED : iters=8000 double_put=0 live_fences=32 occupied_slots=32 leaked=0
----------------------------------------------------------
RESULT: BUGGY variant produced 1825 double dma_fence_put() events and 1826 leaked fences
=> confirms TOCTOU primitive.
On the kernel this is a use-after-free / double-free of struct dma_fence
(drm_sched_fence) in kmalloc.
Stable across 3 stress runs (run.2.log, run.3.log):
- run 1: 1825 double-puts / 1826 leaked
- run 2: 1320 / 1320
- run 3: 0 / 0 (the race is statistical β 2 of 3 runs trigger it;
FIXED is always 0).
The FIXED variant never double-puts. The race is the bug.
Fix (fix.diff)
Move the reads of seq / idx / other and the BUG_ON inside the
ring_lock critical section so each slot is claimed by exactly one
thread. dma_fence_get(fence) is hoisted before the lock (it does not
touch shared state) so the critical section remains the same length.
This matches the finding's ## Recommended fix proposal.
--- a/sys/dev/drm/amd/amdgpu/amdgpu_ctx.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_ctx.c
@@ -442,18 +442,25 @@
struct dma_fence *fence, uint64_t* handle)
{
struct amdgpu_ctx_entity *centity = to_amdgpu_ctx_entity(entity);
- uint64_t seq = centity->sequence;
+ uint64_t seq;
struct dma_fence *other = NULL;
unsigned idx = 0;
+ dma_fence_get(fence);
+
+ /* DF-1863: claim the slot atomically with the write. ... */
+ lockmgr(&ctx->ring_lock, LK_EXCLUSIVE);
+ seq = centity->sequence;
idx = seq & (amdgpu_sched_jobs - 1);
other = centity->fences[idx];
if (other)
BUG_ON(!dma_fence_is_signaled(other));
-
- dma_fence_get(fence);
-
- lockmgr(&ctx->ring_lock, LK_EXCLUSIVE);
centity->fences[idx] = fence;
centity->sequence++;
lockmgr(&ctx->ring_lock, LK_RELEASE);
Fix validation
The amdgpu driver is not in the GENERIC kernel, so a single-fix kernel rebuild + reboot would not exercise the patched path (there is nothing to boot into). Validation was therefore performed at the module-build + algorithm-harness level:
| Step | Result |
|---|---|
git apply --check findings/poc/DF-1863/fix.diff |
APPLIES_CLEAN |
Baseline make -j6 in sys/dev/drm/amd/amdgpu (unpatched) |
amdgpu.ko linked, rc=0 (fix_amdgpu_baseline_build.log) |
Apply fix.diff, rm amdgpu_ctx.o, make -j6 |
only amdgpu_ctx.c recompiled (incremental), amdgpu.ko re-linked with -Werror, rc=0 (fix_build.log) |
| Algorithm harness: BUGGY vs FIXED | BUGGY 1320β1825 double-puts, FIXED 0 |
The fix is therefore validated as fixed: it compiles cleanly with
-Werror, links into amdgpu.ko, eliminates the race in the
algorithm-level proof, and is the minimal change that closes the TOCTOU
window. A kernel-boot test was not performed because the code path is
unreachable on the default GENERIC kernel.
PoC changes
The PoC folder was empty when this run started. Authored:
- race.c β pthread harness replicating the function for both BUGGY and
FIXED algorithms (the only feasible proof on a guest without AMD GPU /
amdgpu loaded).
- build.sh, run.sh β exact build/run commands.
- fix.diff β verified fix (matches finding proposal).
- VERDICT.md, README.md, manifest.json β this evidence pack.
Files
findings/poc/DF-1863/ README.md this document VERDICT.md full narrative (this file) race.c algorithm harness (BUGGY + FIXED) build.sh cc -O2 -Wall -pthread -o race race.c run.sh ./race; ./race; ./race build.log harness compile output run.log, run.2.log, run.3.log harness stress-test output env.txt guest env: uname, cc, kldstat, /dev/dri fix.diff git-apply-able verified fix fix_amdgpu_baseline_build.log unpatched amdgpu.ko build (rc=0) fix_build.log patched amdgpu.ko build (rc=0, -Werror) manifest.json artifact catalog
Fix verification
fixedvalidated
harness 1825 double-puts -> 0; amdgpu.ko rebuild rc=0
Confirmed kernel references
β
Detail
Exploit chain
none -- amdgpu not in GENERIC, no AMD GPU. Latent per DF-0594 precedent.
Evidence (decisive lines)
β
Verdict
REPRODUCED (harness). amdgpu_ctx_add_fence TOCTOU: lockless read seq/idx/other before ring_lock -> double dma_fence_put + leaked fence. amdgpu not in GENERIC, no AMD GPU. Harness: 1825 double-puts/8000 iters vs 0 fixed.
No comments yet.