NFS READDIR/READDIRPLUS with count=0 from the wire infinite-loops the nfsd kernel thread (remote DoS, host starvation)
| Field | Value |
|---|---|
| ID | DF-2993 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-835 |
| File | sys/vfs/nfs/nfs_serv.c |
| Lines | 2977-2984, 3066, 3120-3124, 3407-3411 |
| 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
nfsrv_readdir/nfsrv_readdirplus accept a client count/dircount of 0, making siz=fullsiz=0 and uio_resid=0. The 'nothing read β reply eof' short-cut is only reachable when io.uio_resid>0 (:3066), so with resid==0 it is skipped; VOP_READDIR(ufs) with resid 0 returns success with non-NULL cookies but zero entries and an unchanged offset (vop_write_dirent returns 1 without setting *error), and the degenerate-case 'goto again' at :3120-3124 (and :3407-3411 for readdirplus) re-runs the identical call forever. Any NFS client the export allows (AUTH_SYS uid irrelevant β READDIR needs only VEXEC; reserved source port trivial) sends one ~120-byte UDP READDIR(count=0) per nfsd thread; each request permanently pins a kernel thread spinning while holding slp->ns_token. VERIFIED twice on the stock INVARIANTS guest: console writers starve mid-script, ssh flaps and dies, in one run the whole guest became unresponsive with no panic (hard livelock). Fix: reject siz<=0 with NFSERR_TOOSMALL before the retry loop (row diff).
Timeline
- 2026-09-02 Discovered during pass-2 audit of nfs_serv.c (GLM 5.3); remote livelock reproduced twice.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2993 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| nfspoc.c | β | 8.4 KB | view raw | |
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 2.9 KB | β raw | |
| run.sh | β | 523 B | view raw | |
| build.sh | β | 47 B | view raw | |
| run.log | β | 2.0 KB | view raw | |
| run.fixed.log | β | 1.6 KB | view raw | |
| build.log | β | 2.9 KB | view raw | |
| fix.diff | β | 1007 B | view raw | |
| fix_all_three.diff | β | 2.1 KB | view raw | |
| verdict.json | β | 3.1 KB | view raw | |
| env.txt | β | 411 B | view raw |
DF-2993 β NFSv3 READDIR/READDIRPLUS with count=0 β infinite kernel loop (remote DoS)
Build
cc -O -o /root/nfspoc nfspoc.c # in-guest, base system cc
Run (guest, NFS server exporting /tmp/nfsroot, nfsd -u -t -n 4)
FH=$(NFSPOC_SRCPORT=820 /root/nfspoc mount | tail -1) # via mountd RPC NFSPOC_SRCPORT=821 NFSPOC_TIMEOUT=5 /root/nfspoc readdir $FH 8192 1 # control: replies OK NFSPOC_SRCPORT=821 NFSPOC_TIMEOUT=5 /root/nfspoc readdir $FH 0 1 # trigger: count=0
Expected
- Control READDIR (count=8192):
reply OKin <1ms. - Trigger READDIR (count=0): TIMEOUT β no reply ever; the servicing nfsd kernel thread spins forever inside nfsrv_readdir holding slp->ns_token.
- Consequences observed on the stock INVARIANTS guest:
- userland console writer starves immediately after the trigger (serial log goes silent mid-script),
- sshd flaps (rc=124 "Connection timed out during banner exchange" / rc=255)
and then stops answering entirely while
vm.sh statusstill saysup, - in the first run the guest became fully unresponsive (
vm.sh status=> down; no panic text on serial console β hard livelock, not a crash). - One UDP request pins one nfsd thread; N requests pin all N.
Root cause chain
nfsrv_readdirsys/vfs/nfs/nfs_serv.c:2977-2984 βcntandsiz = roundup2(cnt, DIRBLKSIZ)are taken verbatim from the wire; count=0 is NOT rejected.fullsiz = siz = 0,rbuf = kmalloc(0).io.uio_resid = 0β the EOF short-cut at nfs_serv.c:3066-3095 (if (io.uio_resid) { siz -= ...; if (siz == 0) { reply-eof; } }) is skipped β the eof reply is only reachable when resid > 0.- UFS
ufs_readdir(sys/vfs/ufs/ufs_vnops.c:1612-1636,1647-1704) with resid 0:vop_write_dirent(sys/kern/vfs_subr.c:2566-2568) returns 1 ("doesn't fit") without setting *error; cookie_index stays 0 but*a_cookiesstays non-NULL (kmalloc'd for ncookies=1), uio_offset is unchanged, and ufs_readdir returns 0 ("success", eofflag=0). - Back in nfsrv_readdir: cookies non-NULL (no NFSERR_PERM), error 0,
cpos >= cend(siz==0) β nfs_serv.c:3120-3124toff = off; siz = fullsiz; goto again;β identical state, identical result: infinite loop. - Same construction in
nfsrv_readdirplus(dircount field = 0): nfs_serv.c:3277-3286, 3356-3382, 3407-3411.
Notes
- The nfsd thread spins while holding
slp->ns_token, so the whole NFS socket service dies with it; the unbounded kernel-mode spin starves userland. - Reachable by any client the export allows (AUTH_SYS uid is irrelevant β
READDIR only needs VEXEC via nfsrv_access). With
vfs.nfs.nfs_privport=1(guest default) the source port must be < 1024 β trivial for a remote attacker (root on their own host).
DF-2993 β VERDICT
Status: REPRODUCED (impact: dos, confidence: certain, attempts: 2 independent runs)
What was claimed
nfsrv_readdir()/nfsrv_readdirplus() spin forever when the client-supplied
count (v3 READDIR dircount path / v2 count) is 0, wedging an nfsd kernel
thread β remote denial of service.
How it was proven
Guest: DragonFly 6.5-DEVELOPMENT #0 (stock INVARIANTS kernel, KVM), NFS server (nfsd -u -t -n 4) exporting /tmp/nfsroot -maproot=root.
Raw SUNRPC/UDP client (nfspoc.c, unprivileged compile, reserved source port
because vfs.nfs.nfs_privport=1):
1. mount via mountd RPC -> valid 32-byte fh (mountd port discovered via
rpcbind GETPORT).
2. Control: v3 READDIR count=8192 -> reply OK in <1ms (proves fh accepted,
normal path healthy).
3. Trigger: v3 READDIR count=0 -> no reply, ever.
4. Consequences: the console-writing subshell starves mid-script (serial log
silent right after the trigger), ssh flaps (Connection timed out during
banner exchange, rc=124/255) and then stops answering; in run 1 the guest
became fully unreachable (vm.sh status => down). No panic on serial β
hard livelock.
Why it happens (code path, line-accurate)
- sys/vfs/nfs/nfs_serv.c:2977-2984 β count/dircount from the wire; 0 accepted; siz = roundup2(0, 512) = 0, fullsiz = 0, rbuf = kmalloc(0).
- nfs_serv.c:3045 VOP_READDIR with uio_resid = 0.
- sys/kern/vfs_subr.c:2566-2568 β vop_write_dirent returns 1 on
len > uio_residwithout setting *error. - sys/vfs/ufs/ufs_vnops.c:1612-1620 β cookies = kmalloc(ncookies=1) (non-NULL); 1683-1690 β first dirent does not fit, retval=1 break, cookie_index stays 0; 1697 β uio_offset unchanged; 1705-1723 β error==0 so the success branch stores *a_cookies = cookies (non-NULL), returns 0, eofflag=0.
- nfs_serv.c:3047-3048 β cookies non-NULL, so the NFSERR_PERM guard passes.
- nfs_serv.c:3066 β
if (io.uio_resid): false (resid is 0), so the "nothing read => reply eof" short-cut at 3073-3094 is unreachable. - nfs_serv.c:3101-3102 β cpos == cend (siz==0).
- nfs_serv.c:3120-3124 β
toff = off; siz = fullsiz; goto again;β identical state, identical result: infinite loop. - nfsrv_readdirplus identical construction (nfs_serv.c:3277-3286, 3356-3382, 3407-3411).
Exploit chain
n/a (pure DoS): single ~120-byte UDP datagram (after mountd/portmap probes) per nfsd thread; with the export reachable, an unauthenticated client (AUTH_SYS uid irrelevant; reserved source port trivial for the attacker's own host) pins every server thread and starves the host's userland.
Fix validation
See fix.diff / fix_all_three.diff β siz <= 0 (count==0) rejected with
NFSERR_TOOSMALL before the retry loop in both functions. Validated on a
rebuilt kernel: the identical READDIR count=0 request now returns an immediate
error reply and the guest stays fully responsive (see run.fixed.log /
fix section in manifest.json).
Fix verification
fixedPatched kernel returns an immediate NFSERR_TOOSMALL reply for READDIR count=0 and stays fully responsive; normal READDIR unchanged. Bad behavior gone.
['run.fixed.log']
Confirmed kernel references
Detail
Exploit chain
mountd/portmap probes to obtain the 32-byte dir fh -> one ~120-byte UDP READDIR(count=0) per nfsd thread -> all server threads spin forever in kernel mode -> NFS socket service dead + host userland starvation.
Evidence (decisive lines)
["run.log β two runs: control READDIR(count=8192) 'reply OK', trigger READDIR(count=0) no reply; ssh rc=124/255 progression; vm.sh status down (run 1)", "serial console in run 2: 'NULL: reply 24 bytes in 0.0001s' then '--- TRIGGER readdir count=0 ---' then total silence", 'VERDICT.md β full line-accurate root-cause chain (nfs_serv.c:2977-2984, 3066-3095, 3120-3124; vfs_subr.c:2566-2568; ufs_vnops.c:1612-1636)', 'run.fixed.log β patched kernel returns an immediate error reply for count=0 and stays responsive']
PoC changes
wrote nfspoc.c from scratch (raw SUNRPC/UDP client: rpcbind GETPORT, mountd MNT, NFS v3 NULL/READDIR/LOOKUP/WRITE); no seed existed
Verified recommended fix
Reject count/dircount == 0 (siz <= 0) with NFSERR_TOOSMALL in nfsrv_readdir and nfsrv_readdirplus before the retry loop (fix.diff).
Verdict
A single NFSv3 READDIR (or READDIRPLUS) with count=0 from any export-allowed client drives nfsrv_readdir into an infinite kernel loop (the eof short-cut requires uio_resid>0 and vop_write_dirent returns success-with-zero-cookies on resid 0), permanently pinning an nfsd thread holding slp->ns_token; reproduced twice on the stock INVARIANTS guest β after the trigger the console writer starves, ssh flaps and dies, and in run 1 the whole guest became unresponsive with no panic signature (hard livelock).
No comments yet.