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)
PoC verification
Evidence pack
findings/poc/DF-0810 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 faultinnfs_readdirrpc_uio+0xa4. Guest dies. - Patched kernel (#1):
getdentsreturns 0, guest stays up. No panic.
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)
- 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).
- The user opens the NFS directory, does a partial
getdents()(fills the cookie cache list without reaching EOF, keepingnp->n_direofoffset == 0). 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.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
fixedVALIDATED 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).
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.
No comments yet.