# DF-0673 — `nbssn_recv` (smb_trantcp.c) mbuf leak on receive error / partial chain

## Verdict
**SOURCE-CONFIRMED (not live-testable here).** The bug is unambiguous in source;
the live trigger requires a malicious SMB server speaking NetBIOS Session
Service + enough SMB to advance the kernel SMB client into the post-negotiate
receive loop, which is beyond what this guest's `mount_smbfs` + a minimal fake
server could drive in the available time (the trigger hangs in `mount_smbfs`
waiting on `smbiod` before reaching the leak site).

## Mechanism

`sys/netproto/smb/smb_trantcp.c:313-376` `nbssn_recv`:

Two distinct leak paths, both rooted in `sbinit()` only zeroing fields
(`sys/sys/sockbuf.h:115-126`) — it never frees the prior `sb_mb` chain:

### Path 1 — inner do/while re-iteration orphans a partial chain
```c
do {
    sbinit(&sio, savelen);   // :346 ZEROES sio.sb_mb without freeing it
    rcvflg = MSG_WAITALL;
    error = so_pru_soreceive(so, NULL, NULL, &sio, NULL, &rcvflg);
} while (error == EWOULDBLOCK || error == EINTR || error == ERESTART);
```
If `so_pru_soreceive` returns `EWOULDBLOCK`/`EINTR`/`ERESTART` having already
moved some bytes into `sio.sb_mb` (which happens with `MSG_WAITALL` on a
slow/interrupted TCP receive), the next loop iteration's `sbinit(&sio, savelen)`
zeroes `sio.sb_mb` — orphaning (leaking) the prior partial chain.

### Path 2 — outer-loop `if (error) break;` skips cleanup
```c
if (error)           // :352-353
    break;           // *** exits outer loop ***
...
if (error == 0) {    // :367  <-- cleanup only runs on success
    if (mpp) *mpp = sio.sb_mb;
    else      m_freem(sio.sb_mb);
    ...
}
```
Any non-retry receive error (e.g. `ECONNRESET` after a TCP RST) leaves a partial
`sio.sb_mb` chain that the success-only cleanup never frees.

### Reachability
- `nbssn_recv` is called from `smb_nbst_recv` (smb_trantcp.c:539), the
  `SMB_TRAN_RECV` vector, invoked in a `for(;;)` loop by
  `smb_iod_recvall` (smb_iod.c:304,322-324). Each iteration that errors out and
  re-enters can re-leak.
- Sustained attack (a malicious SMB server that keeps accepting connections and
  RST'ing mid-stream) drives mbuf-pool exhaustion → network collapse / panic.

## Threat model / preconditions (realistic)

- `smbfs.ko` loaded (root action; realistic on systems that mount SMB shares).
- A malicious SMB server the victim mounts from (network attacker / MitM /
  compromised server).

No `kldload` by the attacker; the trigger is the kernel SMB client processing
untrusted server data.

## Why not live-reproduced here

The fake-server PoC in `fake_smb_server.c` completes the NBSS session-request
handshake and emits a malformed SMB negotiate response, then sends a NBSS
message header claiming 4000 B but delivers only 50 B before RST'ing. The
intent was to drive `mount_smbfs` into the `nbssn_recv` post-negotiate path.
In practice `mount_smbfs` hangs in `D1` (uninterruptible wait) before reaching
the leak site — `smbiod` blocks waiting on the bogus negotiate response that
the minimal fake server never properly formats, so the kernel never advances
to `nbssn_recv` for the post-negotiate message. A live PoC would require a
fully-conforming NBSS+SMB negotiate response (correct SMB1 header, dialect
array, security blob, etc.) — substantial additional work beyond this run's
budget.

The bug itself is unambiguous: `sbinit()` does not free, and the error paths
skip `m_freem(sio.sb_mb)`. Source confidence is **certain**.

## Realistic impact ceiling

This is an **mbuf-pool exhaustion DoS** (resource leak). It is **not** a
memory-corruption primitive — no attacker-controlled write, no UAF, no type
confusion — so there is no escalation chain. Ceiling: repeated mounts from a
malicious server eventually exhaust the mbuf pool → network collapse / panic.

## Fix

`sys/netproto/smb/smb_trantcp.c`:
- Free `sio.sb_mb` before `sbinit(&sio, savelen)` in the inner do/while
  (Path 1).
- Free `sio.sb_mb` on the `if (error) break;` outer-loop exit (Path 2).
- Free `sio.sb_mb` on the `ECONNRESET` outer-loop exit (the `so->so_state &
  SS_ISDISCONNECTING|…` path) — same class of leak.

See `fix.diff` (git-apply-able, applies cleanly, compiles, kernel boots).

## Fix validation (Phase 8)

`fix_status: not_testable`. The fix.diff **applies** (`patch -p1` succeeds,
`git apply --check` succeeds) and **compiles** as part of the single-fix-combined
kernel (`6.5-DEVELOPMENT #1` built and booted successfully, see DF-0680
evidence pack for the combined build log). But because the live trigger is not
reproducible on this guest (see above), the before/after leak behavior cannot
be measured here. The change is small, mechanical, and obviously correct given
the source-level analysis.
