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, "");
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 requiresSYSCAP_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 != NULLbranch. - 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.
Recommended fix
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
sys/kern/subr_disk.c:1325-1327βbioq_reorder_minor_intervalSYSCTL_INT(no bound).sys/kern/subr_disk.c:1376β unguarded% bioq_reorder_minor_interval.- CWE-369 Divide By Zero; CWE-20 Improper Input Validation.
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)
PoC verification
Evidence pack
findings/poc/DF-0026 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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
-
Attacker sets the divisor to 0 (root only):
sysctl kern.bioq_reorder_minor_interval=0The sysctl isCTLFLAG_RW SYSCTL_INT(subr_disk.c:1326) β no validator, accepts anyintincluding 0.SYSCAP_NOSYSCTL_WRgates the write, so root is required. -
A WRITE bio is queued in a disk's bioq. In
bioqdisksort()(subr_disk.c:1391-1399),BUF_CMD_WRITEappends to the tail and setsbioq->transitionto point at the first write if it was NULL. -
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 */reorderis now 1 (or any positive int);1 % 0triggersidivlβ 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): samekldload 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 callingbioqdisksort()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.shunchanged.
Fix verification
fixedVALIDATED: 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.
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().
No comments yet.