⬢ DragonFlyBSD Kernel Audit
DF-5422 / verdict.json
← back to finding ↓ download raw
{
  "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"
}