# DF-0599 — VERDICT

## Verdict: REPRODUCED (panic), then FIX VALIDATED

The bug is **real and deterministically triggerable** on the audited
DragonFly master DEV kernel (`6.5-DEVELOPMENT #0`). A single
`SMBIOC_OPENSESSION` ioctl with an unregistered `ioc_localcs` charset name
panics the kernel via a NULL `vc_iod` dereference in `smb_iod_request`. The
authored `fix.diff` closes it: on the patched `smbfs.ko` the ioctl returns
`ENOENT` cleanly with no panic.

## Mechanism (confirmed, every hop cited)

1. **Trigger (root)** — `open("/dev/nsmb")` then `ioctl(fd, SMBIOC_OPENSESSION,
   &ssn)` with `ioc_localcs="BOGUSCS-9"`. `/dev/nsmb` is mode `0700 root:wheel`
   (`sys/netproto/smb/smb_dev.c:356`), so the direct trigger needs root (or a
   privileged confused-deputy such as a hypothetical setuid `mount_smbfs` — on
   this guest `mount_smbfs` is `0555 root:wheel`, not setuid). The netsmb stack
   is a loadable module: `kldload smbfs` brings in `smbfs.ko` + `libmchain.ko`
   + `libiconv.ko` and creates `/dev/nsmb`.

2. **Dispatch** — `SMBIOC_OPENSESSION` (`sys/netproto/smb/smb_dev.c:187-191`) →
   `smb_usr_opensession` (`sys/netproto/smb/smb_usr.c:163-180`) →
   `smb_usr_vc2spec` (only validates `ioc_user[0]`, `ioc_server!=NULL`,
   `ioc_localcs[0]!=0`; does **not** validate the charset against a real
   kiconv cspair — `smb_usr.c:65-72`) → `smb_sm_lookup` with `SMBV_CREATE`.

3. **Create + charset failure** — `smb_sm_lookup` (`smb_conn.c:202`) calls
   `smb_vc_create`. At `smb_conn.c:486` it does
   `iconv_open("tolower", vcspec->localcs, &vcp->vc_tolower)`. With **zero**
   iconv converter modules loaded (only the `libiconv.ko` framework; no
   cspairs registered), `iconv_open` (`sys/libiconv/iconv.c:233-267`) finds no
   matching cspair and returns **`ENOENT`** (iconv.c:266). The `do/while` breaks.

4. **Error cleanup → NULL iod deref** — `smb_vc_create` error path
   (`smb_conn.c:512-513`) calls `smb_vc_put(vcp)` → `smb_co_put`
   (`smb_conn.c:337-364`) drops usecount to 0 → `smb_co_gone`
   (`smb_conn.c:249-268`) → dispatches `cp->co_gone = smb_vc_gone`
   (`smb_conn.c:550-556`) → `smb_vc_disconnect(vcp)` (`smb_conn.c:678-684`) →
   `smb_iod_request(vcp->vc_iod, ...)`. But `vc_iod` is still **NULL**:
   `smb_zmalloc` zeroed it (`smb_conn.c:437`) and `smb_iod_create`
   (`smb_conn.c:504`) never ran.

5. **PANIC** — `smb_iod_request(NULL, ...)` executes
   `SMB_IOD_EVLOCK(iod) = smb_sl_lock(&iod->iod_evlock)`
   (`sys/netproto/smb/smb_iod.c:403`, macro at `smb_iod.c:58`), which
   dereferences `NULL + offsetof(struct smbiod, iod_evlock)` (= `0x58` on this
   build) and takes a page fault.

## Evidence — baseline (unpatched `#0` + original `smbfs.ko`)

Serial console (`dfbsd-qemu/boot.log`):

```
Fatal user address access from kernel mode from panic at ffffffff82609ab8
Fatal trap 12: page fault while in kernel mode
cpuid = 0; lapic id = 0
fault virtual address   = 0x58
fault code              = supervisor write data, page not present
instruction pointer     = 0x8:0xffffffff82609ab8
kernel: type 12 trap, code=2
Stopped at      smb_iod_request+0x58:   lock xaddl      %edx,0x58(%rbx)
db>
```

IP `0xffffffff82609ab8` = `smbfs.ko` base `0xffffffff82600000` + `smb_iod_request@0x9a60`
+ `0x58`. `%rbx=0` ⇒ `vc_iod==NULL`. The fault address `0x58` is the offset of
`iod_evlock` inside `struct smbiod` (`sys/netproto/smb/smb_conn.h:448`), exactly
as predicted. **Deterministic** — straight-line code, no race.

