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

smb_dev2share type-confuses non-vnode fd (socket/pipe/kqueue) as vnode via unchecked fp->f_data cast β€” kernel panic

Summary

smb_dev.c:391 holdfp_fdp(fd FREAD|FWRITE) returns ANY readable+writable fd. :395 vp=(struct vnode*)fp->f_data WITHOUT checking fp->f_type==DTYPE_VNODE. :400 vn_todev(vp) reads vp->v_type and vp->v_rdev at vnode offsets inside wrong struct (socket/pipe/kqueue). If garbage at v_type offset matches VBLK(3)/VCHR(4) vn_todev KKASSERT(vp->v_rdev!=NULL) panics or SMB_GETDEV :405 dereferences garbage cdev_t si_drv1. Trigger: mount_smbfs with args.dev=socket fd. Root-only (mount syscall). smbfs_mount (smbfs_vfsops.c:137) passes args.dev straight through. Fix: check fp->f_type!=DTYPE_VNODE before cast.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0718 Β· 16 files
FileTypeDescriptionSize
panic.c trigger-source deterministic panic trigger: shapes socket SO_RCVLOWAT=3 so the type-confusion reads VBLK(3) at v_type, forging a cdev_t -> wild SMB_GETDEV deref -> panic 2.8 KB view raw
hunter.c trigger-source broad fd-type sweep (socket/pipe/kqueue/vnode); shows the type-confusion path is entered for every non-vnode fd 3.2 KB view raw
fix.diff suggested-fix git-apply-able: add fp->f_type != DTYPE_VNODE -> EINVAL check before the cast in smb_dev2share 713 B view raw
build.sh build-script cc -o panic panic.c ; cc -o hunter hunter.c 266 B view raw
run.sh run-script ./panic (as root, after kldload smbfs) 381 B view raw
baseline_panic.log run-log BUGGY #0 baseline: panic.c -> kernel panic (RUN_RC=124, guest died in DDB) 974 B view raw
fix_run.log run-log PATCHED module: panic.c -> errno=22 (EINVAL), kernel survived (full output) 388 B view raw
fix_run.2.log run-log PATCHED module: hunter.c -> all non-vnode fds EINVAL, vnode fd EBADF (determinism re-run) 867 B view raw
fix_build.log build-log patched smbfs.ko module build output (BUILD_EXIT=0) 16.0 KB view raw
panic.txt panic-signature Fatal trap 12 / smb_dev2share+0x59 / fault 0x70098 from boot.log 325 B view raw
dmesg.txt dmesg kernel 'invalid device handle N (errno)' lines showing smb_dev2share entry 562 B view raw
env.txt environment uname, kern.version, cc version, smbfs module sha256, vfs.usermount 506 B view raw
VERDICT.md verdict full narrative: mechanism, threat model, fix, validation 7.1 KB ↓ raw
README.md readme build/run/expected + how to reproduce the fix 2.1 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme build/run/expected + how to reproduce the fix
↓ download raw

DF-0718 PoC β€” smb_dev2share type-confuses non-vnode fd as vnode

Build (on guest, as any user)

cc -o panic panic.c
cc -o hunter hunter.c

(hunter.c warns about implicit kqueue() β€” harmless; add -include sys/event.h to silence.)

Or just: ./build.sh

Run (as root, after kldload smbfs)

./panic        # deterministic kernel PANIC on the buggy kernel
               #   (Fatal trap 12, smb_dev2share+0x59, fault 0x70098)
               # returns errno=22 (EINVAL) with NO panic on the FIXED kernel

./hunter       # broad sweep: socket/pipe/kqueue fds
               #   BUGGY:   errno=9 (EBADF) β€” type-confusion path entered
               #   FIXED:   errno=22 (EINVAL) β€” DTYPE_VNODE check fires

Reproduce the fix

smb_dev.c is built into the smbfs.ko module. Apply fix.diff to /usr/src, rebuild just the module, install, reload:

