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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5611 Β· 22 files
FileTypeDescriptionSize
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 real kill -XCPU from 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.
VERDICT.md
↓ download raw

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=127 in the driver output is a quirk of sh wait on the env(1)-wrapped job; the core file + gdb signal 4 is 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Guest /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)
↓ fix.diffper-fix-DF-5611

Confirmed kernel references

Detail

Exploit chain

host same-uid process (or RLIMIT_CPU expiry) -> kill -XCPU (process-directed) -> host scheduler picks any thread not blocking SIGXCPU; cothreads do not block it (machintr.c:204-210 omission) -> stopsig() runs on cothread -> gd = mycpu (exception.c:112) faults with %gs=0 -> exc_segfault (SA_NODEFER, exception.c:222-229) -> kern_trap (trap.c:585) loads mycpu again -> unbounded recursion (DF-5587) -> signal-frame push failure -> SIGILL -> entire vkernel dead silently; vCPU threads never scheduled again.

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.