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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4515 Β· 12 files
FileTypeDescriptionSize
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:

  1. 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_count to 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.
  2. Even on the first import, the returned dma_buf is unprotected: a racing close(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=5 runs the full sequence (export β†’ dma_buf_fd β†’ 3Γ— F2H lookup-hit cycle β†’ UAF touch)
  • build.sh / run.sh β€” exact guest commands
  • run.log, panic.txt β€” baseline reproduction (panic: fdrop: invalid f_count -1)
  • fix_run.log β€” same PoC after applying fix.diff (f_count stable at 2, no panic)
  • fix.diff β€” one-line fix: keep the holdfp() reference in dma_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()
VERDICT.md
↓ download raw

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() β€” what drm_gem_prime_handle_to_fd does (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 video group). 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) + freed struct 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?? -sized struct dma_buf gives the classic overwrite β†’ function-pointer (dmabuf->ops->release at 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4515

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.