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

mbrinit/mbr_extended trust device-reported d_media_blksize β€” up to 2 GiB synchronous read into a MAXPHYS (128 KB) pbuf: assertion-cast bypass (all kernels) or INVARIANTS panic; proven OOB write with attacker-controlled content/length

Field Value
ID DF-2902
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CWE CWE-130 / CWE-787 / CWE-190
File sys/kern/subr_diskmbr.c
Lines 127-135 (esp. :128, :132), 434-442; feed: scsi_da.c:2289
Area kern/disk
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

mbrinit() issues a one-block synchronous read sized exactly info->d_media_blksize into a getpbuf_mem() pbuf whose buffer is only MAXPHYS = 128 KB. The sole guard, KKASSERT((int)info->d_media_blksize <= bp->b_bufsize) at subr_diskmbr.c:128 (and :435 for EBR reads), is doubly broken: KKASSERT compiles out on non-INVARIANTS production kernels, and the (int) cast turns any block size β‰₯ 2^31 into a negative int that PASSES the assertion everywhere. d_media_blksize is device-controlled end to end: a malicious USB/SCSI storage device answers READ CAPACITY with length=0x80000000, scsi_da.c stores it into params.secsize with no validation, :1863 propagates it to d_media_blksize, and _setdiskinfo even computes the non-zero d_media_size that defeats mbrinit's media-size guard. bp->b_bcount = 0x80000000 then flows through dscheck (whose int-wrap arithmetic is self-consistent for this exact power of two) into scsi_read_write β€” with scsi_minphys #if 0'd β€” i.e. a device-directed transfer of up to 2 GiB into a 128 KB kernel buffer whose content the device fully controls. Entry: device attach-time probe (runs as root in disk_msg_core), no user interaction; non-hardware entry requires root (vnconfig/KLD), raw disk opens require SYSCAP_RESTRICTEDROOT β€” no unprivileged-local entry.

Proof of contest

VERIFIED (findings/poc/DF-2902/): (a) KLD harness on /dev/vn0 with d_media_blksize=0x40000 β†’ exact-line assertion panic at subr_diskmbr.c:128; (b) d_media_blksize=0x80000000 β†’ assertion bypassed via the cast, 2 GiB read issued, transport memmove overruns the 128 KB pbuf β†’ kernel-mode write page fault (trap 0xc, rip = memmove+0x10a) β€” overflow content is the attacker's media bytes. uid=0 route: hard-blocked β€” the malicious-device vector is unavailable in this guest (QEMU usb-storage cannot lie about READ CAPACITY); entry is device-attach or root-only. Fix validated by in-guest nativekernel rebuild: both legs β†’ EIO, no panic.

Bound the block size before use (< DEV_BSIZE || > MAXPHYS β†’ EIO) in both mbrinit and the mbr_extended read, plus clamp at the source in scsi_da.c dasetgeom. Validated fix.diff in findings/poc/DF-2902/.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_diskmbr.c (GLM 5.3); reproduced both legs + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2902 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 3.1 KB ↓ raw
mbrprobe.c β€” 2.2 KB view raw
Makefile β€” 89 B ↓ download
build.sh β€” 544 B view raw
run.sh β€” 642 B view raw
run.log β€” 2.2 KB view raw
run_fix.log β€” 694 B view raw
panic_runA.txt β€” 756 B view raw
panic_runB.txt β€” 850 B view raw
env.txt β€” 640 B view raw
fix.diff β€” 1.2 KB view raw

DF-2902 β€” unvalidated d_media_blksize in mbrinit β†’ oversized kernel read into 128 KB pbuf

What

mbrinit() (sys/kern/subr_diskmbr.c:127-132) issues a synchronous read of exactly info->d_media_blksize bytes into a getpbuf_mem() pbuf whose buffer is only MAXPHYS (128 KB on x86_64) bytes:

bp = getpbuf_mem(NULL);
KKASSERT((int)info->d_media_blksize <= bp->b_bufsize);   /* :128 β€” broken guard */
bp->b_bio1.bio_offset = (off_t)mbr_offset * info->d_media_blksize;
bp->b_bcount = info->d_media_blksize;                    /* :132 β€” sink */

The guard is broken twice: 1. KKASSERT is compiled out entirely on non-INVARIANTS (production) kernels. 2. The (int) cast makes any block size β‰₯ 2^31 (e.g. 0x80000000 β†’ INT_MIN) pass the assertion even on INVARIANTS kernels.

