DF-5604 / verdict.json
{ "finding_id": "DF-5604", "status": "reproduced", "reproduced": true, "impact": "leak", "confidence": "likely", "verdict": "device_set_usb_desc() (sys/bus/u4b/usb_util.c:94-115) fills the shared per-udev scratch buffer under usbd_enum_lock(), drops the lock at :110-111, and only then hands the scratch pointer to device_set_desc_copy()s unbounded strlen()/strcpy() (sys/kern/subr_bus.c:1759-1775) and device_printf(\"<%s>\") (:114). A concurrent same-udev enumeration writer running usbd_req_get_string_any() (usb_request.c:1095) can leave the 255-byte scratch NUL-free for its whole fetch+conversion window when the device supplies 16-bit string characters (every raw byte nonzero, every char silently skipped by the filter; the only NUL is *s=0 at usb_request.c:1176), so the unlocked readers strlen() walks out of scratch.data into the struct tail and adjacent kernel heap, and those bytes are captured into dev->desc and printed to console/msgbuf. Mechanism reproduced deterministically in-kernel (5/5 runs: control 25 bytes/0 OOB vs raced 1023 bytes/769 past the boundary with adjacent-heap markers captured) via a KLD driving the verbatim production writer/reader/lock shapes; the end-to-end production interleave (malicious USB device + reprobe racing hotplug enumeration) was not driven, which is why severity is Low. Also captured: the case-0 boundary matrix proves usb_make_str_desc()s write discipline is safe (108/108), and case 2 documents the missing s==NULL guard (deliberate panic at strlen, fault address 0x0) \u2014 hardening only, no NULL-passing caller exists.", "exploit_chain": "malicious USB device supplies (a) an earlier malformed bLength=255 fetch leaving scratch.data[254] nonzero and (b) a raced string of 126 skipped 16-bit chars -> concurrent same-udev enumeration op DMAs the raw descriptor over the scratch (clobbering the attaching drivers string and its NUL) and spends the conversion window NUL-free -> the attaching thread, already past usbd_enum_unlock(), runs strlen()/strcpy() on the scratch -> OOB heap read crosses scratch.data into struct usb_device tail and neighboring slab -> bytes are copied into dev->desc and disclosed via device_printf(dev, \"<%s>\") on console/msgbuf (world-readable dmesg); secondary ceiling: panic if the scan reaches an unmapped page. Not a write primitive; no uid=0 route from this bug.", "evidence": "run.log / run.2.log: case1(RACED): strlen(scratch)=1023 ... bytes_past_boundary=769 ... desc[255..290]=\"ZZZZ...\" vs control strlen=25, 0 past boundary; case0 108 cells, 0 failures; panic.txt: Fatal trap 12 ... fault virtual address = 0x0 ... Stopped at strlen: cmpb $0,(%rdi) from usb_make_str_desc(buf,64,NULL); VERDICT.md has the full path:line trace and the reachability caveats.", "kernel_refs": [ "sys/bus/u4b/usb_util.c:94-115", "sys/bus/u4b/usb_request.c:1104-1176", "sys/bus/u4b/usb_device.h:168-179", "sys/kern/subr_bus.c:1759-1775" ], "poc_changes": "Wrote the KLD harness from scratch (no seed pack existed): sysctl-driven module with case 0 = exhaustive usb_make_str_desc boundary matrix with canaries (negative evidence), case 1 = two-kernel-thread race using the verbatim code shapes of usbd_req_get_string_any (raw descriptor bcopy + skipped-char in-place conversion + terminal *s=0) against the post-unlock strlen/copy reader, case 2 = usb_make_str_desc(buf,64,NULL) deliberate panic. Fixed against DragonFly 6.5 APIs (kthread_create 4-arg form, lockinit(lk,wmesg,timo,flags), MALLOC_DEFINE domain).", "attempts": 7, "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", "runtime_sec": 900, "guest_dirty": 0, "build_cmd": "cd /root/uprobe && make (bsd.kmod.mk, -Werror clean -> usbutil_probe.ko)", "run_cmd": "kldload /root/uprobe/usbutil_probe.ko && sysctl hw.usutil_probe=1 && sysctl hw.usutil_probe=2 (case 2 panics by design; guest reset with-src afterwards)", "code_hash": "ab20a84cd3a60071dbf687ea8063852023ba8c3183ff6835c571eb956d1da951", "notes": "Mechanism-level reproduction: the harness drives the exact production code shapes (writer usb_request.c:1129-1176, reader subr_bus.c:1759-1775, enum-lock discipline usb_util.c:94-111) with adversarial scheduling; device_set_usb_desc() itself was not invoked (needs a live enumerated udev). Production interleave requires a malicious USB device plus a concurrent same-udev scratch writer (e.g. devd/kldload reprobe racing hotplug enumeration) - hence Low severity. The writers 1-tick mid-conversion stall widens the window without changing the algorithm. FreeBSDs current device_set_usb_desc no longer fetches device strings into scratch (ctrl lock + cached manufacturer/product only). Negative results recorded in VERDICT.md: usb_make_str_desc math safe (hand + 108/108 dynamic, covers odd max_len tail byte and the /2-1 underflow guard), usb_printbcd bounded, usb_trim_spaces in-bounds, usb_pause_mtx timo+1 safe for all 84 call sites, usb_pause_ls dead code (0 callers), no ivars type confusion. usb_make_str_desc lacks an s==NULL guard (FreeBSD likewise) - hardening note only.", "recommended_fix": "In device_set_usb_desc(), snapshot the scratch string into a 256-byte stack buffer with bcopy(temp_p, buf, sizeof(udev->scratch.data)); buf[255] = 0 while the enum lock is held, then unlock and use the snapshot for device_set_desc_copy()/device_printf (see fix.diff).", "fix_status": "not_testable", "fix_baseline_reproduced": 1, "fix_patched_reproduced": null, "fix_verdict": "fix.diff is a 5-line copy-under-lock snapshot, correct by inspection (both scratch writers NUL-terminate within [0,255) while holding the enum lock, so the snapshot is always a valid string and post-unlock readers touch only the stack copy). Not build-validated: the mechanism harness drives the production code shapes rather than calling device_set_usb_desc() itself, so a patched-kernel re-run could not honestly exercise the fixed path (it would produce an identical result by construction, not a meaningful A/B); the true A/B needs the end-to-end interleave (emulated malicious device + reprobe race) which this guest cannot drive.", "fix_evidence": "findings/poc/DF-5604/fix.diff (git-apply-able vs sys/bus/u4b/usb_util.c; authored post-verification, never applied to sys/)" } |