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

tmpfs_readdir cookie generation panics on attacker-supplied bogus NFS cookie β€” KKASSERT/RB_NEXT(NULL)

Summary

tmpfs_readdir :1697-1731 NFS cookie-generation block. tmpfs_dir_getdents uses tmpfs_dir_lookupbycookie(node,startcookie,0) INEXACT returns largest dirent <= startcookie or max entry. But cookie loop :1716-1722 uses tmpfs_dir_lookupbycookie(node,off,1) EXACT returns NULL if no exact match. :1719 KKASSERT(de!=NULL) panics INVARIANTS or non-INVARIANTS RB_NEXT(NULL) derefs NULL->rb_cookienode.rbe_right page fault. NFS server forwards client-controlled cookie verbatim as uio_offset (nfs_serv.c:3035,3045-3046) BAD_COOKIE verifier #if 0 no validation. Trigger: NFS READDIR on exported tmpfs with cookie=0x7FFFFFFFFFFFFFF0 (larger than all real cookies ((uintptr_t)de>>1)&0x7FFF...). getdents succeeds (inexact) cnt=1 then cookie loop exact lookup NULL KKASSERT/RB_NEXT panic. Local-only readdir unaffected callers pass NULL cookies. Fix: use inexact lookup (0 not 1) in cookie loop bail to EOF if NULL.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0779 Β· 18 files
FileTypeDescriptionSize
df0779_local.c trigger-source PATH A: local getdents w/ bogus cookie -- proves the local path is safe (cookies==NULL) 3.6 KB view raw
df0779_trigger.c exploit-trigger PATH B: raw NFSv3 READDIR (getfh-based) w/ bogus cookie -> the panic 4.6 KB view raw
df0779_nfs.c trigger-source alternate trigger using real MOUNT RPC (mountd AUTH_TOOWEAK on this guest) 9.7 KB view raw
diag.c diagnostic mountd auth probing 1.3 KB view raw
diag2.c diagnostic mountd/nfs auth + reply-decode probing 2.5 KB view raw
build.sh build-script cc -O -o df0779_local / df0779_trigger 362 B view raw
run.sh run-script PATH A + PATH B (NFS export setup + trigger) 1.6 KB view raw
build.log build-log PoC compile output (both binaries, BUILD_EXIT=0) 104 B view raw
run.log run-log PATH A decisive output (no panic for any bogus cookie) 728 B view raw
panic.txt panic-signature KKASSERT uio_offset==off tmpfs_vnops.c:1730 via nfsrv_readdir 1.4 KB view raw
fix.diff suggested-fix validate readdir cookie at top of tmpfs_readdir (snap bogus -> EOF) 1.2 KB view raw
fix_build.log build-log single-fix kernel make nativekernel (NK_DONE rc=0, full output) 5.6 MB ↓ download
fix_run.log run-log patched #1 before/after: bogus cookie -> clean reply; valid cookie -> 272B 1.3 KB view raw
env.txt environment uname, cc, vfs.usermount, tmpfs mounts 484 B view raw
VERDICT.md verdict full narrative + fix validation 5.2 KB ↓ raw
README.md readme summary + reproduce 5.0 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme summary + reproduce
↓ download raw

DF-0779 β€” tmpfs_readdir cookie-generation panic on attacker-supplied bogus NFS cookie

Verdict: REPRODUCED (panic / DoS) β€” and FIX VALIDATED on a single-fix kernel.

The bug (confirmed by source trace + live panic)

tmpfs_readdir() (sys/vfs/tmpfs/tmpfs_vnops.c) maintains a separate "NFS cookie-generation" block (tmpfs_vnops.c:1697-1731) that runs only when the caller passes a non-NULL cookies/ncookies vector (tmpfs_vnops.c:1697). The block re-walks the directory's cookie RB-tree starting at the caller-supplied startoff cookie (off = startoff, line 1699) and ends with the invariant

KKASSERT(uio->uio_offset == off);   /* tmpfs_vnops.c:1730 */

