nm_bioqlen async-bio accounting leaks on every nfs_startio early-exit and underflows on commit→write reissue — permanent mount-wide loss of NFS read-ahead from a malicious/oversized-server attribute claim (and loss of the async throttle)
| Field | Value |
|---|---|
| ID | DF-3043 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-401 |
| File | sys/vfs/nfs/nfs_bio.c |
| Lines | 957 vs nfs_iod.c:139/212; leak sites :1346-1373, :1463-1478, :1687-1693, nfsm_subs.c:826-836; underflow :1753-1755 |
| 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
nfs_asyncio() increments nmp->nm_bioqlen when queueing a bio, but the counter is decremented ONLY when a queued nfsreq completes. Every path where nfs_startio() completes the bio WITHOUT ever queueing an nfsreq leaks one count permanently: readrpc EFBIG early-out, writerpc degenerate-dirty-range and EFBIG early-outs, commitrpc no-write-verifier early-out, and nfsm_request_bio initial-transmission early abort. Conversely, when a COMMIT fails and nfs_commitrpc_bio_done chains nfs_writerpc_bio for the SAME bio, a second request is queued whose completion decrements again after the commit request already decremented — one increment, two decrements → counter underflow. Once nm_bioqlen saturates at min(nfs_maxasyncbio=64, nm_maxasync_scaled/256), nfs_asyncok() returns 0 forever (nothing resets the counter short of unmount), permanently disabling read-ahead for the entire mount; underflow makes the throttle ineffective in the other direction. An unpriv local user against a malicious (or pathologically oversized) NFS server: a plain stat() applies the server-claimed size to n_size (server claims n_size > nm_maxfilesize — itself server-negotiable via FSINFO); a sequential read at the last legal block then makes the read-ahead block pass the n_size gate and be queued, where readrpc's EFBIG check biodone's it without decrement — up to min(readahead, seqcount) leaks per read; ~64 such reads permanently kill read-ahead mount-wide (benign perf degradation). Speculative adjacent hazard sharing the root cause: nfs_asyncok's state check racing nfssvc_iod_stop1 during umount -f can orphan a locked bp on nm_bioq and wedge vflush (microsecond window). Not built (Low/perf-only per contract). Fix: balance the counter at every bypass site + re-increment on the commit→write chain (row diff).
Timeline
- 2026-09-02 Discovered during pass-2 audit of nfs_bio.c (GLM 5.3). DF-0799 re-verified, not re-reported (its dirtyend underflow family re-traced: all guards convert to skip-the-write, data-loss only).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3043 · 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | — | 347 B | ↓ raw | |
| VERDICT.md | — | 1.4 KB | ↓ raw | |
| df3043.c | — | 2.0 KB | view raw | |
| env.txt | — | 248 B | view raw | |
| fix.diff | — | 129 B | view raw | |
| manifest.json | — | 804 B | view raw | |
| nfssrv3.c | — | 15.3 KB | view raw | |
| run.log | — | 297 B | view raw | |
| verdict.json | — | 1.8 KB | view raw |
DF-3043 - nm_bioqlen async-bio accounting leaks on nfs_startio early-exits; underflow on commit-to-write chain
Severity Low | verdict: not_reproduced | impact: none
Reproduce
Sources in this directory; build with cc -O2 -o <bin> <src> -pthread on the guest.
See VERDICT.md for the full narrative and run.log for the decisive output.
DF-3043 verification
Kernel: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
Not demonstrated end-to-end. The malicious-claims route (oversized GETATTR size + client-side EFBIG readahead leak) required the custom NFSv3 server rig which, after extensive bring-up (record-mark, fattr XDR and fh-length bugs fixed), still could not complete a functional client mount against the stock kernel in available time; the forced-unmount abort site (d) of this same accounting bug was exercised heavily under DF-3052 (126 early aborts observed) but nm_bioqlen itself is not userland-visible and the mount dies with the aborts, so the mount-wide read-ahead-death observable was never measurable. Accounting sites remain code-certain: increment only at nfs_bio.c:957, decrement only in nfs_iod.c:139/212, bypasses at nfs_bio.c:1346-1373/:1463-1478/:1687-1693 and the commit-to-write reissue at :1753-1755.
Kernel references
- sys/vfs/nfs/nfs_bio.c:957
- sys/vfs/nfs/nfs_bio.c:1346
- sys/vfs/nfs/nfs_bio.c:1753
- sys/vfs/nfs/nfs_iod.c:139
Fix
See fix.diff.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md + run.log + sources
PoC changes
adjusted from finding sketch to guest syscall/layout reality
Verified recommended fix
diff: decrement nm_bioqlen at every biodone-without-request bypass site and re-increment on the commit-to-write reissue chain.
Verdict
Not demonstrated end-to-end. The malicious-claims route (oversized GETATTR size + client-side EFBIG readahead leak) required the custom NFSv3 server rig which, after extensive bring-up (record-mark, fattr XDR and fh-length bugs fixed), still could not complete a functional client mount against the stock kernel in available time; the forced-unmount abort site (d) of this same accounting bug was exercised heavily under DF-3052 (126 early aborts observed) but nm_bioqlen itself is not userland-visible and the mount dies with the aborts, so the mount-wide read-ahead-death observable was never measurable. Accounting sites remain code-certain: increment only at nfs_bio.c:957, decrement only in nfs_iod.c:139/212, bypasses at nfs_bio.c:1346-1373/:1463-1478/:1687-1693 and the commit-to-write reissue at :1753-1755.
No comments yet.