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

usb_alloc_mbufs() truncates the 64-bit (block_size + sizeof(struct usb_mbuf)) * nblocks product into usb_size_t before kmalloc β€” undersized heap allocation whose data pointers are constructed fully out of bounds (latent CWE-190-to-CWE-787; no unprivileged in-tree path reaches it today)

Summary

The product is evaluated in size_t then assigned to uint32 usb_size_t, silently truncating >=2^32: kmalloc backs only the truncated count while the population loop lays out headers + data areas at the untruncated stride - every data pointer past the truncation boundary points outside the allocation (m[0] at exactly one-past-end = silent adjacent-slab overwrite; m[n-1] a wild write). Demonstrated live: kmalloc(1536) for a 0x100000600-byte requirement, 32 OOB data pointers, Fatal trap 12 on a one-byte write through m[31]. All 12 in-tree call sites audited: largest product (ugen ISO ~94MB) is two orders below the threshold, so NO unprivileged trigger as shipped - armed for any future caller scaling either factor (e.g. device-descriptor-sized video FIFO). size_t-compute + refuse->32bit fix validated on a rebuilt kernel (allocation FAILED NULL, guest up).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5664 Β· 15 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 6.0 KB ↓ raw
usbmbuf_of.c β€” 4.5 KB view raw
Makefile β€” 88 B ↓ download
build.sh β€” 199 B view raw
run.sh β€” 141 B view raw
build.log β€” 5.7 KB view raw
run.log β€” 1.5 KB view raw
panic.txt β€” 4.8 KB view raw
run.fixed.log β€” 74 B view raw
run.fixed.console.txt β€” 2.3 KB view raw
env.txt β€” 606 B view raw
fix.diff β€” 960 B view raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 4.9 KB view raw

DF-5664 β€” usb_alloc_mbufs() 32-bit alloc_size truncation (latent heap overflow)

WHAT sys/bus/u4b/usb_mbuf.c:69 assigns a 64-bit size_t product to usb_size_t alloc_size (uint32_t, sys/bus/u4b/usb_dragonfly.h:92), truncating it. kmalloc() then backs only the truncated byte count while the population loop lays out nblocks headers + data areas at the FULL stride β€” every data pointer past the truncation boundary points outside the allocation.

REACHABILITY (important) Privileged KLD harness only. Every in-tree caller is capped far below 2^32 (ugen bulk <=256KiB x2 blocks; ugen ISO <=49152 x1920 ~ 94MB; USB_FS_INIT 1x127; all drivers fixed constants or <= usbd_xfer_max_len). No unprivileged path reaches a truncating product TODAY. This is a latent CWE-190 -> CWE-787 defended for future callers.

BUILD (on host, from this directory) ./build.sh # pushes usbmbuf_of.c + Makefile into guest, builds KLD # in /root/df5664 (needs: touch opt_usb.h β€” automatic)

RUN ./run.sh # kldload ./usbmbuf_of.ko (BASELINE: panics guest) # after applying fix.diff and rebuilding the kernel (see VERDICT.md): dfbsd-qemu/vm.sh run_root 'cd /root/df5664 && kldload ./usbmbuf_of.ko' # PATCHED: "allocation FAILED (NULL)", guest stays up

EXPECTED (baseline, unpatched kernel) usbmbuf_of: sizeof(struct usb_mbuf)=48 usbmbuf_of: block_size=0x08000000 nblocks=32 usbmbuf_of: bytes truly required = 0x100000600 (4294968832) usbmbuf_of: kmalloc size (truncated!) = 0x600 (1536) <-- BUG usbmbuf_of: m[0..31] ... <== OOB (past allocation) <-- 32 wild ptrs Fatal trap 12: page fault while in kernel mode fault code = supervisor write data, page not present Stopped at usbmbuf_of_modevent+0x1cb: movb $0x41,(%rax) <-- proof

EXPECTED (patched kernel, fix.diff applied) usbmbuf_of: bytes truly required = 0x100000600 (4294968832) usbmbuf_of: allocation FAILED (NULL) <-- rejected guest stays up (kldload rc=0, uptime clean)

ARTIFACTS usbmbuf_of.c KLD harness (source of truth) Makefile guest-side kmod makefile build.sh / run.sh exact commands build.log full compiler output (clean, -Werror) run.log run transcript (baseline; ssh died on panic) panic.txt serial console: truncation math + Fatal trap 12 run.fixed.log patched-kernel run (guest survives) run.fixed.console.txt patched-kernel console excerpt env.txt guest/kernel details fix.diff 3-line guard, git-apply clean against sys/

VERDICT.md
↓ download raw

DF-5664 VERDICT β€” usb_alloc_mbufs() 32-bit alloc_size truncation

Status: REPRODUCED (privileged KLD harness) β€” latent, no unprivileged in-tree path

The bug

sys/bus/u4b/usb_mbuf.c:62 declares usb_size_t alloc_size (uint32_t per sys/bus/u4b/usb_dragonfly.h:92). Line 69:

