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

TSC cputimer registered with unvalidated tsc_frequency β†’ integer division by zero (boot-time kernel panic)

Summary

tsc_cputimer_register copies the calibration-derived tsc_frequency with no zero check and immediately calls cputimer_select, which unconditionally computes (1000000LL<<32)/freq - a fatal trap 18 #DE when calibration returned 0 (non-ticking TSC). Precondition producers: the hw.tsc_cputimer_force tunable (the one the kernel itself recommends for QEMU) and the UP early-out setting tsc_mpsync=1 without consulting the frequency. Live-verified: forced_boot proves the real production path; the verbatim-replay harness with tsc_frequency=0 through the REAL register/select entry points -> 'Fatal trap 18 ... Stopped at cputimer_select+0x29: divq', guest dead. Boot-time root/loader DoS only (boot-loaded KLDs link after POST_SMP - proven - so no precondition race). refuse-zero-at-both-ends fix validated on a rebuilt kernel (warning, guest alive, forced path still selects TSC).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5640 Β· 17 files
FileTypeDescriptionSize
df5640.c β€” 4.1 KB view raw
build.sh β€” 143 B view raw
run.sh β€” 144 B view raw
README.md β€” 3.0 KB ↓ raw
VERDICT.md β€” 5.9 KB ↓ raw
build.log β€” 5.6 KB view raw
run.log β€” 0 B ↓ download
panic_serial.log β€” 2.1 KB view raw
panic.txt β€” 385 B view raw
forced_boot_dmesg.txt β€” 1.1 KB view raw
env.txt β€” 338 B view raw
fix.diff β€” 1.2 KB view raw
fix_build.log β€” 225 B view raw
fix_run.log β€” 458 B view raw
fix_forced_boot.txt β€” 129 B view raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 4.6 KB view raw

DF-5640 β€” TSC cputimer registered with unvalidated tsc_frequency β†’ division by zero (boot-time panic)

File: sys/cpu/x86_64/misc/cputimer_tsc.c (pass 2) Β· Severity: Low (CWE-369)

What

tsc_cputimer_register() (cputimer_tsc.c:122-171) copies the hardware/calibration-derived global tsc_frequency into tsc_cputimer.freq (cputimer_tsc.c:145-146) without validation, then cputimer_select(&tsc_cputimer, 0) (cputimer_tsc.c:156) β†’ cputimer_set_frequency(timer, timer->freq) (kern/kern_cputimer.c:99) β†’

timer->freq64_usec = (1000000LL << 32) / freq;    /* kern_cputimer.c:190 */

tsc_frequency == 0 (failed calibration, clock.c:980-1043) combined with tsc_mpsync == 1 (hw.tsc_cputimer_force=1 β€” clock.c:871/1522, the tunable the kernel recommends for qemu β€” or the ncpus == 1 early-out at clock.c:1513-1516) β‡’ integer division by zero β‡’ fatal trap 18 during boot. The regcnt: invariant-not-mpsync path likewise installs tsc_cpucounter.freq = tsc_frequency unvalidated (cputimer_tsc.c:162).

Low severity: the trigger requires a loader (root) tunable or broken/lying hardware; there is no unprivileged runtime surface (boot SYSINIT only, all globals sysctl-RD after boot).

Reproduce on the dfbsd-qemu guest

./build.sh    # builds df5640.ko on the guest against /usr/src
./run.sh      # kldload -> Fatal trap 18 (integer divide fault) at
              # cputimer_select+0x29 (divq); guest drops to DDB, ssh dies

The harness replays cputimer_tsc.c:145-156 verbatim (replica timer, tsc_frequency = 0, real cputimer_register()/cputimer_select() entry points, pri = CPUTIMER_PRI_DUMMY-1 so only the pre-pri-check sink call at kern_cputimer.c:99 can fire). The faulting instruction is in production code, not the harness.

Also in the pack:

  • forced_boot_dmesg.txt β€” the real production path, no harness: booting with hw.tsc_cputimer_force=1 runs the whole qualification state machine and the genuine registration (TSC: cputimer freq 3600008821, kern.cputimer.name: TSC).
  • env.txt β€” stock guest introspection (HPET selected; tsc_mpsync=0, tsc_invariant=0 β€” the file is inert on the stock config).

Methodology constraint (why kldload-time replay): boot-loaded KLDs link at SI_BOOT2_KLD (0x1e00000), after SI_BOOT2_POST_SMP (0x1cc0000), so no boot-loaded module can precondition the globals before the registration SYSINIT runs β€” established empirically, see VERDICT.md.

Fix validation

