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)
PoC verification
Evidence pack
findings/poc/DF-5664 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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/
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 --checkclean againstsys/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 = 0x100000600thenallocation 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_sizein [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
fixedfix.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).
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.
No comments yet.