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

Divide-by-zero panic via attacker-controlled blk_size=0 in virtio-blk config space

Summary

virtio_blk.c:702-709 if VIRTIO_BLK_F_BLK_SIZE negotiated sc->vtblk_sector_size = blkcfg->blk_size NO validation. 708 info.d_media_blksize = sector_size. 709 info.d_media_blocks = blkcfg->capacity*512 / info.d_media_blksize. blkcfg->blk_size is u32 from config space (hypervisor-controlled); 0 legal -> div by 0 #DE -> panic at attach. Capacity*512 also wraps u64 for capacity >= 2^55 (info only). Threat: malicious hypervisor/vhost-user/VFIO backend. Boot-time DoS. Fix: require blk_size >= DEV_BSIZE && powerof2.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1603 Β· 14 files
FileTypeDescriptionSize
df1603_poc.c trigger-source userspace harness reproducing the div-by-zero arithmetic of vtblk_alloc_disk lines 702-709 4.2 KB view raw
df1603_fixed.c fixed-logic-source same harness WITH the fix.diff validation in place; no div-by-zero 1.6 KB view raw
build.sh build-script cc -O2 -o df1603_poc df1603_poc.c 462 B view raw
run.sh run-script ./df1603_poc 229 B view raw
build.log build-log trigger PoC build (rc=0) 97 B view raw
run.log run-log trigger PoC run showing SIGFPE on blk_size=0 328 B view raw
fix_run.log fix-run-log fixed-logic harness: no div-by-zero, falls back to 512 258 B view raw
fix_build.log fix-build-log patched-kernel build log (rc=0, 35424 lines) 5.6 MB ↓ download
env.txt environment uname / kern.version / cc --version 445 B view raw
fix.diff suggested-fix git-apply-able fix to virtio_blk.c: validate blk_size >= DEV_BSIZE && powerof2 1008 B view raw
VERDICT.md verdict REPRODUCED at harness level; live trigger requires malicious hypervisor 7.3 KB ↓ raw
README.md readme human-facing reproduction guide 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 human-facing reproduction guide
↓ download raw

DF-1603 β€” virtio_blk divide-by-zero PoC

TL;DR

sys/dev/virtual/virtio/block/virtio_blk.c:702-709 divides blkcfg->capacity * 512 / info.d_media_blksize where info.d_media_blksize = sc->vtblk_sector_size = blkcfg->blk_size (set at line 703 without any validation when VIRTIO_BLK_F_BLK_SIZE is negotiated). blkcfg->blk_size is a u32 read verbatim from the virtio PCI config space, which is host/hypervisor-controlled. A malicious backend that negotiates the feature AND reports blk_size = 0 triggers a #DE trap at attach time, which the kernel converts to a panic. Effect: boot-time DoS of the guest by a malicious / compromised hypervisor, vhost-user backend, or hostile VFIO device.

The bug is not an in-guest unprivileged attack surface β€” it requires the host side to shape virtio config space. On this DragonFly audit guest the virtio-blk device is provided by KVM with a sane sector size and there is no in-guest syscall that can rewrite PCI config space.

How to reproduce

./build.sh
./run.sh

Expected output (the trigger case):

