# DF-3031 — VERDICT

**Status: untested (verification skipped by policy — Low severity, latent impact)**
**Defect: certain. Impact: latent (no live in-tree OOB/leak — proven below).**

## The defect is real and certain

1. `sys/vfs/ext2fs/ext2_lookup.c:173-174`:
   `cookies = malloc(ncookies * sizeof(*cookies), M_TEMP, M_WAITOK);`
   (no M_ZERO) and `*ap->a_ncookies = ncookies;` — the caller is handed the
   **allocated** count.
2. `sys/vfs/ext2fs/ext2_lookup.c:241-247`: exactly one cookie slot is filled
   per `vop_write_dirent()` success; skipped entries (`e2d_ino == 0` or
   `offset < startoffset`, line 227-228) fill neither dirent nor cookie.
   The used count is therefore `allocated - ncookies_remaining`, strictly
   smaller than the allocation whenever any entry is skipped or the uio
   fills before the worst case.
3. `sys/vfs/ext2fs/ext2_lookup.c:262`: `ap->a_ncookies -= ncookies;`
   performs pointer arithmetic on the **local copy** of `int *a_ncookies`
   (advancing it `ncookies * sizeof(int)` bytes into nfsrv's frame — unused
   afterwards).  The caller's `ncookies` keeps the allocated value.
   Every sibling implementation writes through the pointer:
   `sys/vfs/msdosfs/msdosfs_vnops.c:1738`, `sys/vfs/ufs/ufs_vnops.c:1634`,
   `sys/vfs/hammer/hammer_vnops.c:1769`, `sys/vfs/hammer2/hammer2_vnops.c:767`,
   `sys/vfs/tmpfs/tmpfs_vnops.c` (cookie_index).  The vop contract (documented
   at `sys/vfs/nfs/nfs_serv.c:2890-2902`) is "VOP_READDIR() returns the number
   of valid cookies".

## Why the impact is only latent today

The only in-tree consumers are `nfsrv_readdir` (nfs_serv.c:3045) and
`nfsrv3_readdir` (nfs_serv.c:3337).  Both walk cookies strictly in lockstep
with dirents inside `while (cpos < cend && ncookies > 0)` loops
(nfs_serv.c:3112-3119/3141-3204 and 3399-3405/3445-3549) where `cend = rbuf +
siz` and `siz` was reduced by `io.uio_resid` (nfs_serv.c:3066-3067) — i.e.
bounded by the bytes ext2_readdir actually wrote.  Cookie reads therefore
never run past the filled region with the current server code.  Consequences
that *do* exist today: the stale-cookie retry guard `if (cpos >= cend ||
ncookies == 0)` (nfs_serv.c:3120, 3407) can never take its `ncookies == 0`
branch for ext2, and any future/refactored consumer that trusts the reported
count reads uninitialized kernel heap (`malloc`, no M_ZERO) straight into NFS
reply buffers — 8 stale bytes per over-counted cookie.

## Why we did not build a guest PoC

- No Critical/High, memcorrupt, privesc bucket; not a trivially-runnable local
  trigger (requires an NFS server fronting an ext2 mount).
- Even with loopback NFS in the guest, the in-tree consumer is
  dirent-bounded, so no externally observable misbehavior exists to observe;
  the demonstration would have to be a code-reading proof identical to the
  one above.

## Recommended fix

`*ap->a_ncookies -= ncookies;` (one character).  Optionally add `M_ZERO` at
the malloc.  See `fix.diff`.