d_media_blksize is attacker-controllable by any malicious storage device: scsi_da.c:dasetgeom() copies the READ CAPACITY length field verbatim into softc->params.secsize (sys/bus/cam/scsi/scsi_da.c:2289) with no validation, so a USB mass-storage gadget answering READ CAPACITY with length=0x80000000 delivers it straight into mbrinit at plug-in (devd/CAM auto-probe runs as root). The same value flows to mbr_extended() (subr_diskmbr.c:435-439).

Observed (stock INVARIANTS guest, DragonFly 6.5-DEVELOPMENT)

  • blksize = 0x40000 (256 KB): panic: assertion "(int)info->d_media_blksize <= bp->b_bufsize" failed in mbrinit at subr_diskmbr.c:128 (see panic_runA.txt).
  • blksize = 0x80000000 (2 GiB): assertion BYPASSED via the cast; the read is issued with b_bcount = 0x80000000 into the 128 KB pbuf; the transfer memmove runs off the end of the mapped buffer β†’ kernel-mode page fault: trap 0xc, rip = memmove+0x10a (see panic_runB.txt). The overflow content is the attacker's data (the bytes the device/backing file supplies).

Threat

Crafted MBR media is NOT required β€” only a device that lies about its block size (USB mass-storage, malicious virtual disk, misbehaving HBA). On the real CAM path the same b_bcount becomes the SCSI dxfer_len (scsi_da.c:1492-1506, scsi_minphys is #if 0'd), i.e. a device-directed DMA of up to 2 GiB into a 128 KB kernel buffer β€” attacker-chosen content and length: code-execution-grade primitive. Reach: plug-in auto-probe (no privilege needed by the attacker beyond physical access).

Reproduce (guest, root)

dd if=/dev/zero of=/root/med.img bs=1m count=256
vnconfig -c -S 512 vn0 /root/med.img        # clean zero media
cd /root/mbrprobe && make                   # KLD harness
kenv mbrprobe.blksize=262144   && kldload ./a/mbrprobe_a.ko   # panic at :128
# reset, reattach vn, then:
kenv mbrprobe.blksize=2147483648 && kldload ./b/mbrprobe_b.ko # OOB memmove panic

Success criterion: panics above on the stock kernel; after fix.diff both runs print mbrprobe: mbrinit returned 5 (EIO) and the guest stays up.

VERDICT.md
↓ download raw

DF-2902 VERDICT

Status: reproduced (both legs) β€” impact: kernel memory corruption (proven OOB write w/ attacker-controlled content and length; demonstrated as panic), gated on a malicious-storage-device / root trigger.

Chain (all lines verified in-guest)

  1. Attacker device answers SCSI READ CAPACITY(10/16) with length = chosen value. scsi_da.c:1831/1850 block_size = scsi_4btoul(...); dasetgeom() scsi_da.c:2289 dp->secsize = block_len; β€” no validation anywhere.
  2. scsi_da.c:1863 info.d_media_blksize = softc->params.secsize β†’ disk_setdiskinfo() (subr_disk.c:853) β†’ _setdiskinfo computes d_media_size = blocks*blksize (nonzero, so mbrinit's media-size guard passes) β†’ DISK_DISK_PROBE.
  3. disk_msg_core (holding ds_token, subr_disk.c:512) β†’ disk_probe (subr_disk.c:371) β†’ mbrinit().
  4. subr_diskmbr.c:118 blksize & DEV_BMASK β€” passes for any 512-multiple. subr_diskmbr.c:128 KKASSERT((int)blksize <= bp->b_bufsize): * 0x20000 < blksize < 0x80000000 (512-multiple) β†’ fires on INVARIANTS kernels (panic reproduced, panic_runA.txt: blksize=0x40000); * blksize β‰₯ 0x80000000 β†’ (int) cast makes it negative β†’ assertion passes on every kernel.
  5. subr_diskmbr.c:132 bp->b_bcount = blksize on a pbuf whose b_data is a MAXPHYS (128 KB, sys/cpu/x86_64/include/param.h:122) KVA window (vm_pager.c:263/391).
  6. The read walks diskstrategy→dscheck (all boundary checks pass: the arithmetic is self-consistent in wrap-around int space — verified subr_diskslice.c:124-282) → raw dev strategy. On CAM: scsi_da.c:1492-1506 scsi_read_write(..., bp->b_data, bp->b_bcount) with scsi_minphys #if 0'd (scsi_da.c:706-711) → dxfer_len = up to 2 GiB DMA into the 128 KB pbuf. In the harness (vn transport) the equivalent is the uiomove copy (vn.c:323-333): kernel-mode page fault in memmove reproduced (panic_runB.txt: trap 000000000000000c, rip = memmove+0x10a).

Why not uid0

The entry vector is device-attach-time probe: it requires a malicious storage device (hardware; no USB gadget emulation is available on this QEMU guest to fake READ CAPACITY block length) or root (vnconfig/KLD). There is no unprivileged-local entry to this path (opening disk devices requires SYSCAP_RESTRICTEDROOT, subr_disk.c:1072). The corruption primitive itself (attacker bytes, attacker length, deterministic forward overflow from a pager_map pbuf slot) is fully demonstrated; weaponizing to uid0 needs the hardware vector and target-grooming work that the guest cannot host. Recorded as memcorrupt/High with panic-level demonstrated impact.

Fix validation (this run)

  • fix.diff adds if (info->d_media_blksize < DEV_BSIZE || info->d_media_blksize > MAXPHYS) return (EIO); at subr_diskmbr.c:121 (plus the DF-2903 hunk), built with make nativekernel in-guest and booted.
  • Baseline (stock kernel): panic at :128 (blksize 0x40000); OOB memmove page-fault panic (blksize 0x80000000). Patched kernel: both runs print mbrprobe: mbrinit returned 5 (EIO) and the guest stays up β€” bad behavior GONE. See run_fix_*.log, fix_build.log.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel rejects hostile block sizes with EIO before issuing any I/O; both baseline panics gone (run_fix.log).

['run_fix.log', 'env.txt (BUILD_RC=0 INSTALL_RC=0, kernel #1 uname)']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Sep 3 05:01:17 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

malicious USB/SCSI device -> READ CAPACITY lies block_len=0x80000000 -> dasetgeom (no validation) -> disk_setdiskinfo -> disk_msg_core probe -> mbrinit: (int)cast bypasses KKASSERT -> bp->b_bcount=0x80000000 on 128KB pbuf -> dscheck passes (int-wrap arithmetic is self-consistent) -> scsi_read_write(dxfer_len=2GiB, data_ptr=pbuf) -> device DMA / transport copy writes attacker bytes over kernel memory from the pbuf KVA slot forward. Harness equivalent (vn transport): memmove page fault reproduced.

Evidence (decisive lines)

['panic_runA.txt: exact-line KKASSERT panic at subr_diskmbr.c:128 (blksize 0x40000)', "panic_runB.txt / run.log LEG B: trap 000000000000000c rip=memmove+0x10a after 'mbrprobe: dev=vn0 blksize=0x80000000' β€” the assertion was bypassed and the oversized copy faulted", "run_fix.log: patched kernel β€” both legs 'mbrinit returned 5' (EIO), guest survives", 'VERDICT.md: full path trace with citations']

PoC changes

Seed sketch did not exist; harness authored from scratch: KLD (DEV_MODULE) fetches /dev/vn0 via devfs_find_device_by_name, builds a hostile struct disk_info from the kenv tunable mbrprobe.blksize, allocates a slice struct and invokes mbrinit() directly β€” replicating the probe call with attacker-chosen block size; a/b module-name variants allow repeat runs. Initial 0x20000 run was benign (MAXPHYS is 128KB on x86_64, not 64KB) β€” corrected to 0x40000/0x80000000.

Verified recommended fix

Validate d_media_blksize in mbrinit/mbr_extended: reject < DEV_BSIZE or > MAXPHYS (and clamp at the dasetgeom source in scsi_da.c)

Verdict

mbrinit() trusts the device-reported d_media_blksize: subr_diskmbr.c:132 programs bp->b_bcount = blksize on a getpbuf_mem() pbuf whose buffer is MAXPHYS (128KB) bytes. The only guard (:128) is a KKASSERT that (a) compiles out on production kernels and (b) is bypassed for blksize >= 2^31 by the (int) cast. blksize is fully attacker-controlled by a malicious storage device via READ CAPACITY (scsi_da.c:dasetgeom stores it unvalidated). Reproduced both legs on the stock INVARIANTS guest: blksize 0x40000 -> exact-line panic at subr_diskmbr.c:128; blksize 0x80000000 -> assertion bypassed, 2GiB copy into the 128KB pbuf, kernel-mode write page fault in memmove (trap 0xc) β€” a proven OOB write whose content is the attacker's media bytes. Demonstrated impact is panic; on the real CAM path the same b_bcount becomes the SCSI dxfer_len (scsi_minphys is #if 0'd), i.e. a device-directed DMA of up to 2GiB into a 128KB kernel buffer β€” code-execution-grade primitive gated on malicious hardware / root (no unprivileged-local entry exists: raw disk opens require SYSCAP_RESTRICTEDROOT). fix.diff (blksize bound check) rebuilt in-guest: both legs then return EIO with no crash.