DF-2816 / verdict.json
{ "finding_id": "DF-2816", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "boot() calls shutdown_cleanup_proc(curproc) (kern_shutdown.c:291) on the still-running reboot(2)-calling process while its sibling threads keep executing on other CPUs (only initproc is SIGSTOPped first, :293-297; other CPUs are never stopped in the sys_reboot path). The cleanup closes every fd, cache_drop()s - and thereby NULLs (vfs_cache.c:1090-1096) - fd_ncdir/fd_nrdir/fd_njdir, and vm_map_remove()s the whole user address space (:601-609). A sibling thread that then performs any path lookup copies the NULL-ed nchandle into nd->nl_nch (nlookup_init has no NULL-ncp guard for the cwd/root anchors, vfs_nlookup.c:149-161) and naccess() dereferences the NULL ncp at +0x58 (vfs_nlookup.c:655-659; disasm naccess+0x33 'movzwl 0x58(%r15),%edx' with r15==NULL) -> kernel page fault -> panic. Reproduced deterministically two ways on the stock INVARIANTS guest: (1) race2 - 4 sibling threads spamming ioctl(DIOCGKERNELDUMP) while the main thread calls reboot(RB_DUMP): panic 'Fatal user address access from kernel mode', fault VA 0x58, naccess+0x33 <- nlookup+0x15e, 2/2 runs; (2) sibs - 4 siblings looping only open(\"etc/passwd\") with main calling reboot(RB_DUMP|RB_NOSYNC): identical naccess+0x33 panic, then the machine hard-wedges (endless 'send_ipiq 2->4 tgt not draining', no DDB, no reboot), 1/1. Requires the reboot privilege (SYSCAP_NOREBOOT) - an already-privileged caller converting a clean reboot into a kernel panic/wedge and losing the crash dump; not an escalation (NULL deref only; page 0 unmappable on x86-64). Fix validated in the same rebuilt kernel: only shutdown_cleanup_proc(curproc) when p_nthreads < 2; 'sibs 4' then reboots cleanly with a completed dump and no panic (run.patched.log).", "exploit_chain": "privileged multi-threaded process -> N sibling threads loop relative-path open() (or any syscall doing path lookups / copyin of user memory) -> main thread calls reboot(2) -> boot() tears down the process's fds, NULLs fd_ncdir.ncp, removes its user address space -> sibling's next open() runs nlookup_init -> cache_copy of NULL-ncp fd_ncdir into nd->nl_nch -> naccess() NULL+0x58 deref -> kernel panic (and in the observed run, a hard IPI wedge). No uid0 route: pure NULL-deref DoS at shutdown time.", "evidence": [ "serial.race2-panic.log - clean panic trace: naccess+0x33 <- nlookup+0x15e, fault VA 0x58, process race2, churn cpu (2/2 runs)", "serial.race2-full.log - full serial log of that run", "serial.sibs-panic.log - minimal deterministic sibs trigger: same naccess+0x33 panic, then 'send_ipiq 2->4 tgt not draining' hard wedge", "sibs.c / race2.c - PoC sources; run.sh - recipes", "run.patched.log - fix validation on patched kernel #1: sibs 4 -> full dump ('Dumping 676 MB ... Dump complete') and clean reboot, no panic, no wedge", "fix.diff - validated diff (curproc cleanup only when single-threaded)" ], "kernel_refs": [ "sys/kern/kern_shutdown.c:290-301", "sys/kern/kern_shutdown.c:566-610", "sys/kern/vfs_cache.c:1090-1096", "sys/kern/vfs_nlookup.c:145-177", "sys/kern/vfs_nlookup.c:655-659" ], "poc_changes": "Seed had no PoC; developed two triggers. race2 (ioctl-churning siblings + reboot(RB_DUMP)) reproduced 2/2 but the exact sibling syscall reaching nlookup was not pinned; sibs.c (4 siblings looping open(\"etc/passwd\") + reboot(RB_DUMP|RB_NOSYNC)) was written from the code analysis (fd_ncdir anchor NULL) and reproduced the identical naccess+0x33 panic 1/1, deterministically, with a 30-line program and no dump-device churn at all.", "attempts": 3, "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": 900, "guest_dirty": 0, "build_cmd": "cc -O2 -Wall -pthread -o /root/sibs /root/sibs.c && cc -O2 -Wall -pthread -o /root/race2 /root/race2.c", "run_cmd": "sysctl debug.debugger_on_panic=0 debug.trace_on_panic=1; /root/sibs 4 # reboots; panic+wedge visible on serial console (dfbsd-qemu/vm.sh log)", "code_hash": "sibs.c=774993a5dd260a65a93a71dc2369daccd407c196938aeca09b6b7d7c17ec22fe race2.c=6829b6869d09a58b666245e75a3a4280350781d4107695dc6f3d0552b14336bc", "notes": "naccess+0x33 disassembly (stock kernel binary): 'mov (%rsi),%r15' (r15=nch->ncp) then 'movzwl 0x58(%r15),%edx' - NULL ncp confirmed as the faulting dereference; nlookup+0x15e resolves to vfs_nlookup.c:658 (directory-search permission check on nd->nl_nch). The panic-path boot() is unaffected (panicstr != NULL skips the cleanup). The panic sometimes garbles the console because two CPUs print simultaneously (itself evidence of the multi-CPU crossfire). Recommended additional hardening outside this file: NULL-guard cache_copy of fd_ncdir/fd_nrdir in nlookup_init (vfs_nlookup.c:149-161), mirroring the existing fd_njdir.ncp checks at :153/:163.", "recommended_fix": "Only run shutdown_cleanup_proc(curproc) when the calling process is single-threaded (curproc->p_nthreads < 2); plus NULL-guard the cwd/root nchandle anchors in nlookup_init() for defense in depth.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 17:21:28 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Same rebuilt kernel as DF-2815 (fix.diff includes the DF-2816 hunk). Patched-kernel run of the exact sibs 4 trigger: no naccess panic, no Fatal trap, no IPI wedge; the shutdown proceeds to a full dump ('Dumping 676 MB ... Dump complete') and a clean automatic reboot (run.patched.log). Baseline on stock kernel #0: panic naccess+0x33 plus hard machine wedge.", "fix_evidence": [ "run.patched.log - patched run: full dump + clean reboot under the identical sibling workload", "fix.diff - the validated diff" ] } |