⬢ DragonFlyBSD Kernel Audit
← triage · dashboard
DF-3043

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3043 · 9 files
FileTypeDescriptionSize
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
README.md
↓ download 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.

VERDICT.md
↓ download raw

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

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_testable
↓ fix.diffper-fix-DF-3043

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