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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4424 Β· 5 files
FileTypeDescriptionSize
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)

  1. 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_SEGMENTS rejects only <=2) and the per-queue sglist is allocated with exactly that capacity (:315).
  2. virtio_blk.c:753 β€” sc->cdev->si_iosize_max = MAXPHYS regardless of max_nsegs, so the disk layer still submits MAXPHYS (128KB) bios.
  3. 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.
  4. 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.

VERDICT.md
↓ download raw

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_testable
baseline no→ patch + rebuild →patched clean

not_testable: baseline itself untriggerable on this rig (no seg_max control in QEMU 7.2.22)

↓ fix.diffper-fix-DF-4424

Confirmed kernel references

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.