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

Cothread signal-mask discipline leaves synchronous fault signals deliverable on %gs=0 threads β€” the file-owned enabling condition of the DF-5587 silent-recursion death, now proven on a real vkernel

Summary

cpu_mask_all_signals blocks only the 8 interrupt signals while tls_set_gs(0) makes every mycpu load fatal, and the process-wide exc_segfault is SA_NODEFER with empty mask: any cothread fault recurses exc_segfault->kern_trap->mycpu->fault without bound until the host kills the vkernel silently. Proven on a REAL VKERNEL64 boot via LD_PRELOAD shim injecting SIGSEGV into the vktimer cothread (verified %gs base==NULL via get_tls_area): 3/3 silent whole-VM deaths, core shows SIGILL at _get_mycpu under kern_trap under exc_segfault, other threads parked unaware. The actionable fix is the exc_segfault __thread re-entrancy guard (shared locus with DF-5587) - masking fault signals in the cothread would trade silent death for silent livelock. Latent in-tree trigger class: vke_rx_thread's deliberate NULL-ring write.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5607 Β· 15 files
FileTypeDescriptionSize
cotdfault.c β€” 4.3 KB view raw
build_image.sh β€” 1.0 KB view raw
run.sh β€” 114 B view raw
build.log β€” 342 B view raw
run.clean.log β€” 1.8 KB view raw
run.baseline.log β€” 938 B view raw
run.baseline.driver.log β€” 1.2 KB view raw
baseline_core.txt β€” 668 B view raw
run.fixed.log β€” 979 B view raw
run.fixed.driver.log β€” 1.3 KB view raw
fixed_core.txt β€” 592 B view raw
fix.diff β€” 1.5 KB view raw
env.txt β€” 422 B view raw
verdict.json β€” 4.4 KB view raw
VERDICT.md β€” 5.3 KB ↓ raw
VERDICT.md
↓ download raw

DF-5607 β€” VERDICT

Status: reproduced (3/3 deterministic on a real VKERNEL64 boot). Impact: dos (silent whole-vkernel death β€” strictly worse than a panic: no message, no dump, no trace). Confidence: certain.

What was proven, and how

The finding is the cothread.c-owned enabling condition of the DF-5587 dynamic. DF-5587 demonstrated the recursion with a standalone host program (coresig.c) modeling the vkernel's signal installs. This pack upgrades the proof to a real vkernel: a stock VKERNEL64 kernel.debug built from the guest's /usr/src, booted -m 512m -r /vkroot.img -n 2, with an LD_PRELOAD shim (cotdfault.so, no sys/ modification) that:

  1. interposes sigblock() for diagnostics β€” every cpu_mask_all_signals() (machintr.c:205-211, called at cothread.c:109 and cothread.c:143) passes through it;
  2. interposes nanosleep() β€” the vktimer cothread reaches it via cothread_sleep() (cothread.c:186-189, called from vktimer_thread, systimer.c:244) after tls_set_gs(0) (cothread.c:149) executed; the shim confirms the caller is a cothread by reading its %gs base with the get_tls_area(2) syscall (nr 473) β€” NULL only for cothreads;
  3. injects pthread_kill(self, SIGSEGV) there β€” indistinguishable from any real cothread-side fault (in-tree example: the deliberate *(volatile int *)0 = 1 NULL-ring write in vke_rx_thread, if_vke.c:672-675).

Observations (stock kernel)

  • Console at the moment of injection (run.baseline.log): COTDFAULT: ... injecting SIGSEGV ... β€” then silence. The vkernel child dies without panic, without Fatal trap, without a core note on the console, without the ddb Debugger() path. The init.c fork wrapper (init.c:203-222) swallows the child's status and exits 0, so even the parent's exit status is unremarkable.
  • The child's core (baseline_core.txt): Program terminated with signal 4, Illegal instruction β€” the host killed it when the recursive signal-frame pushes exhausted the cothread's pthread stack. Deepest frame: #0 _get_mycpu () at ./machine/thread.h:77 (movq %gs:globaldata β€” the mycpu load that re-faults with %gs=0), under #1 kern_trap (trap.c:585) under #2 exc_segfault (exception.c:269) β€” the recursion ladder itself. The two vkernel cpu threads are parked in nanosleep() (gdb info threads), i.e. no cpu thread ever ran the panic path.
  • 3/3 deterministic (three separate boots).

Root cause chain (path:line)

Masking the fault signals in the cothread instead is NOT a correct fix: a blocked synchronous signal makes the host retry the faulting instruction forever (silent livelock). The actionable fix is the re-entrancy guard in exc_segfault (cross-file, shared with DF-5587); cothread.c's contribution is the invariant documentation and the fact that its mask discipline is what leaves the thread exposed.

