drm_prime fd_to_handle: port's dma_buf_get() drops its file reference before returning, making drm_prime.c's trailing dma_buf_put() an unbalanced fdrop β refcount underflow, premature free of dma_buf+GEM object while the prime fd and rb-tree handle are still live, and an unreference'd dma_buf* used across the whole ioctl (racing-close UAF)
Summary
dma_buf_get is the fget contract: return referenced; the DF shim calls dropfp() before returning. The already-imported fast path then performs one unbalanced fdrop per FD_TO_HANDLE ioctl - repeated ioctls drive f_count to 0 and run dmabuf_close -> release + kfree while the fd is still installed and the GEM handle still points at the freed objects; every path also uses the unreferenced pointer across a close race. VERIFIED on guest via KLD driving the exact shipped drm.ko symbols: f_count 2->1->0, release fires with fd installed, UAF touch on next get, exit panics 'fdrop: invalid f_count -1'. Unpriv video-group ioctls on any GPU; spray -> hijack release fn ptr -> uid=0 analyzed. Keep-the-hold one-line fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4515 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.0 KB | β raw | |
| VERDICT.md | β | 5.2 KB | β raw | |
| df4515mod.c | β | 7.0 KB | view raw | |
| Makefile | β | 231 B | β download | |
| build.sh | β | 127 B | view raw | |
| run.sh | β | 178 B | view raw | |
| run.log | β | 1.3 KB | view raw | |
| panic.txt | β | 488 B | view raw | |
| fix_run.log | β | 885 B | view raw | |
| fix.diff | β | 689 B | view raw | |
| env.txt | β | 611 B | view raw | |
| verdict.json | β | 4.4 KB | view raw |
DF-4515 β drm_prime PRIME fd_to_handle unbalanced dma_buf_put β file refcount underflow β UAF
What
drm_gem_prime_fd_to_handle() (sys/dev/drm/drm_prime.c:792) calls dma_buf_get(prime_fd)
at drm_prime.c:802 and executes a trailing dma_buf_put(dma_buf) at drm_prime.c:848/857/864
β the exact pairing of upstream Linux, where dma_buf_get() is fget() and returns the
dma_buf with a file reference held. On this port the shim
dma_buf_get() (sys/dev/drm/linux_dma-buf.c:168-187) calls dropfp() before returning,
so the pointer handed to drm_prime.c is unreferenced and the trailing dma_buf_put() is an
unbalanced fdrop.
Two consequences, both proven on the QEMU guest against the shipped drm.ko:
- Every DRM_IOCTL_PRIME_FD_TO_HANDLE that hits the already-imported fast path
(drm_prime.c:811-815 β out_put at :862-865) decrements the file refcount with no
matching increment. A handful of repeated ioctls drive
f_countto 0 while the prime fd is still installed and the GEM handle / rb-tree member still exist:dmabuf_close()fires (releaseβ GEM object put +kfree(dmabuf)), and the next touch of the installed fd is a use-after-free. - Even on the first import, the returned
dma_bufis unprotected: a racingclose(prime_fd)from another thread frees it mid-ioctl (classic TOCTOU UAF across drm_prime.c:811-866 and drm_gem_prime_import_dev drm_prime.c:718-761).
The guest has no GPU (/dev/dri absent), so the unprivileged-ioctl entry could not be
exercised directly. The PoC therefore drives the exact call sequence of
drm_prime.c:802/811-815/864 (and the export side :599-696) through the real, shipped
drm.ko symbols via a KLD, which reproduces the whole failure chain deterministically:
underflow β premature free β use-after-free β kernel panic.
Files
df4515mod.c+Makefileβ KLD;sysctl hw.df4515mod.fire=5runs the full sequence (export β dma_buf_fd β 3Γ F2H lookup-hit cycle β UAF touch)build.sh/run.shβ exact guest commandsrun.log,panic.txtβ baseline reproduction (panic:fdrop: invalid f_count -1)fix_run.logβ same PoC after applyingfix.diff(f_count stable at 2, no panic)fix.diffβ one-line fix: keep theholdfp()reference indma_buf_get()VERDICT.md,manifest.json,verdict.json
Reproduce (as root in the guest)
cd /usr/src/sys/dev/drm/drm && make obj && make # builds drm.ko (~4 min) cd /root/df4515 && make # builds df4515mod.ko kldload /usr/obj/usr/src/sys/dev/drm/drm/drm.ko kldload df4515mod.ko sysctl hw.df4515mod.fire=5 # expect: f_count 2β1β0, "release fired", then PANIC
Expected decisive output (see run.log / panic.txt):
df4515mod: [5] F2H lookup-hit cycle #3 done; f_count now 0 <== UNDERFLOW: file freed, fd still installed df4515mod: [5] survived UAF touch (dma_buf_get -> 0) panic: fdrop: invalid f_count -1 fdrop() <- closef() <- fdfree() <- exit1() <- sys_exit()
DF-4515 VERDICT β REPRODUCED (primitive: refcount underflow β premature free β UAF β panic)
Bottom line
The DragonFly dma-buf port's dma_buf_get() (sys/dev/drm/linux_dma-buf.c:168-187) drops its
only file reference (dropfp) before returning, violating the Linux fget() contract that
drm_gem_prime_fd_to_handle() (sys/dev/drm/drm_prime.c:792-866) is written against. Every
PRIME FD_TO_HANDLE ioctl therefore executes one unbalanced dma_buf_put() β fdrop()
(drm_prime.c:864 on the already-imported fast path; :848/:857 on the import paths), and the
dma_buf* pointer is used reference-free across the whole ioctl (racing-close UAF).
How it was proven
Guest: DragonFly 6.5-DEVELOPMENT x86_64, X86_64_GENERIC kernel (2026-07-02 build), no GPU
(/dev/dri absent β DRM is a module and no drm driver can attach in QEMU). To exercise the
shipped production code anyway, drm.ko was built from the guest's /usr/src
(sys/dev/drm/drm/Makefile) and loaded; a KLD (df4515mod.ko) then executes byte-for-byte
the drm_prime.c sequence through the real exported symbols:
dma_buf_export()+dma_buf_fd()β whatdrm_gem_prime_handle_to_fddoes (drm_prime.c:599-696, f_count 1 β 2: falloc + fsetfd, both confirmed live)- 3 Γ
dma_buf_get(); dma_buf_put();β the F2H lookup-hit path (drm_prime.c:802 + :811-815 β :864, no reference taken in between β verified by reading every line of the path)
Observed (findings/poc/DF-4515/run.log, panic.txt):
[5] H2F: dmabuf=0xfffff8004f145530 fp=0xfffff801167ece00 fd=3 f_count=2
[5] F2H lookup-hit cycle #1 done; f_count now 2
[5] F2H lookup-hit cycle #2 done; f_count now 1
dma_buf_ops.release fired (dmabuf=0xfffff8004f145530) <- dmabuf kfree'd at 1->0
[5] F2H lookup-hit cycle #3 done; f_count now 0 <== UNDERFLOW: file freed, fd still installed
[5] touching freed file via installed fd:
[5] survived UAF touch (dma_buf_get -> 0) <- use-after-free READ+WRITE
panic: fdrop: invalid f_count -1 <- UAF at process exit
fdrop() <- fdrop() <- closef() <- fdfree() <- exit1() <- sys_exit()
The release callback firing while fd 3 is still installed is the premature free; the
surviving dma_buf_get on that fd is the UAF; the exit-path panic
(fdrop: invalid f_count -1) is the freed-file refcount being decremented again.
Deterministic, reproduced first try (attempts: 1 for the working sequence; 2 build
iterations before it).
Why it is a real security bug (not just a demo)
- Reachability (source-certain): DRM_IOCTL_PRIME_FD_TO_HANDLE is wired by radeon
(radeon_drv.c:697), amdgpu (amdgpu_drv.c:1326) and i915 (i915_drv.c:3296); ioctls on
/dev/dri/cardN are reachable by any user with the device open (typically the
videogroup). The unbalanced-put path needs a single GPU: export once (H2F), import (F2H), then repeat F2H on the same fd β each repeat drops one f_count. On real hardware the initial refs are falloc + obj->dma_buf + prime-member + fdtab (β4), so ~4 repeated F2H ioctls free the file/dmabuf while the fd and GEM handle are still alive; the racing-close variant frees it mid-ioctl. - Primitives:
kfree(dmabuf)(M_DRM zone) + freedstruct file(M_FILE objcache) still reachable through (a) the installed fd (holdfp β fhold on freed memory), (b) the drm_prime_member rb-tree (dma_buf_put on free), (c) obj->dma_buf (drm_gem.c:271 dma_buf_put at handle close). Attacker-controlled heap spray around a 0x?? -sizedstruct dma_bufgives the classic overwrite β function-pointer (dmabuf->ops->releaseat dmabuf_close, linux_dma-buf.c:63) hijack. On this port there is no SMAP/SMEP/KASLR obstacle in principle; we did not carry the chain to uid=0 because the guest has no DRM device to reach the unprivileged ioctl entry β that is an environment limitation, not a property of the bug. - The same unbalanced put also fires on the first import of a foreign dmabuf's error unwind (drm_prime.c:852-858) β but on this port that path panics earlier (see DF-4516).
Fix validation (mandatory for memcorrupt)
fix.diff restores the Linux contract: dma_buf_get() keeps the holdfp() reference
(the single caller drm_prime.c:802 balances it with the existing trailing put).
Applied to the guest /usr/src, drm.ko + module rebuilt, same PoC re-run
(findings/poc/DF-4515/fix_run.log):
[5] F2H lookup-hit cycle #1/#2/#3 done; f_count now 2 (stable β no underflow) [5] survived UAF touch (dma_buf_get -> 0) (file alive β no UAF) (no panic; guest kept running)
Baseline panic gone; fixed behavior correct. fix_status = fixed.
References
sys/dev/drm/drm_prime.c:802, :811-815, :848, :857, :862-865, :792-866 sys/dev/drm/linux_dma-buf.c:168-187 (root cause), :55-68 (dmabuf_closeβkfree) sys/kern/kern_descrip.c:3107-3117 (fhold/fdrop), :2206-2213 (fsetfd does fhold) sys/dev/drm/drm_gem.c:270-273 (obj->dma_buf put), :239-242 (member removal)
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff applied to guest /usr/src, drm.ko + df4515mod rebuilt, identical PoC step 5 re-run: f_count remained 2 through all three F2H cycles (baseline: 2->1->0 + free + panic), the post-free touch operated on a live file, and the guest kept running (baseline panicked in fdrop at process exit). Bad behavior gone; correct error/return semantics intact.
findings/poc/DF-4515/fix_run.log vs run.log + panic.txt
Confirmed kernel references
Detail
Exploit chain
On GPU hardware (source-certain, not exercisable in this guest): video-group user opens /dev/dri/cardN -> DRM_IOCTL_PRIME_HANDLE_TO_FD (export, ~4 file refs: falloc + obj->dma_buf + prime-member + fdtab) -> DRM_IOCTL_PRIME_FD_TO_HANDLE once (import) -> repeat FD_TO_HANDLE on the same fd: each ioctl drops one f_count (drm_prime.c:864) -> after ~4 repeats f_count==0 -> dmabuf_close kfree's the M_DRM dma_buf and drops the GEM object while fd + GEM handle + rb-tree member still point at it -> spray M_DRRM-sized objects -> hijack dmabuf->ops->release (called via dmabuf_close, linux_dma-buf.c:63) or corrupt the freed struct file -> code exec in kernel context -> uid=0. The racing-close variant frees the dma_buf mid-ioctl for a second UAF window. Not carried to uid0 here only because the QEMU guest has no DRM device.
Evidence (decisive lines)
run.log (f_count 2->1->0 + 'release fired ... fd still installed' + 'survived UAF touch'), panic.txt ('panic: fdrop: invalid f_count -1', trace fdrop<-closef<-fdfree<-exit1<-sys_exit), fix_run.log (f_count stays 2, no panic after fix.diff), VERDICT.md (full narrative)
PoC changes
Original audit sketch was ioctl-driven; guest has no GPU, so the PoC was rebuilt as a KLD that calls the shipped drm.ko symbols dma_buf_export/dma_buf_fd/dma_buf_get/dma_buf_put in the exact drm_prime.c order. Sysctl-per-step did not work (each sysctl(8) is its own process, so the fd died between steps) - steps folded into one handler (fire=5).
Verified recommended fix
dma_buf_get(): keep the holdfp() reference (drop the early dropfp) so the returned dma_buf carries the fget-style reference drm_prime.c:864's dma_buf_put expects.
Verdict
dma_buf_get() on this port drops its file reference before returning (linux_dma-buf.c:184 dropfp), so drm_gem_prime_fd_to_handle()'s trailing dma_buf_put (drm_prime.c:864, also :848/:857) is an unbalanced fdrop and the dma_buf pointer is used unreference'd across the whole ioctl. Proven on the guest against the shipped drm.ko: repeated F2H lookup-hit cycles drive f_count 2->1->0, dma_buf_ops.release fires while the prime fd is still installed (premature free), a subsequent dma_buf_get on the installed fd is a use-after-free, and the process exit panics with 'fdrop: invalid f_count -1'. Guest has no GPU so the unprivileged ioctl entry itself could not be driven; the KLD executes the exact drm_prime.c call sequence through the real module symbols. Fix (keep the holdfp reference) validated on the same guest: f_count stable, no panic.
No comments yet.