SIGXCPU omitted from cpu_disable_intr()/cpu_mask_all_signals() while stopsig() dereferences mycpu β one process-directed host signal silently kills the entire vkernel via the DF-5587 recursion
Summary
Both sigblock sets omit SIGXCPU while the installed handler stopsig executes gd=mycpu before anything else: a process-directed SIGXCPU (RLIMIT_CPU expiry or any same-uid host kill) routed to any cothread (%gs deliberately 0) faults at the mycpu load and enters the unbounded exc_segfault->kern_trap->mycpu recursion - whole-vkernel silent death, vCPUs parked mid-umtx_sleep. Unlike DF-5607's fault signals this is an asynchronous freely-maskable signal with zero protocol impact if masked (stop_cpus only targets vCPU threads; running vCPUs unblock everything at idle entry). Proven on a real vkernel with an observer-only shim: 3/3 dead on the FIRST kill -XCPU with the log showing delivery to a %gs=0x0 cothread and runtime masks proving the missing 0x800000 bit; add-SIGXCPU fix validated 2/2 (all 40 kills survived, deliveries redirected to vCPU threads, boot unregressed). Unprivileged same-uid host DoS ceiling.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5611 Β· 22 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| VERDICT.md | β | 6.5 KB | β raw | |
| siglog.c | β | 5.0 KB | view raw | |
| run5611.sh | β | 2.8 KB | view raw | |
| image5611.sh | β | 1.0 KB | view raw | |
| buildk.sh | β | 325 B | view raw | |
| build.sh | β | 210 B | view raw | |
| run.sh | β | 189 B | view raw | |
| fix.diff | β | 1.3 KB | view raw | |
| panic.txt | β | 1.4 KB | view raw | |
| env.txt | β | 635 B | view raw | |
| logs/vk5611.ctrl1.log | β | 1.9 KB | view raw | |
| logs/vk5611.fault1.log | β | 2.1 KB | view raw | |
| logs/vk5611.fault2.log | β | 2.1 KB | view raw | |
| logs/vk5611.fault3.log | β | 2.1 KB | view raw | |
| logs/vk5611.fixed1.log | β | 5.5 KB | view raw | |
| logs/vk5611.fixed2.log | β | 5.5 KB | view raw | |
| logs/vk5611.boot.ctrl1 | β | 3.0 KB | β download | |
| logs/vk5611.boot.fault1 | β | 3.1 KB | β download | |
| logs/vk5611.boot.fault3 | β | 3.1 KB | β download | |
| logs/vk5611.boot.fixed1 | β | 6.0 KB | β download | |
| logs/link5611.log | β | 124 B | view raw |
DF-5611 β SIGXCPU omitted from the vkernel's signal-block sets β one host signal silently kills the whole vkernel
Finding: cpu_mask_all_signals() (sys/platform/vkernel64/platform/machintr.c:204-210)
and cpu_disable_intr() (machintr.c:191-196) deliberately omit SIGXCPU from
the sets of host signals they block. SIGXCPU's handler stopsig()
(sys/platform/vkernel64/x86_64/exception.c:108-137) dereferences mycpu
(%gs-relative) before doing anything else β so a process-directed SIGXCPU
that the host kernel routes to any cothread (%gs == 0) faults at the mycpu
load and kills the entire vkernel via the DF-5587 unbounded
exc_segfaultβkern_trapβmycpu recursion: no panic, no dump request, no console
output. The same omission in cpu_disable_intr() leaves SIGXCPU deliverable
to a freshly-created AP vCPU pthread during the window between
pthread_create() (mp.c:462-465) and init_secondary()'s tls_set_gs()
(mp.c:369), where %gs is still the pthread TCB, not a privatespace.
Distinct from DF-5607 (fault signals on cothreads β those cannot be masked
without breaking the vkernel fault mechanism): SIGXCPU is asynchronous and
maskable in cothreads without side effects, because stop_cpus() /
restart_cpus() only ever pthread_kill() vCPU threads (mp.c:239, mp.c:260)
and a running vCPU unblocks everything at cpu_idle entry
(cpu_enable_intr β sigsetmask(0), cpu_regs.c:485). Natural triggers:
RLIMIT_CPU soft-limit expiry (the host kernel sends process-directed
SIGXCPU), or any same-uid host process / admin kill -XCPU <vkernel-pid>.
Reproduce (guest, root)
sh build.sh # /vkroot.img + siglog.so + kernel.debug (see env notes) sh run.sh # ctrl1(control) fault1-3(fault) fixed1-2(after fix.diff)
Success criterion (baseline): RESULT: DEAD at iteration 1, console shows
SIGLOG: XCPU-IN thr=<cothread> gs=0x0 immediately before silent death, and
gdb kernel.debug kernel.debug.core reports signal 4 (SIGILL) with the
innermost frames _get_mycpu (thread.h:77) β kern_trap (trap.c:585) β
exc_segfault (exception.c:269) on the cothread process, the vCPU threads
parked in umtx_sleep. After fix.diff: RESULT: ALIVE after 40 SIGXCPU
sends, all XCPU-IN lines on vCPU threads (nonzero %gs).
Files
siglog.cβ LD_PRELOAD observation-only shim (wraps sigaction(SIGXCPU), pthread_create, sigblock; logs thread id + %gs base at delivery time). The trigger itself is a realkill -XCPUfrom the driver.run5611.shβ boot/kill driver (control | fault modes).image5611.sh,buildk.sh,build.sh,run.shβ rig.fix.diffβ add SIGXCPU to both sigblock sets (+ comment), validated.panic.txtβ gdb backtrace of the death core (both fault runs identical).env.txt,logs/β full driver + console logs (ctrl/fault1-3/fixed1-2).
Observed (3/3 baseline runs, 2/2 fixed runs)
baseline fault1-3: DEAD at iteration 1; XCPU-IN thr=0x800a92dc0 (cothread #1,
start=cothread_thread) gs=0x0; core: signal 4, #0 _get_mycpu
thread.h:77 / #1 kern_trap trap.c:585 / #2 exc_segfault
exception.c:269; vCPUs parked in umtx_sleep.
fixed1-2: ALIVE after 40 sends; every XCPU-IN on the BSP vCPU
(thr=0x800a930c0, gs=0x8000400000 = privatespace);
runtime sigblock masks 0x20c0a000 / 0x6840e804 now include
the SIGXCPU bit (0x800000); boot unregressed.
DF-5611 VERDICT β reproduced (dos, certain) / fix validated
What was claimed
cpu_mask_all_signals() (machintr.c:204-210) and cpu_disable_intr()
(machintr.c:191-196) omit SIGXCPU. stopsig() (exception.c:108-137), the
installed SIGXCPU handler, executes gd = mycpu (exception.c:112) before
anything else. On a cothread β %gs deliberately cleared by
tls_set_gs(0, ...) (cothread.c:149) β the mycpu load
(movq %gs:globaldata, thread.h:77) faults, and the process-wide
exc_segfault handler (installed SA_SIGINFO|SA_NODEFER with an empty
sa_mask, exception.c:222-229) enters kern_trap() (trap.c:585) which itself
loads mycpu β unbounded recursion (the DF-5587 dynamic) β the vkernel dies
silently (SIGILL when the signal-frame push finally fails). One
process-directed SIGXCPU therefore destroys the whole virtual machine.
Not re-reported here (pass-2 discipline): DF-5607 owns the fault-signal exposure of the same mask function; DF-5587 owns the recursion mechanism and its fix locus; DF-5586 owns the unguarded exc_segfault-tail splz. The delta of this finding: an asynchronous, trivially maskable signal with a mycpu-dereferencing handler is left deliverable to %gs-less threads β with a fix that has no protocol side effects (stop_cpus/restart_cpus pthread_kill only vCPU threads, mp.c:239/mp.c:260; running vCPUs fully unblock at cpu_idle entry, cpu_regs.c:485; stopsig's sigsuspend set never contained SIGXCPU, exception.c:118-126).
How it was proven (real boot, real signal, observer-only instrumentation)
Rig: DF BSD guest, VKERNEL64 kernel.debug built from pristine /usr/src
(md5-verified machintr.c), booted -m 512m -r /vkroot.img -n 2 (BSP + 1 AP
host thread + 2 cothreads: vktimer + vkd). LD_PRELOAD=siglog.so only
observes: it wraps sigaction(SIGXCPU) (logs handler install), wraps
pthread_create (logs every host thread created + its start routine), wraps
sigblock (logs the exact masks), and wraps the SIGXCPU handler to log
{pthread_self, %gs base} at delivery time before calling the real
stopsig. The trigger is a real kill -XCPU <vkernel-child-pid> from the
driver script (the vkernel child's pid comes from -p /root/vk.pid;
writepid, init.c:1383).
Runtime proof of the enabling condition (control boot, no signals)
SIGLOG: SIGBLOCK mask=0x2040a000 β cpu_disable_intr(): ALRM|URG|IO|USR1 (machintr.c:194-195)
SIGLOG: SIGBLOCK mask=0x6840e004 β cpu_mask_all_signals(): QUIT|ALRM|TERM|URG|IO|WINCH|USR1|USR2
(machintr.c:207-209)
SIGXCPU bit = 0x800000 β absent from both, exactly as read from source.
SIGLOG: HOOK-XCPU handler=0x71fd50 β stopsig installed (init_exceptions)
SIGLOG: THREAD-CREATE ... start=0x7156c0 β start_ap (AP vCPU)
SIGLOG: THREAD-CREATE ... start=0x72b260 (x2) β cothread_thread (vktimer, vkd)
Baseline (fault): 3/3 runs, deterministic death on the FIRST signal
RESULT: DEAD at iteration 1 SIGLOG: XCPU-IN thr=0x0000000800a92dc0 0x0000000000000000 ... β cothread #1, %gs=0
0x800a92dc0 is the first cothread per the THREAD-CREATE trace. The console
then goes silent β no panic, no trap output. Core (13 MB, gdb on guest):
Program terminated with signal 4, Illegal instruction.
#0 _get_mycpu () at ./machine/thread.h:77 (movq %gs:globaldata)
#1 kern_trap (frame=0xb) at trap.c:585
#2 exc_segfault (...) at exception.c:269
threads: 1,2 = vCPUs parked in umtx_sleep (cpu_idle) β never aware;
3 = crashing cothread; 4 = other cothread in _umtx_sleep.
Identical to the DF-5607/DF-5587 signature: the death mechanism is the exc_segfault recursion; the entry into it is stopsig's mycpu load on the cothread, reachable by one unprivileged host signal because of the missing SIGXCPU bit in machintr.c.
Fix validation: patched mask set β 2/2 runs fully survive
fix.diff adds sigmask(SIGXCPU) to both sets (machintr.c:195/209 region).
Guest /usr/src patched, kernel.debug rebuilt (same link workaround), same
driver, same 40Γkill -XCPU:
fixed1: RESULT: ALIVE after 40 SIGXCPU sends
fixed2: RESULT: ALIVE after 40 SIGXCPU sends
SIGLOG: SIGBLOCK mask=0x20c0a000 ... β 4-set + SIGXCPU bit at runtime
every SIGLOG: XCPU-IN thr=0x800a930c0 gs=0x8000400000 β deliveries now land only
on vCPU threads; stopsig runs
benignly (stopped_cpus bit
clear β loop exits instantly)
Boot unregressed (full shell reached both fixed runs). Guest reset to clean-source snapshot afterwards.
Impact ceiling
Local/host DoS of the vkernel process: any same-uid host process (or RLIMIT_CPU expiry) kills an arbitrary vkernel with one signal, silently β no panic, no dump, no diagnostic. Not memory corruption, not guestβhost escape; consistent with the family's Low ceiling (same as DF-5607/5587). The AP-window arm (pre-tls_set_gs delivery, cpu_disable_intr's omission) is code-proven (mp.c:462-465 β mp.c:369) but a microsecond-scale boot race; the persistent, deterministically-proven arm is the cothread one.
Rig caveats (honest notes)
- The in-tree VKERNEL64 build is broken on this snapshot: the vkernel
Makefile links kernel.debug as a plain user program
(sys/platform/vkernel64/conf/Makefile:23) while kern_mib.c references
__build_id_start/__build_id_end, which only the never-passed ldscript.x86_64 PROVIDEs; additionally the guest default linker is gold (binutils234), which internal-errors on that script. Rig workaround (no /usr/src source edits):make DEBUG="-g -fuse-ld=bfd -Wl,--defsym, __build_id_start=0 -Wl,--defsym,__build_id_end=0" kernel.debug. Upstream should fix the Makefile (pass -T or drop the kern_mib refs for _KERNEL_VIRTUAL); worth a separate report, not a security finding. wait_status=127in the driver output is a quirk ofsh waiton the env(1)-wrapped job; the core file + gdbsignal 4is the authoritative death record.- Cores were 13 MB each and not archived; backtraces (panic.txt) are verbatim from gdb on the guest.
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 (both hunks), kernel.debug rebuilt, identical 40x kill -XCPU driver re-run twice: vkernel ALIVE through all 40 sends both times (baseline died on the first send 3/3), every delivery now landing on vCPU threads with %gs=0x8000400000 (privatespace), runtime sigblock mask now 0x20c0a000 (SIGXCPU bit present), boot path unregressed (full shell reached). Bad behavior gone.
findings/poc/DF-5611/fix.diff; logs/vk5611.fixed1.log and logs/vk5611.fixed2.log (RESULT: ALIVE after 40 SIGXCPU sends; SIGBLOCK mask=0x20c0a000; XCPU-IN gs=0x8000400000); contrast logs/vk5611.fault1-3.log (DEAD at iteration 1)
Confirmed kernel references
- sys/platform/vkernel64/platform/machintr.c:191
- sys/platform/vkernel64/platform/machintr.c:204
- sys/platform/vkernel64/x86_64/exception.c:108
- sys/platform/vkernel64/x86_64/exception.c:112
- sys/platform/vkernel64/x86_64/exception.c:222
- sys/platform/vkernel64/platform/cothread.c:149
- sys/platform/vkernel64/x86_64/mp.c:369
- sys/platform/vkernel64/x86_64/mp.c:462
- sys/platform/vkernel64/x86_64/cpu_regs.c:485
- sys/platform/vkernel64/x86_64/trap.c:585
Detail
Exploit chain
host same-uid process (or RLIMIT_CPU expiry) -> kill -XCPU
Evidence (decisive lines)
findings/poc/DF-5611/logs/vk5611.fault1.log (RESULT: DEAD at iteration 1 + XCPU-IN thr=0x800a92dc0 gs=0x0, the cothread identified by THREAD-CREATE start=0x72b260), panic.txt (gdb: signal 4, #0 _get_mycpu thread.h:77 / #1 kern_trap trap.c:585 / #2 exc_segfault exception.c:269, crashing thread = cothread, vCPUs in umtx_sleep), logs/vk5611.ctrl1.log (healthy control + runtime masks 0x2040a000/0x6840e004 proving the omitted SIGXCPU bit), logs/vk5611.fixed1.log + fixed2.log (ALIVE after 40 sends, mask 0x20c0a000, deliveries on vCPU threads), fix.diff
PoC changes
Authored from scratch (no seed). First design keyed on flooding the boot-time AP window (pthread_create..tls_set_gs); boot-order analysis showed SIGWINCH/SIGTERM handlers are not yet installed at mp_start time, so the reachable asynchronous exposure is SIGXCPU (stopsig, installed by init_exceptions pre-mi_startup) against the persistent cothread population - switched to post-boot kill -XCPU with the observation shim logging per-delivery %gs base. Driver needed the vkernel CHILD pid (main() forks a watchdog, init.c:204) via -p/writepid (init.c:1383).
Verified recommended fix
Add sigmask(SIGXCPU) to both sigblock sets in machintr.c (cpu_disable_intr and cpu_mask_all_signals) - exactly fix.diff; defense-in-depth pair: the DF-5607 exc_segfault re-entrancy guard converts any residual silent recursion into a diagnostic.
Verdict
cpu_disable_intr() (machintr.c:191-196) and cpu_mask_all_signals() (machintr.c:204-210) omit SIGXCPU from their sigblock sets while the installed SIGXCPU handler stopsig() (exception.c:108-137) dereferences mycpu before anything else: one process-directed SIGXCPU routed by the host to any cothread (%gs==0, cothread.c:149) faults at the mycpu load (thread.h:77) and kills the whole vkernel silently via the DF-5587 unbounded exc_segfault->kern_trap->mycpu recursion. Proven on a real VKERNEL64 boot (-m 512m -n 2) with an observer-only LD_PRELOAD shim and a real kill -XCPU: 3/3 baseline runs DEAD on the FIRST signal (XCPU-IN logged on the cothread with %gs base 0x0; core = signal 4 SIGILL, #0 _get_mycpu thread.h:77 / #1 kern_trap trap.c:585 / #2 exc_segfault exception.c:269, vCPU threads still parked in umtx_sleep). Runtime sigblock masks 0x2040a000 (cpu_disable_intr) and 0x6840e004 (cpu_mask_all_signals) confirm the missing SIGXCPU bit (0x800000) live. Unlike DF-5607's fault signals, SIGXCPU is maskable in cothreads with zero protocol impact (stop_cpus/restart_cpus pthread_kill only vCPU threads, mp.c:239/mp.c:260; running vCPUs unblock at cpu_idle entry, cpu_regs.c:485) - and fix.diff adding sigmask(SIGXCPU) to both sets was validated: 2/2 patched runs ALIVE through 40 kills with all deliveries landing on vCPU threads (%gs=0x8000400000), boot unregressed. Natural triggers: RLIMIT_CPU soft-limit expiry or any same-uid host kill -XCPU. Host-process DoS scope, Low like the rest of the vkernel family; the same omission also leaves a microsecond-scale pre-tls_set_gs AP window (mp.c:462-465 vs mp.c:369) via the inherited cpu_disable_intr mask.
No comments yet.