fix.diff guards the producer (cputimer_tsc.c: both the mpsync path and the regcnt: path refuse tsc_frequency == 0) and the sink (kern_cputimer.c: cputimer_set_frequency() refuses zero). Applied in the guest's /usr/src, make nativekernel + installkernel, rebooted:

  • identical harness: cputimer: refusing zero frequency for DF5640 (cputimer unchanged) β€” no trap, module load returns, system healthy (fix_run.log);
  • legit path regression check: forced boot still selects the TSC cputimer (fix_forced_boot.txt).

See VERDICT.md for the full narrative and attempt log.

VERDICT.md
↓ download raw

DF-5640 β€” VERDICT

Status: reproduced (impact: panic / boot-time DoS, severity Low β€” privileged preconditions) β€” fix validated (fixed).

The finding

sys/cpu/x86_64/misc/cputimer_tsc.c:145-156 β€” tsc_cputimer_register() copies the hardware/calibration-derived global tsc_frequency into tsc_cputimer.freq with no validation, then cputimer_select(&tsc_cputimer, 0) β†’ cputimer_set_frequency(timer, 0) β†’ sys/kern/kern_cputimer.c:190 (1000000LL << 32) / freq β€” integer division by zero (#DE) β†’ fatal trap β†’ kernel panic during boot (SI_BOOT2_POST_SMP). The regcnt: path likewise installs tsc_cpucounter.freq = tsc_frequency unvalidated (cputimer_tsc.c:162).

Preconditions for tsc_mpsync == 1 with tsc_frequency == 0: - hw.tsc_cputimer_force=1 loader tunable (clock.c:871 sets tsc_invariant, clock.c:1522 sets tsc_mpsync) β€” the tunable the kernel itself recommends for qemu (clock.c:863-871) β€” combined with startrtclock() calibration returning 0 (clock.c:980-1043; a non-ticking TSC measures 0), or - ncpus == 1 with CPUID-invariant TSC (clock.c:1513-1516 sets tsc_mpsync=1 unconditionally) with failed calibration.

No unprivileged trigger exists: the file has zero runtime attack surface (boot SYSINIT + loader tunables only). Hence Low, not High.

How it was verified (guest dfbsd-qemu, kernel of 2026-07-02, #0)

  1. Live introspection (env.txt): stock guest runs HPET (kern.cputimer.name: HPET), hw.tsc_mpsync=0, hw.tsc_invariant=0, hw.tsc_frequency=3600011246 β€” on the stock config the file's registration early-returns at cputimer_tsc.c:134.

  2. Production-path proof (forced_boot_dmesg.txt): booting with hw.tsc_cputimer_force=1 (the real documented tunable, no harness) runs the genuine path end-to-end: the mpsync qualification state machine (TSC testing MP synchronization ... TSC is MP synchronized, clock.c:1567-1705) then the real tsc_cputimer_register() prints TSC: cputimer freq 3600008821 (cputimer_tsc.c:148) and kern.cputimer.name becomes TSC.

  3. Baseline panic (run.log, panic_serial.log, panic.txt): a KLD harness replays cputimer_tsc.c:145-156 verbatim (replica struct, tsc_frequency = 0, real cputimer_register()/cputimer_select() production entry points; replica pri = CPUTIMER_PRI_DUMMY-1 so the sink call at kern_cputimer.c:99 β€” which precedes the pri check β€” is the only thing that can fire). Result, live serial console:

DF5640: live state: tsc_mpsync=1 tsc_invariant=1 tsc_frequency=3600008821 sys_cputimer=TSC (3600008821 Hz) DF5640: replaying cputimer_tsc.c:145-156 with freq=0 (divide error at kern_cputimer.c:190 expected) TSC: cputimer freq 0 Fatal trap 18: integer divide fault while in kernel mode kernel: type 18 trap, code=0 Stopped at cputimer_select+0x29: divq %rcx,%eax db>

Guest dropped to DDB, ssh dead, vm.sh status β†’ down. The faulting instruction is divq inside cputimer_select (production code); the harness never executes the faulting instruction itself.

Methodology note: a boot-loaded KLD cannot put the globals into the precondition state before tsc_cputimer_register runs β€” preloaded modules link at SI_BOOT2_KLD (0x1e00000), after SI_BOOT2_POST_SMP (0x1cc0000). Proven empirically: a forcing SYSINIT at SI_BOOT2_FINISH_SMP from a loader-loaded module executed after the whole registration pass (dmesg ordering showed DF5640: FORCING ... with no TSC: cputimer freq line, and the boot completed). Hence the kldload-time verbatim replay.

  1. Fix validation (fix.diff, fix_build.log, fix_run.log, fix_forced_boot.txt): fix applied inside the guest's /usr/src copy (producer guards in cputimer_tsc.c at both the mpsync path and the regcnt: path + sink guard in kern_cputimer.c), make nativekernel KERNCONF=X86_64_GENERIC + installkernel (RC=0), reboot into the patched kernel (#1 Fri Oct 9 03:49:03 UTC 2026). Identical harness:

DF5640: replaying cputimer_tsc.c:145-156 with freq=0 ... TSC: cputimer freq 0 cputimer: refusing zero frequency for DF5640 (cputimer unchanged) DF5640: NOT REACHED on an unpatched kernel SURVIVED-rc=0 kern.cputimer.name: HPET (unchanged, system healthy)

No trap, module load returns, guest stays up. Legit-path regression check on the patched kernel: hw.tsc_cputimer_force=1 boot still prints TSC: cputimer freq 3600009374 and selects kern.cputimer.name: TSC β€” the guards do not affect the normal registration path.

Why not higher severity

Memory corruption this is not: it is a deterministic #DE. The operand (tsc_frequency) is set only at boot from CPUID/calibration; all three involved globals are read-only sysctls afterwards; the forcing tunable lives in /boot/loader.conf (root). The realistic scenario is a forced-TSC misconfiguration on a machine/VM whose TSC calibration fails β†’ unbootable system (local DoS at boot, admin-fixable). The cpucounter half is currently dead surface: cpucounter_find()/cpucounter_find_pcpu() have no callers outside kern_cputimer.c in this tree.

Attempts

  1. Boot-forcing SYSINIT in loader-loaded KLD β€” failed by design (module linking happens after POST_SMP); documented, valuable negative ordering evidence.
  2. Same module via /boot/modules β€” directory absent on guest image; installed to /boot/kernel; ran but (per 1) too late to affect the production path.
  3. kldload-time verbatim replay β€” panicked deterministically at cputimer_select+0x29 (divq).

Guest reset to the clean with-src snapshot after validation.

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

fix.diff applied in guest /usr/src; make nativekernel KERNCONF=X86_64_GENERIC + installkernel (RC=0, fix_build.log); rebooted into patched kernel #1 Oct 9 03:49:03. Identical harness: no trap, 'cputimer: refusing zero frequency for DF5640 (cputimer unchanged)', module load returns 0, system healthy with HPET unchanged. Legit-path regression: hw.tsc_cputimer_force=1 boot still selects the TSC cputimer (TSC: cputimer freq 3600009374, kern.cputimer.name=TSC).

fix_run.log (warning + SURVIVED-rc=0), fix_build.log (kernel install completed RC=0), fix_forced_boot.txt (legit forced path still registers TSC), fix.diff (git apply -p0 --check clean against sys/)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Fri Oct 9 03:49:03 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt: 'Fatal trap 18: integer divide fault while in kernel mode' + 'Stopped at cputimer_select+0x29: divq %rcx,%eax'; panic_serial.log: full serial console with DF5640 live-state lines showing tsc_mpsync=1/tsc_frequency=0 precondition then 'TSC: cputimer freq 0'; forced_boot_dmesg.txt: the REAL production path executed under hw.tsc_cputimer_force=1 ('TSC: cputimer freq 3600008821', kern.cputimer.name=TSC); fix_run.log: patched kernel prints 'cputimer: refusing zero frequency for DF5640 (cputimer unchanged)' and survives (SURVIVED-rc=0)

PoC changes

Seed PoC sketch replaced entirely: (1) a boot-loaded KLD cannot precede tsc_cputimer_register because preloaded modules link at SI_BOOT2_KLD (0x1e00000) after SI_BOOT2_POST_SMP (0x1cc0000) -- proven empirically with a forcing SYSINIT that ran too late; (2) final harness replays cputimer_tsc.c:145-156 verbatim at kldload time on a replica struct (pri=CPUTIMER_PRI_DUMMY-1 so only the pre-pri-check sink call at kern_cputimer.c:99 can fire), driving the real cputimer_register()/cputimer_select() production entry points with tsc_frequency=0.

Verified recommended fix

Guard tsc_frequency==0 before registering the TSC cputimer/cpucounter (cputimer_tsc.c, both the mpsync path and the regcnt path) and add a zero-frequency guard to cputimer_set_frequency() (kern_cputimer.c).

Verdict

tsc_cputimer_register() (cputimer_tsc.c:145-156) copies the unvalidated hardware/calibration-derived tsc_frequency into tsc_cputimer.freq and calls cputimer_select(), whose cputimer_set_frequency() divides (1000000LL<<32)/freq (kern_cputimer.c:190) -> #DE -> fatal trap 18 -> boot-time kernel panic whenever tsc_mpsync was forced (hw.tsc_cputimer_force=1, or ncpus==1 with invariant TSC) while TSC calibration returned 0. Reproduced live on the dfbsd-qemu guest: harness replayed the registration body verbatim with freq=0 via the real cputimer_register/cputimer_select entry points; production divq faulted at cputimer_select+0x29, guest dropped to DDB, ssh died. Severity Low: trigger requires loader (root) tunable or lying/broken hardware; no unprivileged runtime surface exists (all three globals are boot-time only, sysctl RD).