# 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

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

```c
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 through
  `kern_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_local` on a tmpfs dir with cookies
  `0xDEAD`, `0x7FFF…FF`, `0x4141…41`, `0x0BADF00D` all 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.1` or
  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

```sh
./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
```
