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)
PoC verification
Evidence pack
findings/poc/DF-0779 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 throughkern_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_localon a tmpfs dir with cookies0xDEAD,0x7FFFβ¦FF,0x4141β¦41,0x0BADF00Dall 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.1or 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
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)
- An NFS client sends a READDIR RPC whose 64-bit cookie is attacker-chosen and bogus (does not map to any live tmpfs dirent).
nfsrv_readdir()decodes it raw and unvalidated:toff = fxdr_hyper(tl);(sys/vfs/nfs/nfs_serv.c:2963), thenio.uio_offset = (off_t)off;(nfs_serv.c:3035), thenVOP_READDIR(vp, &io, cred, &eofflag, &ncookies, &cookies);(nfs_serv.c:3045) β non-NULL cookies, so tmpfs's NFS cookie-generation block runs.- In
tmpfs_readdir()the bogusstartoffis captured (tmpfs_vnops.c:1662,off = startoffat:1699).tmpfs_dir_getdents()(tmpfs_vnops.c:1684) handles the bogus cookie gracefully: its inexacttmpfs_dir_lookupbycookie(node, startcookie, 0)(tmpfs_subr.c:891) returns NULL for a cookie below all dirent cookies, so it snapsuio_offset = TMPFS_DIRCOOKIE_EOFand returns 0 (tmpfs_subr.c:892-895). - Back in the cookie block,
cnt == 0so the cookie loop (for (i = 0; i < cnt; i++),tmpfs_vnops.c:1705) never executes;offis still the bogusstartoffβuio_offset == EOF. - The trailing
KKASSERT(uio->uio_offset == off);(tmpfs_vnops.c:1730) fires β panic. (For a cookie whosetmpfs_cookiedir()lands between two real dirents,tmpfs_dir_getdentsreads from the predecessor and the block's first iteration tripsKKASSERT(de != NULL)at:1719instead. 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
fixedVALIDATED. 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.
Confirmed kernel references
- sys/vfs/tmpfs/tmpfs_vnops.c:1662
- sys/vfs/tmpfs/tmpfs_vnops.c:1697
- sys/vfs/tmpfs/tmpfs_vnops.c:1699
- sys/vfs/tmpfs/tmpfs_vnops.c:1719
- sys/vfs/tmpfs/tmpfs_vnops.c:1730
- sys/vfs/tmpfs/tmpfs_subr.c:891
- sys/vfs/tmpfs/tmpfs_subr.c:822
- sys/vfs/nfs/nfs_serv.c:2963
- sys/vfs/nfs/nfs_serv.c:3035
- sys/vfs/nfs/nfs_serv.c:3045
- sys/kern/vfs_syscalls.c:4645
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.
No comments yet.