# 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:

```c
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                                 |
