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

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 *fence via amdgpu_ttm.c:2139 amdgpu_job_submit),
  • dma_fence_wait(fence, false) (line 46),
  • dma_fence_put(fence) (line 49) β€” but does NOT set fence = NULL after 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 benchmark module parameter (amdgpu_drv.c:185 module_param_named(benchmark, amdgpu_benchmarking, int, 0444); default 0). It is consumed only at amdgpu_device.c:2690-2692 during 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 via loader.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_fence on the kernel slab (kmalloc-192 / dma_fence cache). 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 the fence->ops callback 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:H reflects 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=1 set at load time.
  • Reachability: amdgpu_device.c:2690 calls amdgpu_benchmark(adev, 1) β†’ amdgpu_benchmark_move() β†’ amdgpu_benchmark_do_move() with n=AMDGPU_BENCHMARK_ITERATIONS (1024) and size=1 MiB. The copy loop runs 1024 times; the 1024th fence is dma_fence_put() at amdgpu_benchmark.c:49 (refcount 1β†’0, freed), then dma_fence_put() again at amdgpu_benchmark.c:56 on the freed pointer.

Proof of Concept

PoC source: findings/poc/DF-2119/

Reproduce (deterministic, no gadgetry needed to prove the bug)

  1. On a DragonFlyBSD system with a supported AMD GPU, enable the benchmark tunable. Either add to /boot/loader.conf: amdgpu.benchmark=1 or, before the module is loaded, run as root: kenv amdgpu.benchmark=1 (sysctl node is hw.amdgpu.benchmark, mode 0444 so it is read-only at runtime β€” must be set at load.)
  2. Reboot / load the amdgpu driver.
  3. Observe: - On a DEBUG/KMSAN/SLUB_DEBUG kernel: immediate BUG: KASAN: slab-use-after-free in dma_fence_put or SLUB RED/FREE POINTER INVALID / Object already free panic in dmesg during amdgpu attach. - On a production kernel: often silent; rerun (reload module) and watch for delayed general protection fault / page fault at <addr> in dma_fence_release or fence_signal paths 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:

  1. Groom the dma_fence slab (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.
  2. The second dma_fence_put() at amdgpu_benchmark.c:56 calls kref_put β†’ fence->ops->release. If the reclaimed slot overlaps a victim whose controlled qword is interpreted as fence->ops, RIP is hijacked to an arbitrary pointer.
  3. Stack pivot into a ROP/JOP chain to commit_creds(prepare_kernel_cred(0)) and return to userland uid=0.
  4. 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.benchmark defaults to 0).
  • Blast radius: when the tunable is set, deterministic double-free β†’ kernel panic or LPE with heap grooming.

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_put in amdgpu_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 β€” benchmark module param (mode 0444).

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2119 Β· 4 files
FileTypeDescriptionSize
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
VERDICT.md file
↓ download 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

fixed
baseline reproduced→ patch + rebuild →patched clean

batch build rc=0

batch build rc=0
↓ fix.diffcombined 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