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

nfs_getcookie() OOB array index via int truncation of 64-bit directory offset β€” local panic

Summary

nfs_subs.c:1341 pos=(uoff_t)off/NFS_DIRBLKSIZ(4096) β€” uint64 quotient. :1342 if(pos==0||off<0) MISSES the truncation wrap. pos is int (line 1338). off=(2^31+1)*4096: quotient=2147483649 cast to int=-2147483647. :1349 pos-- = INT_MIN. :1360 while(pos>=NFSNUMCOOKIES) skipped (negative). :1376 if(pos>=dp->ndm_eocookie) skipped. :1382 return(&dp->ndm_cookies[pos]) with hugely negative index = wild pointer. Callers (nfs_readdirrpc_uio :2518-2519 nfs_readdirplusrpc_uio :2965) immediately cookie=*cookiep = page fault panic. vn_seek only rejects negative for VDIR. nfs_bioread no upper-bound on uio_offset for VDIR. Trigger: lseek(nfs_dir_fd, (2^31+1)*4096, SEEK_SET) then getdents. Unprivileged local user with read access to non-empty NFS directory. Fix: if(upos>INT_MAX) return NULL before cast.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0810 Β· 12 files
FileTypeDescriptionSize
nfs_getcookie_oob.c trigger-source NFS readdir OOB index trigger: partial getdents + lseek to (2^31+1)*4096 + getdents 4.4 KB view raw
build.sh build-script cc -O2 -Wall -o nfs_getcookie_oob nfs_getcookie_oob.c 108 B view raw
run.sh run-script ./nfs_getcookie_oob /mnt/bigdir 519 B view raw
VERDICT.md verdict Full analysis: mechanism, trigger path, impact, fix 4.9 KB ↓ raw
README.md readme Build/run instructions and NFS setup 1.1 KB ↓ raw
panic.txt panic-signature Fatal trap 12 page fault in nfs_readdirrpc_uio+0xa4 (wild pointer deref) 656 B view raw
fix.diff suggested-fix Bound-check uoff_t quotient against INT_MAX before truncating to int 555 B view raw
fix_build.log build-log Single-fix kernel build output (rc=0) 5.6 MB ↓ download
fix_run.log run-log Before/after PoC runs: baseline panics, patched does not 1.1 KB view raw
env.txt environment uname, kern.version, cc version, NFS mount 500 B view 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 Build/run instructions and NFS setup
↓ download raw

DF-0810 β€” nfs_getcookie() OOB array index via int truncation

Summary

nfs_getcookie() (sys/vfs/nfs/nfs_subs.c:1341) computes a cookie array index by truncating a 64-bit directory offset quotient to int. A large seek offset on an NFS directory causes a wild pointer dereference β†’ kernel panic.

Build

./build.sh

Run (requires NFS setup)

Prerequisites (root setup, acceptable per audit realism test):

service rpcbind onestart
service mountd onestart
service nfsd onestart
echo "/nfsroot -maproot=root -network 127.0.0.0 -mask 255.0.0.0" > /etc/exports
mkdir -p /nfsroot/bigdir
(cd /nfsroot/bigdir && i=1; while [ $i -le 500 ]; do touch f$i; i=$((i+1)); done)
chown -R maxx:maxx /nfsroot/bigdir
mount_nfs -3 127.0.0.1:/nfsroot /mnt
# Do NOT ls /mnt/bigdir β€” that would set n_direofoffset and block the trigger

Run as maxx:

./run.sh

