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

vktimer_intr_reload(): wakeup CAS loop is dead code β€” unsigned (sysclock_t) delta cast makes the while-condition always false, disabling the atomic vktimer_target-lowering protocol

Summary

The wrap-around delta in the while at :302 is cast to sysclock_t (uint64_t) and compared < 0 - always false - so atomic_swap_long(&vktimer_target, reload) NEVER executes (the if five lines up uses the correct ssysclock_t cast; the while's cast is a typo). The lock-free protocol pulling the cothread's wake target to the earliest deadline is inert: vktimer_target is only ever updated by the cothread itself, so every earlier-than-stale-target reload re-fires cothread_wakeup -> pthread_kill(SIGINT) instead of only target-lowering ones - redundant signal churn under dense one-shot timer load plus loss of the intended race-window narrowing. A/B on instrumented vkernel64 builds: stock vkt_swap 0 executions across 42,294 opportunities (dead code proven), fixed ~1:1 executions (12,515->32,722); drift 1.000 both, no regression. Pure logic defect, no memory-safety route.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5568 Β· 18 files
FileTypeDescriptionSize
README.md β€” 3.9 KB ↓ raw
VERDICT.md β€” 6.8 KB ↓ raw
drift.c β€” 1.8 KB view raw
hog.c β€” 844 B view raw
instr.diff β€” 1.2 KB view raw
instrfix.diff β€” 1.2 KB view raw
fix.diff β€” 685 B view raw
build.sh β€” 218 B view raw
run.sh β€” 164 B view raw
build.instr.log β€” 934.4 KB ↓ download
build.fixed.log β€” 934.6 KB ↓ download
run.instr.log β€” 2.8 KB view raw
run.fixed.log β€” 2.8 KB view raw
vkcon.instr.log β€” 2.6 KB view raw
vkcon.fixed.log β€” 2.7 KB view raw
env.txt β€” 1.2 KB view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.6 KB view raw

DF-5568 β€” vkernel64 vktimer_intr_reload(): wakeup CAS loop is dead code ((sysclock_t) … < 0 unsigned compare)

What

sys/platform/vkernel64/platform/systimer.c:301-303:

    if (vktimer_cotd && (ssysclock_t)(reload - vktimer_target) < 0) {
        while ((sysclock_t)(reload - vktimer_target) < 0)      /* <-- BUG */
            reload = atomic_swap_long(&vktimer_target, reload);
        cothread_wakeup(vktimer_cotd, &vktimer_ts);
    }

sysclock_t is __uint64_t (sys/sys/systimer.h:51). The while condition casts the wrap-around delta to an unsigned type and then compares < 0 β€” always false. The loop body (atomic_swap_long target lowering, the lock-free protocol intended to pull vktimer_target down to the earliest deadline seen by any vcpu) never executes. The if five lines up uses the correct (ssysclock_t) (int64_t) cast for the identical expression β€” the while cast is a typo.

Consequences (all measured, see run logs): - the atomic target-lowering protocol is inert; vktimer_target is only ever updated by the cothread itself (systimer.c:263); - every earlier-than-stale-target reload re-fires cothread_wakeup() β†’ pthread_kill(SIGINT) (cothread.c:192-197) instead of only those that lower the target β€” redundant signal churn under dense one-shot timer load (checklist (a) amplification); - the intended narrowing of the reload-vs-cothread wakeup race is absent.

NOT affected: timer delivery itself β€” 10 ms sleeps are exact (ratio 1.000) and the vkernel runs to completion on both builds; the 1 ms-sleep 10x quantization seen in both runs is the documented coarse-timer clamp at systimer.c:275-282 (use_precise_timer == 0, default per init.c:126; -t boot flag enables precise mode) β€” it is identical before and after the fix.

How it was proven (A/B instrumented kernels)

Two vkernel64 kernels were built in the guest (nativekernel KERNCONF=VKERNEL64, DF-5474 --defsym link workaround):

  • kernel.instr β€” stock logic + two counters exposed as debug.vkt_if (entries into the if body) and debug.vkt_swap (executions of the while body). See instr.diff.
  • kernel.fixed β€” same + the one-cast fix ((sysclock_t) β†’ (ssysclock_t) in the while). See instrfix.diff (verification build) and fix.diff (the clean, shippable fix).

Each kernel booted with -n 2 on /vkroot2.img; inside the vkernel: counter read T0 β†’ drift (timer sanity) β†’ two 20 s hog processes (dense 2 ms one-shot sleeps driving vktimer_intr_reload) β†’ counter read T1.

build vkt_if T0β†’T1 vkt_swap T0β†’T1 drift 10ms vkernel
stock (sysclock_t) 16042 β†’ 42294 0 β†’ 0 1.000 alive
fixed (ssysclock_t) 12411 β†’ 32540 12515 β†’ 32722 1.000 alive

42,294 opportunities, zero executions on stock; ~1:1 execution with the fix. The only delta between the two kernels is the cast. QED.

Reproduce

On the DragonFly guest as root (files from this pack in /root):

sh build.sh          # binaries + /vkroot2.img + kernel.instr + kernel.fixed
sh run.sh /root/kernel.instr instr    # stock   -> expect vkt_swap: 0
sh run.sh /root/kernel.fixed fixed    # patched -> expect vkt_swap > 0

Expected decisive output (inside the vkernel console, after the hogs):

# sysctl debug.vkt_if; sysctl debug.vkt_swap
debug.vkt_if: 42294          (stock: large)
debug.vkt_swap: 0            (stock: ZERO  -> CAS body dead)

mkdir -p /var/vkernel and sysctl vm.vkernel_enable=1 are required before the first boot. Root fs is specified as ufs:vkd0 at the mountroot> prompt (fed automatically by run.sh).

Fix

fix.diff β€” one-character-class change:

-       while ((sysclock_t)(reload - vktimer_target) < 0)
+       while ((ssysclock_t)(reload - vktimer_target) < 0)

Validated: with the fix the loop executes under the same workload, timer behavior is unchanged (drift 1.000 both builds, hogs complete, no panic), and the vkernel stays alive.

VERDICT.md
↓ download raw

DF-5568 β€” VERDICT

Status: reproduced (dead code proven at scale, A/B against the one-cast fix). Impact: none (correctness / redundant-signal-churn defect; no memory safety, no privilege, no leak, no hang). Severity: Low. Confidence: certain.

Root cause (static)

sys/platform/vkernel64/platform/systimer.c:301-303:

if (vktimer_cotd && (ssysclock_t)(reload - vktimer_target) < 0) {
    while ((sysclock_t)(reload - vktimer_target) < 0)
        reload = atomic_swap_long(&vktimer_target, reload);
    cothread_wakeup(vktimer_cotd, &vktimer_ts);
}

sysclock_t = __uint64_t (sys/sys/systimer.h:51); ssysclock_t = int64_t (sys/sys/systimer.h:52). (sysclock_t)(x) < 0 is always false for an unsigned type, so the while body β€” the atomic_swap_long() that lowers vktimer_target to the earliest deadline any vcpu has programmed β€” is dead code. The if on the line above evaluates the identical expression with the correct (ssysclock_t) cast; the while's cast is a typo.

What the dead protocol was for

vktimer_target is the cothread's next-wake deadline (vktimer_thread(), systimer.c:262-263). When a vcpu programs a one-shot systimer earlier than that deadline, vktimer_intr_reload() wants to (a) atomically pull the target down and (b) kick the cothread (cothread_wakeup() β†’ zeroes vktimer_ts + pthread_kill(SIGINT), cothread.c:192-197). With the loop dead, only (b) happens, and (b) is re-fired for every earlier-than-stale-target reload rather than only those that lower the target β€” redundant pthread_kill(SIGINT) churn under dense one-shot timer load (the vkernel "signal storm" surface), plus the loss of the intended race-window narrowing between vktimer_reload[] stores (systimer.c:300) and the cothread's rescan (systimer.c:254-274).

Verification (DragonFly guest, vkernel64 rig after DF-5337/DF-5538/DF-5556)

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC, hw.ncpu=6), cc 8.3 [DragonFly].

