DF-5381 / verdict.json
{ "finding_id": "DF-5381", "status": "reproduced", "reproduced": true, "impact": "leak", "confidence": "certain", "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.", "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": "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", "kernel_refs": [ "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" ], "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.", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline; fix validated on #1: Sat Oct 3 21:56:23 UTC 2026)", "runtime_sec": 3780, "guest_dirty": 0, "build_cmd": "cd /root/df5381 && make -m /usr/src/share/mk", "run_cmd": "kldload /root/df5381/df5381.ko; dmesg | awk /DF5381/{f=1} f; kldunload df5381 (3x via run3.sh)", "code_hash": "45f6599ce0e4f7ea4b2d86aa631d379905d84863a438835e89f914060acf0c81", "notes": "T4 on the patched kernel prints non-zero bytes but is an UNWRITTEN destination buffer (copy loop breaks first; byte-identical across runs) - proof the fix truncates, not a leak. T2b PASS on both kernels writes within the engines own doubled 400-byte kmalloc block (usb_busdma.c:526-528), safe by design. Bonus contract observation: usb_pc_alloc_mem NULL-derefs pc->tag_parent (usb_busdma.c:495) - kernel-driver-only. Residual gap: Case 2 PIO caches (usb_busdma.c:119-125) track no extent; needs a size field in struct usb_page_cache (follow-up). Guest reset to clean-source snapshot after validation.", "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.", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "fix.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.", "fix_evidence": "run.fixed.log (3 runs); VERDICT.md baseline-vs-patched table; kernel identity in env.txt" } |