DF-5578 / verdict.json
{ "finding_id": "DF-5578", "status": "reproduced", "verdict": "Reproduced via the DF-5455-family KLD harness (verbatim production ufm.c, sha256 845738f2..., one guard hunk rerouting the EP0 transport to a traced-semantics capture shim; no Cypress radio exists on the guest): ufm_do_req() loads uninitialized stack buf[0] into *retbuf BEFORE the error check (ufm.c:183,191,193-194), so every failed/short EP0 request hands stale kernel stack to the caller. On the CURRENT stack this never reaches userspace -- the plain usbd_do_request macro passes flags=0 (usbdi.h:511-512), the default EP0 template has no short-ok flags (usb_transfer.c:76-88), a short DATA stage completes with USB_ERR_SHORT_XFER (usb_transfer.c:2979-2985), ufm_do_req returns ENXIO and both callers discard the byte (ufm.c:253-256,288-291); err==0 without the 1-byte copy-out is provably impossible (success requires actlen==sumlen, summed over completed frames, usb_transfer.c:2936-2940,2952+; copy at usb_request.c:680-681; hr_func path unreachable for host-mode devices, usb_request.c:335-345). Runtime proof: FULL FM_GET_STAT chain with short/timeout semantics -> EIO with the output words sentinel UNCHANGED (leak blocked) in 3/3 loads. The one-flag DF-1062 counterfactual (USB_SHORT_XFER_OK / usbd_do_request_proc with actlen=NULL, as uchcom.c:398 + usb_serial.h:220 do) was exercised explicitly: 18/18 deterministic pattern-match of the stale byte through the production ufm_do_req, and the one-line fixed twin (buf[1]={0}) is 18/18 clean. Exposure ceiling even in the counterfactual: 1 byte per FM_GET_STAT, behind an O_RDWR open of a 0644 root:operator node (ufm.c:153,304-306) -> root-only by default. Info severity, filed so drift does not silently turn ufm into DF-1062/DF-5455.", "reproduced": true, "impact": "none", "confidence": "certain", "exploit_chain": "", "evidence": "run.log (160 lines, 3 loads): gate fflags=FREAD : rc=13 (EACCES), FULL FM_GET_STAT short/silent : rc=5 (EIO) out=0x000000cc -- LEAK BLOCKED (discarded), FULL FM_GET_STAT full : rc=0 out=0x0000005c -- device byte delivered, FULL FM_GET_STAT shortok : rc=0 -- STALE STACK BYTE REACHES IOCTL CALLER, calibrate: poison depth 1 MATCHES buf[0] slot, RESULT: counterfactual short-ok: 18/18 pattern-match; fixed twin clean 18/18, plus the EP0 request capture (SET_FREQ 100.7MHz -> bReq=1 wVal=0x0022 wIdx=0x22d0 then CMD0 0x96/0xb7; INT_MAX/INT_MIN -> 0xfd64/0x6442 wrap; START/STOP tuples match ufm.c:249-273). build.werror_evidence.log: -Wuninitialized-armed production-shaped build rc=0 with ZERO diagnostics. fix_build.log: patched production ufm.ko builds -Werror-clean rc=0. env.txt: guarded copy == production + 1 guard hunk (diff shown), guest tree == audited tree.", "kernel_refs": [ "sys/bus/u4b/misc/ufm.c:176-200", "sys/bus/u4b/misc/ufm.c:183", "sys/bus/u4b/misc/ufm.c:193-194", "sys/bus/u4b/misc/ufm.c:253-257", "sys/bus/u4b/misc/ufm.c:288-292", "sys/bus/u4b/misc/ufm.c:304-306", "sys/bus/u4b/misc/ufm.c:153", "sys/bus/u4b/usbdi.h:511-512", "sys/bus/u4b/usb_request.c:646-682", "sys/bus/u4b/usb_request.c:697-704", "sys/bus/u4b/usb_request.c:782-787", "sys/bus/u4b/usb_request.c:335-345", "sys/bus/u4b/usb_transfer.c:76-88", "sys/bus/u4b/usb_transfer.c:2936-2940", "sys/bus/u4b/usb_transfer.c:2952-2985", "sys/bus/u4b/usb_dev.c:593-618", "sys/bus/u4b/usb_dev.c:2499-2502", "sys/kern/sys_generic.c:668-730" ], "poc_changes": "No seed PoC existed. Authored the family-standard harness from scratch: guarded production copy + cross-TU shim (modes: short->SHORT_XFER error, silent->TIMEOUT error, full 1-byte, counterfactual short-ok success), fabricated struct usb_fifo (public in usb_dev.h:102) to drive the REAL ufm_ioctl dispatch, recursive stack-poison with load-time depth calibration (depth 1), in-harness fixed twin, EP0 request-tuple capture. Build iterations: (1) -Wmissing-prototypes in shim TU, (2) prototype placement before struct def -- both fixed; final build clean.", "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", "runtime_sec": 240, "guest_dirty": 0, "build_cmd": "sh build.sh (in-tree kmod: /usr/src/sys/bus/u4b/misc/df5578; cp production ufm.c -> ufm_prod_guarded.c; patch -p0 < prod_guard.diff; make)", "run_cmd": "sh run.sh (kldload /root/df5578/ufmunleak.ko x3; kldunload; dmesg | grep UFM5578)", "code_hash": "b702c06a06e3b621b6205fd2b09eab7271eaafa26b7d4c8b6de4902d14176497", "notes": "Info severity, no user-visible impact on the current stack (leak blocked by error semantics -- runtime-proven); counterfactual demonstrates the DF-1062 one-flag delta. code_hash = sha256(ufm5578_harness.c); shim sha256 2b49aa13c5af83476d66e48dd18d5d432fd58dc055899934b3895a1cb22083a7, fix.diff sha256 473be17de005db4e528f2603ec0b161747d587c7a9b4c8b72dbcfd391047ab2d. Pass-2 negative results with citations in VERDICT.md: DF-1062 class contained (flags=0), DF-5320 family clean (fifo refcount + cv_drain before lockuninit), ioctl buffers kernel-copied, gate correct, attach-error path NULL-safe, freq-math overflow device-bound only. Guest reset with-src after evidence collection.", "recommended_fix": "Initialize the EP0 reply buffer in ufm_do_req(): uint8_t buf[1] = {0}; (full diff in fix.diff; optionally move *retbuf = buf[0] under !error).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 1, "fix_verdict": "Fix validated at the family-standard module level (Info finding, no kernel rebuild required): baseline (unfixed production code, counterfactual short-ok mode) reproduces the stale-byte flow 18/18; the fixed twin (verbatim ufm_do_req + the one-line init, same poisoning, same mode) returns 0x00 18/18; the patched production ufm.ko builds -Werror-clean on the guest (fix_build.log, rc=0).", "fix_evidence": "fix.diff (sha256 473be17d...); fix_build.log (patched production ufm.ko, RC=0); run.log FIXED(buf={0}) lines (18/18 clean)." } |