# DF-3031 — ext2_readdir never updates the caller's `*a_ncookies` (missing `*` deref)

**Severity:** Low (latent uninitialized-kernel-heap disclosure / VOP contract violation)
**Class:** CWE-628 (incorrect API contract) + latent CWE-908 (uninitialized memory exposure)
**File:** `sys/vfs/ext2fs/ext2_lookup.c:262` (function dispatched from
`sys/vfs/ext2fs/ext2_vnops.c:2142` — `.vop_readdir = ext2_readdir`)
**Confidence:** certain (the defect is objectively wrong vs. its own intent and every
sibling FS; impact ceiling is honestly latent — see below)

## Summary

`ext2_readdir` allocates a cookie array sized for the worst case, reports the
**allocated** count to the caller, fills only part of it, and then tries to
subtract the unfilled remainder — but forgets the `*`, so the caller's count is
never corrected:

```c
/* ext2_lookup.c:171-175 — allocation is NOT zeroed */
ncookies = ncookies / (offsetof(struct ext2fs_direct_2, e2d_namlen) + 4) + 1;
cookies = malloc(ncookies * sizeof(*cookies), M_TEMP, M_WAITOK);
*ap->a_ncookies = ncookies;          /* caller sees the ALLOCATED count */
*ap->a_cookies = cookies;
...
/* ext2_lookup.c:241-246 — one cookie filled per dirent actually written */
...
/* ext2_lookup.c:260-262 */
if (ap->a_ncookies != NULL) {
        if (error == 0) {
                ap->a_ncookies -= ncookies;   /* BUG: pointer arithmetic on the
                                                  arg pointer; should be
                                                  *ap->a_ncookies -= ncookies */
```

Every other DragonFly filesystem dereferences here:
`sys/vfs/msdosfs/msdosfs_vnops.c:1738` (`*ap->a_ncookies -= ncookies`),
`sys/vfs/ufs/ufs_vnops.c:1634` (`*ap->a_ncookies = cookie_index`),
hammer/hammer2/tmpfs all set the used count.  The typo corrupts only the local
copy of the `int *` (harmless in itself) while leaving the caller's `ncookies`
at the full allocated value (e.g. `uio_resid/10 + 1`), of which only
`allocated - remaining` slots were initialized.

## Impact

* The cookies array is `malloc`'d **without M_ZERO** (ext2_lookup.c:173), so
  the unfilled tail is stale kernel heap.  Any consumer that iterates
  `cookies[0 .. ncookies)` (as the contract entitles it to) reads
  uninitialized kernel memory — for the only in-tree cookie consumer (the NFS
  server, `sys/vfs/nfs/nfs_serv.c:3045` and `:3337`) that would turn into a
  kernel-heap disclosure over the network in READDIR replies.
* Today's in-tree consumers are *coincidentally* safe: both nfsrv loops pair
  each cookie with a dirent and are bounded by `cpos < cend` where `cend`
  tracks the bytes actually written (nfs_serv.c:3066-3067, 3101-3102), so they
  never touch the uninitialized tail.  The over-report additionally makes the
  stale-cookie recovery check `ncookies == 0 → retry` (nfs_serv.c:3120, 3407)
  unreachable for ext2, degrading NFS READDIR restart correctness on ext2.
* Net: contract violation + latent heap-info-leak/OOB read primitive for any
  current or future consumer that trusts the count.  Filed Low because no
  live disclosure exists in-tree today.

## Fix

One character: dereference the pointer.

```diff
--- a/sys/vfs/ext2fs/ext2_lookup.c
+++ b/sys/vfs/ext2fs/ext2_lookup.c
@@ -259,7 +259,7 @@
 	if (error == EJUSTRETURN)
 		error = 0;
 	if (ap->a_ncookies != NULL) {
 		if (error == 0) {
-			ap->a_ncookies -= ncookies;
+			*ap->a_ncookies -= ncookies;
 		} else {
 			free(*ap->a_cookies, M_TEMP);
```

(Applying `M_ZERO` to the malloc at :173 is a reasonable belt-and-suspenders
addition, but the deref fix restores the contract.)

## Verification status

Verification skipped by policy (Low severity / latent impact — no Critical/High,
memcorrupt, privesc, or trivially-runnable local trigger).  A runtime
demonstration would require exporting an ext2 filesystem over loopback NFS in
the single-tenant guest and instrumenting the server; even then the in-tree
consumer's dirent-bounded loop would show no visible misbehavior — the defect
is a code-level contract violation proven by inspection above.  See
`VERDICT.md` for the full static proof.
