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

```c
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.