cd /usr/src && patch -p1 < /path/to/fix.diff
cd /usr/src/sys/vfs/smbfs && \
    KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC make obj && \
    KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC make
cp /usr/obj/usr/src/sys/vfs/smbfs/smbfs.ko /boot/kernel/smbfs.ko
chmod 555 /boot/kernel/smbfs.ko
kldload smbfs          # (or kldunload+kldload if already loaded)

Re-run ./panic β€” it must return EINVAL with the kernel staying up.

Expected outcome

  • Bug present (unpatched #0 smbfs.ko, sha d854773e…): panic β†’ kernel panic (deterministic). hunter β†’ every non-vnode fd returns EBADF.
  • Bug fixed (patched smbfs.ko, sha 73038f80…): panic β†’ EINVAL, kernel survives. hunter β†’ every non-vnode fd returns EINVAL; vnode-non-device fd still EBADF (unchanged).

Notes

  • The trigger must run as root: mount(2) of smbfs requires root (SYSCAP_RESTRICTEDROOT). This is a rootβ†’kernel path, not an unprivileged LPE.
  • The deterministic panic requires SO_RCVLOWAT=3 so the misread v_type equals VBLK(3). Without shaping, default SO_RCVLOWAT=1 reads as VREG and the bug manifests as a (still-wrong) EBADF rather than a panic.
VERDICT.md verdict full narrative: mechanism, threat model, fix, validation
↓ download raw

DF-0718 β€” smb_dev2share type-confuses non-vnode fd as vnode

Verdict: REPRODUCED β€” deterministic kernel panic via type confusion; FIX VALIDATED.

Summary

sys/netproto/smb/smb_dev.c:395 casts fp->f_data to struct vnode * without checking fp->f_type == DTYPE_VNODE. For a non-vnode fd (socket, pipe, kqueue), f_data is not a vnode β€” it is a struct socket * / struct pipe * / struct kqueue *. The code then feeds this type-confused pointer to vn_todev() (sys/kern/vfs_subr.c:2499), which reads vp->v_type and vp->v_rdev at vnode offsets inside the wrong struct. If the bytes that land at the v_type offset match VBLK(3)/VCHR(4), vn_todev returns the bytes at the v_rdev offset as a forged cdev_t, and smb_dev2share then runs SMB_GETDEV(dev) (smb_dev.c:405 = ((struct smb_dev*)dev)->si_drv1) which dereferences the forged pointer β†’ page-fault panic.

Reproduction (deterministic panic)

The cast is unconditional, but whether it panics depends on the byte at the v_type offset (0xe8 = 232) inside the type-confused object. For a struct socket, offset 232 is so_rcv.ssb_lowat (the SO_RCVLOWAT value, default 1 = VREG, which makes vn_todev return NULL β†’ benign EBADF). Setting SO_RCVLOWAT = 3 makes the kernel read VBLK(3) at the v_type offset, so vn_todev proceeds and returns so_rcv.ssb_mbmax (β‰ˆ0x70000, non-NULL) as the forged cdev_t. SMB_GETDEV(dev)->si_drv1 (offset 0x98 in struct cdev) dereferences 0x70098 β†’ Fatal trap 12, page fault.

panic.c: socket(AF_INET,SOCK_STREAM,0); setsockopt(SO_RCVLOWAT, 3);
         mount("smbfs", "/mnt/df0718", 0, &args)   // args.dev = the socket fd

Observed panic (deterministic across two fresh vm.sh reset with-src boots):

Fatal user address access from kernel mode from panic at ffffffff82602cc9
Fatal trap 12: page fault while in kernel mode
cpuid = 0; lapic id = 0
fault virtual address    = 0x70098
fault code               = supervisor read data, page not present
instruction pointer      = 0x8:0xffffffff82602cc9
Stopped at  smb_dev2share+0x59:   movq  0x98(%rax),%rax
db>

0x70098 = ssb_mbmax (β‰ˆ0x70000) + 0x98 (cdev_t::si_drv1 offset) β€” exactly the SMB_GETDEV(dev)->si_drv1 wild dereference. The crash is inside smb_dev2share β€” the cited buggy function.

Mechanism (path:line)

  1. sys/vfs/smbfs/smbfs_vfsops.c:137 β€” smbfs_mount calls smb_dev2share(args.dev, ...) with the user-supplied fd.
  2. sys/netproto/smb/smb_dev.c:391 β€” holdfp_fdp(..., fd, FREAD|FWRITE) returns the struct file *. holdfp checks f_flag, not f_type. (Sockets carry FREAD|FWRITE β€” sys/kern/uipc_syscalls.c:127 β€” so holdfp succeeds.)
  3. sys/netproto/smb/smb_dev.c:395 β€” vp = (struct vnode*)fp->f_data; with NO DTYPE_VNODE check. For a socket fd this is a struct socket *.
  4. sys/netproto/smb/smb_dev.c:400 β€” dev = vn_todev(vp).
  5. sys/kern/vfs_subr.c:2501-2504 β€” vn_todev reads vp->v_type and vp->v_rdev at vnode offsets inside the socket. With SO_RCVLOWAT=3, v_type reads as VBLK, the KKASSERT(vp->v_rdev != NULL) passes (v_rdev = ssb_mbmax β‰ˆ 0x70000), and it returns that as dev.
  6. sys/netproto/smb/smb_dev.c:405 β€” SMB_CHECKMINOR(dev) expands to sdp = SMB_GETDEV(dev) = ((struct smb_dev*)dev)->si_drv1 β€” dereferences 0x70098 β†’ page fault panic.

Threat model / privilege boundary (Phase 6 escalation assessment)

The only caller of smb_dev2share is smbfs_vfsops.c:137 (the smbfs mount(2) path). mount(2) of smbfs requires root: sys/kern/vfs_syscalls.c:5383-5397 get_fscap() returns SYSCAP_RESTRICTEDROOT for smbfs (it is not in the nullfs/devfs/procfs/tmpfs/fusefs allowlist), and caps_priv_check() (sys/kern/kern_caps.c:328) rejects any non-root cred without that cap. vfs.usermount=1 does not help: the __SYSCAP_NOROOTTEST path still requires the cred to actually hold the RESTRICTEDROOT cap bit, which non-root creds do not.

Valid hard blocker — root-only reachability. The vulnerable write/deref is reachable only from an already-root context (mount(SMBFS,...)). Root→kernel is game-over by definition; there is no privilege boundary to cross, so no unprivileged→root escalation exists via this path. This is a root→kernel robustness/correctness gap: an unprivileged user cannot reach smb_dev2share at all.

The primitive itself is a real memory-corruption type-confusion: a root attacker controls the forged cdev_t value via socket-buffer shaping (SO_RCVLOWAT sets the v_type byte; ssb_mbmax/ssb_hiwat shape the v_rdev/v_un fields), producing a semi-controlled kernel pointer dereference. If an unprivileged path to smb_dev2share ever existed, the chain would be: forge cdev_t β†’ control SMB_GETDEV(dev)->si_drv1 β†’ fake struct smb_dev with a crafted sd_share β†’ hijack smb_share ops. No such unprivileged path exists on this kernel, so the demonstrated impact is a root-triggered kernel panic (DoS / type confusion), Medium severity β€” matching the finding's CVSS AV:L/AC:L/PR:H/.../A:H.

Fix

Add a DTYPE_VNODE check before the cast so non-vnode fds are rejected with EINVAL (and fdrop'd) before any type confusion:

fp = holdfp_fdp(..., fd, FREAD|FWRITE);
if (fp == NULL) return EBADF;
if (fp->f_type != DTYPE_VNODE) { error = EINVAL; goto done; }   /* NEW */
vp = (struct vnode*)fp->f_data;

See fix.diff (git-apply-able). smb_dev.c is compiled into the smbfs.ko module (sys/vfs/smbfs/Makefile), so the fix ships as a module rebuild β€” no full kernel rebuild required.

Fix validation (Phase 8) β€” VALIDATED

Test BUGGY baseline (#0, orig smbfs.ko) PATCHED (smbfs.ko sha 73038f80…)
panic.c (SO_RCVLOWAT=3 socket fd) PANIC: trap 12, smb_dev2share+0x59, fault 0x70098 errno=22 (EINVAL), kernel survives, fully up
hunter.c socket/pipe/kqueue fds EBADF (type-confusion path entered) EINVAL (check fires before cast)
hunter.c regular-file vnode fd EBADF (vnode VREG β†’ vn_todev NULL) EBADF (unchanged β€” correct, vnode bypasses new check)

The fix is deterministic: the panic path returns EINVAL and the guest stays up; the legitimate vnode-non-device path is unchanged. fix_status = fixed.

Files

  • panic.c β€” deterministic panic trigger (shapes socket SO_RCVLOWAT=3).
  • hunter.c β€” broad fd-type sweep (shows the type-confusion path is entered for every non-vnode fd; the EBADFβ†’EINVAL delta is the fix signal).
  • fix.diff β€” the DTYPE_VNODE check (git-apply-able).
  • build.sh / run.sh β€” exact build/run.
  • run.log / fix_run.log / fix_run.2.log β€” full untrimmed run output.
  • fix_build.log β€” patched-module build output.
  • panic.txt β€” the panic signature from dfbsd-qemu/boot.log.
  • dmesg.txt β€” kernel invalid device handle N (errno) lines.
  • env.txt β€” guest environment.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. On the unpatched #0 baseline (orig smbfs.ko sha d854773e) panic.c deterministically panics (Fatal trap 12, smb_dev2share+0x59, fault 0x70098); on the single-fix patched smbfs.ko module panic.c returns errno=22 (EINVAL) with the kernel fully up and stable across two runs. hunter.c confirms the broader fix: all non-vnode fds go EBADF->EINVAL (type confusion blocked), while the legitimate vnode-non-device fd stays EBADF (vn_todev NULL for VREG, unchanged). The DTYPE_VNODE check closes the bug.

BASELINE: ./panic on #0+orig-module -> RUN_RC=124 (hang), boot.log: 'fault virtual address = 0x70098 / Stopped at smb_dev2share+0x59: movq 0x98(%rax),%rax / db>'. hunter BUGGY: '[inet stream] errno=9 (Bad file descriptor)'. PATCHED: ./panic on #0+patched-module -> '[!] mount returned rv=-1 errno=22 (Invalid argument) - kernel survived'; guest STILL_UP. hunter PATCHED: '[inet stream] errno=22 (Invalid argument)' / '[regular-file-vnode] errno=9 (Bad file descriptor)' (vnode path preserved).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (kernel unchanged; fix deployed as patched smbfs.ko module, sha256 73038f80b09a98e203e9cec483296652c6fb7c54e9e33a0ea25adb4284ff0195, loaded via kldload on the #0 kernel)

Confirmed kernel references

Detail

Exploit chain

Primitive class: type confusion (CWE-843) -> forged cdev_t -> wild kernel pointer deref. The trigger shapes the socket via SO_RCVLOWAT=3 so the misread v_type=VBLK and the forged dev=ssb_mbmax (~0x70000, semi-controlled via socket buffer state). Conversion attempted: SMB_GETDEV(dev)->si_drv1 deref at dev+0x98 panics. Outcome: PANIC ACHIEVED (deterministic), NOT uid0. BLOCKED from uid0 by a VALID hard blocker: the ONLY caller of smb_dev2share is smbfs_vfsops.c:137 (the smbfs mount(2) path), and get_fscap (sys/kern/vfs_syscalls.c:5397) returns SYSCAP_RESTRICTEDROOT for smbfs, so mount requires root even with vfs.usermount=1 (caps_priv_check kern_caps.c:328 rejects non-root creds lacking the RESTRICTEDROOT cap). The vulnerable deref is reachable only from an already-root context -> root->kernel, no privilege boundary to cross. Concrete next move IF an unprivileged path existed: the forged cdev_t value is attacker-shaped (SO_RCVLOWAT sets v_type byte; ssb_hiwat/ssb_mbmax shape v_un/v_rdev), so a fake struct smb_dev could be planted and SMB_GETDEV(dev)->si_drv1 -> sd_share hijacked; no such unpriv path exists on this kernel. Chain file: panic.c.

Evidence (decisive lines)

BASELINE (BUGGY #0, orig smbfs.ko sha d854773e): ./panic -> process did not return (RUN_RC=124); guest died in DDB. boot.log: 'Fatal trap 12: page fault while in kernel mode / fault virtual address = 0x70098 / Stopped at smb_dev2share+0x59: movq 0x98(%rax),%rax'. Determinism: identical 0x70098 / smb_dev2share+0x59 on a 2nd fresh reset. PATCHED module (smbfs.ko sha 73038f80): ./panic -> 'mount returned rv=-1 errno=22 (Invalid argument) - kernel survived'; guest STILL_UP. hunter.c: BUGGY non-vnode fds -> EBADF (type-confusion path entered); PATCHED -> EINVAL (check fires); vnode non-device fd stays EBADF (unchanged).

PoC changes

findings/poc/DF-0718/ did not exist; created the entire evidence pack from scratch: panic.c (deterministic panic trigger via SO_RCVLOWAT=3 socket shaping, derived from gdb offset analysis showing v_type@0xe8 overlaps so_rcv.ssb_lowat and v_rdev@0xf8 overlaps so_rcv.ssb_mbmax), hunter.c (broad fd-type sweep showing the type-confusion path is entered for every non-vnode fd), trigger.c (initial EBADF-observation trigger), fix.diff (DTYPE_VNODE check), build.sh/run.sh/VERDICT.md/README.md/manifest.json, plus full untrimmed logs.

Verified recommended fix

In sys/netproto/smb/smb_dev.c smb_dev2share(), immediately after the holdfp_fdp NULL check and before the cast, add: if (fp->f_type != DTYPE_VNODE) { error = EINVAL; goto done; }. This rejects socket/pipe/kqueue/etc fds before f_data is mis-typed as a vnode, eliminating the forged-cdev_t deref. smb_dev.c is compiled into the smbfs.ko module (sys/vfs/smbfs/Makefile), so the fix ships as a module rebuild. Matches the finding's stated proposal (the finding summary itself recommends 'check fp->f_type != DTYPE_VNODE before cast'). Full git-apply-able diff in findings/poc/DF-0718/fix.diff.

Verdict

REPRODUCED as a deterministic kernel panic. smb_dev2share (sys/netproto/smb/smb_dev.c:395) casts fp->f_data to struct vnode with NO fp->f_type==DTYPE_VNODE check; for a socket fd f_data is a struct socket. Confirmed via gdb that vnode v_type@0xe8(232) overlaps socket so_rcv.ssb_lowat@96(=232) and v_rdev@0xf8(248) overlaps so_rcv.ssb_mbmax@112(=248). setsockopt(SO_RCVLOWAT,3) makes the kernel read VBLK(3) at v_type, so vn_todev (sys/kern/vfs_subr.c:2499) returns ssb_mbmax (~0x70000) as a forged cdev_t; SMB_CHECKMINOR(dev) -> SMB_GETDEV(dev)->si_drv1 (smb_dev.c:405) derefs 0x70098 -> Fatal trap 12 page fault, deterministic across two fresh vm.sh reset with-src boots. Crash is literally inside smb_dev2share+0x59 (movq 0x98(%rax),%rax).