# DF-2996 VERDICT — nfs_sillyrename stores `sp` through an unvalidated nfsnode → wild write / VDIR cookie-list type confusion → deterministic kernel panic

**Status: REPRODUCED — impact: panic (kernel memory corruption, deterministic)**
Fix v2 validated on a rebuilt kernel: all malicious modes cleanly rejected,
honest mode fully functional, zero panics.

## Root cause (line-accurate)

`sys/vfs/nfs/nfs_vnops.c:3036-3038` (nfs_sillyrename, reached by any
`unlink()` of an open file — unpriv user, any NFS mount):

```c
	error = nfs_lookitup(dvp, sp->s_name, sp->s_namlen, sp->s_cred,
		cnp->cn_td, &np);
	np->n_sillyrename = sp;
	return (0);
```

Three independent defects in two lines:

1. **`np` is uninitialized** (`struct nfsnode *np;` at :2993) and the error
   return is ignored — on ANY lookup failure `np->n_sillyrename = sp` is a
   wild-pointer write through stale stack contents, and the error is
   swallowed (`unlink()` returns 0).
2. **On success with garbage non-NULL `np`**, `nfs_lookitup()` takes its
   `*npp != NULL` update branch (:3077-3086): it overwrites the file handle
   of whatever node the stale pointer selects (`bcopy` at :3084) and applies
   the server's attributes to that node — including type changes
   (`nfs_setvtype`, sys/vfs/nfs/nfs_subs.c:718-731).  Demonstrated: even an
   HONEST final LOOKUP reply crashes the stock kernel this way ("control"
   mode).
3. **On success with any server-chosen node**, identity and type are never
   validated: `n_sillyrename` (`n_un3.nf_silly`) **aliases the directory
   cookie LIST_HEAD** `n_un3.nd_cook` (sys/vfs/nfs/nfsnode.h:125-133).
   Storing `sp` on a VDIR node (server returns a directory fh, OR the
   object attrs retype the node VREG→VDIR — "typelie" mode, which needs no
   fh lie at all) plants `sp` as `n_cookies.lh_first`.

The crash sink — `nfs_reclaim()` cookie walk (sys/vfs/nfs/nfs_node.c:466-473):

```c
	if (vp->v_type == VDIR) {
		dp = np->n_cookies.lh_first;		/* = sp          */
		while (dp) {
			dp2 = dp;
			dp = dp->ndm_list.le_next;	/* *(sp+8)=s_dvp */
			kfree((caddr_t)dp2, M_NFSDIROFF); /* wrong zone  */
		}
	}
```

## Reproduction matrix (guest = DragonFly 6.5-DEVELOPMENT, INVARIANTS
X86_64_GENERIC; fake userland NFSv3 server in-guest; DF wire order for
LOOKUP replies as emitted by DragonFly's own nfsrv_lookup
[ fh ][ object attrs ][ dir attrs ] — sys/vfs/nfs/nfs_serv.c:652-656)

| kernel | server mode (final LOOKUP(".nfsXXX") reply) | result |
|---|---|---|
| stock | `err`: NFSERR_NOENT | silent wild write; `unlink` returns 0; no visible fault (stale slot benign that run) |
| stock | `dirfh`: root-directory fh | **PANIC at trigger exit, deterministic (4/4)** — `nfs_reclaim+0x150` |
| stock | `control`: honest file fh + honest attrs | **PANIC, identical signature** (uninit `np` → update-branch corruption) |
| fixed v1 | `err` | clean: `unlink` fails with the RPC error, sp freed, no crash |
| fixed v1 | `control` | clean: full silly-rename lifecycle, unmount clean |
| fixed v1 | `dirfh` / `freshdir` / `typelie` | **STILL PANICS** — v1 checked identity but not TYPE |
| fixed v2 | `err`/`dirfh`/`freshdir`/`typelie`/`control` | **ALL CLEAN** (run.fixed.log): malicious modes → clean error; control → normal lifecycle; no panic; mount usable |

Panic signature (identical every time; captured via serial console + DDB
`trace` through a socket serial):

```
Fatal user address access from kernel mode from trigger at ffffffff807f6[1b0|6aa0]
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x1
Stopped at nfs_reclaim+0x150: movq (%rdi),%r12

trace:
nfs_reclaim() at nfs_reclaim+0x150
vop_reclaim() at vop_reclaim+0x4b
vgone_vxlocked() at vgone_vxlocked+0x266
vrecycle() at vrecycle+0x48
nfs_inactive() at nfs_inactive+0xfe      <- close(fd) of the unlinked file
vop_inactive() ... vrele() ... vn_close() ... kern_close() syscall2()
```

Disassembly of the faulting instruction region (both stock snapshots and
the rebuilt kernels) confirms +0x150 is the cookie walk:

```
+0x134: mov 0x198(%rbx),%rdi   ; dp = np->n_cookies.lh_first
+0x150: mov (%rdi),%r12        ; dp = dp->ndm_list.le_next   <<< FAULT (dp=1)
+0x15b: call _kfree            ; kfree(dp2, M_NFSDIROFF)
+0x160: jne +0x150             ; while (dp)
```

The `typelie` run is the cleanest demonstration that the bug is fully
client-side reachable with a *well-formed* reply: the file's own file
handle + directory-typed attributes.  `nfs_lookitup()` applies the attrs
(vnode retyped VREG→VDIR mid-life), the identity check passes, sp is stored
into the cookie head, and the very next `close()` of the unlinked file
reclaims the node and walks `sp` → `s_dvp` (a `struct vnode *` reinterpreted
as `nfsdmap *`) → wild walk → `kfree(sp, M_NFSDIROFF)` (wrong zone) →
page fault at 0x1.

Additional primitives in the same line (not pushed to uid=0 within this
run): mode `err` is an 8-byte heap-pointer write at a stale-stack-chosen
address; the cookie-slot window `sp+20+4*pos` (pos ≥ 5) reads/writes past
the 40-byte `sp` into adjacent M_NFSREQ objcache objects with
server-chosen 64-bit cookie values, and those bytes are echoed back to the
server in subsequent READDIR cookies (kernel-heap disclosure).

## Fix validation

`fix.diff` (v2; applied only inside the guest's /usr/src, kernel rebuilt
with `make nativekernel KERNCONF=X86_64_GENERIC`):

1. `nfs_sillyrename`: initialize `np = NULL`; check the error; require the
   returned node to be the renamed file (fh identity) **and still VREG**
   (type) — otherwise release the acquired vnode (vput / vrele for the
   parent-echo case), free sp, and fail with EBADRPC.
2. `nfs_lookitup`: the parent-fh-echo branch sets `np = dnp` so `*npp` can
   never remain uninitialized on success (DF-0767's root cause).

Baseline: stock panics (dirfh 4/4, control 1/1, typelie/freshdir observed).
Patched: all five modes clean (run.fixed.log), honest lifecycle unaffected.

## Honesty notes

* The earliest baseline runs used a fake-server LOOKUP reply in RFC order
  ([dir attrs][fh][obj attrs]) while the DragonFly client parses
  [fh][obj attrs] — those runs still panicked identically (the misaligned
  attrs fed the same type-confusion machinery), but the final matrix above
  was re-run with the server matching DragonFly's actual wire order, so the
  reported mechanisms correspond exactly to well-formed (or precisely-lying)
  server replies.
* An OOM side-effect observed in one matrix iteration (client `ls` reading
  an endless fake directory) was a fake-server artifact, removed by giving
  the fake directory a finite size; it is unrelated to the finding.
