dmabuf_fileops leaves fo_shutdown NULL: shutdown(2) on any dma-buf fd is an unprivileged kernel NULL function-pointer call (panic)
Summary
The only fileops in the kernel missing the member; kern_shutdown() dispatches fo_shutdown with no type check - one shutdown(fd) on any PRIME-exported fd calls *0. REPRODUCED as uid 65534 on stock guest (kmod performing the stock export+fd in trigger context): fo_shutdown=0 printed from the live f_ops then Fatal trap 12 at ip 0x0. badfo_shutdown one-liner fix validated on patched drm.ko.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5133 · 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | — | 3.0 KB | ↓ raw | |
| VERDICT.md | — | 3.9 KB | ↓ raw | |
| panic_trigger.c | — | 1007 B | view raw | |
| dmabuf_demo.c | — | 5.6 KB | view raw | |
| build.sh | — | 135 B | view raw | |
| run.sh | — | 151 B | view raw | |
| build.log | — | 563 B | view raw | |
| run.log | — | 1.9 KB | view raw | |
| panic.txt | — | 3.5 KB | view raw | |
| serial_full.log | — | 3.5 KB | view raw | |
| env.txt | — | 480 B | view raw | |
| fix.diff | — | 457 B | view raw | |
| fix_build.log | — | 457 B | view raw | |
| fix_run.log | — | 663 B | view raw | |
| verdict.json | — | 3.8 KB | view raw |
DF-5133 — shutdown(2) on a dma-buf fd calls a NULL fo_shutdown (kernel panic)
sys/dev/drm/linux_dma-buf.c:107-115 — dmabuf_fileops initializes only 7 of
the 8 members of DragonFly's struct fileops (sys/sys/file.h:76-89);
.fo_shutdown is left NULL. Every other fileops in the kernel sets it
(vnops→nofo_shutdown, sockets→soo_shutdown, pipes→pipe_shutdown,
kqueue→nofo_shutdown, mqueue/badfileops→badfo_shutdown).
kern_shutdown() (sys/kern/kern_descrip.c:1522-1535) — the backend of
shutdown(2) — performs no file-type check: it holdfp()s any fd and
immediately calls fp->f_ops->fo_shutdown(fp, how). On a dma-buf fd this is an
indirect call through a NULL pointer → fatal trap 12 at IP 0.
Reachability on real hardware: an unprivileged user with a drm render node
obtains a dma-buf fd via DRM_IOCTL_PRIME_HANDLE_TO_FD
(drm_prime.c:669 dma_buf_fd()), then calls shutdown(fd, SHUT_RDWR).
Reproduce (on the audit QEMU guest, which has no GPU)
The guest's QEMU std-VGA has no drm device, so the unprivileged render-node
path cannot produce a dma-buf fd. The dmabugdemo kmod stands in for that
hardware step: it calls the REAL dma_buf_export() + dma_buf_fd() from
sys/dev/drm/linux_dma-buf.c (resolved from the stock drm.ko) in the context
of the calling process, then kern_shutdown() — i.e. the last two steps are
the genuine kernel code paths under test, run as uid 65534.
# build (guest) cd /root/poc cc -O -o panic_trigger panic_trigger.c printf 'KMOD=\tdmabugdemo\nSRCS=\tdmabuf_demo.c\n\n.include <bsd.kmod.mk>\n' > Makefile make # run (guest) kldload drm kldload /usr/obj/root/poc/dmabugdemo.ko su -m nobody -c /tmp/panic_trigger
Expected output (baseline, unpatched modules)
[panic_trigger] uid=65534 firing hw.dmabug.panic: kern_shutdown(dmabuf_fd, SHUT_RDWR) with dmabuf_fileops.fo_shutdown == NULL <guest dies: ssh hangs>
Serial console (vm.sh log):
dmabug: panic: uid=65534 dmabuf fd=3 f_ops=0xffffffff82672180 fo_shutdown=0 (NULL => indirect call to 0) dmabug: panic: calling kern_shutdown(3, SHUT_RDWR) -- same path as an unprivileged shutdown(2) on a prime fd Fatal user address access from kernel mode from panic_trigger at 0000000000000000 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 instruction pointer = 0x8:0x0 current process = 1312 (panic_trigger) db>
Fix validation
fix.diff adds .fo_shutdown = badfo_shutdown to dmabuf_fileops.
After applying to the guest's /usr/src copy, rebuilding + installing drm.ko
and rebooting, the same trigger returns cleanly:
[panic_trigger] uid=65534 firing hw.dmabug.panic: ... dmabug: panic: uid=65534 dmabuf fd=3 f_ops=0xffffffff82654900 fo_shutdown=0xffffffff8062e5d0 (NULL => indirect call to 0) dmabug: panic: shutdown RETURNED (fo_shutdown set => fixed) [panic_trigger] shutdown() returned 0 -- kernel survived (fo_shutdown present / fixed)
(full logs: run.log = baseline panic run + serial excerpt; fix_run.log = patched-kernel run)
DF-5133 — VERDICT: reproduced (unprivileged kernel NULL-call panic), fix validated
What was run
- Built the
dmabugdemokmod against the stock guest kernel (DragonFly 6.5-DEVELOPMENT #0, Thu Jul 2 06:02:54 UTC 2026, x86_64). The kmod resolves the REALdma_buf_export/dma_buf_fdsymbols from the stock/boot/kernel/drm.koand exposes three CTLFLAG_ANYBODY sysctl nodes.hw.dmabug.panic(fireable by uid 65534) creates a genuine dma-buf fd in the CALLER's fd table and then callskern_shutdown(fd, SHUT_RDWR)— the exact backend of the unprivilegedshutdown(2)syscall. - Baseline run as nobody:
su -m nobody -c /tmp/panic_trigger.
Result (baseline)
- ssh session hung (guest died);
vm.sh status→down. - Serial console shows the NULL function-pointer call:
dmabug: panic: uid=65534 dmabuf fd=3 f_ops=0xffffffff82672180 fo_shutdown=0 dmabug: panic: calling kern_shutdown(3, SHUT_RDWR) ... Fatal user address access from kernel mode from panic_trigger at 0000000000000000 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 instruction pointer = 0x8:0x0 current process = 1312 (panic_trigger) db>
The kernel printed fo_shutdown=0 from the live fp->f_ops — direct proof
the ops vector member is NULL on a real dma-buf file created by the stock
dma_buf_export(). IP=0 = the indirect call target. The firing process was
uid 65534 (nobody).
Why this is real (source trace)
- sys/sys/file.h:76-89 —
struct fileopshas 8 members;.fo_shutdownis member 7 and every kernel fileops sets it exceptdmabuf_fileops. - sys/dev/drm/linux_dma-buf.c:107-115 —
dmabuf_fileopssets only read/write/ioctl/kqfilter/stat/close/seek;fo_shutdownstays NULL from the C99 designated-initializer zero-fill. - sys/kern/kern_descrip.c:1522-1535 —
kern_shutdown()= backend ofshutdown(2):holdfp(td, fd, -1)(any fd, no type check, no privilege check) thenerror = fo_shutdown(fp, how)— a blind indirect call. (FreeBSD's equivalent checksfp->f_type != DTYPE_SOCKET; DragonFly removed the check and relies on each fileops returning an error instead.) - Any unprivileged process holding a dma-buf fd (on real hardware: drm
render node +
DRM_IOCTL_PRIME_HANDLE_TO_FD, drm_prime.c:669) triggers it.
Exploitability
This is a NULL function-pointer call, i.e. control transfer to address 0. Userland cannot map page 0 on DragonFly (no MAP_FIXED-at-0 path), so the practical result is a denial of service (kernel panic), not code execution. Impact: unprivileged local DoS on any machine with a drm device that supports PRIME export (i915/radeon/amdgpu — the standard desktop configuration).
Fix validation
fix.diff (one line: .fo_shutdown = badfo_shutdown in dmabuf_fileops),
applied to the guest's /usr/src, drm.ko rebuilt + installed, reboot:
dmabug: panic: ... fo_shutdown=0xffffffff8062e5d0 (NULL => indirect call to 0) dmabug: panic: shutdown RETURNED (fo_shutdown set => fixed) [panic_trigger] shutdown() returned 0 -- kernel survived PANIC_RC=0 (guest stayed up)
Baseline reproduced the panic (fix_baseline_reproduced=1); patched kernel did not (fix_patched_reproduced=0). fix_status=fixed.
Caveat
The QEMU guest has no GPU, so the dma-buf fd was created by the kmod calling
the stock dma_buf_export()/dma_buf_fd() in the trigger process's context
instead of by a render node ioctl. Everything downstream of fd creation —
kern_shutdown() → fo_shutdown() — is the genuine unmodified kernel path,
executed with the credentials of uid 65534.
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
fixedApplied fix.diff (.fo_shutdown = badfo_shutdown) to the guest's /usr/src copy, rebuilt and installed drm.ko, rebooted, re-ran the identical uid-65534 trigger: fo_shutdown now resolves (0xffffffff8062e5d0 = badfo_shutdown), shutdown() returns, guest stays up. Baseline panic reproduced on the stock module minutes earlier on the same guest.
fix_build.log (patch + drm.ko rebuild + install); fix_run.log ('shutdown RETURNED (fo_shutdown set => fixed)', PANIC_RC=0, guest up).
Confirmed kernel references
Detail
Exploit chain
unpriv render node -> DRM_IOCTL_PRIME_HANDLE_TO_FD -> dma_buf_fd() (drm_prime.c:669) -> dma-buf fd in table; shutdown(fd, 2) -> sys_shutdown -> kern_shutdown (kern_descrip.c:1522) -> holdfp(any fd) -> fo_shutdown == NULL -> call *0 -> fatal trap 12 (IP=0). NULL-call is panic/DoS only; userland cannot map page 0 on DragonFly.
Evidence (decisive lines)
run.log (baseline: trigger output + serial Fatal trap 12, instruction pointer 0x8:0x0, current process = panic_trigger uid 65534, db> prompt); panic.txt / serial_full.log (full serial capture); kmod live-printed 'fo_shutdown=0' from fp->f_ops before the call; fix_run.log (patched: fo_shutdown=0xffffffff8062e5d0, 'shutdown RETURNED', guest up).
PoC changes
PoC written from scratch (no seed). The guest has no GPU, so a kmod (dmabugdemo.ko) creates the dma-buf fd by calling the stock dma_buf_export()/dma_buf_fd() from drm.ko in the firing process's context; kern_shutdown() -> NULL fo_shutdown is the genuine unmodified path. Fixed one build error (SHUT_RDWR needed sys/socket.h).
Verified recommended fix
Add .fo_shutdown = badfo_shutdown to dmabuf_fileops in sys/dev/drm/linux_dma-buf.c (one line); optionally also add a DTYPE check to kern_shutdown() as defense-in-depth.
Verdict
dmabuf_fileops (sys/dev/drm/linux_dma-buf.c:107-115) is the only fileops in the kernel that leaves .fo_shutdown NULL; kern_shutdown() (kern_descrip.c:1522), the backend of shutdown(2), has no file-type or privilege check and calls it on any fd. An unprivileged process holding a dma-buf fd (render node PRIME export on real hardware) calling shutdown(fd, SHUT_RDWR) executes an indirect call to address 0: reproduced live as uid 65534 with Fatal trap 12, IP=0x0, guest wedged in DDB. fix.diff (.fo_shutdown = badfo_shutdown) was rebuilt into drm.ko on the guest and the identical trigger then returned cleanly with the guest up: fix validated.
No comments yet.