VERDICT — DF-3255 ================= FINDING: nfs_clientd() use-after-free / double-free of the nfsmount on non-KERB NFS mounts. sys/vfs/nfs/nfs_kerb.c:106-141 (loop + teardown), :126 (self-dounmount); free at sys/vfs/nfs/nfs_vfsops.c:1242-1244. VERDICT: REPRODUCED (2/2 trigger variants + clean discriminating control), impact = panic (kernel double free / memory corruption primitive), root-gated trigger. ROOT CAUSE (path:line) ---------------------- 1. sys_nfssvc() enters nfs_clientd() for ANY mount passed to nfssvc(NFSSVC_MNTD) — there is no NFSMNT_KERB check (sys/vfs/nfs/nfs_syscalls.c:160-187). The flag is simply recorded (NFSSTA_MNTD, :186) and nfs_clientd() runs (:187) sleeping in its "nqnfstimr" loop on &nmp->nm_authstr (sys/vfs/nfs/nfs_kerb.c:123-124), re-reading nmp->nm_state every hz/3 (:106). 2. nfs_unmount() defers the nfsmount free to the clientd ONLY for kerberos mounts: `if (nmp->nm_flag & NFSMNT_KERB) nmp->nm_state |= NFSSTA_DISMNT;` and `if ((nmp->nm_flag & NFSMNT_KERB) == 0) nfs_free_mount(nmp);` (sys/vfs/nfs/nfs_vfsops.c:1230, 1242-1244). For a non-KERB mount with a clientd running, VFS_UNMOUNT kfree()s nmp while nfs_clientd() still references it — and nfs_clientd() itself DRIVES the unmount on EINTR: `if (error == EINTR || error == ERESTART) (void) dounmount(nmp->nm_mountp, 0, 0);` (nfs_kerb.c:125-126). dounmount() also mount_drop()s and kfree()s the struct mount, so nmp->nm_mountp is stale immediately after. 3. Control returns from dounmount() straight into the loop condition (:106) — a read of freed memory. When the freed/reused chunk reads with NFSSTA_DISMNT (0x00800000) set (deterministic with debug.use_weird_array=1: the INVARIANTS slab poison WEIRD_ADDR 0xdeadc0de has bit 23 set — sys/kern/kern_slaballoc.c:191,232-234; or probabilistic via chunk reuse), the loop falls out and executes the teardown (nfs_kerb.c:133-138): TAILQ/LIST_REMOVE walk over the dead nm_uidlruhead and nfs_free_mount(nmp) a SECOND time → kernel double free. Repeated signals re-execute dounmount(nmp->nm_mountp) on freed pointers. REPRODUCTION ------------ Baseline stock INVARIANTS kernel #0 (X86_64_GENERIC), loopback NFS (127.0.0.1:/tmp on /mnt/nfspoc, non-kerb), debug.use_weird_array=1: * variant "sig" → panic: memory chunk 0xfffff8008971f880 is already free! (chunk_mark_free/slab_cleanup/slotimer) * variant "umount" → same panic (cpuid 1); child's nfssvc returned 0 first — the ENTIRE teardown ran on the freed nfsmount and returned before the slab checker fired. * control (identical setup, NO clientd) → no panic, guest stays up. See run.log, run.2.log, panic.txt. EXPLOIT CHAIN (why it stops at panic) ------------------------------------- nfssvc(2) is gated by caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT) (sys/vfs/nfs/nfs_syscalls.c:144) and mounting/unmounting requires root (vfs.usermount=0 by default). The trigger is therefore privileged; there is no unprivileged→root escalation path through this bug. What the primitive gives a privileged caller is kernel heap corruption: post-free execution of nfs_free_mount() (crfree/kfree/destroy_obj through a reused chunk) and a LIST_REMOVE walk over attacker-influenceable reused memory — i.e., the classic slab-groom double-free, relevant for integrity (zero-trust/containers, jailed roots, capability-delegated subroots) but not for unpriv→uid0. FIX (validated) --------------- fix.diff — sys/vfs/nfs/nfs_vfsops.c: nfs_unmount() delegates the nfsmount free to nfs_clientd() whenever NFSSTA_MNTD indicates a clientd is (or was) active (not just KERB), sets NFSSTA_DISMNT + wakeup() as its LAST action on nmp, and frees only when no clientd can be looping; this also fixes the pre-existing leak of kerb nfsmounts whose clientd never ran. sys/vfs/nfs/nfs_kerb.c: the EINTR path breaks out before touching nm_mountp if NFSSTA_DISMNT is already set (external unmount already tore the mount down). Validated in-guest: kernel #1 rebuilt (make -j6 nativekernel, BUILD_RC=0, 0 errors), installed, booted; BOTH variants re-run cleanly (no panic, single-free teardown, child exits, guest up); control unchanged. See fix.log. RESIDUAL (documented, much narrower than the bug fixed): the clientd call still does not hold a mount reference across its lifetime; a fully-correct fix would vfs_hold(mp) at nfs_clientd() entry / vfs_drop at exit. If a clientd exits via the ENEEDAUTH return and userland never re-enters, the delegated free never happens (leak, no UAF) — same as the historical KERB behavior. CONFIDENCE: certain (2/2 panic reproductions, clean control, fix validated). ## Fix status (batch validation) Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.