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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2816 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 NULLsfd_ncdir.ncpand.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()copiesfd_ncdir/fd_nrdirintond->nl_nchwith no NULL-ncp guard (sys/kern/vfs_nlookup.c:149-161; cf. the guardedfd_njdirat :153/163), andnlookup()'s permission checknaccess()dereferencesnch->ncpat +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:
race2(4 sibling threads spammingioctl(DIOCGKERNELDUMP)+ main thread callingreboot(RB_DUMP)) β panic innaccess+0x33 β nlookup+0x15e,fault virtual address = 0x58, processrace2, faulting CPU = a churn CPU (serial.naccess-panic.log / panic trace in VERDICT). Reproduced twice.sibs(4 sibling threads doing onlyopen("etc/passwd")in a loop + main thread callingreboot(RB_DUMP|RB_NOSYNC)) β identicalnaccess+0x33NULL-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.
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.
- 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) - 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). Identicalnaccess+0x33panic on cpu3, then the machine wedges hard:send_ipiq 2->4 tgt not drainingforever, 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
- sys/kern/kern_shutdown.c:290-301 (unguarded curproc cleanup; contrast initproc SIGSTOP at :293-297), :566-610 (shutdown_cleanup_proc: closefrom, cache_drop of fd_ncdir/fd_nrdir/fd_njdir, pmap/vm_map teardown)
- sys/kern/vfs_cache.c:1090-1096 (cache_drop NULLs the nchandle)
- sys/kern/vfs_nlookup.c:145-177 (un NULL-guarded cwd/root anchors), :655-659 + disasm (naccess NULL ncp deref sink)
Fix verification
fixedSame 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']
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).
No comments yet.