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)
PoC verification
Evidence pack
findings/poc/DF-5607 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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:
- interposes
sigblock()for diagnostics β everycpu_mask_all_signals()(machintr.c:205-211, called at cothread.c:109 and cothread.c:143) passes through it; - interposes
nanosleep()β the vktimer cothread reaches it viacothread_sleep()(cothread.c:186-189, called fromvktimer_thread, systimer.c:244) aftertls_set_gs(0)(cothread.c:149) executed; the shim confirms the caller is a cothread by reading its %gs base with theget_tls_area(2)syscall (nr 473) β NULL only for cothreads; - injects
pthread_kill(self, SIGSEGV)there β indistinguishable from any real cothread-side fault (in-tree example: the deliberate*(volatile int *)0 = 1NULL-ring write invke_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, withoutFatal trap, without a core note on the console, without the ddbDebugger()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 innanosleep()(gdbinfo threads), i.e. no cpu thread ever ran the panic path. - 3/3 deterministic (three separate boots).
Root cause chain (path:line)
sys/platform/vkernel64/platform/cothread.c:149βtls_set_gs(0, ...)makes every mycpu dereference fatal in a cothread (by design).sys/platform/vkernel64/platform/cothread.c:109and:143β the cothread's signal discipline iscpu_mask_all_signals(), which blocks only SIGALRM|SIGIO|SIGQUIT|SIGUSR1|SIGTERM|SIGWINCH|SIGUSR2|SIGURG (sys/platform/vkernel64/platform/machintr.c:205-211) β not SIGSEGV/SIGBUS/SIGTRAP/SIGFPE.sys/platform/vkernel64/x86_64/exception.c:227-233β those fault signals are handled process-wide byexc_segfault, installedSA_SIGINFO|SA_NODEFERwith an emptysa_mask, so a fault taken inside the handler is re-delivered on the same stack, unbounded.sys/platform/vkernel64/x86_64/trap.c:585βkern_trap()'s first statement is the mycpu load that re-faults.
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_segfaultthen death β a diagnosable failure for the first time; - core (
fixed_core.txt):Program terminated with signal 11, Segmentation faultwith theraise β _thr_send_sig β lwp_killpath β 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
fixedGuest /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
Confirmed kernel references
- sys/platform/vkernel64/platform/cothread.c:109
- sys/platform/vkernel64/platform/cothread.c:143
- sys/platform/vkernel64/platform/cothread.c:149
- sys/platform/vkernel64/platform/cothread.c:186
- sys/platform/vkernel64/platform/machintr.c:205
- sys/platform/vkernel64/x86_64/exception.c:227
- sys/platform/vkernel64/x86_64/trap.c:585
- sys/dev/virtual/vkernel/net/if_vke.c:672
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.
No comments yet.