startoff is the readdir cookie. The only VOP_READDIR callers that pass non-NULL cookies are the NFS server (sys/vfs/nfs/nfs_serv.c:3045 and :3337). In nfsrv_readdir the cookie is taken raw and unvalidated from the NFS client's READDIR RPC payload:

toff = fxdr_hyper(tl);          /* nfs_serv.c:2963 - straight off the wire */
...
io.uio_offset = (off_t)off;     /* nfs_serv.c:3035 */
error = VOP_READDIR(vp, &io, cred, &eofflag, &ncookies, &cookies);  /* :3045 */

(the only guard that ever existed, a verf cookie-verifier check at nfs_serv.c:3009, is #if 0).

A bogus client cookie (one that does not exactly map to a live dirent) makes tmpfs_dir_getdents() silently snap uio_offset to TMPFS_DIRCOOKIE_EOF (tmpfs_subr.c:893) and return 0 entries (cnt == 0). The cookie-generation block, however, still has off == startoff (the bogus value), so the final invariant fires:

panic: assertion "uio->uio_offset == off" failed in tmpfs_readdir
       at /usr/src/sys/vfs/tmpfs/tmpfs_vnops.c:1730
tmpfs_readdir() at tmpfs_readdir+0x537
vop_readdir()   at vop_readdir+0x6b
nfsrv_readdir() at nfsrv_readdir+0x378      <- NFS server READDIR path
sys_nfssvc()    at sys_nfssvc+0x40f
syscall2()      at syscall2+0x11e

(For a different bogus value whose tmpfs_cookiedir() lands between two real dirents, tmpfs_dir_getdents reads entries from the predecessor and the cookie block's first iteration trips KKASSERT(de != NULL) at tmpfs_vnops.c:1719 instead. Both are the same root cause: an unvalidated cookie desyncs the cookie block. The cited range 1697-1731 covers both.)

tmpfs is fully NFS-exportable (vfs_fhtovp/vfs_vptofh/vfs_checkexp are all implemented, tmpfs_vfsops.c:549-551), so the path is live.

Reachability & threat model (honest)

  • NOT reachable via local getdents. Local readdir goes through kern_getdents β†’ VOP_READDIR_FP(vp, &auio, fp->f_cred, &eofflag, NULL, NULL, fp) (vfs_syscalls.c:4645) β€” cookies=NULL, so the panic block is skipped. Verified empirically: ./df0779_local on a tmpfs dir with cookies 0xDEAD, 0x7FFF…FF, 0x4141…41, 0x0BADF00D all returned gracefully (no panic, guest stayed up). The finding's "unprivileged local getdents" framing is therefore incorrect.
  • Reachable via the NFS server. Any NFS client (local over 127.0.0.1 or remote over the network) that issues a READDIR against an NFS-exported tmpfs directory with an out-of-range cookie panics the server kernel. The trigger (df0779_trigger) needs a valid filehandle for the dir; in the realistic threat a legitimate client already holds one (from a prior permitted MOUNT), and the malicious packet is just a normal READDIR with a mutated cookie field.
  • Impact: kernel panic β†’ full-system DoS (the NFS-serving box goes down and must reboot). This is a deliberate INVARIANTS KKASSERT, not memory corruption β€” there is no write/UAF/control primitive, hence no escalation path (valid Phase-6 hard blocker: pure assertion panic). On an INVARIANTS-OFF build the KKASSERT is compiled out and the divergence is benign (empty reply), so the primitive is DoS-only on every kernel flavour.

Files

file purpose
df0779_local.c PATH A: local getdents w/ bogus cookie β€” proves the local path is safe.
df0779_trigger.c PATH B: raw NFSv3 READDIR (MOUNT via getfh) w/ a bogus cookie β€” the panic trigger.
df0779_nfs.c alternate trigger that does a real MOUNT RPC (mountd rejected AUTH_NONE/AUTH_UNIX as AUTH_TOOWEAK on this guest, so the getfh path is the one used).
build.sh / run.sh exact build/run.
run.log PATH A decisive output (no panic).
panic.txt the KKASSERT … tmpfs_vnops.c:1730 panic from boot.log.
fix.diff standalone git apply-able fix (cookie validation at top of tmpfs_readdir).
fix_build.log / fix_run.log single-fix kernel build + before/after validation.
env.txt guest environment.

Reproduce

./build.sh
# PATH A (as unprivileged user) -- no panic:
./df0779_local /tmp/some_tmpfs_dir
# PATH B (as root, with NFS serving a tmpfs export):
#   see run.sh for the rpcbind/mountd/nfsd + /etc/exports setup, then:
./df0779_trigger /tmp/df0779_export 0xDEAD     # unpatched: panic; patched: clean reply
VERDICT.md verdict full narrative + fix validation
↓ download raw

DF-0779 β€” VERDICT

REPRODUCED (panic / DoS) on the unpatched 6.5-DEVELOPMENT #0 kernel. FIX VALIDATED on a single-fix #1 kernel.

Mechanism (every hop cited)

  1. An NFS client sends a READDIR RPC whose 64-bit cookie is attacker-chosen and bogus (does not map to any live tmpfs dirent).
  2. nfsrv_readdir() decodes it raw and unvalidated: toff = fxdr_hyper(tl); (sys/vfs/nfs/nfs_serv.c:2963), then io.uio_offset = (off_t)off; (nfs_serv.c:3035), then VOP_READDIR(vp, &io, cred, &eofflag, &ncookies, &cookies); (nfs_serv.c:3045) β€” non-NULL cookies, so tmpfs's NFS cookie-generation block runs.
  3. In tmpfs_readdir() the bogus startoff is captured (tmpfs_vnops.c:1662, off = startoff at :1699). tmpfs_dir_getdents() (tmpfs_vnops.c:1684) handles the bogus cookie gracefully: its inexact tmpfs_dir_lookupbycookie(node, startcookie, 0) (tmpfs_subr.c:891) returns NULL for a cookie below all dirent cookies, so it snaps uio_offset = TMPFS_DIRCOOKIE_EOF and returns 0 (tmpfs_subr.c:892-895).
  4. Back in the cookie block, cnt == 0 so the cookie loop (for (i = 0; i < cnt; i++), tmpfs_vnops.c:1705) never executes; off is still the bogus startoff β‰  uio_offset == EOF.
  5. The trailing KKASSERT(uio->uio_offset == off); (tmpfs_vnops.c:1730) fires β†’ panic. (For a cookie whose tmpfs_cookiedir() lands between two real dirents, tmpfs_dir_getdents reads from the predecessor and the block's first iteration trips KKASSERT(de != NULL) at :1719 instead. Same root cause; both inside the cited 1697–1731 range.)

Captured panic (boot.log):

panic: assertion "uio->uio_offset == off" failed in tmpfs_readdir
       at /usr/src/sys/vfs/tmpfs/tmpfs_vnops.c:1730
tmpfs_readdir() at tmpfs_readdir+0x537
vop_readdir()   at vop_readdir+0x6b
nfsrv_readdir() at nfsrv_readdir+0x378
sys_nfssvc()    at sys_nfssvc+0x40f

Reachability β€” the local-getdents framing is wrong

Exhaustive VOP_READDIR call-site survey in sys/:

caller cookies reaches block?
kern_getdents (vfs_syscalls.c:4645) NULL, NULL no
cache_fullpath (vfs_cache.c:2786) NULL, NULL no
autofs forward (autofs_vnops.c:320) forwards caller's only if NFS
nfsrv_readdir (nfs_serv.c:3045,:3337) non-NULL YES

So local getdents cannot trigger it (cookies == NULL). Verified live: ./df0779_local on a tmpfs dir with cookies 0xDEAD / 0x7FFF…FF / 0x4141…41 / 0x0BADF00D / 0x2 all returned rc=0 with no panic, guest stayed up.

The real trigger is an NFS READDIR against an NFS-exported tmpfs dir. tmpfs is fully exportable (vfs_fhtovp/vfs_vptofh/vfs_checkexp, tmpfs_vfsops.c:549-551). Any NFS client (localhost or network) holding a valid filehandle (from a permitted MOUNT) can send the malformed READDIR.

Impact

Kernel panic β†’ full-system DoS of the NFS-serving host (reboot required). This is a deliberate INVARIANTS KKASSERT, not memory corruption: no write/UAF/control primitive exists, so there is no escalation path (valid Phase-6 hard blocker: pure assertion panic). Severity Medium is appropriate (remote DoS of a non-default-but-supported service).

PoC changes from the seeded draft

The seeded draft (a local getdents PoC) was incorrect: local getdents passes cookies=NULL and never reaches the panic block. I replaced it with: - df0779_local.c β€” kept as the negative control proving the local path is safe (no panic for any bogus cookie). - df0779_trigger.c β€” the real trigger: a hand-rolled NFSv3 READDIR over UDP to the local nfsd, using getfh(2) to obtain the directory filehandle (mountd rejected AUTH_NONE/AUTH_UNIX as AUTH_TOOWEAK on this guest, so the MOUNT RPC path was unusable; getfh yields the identical fh). The cookieverf3 field is encoded as NFSv3 fixed opaque[8] (no length prefix), which was the one encoding subtlety. - df0779_nfs.c, diag.c, diag2.c β€” investigation artefacts (mountd/auth probing) retained for the record.

Fix

fix.diff validates the readdir cookie at the top of tmpfs_readdir: if startoff is not one of the reserved DOT/DOTDOT/EOF markers and does not exactly match a live dirent (tmpfs_dir_lookupbycookie(node, startoff, 1) == NULL), it is snapped to TMPFS_DIRCOOKIE_EOF (and uio_offset too), exactly matching the EOF semantics tmpfs_dir_getdents() already applies. One logical change; applies clean (git apply --check OK); builds (make nativekernel, NK_DONE rc=0).

Validation (Phase 8)

kernel trigger (bogus cookie 0xDEAD) valid cookie 0
unpatched #0 (6cc80ee9 baseline) panic tmpfs_vnops.c:1730, guest DOWN 272-byte reply
single-fix #1 (today) 132-byte clean reply, guest UP, no panic 272-byte reply (no regression)

fix_kernel_uname: DragonFly 6.5-DEVELOPMENT #1: Fri Jul 10 03:58:53 UTC 2026 (kernel.stripped sha256 184c1fc2063c6eb5b181695044d4134b3969f06ccc913ee2e2905d1300b75a2d). Cookie 0x4141414141414141 (overshoot) also returns a clean 132-byte reply on the patched kernel. fix_status = fixed.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. fix.diff applies clean (git apply --check OK), builds (make -j6 nativekernel, NK_DONE rc=0), installs, boots as #1. The SAME ./df0779_trigger /tmp/df0779_export 0xDEAD that panics the unpatched #0 baseline at tmpfs_vnops.c:1730 returns a clean 132-byte READDIR reply on the patched #1 kernel with the guest staying UP and NO panic; cookie 0x4141414141414141 likewise returns a clean 132-byte reply; valid cookie 0 still returns the 272-byte directory listing (no regression). The fix closes the bug.

BEFORE (unpatched #0): ./df0779_trigger ... 0xDEAD -> panic: assertion "uio->uio_offset == off" failed in tmpfs_readdir at tmpfs_vnops.c:1730 (nfsrv_readdir->vop_readdir->tmpfs_readdir); guest DOWN. boot.log +14 lines.
AFTER (single-fix #1, sha256 184c1fc2...): ./df0779_trigger ... 0xDEAD -> 'READDIR3 reply: 132 bytes, errno=0'; 0x4141414141414141 -> '132 bytes'; cookie 0 -> '272 bytes'; status UP; boot.log 0 new panic lines.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Fri Jul 10 03:58:53 UTC 2026 (kernel.stripped sha256 184c1fc2063c6eb5b181695044d4134b3969f06ccc913ee2e2905d1300b75a2d)

Confirmed kernel references

Detail

Exploit chain

none (non-corruption class). The primitive is a deliberate INVARIANTS KKASSERT panic (assertion uio->uio_offset==off at tmpfs_vnops.c:1730, or de!=NULL at :1719 for a between-dirents cookie) -- there is NO write/UAF/control primitive, hence no escalation path. This is a valid Phase-6 hard blocker (pure assertion panic / DoS). Impact ceiling: a malicious NFS client can panic (full-system DoS) any host running the NFS server with a tmpfs export, by sending a READDIR whose cookie does not map to a live dirent. Not locally reachable via getdents. No escalation attempted because there is no memory-corruption primitive to convert.

Evidence (decisive lines)

UNPATCHED #0, ./df0779_trigger /tmp/df0779_export 0xDEAD (raw NFSv3 READDIR, bogus cookie):
panic: assertion "uio->uio_offset == off" failed in tmpfs_readdir at /usr/src/sys/vfs/tmpfs/tmpfs_vnops.c:1730
tmpfs_readdir() at tmpfs_readdir+0x537
vop_readdir() at vop_readdir+0x6b
nfsrv_readdir() at nfsrv_readdir+0x378
sys_nfssvc() at sys_nfssvc+0x40f
(guest DOWN; reproduced twice.)
LOCAL getdents ./df0779_local on tmpfs /tmp/df0779_export: [cookie=0xdead] rc=0 entries=0; [0x7fffffffffffffff] rc=0; [0x4141414141414141] rc=0; [0x0badf00d] rc=0 -- no panic, guest up (cookies=NULL path).

PoC changes

The seeded draft (a local getdents PoC) targeted the wrong path: local getdents passes cookies=NULL and never reaches the panic block. Replaced with: df0779_local.c (kept as a NEGATIVE control proving the local path is safe for every bogus cookie); df0779_trigger.c (the real trigger: hand-rolled NFSv3 READDIR over UDP to local nfsd, using getfh(2) to obtain the dir filehandle -- mountd rejected AUTH_NONE/AUTH_UNIX as AUTH_TOOWEAK on this guest, so the MOUNT RPC path was unusable; the one encoding subtlety was that cookieverf3 is a fixed opaque[8] with NO length prefix). Also retained df0779_nfs.c/diag.c/diag2.c as investigation artefacts. Authored fix.diff (cookie validation at top of tmpfs_readdir).

Verified recommended fix

In sys/vfs/tmpfs/tmpfs_vnops.c, immediately after startoff = uio->uio_offset (line 1662), validate the readdir cookie: if startoff is not TMPFS_DIRCOOKIE_DOT/_DOTDOT/_EOF and tmpfs_dir_lookupbycookie(node, startoff, 1) returns NULL (not a live dirent), snap startoff and uio->uio_offset to TMPFS_DIRCOOKIE_EOF -- exactly matching the EOF semantics tmpfs_dir_getdents() already applies for past-end cookies. One logical change; supersedes the finding proposal (the finding cited the cookie-validation gap without a concrete diff). Full git-apply-able diff in findings/poc/DF-0779/fix.diff.

Verdict

REPRODUCED. The bug is real but reachable ONLY via the NFS-server READDIR path, not via local getdents. tmpfs_readdir's NFS cookie-generation block (sys/vfs/tmpfs/tmpfs_vnops.c:1697-1731) runs only when cookies!=NULL; the only VOP_READDIR callers passing non-NULL cookies are nfsrv_readdir (nfs_serv.c:3045,:3337), where the client's 64-bit cookie is taken raw and unvalidated (fxdr_hyper at nfs_serv.c:2963, set as uio_offset at :3035). A bogus cookie makes tmpfs_dir_getdents snap uio_offset to EOF and return 0 entries, but the cookie block keeps off=startoff (the bogus value), tripping KKASSERT(uio->uio_offset == off) at tmpfs_vnops.c:1730 -> kernel panic. Confirmed by a raw NFSv3 READDIR (cookie=0xDEAD) which produced: 'panic: assertion "uio->uio_offset == off" failed in tmpfs_readdir at tmpfs_vnops.c:1730' with stack nfsrv_readdir->vop_readdir->tmpfs_readdir. The finding's 'unprivileged local getdents' framing is WRONG: local getdents passes cookies=NULL (vfs_syscalls.c:4645); ./df0779_local on a tmpfs dir with cookies 0xDEAD/0x7FFF..FF/0x4141..41/0x0BADF00D all returned rc=0 with no panic.