alloc_size = (block_size + sizeof(struct usb_mbuf)) * nblocks;

block_size is usb_size_t (u32), sizeof() is size_t (u64 on LP64), and nblocks is uint16_t β€” usual arithmetic conversions evaluate the RHS in 64-bit, and the assignment then truncates to 32 bits. kmalloc() (usb_mbuf.c:71) receives only the truncated count, while the population loop (usb_mbuf.c:77-91) lays out nblocks headers followed by data areas at the full, untruncated stride:

data_ptr = (void *)(m_ptr + nblocks);      /* headers first            */
...
data_ptr += block_size;                    /* full stride per block    */

Any product β‰₯ 2^32 therefore yields an undersized allocation whose data pointers (and, for larger nblocks, even the header array) reach past its end. Because block_size is always 8-aligned and sizeof(struct usb_mbuf) is 48 (multiple of 8), a caller can choose nblocks = 2^32 / block_size exactly, making the truncated size land precisely at nblocks*48 β€” headers fit, and the first data pointer sits at exactly one-past-the-end of the chunk (quiet adjacent-slab corruption), with later data pointers up to nblocks * block_size bytes beyond (wild writes). Full attacker control of both factors is required; today only in-kernel drivers call this API.

Reachability analysis (why this is Low/hardening, not High)

The only in-tree caller is usb_fifo_alloc_buffer() (usb_dev.c:1984, called from usb_dev.c:1993). All its call sites and their ceilings:

Caller block_size nblocks product
ugen bulk/interrupt read/write (usb_generic.c:156-157) max_data_length ≀ 256 KiB + 48 (ugen_set_buffer_size cap, usb_generic.c:1669-1672) 2 ~512 KiB
ugen ISO (usb_generic.c:159-161) max_frame_size ≀ 1024Γ—48 = 49152 (xHCI hc_max_packet_count=16*3, xhci.c:3767; std size filter usb_transfer.c:477) 2Γ—nframes ≀ 2Γ—960 (USB_MAX_HS_ISOC_FRAMES_PER_XFER=8*120) β‰ˆ 94 MB
USB_FS_INIT (usb_generic.c:2280) 1 ≀ 127 β‰ˆ 7 KB
uhid (uhid.c:519,525) sc_isize+1 ≀ 64K-ish (HID report size) UHID_FRAME_NUM β‰ͺ 2^32
ulpt/urio (usbd_xfer_max_len) ≀ 1024Γ—16 IFQ maxlen (~64) β‰ͺ 2^32
ums/wsp/uep/uaudio/ubtbcmfw fixed constants fixed β‰ͺ 2^32

Largest in-tree product β‰ˆ 94 MB β€” two orders of magnitude below 2^32. No unprivileged user can reach a truncating product today. The primitive is real and armed for any future caller that scales either factor; the fix is a 3-line guard.

Reproduction (guest: DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC)

KLD harness usbmbuf_of.c (in pack) calls the real in-kernel usb_alloc_mbufs (symbol at ffffffff80a4f250, compiled into GENERIC) with block_size=0x08000000, nblocks=0x20:

  • true requirement: (0x08000000 + 48) * 32 = 0x100000600 (4,294,968,832)
  • truncated allocation: 0x600 = 1536 bytes (console printout, panic.txt)
  • all 32 mbufs enqueued; m[0]->cur_data_ptr == base + 0x600 β€” exactly one-past-end of a 1536-byte chunk (adjacent-object corruption)
  • m[31]->cur_data_ptr == base + 0xF8000600 (~3.7 GiB past the chunk)
  • one-byte write through m[31]->cur_data_ptr:
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff8020ebade80
fault code            = supervisor write data, page not present
Stopped at usbmbuf_of_modevent+0x1cb: movb $0x41,(%rax)

Guest died in DDB (ssh dropped, vm.sh status = down). Full serial-console transcript in panic.txt. This is proof the primitive exists; it is NOT an unprivileged exploit β€” loading the harness requires root.

