Negotiated SEG_MAX smaller than a MAXPHYS transfer overflows the per-queue sglist: KASSERT panic on INVARIANTS kernels, truncated/mislabeled descriptor chain in production
Summary
vtblk_maximum_segments accepts seg_max down to 3 while si_iosize_max stays MAXPHYS, so a 35-segment MAXPHYS bio hits EFBIG in sglist_append; only KASSERTed - panic on stock INVARIANTS kernels, and on production kernels a partial sglist is enqueued with wrong readable/writable counts (spurious EIO; write path marks the last data page as status target - 1-byte device clobber). Malicious/buggy virtio device precondition (no QEMU knob). Clamp si_iosize_max fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4424 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.0 KB | β raw | |
| VERDICT.md | β | 1.5 KB | β raw | |
| fix.diff | β | 724 B | view raw | |
| manifest.json | β | 825 B | view raw | |
| verdict.json | β | 2.8 KB | view raw |
DF-4424 β vtblk negotiates small VIRTIO_BLK_F_SEG_MAX but keeps MAXPHYS iosize: sglist overflow -> KASSERT panic (INVARIANTS) / bogus descriptor chain (production)
Status: not_reproduced on the live guest β the trigger requires a virtio-blk
device advertising 3 <= seg_max <= 34; QEMU (7.2.22) exposes no way to set
seg_max (no CLI/monitor property) on this rig, so the device-side
precondition cannot be constructed. The code path is traced end-to-end below.
Trace (why it fires)
- virtio_blk.c:582-586 β with SEG_MAX negotiated,
nsegs = MIN(blkcfg->seg_max, MAXPHYS/PAGE_SIZE+1+2). A device advertising seg_max in [3,34] passes the :282 sanity check (sc->vtblk_max_nsegs <= VTBLK_MIN_SEGMENTSrejects only <=2) and the per-queue sglist is allocated with exactly that capacity (:315). - virtio_blk.c:753 β
sc->cdev->si_iosize_max = MAXPHYSregardless of max_nsegs, so the disk layer still submits MAXPHYS (128KB) bios. - MAXPHYS buffer spanning 33 pages needs 1+33+1 = 35 sglist segments. sys/kern/subr_sglist.c _sglist_append_buf() splits per page and returns EFBIG when the list is full.
- virtio_blk.c:862-878 β the append failures are only checked with KASSERT:
- INVARIANTS kernels (stock DragonFly X86_64_GENERIC, incl. the audit guest):
KASSERT(error == 0, "error adding buffer to sglist")-> kernel panic on the first large I/O. - production kernels (KASSERT is a no-op, sys/sys/systm.h:117): the partial sglist is enqueued with mis-computed readable/writable counts (readable=0 for reads; header seg marked device-writable; no ack descriptor) -> device never writes vbr_ack (stays 0xFF) -> vtblk_vq_intr:918-926 reports EIO for I/O that was partially submitted; for writes the last data page is marked as the status descriptor and gets a 1-byte clobber by the device.
Fix
Either clamp sc->cdev->si_iosize_max = min(MAXPHYS, (sc->vtblk_max_nsegs - 2) * PAGE_SIZE)
or reject seg_max < MAXPHYS/PAGE_SIZE + 1 + VTBLK_MIN_SEGMENTS at attach.
DF-4424 VERDICT β not_reproduced (device-config precondition unconstructible with available tooling); code path certain
- QEMU 7.2.22 virtio-blk-pci exposes no seg_max property (monitor probe and source knowledge); the guest's own vtblk0 advertises a large seg_max, so the bug cannot be triggered by the standard rig. A malicious/buggy virtio device (nested virt, compromised host process, VFIO passthrough of hostile HW) can.
- The INVARIANTS panic is deterministic once the precondition holds: any bio whose b_bcount spans more pages than (max_nsegs - 2) hits EFBIG in sglist_append and the KASSERT at virtio_blk.c:864/870/878.
- Guest kernel is INVARIANTS (options INVARIANTS in X86_64_GENERIC; verified /usr/src/sys/config on the guest), so the panic variant applies to stock configs.
- Classification: device-controlled local DoS at minimum (panic), spec-compliant small-seg_max devices break the driver outright. Same threat family as DF-1603/DF-1604 (malicious virtio config), distinct root cause (driver-side iosize/seg accounting). Related but distinct from DF-1767 (virtqueue.c host-controlled descriptor indices).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablenot_testable: baseline itself untriggerable on this rig (no seg_max control in QEMU 7.2.22)
Confirmed kernel references
- sys/dev/virtual/virtio/block/virtio_blk.c:582
- sys/dev/virtual/virtio/block/virtio_blk.c:282
- sys/dev/virtual/virtio/block/virtio_blk.c:753
- sys/dev/virtual/virtio/block/virtio_blk.c:862
- sys/dev/virtual/virtio/block/virtio_blk.c:864
- sys/dev/virtual/virtio/block/virtio_blk.c:870
- sys/dev/virtual/virtio/block/virtio_blk.c:878
Detail
Evidence (decisive lines)
VERDICT.md trace; sys/kern/subr_sglist.c _sglist_append_buf (per-page split, EFBIG at capacity); sys/sys/systm.h:95 vs :117 (KASSERT compiled out without INVARIANTS); guest /usr/src/sys/config/X86_64_GENERIC contains 'options INVARIANTS'.
PoC changes
n/a (no runnable PoC possible: QEMU offers no seg_max control; documented monitor probe)
Verified recommended fix
si_iosize_max = min(MAXPHYS, (sc->vtblk_max_nsegs - VTBLK_MIN_SEGMENTS) * PAGE_SIZE) in vtblk_alloc_disk, or EINVAL at attach when negotiated seg_max cannot cover a MAXPHYS transfer
Verdict
not_reproduced: triggering requires a virtio-blk device advertising SEG_MAX in [3,34]; QEMU 7.2.22 (this rig) provides no way to set seg_max, and the guest's real vtblk0 advertises a large value, so the precondition cannot be constructed. The code path is traced line-by-line and is deterministic once the precondition holds: vtblk_maximum_segments (virtio_blk.c:582-586) accepts small seg_max, the :282 check only rejects <=2, si_iosize_max stays MAXPHYS (:753), a MAXPHYS bio needs up to 35 segments (header+33 pages+ack), sglist_append returns EFBIG (subr_sglist.c splits per page), and the failures are only KASSERTed (virtio_blk.c:864/870/878). On INVARIANTS kernels (stock X86_64_GENERIC, verified present on the audit guest) this panics on first large I/O; on production kernels (KASSERT no-op, systm.h:117) a truncated/mislabeled descriptor chain is enqueued (no ack descriptor -> vbr_ack stays 0xFF -> spurious EIO; write path marks the last data page as status target -> 1-byte clobber). Device-controlled DoS/data-integrity break, same malicious-device family as DF-1603/1604.
No comments yet.