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