(The finding predicted fault addr ≈ `0x40`; the actual offset is `0x58` because
`iod_evlock` sits after several pointer/int fields. Same bug, same function,
same root cause.)

## PoC changes (what I authored and why)

The finding shipped **no trigger source** — only a README. I authored
`panic.c` from the cited path. One precondition fix during iteration:

- **First run panicked at `dup_sockaddr+0x18` (fault addr `0x0`)**, not at
  `smb_iod_request`. Root cause: my first PoC left `ioc_local=NULL`, and
  `smb_vc_create` calls `dup_sockaddr(vcspec->lap)` *unconditionally* at
  `smb_conn.c:466` — *before* reaching the `iconv_open` at line 486 — so it
  NULL-deref'd `lap` first. Fix: supply a valid `ioc_local` sockaddr so the
  flow reaches the charset error path. (This is an **incidental additional
  latent NULL-deref** in the same function — `smb_usr_vc2spec` skips setting
  `lap` when `ioc_local==NULL` but `smb_vc_create` assumes it is set — same
  class, same trigger surface; noted but not the subject of DF-0599.)
- Used a 9-char bogus charset (`"BOGUSCS-9"`) because `ioc_localcs` is only 16
  bytes; the README's `"THIS_CHARSET_DOES_NOT_EXIST_9"` would not fit.
- Set `ioc_servercs[0]='\0'` so only the two `tolower`/`toupper` `iconv_open`
  calls run (the trigger), skipping the `toserver`/`tolocal` block.
- Defined the `smbioc_ossn` struct field-for-field from
  `sys/netproto/smb/smb_dev.h:65-83` so the ioctl `copyin` size matches the
  kernel exactly (computed via `_IOW('n',100,struct smbioc_ossn)`).

## Exploit chain

Not a memory-corruption primitive — it is a NULL-deref page fault at a fixed
small offset (`0x58`). `map_at_zero` is off by default on DragonFly, so the
fault address is unmappable and yields only a **local kernel panic / DoS**
(whole-host). No info leak, no code execution, no privilege escalation. The
finding's severity (Medium, CVSS 6.2, `C:N/I:N/A:H`) is accurate.

## Fix (verified)

`fix.diff` (git-apply-able, plain unified diff so DragonFly `patch -p1` also
accepts it) makes two minimal changes:

1. **`smb_vc_gone`** (`sys/netproto/smb/smb_conn.c`) — guard the disconnect:
   `if (vcp->vc_iod != NULL) smb_vc_disconnect(vcp);`. This is the real fix
   for the NULL-deref: when `smb_vc_create` failed before `smb_iod_create`
   succeeded, there is no iod to disconnect from.

2. **`smb_iod_create`** (`sys/netproto/smb/smb_iod.c`) — on
   `kthread_create_compat` failure, set `vcp->vc_iod = NULL` *before*
   `kfree(iod)`. This closes the related **UAF variant** the finding describes
   (`smb_iod.c:699` sets `vc_iod=iod`, then `:708` frees `iod` without
   clearing `vc_iod`, so the same gone-hook would UAF the freed iod).

This **matches** the finding's `## Recommended fix` proposal (gone-hook guard
+ iod_create NULL-clear). It omits the optional `KKASSERT` in
`smb_vc_disconnect` (the guard makes it unreachable with NULL) and omits the
optional early `localcs`/`servercs` validation in `smb_usr.c` (defense in
depth, not required to close the panic).

### Why I rebuilt the module, not `nativekernel`

`smbfs` is `optional netsmb` in `sys/conf/files` and ships as a **loadable
module** (`sys/vfs/smbfs/Makefile`, `.PATH netproto/smb`) — it is **not**
compiled into the `X86_64_GENERIC` kernel (confirmed: no smb symbols /
`/dev/nsmb` until `kldload smbfs`). Therefore `make nativekernel` would **not**
have picked up the fix. The correct validation unit was rebuilding only
`smbfs.ko` from patched source (`cd /usr/src/sys/vfs/smbfs && make`), replacing
`/boot/kernel/smbfs.ko`, and `kldload`-ing it. The base kernel stays `#0`
(unchanged) — the fix lives entirely in the module.

### Before / after

| | kernel | smbfs.ko | result |
|---|---|---|---|
| **baseline** | `#0` (unpatched) | original | `panic: smb_iod_request+0x58`, fault 0x58, guest DOWN |
| **patched**  | `#0` (unchanged) | rebuilt w/ fix | `ioctl → ENOENT` (errno 2), guest UP, no panic, deterministic ×3 |