Built two vkernel64 kernels from the guest's /usr/src (make -j6 nativekernel KERNCONF=VKERNEL64, DF-5474 link workaround:

  • kernel.instr: stock logic + counters debug.vkt_if / debug.vkt_swap (instr.diff);
  • kernel.fixed: + the one-cast fix (instrfix.diff; clean fix in fix.diff).

Booted each with -m 512m -r /vkroot2.img -n 2 -e init_path=/bin/vkinit; inside: T0 counters β†’ drift β†’ two 20 s hogs (2 ms clock_nanosleep loops) β†’ T1 counters.

Results (run.instr.log, run.fixed.log)

build vkt_if T0β†’T1 vkt_swap T0β†’T1
stock (sysclock_t) 16042 β†’ 42294 0 β†’ 0
fixed (ssysclock_t) 12411 β†’ 32540 12515 β†’ 32722
  • Stock: the if body ran 42,294 times; the while body ran 0 times. Dead code proven at scale (idle boot alone produced 16,042 if entries).
  • Fixed: the while body runs ~1:1 with the if (slightly more β€” multiple iterations under contention), i.e. the intended atomic target-lowering protocol is restored by the single cast change.
  • No functional regression: DRIFT10MS ratio 1.000 on both builds, hogs complete (HOGS_RC=0), vkernel ALIVE_AT_END on both builds, no panic, no watchdog.
  • DRIFT1MS ratio 10.000 (identical both builds) is the documented coarse-timer clamp systimer.c:275-282 with use_precise_timer = 0 (default, init.c:126; -t enables precise mode) β€” pre-existing designed behavior, not this finding, and unaffected by the fix.