Fix validation (performed in this run)

fix.diff: __thread int exc_nesting guard; on re-entry print vkernel: recursive fault in exc_segfault, restore SIG_DFL, unblock, raise(signo). Applied to the guest's /usr/src (validation copy only), VKERNEL64 rebuilt (log RC=0), same injection re-run (run.fixed.log):

  • console: vkernel: recursive fault in exc_segfault then death β€” a diagnosable failure for the first time;
  • core (fixed_core.txt): Program terminated with signal 11, Segmentation fault with the raise β†’ _thr_send_sig β†’ lwp_kill path β€” the honest SIGSEGV, exactly 2 handler entries instead of ~308k.

Baseline vs patched: silent-SIGILL-recursion vs diagnostic-SIGSEGV. Bad behavior gone. (This also upgrades DF-5587's fix from simulated-only to real-vkernel-validated.)

Why not uid0

The vkernel is a host userland process; this bug class kills the vkernel instance (DoS of the virtual kernel) and provides no memory corruption primitive β€” the recursion is deterministic death, not control over saved state.

Environment

env.txt; guest DragonFly 6.5-DEVELOPMENT X86_64_GENERIC #0 Thu Jul 2 2026; VKERNEL64 built in-guest with the DF-5474 defsym workaround; vkernel boots SMP (-n 2) with /vkroot.img (rig lineage DF-5337/DF-5579).

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Guest /usr/src patched with fix.diff, VKERNEL64 rebuilt (RC=0), identical injection re-run: console prints 'vkernel: recursive fault in exc_segfault' and the process dies of a true SIGSEGV (core: signal 11 via raise->_thr_send_sig->lwp_kill) after 2 handler entries. Silent unbounded recursion eliminated.

findings/poc/DF-5607/fix.diff; run.fixed.log (diagnostic line); run.fixed.driver.log; fixed_core.txt (signal 11); baseline vs fixed contrast in VERDICT.md
↓ fix.diffper-fix-DF-5607

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5607/run.baseline.log (COTDFAULT markers then SILENT death), baseline_core.txt (signal 4, #0 _get_mycpu thread.h:77 / #1 kern_trap trap.c:585 / #2 exc_segfault exception.c:269), run.clean.log (healthy control boot), run.fixed.log + fixed_core.txt (recursive-fault diagnostic, signal 11 via raise), build.log, cotdfault.c, run.sh, build_image.sh

PoC changes

Authored from scratch (no seed). First attempt keyed on sigblock()+%gs==NULL alone and missed (vkernel crit sections never sigblock; first %gs-NULL sigblock fires pre-boot), so the shim was reworked to nanosleep()+get_tls_area(473) cothread detection, which fires exactly in vktimer_thread's cothread_sleep after tls_set_gs(0).

Verified recommended fix

Apply the exc_segfault __thread re-entrancy guard (fix.diff; shared locus with DF-5587) and document in cothread.c that fault signals are intentionally deliverable and must never fault in a cothread.

Verdict

cothread.c runs every vkernel cothread with %gs=0 (tls_set_gs(0), cothread.c:149) while cpu_mask_all_signals() (cothread.c:109/:143 -> machintr.c:205-211) blocks only the 8 interrupt signals, leaving SIGSEGV/SIGBUS/SIGTRAP/SIGFPE deliverable to the process-wide SA_NODEFER exc_segfault handler (exception.c:227-233): any cothread fault recurses exc_segfault->kern_trap->mycpu-load->fault without bound. Proven on a real VKERNEL64 boot (DF-5587 proved the dynamic only in a standalone model): an LD_PRELOAD shim injects SIGSEGV in the vktimer cothread (verified %gs base NULL via get_tls_area) during cothread_sleep; 3/3 boots the whole vkernel dies SILENTLY (no panic, no dump, no console output), core shows signal 4 (SIGILL from signal-frame push failure) with the deepest frame in _get_mycpu (thread.h:77 movq %gs:globaldata) under kern_trap (trap.c:585) under exc_segfault (exception.c:269), cpu threads parked unaware. Fix validated on a rebuilt vkernel: __thread exc_nesting guard converts the death to a one-line diagnostic + honest SIGSEGV (2 handler entries vs ~308k). Latent trigger (needs a cothread-side fault: driver bug, corrupted ring, stack overflow), so severity Low like DF-5587; the delta is the file-owned mask discipline + real-boot reproduction + real-boot fix validation.