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)
PoC verification
Evidence pack
findings/poc/DF-2995 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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 callsnfsm_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
fixedPatched 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']
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.
No comments yet.