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

kern.stathz boot tunable ingested with no validation β†’ kernel integer-divide fault at statclock systimer registration (unbootable system)

Field Value
ID DF-2946
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-369 / CWE-20
File sys/kern/subr_param.c
Lines 201-203 (sinks: kern_systimer.c:270-273, kern_cputimer.c:197-200)
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

init_param1() overwrites the derived stathz (hz+1) with the raw kern.stathz loader tunable and performs no validation. The value flows untouched into initclocks_other(), which registers the per-CPU statclock systimer with SYSTF_MSSYNC; _systimer_init_periodic() then computes the period with an unguarded '1000 / freq' (MSSYNC path) and cputimer_default_fromhz() computes 'freq / freq' on the other branch. kern.stathz=0 boots past init_param1 (hz itself stays valid, so the DF-0173 in-file divides succeed) and kills the kernel later at SI_BOOT2_POST_SMP. VERIFIED on the guest: single line in /boot/loader.conf β†’ 'Fatal trap 18: integer divide fault while in kernel mode', 'Stopped at ... idivq %r12,%eax', dead in DDB, ssh never up. Negative stathz yields a negative period (garbage expiry math, broken statclock); huge stathz yields a sub-nanosecond statclock period (interrupt storm). The tree even guards stathz==0 for the clockrate sysctl ('stathz ? stathz : hz') β€” the systimer path is the omission. Distinct from DF-0173 (different tunable, later sink) and from the DF-2915/16/17 sleepq/systimer API defects. The hz>1000000 variant (ustick==0 β†’ post-boot UNPRIVILEGED divide-by-zero via tv.tv_usec/ ustick in kern_time/bpf) and hz<0 variant fold into the same fix locus (init_param1 validation). Anyone with /boot/loader.conf control makes the machine permanently unbootable with one line β€” same trust boundary as DF-0173/DF-2894; persistent boot DoS ceiling.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_param.c (GLM 5.3); unbootable state reproduced first attempt on stock guest.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2946 Β· 10 files
FileTypeDescriptionSize
README.md file 2.3 KB ↓ raw
VERDICT.md file 4.1 KB ↓ raw
env.txt file 218 B view raw
fix.diff file 1.2 KB view raw
manifest.json file 1.3 KB view raw
panic.txt file 5.8 KB view raw
plant.sh file 1.7 KB view raw
run.sh file 650 B view raw
sha256.txt file 148 B view raw
verdict.json file 4.8 KB view raw
README.md file
↓ download raw

DF-2946 β€” kern.stathz boot tunable fetched with no validation β†’ boot-time integer divide fault

sys/kern/subr_param.c:202 (init_param1) overwrites the computed stathz (default hz + 1) with the raw loader tunable kern.stathz and never validates it. The value flows into initclocks_other() (sys/kern/kern_clock.c:438-441), which registers the per-CPU statclock systimer with SYSTF_MSSYNC. _systimer_init_periodic() (sys/kern/kern_systimer.c:270-273) then computes the timer period with an unguarded division:

if ((flags & SYSTF_MSSYNC) && freq <= 1000)
        info->periodic = sys_cputimer->fromhz(1000) * (1000 / freq);  /* 1000/0 */
else
        info->periodic = sys_cputimer->fromhz(freq);                  /* freq/freq */

and cputimer_default_fromhz() (sys/kern/kern_cputimer.c:197-200) divides sys_cputimer->freq / freq on the other branch.

  • kern.stathz="0" β†’ integer divide fault panic during boot (demonstrated).
  • kern.stathz="-N" β†’ signed period, garbage modulo/expiry math, broken statclock.
  • kern.stathz huge β†’ multi-GHz statclock (interrupt storm / unusable system).

Sibling of DF-0173 (kern.hz=0 divides at subr_param.c:204-207 inside init_param1); this member survives init_param1 and detonates later, at SI_BOOT2_POST_SMP. The clkinfo sysctl handler even guards for this (stathz ? stathz : hz, kern_clock.c:1337) β€” the systimer path does not.

Reproduce (host side, guest up & clean)

dfbsd-qemu/vm.sh run_root 'cp /boot/loader.conf /boot/loader.conf.df2946bak && printf "kern.stathz=\"0\"\n" >> /boot/loader.conf && sync'
dfbsd-qemu/vm.sh run_root 'shutdown -r now'
sleep 75; dfbsd-qemu/vm.sh status     # -> down
grep -n "Fatal trap" dfbsd-qemu/boot.log
dfbsd-qemu/vm.sh reset with-src

