USB copy engine enforces no bounds: usbd_get_page + usbd_copy_in/out/_user/frame_zero/usb_uiomove memcpy across the loaded extent with no clamp β single chokepoint for ~120 call sites (cross-object heap OOB read/write/zero + wild KVA/UINT32_MAX span + instant panic demonstrated via root KLD; in-tree precedent DF-0998/DF-2357)
Summary
usbd_get_page resolves any caller offset without consulting the loaded extent the same struct tracks, and all six copy primitives then memcpy/copyin/copyout min(res.length,len) to that unbounded address - a single driver-side length/offset slip becomes a direct kernel-heap OOB read/write/zero. Root-KLD-proven on the stock guest 3/3: 16-byte cross-object OOB WRITE (read back through the victim object), OOB read, frame_zero cross-object zeroing, get_page returning buffer+0xFFFFFF00/length=UINT32_MAX, stale physaddr from 12 pages past a 2-page load, and an instant Fatal trap 12 inside usbd_copy_out. No live unpriv trigger in-tree (ugen clamps; actlen truncated) but DF-0998/DF-2357 shipped exactly this slip class as 16-byte DMA overwrites - the chokepoint clamp would have caught both. Extent-clamp-in-get_page + zero-length-terminate loops fix, validated on rebuilt kernel (OOB gone, in-extent + ehci/xhci unaffected).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5381 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.0 KB | β raw | |
| VERDICT.md | β | 6.3 KB | β raw | |
| df5381.c | β | 11.5 KB | view raw | |
| Makefile | β | 73 B | β download | |
| build.sh | β | 137 B | view raw | |
| run.sh | β | 160 B | view raw | |
| guest_build.sh | β | 76 B | view raw | |
| guest_run.sh | β | 136 B | view raw | |
| build.log | β | 65 B | view raw | |
| run.log | β | 3.7 KB | view raw | |
| run.2.log | β | 18 B | view raw | |
| run.fixed.log | β | 3.6 KB | view raw | |
| panic.txt | β | 3.0 KB | view raw | |
| env.txt | β | 326 B | view raw | |
| fix.diff | β | 1.7 KB | view raw | |
| verdict.json | β | 6.1 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw |
DF-5381 β USB copy engine has no bounds enforcement (usb_busdma.c)
What this is
A root KLD harness that drives the in-kernel USB copy engine
(usbd_get_page / usbd_copy_in / usbd_copy_out / usbd_frame_zero,
sys/bus/u4b/usb_busdma.c) with offsets/lengths beyond the extent of the
DMA buffer backing a usb_page_cache. All allocations are made through
the engine's own public API (usb_dma_tag_setup + usb_pc_alloc_mem),
i.e. fully legitimate page caches; only the copy offsets/lengths are out
of extent β exactly what a single driver-side arithmetic slip produces.
Build (on the guest, as root; /usr/src must exist)
mkdir -p /root/df5381 cp df5381.c Makefile /root/df5381/ cd /root/df5381 && make -m /usr/src/share/mk # -> df5381.ko
(guest_build.sh wraps this)
Run
kldload /root/df5381/df5381.ko
dmesg | awk '/DF5381/{f=1} f'
kldunload df5381
(guest_run.sh = one load/unload; run3.sh = three consecutive runs)
Expected output β UNPATCHED kernel (baseline, 3/3 runs identical)
[DF5381] T1 PASS cross-object OOB READ (read pcB's bytes via pcA offset 512) [DF5381] T2 PASS cross-object OOB WRITE (planted 16 bytes into pcB via pcA offset 512) [DF5381] T2b PASS write past object extent offset=200 accepted [DF5381] T3 PASS cross-object OOB ZERO via usbd_frame_zero [DF5381] T5 PASS engine hands out wild KVA +4GB-256 and length=UINT32_MAX [DF5381] T6 PASS page-array walked 12 pages past a 2-page load; stale physaddr returned verbatim [DF5381] RESULT 6/7 primitive tests passed
Additionally, the unguarded foreign-page variant (reading pcM.buffer+8192
where that page is an unmapped hole) took the whole kernel down β
Fatal trap 12 ... Stopped at memcpy+0x55 β captured in panic.txt.
Expected output β PATCHED kernel (fix.diff applied, 3/3 runs identical)
All OOB primitives are refused:
T1 FAIL cross-object read mismatch <- clamp fired, chk[] never written T2 FAIL cross-object write not observed <- pcB still holds its sentinel = NOT corrupted T3 FAIL zero not observed <- pcB untouched T5 ... length=0x00000000 <- no more wild KVA / UINT32_MAX span T6 ... physaddr=0x0 length=0 <- stale page-array entry no longer served
(T2b "PASS" on both kernels is a write at offset 200 < 400-byte doubled
allocation β inside the engine's own kmalloc'd block, safe by design.
T4 "PASS" on the patched kernel is an unwritten-destination artifact:
the copy loop breaks before touching leak[], which then holds stale
harness stack β see VERDICT.md.)
Success criterion: on the unpatched kernel, bytes written through pcA at an out-of-extent offset are read back through the neighbouring pcB (cross-object corruption) and vice versa; on the patched kernel the neighbour's contents are never modified and no wild pointers are handed out, while in-extent copies (sentinel plant + read-back in T1 setup) keep working and the USB stack (ehci.ko/xhci.ko) loads normally.
DF-5381 VERDICT β reproduced (engine primitive), fix validated
File: sys/bus/u4b/usb_busdma.c
Finding: The USB DMA copy engine performs no bounds enforcement
whatsoever. usbd_get_page() (usb_busdma.c:79-126) resolves any caller
offset against the page cache without consulting the loaded extent that
is already tracked in the same structure (page_offset_end /
page_offset_buf, set at usb_busdma.c:545/616-617 and 427-428), and
every copy primitive β usbd_copy_in (131), usbd_copy_in_user (159),
usb_uiomove (216), usbd_copy_out (248), usbd_copy_out_user (276),
usbd_frame_zero (304) β then memcpy/copyin/copyout/uiomoves
min(res.length, len) bytes to/from that unbounded address
(usb_busdma.c:140-147, 254-265).
Reproduced? YES β three faces of the primitive, all on the standard guest
Baseline kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
(x86_64, USB core compiled in; nm /boot/kernel/kernel shows
T usbd_get_page/usbd_copy_in/usbd_copy_out/...).
A root KLD (df5381.c) builds 32 fully legitimate 200-byte
usb_page_cache objects via the engine's own usb_dma_tag_setup() +
usb_pc_alloc_mem() API (the same path usb_bus_mem_alloc_all uses),
picks two heap-adjacent objects (KVA gap 512 = the kmalloc chunk), and
drives the engine exactly as a buggy driver would:
- Cross-object kernel-heap OOB WRITE (3/3 runs) β
usbd_copy_in(pcA, 512, pattern, 16)plants 16 bytes into pcB's allocation; read-back through pcB returns the planted bytes (run.log T2). This is precisely the DF-0998 / DF-2357 in-tree bug class (16-byte heap overwrites past USB DMA buffers that shipped in this tree), caught live. - Cross-object kernel-heap OOB READ (3/3 runs) β
usbd_copy_out(pcA, 512, ...)returns pcB's contents (run.log T1);usbd_frame_zero(pcA, 512, 16)zeroes them (T3). In a real driver this is how adjacent kernel heap is exfiltrated through /dev/ugen* (the DF-1103 / DF-2608 leak pattern). - Instant kernel panic (1/1) β the same OOB read aimed at the page
after a 2-page kmem buffer hit an unmapped kernel VA hole:
Fatal trap 12: page fault ... fault virtual address = 0xfffff8011863e000 (= pcM.buffer+8192) ... Stopped at memcpy+0x55from insideusbd_copy_outduringkldload(panic.txt). Guest went down β a single miscalculated driver-side length is a kernel DoS. - Unbounded pointers out of
usbd_get_page(3/3) βget_page(pc, 0xFFFFFF00)hands outbuffer + 0xFFFFFF00withlength = UINT32_MAX(T5); the multiseg page-array walk serves a stale/poisonedphysaddr(0xdeadbeef0000) from 12 pages past a 2-page load (T6) β i.e. HCDs would program DMA descriptors from garbage.
No escalation chain is claimed: no live unprivileged trigger exists in
the tree today. The one userspace-reachable client (usb_generic.c)
clamps nFrames (usb_generic.c:1069) and sum(length) <=
max_data_length (usb_generic.c:1166-1170), and usbd_transfer_done
truncates actlen > sumlen (usb_transfer.c:2952-2959). The finding is
therefore filed as Info / defense-in-depth: the engine is the single
chokepoint for ~120 call sites, at least four of which
(DF-0998, DF-2357, if_axge's device-derived hdr_off, if_urndis's
device-derived rm_dataoffset) have already depended on that arithmetic
being perfect everywhere.
Bonus observation (same contract class)
usb_pc_alloc_mem dereferences pc->tag_parent without a NULL check
(usb_busdma.c:495 β usb_dma_tag_find β movzbl 0x3a(%rdi), rdi=0):
an uninitialized page cache panics the kernel instantly (observed on the
guest during harness bring-up; usb_transfer.c:842 is the only thing
that guarantees tag_parent is set). Kernel-driver-contract issue only.
Fix validation β YES, fixed
fix.diff (git-apply-able against sys/bus/u4b/usb_busdma.c) adds:
- a bound in usbd_get_page Case 1: offset β₯ page_offset_end -
page_offset_buf β return length = 0, buffer = NULL,
physaddr = 0;
- break (or EFAULT for the _user/uiomove variants) on zero-length
results in all six copy loops, so truncation terminates cleanly.
Applied to the guest's /usr/src, incremental nativekernel rebuild,
installkernel, reboot into #1: Sat Oct 3 21:56:23 UTC 2026, exact
same harness 3Γ:
| test | baseline (unpatched) | patched |
|---|---|---|
| T1 cross-object read | PASS (sentinel read via pcA) | FAIL β clamp fired, no copy |
| T2 cross-object write | PASS (pattern in pcB) | FAIL β pcB still holds sentinel β not corrupted |
| T3 cross-object zero | PASS | FAIL β pcB untouched |
| T5 wild KVA/UINT32_MAX length | PASS | length=0, no wild pointer |
| T6 stale physaddr served | PASS | physaddr=0, length=0 |
| guest | up | up (USB stack loads: ehci.ko, xhci.ko) |
In-extent engine copies keep working on the patched kernel (the T1 setup plants and reads back the sentinel through the engine itself), so the clamp does not break legitimate transfers. Two artifacts to not mis-read on the patched runs (both are proof the fix works): "T2b PASS" writes at offset 200 < the engine's own doubled 400-byte allocation (safe by design of the usb_busdma.c:526-528 doubling), and "T4 PASS" shows an unwritten destination buffer (the copy loop breaks before touching it; the printed bytes are stale harness stack β note they are byte-identical across runs, unlike a real read).
Residual gap (documented, out of scope of this diff): Case 2 ("Plain
PIO", usb_busdma.c:119-125) caches have no extent tracked at all; fully
closing them needs a new size field in struct usb_page_cache.
Harness changes vs. the seeded idea
No seed PoC existed; the harness was authored from scratch. Iterations:
(1) first version panicked via the tag_parent NULL contract (kept as
evidence, harness now wires tag_parent like usb_transfer.c:842);
(2) second version's foreign-page read panicked on an unmapped hole
(kept as panic.txt, read now skipped); (3) final version stable 3/3.
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, incremental nativekernel rebuild, installkernel, reboot into DragonFly 6.5-DEVELOPMENT #1 Sat Oct 3 21:56:23 UTC 2026, exact same harness 3x: all out-of-extent primitives refused (T1/T2/T3 no longer touch the neighbouring object - pcB retains its sentinel; T5 length=0, no wild KVA; T6 physaddr=0), in-extent copies still function, ehci.ko/xhci.ko load normally, guest stable.
run.fixed.log (3 runs); VERDICT.md baseline-vs-patched table; kernel identity in env.txt
Confirmed kernel references
- sys/bus/u4b/usb_busdma.c:79
- sys/bus/u4b/usb_busdma.c:92
- sys/bus/u4b/usb_busdma.c:94
- sys/bus/u4b/usb_busdma.c:108
- sys/bus/u4b/usb_busdma.c:121
- sys/bus/u4b/usb_busdma.c:131
- sys/bus/u4b/usb_busdma.c:140
- sys/bus/u4b/usb_busdma.c:159
- sys/bus/u4b/usb_busdma.c:216
- sys/bus/u4b/usb_busdma.c:248
- sys/bus/u4b/usb_busdma.c:254
- sys/bus/u4b/usb_busdma.c:276
- sys/bus/u4b/usb_busdma.c:304
- sys/bus/u4b/usb_busdma.c:545
- sys/bus/u4b/usb_busdma.c:616
Detail
Exploit chain
Primitive chain (root KLD harness, no unpriv in-tree trigger found): (1) driver-side arithmetic slip supplies offset/len beyond the frame buffer extent into usbd_copy_in/out; (2) engine resolves pc->buffer+offset with no clamp -> arbitrary-direction kernel heap read/write past the DMA buffer into adjacent slab objects (T1/T2/T3: cross-object read/write/zero of a 512-byte-chunk neighbour, 3/3 runs); (3a) read variant: adjacent kernel heap flows to the USB device/userspace (ugen read path) - leak; (3b) write variant: forge adjacent object fields - DF-0998/DF-2357 shipped exactly this as 16-byte overwrites; (3c) large-offset variant: unmapped kernel VA -> instant fatal kernel page fault (panic.txt, guest down). Weaponization to uid=0 requires a live buggy call site; none found in this pass (ugen bounds enforced, HCD actlen truncated).
Evidence (decisive lines)
run.log (baseline 3 runs: T1/T2/T2b/T3/T5/T6 PASS each, RESULT 6/7); panic.txt (Fatal trap 12 at memcpy+0x55, fault VA 0xfffff8011863e000 = pcM.buffer+8192, from usbd_copy_out during kldload - guest down); run.fixed.log (patched kernel 3 runs: pcB keeps sentinel a0 a1 .. af = not corrupted, length=0/physaddr=0 for T5/T6, guest up); VERDICT.md (full narrative incl. table baseline-vs-patched); fix.diff (git-apply-able); env.txt; build.log
PoC changes
Authored from scratch (no seed). Three iterations: (1) wire pcs[].tag_parent like usb_transfer.c:842 or usb_pc_alloc_mem NULL-derefs (observed panic, kept as note); (2) T7 unguarded foreign-page read panicked on an unmapped kernel hole (kept as panic.txt; read now skipped with explanation); (3) final harness stable. Also required sys/condvar.h + struct forward-decls before usb_busdma.h, and sys/conf.h for DEV_MODULE under -Werror.
Verified recommended fix
Clamp usbd_get_page Case 1 to the loaded extent (page_offset_end - page_offset_buf) returning length=0/buffer=NULL/physaddr=0, and break (or EFAULT) on zero-length in all six copy loops - see fix.diff.
Verdict
The USB copy engine in sys/bus/u4b/usb_busdma.c enforces no bounds at all: usbd_get_page (usb_busdma.c:79-126) resolves any caller offset without consulting the loaded extent already tracked in the same struct (page_offset_end/page_offset_buf), and usbd_copy_in/_out/_user/frame_zero/usb_uiomove memcpy/copyin/copyout the full requested span (usb_busdma.c:140-147, 254-265). A root KLD driving the engine with legitimate usb_dma_tag_setup+usb_pc_alloc_mem page caches and out-of-extent offsets demonstrated, 3/3 runs each on the stock guest kernel: a 16-byte cross-object kernel-heap WRITE into the neighbouring kmalloc chunk (read back through the victim), a cross-object READ, cross-object ZEROing, usbd_get_page handing out buffer+0xFFFFFF00 with length=UINT32_MAX, and the multiseg page-array walk serving a stale physaddr from 12 pages past a 2-page load. The same OOB read aimed one page past a 2-page kmem buffer hit an unmapped kernel VA hole and took the kernel down (Fatal trap 12, memcpy+0x55 inside usbd_copy_out; panic.txt). This is the exact primitive behind the in-tree DF-0998 and DF-2357 16-byte USB DMA heap overwrites. No live unprivileged trigger exists today (ugen clamps nFrames at usb_generic.c:1069 and sum(length) at 1166-1170; actlen is truncated at usb_transfer.c:2952-2959), so the finding is filed as Info/defense-in-depth at the single chokepoint of ~120 call sites. fix.diff (extent clamp in usbd_get_page Case 1 + break/EFAULT on zero length in all six copy loops) was built into the guest kernel and validated: every OOB primitive is refused, the neighbouring object keeps its sentinel (uncorrupted), in-extent transfers still work, and the USB stack (ehci.ko/xhci.ko) loads normally.
No comments yet.