# DF-3195 — `nfs_inactive`/`nfs_reclaim` discriminate the `n_un3` union (`nf_silly` vs `nd_cook`) by the runtime-mutable `v_type` → server-side VREG→VDIR retype turns a pending sillyrename into a cookie-list walk: mismatched/wild `kfree(M_NFSDIROFF)` chain → deterministic kernel panic (2/2) ## What this is `struct nfsnode` overlays `struct sillyrename *nf_silly` and `LIST_HEAD(, nfsdmap) nd_cook` in one union (`n_un3`, sys/vfs/nfs/nfsnode.h:130-133). `nfs_inactive()` and `nfs_reclaim()` pick which interpretation to use **by testing `vp->v_type`**: ```c /* nfs_inactive, sys/vfs/nfs/nfs_node.c:392-397 */ if (ap->a_vp->v_type != VDIR) { sp = np->n_sillyrename; np->n_sillyrename = NULL; } else { sp = NULL; /* ← sillyrename flush SKIPPED */ } ... /* nfs_reclaim, sys/vfs/nfs/nfs_node.c:466-473 */ if (vp->v_type == VDIR) { dp = np->n_cookies.lh_first; /* ← walks sp as an nfsdmap list */ while (dp) { dp2 = dp; dp = dp->ndm_list.le_next; kfree((caddr_t)dp2, M_NFSDIROFF); } } ``` But `v_type` of a live NFS vnode is **mutable at runtime**: `nfs_loadattrcache()` applies any server-claimed `ftype` to an existing vnode (`if (vp->v_type != vtyp) nfs_setvtype(vp, vtyp) …`, sys/vfs/nfs/nfs_subs.c:723-724 — the DF-2998 retyping behavior). A malicious/compromised/spoofing server (AUTH_SYS over UDP has no integrity) can therefore retype a **VREG node that carries a pending sillyrename** to VDIR, after which the last close executes: 1. `nfs_inactive()`: `v_type == VDIR` → the `struct sillyrename` (allocated `M_NFSREQ` in `nfs_sillyrename`, sys/vfs/nfs/nfs_vnops.c:3015) is **never flushed** — `sp`, `sp->s_cred` (a `crdup`'d ucred) and the `vref`'d `sp->s_dvp` all leak. 2. `NREMOVED` is set → `vrecycle` → `nfs_reclaim()`: `v_type == VDIR` → the cookie teardown interprets the union: `dp = np->n_cookies.lh_first` **is** `sp`. Loop iteration 1: `kfree(sp, M_NFSDIROFF)` — an `M_NFSREQ` object freed as `M_NFSDIROFF`. Iteration 2: `dp = *(void **)sp = sp->s_cred` (a live `struct ucred *`) → `kfree(ucred, M_NFSDIROFF)`. Iteration 3: `dp = *(void **)ucred` = `{u_int cr_ref = 1; padding}` ≈ 0x1 → `movq (%rdi),%r12` page-faults at 0x1 → **panic** (`nfs_reclaim+0x150`). This is distinct from DF-2996 (vnops consuming an unchecked lookitup result): here every step up to the retype is *honest* — the sillyrename is validly stored on the correct node, and the DF-2996 vnops fix (validating the lookitup result and its type) does **not** prevent a *later* GETATTR from retyping the node. The defect is purely the union discrimination by mutable type in nfs_node.c. Trigger: any unprivileged local user `unlink()`ing an open file on a mounted NFS share whose server then answers any attribute-bearing RPC for that file with directory-typed attributes. ## PoC files * `fakesrv_retype.c` — DF-2996's fake NFSv3 server + a `retype` mode: everything honest (CREATE/RENAME/LOOKUP all truthful, the silly-rename LOOKUP replies the file's own fh with honest REG attrs), EXCEPT `GETATTR(fh_file)` replies **DIR-typed attributes once the silly-rename completed** (the "late retype"). * `trigger_retype.c` — `open(O_CREAT)` + `unlink()` (valid sp stored on the VREG node) → sleep 16 s (attr-cache expiry) → `fstat(fd)` (RPC GETATTR → live retype, printed as `type = DIRECTORY`) → `close(fd)` (VREF_FINALIZE → VOP_INACTIVE → vrecycle → VOP_RECLAIM → wild walk). ## Build (in-guest) ``` cd /root/df3195 cc -O2 -o fakesrv_retype fakesrv_retype.c cc -O2 -o trigger_retype trigger_retype.c ``` ## Run (baseline, stock kernel) ``` (./fakesrv_retype retype > /dev/console 2>&1 &) sleep 1 mount_nfs -o port=2049,udp 127.0.0.1:/P /mnt rm -f /mnt/f ./trigger_retype > /dev/console 2>&1 ``` ## Expected (baseline — verified 2/2, deterministic) ``` [1] open(/mnt/f) = fd 3 [2] unlink() = 0 (silly-rename pending: valid sp on VREG node) [3] sleeping 16s for attr-cache expiry... [4] fstat(fd): server-claimed type = DIRECTORY (live retype applied) [5] close(fd) -> VOP_INACTIVE (sp flush skipped) + vrecycle -> VOP_RECLAIM ... Fatal user address access from kernel mode from trigger_retype Fatal trap 12: page fault while in kernel mode fault virtual address = 0x1 Stopped at nfs_reclaim+0x150: movq (%rdi),%r12 ``` (the `0x1` is `sp->s_cred`'s `cr_ref=1` + padding read as the next `nfsdmap *` — after TWO wrong-object frees already executed: `sp` (M_NFSREQ) and the live ucred.) ## Expected (with fix.diff) The late GETATTR retype may still be applied (that's DF-2998's side), but `nfs_inactive` flushes the sillyrename regardless of the current type (`NSILLY` flag), the REMOVE RPC fires, and `nfs_reclaim` never walks the union as cookies: no panic, guest stays up, trigger prints `[6] close() = 0`. Verified on the rebuilt kernel (run.fixed.log). ## Fix `fix.diff`: add `NSILLY` (reusing the free `NUNUSED040` bit) as the union discriminator — set at the sp store site, honored (and cleared) in `nfs_inactive`, checked in `nfs_reclaim` (cookie walk only when not NSILLY; unreachable safety-net free otherwise).