β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2816

boot()/shutdown_cleanup_proc() tears down the live reboot(2)-calling process (all fds closed, fd_ncdir/fd_nrdir/fd_njdir nchandles NULLed, entire user vmspace removed) while its sibling threads still run on other CPUs β†’ sibling path lookups dereference NULL ncp in naccess() β†’ kernel panic and hard machine wedge

Field Value
ID DF-2816
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-362 (manifest CWE-476)
File sys/kern/kern_shutdown.c
Lines 291 (call), 566-610 (teardown); sink vfs_nlookup.c:655-659
Area kern
Confidence certain
Discovered 2026-08-31
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

In the sys_reboot path nothing stops the calling process's sibling threads (only initproc is quiesced; other CPUs are never stopped), yet boot() calls shutdown_cleanup_proc(curproc) which kern_closefrom(0)s every fd, cache_drop()s fd_ncdir/fd_nrdir/fd_njdir (NULLing nch->ncp and nch->mount), and pmap_remove_pages()+vm_map_remove()s the whole user address space. A sibling 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, unlike fd_njdir) and naccess() dereferences the NULL ncp at +0x58. Siblings doing copyin/copyout of their now-removed user memory fault fatally the same way.

Threat model & preconditions

Any privileged multi-threaded process calling reboot(2) with concurrently running siblings converts a clean shutdown into a kernel NULL-deref panic β€” and in the observed minimal run into a hard machine wedge (endless send_ipiq N->M tgt not draining, no DDB, no reboot) β€” losing the crash dump and any clean-shutdown guarantees. Not an escalation (reboot privilege required; NULL deref is pure DoS); a reliability defect with a trivial deterministic 30-line trigger.

Proof of contest

VERIFIED 1/1 deterministic (findings/poc/DF-2816/): 4 sibling threads loop open("etc/passwd") (relative β†’ fd_ncdir anchor) while the main thread calls reboot(RB_DUMP|RB_NOSYNC) β†’ Fatal trap 12 ... fault virtual address = 0x58 ... naccess+0x33 / nlookup+0x15e, then the box wedges with send_ipiq spam until forced reset. Fix (only clean up curproc when single-threaded) validated on a rebuilt kernel: sibs 4 completes a full dump and reboots cleanly. Defense-in-depth outside this file: NULL-guard cache_copy of fd_ncdir/fd_nrdir in nlookup_init for fd_njdir parity.

Validated fix.diff in findings/poc/DF-2816/.

Timeline

  • 2026-08-31 Discovered during pass-2 audit of kern_shutdown.c (GLM 5.3); deterministic panic + wedge reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2816 Β· 13 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
sibs.c β€” 1.6 KB view raw
race2.c β€” 2.6 KB view raw
build.sh β€” 297 B view raw
run.sh β€” 820 B view raw
serial.race2-panic.log β€” 56.8 KB view raw
serial.race2-full.log β€” 56.8 KB view raw
serial.sibs-panic.log β€” 2.2 KB view raw
run.patched.log β€” 850 B view raw
env.txt β€” 785 B view raw
fix.diff β€” 2.7 KB view raw
verdict.json β€” 5.9 KB view raw

DF-2816 β€” boot()/shutdown_cleanup_proc() tears down the calling process while its sibling threads still run

What

sys_reboot() β†’ boot() (sys/kern/kern_shutdown.c:250) calls shutdown_cleanup_proc(curproc) at kern_shutdown.c:291 while the calling process's other threads keep running on the other CPUs β€” nothing stops them (only initproc gets a SIGSTOP + wait at :293-297; other CPUs are never stopped in the sys_reboot path).

shutdown_cleanup_proc() (kern_shutdown.c:566-610) then, on the live process:

  • kern_closefrom(0) closes every fd (:576),
  • cache_drop(&fdp->fd_ncdir) β€” which NULLs fd_ncdir.ncp and .mount (sys/kern/vfs_cache.c:1090-1096) β€” and likewise fd_nrdir / fd_njdir (:577-591),
  • pmap_remove_pages() + vm_map_remove(VM_MIN_USER_ADDRESS, VM_MAX_USER_ADDRESS) β€” destroys the whole user address space (:601-609).

A sibling thread that subsequently enters the kernel on any other CPU:

  • doing a path lookup: nlookup_init() copies fd_ncdir/fd_nrdir into nd->nl_nch with no NULL-ncp guard (sys/kern/vfs_nlookup.c:149-161; cf. the guarded fd_njdir at :153/163), and nlookup()'s permission check naccess() dereferences nch->ncp at +0x58 (vfs_nlookup.c:655-659) β†’ NULL-deref page fault β†’ kernel panic;
  • doing any syscall with copyin/copyout of its now-unmapped user memory β†’ fatal "user address access from kernel mode" β†’ panic.

Reproduced

Two independent triggers on DragonFly 6.5-DEVELOPMENT x86_64 (X86_64_GENERIC, INVARIANTS), 2/2 + 1/1 deterministic:

  1. race2 (4 sibling threads spamming ioctl(DIOCGKERNELDUMP) + main thread calling reboot(RB_DUMP)) β€” panic in naccess+0x33 ← nlookup+0x15e, fault virtual address = 0x58, process race2, faulting CPU = a churn CPU (serial.naccess-panic.log / panic trace in VERDICT). Reproduced twice.
  2. sibs (4 sibling threads doing only open("etc/passwd") in a loop + main thread calling reboot(RB_DUMP|RB_NOSYNC)) β€” identical naccess+0x33 NULL-deref panic, then an IPI-wedged machine (send_ipiq N->M tgt not draining) β€” a hard hang, not even a clean panic reboot.