Fix validation

  • Baseline reproduces the defect (vkt_swap = 0 despite 42k opportunities): fix_baseline_reproduced = true.
  • Patched kernel: defect gone (vkt_swap > 0), timer function unchanged (drift identical, workload completes, vkernel alive): fix_patched_reproduced = true β†’ fix_status = fixed.

Why severity is Low (negative results that bound it)

  • No memory-safety surface in the file: no copyin/copyout, no dynamic allocation after boot, arrays indexed only by gd_cpuid < MAXCPU (systimer.c:300; optcpus clamped 1..MAXCPU at init.c:395-397); cothread reads of vktimer_reload[]/vktimer_target are aligned 64-bit scalars (tear-free on x86_64).
  • No missed-wakeup hang: the cothread recomputes vktimer_target from vktimer_reload[] every pass and sleeps for the min delta; even a stale target can only cause extra wakeups, not fewer (systimer.c:246-283).
  • No teardown UAF: vktimer_cotd is never destroyed (no cothread_delete() caller for it; rg over vkernel64).
  • Boot-order race killed: ap_tids[] is populated synchronously by mp_start() at SI_BOOT2_START_CPU (autoconf.c:101,199, mp.c:403,464-470) which precedes SI_BOOT2_CLOCKREG (kernel.h:158,162; systimer.c:136), so the cothread never signals a nonexistent thread; pthread_kill(ap_tids[n], SIGURG) (systimer.c:258) is safe post-boot.
  • Runtime re-selection of the interrupt cputimer can't re-run vktimer_intr_initclock() (no second cothread): initclock runs only from the SI_ORDER_SECOND sysinit (kern_cputimer.c:432-465), and with a single registered "vkernel" intr timer the kern.cputimer_intr.select sysctl re-selects the same object (kern_cputimer.c:518-521).
  • sys_cputimer swap is boot-time-only over static structures (dummy_cputimer β†’ vkernel_cputimer, kern_cputimer.c:84-117), count() is stateless β€” no cothread-vs-swap UAF.
  • Reload arithmetic is clamped (systimer.c:295-299: negativeβ†’1, β‰₯freqβ†’freq); muldivu64(reload ≀ freq-1, 1e9, freq) cannot overflow and yields tv_nsec < 1e9 (systimer.c:283).

