# DF-2995 — VERDICT

**Status: REPRODUCED** (impact: leak — kernel mbuf memory exhaustion, confidence: certain, attempts: 1 + control)

## What was claimed
`nfsrv_writegather()` leaks a reply mbuf chain whenever a v3 WRITE carries a
filehandle-length word that is neither 0 nor NFSX_V3FH, because NEGREPLYOUT's
-2 path builds reply #1 via nfsm_reply() and the local nfsmout label then
builds (and drops) reply #2.

## How it was proven
Guest: DragonFly 6.5-DEVELOPMENT #0, NFS server exporting /tmp/nfsroot.

    $ sysctl vfs.nfs.gatherdelay_v3=5000
    $ netstat -m | head -3
        8/146632 mbufs in use (current/max):
    $ /root/nfspoc writebadfh 300          # 300 x v3 WRITE, fhlen=5
        write[299]: reply status=5
    $ netstat -m | head -3
        607/146632 mbufs in use (current/max):
        ...
        863 mbufs and mbuf clusters allocated to data   (+599)

~2 mbufs leaked per request (8 -> 607 after 300 requests), persistent (server
still serving NULL RPCs at 0.0002s — no recovery, no free).

Control: `sysctl vfs.nfs.gatherdelay_v3=0` then 300 identical requests ->
mbuf count unchanged at 607. The leak is exclusively the writegather path.

## Why it happens (code path, line-accurate)
- sys/vfs/nfs/nfs_syscalls.c:667-675 — a v3 NFSPROC_WRITE reaches
  nfsrv_writegather only when `writes_todo` (a gather flush is pending) or
  procrastinate>0; v3 default is gatherdelay_v3=0, so the bug needs the knob
  raised (write-performance tuning) — hence severity Low.
- sys/vfs/nfs/nfs_serv.c:1263 — `NEGREPLYOUT(nfsm_srvmtofh(...))`:
  nfsm_srvmtofh (sys/vfs/nfs/nfsm_subs.c:946-950) returns -2 for garbage
  fhlen; NEGREPLYOUT (nfsm_subs.h:114-122) then calls
  `nfsm_reply(&info, nfsd, slp, 0, &error)` which builds reply #1 into
  info.mreq (and frees the request mbuf, error zeroed).
- nfs_serv.c:1306-1319 — the local nfsmout unconditionally executes
  `error = EIO; nfsm_writereply(...)` -> nfs_rephead allocates a fresh chain
  into info.mreq — reply #1 is overwritten **without m_freem** -> leaked.
- All other entries into nfsmout have info.mreq == NULL, so only this path
  leaks.

## Exploit chain
Remote unauthenticated (when gatherdelay_v3>0) memory exhaustion: each ~100
byte request permanently removes ~2 mbufs from the zone; at scale m_getl
M_WAITOK blocks and every nfsd thread hangs. Measured rate ~600 mbufs / 300
requests.

## Fix validation
fix.diff guards the nfsmout reply build with `if (info.mreq == NULL)`, reusing
the already-built reply. Validated on a rebuilt kernel: 300 malformed-fh v3
WRITEs with gatherdelay_v3=5000 leak 0 mbufs (count stable) while error
replies still come back (see run.fixed.log).