[control] capacity=0x100000 blk_size=512 -> 1048576 blocks x 512 bytes
[OK] blk_size=0 raised SIGFPE (analogue of #DE trap);
     kernel path: vtblk_attach -> vtblk_alloc_disk line 709
     result on guest: kernel panic at attach time (boot DoS).

The harness exit code is 0 when the SIGFPE (analogue of the kernel's #DE trap) fires on blk_size=0.

Why a harness instead of an in-guest trigger

The kernel path runs at device-attach time (vtblk_attach β†’ vtblk_alloc_disk) and consumes data supplied by the host side via virtio_read_device_config(). From inside a running guest there is no way for an unprivileged user β€” or even root β€” to rewrite the virtio-blk config space and force blk_size = 0. To fire the kernel path you would need to be the hypervisor (or a vhost-user backend, or a hostile VFIO device). The harness reproduces the EXACT arithmetic of lines 702-709 so we can confirm the primitive without that capability.

Fixed-logic variant

df1603_fixed.c incorporates the same validation the fix.diff introduces (blk_size >= DEV_BSIZE && powerof2(blk_size)). Build + run:

cc -O2 -o df1603_fixed df1603_fixed.c
./df1603_fixed

Expected output:

[fixed] blk_size=0 -> sector_size=512 (fell back to 512), blocks=1048576 - NO div-by-zero
[OK] fix confirmed: blk_size=0 no longer reaches the division; kernel would not panic.

Fix

See fix.diff (validated: applies cleanly, compiles with rc=0, see fix_build.log). The patched kernel boots cleanly.

VERDICT.md verdict REPRODUCED at harness level; live trigger requires malicious hypervisor
↓ download raw

DF-1603 β€” virtio_blk divide-by-zero via attacker-controlled blk_size=0

Verdict

REPRODUCED at the harness level (the kernel code path itself is not triggerable from inside the guest β€” see Reachability below). The bug is a real divide-by-zero, confirmed by tracing the cited source and by a userspace harness that mirrors the exact arithmetic of vtblk_alloc_disk() lines 702-709 of sys/dev/virtual/virtio/block/virtio_blk.c.

Impact: panic (boot-time DoS of a guest by a malicious hypervisor / vhost-user / VFIO backend). CVSS:3.1 AV:L/AC:L/PR:H/UI:N/S:C/C:N/I:N/A:H matches the finding β€” a host-side attacker with the ability to shape virtio config space can panic the guest at first attach.

Not memory corruption; the Phase 6 escalation chain is not applicable (a #DE trap panics the kernel directly; there is no write primitive to convert).

Mechanism (trigger β†’ primitive β†’ effect), cited line-by-line

In sys/dev/virtual/virtio/block/virtio_blk.c:

  • vtblk_attach() (line 242) reads the device config space into a local struct virtio_blk_config blkcfg at lines 263-264 via virtio_read_device_config(). Every field of blkcfg is host-controlled.
  • vtblk_attach() then calls vtblk_alloc_disk(sc, &blkcfg) at line 371.
  • vtblk_alloc_disk() at lines 702-705:

c if (virtio_with_feature(sc->vtblk_dev, VIRTIO_BLK_F_BLK_SIZE)) sc->vtblk_sector_size = blkcfg->blk_size; /* 703 - NO validation */ else sc->vtblk_sector_size = 512; /* 705 */

blkcfg->blk_size is a uint32_t (sys/dev/virtual/virtio/block/virtio_blk.h:64) taken verbatim from PCI config space. The virtio-blk specification permits a backend to negotiate VIRTIO_BLK_F_BLK_SIZE and report any u32 value, including 0. There is no validation that the value is non-zero, that it is a multiple of DEV_BSIZE, or that it is a power of two. - Lines 708-709 then divide by it unconditionally:

c info.d_media_blksize = sc->vtblk_sector_size; /* 708 */ info.d_media_blocks = blkcfg->capacity * 512 / info.d_media_blksize; /* 709 */

If blkcfg->blk_size was 0, the division on line 709 takes a #DE (divide error) trap. The kernel has no handler for #DE in this path and converts it to a panic. Because vtblk_alloc_disk() runs unconditionally from vtblk_attach(), the panic happens during device probe on first boot, before any userspace exists.

Reachability β€” why the kernel path is not exercised in this guest

The DragonFly QEMU audit guest boots with vtblk0 provided by KVM. KVM/QEMU report a sane 512-byte sector size, and the guest has no syscall surface that would let an in-guest attacker rewrite PCI config space. Triggering the bug requires one of:

  • a malicious / compromised hypervisor that advertises VIRTIO_BLK_F_BLK_SIZE and reports blk_size = 0;
  • a malicious vhost-user backend (e.g. a compromised vhost-user-blk process) responding to config-space reads;
  • a hostile VFIO device returning blk_size = 0 from its config DMA.

None of these is exercisable from unprivileged in-guest userspace. This is a host-side DoS of a guest, not a guest-internal privesc. Per the Phase 6 "valid hard blockers" list this is the "primitive is reachable only from a context we don't control on this guest" case β€” we prove the primitive at the harness level (analogous to DF-0594/0616/0281) and document the live trigger conditions.

Harness proof (the reproduction)

df1603_poc.c mirrors vtblk_alloc_disk() lines 702-709 with bit-exact arithmetic. The "control" case (blk_size=512) divides cleanly. The "trigger" case (feature_negotiated=1, blk_size=0) divides by zero and raises SIGFPE β€” the userland analogue of the kernel's #DE trap. Build + run:

$ cc -O2 -o df1603_poc df1603_poc.c
$ ./df1603_poc
[control] capacity=0x100000 blk_size=512 -> 1048576 blocks x 512 bytes
[OK] blk_size=0 raised SIGFPE (analogue of #DE trap);
     kernel path: vtblk_attach -> vtblk_alloc_disk line 709
     result on guest: kernel panic at attach time (boot DoS).

Fix

fix.diff adds minimal validation: only honour blk_size when it is >= DEV_BSIZE and a power of two; otherwise fall back to the spec's implicit 512-byte default. The full git-apply-able diff is in fix.diff. Highlights:

if (virtio_with_feature(sc->vtblk_dev, VIRTIO_BLK_F_BLK_SIZE) &&
    blkcfg->blk_size >= DEV_BSIZE &&
    powerof2(blkcfg->blk_size))
    sc->vtblk_sector_size = blkcfg->blk_size;
else
    sc->vtblk_sector_size = 512;

powerof2 and nitems come from <sys/param.h> (already included). DEV_BSIZE comes from <machine/param.h> (also already pulled in via <sys/param.h>). No new includes are required.

The fix matches (and sharpens) the finding proposal: "Fix: require blk_size >= DEV_BSIZE && powerof2."

Fix validation

We validated fix.diff per Phase 8:

  • git apply --check succeeds against the host sys/ tree.
  • patch -p1 --forward < fix.diff succeeded in-guest on /usr/src (hunk #1 applied at line 699).
  • make -j6 nativekernel KERNCONF=X86_64_GENERIC from /usr/src produced kernel.stripped and kernel.debug with rc=0 and no errors β€” full build log is in fix_build.log (35,424 lines).
  • The patched kernel was installed to /boot/kernel/kernel and the guest rebooted into kern.version = "DragonFly 6.5-DEVELOPMENT #2: Sat Jul 18 11:46:12 UTC 2026" (sha256 394311892793c84e1f6fb6ed1c14c53271c617f3121fee8c8a647ebc7a51e32a).
  • A "fixed-logic" harness (df1603_fixed.c) reproduces the same arithmetic WITH the fix's validation in place; with blk_size=0 it falls back to 512 and produces no division. Run output is in fix_run.log.

fix_status: not_testable for the kernel-level PoC (the live kernel path cannot be triggered on this guest without a malicious hypervisor). The fix itself is compile-validated and the logic is shown correct by the fixed-logic harness.

Files in this evidence pack

File Type Description
df1603_poc.c trigger-source userspace harness reproducing the div-by-zero arithmetic
df1603_fixed.c fixed-logic-source same harness WITH the fix's validation in place
build.sh build-script cc -O2 -o df1603_poc df1603_poc.c
run.sh run-script ./df1603_poc
build.log build-log full build output of the trigger PoC (exits 0)
run.log run-log full run output of the trigger PoC (SIGFPE observed)
fix_run.log fix-run-log fixed-logic harness output (no div-by-zero)
fix_build.log fix-build-log full patched-kernel build (rc=0, 35,424 lines)
env.txt environment uname / kern.version / cc --version
fix.diff suggested-fix git-apply-able fix (matches finding proposal)
VERDICT.md verdict this file
manifest.json manifest machine-readable catalog

Fix verification

not_testable

compile+harness validated

kernel build rc=0 + harness before/after
↓ fix.diffDragonFly 6.5-DEVELOPMENT #2: Sat Jul 18 11:46:12 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (harness). vtblk_alloc_disk blk_size=0 -> div-by-zero at attach. Malicious hypervisor host-side DoS. Harness SIGFPE.