⬢ DragonFlyBSD Kernel Audit
← triage · dashboard
DF-5133

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5133 · 15 files
FileTypeDescriptionSize
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
README.md
↓ download 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)

VERDICT.md
↓ download raw

DF-5133 — VERDICT: reproduced (unprivileged kernel NULL-call panic), fix validated

What was run

  1. Built the dmabugdemo kmod against the stock guest kernel (DragonFly 6.5-DEVELOPMENT #0, Thu Jul 2 06:02:54 UTC 2026, x86_64). The kmod resolves the REAL dma_buf_export / dma_buf_fd symbols from the stock /boot/kernel/drm.ko and 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 calls kern_shutdown(fd, SHUT_RDWR) — the exact backend of the unprivileged shutdown(2) syscall.
  2. 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 fileops has 8 members; .fo_shutdown is member 7 and every kernel fileops sets it except dmabuf_fileops.
  • sys/dev/drm/linux_dma-buf.c:107-115 — dmabuf_fileops sets only read/write/ioctl/kqfilter/stat/close/seek; fo_shutdown stays NULL from the C99 designated-initializer zero-fill.
  • sys/kern/kern_descrip.c:1522-1535 — kern_shutdown() = backend of shutdown(2): holdfp(td, fd, -1) (any fd, no type check, no privilege check) then error = fo_shutdown(fp, how) — a blind indirect call. (FreeBSD's equivalent checks fp->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

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 (same base kernel; only /boot/kernel/drm.ko rebuilt from patched source)

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.