Expected output

Guest never reaches ssh (vm.sh status β†’ down ~75 s after reboot command); serial console (dfbsd-qemu/boot.log, see panic.txt) shows, immediately after "Initialize MI interrupts for 6 cpus":

Fatal trap 18: integer divide fault while in kernel mode
cpuid = 0; lapic id = 0
...
kernel: type 18 trap, code=0
CPU0 stopping CPUs: 0x0000003e
Stopped at      -0x7f9a53d9:    idivq   %r12,%eax
db>

Impact: unbootable system (boot-time kernel panic). Trust boundary: /boot/loader.conf (root / loader prompt / console), same as DF-0173.

VERDICT.md file
↓ download raw

DF-2946 β€” VERDICT

Status: reproduced (impact: panic β€” boot-time kernel divide fault; single-variable, pristine guest)

What was run

Stock guest, kernel DragonFly 6.5-DEVELOPMENT #0 (INVARIANTS build, X86_64_GENERIC, 6 vCPU, 4 GB). Single change: one line appended to /boot/loader.conf:

kern.stathz="0"

then shutdown -r now.

What happened (vs. what was predicted)

Predicted: 1000 / freq (kern_systimer.c:271, SYSTF_MSSYNC path, or fromhz() at kern_cputimer.c:199) traps during initclocks_other() (SI_BOOT2_POST_SMP).

Observed (dfbsd-qemu/boot.log, full excerpt in panic.txt): the kernel boots through init_param1 without complaint (kern.stathz=0 is merely assigned β€” subr_param.c:202 has no validation), gets through SMP/MI-interrupt initialization ("Initialize MI interrupts for 6 cpus"), and dies with:

Fatal trap 18: integer divide fault while in kernel mode
cpuid = 0; lapic id = 0
kernel: type 18 trap, code=0
CPU0 stopping CPUs: 0x0000003e
Stopped at      -0x7f9a53d9:    idivq   %r12,%eax
db>

idivq = the 64-bit signed division of the period computation; the boot position matches initclocks_other() exactly (it runs right after MI interrupt init for all CPUs, before userland). Guest sits in DDB; ssh never comes up; vm.sh status β†’ down. Reproduced on the first attempt; single variable planted (loader.conf otherwise untouched).

Why it is a finding and not a duplicate

  • DF-0173 covers kern.hz=0 β†’ division inside init_param1 (subr_param.c:204-207: ustick/nstick/ntp_default_tick_delta). With kern.stathz=0 alone, hz stays 100, those divisions succeed, the system passes init_param1 β€” and dies later in the systimer subsystem.
  • DF-2915/2916/2917 are sleepq/systimer API defects, not clock-tunable validation.
  • The codebase itself knows stathz can be zero β€” sysctl_kern_clockrate exports stathz ? stathz : hz (kern_clock.c:1337) β€” but the systimer registration path (kern_clock.c:439) passes it unguarded.

Trust boundary / severity

Boot-tunable (loader.conf / loader prompt / console) β€” requires root or boot-loader control, identical to DF-0173 (rated Low). No post-boot or unprivileged write path exists (kern.stathz has no runtime sysctl; maxprocperuid is RW but stathz is not). Ceiling: unbootable system. Rated Low (local boot DoS, privileged trigger), consistent with DF-0173.

Negative results worth recording

  • kern.nbuf="-1048576" was planted first-hand and did not break boot: every nbuf cap in cpu_startup (machdep.c:424-458) compares long nbuf against unsigned vm_offset_t expressions, so the negative value is promoted to a huge unsigned, "capped due to kvm" then "due to physmem", and the system boots with a sane positive nbuf (129940 on this guest, warnings in boot.log). The apparent negative-nbuf valloc-corruption hypothesis is refuted on pc64 (same unsigned arithmetic in vkernel64 autoconf.c:156-158 rescues it there too).
  • MSGBUFSIZE/msgbuf sizing: compile-time only (opt_msgbuf.h), no kenv tunable anywhere β€” not attacker-influenced in this threat model.
  • maxthrssiz is fetched (subr_param.c:230) but has zero consumers in the tree β€” dead knob.
  • vmm_vendor[16] is written only from the 12-char CPUID hypervisor signature (initcpu.c:195,204, memcpy of 13 bytes incl. NUL) β€” bounded, hardware-trusted.
  • 32-bit limsize overflow in init_param2 is unreachable: only pc64 and vkernel64 call it (both 64-bit; (size_t)physpages * PAGE_SIZE ≀ 2^43).
  • kern.hz > 1000000 β†’ ustick == 0 β†’ post-boot unprivileged divide-by-zero via tv.tv_usec / ustick (kern_time.c:511 and ~8 more sites) β€” same root cause and fix locus as DF-0173 (hz validation in init_param1); noted here to make the fix clamp the upper bound too, not re-filed as a separate finding.

