# 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.
