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, mode0444, default 0), which makesamdgpu_device_initcallamdgpu_test_movesduring GPU bring-up (amdgpu_device.c:2684-2686). - Reachability: the double-free fires if either:
(a)
amdgpu_bo_kmapfails 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):
- Build a kernel with amdgpu and
CONFIG_FAULT_INJECTION/fail_page_alloc(or use the existing kmem fault-injection sysctls). - Load amdgpu with the test flag:
kenv hw.amdgpu.testing=1in/boot/loader.conf, reboot. - Use
fail_page_allocto make the firstamdgpu_bo_kmapafter 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-failscoped to the init thread, or setfail_page_alloc.probability=100+intervaltuned so a page alloc insideamdgpu_bo_kmapfaults.
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.testingdefaults to 0). - Blast radius: when triggered by the admin, kernel double-free → panic or potential LPE with slab grooming.
Recommended fix
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], >t_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
sys/dev/drm/include/linux/dma-fence.h:106-110—dma_fence_putdoes not clear the caller's pointer.sys/dev/drm/linux_fence.c:42-51—dma_fence_releasecallskfree(fence).sys/dev/drm/amd/amdgpu/amdgpu_drv.c:192—amdgpu.testingmodule 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-2115 · 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
fixedbatch build rc=0
batch 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
No comments yet.