β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2995

nfsrv_writegather leaks a reply mbuf chain per malformed-fh v3 WRITE when vfs.nfs.gatherdelay_v3 > 0 (remote mbuf exhaustion)

Field Value
ID DF-2995
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE CWE-401
File sys/vfs/nfs/nfs_serv.c
Lines 1263, 1307, 1311
Area vfs/nfs
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:vfs
Reported pending
Known CVE none
CVE match novel

Summary

In nfsrv_writegather, NEGREPLYOUT(nfsm_srvmtofh(...)) at :1263 handles the βˆ’2 return (filehandle length neither 0 nor NFSX_V3FH) by calling nfsm_reply(), which builds reply #1 into info.mreq; control then reaches the local nfsmout label (:1307) which unconditionally calls nfsm_writereply() β†’ nfs_rephead, overwriting info.mreq β€” reply #1 is dropped without m_freem and leaks. MEASURED: ~2 mbufs permanently leaked per request (8 β†’ 607 after 300 requests); control with gatherdelay_v3=0 leaks exactly 0; v3 writes only reach writegather when vfs.nfs.gatherdelay_v3 > 0 (non-default, raised for write-performance tuning). Remote unauthenticated memory exhaustion: at scale every m_getl(M_WAITOK) blocks and all nfsd threads hang. Fix: only build the EIO reply when none was built yet (row diff).

Timeline

  • 2026-09-02 Discovered during pass-2 audit of nfs_serv.c (GLM 5.3); leak measured on guest. DF-0766 re-verified, not re-reported.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2995 Β· 10 files
FileTypeDescriptionSize
nfspoc.c β€” 8.4 KB view raw
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 2.6 KB ↓ raw
run.sh β€” 371 B view raw
build.sh β€” 47 B view raw
run.log β€” 1.2 KB view raw
run.fixed.log β€” 1.6 KB view raw
fix.diff β€” 896 B view raw
verdict.json β€” 2.7 KB view raw
env.txt β€” 411 B view raw

DF-2995 β€” nfsrv_writegather leaks the first reply mbuf on malformed v3 filehandle length

Build

cc -O -o /root/nfspoc nfspoc.c

Setup

NFS server running; sysctl vfs.nfs.gatherdelay_v3=5000 (write-gathering for v3; stock default is 0 β€” the bug needs this knob > 0, e.g. write-performance tuning).

Run

netstat -m | head -3                       # baseline
NFSPOC_SRCPORT=813 /root/nfspoc writebadfh 300
netstat -m | head -3                       # +~600 mbufs, never freed

Expected

Each v3 WRITE whose filehandle length word is neither 0 nor NFSX_V3FH leaks ~2 mbufs permanently (observed 8 -> 607 after 300 requests; control with gatherdelay_v3=0 leaks exactly 0). Repeated at scale this exhausts the mbuf zone (all further m_getl(M_WAITOK) block) β€” remote unauthenticated memory exhaustion / nfsd-thread hang, gated on gatherdelay_v3 > 0.

Root cause

sys/vfs/nfs/nfs_serv.c nfsrv_writegather(): - NEGREPLYOUT(nfsm_srvmtofh(&info, nfsd, &nfsd->nd_fh, &error)) (line 1263): nfsm_srvmtofh returns -2 for a garbage fh length (nfsm_subs.c:946-950), and NEGREPLYOUT (nfsm_subs.h:114-122) then calls nfsm_reply(&info, nfsd, slp, 0, &error) which builds reply #1 into info.mreq (nfs_rephead). - Control then jumps to the local nfsmout: (line 1307) which β€” unconditionally β€” sets error=EIO and calls nfsm_writereply(...) (line 1311) β†’ nfs_rephead again β†’ info.mreq is overwritten: reply #1 is dropped without m_freem β†’ leaked (an m_getl pkthdr mbuf per request; the request mbuf accounting adds a second). - Every other entry into that nfsmout builds no prior reply, so only this path leaks.

VERDICT.md
↓ download raw

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).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel: mbuf usage no longer scales with request count (300/900 requests both plateau at 307; bounded reply-cache retention only) vs the buggy kernel +2/request unbounded. Bad behavior gone.

['run.fixed.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Fri Sep 4 21:02:10 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

remote mbuf-zone exhaustion: ~100-byte UDP request removes ~2 mbufs forever; at scale every m_getl(M_WAITOK) blocks and all nfsd threads hang

Evidence (decisive lines)

['run.log β€” netstat -m before (8 mbufs) / after 300 malformed WRITEs with gatherdelay_v3=5000 (607 mbufs) / after 300 more with gatherdelay_v3=0 (unchanged 607)', 'VERDICT.md β€” line-accurate chain: nfs_serv.c:1263 NEGREPLYOUT(-2) builds reply #1 (nfsm_subs.c:946-950, nfsm_subs.h:114-122), nfs_serv.c:1306-1319 nfsmout builds reply #2 unconditionally']

PoC changes

wrote nfspoc.c from scratch (writebadfh subcommand: v3 WRITE with fhlen=5)

Verified recommended fix

In nfsrv_writegather's nfsmout, only build the EIO reply when info.mreq == NULL, reusing the reply nfsm_reply() already built (fix.diff).

Verdict

With vfs.nfs.gatherdelay_v3 > 0, every v3 WRITE whose filehandle-length word is neither 0 nor NFSX_V3FH leaks ~2 mbufs permanently in nfsrv_writegather: NEGREPLYOUT's -2 path builds reply #1 via nfsm_reply(), then the local nfsmout unconditionally builds reply #2 via nfsm_writereply(), dropping #1 without m_freem. Measured 8 -> 607 mbufs after 300 requests (~599 leaked, persistent, server otherwise healthy); control with gatherdelay_v3=0 leaked exactly 0.