Expected behavior

  • Unpatched kernel (#0): kernel panic β€” Fatal trap 12: page fault in nfs_readdirrpc_uio+0xa4. Guest dies.
  • Patched kernel (#1): getdents returns 0, guest stays up. No panic.
VERDICT.md verdict Full analysis: mechanism, trigger path, impact, fix
↓ download raw

DF-0810 β€” nfs_getcookie() OOB array index via int truncation of 64-bit dir offset

Verdict: REPRODUCED (kernel panic / local DoS) β€” fix VALIDATED

Mechanism

In sys/vfs/nfs/nfs_subs.c, nfs_getcookie() (line 1335) computes a cookie array index by truncating a 64-bit directory offset quotient to int:

int pos;                                   // line 1339
pos = (uoff_t)off / NFS_DIRBLKSIZ;         // line 1341 β€” 64-bit quotient truncated to int

off is off_t (64-bit signed), coming from uiop->uio_offset which is set from the file descriptor's seek position. NFS_DIRBLKSIZ is 4096.

For off = (2^31 + 1) * 4096 = 8796093026304: - (uoff_t)off / 4096 = 2147483649 (0x80000001) - Truncated to int: -2147483647 - pos-- β†’ -2147483648 (INT_MIN)

The subsequent bounds checks all use signed comparison and are bypassed: - while (pos >= NFSNUMCOOKIES=31) β€” INT_MIN < 31 β†’ skipped - if (pos >= dp->ndm_eocookie) β€” INT_MIN < any positive eocookie β†’ skipped

The function returns &dp->ndm_cookies[INT_MIN] β€” a wild pointer ~17 GB before the nfsdmap struct (offset = INT_MIN Γ— 8 = βˆ’17179869184).

The caller nfs_readdirrpc_uio() (nfs_vnops.c:2517–2519) immediately dereferences it:

cookiep = nfs_getcookie(dnp, uiop->uio_offset, 0);
if (cookiep)
    cookie = *cookiep;     // dereference of wild pointer β†’ page fault

Trigger path (unprivileged)

  1. An admin has mounted a loopback NFS export and chowned a directory with >4096 bytes of entries to the unprivileged user (acceptable precondition per audit realism test).
  2. The user opens the NFS directory, does a partial getdents() (fills the cookie cache list without reaching EOF, keeping np->n_direofoffset == 0).
  3. lseek(fd, (2^31+1)*4096, SEEK_SET) β€” vn_seek() (vfs_vnops.c:1341) only rejects negative offsets for VDIR; the large positive offset is accepted.
  4. getdents() β†’ VOP_READDIR β†’ nfs_readdir β†’ nfs_bioread: - n_direofoffset == 0 β†’ EOF gate at nfs_bio.c:285 passes - nfs_getcacheblk(vp, wild_offset) β†’ nfs_doio β†’ nfs_readdirrpc_uio - nfs_getcookie(np, wild_offset, 0) β†’ returns wild pointer - cookie = *cookiep β†’ PAGE FAULT β†’ kernel panic

Panic signature (unpatched #0 kernel)

Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0xfffff7fc4f2971d4
fault code               = supervisor read data, page not present
instruction pointer      = 0x8:0xffffffff80811254
Stopped at      nfs_readdirrpc_uio+0xa4:        movl    (%rax),%ebx

The faulting instruction movl (%rax),%ebx is the cookie = *cookiep dereference. The fault address 0xfffff7fc4f2971d4 is in unmapped kernel space, consistent with a wild pointer 17 GB before the nfsdmap allocation.

Impact

Local DoS (kernel panic). An unprivileged user with read access to a non-empty NFS-mounted directory can panic the kernel. The precondition (NFS mount + readable directory) is realistic for multi-user systems with NFS home directories.

This is a read-dereference of a wild pointer β€” the dereference always hits unmapped memory (the offset is ~17 GB), so it manifests as a panic rather than an exploitable read/write. The add=1 write path (nfs_vnops.c:2699) is unreachable because the add=0 read-dereference at line 2518 panics first.

Exploit chain

Not applicable β€” this is a wild-pointer read dereference that always panics (the computed address is always unmapped). There is no write primitive derivable from this specific trigger path (the add=1 write to the wild pointer at nfs_vnops.c:2699 is unreachable because the add=0 read at line 2518 panics first). Impact ceiling: local DoS.

Fix

Compute the quotient as uoff_t and reject any offset whose block index exceeds INT_MAX before truncating:

uoff_t upos = (uoff_t)off / NFS_DIRBLKSIZ;
if (upos > (uoff_t)INT_MAX) {
    return (NULL);   // caller returns NFSERR_BAD_COOKIE β€” graceful error
}
pos = (int)upos;

When nfs_getcookie returns NULL, the caller returns NFSERR_BAD_COOKIE, which triggers the existing "got bad cookie" recovery in nfs_bioread (re-reads directory from the beginning). No panic.

Fix Validation

  • Baseline (#0 unpatched): PoC panics the kernel (page fault in nfs_readdirrpc_uio+0xa4, wild pointer dereference).
  • Patched (#1 single-fix kernel): PoC exits cleanly (getdents returns 0, guest stays up). Verified 3/3 runs β€” deterministic fix.

PoC changes

Authored nfs_getcookie_oob.c β€” the original finding had no PoC source. The trigger performs: (1) partial getdents to populate cookie cache without setting n_direofoffset, (2) lseek to the wild offset, (3) getdents to drive the dereference. The correct wild offset is (2^31+1)*4096 = 8796093026304 β€” NOT (2^31)*4096, which produces pos=INT_MIN and after pos-- wraps to INT_MAX (UB on x86), entering the while loop and returning NULL (NFSERR_BAD_COOKIE, no panic).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: nfs_getcookie_oob panics the unpatched #0 baseline (page fault in nfs_readdirrpc_uio+0xa4, fault VA 0xfffff7fc4f2971d4) and does NOT panic the single-fix #1 kernel (getdents returns 0, guest stays up). Fix closes the bug by returning NULL for offsets > INT_MAX blocks, causing the caller to return NFSERR_BAD_COOKIE instead of dereferencing a wild pointer.

baseline (#0): Fatal trap 12 page fault, fault VA 0xfffff7fc4f2971d4, Stopped at nfs_readdirrpc_uio+0xa4: movl (%rax),%ebx, db> prompt (guest dead). patched (#1): getdents returned 0 (errno=0), guest UP, no panic (3/3 runs).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Fri Jul 10 19:39:22 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

Not applicable (read-dereference of a wild pointer that always hits unmapped memory). The computed address is dp + 0x14 + INT_MIN8 = dp - 17GB, which is always unmapped in kernel space, so it manifests as a deterministic panic rather than an exploitable read or write. The add=1 write path (nfs_vnops.c:2699, cookiep = cookie) is unreachable because the add=0 read at line 2518 panics first. Impact ceiling: local DoS (kernel panic). No uid0 escalation derivable from this primitive.

Evidence (decisive lines)

BASELINE (#0 unpatched): SSH dies after step3 getdents. boot.log: 'Fatal trap 12: page fault while in kernel mode / fault virtual address = 0xfffff7fc4f2971d4 / fault code = supervisor read data, page not present / instruction pointer = 0x8:0xffffffff80811254 / Stopped at nfs_readdirrpc_uio+0xa4: movl (%rax),%ebx / db>'. PATCHED (#1): step3 getdents returns 0, guest stays UP, no panic. Verified 3/3 runs deterministic.

PoC changes

Authored nfs_getcookie_oob.c from scratch (no prior PoC source existed). The trigger performs: (1) partial getdents on a large NFS directory to populate the cookie cache without setting n_direofoffset (critical: the directory must NOT be pre-read to EOF by e.g. 'ls', or n_direofoffset blocks the trigger), (2) lseek to (2^31+1)4096 = 8796093026304, (3) getdents to drive the wild pointer dereference. Initial attempt used (2^31)4096 = 8796093022208 which produces pos=INT_MIN; after pos-- (signed overflow UB on x86, wraps to INT_MAX) the while(pos>=31) loop is entered and returns NULL (NFSERR_BAD_COOKIE, no panic). The correct offset is (2^31+1)*4096 which gives pos=-2147483647, pos-- = INT_MIN (representable), bypasses all checks.

Verified recommended fix

In nfs_getcookie() at sys/vfs/nfs/nfs_subs.c:1341, compute the quotient as uoff_t and reject offsets whose block index exceeds INT_MAX before truncating: 'uoff_t upos = (uoff_t)off / NFS_DIRBLKSIZ; if (upos > (uoff_t)INT_MAX) return (NULL); pos = (int)upos;'. When nfs_getcookie returns NULL, callers return NFSERR_BAD_COOKIE which triggers the existing graceful recovery in nfs_bioread. Supersedes finding proposal (the finding summary's suggested 'if(upos>INT_MAX) return NULL' matches this fix). Full git-apply-able diff in findings/poc/DF-0810/fix.diff.

Verdict

REPRODUCED. nfs_getcookie() (sys/vfs/nfs/nfs_subs.c:1341) computes a cookie array index by truncating a 64-bit directory offset quotient to int. An unprivileged user with read access to a non-empty NFS-mounted directory (>4096 bytes of entries) can: (1) partial getdents to populate the cookie cache without setting n_direofoffset, (2) lseek to (2^31+1)4096 = 8796093026304 (vn_seek at vfs_vnops.c:1341 only rejects negative for VDIR), (3) getdents drives nfs_bioread -> nfs_doio -> nfs_readdirrpc_uio -> nfs_getcookie(np, wild_off, 0). The quotient 0x80000001 truncates to int -2147483647, pos-- = INT_MIN, all signed bounds checks are bypassed (INT_MIN < 31, INT_MIN < eocookie), and &dp->ndm_cookies[INT_MIN] is returned -- a wild pointer 17GB before the nfsdmap. The caller dereferences it (cookie = cookiep, nfs_vnops.c:2518) -> page fault -> kernel panic. Confirmed by Fatal trap 12 in nfs_readdirrpc_uio+0xa4 (movl (%rax),%ebx), fault VA 0xfffff7fc4f2971d4.