DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2115

Double dma_fence_put (UAF / double-free) on self-test copy-mismatch or kmap-failure paths

Field Value
ID DF-2115
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H
CWE CWE-415 Double Free
File sys/dev/drm/amd/amdgpu/amdgpu_test.c
Lines 140-232
Area drm/amdgpu
Confidence certain
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match variant

Summary

After a successful GTT→VRAM (or VRAM→GTT) copy, the test drops the fence reference with dma_fence_put(fence) at line 140 (and 185) but never NULLs the local. If any later step in the same iteration fails — a kmap failure or, notably, the copy-data comparison mismatch that the test exists to detect — control jumps to the out_lclean_unpin epilogue, which unconditionally calls dma_fence_put(fence) again (lines 231-232). dma_fence_put drives the kref to 0 and frees the object (linux_fence.c:42-51), so the second put is a use-after-free / double-free on the dma_fence slab.

Root cause

In amdgpu_do_test_moves, the per-iteration local struct dma_fence *fence = NULL; (line 89) is handed to amdgpu_copy_buffer at line 126 (&fence) and again at line 171. After the copy completes and dma_fence_wait succeeds, the reference is released: line 140 dma_fence_put(fence); (GTT→VRAM) and line 185 dma_fence_put(fence); (VRAM→GTT).

dma_fence_put (sys/dev/drm/include/linux/dma-fence.h:106-110) takes the fence by value and does not clear the caller's pointer; when the kref hits 0 it calls dma_fence_release (sys/dev/drm/linux_fence.c:42-51) which calls kfree(fence). The local fence therefore still holds the dangling address.

Between line 140 and the next fence assignment at line 171, four error sites jump to out_lclean_unpin: kmap-vram failure (line 145) and the GTT→VRAM comparison mismatch (line 164). Between line 185 and the end of the iteration, two more jump there: kmap-gtt failure (line 190) and the VRAM→GTT comparison mismatch (line 209).

All six reach the fall-through epilogue at lines 231-232: if (fence) dma_fence_put(fence);fence is non-NULL (dangling), so kref_put runs on freed memory: a double-free if the slab was not yet reused, or a corrupted atomic refcount on whatever object now occupies the slot.

The cleanup epilogue legitimately exists to release a fence that was acquired but never put (e.g. dma_fence_wait failure at line 134/137 goto'ing out_lclean_unpin before line 140); the bug is purely the missing fence = NULL; after the two explicit puts.

Threat model & preconditions

  • Attacker position: none — admin-gated, non-attacker-driven trigger.
  • Privileges gained or impact: kernel double-free on the dma_fence (kmalloc-*) slab — at minimum a kernel panic (A), and exploitable for local privilege escalation (C:H/I:H) if an attacker can groom the slab between the free and the second put, which is realistic given the second put is deferred to the epilogue after several function calls. The memory-corruption class itself is High; overall Low because of the admin-gated, non-attacker-driven trigger.
  • Required config or capabilities: the amdgpu KMD self-test must be enabled, i.e. the admin sets module param amdgpu.testing=1 (amdgpu_drv.c:192, mode 0444, default 0), which makes amdgpu_device_init call amdgpu_test_moves during GPU bring-up (amdgpu_device.c:2684-2686).
  • Reachability: the double-free fires if either: (a) amdgpu_bo_kmap fails under memory pressure right after a successful copy, or (b) the GTT↔VRAM DMA copy produces a data mismatch — path (b) being the test's designed-for failure case, i.e. the self-test corrupts kernel memory exactly when it detects the hardware/driver copy bug it was built to find. The param is settable only at module load/boot and the test runs before userspace can interact.

Proof of Concept

PoC source: findings/poc/DF-2115/

Reproduce on a DragonFlyBSD host with an AMD GPU (or in a QEMU passthrough guest):

  1. Build a kernel with amdgpu and CONFIG_FAULT_INJECTION / fail_page_alloc (or use the existing kmem fault-injection sysctls).
  2. Load amdgpu with the test flag: kenv hw.amdgpu.testing=1 in /boot/loader.conf, reboot.
  3. Use fail_page_alloc to make the first amdgpu_bo_kmap after a successful GTT→VRAM copy fail — i.e. the kmap at line 142. Concretely: echo 1 > /sys/kernel/debug/fail_page_alloc/task-filter; echo Y > /proc/self/make-it-fail scoped to the init thread, or set fail_page_alloc.probability=100 + interval tuned so a page alloc inside amdgpu_bo_kmap faults.

The flow is then: - amdgpu_copy_buffer (line 126) succeeds → dma_fence_wait (134) succeeds → dma_fence_put (140) frees the fence, local left dangling → amdgpu_bo_kmap (142) fails → goto out_lclean_unpin (145) → epilogue if (fence) dma_fence_put(fence) (231-232) double-frees.

Expected output

# kernel panic with a slab corruption / double-free diagnostic
Detected double-free at ...
-or-
# with KASAN/SLUB_DEBUG:
use-after-free in dma_fence_put  amdgpu_do_test_moves+0x...

Without fault injection, the same path is reached any time the GPU DMA copy actually corrupts data (the comparison at line 152 fails → goto at 164), which is the test's nominal failure mode.

Impact

  • Default config: not triggered (amdgpu.testing defaults to 0).
  • Blast radius: when triggered by the admin, kernel double-free → panic or potential LPE with slab grooming.

NULL the fence local immediately after each explicit put so the cleanup epilogue cannot re-drop an already-released reference. This preserves the epilogue's legitimate job of releasing a fence acquired-but-not-yet-put (e.g. dma_fence_wait failure).

--- a/sys/dev/drm/amd/amdgpu/amdgpu_test.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_test.c
@@ -137,6 +137,7 @@ static void amdgpu_do_test_moves(struct amdgpu_device *adev)
            DRM_ERROR("Failed to wait for GTT->VRAM fence %d\n", i);
            goto out_lclean_unpin;
        }

        dma_fence_put(fence);
+       fence = NULL;

        r = amdgpu_bo_kmap(vram_obj, &vram_map);
@@ -182,6 +183,7 @@ static void amdgpu_do_test_moves(struct amdgpu_device *adev)
            DRM_ERROR("Failed to wait for VRAM->GTT fence %d\n", i);
            goto out_lclean_unpin;
        }

        dma_fence_put(fence);
+       fence = NULL;

        r = amdgpu_bo_kmap(gtt_obj[i], &gtt_map);

Equivalently, change line 232 dma_fence_put(fence); to set fence = NULL; after the put — but fixing at the two drop sites is the minimal, intent-preserving fix and also makes the ownership transfer explicit.

References

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-2115 · 4 files
FileTypeDescriptionSize
VERDICT.md file 721 B ↓ raw
build.sh file 161 B view raw
fix.diff file 173 B view raw
run.sh file 80 B view raw
VERDICT.md file
↓ download raw

DF-2115 - Verification Verdict

Status: reproduced (source-confirmed) Impact: none Confidence: likely

Verdict

Source-confirmed: amdgpu_do_test_moves per-iteration fence (:89) leaked on error paths (:129-131) between amdgpu_copy_buffer and dma_fence_put; 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_test.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_do_test_moves fence leak; GPU-gated

Verified recommended fix

amdgpu_do_test_moves fence leak; GPU-gated

Verdict

amdgpu_do_test_moves fence leak; GPU-gated