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

Root-writable bioq_reorder_minor_interval used as modulus divisor without validation -> divide-by-zero panic

Field Value
ID DF-0026
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 Divide By Zero; CWE-20 Improper Input Validation
File sys/kern/subr_disk.c
Lines 1325-1327, 1376
Area kern
Confidence certain
Discovered 2026-06-29
Reported pending

Summary

kern.bioq_reorder_minor_interval is exported as a CTLFLAG_RW SYSCTL_INT with no bounds validation and is used directly as the right-hand operand of a modulus (bioq->reorder % bioq_reorder_minor_interval) in the disk bio-sort hot path bioqdisksort(). Setting it to 0 (root) causes an immediate integer divide-by-zero (FPE_INTDIV) and kernel panic on the next disk read dispatched to a bioq that already has pending writes (bioq->transition != NULL).

Root cause

sys/kern/subr_disk.c:1325-1327:

int bioq_reorder_minor_interval = 5;
SYSCTL_INT(_kern, OID_AUTO, bioq_reorder_minor_interval,
           CTLFLAG_RW, &bioq_reorder_minor_interval, 0, "");

sys/kern/subr_disk.c:1376:

if (bioq->reorder % bioq_reorder_minor_interval == 0) {     /* no divisor guard */

There is no lower-bound check on the divisor. The other three bioq_reorder_* knobs are not divisors (burst_interval is only compared with >=; the *_bytes knobs are subtracted under a left < n guard), so this is the only fatal one.

Threat model & preconditions

  • Attacker position: privileged β€” writing kern.* sysctls requires SYSCAP_NOSYSCTL_WR (root, cr_uid==0). So this is a privileged-user- triggered kernel panic (self-DoS / hardening defect), not an escalation.
  • Privileges gained or impact: kernel panic (full-system DoS). No integrity/confidentiality impact.
  • Required config or capabilities: root; a disk with mixed read/write I/O so a read reaches the bioq->transition != NULL branch.
  • Reachability: sysctl kern.bioq_reorder_minor_interval=0, then any disk read on a bioq with pending writes.

Proof of concept

PoC source: findings/poc/DF-0026/bioq_div0.sh

Run (root, disposable VM)

sh findings/poc/DF-0026/bioq_div0.sh

Expected output

panic: integer divide fault   (FPE_INTDIV in bioqdisksort)

Impact

Root-only self-DoS. The kernel should not panic from a sysctl write of an in-range integer β€” an unintentional panic knob reachable through normal disk I/O. Rated Low (privileged trigger). Same class as DF-0019.

Guard the divisor in the consumer (and/or validate in a SYSCTL_PROC handler):

--- a/sys/kern/subr_disk.c
+++ b/sys/kern/subr_disk.c
@@ -1373,8 +1373,14 @@
         * Insert before the first write.  Bleedover writes
         * based on reorder intervals to prevent starvation.
         */
+       int minor_interval = bioq_reorder_minor_interval;
+       int burst_interval = bioq_reorder_burst_interval;
+       if (minor_interval < 1)
+           minor_interval = 1;
+       if (burst_interval < minor_interval)
+           burst_interval = minor_interval;
        TAILQ_INSERT_BEFORE(bioq->transition, bio, bio_act);
        ++bioq->reorder;
-       if (bioq->reorder % bioq_reorder_minor_interval == 0) {
+       if (bioq->reorder % minor_interval == 0) {
            bioqwritereorder(bioq);
-           if (bioq->reorder >= bioq_reorder_burst_interval) {
+           if (bioq->reorder >= burst_interval) {
                bioq->reorder = 0;

Defense-in-depth: convert the knob to a SYSCTL_PROC handler that rejects values < 1 (and enforces the existing "burst must be a multiple of minor" invariant noted in the comment at :1322).

References

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/subr_disk.c.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0026 Β· 18 files
FileTypeDescriptionSize
df26_harness.c trigger-source kernel module harness β€” constructs bioq state (WRITE then READ) and calls real bioqdisksort() to trigger div0 3.0 KB view raw
Makefile build-file kld module build (SYSDIR=/usr/src/sys) 134 B ↓ download
bioq_div0.sh trigger-source original trigger script (sysctl=0 + illustrative workload) 1.4 KB view raw
trigger.c trigger-source aggressive parallel I/O flood trigger (vtblk target) 2.9 KB view raw
trigger_md.c trigger-source md0 SMP-race trigger 2.5 KB view raw
trigger_md2.c trigger-source md0 O_DIRECT + disjoint-range trigger 2.7 KB view raw
fix.diff suggested-fix consumer-side divisor clamp: clamp bioq_reorder_minor_interval to >=1 at subr_disk.c:1376 529 B view raw
build.sh build-script builds the kld harness module 240 B view raw
run.sh run-script sets sysctl=0 and loads harness module 540 B view raw
run.log run-log baseline panic: Fatal trap 18 in bioqdisksort+0x9f 477 B view raw
fix_run.log fix-run-log patched kernel: BUG NOT TRIGGERED, guest stays up 952 B view raw
fix_build.log fix-build-log full nativekernel build output for the fix (rc=0) 5.6 MB ↓ download
panic.txt panic-signature Fatal trap 18: integer divide fault, bioqdisksort+0x9f idivl 511 B view raw
env.txt environment uname, cc version, sysctl state 447 B view raw
VERDICT.md verdict full narrative: mechanism, evidence, fix validation 4.3 KB ↓ raw
README.md readme PoC overview and reproduction instructions 2.5 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme PoC overview and reproduction instructions
↓ download raw

DF-0026 β€” Proof of Concept

The bug

bioq_reorder_minor_interval (sys/kern/subr_disk.c:1325-1327) is a SYSCTL_INT(..., CTLFLAG_RW, ...) with no bounds validation. It is used directly as a modulus divisor at :1376:

if (bioq->reorder % bioq_reorder_minor_interval == 0) {

Setting kern.bioq_reorder_minor_interval=0 (root) causes an integer divide-by-zero (FPE_INTDIV, trap 18) panic in bioqdisksort() the next time a READ bio is sorted into a bioq that already has a pending WRITE (transition != NULL).

Root-only (sysctl writes gated by SYSCAP_NOSYSCTL_WR). Low severity.

Files

File Description
bioq_div0.sh Original trigger script (sets sysctl; workload illustrative)
trigger.c Aggressive parallel I/O flood trigger (vtblk target)
trigger_md.c md0 SMP-race trigger
trigger_md2.c md0 O_DIRECT + disjoint-range trigger
df26_harness.c Kernel module harness β€” directly constructs the div0 bioq state
Makefile kld module build for the harness
fix.diff Standalone git-apply-able fix (consumer-side divisor clamp)
build.sh / run.sh Repro scripts

How to reproduce

Prerequisites

  • DragonFlyBSD 6.5-DEVELOPMENT (master DEV) guest, default X86_64_GENERIC kernel
  • Root access (sysctl write + kldload)

Build the harness

./build.sh

Run (root, disposable VM)

./run.sh

Expected (bug present β€” unpatched #0 kernel)

Fatal trap 18: integer divide fault while in kernel mode
Stopped at      bioqdisksort+0x9f:      idivl   0xa4b82b(%rip),%eax
db>

Expected (fixed β€” patched #1 kernel)

DF-0026: harness loaded. bioq_reorder_minor_interval=0
DF-0026: queuing WRITE (transition before=0)
DF-0026: queued WRITE, transition=0xfffff8008d640840. Now queuing READ -> DIV0
DF-0026: BUG NOT TRIGGERED (unexpected!)

Guest stays up, no panic.

Why a kernel module harness?

The div0 code path requires bioq->transition != NULL (a WRITE bio already queued) when a READ bio enters bioqdisksort(). On real hardware/virtio, the disk strategy drains the bioq immediately after each bioqdisksort() call, making the window where transition != NULL extremely tight from userspace. The finding is root-only (sysctl write), so a root-loaded harness module is a legitimate proof: it constructs the exact bioq state and calls the real bioqdisksort() (the actual vulnerable function), not a simulation. The sysctl is still set from userspace β€” the module only sets up the I/O state.

VERDICT.md verdict full narrative: mechanism, evidence, fix validation
↓ download raw

DF-0026 β€” VERDICT

Verdict: REPRODUCED (panic / root-only DoS)

The bug is real and confirmed. bioq_reorder_minor_interval is a root-writable SYSCTL_INT (sys/kern/subr_disk.c:1325-1327) used as a modulus divisor at :1376 (bioq->reorder % bioq_reorder_minor_interval) with no lower-bound validation. Setting it to 0 via sysctl(8) (root) causes an integer divide-by-zero (Fatal trap 18: integer divide fault) in bioqdisksort() on the next READ bio that enters a bioq with transition != NULL (pending write).

Mechanism

  1. Attacker sets the divisor to 0 (root only): sysctl kern.bioq_reorder_minor_interval=0 The sysctl is CTLFLAG_RW SYSCTL_INT (subr_disk.c:1326) β€” no validator, accepts any int including 0. SYSCAP_NOSYSCTL_WR gates the write, so root is required.

  2. A WRITE bio is queued in a disk's bioq. In bioqdisksort() (subr_disk.c:1391-1399), BUF_CMD_WRITE appends to the tail and sets bioq->transition to point at the first write if it was NULL.

  3. A READ bio arrives while the WRITE is still in the bioq (bioq->transition != NULL). The READ branch (:1368-1382) executes: c TAILQ_INSERT_BEFORE(bioq->transition, bio, bio_act); ++bioq->reorder; if (bioq->reorder % bioq_reorder_minor_interval == 0) { /* :1376 β€” div0 */ reorder is now 1 (or any positive int); 1 % 0 triggers idivl β†’ trap 18 (integer divide fault) β†’ kernel panic.

Evidence

Baseline (unpatched #0 kernel)

Fatal trap 18: integer divide fault while in kernel mode
Stopped at      bioqdisksort+0x9f:      idivl   0xa4b82b(%rip),%eax
db>

The idivl at bioqdisksort+0x9f is the modulus at :1376. The RIP-relative operand 0xa4b82b(%rip) is the global bioq_reorder_minor_interval (value 0).

Trigger method

A kernel module harness (df26_harness.c) constructs the exact bioq state: initializes a bio_queue_head, queues a WRITE bio (sets transition), then queues a READ bio (hits the divisor). The module calls the real bioqdisksort() β€” the actual vulnerable function β€” not a simulation. The sysctl is set from userspace (root) before loading.

Userspace-only triggers (parallel dd / C programs doing mixed read/write on vtblk and md devices) were attempted extensively but the race window is extremely tight: vtblk_strategy drains its bioq into the virtqueue immediately after each bioqdisksort(), and mdstrategy clears transition via bioq_remove() before the processing memcpy begins. The harness module is the appropriate proof for a root-only DoS where the runtime race window is sub-microsecond.

Impact

Root-only self-DoS (kernel panic). No privilege escalation, no info leak, no memory corruption. Rated Low (privileged trigger, availability-only impact). Same class as DF-0019.

Fix (validated)

Consumer-side divisor clamp at subr_disk.c:1376 (fix.diff):

-           if (bioq->reorder % bioq_reorder_minor_interval == 0) {
+           int minor_interval = bioq_reorder_minor_interval;
+           if (minor_interval < 1)
+               minor_interval = 1;
+           if (bioq->reorder % minor_interval == 0) {

Fix validation (Phase 8)

  • Unpatched baseline (#0): kldload df26.ko (after sysctl=0) β†’ Fatal trap 18: integer divide fault ... bioqdisksort+0x9f: idivl β†’ guest in DDB.
  • Patched kernel (#1, fix applied): same kldload df26.ko (after sysctl=0) β†’ module prints "BUG NOT TRIGGERED", guest stays up, no panic. Verified twice (deterministic).

The fix supersedes the finding markdown's proposal (which additionally clamped burst_interval); since burst_interval is only used in a >= comparison (:1378), not as a divisor, clamping it is unnecessary for preventing the div0 β€” though it remains good defense-in-depth.

PoC changes

  • Added df26_harness.c β€” kernel module harness that directly triggers the div0 by calling bioqdisksort() with a constructed bioq state.
  • Added Makefile β€” kld module build (SYSDIR=/usr/src/sys).
  • Added trigger.c, trigger_md.c, trigger_md2.c β€” userspace I/O flood triggers (attempted but race window too tight from userspace).
  • Added fix.diff β€” standalone git-apply-able fix.
  • Added build.sh / run.sh β€” repro scripts.
  • Original bioq_div0.sh unchanged.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline idivl #DE panic; patched BUG NOT TRIGGERED, guest UP x2.

BEFORE #0: Fatal trap 18 at bioqdisksort+0x9f: idivl. AFTER #1: BUG NOT TRIGGERED, guest UP x2.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 20:32:08 UTC 2026 (sha256 660ff4bf48e4d09f06717487bede4a4acb2047b7d3216c237b6fde5287f94b0c)

Confirmed kernel references

Detail

Exploit chain

none -- divide-by-zero (CWE-369). Root-only self-DoS. No escalation.

Evidence (decisive lines)

BASELINE #0: sysctl=0 + kldload -> Fatal trap 18: integer divide fault at bioqdisksort+0x9f: idivl -> guest DOWN. FIXED #1: same trigger -> BUG NOT TRIGGERED, guest UP x2.

PoC changes

Added df26_harness.c (kernel module constructing bioq state + calling real bioqdisksort), Makefile, trigger variants, fix.diff (consumer-side divisor clamp), build.sh/run.sh.

Verified recommended fix

At sys/kern/subr_disk.c:1376, clamp the divisor before the modulus: read into local, clamp to >=1 if <1, use the local. Full git-apply-able diff in findings/poc/DF-0026/fix.diff.

Verdict

REPRODUCED. kern.bioq_reorder_minor_interval (sys/kern/subr_disk.c:1325-1327) is CTLFLAG_RW SYSCTL_INT with no bounds validation, used as modulus divisor at :1376. Setting to 0 via sysctl causes idivl #DE at bioqdisksort+0x9f when a READ bio enters a bioq with transition != NULL. Kernel module harness constructs exact bioq state (WRITE sets transition, READ hits divisor) and calls real bioqdisksort().