DF-5422 / verdict.json
{ "finding_id": "DF-5422", "status": "reproduced", "reproduced": true, "impact": "dos", "confidence": "certain", "verdict": "Live-device reproduction on the QEMU usb-serial rig bound to uftdi: write(2) of >64 bytes (more than one 64-byte bulk-OUT xfer, uftdi.c:82) followed by immediate close(2) wedges the closer forever in ttywait (wchan ttywai, tty.c:1396-1398) while only the first 64-byte packet is transmitted (host-verified). Root cause is the DragonFly ucom core close path: ucom_close() waits only for the cfg_close message (usb_serial.c:955-957), not the t_outq drain, then clears UCOM_FLAG_HL_READY (959) which kills t_oproc (ucom_start early-return, 1734-1739), and the l_close->ttylclose->ttywflush->ttywait sleep on TSA_OCOMPLETE has no timeout and no waker (ucom_get_data never calls ttwwakeup); QEMU packet tracing confirms the bulk-OUT chain is fully idle while hung. 10+ hangs reproduced across sizes 64..1MiB; settle-before-close drains cleanly (control). Any dialer-group user (cuaU* 0660) can trigger; an evil device that NAKs bulk-OUT makes it deterministic.", "exploit_chain": "local user opens /dev/cuaU0 (uucp:dialer 0660), writes >64 bytes, closes -> close(2) never returns (killable, but TX payload silently lost and port left polluted until replug); evil-USB variant: FTDI-alike NAKs bulk-OUT -> every write+close wedges deterministically. No memory-safety primitive: data-path contents verified byte-exact both directions.", "evidence": "run.log (10.05s real / exit=124 on timeout 10 uftdi_live w 1024); run.2.log (sizes 64/65/128/1024 all rc=124; settle control); host-side TX counter grew by exactly +64 bytes per 1024-byte hang; QEMU trace line count frozen 118553->118553 during the hang; ps wchan ttywai; termios_sweep.log (pre_param bounds live-verified); VERDICT.md full narrative", "kernel_refs": [ "sys/bus/u4b/serial/usb_serial.c:955-975", "sys/bus/u4b/serial/usb_serial.c:1734-1739", "sys/bus/u4b/serial/usb_serial.c:1844-1942", "sys/bus/u4b/serial/uftdi.c:390-424", "sys/kern/tty.c:1384-1406", "sys/kern/tty.c:1562" ], "poc_changes": "No seed PoC existed; authored from scratch (uftdi_live.c multi-mode trigger + uftdi_probe2.c isolation probes). Two iterations: added cfmakeraw to make RX visible (canonical mode was eating headerless test data), and split blocking vs nonblocking variants to isolate the close-race from carrier semantics.", "attempts": 12, "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": 2400, "guest_dirty": 0, "build_cmd": "cc -O2 -o /root/uftdi_live /root/uftdi_live.c (guest, DragonFly cc 8.3)", "run_cmd": "timeout 10 /root/uftdi_live w 1024; echo exit=$? (see run.sh for the full battery incl. controls)", "code_hash": "28ebfb44071abc55674e92a27732c54c54a132f906855021c1d07b097bae9000", "notes": "Rig: QEMU 7.2 piix3-usb-uhci + -device usb-serial (FTDI 0403:6001 model, incl. 2-byte IN status headers) with a host-side chardev socket daemon for RX injection / TX accounting. RX path verified byte-exact (2-byte status header stripped per packet by uftdi_read_callback). Baseline good-device open/param/close = ~0/~0/~330-390 ms. The hang is race-shaped on a good device and deterministic on an evil (NAKing) device. Not uftdi.c-local: the defect is the ucom core close/drain ordering + missing TX-complete wakeup; uftdi is the attack surface it was reproduced through.", "recommended_fix": "In ucom_close(), drain t_outq (bounded by tp->t_timeout) while HL_READY is still set so t_oproc stays live, and add the missing ttwwakeup(tp) in ucom_get_data() when the TX queue empties (full diff in fix.diff).", "fix_status": "not_testable", "fix_baseline_reproduced": 1, "fix_patched_reproduced": null, "fix_verdict": "fix.diff authored against read-only sys/ (usb_serial.c), git apply --check passes on the host repo. Not boot-tested: validating it requires a full GENERIC kernel rebuild on the guest (no /usr/obj tree present); mechanism argued line-by-line in VERDICT.md (bounded drain while t_oproc is live + the missing TSA_OCOMPLETE wakeup).", "fix_evidence": "fix.diff; VERDICT.md Fix validation section; baseline reproduction in run.log/run.2.log" } |