⬢ DragonFlyBSD Kernel Audit
DF-3195 / verdict.json
← back to finding ↓ download raw
{
 "finding_id": "DF-3195",
 "status": "reproduced",
 "reproduced": 1,
 "impact": "panic",
 "confidence": "certain",
 "verdict": "Deterministic kernel panic reproduced 2/2 on the stock INVARIANTS kernel: nfs_inactive() and nfs_reclaim() discriminate the n_un3 union (struct sillyrename * vs nfsdmap cookie LIST_HEAD) by the runtime-mutable vp->v_type, so a server-side VREG->VDIR retype (nfs_loadattrcache -> nfs_setvtype, nfs_subs.c:723-724) of a node with a pending sillyrename makes the last close skip the sp flush and makes reclaim walk sp as a cookie list: kfree(sp M_NFSREQ-as-M_NFSDIROFF), then kfree of the live s_cred ucred as M_NFSDIROFF, then a wild deref of *(void**)ucred (=0x1 from cr_ref) -> Fatal trap 12 at nfs_reclaim+0x150, fault address 0x1. Every step is source-verified and the whole chain is driven by unprivileged local syscalls (open+unlink, sleep, fstat, close) against a malicious server. Fix validated on a rebuilt kernel: identical trigger survives ([6] close()=0, clean unmount) while the retype is still applied.",
 "exploit_chain": "unprivileged user: open(O_CREAT) + unlink() on mounted NFS share -> nfs_sillyrename stores valid sp on VREG node (all server replies honest so far) -> sleep past attrcache timeout -> fstat(fd) -> server GETATTR reply claims ftype=DIR -> nfs_setvtype(VDIR) on live node -> close(fd) -> VREF_FINALIZE -> VOP_INACTIVE skips sp flush (v_type==VDIR branch, nfs_node.c:392) + NREMOVED vrecycle -> VOP_RECLAIM cookie-walk on sp (nfs_node.c:466-473): kfree(sp, M_NFSDIROFF) -> dp=sp->s_cred -> kfree(ucred, M_NFSDIROFF) -> dp=*(void**)ucred=0x1 -> page fault panic at nfs_reclaim+0x150. Escalation ceiling: the walks third pointer derives from ucred cr_ref (not attacker-shaped), so the realistic outcome is the panic plus two premature/wrong-type frees of live kernel objects (sp, ucred); uid0 not reached.",
 "evidence": [
  "run.log: full serial-console capture - steps [1]-[5] then Fatal trap 12, fault 0x1, Stopped at nfs_reclaim+0x150 movq (%rdi),%r12",
  "panic.txt: panic block from the capture run",
  "run.fixed.log: same PoC on fix-validation kernel #1 Sep 8 08:54:41 - [4] retype still applied, [6] close()=0, SURVIVED rc=0, UNMOUNT_OK",
  "VERDICT.md: full chain with path:line for every step, fix-diff design, honesty notes (incl. the intermediate patch-application failure)",
  "fakesrv_retype.c mode retype: honest silly-rename lifecycle, only the post-rename GETATTR(fh_file) lies with DIR attrs"
 ],
 "kernel_refs": [
  "sys/vfs/nfs/nfs_node.c:392-397",
  "sys/vfs/nfs/nfs_node.c:466-473",
  "sys/vfs/nfs/nfsnode.h:130-133",
  "sys/vfs/nfs/nfs_subs.c:723-724",
  "sys/vfs/nfs/nfs_vnops.c:3015",
  "sys/vfs/nfs/nfs_vnops.c:3036-3041"
 ],
 "poc_changes": "Extended DF-2996s fake NFSv3 server with a retype mode (honest everything; GETATTR of the file fh answers DIR-typed attrs once the silly-rename completed) and wrote trigger_retype.c (open+unlink -> 16s sleep for attrcache expiry -> fstat to force the GETATTR RPC -> close). PoC output routed to /dev/console so evidence survives the panic.",
 "attempts": 2,
 "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
 "runtime_sec": 35,
 "guest_dirty": 0,
 "build_cmd": "cd /root/df3195 && sh build.sh  (cc -O2 -o fakesrv_retype fakesrv_retype.c; cc -O2 -o trigger_retype trigger_retype.c)",
 "run_cmd": "(./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",
 "code_hash": "sha256(trigger_retype.c)=947c1d4ce9f9ab7a05ca5bd7c3f2d7f73751182a29e98e09de997a10a545e1e4 sha256(fakesrv_retype.c)=a451dfcd644f54fb3632996b548bf1c0c435de418e5d3384ff0c45adf18c1950",
 "notes": "Distinct root cause from DF-2996 (vnops unchecked lookitup): here the sillyrename is stored on the correct node through fully honest replies; only a LATER attribute load retypes the node (DF-2998s class) and nfs_node.cs v_type-based union discrimination turns that into the cookie walk. DF-2996s vnops fix does not prevent this chain. Requires malicious/compromised/spoofing server; local user part is entirely unprivileged.",
 "recommended_fix": "Discriminate the n_un3 union with an NSILLY flag bit (0x0040, currently NUNUSED040) set at sp-store time, honored/cleared in nfs_inactive and checked in nfs_reclaim, instead of testing the mutable vp->v_type (see fix.diff).",
 "fix_status": "fixed",
 "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 8 08:54:41 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
 "fix_baseline_reproduced": 1,
 "fix_patched_reproduced": 0,
 "fix_verdict": "fix.diff applied in-guest to /usr/src (verified by grep before build: NSILLY present in nfsnode.h/nfs_node.c/nfs_vnops.c), kernel rebuilt with make -j6 nativekernel KERNCONF=X86_64_GENERIC (RC=0, nfs_node.c compile confirmed in build log), installed and booted. Identical PoC: baseline panicked 2/2 at nfs_reclaim+0x150 fault 0x1; patched kernel prints [6] close()=0, SURVIVED rc=0, UNMOUNT_OK and stays healthy, while [4] proves the server retype is still applied (the fix is in the union discrimination, not in blocking the retype).",
 "fix_evidence": "findings/poc/DF-3195/run.fixed.log (patched run transcript from serial console); fix.diff (applied diff); VERDICT.md Fix validation section; note in VERDICT.md about the intermediate build that silently lacked the patch (identical nfs_reclaim+0x150 offset exposed it) and was discarded."
}