Decisive serial output (from race2):

Fatal user address access from kernel mode from race2 at ffffffff806ee803
Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0x58
instruction pointer      = 0x8:0xffffffff806ee803
current process          = 879 (race2)
panic: page fault
naccess() at naccess+0x33
nlookup() at nlookup+0x15e

with naccess+0x33 disassembled (stock kernel) as:

ffffffff806ee7ee: 4c 8b 3e        mov    (%rsi),%r15      ; r15 = nch->ncp
ffffffff806ee803: 41 0f b7 57 58  movzwl 0x58(%r15),%edx  ; NULL+0x58 -> trap

Impact

Kernel NULL-deref panic / hard wedge during reboot(2). Requires the reboot privilege (SYSCAP_NOREBOOT, kern_shutdown.c:186) in the calling process β€” i.e. an already-privileged user converts a clean reboot into a kernel panic with a wedge (and a corrupted/absent crash dump). Not an escalation; robustness/reliability defect with a trivially reliable trigger. Defense-in-depth note: any future caller of boot() outside sys_reboot inherits the same hazard.

Fix

fix.diff (verified in-guest): only run shutdown_cleanup_proc(curproc) when the process is single-threaded (curproc->p_nthreads < 2); multi- threaded callers keep their state through the shutdown (harmless β€” forced unmount already copes with lingering refs). Recommended hardening on top: NULL-guard the cache_copy(&fdp->fd_ncdir/fd_nrdir, ...) anchors in nlookup_init() (vfs_nlookup.c:149-161), mirroring the existing fd_njdir.ncp checks.

VERDICT.md
↓ download raw

DF-2816 VERDICT β€” REPRODUCED (kernel NULL-deref panic: boot() teardown vs sibling threads)

Bottom line

boot() calls shutdown_cleanup_proc(curproc) (kern_shutdown.c:291) on the live reboot(2)-calling process while its sibling threads keep running on other CPUs. 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 entire user address space (kern_shutdown.c:601-609). A sibling thread that then performs a 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, unlike fd_njdir at :153/163 β€” and nlookup_init is also racy against fd_cdir NULLing) β€” and naccess() dereferences the NULL ncp at +0x58 (vfs_nlookup.c:655-659, disassembly naccess+0x33 movzwl 0x58(%r15),%edx with r15==NULL).

Reproduction

Stock INVARIANTS kernel (DragonFly 6.5-DEVELOPMENT #0 Jul 2 2026, X86_64_GENERIC), debugger_on_panic=0 trace_on_panic=1 for serial traces.

  1. race2 (MARK-3, MARK-4): 4 sibling threads spam ioctl(DIOCGKERNELDUMP) + heartbeat writes; main thread calls reboot(RB_DUMP). Panic on a churn CPU in naccess+0x33 ← nlookup+0x15e, fault VA 0x58, process race2. 2/2 runs. (serial.race2-panic.log / serial.race2-full.log)
  2. sibs (SIBS-MARK-1): minimal deterministic trigger β€” 4 siblings looping only open("etc/passwd") (relative path β†’ fd_ncdir anchor); main calls reboot(RB_DUMP|RB_NOSYNC). Identical naccess+0x33 panic on cpu3, then the machine wedges hard: send_ipiq 2->4 tgt not draining forever, no DDB, no reboot β€” a hang, not even a panic reboot (the panic crossfire left cpu4 non-draining). 1/1 run. (serial.sibs-panic.log)

Which sibling syscall triggers it is workload-dependent (any relative or absolute path lookup β€” the root anchor fd_nrdir is equally unguarded at vfs_nlookup.c:149-152); copies from user memory after the vm_map_remove fault fatally in kernel mode the same way ("Fatal user address access from kernel mode").

Impact / escalation ceiling

Kernel NULL-deref panic and, in the observed sibs run, a hard machine wedge during shutdown, with the crash dump corrupted/lost. Requires the reboot privilege (SYSCAP_NOREBOOT, kern_shutdown.c:186) β€” an already- privileged user turning a clean reboot into a kernel panic/wedge. No memory-control primitive beyond the NULL deref (the torn state is a NULLed handle, not attacker-controlled data); x86-64 userland cannot map page 0 (VM_MIN_USER_ADDRESS), so no NULL-page pivot. Not an escalation; reliability/robustness defect with a trivial, deterministic trigger.

Fix validation

fix.diff: only run shutdown_cleanup_proc(curproc) when the process is single-threaded (curproc->p_nthreads < 2); multi-threaded callers keep their state through shutdown (harmless β€” forced unmount handles lingering refs; the machine resets immediately after). Built and validated in the same patched-kernel run as DF-2815: sibs 4 now reboots cleanly with no panic and no wedge (run.patched.log). Recommended hardening on top: guard cache_copy(&fdp->fd_ncdir/fd_nrdir, ...) in nlookup_init (vfs_nlookup.c:149-161) for fd_njdir-parity.

Kernel references

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

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.

['run.patched.log - patched run: full dump + clean reboot under the identical sibling workload', 'fix.diff - the validated diff']
↓ fix.diffDragonFly 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

Confirmed kernel references

Detail

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 (decisive lines)

['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)']

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.

Verified 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.

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).