# 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`:

```c
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:
```c
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:

```c
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).