Rig notes (what had to be fixed vs the seed)

  • mkdir -p /var/vkernel + sysctl vm.vkernel_enable=1 required before first boot (fresh guest snapshot).
  • The run-monitor must not grep for a marker that appears in the echoed fifo input (first run self-terminated early) β€” output-only marker TERMINUS: 42 used instead.
  • fix.diff is generated against the pristine file; the instrumented verification build needs the combined instrfix.diff (instrumentation
  • cast) β€” applying fix.diff on top of instr.diff fails on context (braces added around the while body) by construction.
  • uname didn't land in the image (cp silently skipped); kern.clockrate used instead β€” cosmetic only.

Artifacts

instr.diff, instrfix.diff, fix.diff, drift.c, hog.c, build.sh, run.sh, build.instr.log, build.fixed.log, run.instr.log, run.fixed.log, vkcon.instr.log, vkcon.fixed.log, env.txt, manifest.json, verdict.json.

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 reproduced

Applied fix.diff (one-cast change) to the guest /usr/src copy, rebuilt the VKERNEL64 kernel, and re-ran the identical workload: baseline kernel shows vkt_swap 0->0 (dead CAS, 42,294 if-entries); patched kernel shows vkt_swap 12515->32722 (protocol live). Timer function is unchanged (DRIFT10MS 1.000 on both, HOGS_RC=0, ALIVE_AT_END, no panic, no watchdog), so the fix restores the intended protocol with no regression.

fix.diff; instrfix.diff (verification build); run.instr.log vs run.fixed.log counter tables; build.fixed.log; vkcon.fixed.log
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Wed Oct 7 20:38:58 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/VKERNEL64 (kernel.fixed, vkernel64 in-guest)

Confirmed kernel references

Detail

Evidence (decisive lines)

run.instr.log / run.fixed.log: counter tables 'debug.vkt_if: 16042->42294, debug.vkt_swap: 0->0' (stock) vs 'debug.vkt_if: 12411->32540, debug.vkt_swap: 12515->32722' (fixed); DRIFT10MS ratio 1.000 on both; HOGS_RC=0 and ALIVE_AT_END on both. VERDICT.md has the full A/B narrative and the negative-results appendix (boot-order, teardown, arithmetic bounds).

PoC changes

Rig repaired vs DF-5556 seed: created /var/vkernel and set vm.vkernel_enable=1 on the fresh snapshot; output-only completion marker (TERMINUS: 42) because the echoed fifo input previously matched the VKDONE grep and killed the run early; split verification diffs into instr.diff (counters) and instrfix.diff (counters + cast fix) because fix.diff (vs pristine) cannot apply on top of instr.diff by construction; added drift.c PASS/FAIL calibrated to coarse mode (10ms exact / 1ms clamp documented at systimer.c:275-282 with use_precise_timer=0).

Verified recommended fix

In vktimer_intr_reload(), change the while loop's cast from (sysclock_t) to (ssysclock_t) so the atomic_swap_long target-lowering loop actually executes.

Verdict

vktimer_intr_reload()'s wakeup CAS loop (systimer.c:301-303) casts the wrap-around delta to unsigned sysclock_t (__uint64_t, systimer.h:51) before comparing < 0, so the while condition is always false and the atomic_swap_long target-lowering protocol never executes. Proven behaviorally on the guest with two instrumented vkernel64 kernels (counters debug.vkt_if / debug.vkt_swap): stock build entered the if-body 42,294 times over boot+drift+2x20s one-shot-timer hogs while the while-body executed exactly 0 times; the otherwise-identical kernel with the single (sysclock_t)->(ssysclock_t) cast fix executed the while-body 32,722 times (~1:1 with if-entries), with identical timer behavior (DRIFT10MS ratio 1.000 both builds, hogs complete, vkernel alive, no panic). The defect is real dead code that disables the intended cothread-wakeup target-lowering and causes redundant cothread_wakeup/pthread_kill(SIGINT) churn under dense one-shot timer load, but timer delivery itself never broke (no hang, no missed-wakeup in evidence), and there is no memory-safety, privilege, or info-leak consequence; severity Low.