Fix validation

  • fix.diff: compute in size_t, reject > 0xFFFFFFFFU (return NULL). git apply --check clean against sys/bus/u4b/usb_mbuf.c.
  • vm.sh reset with-src β†’ patched guest /usr/src β†’ make nativekernel β†’ make installkernel β†’ reboot (kernel #1, Oct 9 2026).
  • Re-ran the exact harness: console shows bytes truly required = 0x100000600 then allocation FAILED (NULL) β€” no kmalloc, no queue population, kldload rc=0, guest stayed up (run.fixed.log / run.fixed.console.txt). Baseline panic is GONE.

What I checked and found CLEAN (negative results with weight)

  • Layout math (usb_mbuf.c:77-91): perfect fit, last data byte == end of allocation; no off-by-one; 48-byte header keeps data 8-aligned on x86_64.
  • Alignment wrap (usb_mbuf.c:65): block_size in [0xFFFFFFF9..0xFFFFFFFF] wraps to 0 β†’ if (nblocks && block_size) skips β†’ caller sees ENOMEM via usb_dev.c:1996. Benign, no bypass (FreeBSD-identical code).
  • M_WAITOK + NULL check (usb_mbuf.c:71-75): dead-but-harmless defensive code.
  • Queue macros (usb_mbuf.h:46-84 ENQUEUE/DEQUEUE/PREPEND/QFULL/POLL): correct singly-linked discipline, empty-queue tail handling verified.
  • Consumer copies (usb_dev.c:2064-2137, 2175-2214): all MIN(len, cur_data_len)-bounded under f->priv_lock.
  • M_ZERO on allocation β†’ no uninitialized-kernel-memory leak path.
  • Consumer race hazard (NOT this file): ugen_transfer_setup (usb_generic.c:145-167) drops priv_lock around usbd_transfer_setup + usb_fifo_alloc_buffer β€” a second read()/write() on the same ugen fifo can interleave (f->xfer still NULL) and kfree queue_data mid-population. Same family as existing DF-5309/DF-5314/DF-5315 (usb_dev/usb_generic); root cause lives there, not in usb_mbuf.c.
  • usb_ethernet.c (DF-5493..5495 this pass) does NOT touch this file (zero references to usb_mbuf/USB_IF_*/usb_alloc_mbufs).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src/sys/bus/u4b/usb_mbuf.c, kernel rebuilt (make nativekernel) and installed; the identical harness then prints 'usbmbuf_of: allocation FAILED (NULL)' and the guest stays up (kldload rc=0) β€” the truncation panic is gone. fix_patched_reproduced=false means the BAD behavior did NOT reproduce on the patched kernel, i.e. the fix holds.

run.fixed.console.txt ('bytes truly required = 0x100000600' then 'allocation FAILED (NULL)'), run.fixed.log (kldload rc=0 + uptime after load), panic.txt (baseline Fatal trap 12 for contrast).
↓ fix.diffper-fix-DF-5664

Confirmed kernel references

Detail

Exploit chain

root-only KLD: kldload usbmbuf_of.ko -> usb_alloc_mbufs(0x08000000, 0x20) -> kmalloc(1536) backs only nblocks*48 -> 32 data pointers constructed OOB (first exactly at chunk end = quiet adjacent-slab overwrite primitive, last +3.7 GiB = wild write) -> deterministic kernel page fault. No unprivileged chain exists: every in-tree caller (usb_fifo_alloc_buffer at usb_dev.c:1993 and its 12 driver call sites) is capped >=2 orders of magnitude below the 2^32 truncation threshold.

Evidence (decisive lines)

panic.txt lines: 'kmalloc size (truncated!) = 0x600 (1536)' vs 'bytes truly required = 0x100000600 (4294968832)'; 32 lines 'm[N] ... <== OOB (past allocation)'; 'Fatal trap 12: page fault while in kernel mode / fault code = supervisor write data, page not present / Stopped at usbmbuf_of_modevent+0x1cb: movb $0x41,(%rax)'. Patched proof in run.fixed.console.txt: 'allocation FAILED (NULL)', guest up (run.fixed.log, kldload rc=0, uptime clean). build.log: clean -Werror kmod build.

PoC changes

Seed harness authored fresh (no prior seed): iterated 4 build fixes β€” added opt_usb.h stub (usb.h includes opt_usb.h in kmod builds), sys/condvar.h + sys/bus.h includes for usb_dev.h's struct cv/device_t, device_if.h+bus_if.h in SRCS, dropped redundant extern of usb_alloc_mbufs (usb_mbuf.h already declares it). Guest has no base64(1) β€” switched transport to base64|openssl base64 -d -A.

Verified recommended fix

Compute alloc_size in size_t and return NULL when it exceeds 0xFFFFFFFFU (see fix.diff).

Verdict

REPRODUCED with a privileged KLD harness on the live guest: sys/bus/u4b/usb_mbuf.c:69 assigns the 64-bit product (block_size + sizeof(struct usb_mbuf)) * nblocks into a usb_size_t (uint32_t) before kmalloc, so any product >= 2^32 truncates. With block_size=0x08000000/nblocks=0x20 the guest kmalloc'd 0x600 (1536) bytes for a 0x100000600 requirement, enqueued 32 usb_mbufs whose data pointers all land past the allocation (m[0] exactly one-past-end = adjacent-slab corruption; m[31] ~3.7 GiB out), and a one-byte write through m[31]->cur_data_ptr produced 'Fatal trap 12: page fault, supervisor write data, page not present' stopped at 'movb $0x41,(%rax)' in the harness. CRITICAL CAVEAT: no in-tree caller can reach a truncating product today (largest is ~94 MB via ugen ISO; all others fixed constants or capped at 256 KiB), so this is a latent CWE-190->CWE-787 hardening defect armed for future callers, not an unprivileged exploit. fix.diff (compute in size_t + reject >0xFFFFFFFFU) validated by full kernel rebuild in the guest: harness then prints 'allocation FAILED (NULL)' and the guest survives.