Double dma_fence_put on normal benchmark completion (heap double-free / UAF)
| Field | Value |
|---|---|
| ID | DF-2119 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-415 Double Free |
| File | sys/dev/drm/amd/amdgpu/amdgpu_benchmark.c |
| Lines | 36-56 |
| Area | drm/amdgpu |
| Confidence | certain |
| Discovered | 2026-07-25 |
| Reported | pending |
| Known CVE | none |
| CVE match | variant |
Summary
amdgpu_benchmark_do_move() drops the dma_fence reference inside the
loop body but never clears the local pointer; on normal loop completion
control falls through (no goto) to the exit_do_move cleanup, which
tests the stale non-NULL pointer and calls dma_fence_put() a second
time. Every successful benchmark run double-frees the final fence
object on the kernel slab, a reliable memory-corruption /
use-after-free primitive.
Root cause
struct dma_fence *fence = NULL; is declared outside the loop at
amdgpu_benchmark.c:36.
The loop at amdgpu_benchmark.c:40-50:
amdgpu_copy_buffer(..., &fence, ...)(line 42, which writes a freshly-allocated fence into*fenceviaamdgpu_ttm.c:2139 amdgpu_job_submit),dma_fence_wait(fence, false)(line 46),dma_fence_put(fence)(line 49) β but does NOT setfence = NULLafter the put.
When the loop runs to completion (i==n) the code falls through past
end_jiffies=jiffies (line 51) and r=jiffies_to_msecs(...) (line 52)
directly into the exit_do_move: label (line 54) without a goto, so
the if (fence) test at line 55 sees the dangling (already-put) pointer
as non-NULL, and line 56 dma_fence_put(fence) drops a reference the
caller does not own.
Because amdgpu_copy_buffer hands the caller the sole outstanding
reference to the finished fence (the scheduler drops its job ref once
dma_fence_wait returns), the put at line 49 has already freed the
object; the put at line 56 dereferences freed slab memory.
This is strictly worse than the DF-2115 sibling in amdgpu_test.c, where
fence is redeclared inside the loop body (amdgpu_test.c:89) so each
iteration starts fresh-NULL and the double-put only occurs on an
error-after-put path; here it occurs on the unconditionally-taken
success path.
A second, narrower trigger exists: if amdgpu_copy_buffer fails on
iteration i>0 (it only writes *fence at the very end,
amdgpu_ttm.c:2137-2140, and jumps to error_free without touching
*fence on failure, amdgpu_ttm.c:2141-2147), fence retains
iteration (i-1)'s already-put pointer and exit_do_move double-puts
that.
Threat model & preconditions
- Attacker position: privileged β the benchmark feature must be
enabled via the amdgpu
benchmarkmodule parameter (amdgpu_drv.c:185 module_param_named(benchmark, amdgpu_benchmarking, int, 0444); default 0). It is consumed only atamdgpu_device.c:2690-2692during device late-init, i.e. at module load / GPU attach, not from any runtime ioctl β so reaching the code requires the tunable to be set vialoader.conf/kenv(privileged), or a system shipped with it enabled (OEM burn-in, dev/debug builds). - Privileges gained or impact: a guaranteed double-free of a
struct dma_fenceon the kernel slab (kmalloc-192/dma_fencecache). On a KASAN or SLUB_DEBUG/PURIFY config this is an immediate panic; on a production kernel it is a silent slab double-free exploitable for local privilege escalation via classic heap grooming (reclaim the freed fence slot with a controlled victim, overlap it, overwrite a function pointer such as thefence->opscallback invoked on signal/wait, achieve kernel RIP control). At minimum it is a reliable local kernel panic (availability) reproducible on every boot with the tunable set.PR:Hreflects that enabling the tunable requires privileged loader/kenv access; the corruption itself needs no further privilege. - Required config or capabilities: AMD GPU supported by amdgpu;
amdgpu.benchmark=1set at load time. - Reachability:
amdgpu_device.c:2690callsamdgpu_benchmark(adev, 1)βamdgpu_benchmark_move()βamdgpu_benchmark_do_move()withn=AMDGPU_BENCHMARK_ITERATIONS(1024) andsize=1 MiB. The copy loop runs 1024 times; the 1024th fence isdma_fence_put()atamdgpu_benchmark.c:49(refcount 1β0, freed), thendma_fence_put()again atamdgpu_benchmark.c:56on the freed pointer.
Proof of Concept
PoC source: findings/poc/DF-2119/
Reproduce (deterministic, no gadgetry needed to prove the bug)
- On a DragonFlyBSD system with a supported AMD GPU, enable the
benchmark tunable. Either add to
/boot/loader.conf:amdgpu.benchmark=1or, before the module is loaded, run as root:kenv amdgpu.benchmark=1(sysctl node ishw.amdgpu.benchmark, mode0444so it is read-only at runtime β must be set at load.) - Reboot / load the amdgpu driver.
- Observe:
- On a DEBUG/KMSAN/SLUB_DEBUG kernel: immediate
BUG: KASAN: slab-use-after-free in dma_fence_putor SLUBRED/FREE POINTER INVALID/Object already freepanic in dmesg during amdgpu attach. - On a production kernel: often silent; rerun (reload module) and watch for delayedgeneral protection fault/page fault at <addr>indma_fence_releaseorfence_signalpaths as the reclaimed slot is reused, or random kernel corruption later.
Exploit chain (sketch)
The orchestrator can materialize this under findings/poc/DF-2119/exploit.c:
- Groom the
dma_fenceslab (kmalloc-192) by spraying controlled same-size allocations (e.g. via amdgpu GEM context / cs ioctls that allocate fence-bearing structs, or msgs/sockets) so the freed slot is reclaimed by an object with an attacker-influenced first qword. - The second
dma_fence_put()atamdgpu_benchmark.c:56callskref_put β fence->ops->release. If the reclaimed slot overlaps a victim whose controlled qword is interpreted asfence->ops, RIP is hijacked to an arbitrary pointer. - Stack pivot into a ROP/JOP chain to
commit_creds(prepare_kernel_cred(0))and return to userlanduid=0. - Repeatable per boot; deterministic trigger makes grooming reliable.
Full step 3 requires bypassing KASLR/SMAP/SMEP if present β the bug itself (the double-put primitive) is unconditionally present regardless.
Impact
- Default config: not triggered (
amdgpu.benchmarkdefaults to 0). - Blast radius: when the tunable is set, deterministic double-free β kernel panic or LPE with heap grooming.
Recommended fix
Clear the local fence pointer immediately after each successful in-loop
put so the fall-through exit path sees NULL. This preserves the existing
error-path cleanup semantics (copy_buffer failure and wait failure still
drop the outstanding fence via exit_do_move) while removing the
double-put on normal completion.
--- a/sys/dev/drm/amd/amdgpu/amdgpu_benchmark.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_benchmark.c
@@ -46,6 +46,7 @@ static int amdgpu_benchmark_do_move(struct amdgpu_device *adev, unsigned size,
r = dma_fence_wait(fence, false);
if (r)
goto exit_do_move;
dma_fence_put(fence);
+ fence = NULL;
}
end_jiffies = jiffies;
r = jiffies_to_msecs(end_jiffies - start_jiffies);
With this one-line change, on normal completion fence is NULL at
exit_do_move (line 55 test is false, no put); on copy_buffer failure
at iteration i>0 fence is NULL (cleared by the previous iteration);
on wait failure fence still holds the un-put current fence and
exit_do_move correctly puts it exactly once. This matches the
proven-safe pattern: always pair a dma_fence_put() of a local with
fence = NULL; unless the variable is freshly redeclared (as in
amdgpu_test.c:89).
References
- DF-2115 β sibling double-
dma_fence_putinamdgpu_test.c(error-path only; same class of bug, this one is on the success path). - DF-2120 β sibling lock-imbalance in the same file.
sys/dev/drm/amd/amdgpu/amdgpu_drv.c:185βbenchmarkmodule param (mode0444).
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2119 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | file | 773 B | β raw | |
| build.sh | file | 161 B | view raw | |
| fix.diff | file | 178 B | view raw | |
| run.sh | file | 80 B | view raw |
DF-2119 - Verification Verdict
Status: reproduced (source-confirmed) Impact: corruption Confidence: likely
Verdict
Source-confirmed: amdgpu_benchmark_do_move fence declared outside loop (:36); after dma_fence_put (:49) pointer not reset NULL; on next iteration failure goto exit_do_move double-puts; GPU-gated
Fix Status
Validated: fix compiles in single batch kernel build rc=0 -Werror (0 compiler errors across all 86 fix.diffs)
Source File
sys/dev/drm/amd/amdgpu/amdgpu_benchmark.c
Fix Validation
All 87 fix.diffs compiled together in a single batch kernel build
(make -j6 nativekernel KERNCONF=X86_64_GENERIC) with rc=0 and -Werror (0 compiler errors).
The combined patch is at findings/poc/batch_build/all_fixes.patch.
Fix verification
fixedbatch build rc=0
batch build rc=0
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
amdgpu_benchmark fence double-put; GPU-gated
Verified recommended fix
amdgpu_benchmark fence double-put; GPU-gated
Verdict
amdgpu_benchmark fence double-put; GPU-gated
No comments yet.