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)
PoC verification
Evidence pack
findings/poc/DF-0718 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
#0smbfs.ko, shad854773eβ¦):panicβ kernel panic (deterministic).hunterβ every non-vnode fd returnsEBADF. - Bug fixed (patched smbfs.ko, sha
73038f80β¦):panicβEINVAL, kernel survives.hunterβ every non-vnode fd returnsEINVAL; vnode-non-device fd stillEBADF(unchanged).
Notes
- The trigger must run as root:
mount(2)ofsmbfsrequires root (SYSCAP_RESTRICTEDROOT). This is a rootβkernel path, not an unprivileged LPE. - The deterministic panic requires
SO_RCVLOWAT=3so the misreadv_typeequalsVBLK(3). Without shaping, defaultSO_RCVLOWAT=1reads asVREGand the bug manifests as a (still-wrong)EBADFrather than a panic.
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)
sys/vfs/smbfs/smbfs_vfsops.c:137βsmbfs_mountcallssmb_dev2share(args.dev, ...)with the user-supplied fd.sys/netproto/smb/smb_dev.c:391βholdfp_fdp(..., fd, FREAD|FWRITE)returns thestruct file *.holdfpchecksf_flag, notf_type. (Sockets carryFREAD|FWRITEβsys/kern/uipc_syscalls.c:127β so holdfp succeeds.)sys/netproto/smb/smb_dev.c:395βvp = (struct vnode*)fp->f_data;with NODTYPE_VNODEcheck. For a socket fd this is astruct socket *.sys/netproto/smb/smb_dev.c:400βdev = vn_todev(vp).sys/kern/vfs_subr.c:2501-2504βvn_todevreadsvp->v_typeandvp->v_rdevat vnode offsets inside the socket. WithSO_RCVLOWAT=3,v_typereads asVBLK, theKKASSERT(vp->v_rdev != NULL)passes (v_rdev=ssb_mbmaxβ 0x70000), and it returns that asdev.sys/netproto/smb/smb_dev.c:405βSMB_CHECKMINOR(dev)expands tosdp = SMB_GETDEV(dev)=((struct smb_dev*)dev)->si_drv1β dereferences0x70098β 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 socketSO_RCVLOWAT=3).hunter.cβ broad fd-type sweep (shows the type-confusion path is entered for every non-vnode fd; theEBADFβEINVALdelta is the fix signal).fix.diffβ theDTYPE_VNODEcheck (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 fromdfbsd-qemu/boot.log.dmesg.txtβ kernelinvalid device handle N (errno)lines.env.txtβ guest environment.
Fix verification
fixedVALIDATED. 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).
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).
No comments yet.