Fix validation

Not performed in-guest (Low-severity boot-DoS class, no demonstrated memory corruption; kernel rebuild cycle not warranted for a 3-line clamp). fix.diff is authored against the read-only sys/ tree: validate hz/stathz in init_param1, plus a defense-in-depth freq <= 0 guard in _systimer_init_periodic.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

Fix not built in-guest: Low-severity boot-DoS class with no demonstrated memory corruption (per contract, guest fix-validation cycles are reserved for memory-corruption findings); fix.diff is a minimal, line-accurate clamp authored against the read-only sys/ tree.

['fix.diff']
↓ fix.diffper-fix-DF-2946

Confirmed kernel references

Detail

Exploit chain

1) privileged/console plant: kern.stathz="0" in /boot/loader.conf; 2) init_param1 ingests it unvalidated into stathz; 3) boot proceeds (hz valid, in-file divides fine); 4) initclocks_other at SI_BOOT2_POST_SMP calls systimer_init_periodic_flags(..., stathz=0, SYSTF_MSSYNC); 5) 1000/freq (or fromhz) executes idivq by zero -> Fatal trap 18 -> DDB; machine will not boot until loader.conf is repaired. Ceiling: persistent boot DoS (no post-boot or unprivileged trigger exists β€” kern.stathz has no runtime sysctl).

Evidence (decisive lines)

["panic.txt β€” 'Fatal trap 18: integer divide fault while in kernel mode' / 'kernel: type 18 trap' / 'Stopped at -0x7f9a53d9: idivq %r12,%eax' / 'db>' immediately after 'Initialize MI interrupts for 6 cpus'", 'env.txt β€” stock kernel identity (6.5-DEVELOPMENT #0, X86_64_GENERIC, hw.ncpu=6) and clean baseline vfs.nbuf/maxfiles/maxproc before the plant', 'run.sh β€” exact host-side reproduction sequence (plant -> reboot -> down at 75s -> reset)', 'VERDICT.md β€” analysis, duplicate-separation vs DF-0173/2915-17, and the refuted negative-kern.nbuf hypothesis (unsigned-promotion rescue in cpu_startup caps)', 'fix.diff β€” hz/stathz validation in init_param1 + freq<=0 guard in _systimer_init_periodic']

PoC changes

no compilation needed: the trigger is a single loader.conf line (plant.sh); predicted sink (1000/freq under SYSTF_MSSYNC vs fromhz) confirmed only by trap signature + boot position since the INVARIANTS kernel exports no symbols to the serial console

Verified recommended fix

Validate clock tunables in init_param1: clamp hz to [1,1000000] (default on violation) and floor stathz at 1 (default hz+1); defense-in-depth freq<=0 guard in _systimer_init_periodic.

Verdict

init_param1() (sys/kern/subr_param.c:202) overwrites stathz with the raw kern.stathz boot tunable with no validation; the value reaches initclocks_other() (kern_clock.c:438-441) which registers the statclock systimer with SYSTF_MSSYNC, and _systimer_init_periodic() (kern_systimer.c:270-273) / cputimer_default_fromhz() (kern_cputimer.c:199) divide by it unguarded. A single planted line kern.stathz="0" in /boot/loader.conf boots past init_param1 and kills the kernel at SI_BOOT2_POST_SMP with 'Fatal trap 18: integer divide fault ... idivq' (panic.txt), leaving the machine dead in DDB β€” unbootable until the tunable is removed. Negative or huge stathz similarly produce a negative/garbage statclock period or an interrupt storm. Same loader.conf trust boundary as DF-0173 (kern.hz=0), but a different tunable and a different, later sink: DF-0173 divides inside init_param1 (subr_param.c:204-207); kern.stathz=0 passes those divides (hz itself stays valid) and detonates in the systimer subsystem. The tree even guards stathz==0 elsewhere (kern_clock.c:1337 'stathz ? stathz : hz'), confirming the systimer path is the omission.