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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2993 Β· 12 files
FileTypeDescriptionSize
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 OK in <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 status still says up,
  • 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

  1. nfsrv_readdir sys/vfs/nfs/nfs_serv.c:2977-2984 β€” cnt and siz = roundup2(cnt, DIRBLKSIZ) are taken verbatim from the wire; count=0 is NOT rejected. fullsiz = siz = 0, rbuf = kmalloc(0).
  2. 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.
  3. 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_cookies stays non-NULL (kmalloc'd for ncookies=1), uio_offset is unchanged, and ufs_readdir returns 0 ("success", eofflag=0).
  4. Back in nfsrv_readdir: cookies non-NULL (no NFSERR_PERM), error 0, cpos >= cend (siz==0) β‡’ nfs_serv.c:3120-3124 toff = off; siz = fullsiz; goto again; β€” identical state, identical result: infinite loop.
  5. 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).
VERDICT.md
↓ download raw

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_resid without 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel returns an immediate NFSERR_TOOSMALL reply for READDIR count=0 and stays fully responsive; normal READDIR unchanged. 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

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