β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5504

ucycom cfg callbacks issue synchronous 1000 ms-timeout EP0 SET_REPORT requests with discarded errors: an evil DeLorme Earthmate stretches every open(2)/tcsetattr to ~1.1 s of uninterruptible ucom-taskqueue occupancy, blocking close(2) SIGKILL-immune behind the queue and stalling the USB hub event thread on unplug (user-amplifiable; smallest cfg-stall family member)

Summary

One sync 1000ms SET_REPORT per cfg callback with the error discarded; open and every tcsetattr trigger it. ucycom-specific precision: the chains are single-request so abort-on-error is structurally useless (a device answering at 950ms makes every request SUCCEED - measured occupancy unchanged), leaving the timeout bound the only lever. Verbatim-code harness: closer blocked 2094/2111ms (silent) and 1987/1989ms (950ms-answering device - requests succeed); 250ms-bound fix-modeled 638-661ms.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5504 Β· 14 files
FileTypeDescriptionSize
ucycom_cfgstall_harness.c β€” 12.4 KB view raw
Makefile β€” 108 B ↓ download
extract.sh β€” 2.8 KB view raw
build.sh β€” 181 B view raw
run.sh β€” 180 B view raw
build.log β€” 7.1 KB view raw
extract.log β€” 194 B view raw
run.log β€” 2.6 KB view raw
run.2.log β€” 558 B view raw
run.patchedkernel.log β€” 1.3 KB view raw
fix_compile.log β€” 7.0 KB view raw
fix.diff β€” 1.1 KB view raw
env.txt β€” 360 B view raw
VERDICT.md β€” 6.8 KB ↓ raw
VERDICT.md
↓ download raw

DF-5504 VERDICT β€” ucycom cfg-stall (reproduced, mechanism-level)

Finding

ucycom_cfg_write() (ucycom.c:412-444) issues ONE synchronous EP0 SET_REPORT(feature) per cfg callback via ucom_cfg_do_request(sc->sc_udev, &sc->sc_ucom, &req, sc->sc_temp_cfg, 0, 1000) (ucycom.c:438 β€” 1000 ms timeout) and DISCARDS the usb_error_t ("device request failed, err=%s (ignored)", ucycom.c:440-443). It is called from:

  • ucycom_cfg_open() (ucycom.c:299-306) β€” every first open(2) of the tty (ucom core queues it at usb_serial.c:865-867 after a cfg_param task queued by the in-open ucom_param() call at usb_serial.c:842; that first param task no-ops before UCOM_FLAG_LL_READY is set β€” usb_serial.c:1626 β€” so the first open costs one request, not two);
  • ucycom_cfg_param() (ucycom.c:474-507) β€” every tcsetattr(2) of an open port (usb_serial.c:1691-1693).

The ucom core adds usb_pause_mtx(sc->sc_lock, hz/10) after each callback (usb_serial.c:777 and :1636), so each cfg task occupies the per-adapter ucom taskqueue for ~1100 ms against an evil device. close(2) is synchronous against that queue β€” ucom_close() β†’ ucom_queue_command(ucom_cfg_close) β†’ usb_proc_mwait() (usb_serial.c:671-672 + 955-957), an uninterruptible wait (usb_process.c:385-393) β€” and unplug pins the USB hub event thread the same way through ucom_detach() β†’ usb_proc_drain() (usb_serial.c:394). ucycom has NO ucom_cfg_close callback, so close itself issues zero EP0 requests; the damage is purely the wait behind queued cfg tasks. ucycom is the mildest member of the cfg-stall family (1 request per task vs. 3–14 for uchcom/uslcom/umoscom/ubsa/ umct/uftdi/uvscom), but the amplification is the same shape: a local user sustaining a tcsetattr loop on the open /dev/cuaU* keeps ~1.1 s/task of uninterruptible occupancy going for as long as the loop runs, and SIGKILL cannot preempt a closer already inside usb_proc_mwait.

Reproduction (KLD-harness-with-production-code)

No Cypress CY7C64013 device model exists on the guest. The harness compiles the VERBATIM production code (sed-extracted at build time from the guest's md5-gated /usr/src: ucycom.c c7b432ce…, usb_serial.c dc5c04db…):

  • ucycom.c:62-101+181-182 (softc/constants), :112,117-119 (protos), :299-306, :412-444, :446-472, :474-507 β€” the driver's whole cfg chain;
  • usb_serial.c:635-680 (ucom_queue_command), :756-780, :906-922, :1619-1637 (core cfg task wrappers);

and runs it on the REAL usb_proc_create/msignal/mwait taskqueue machinery (the same machinery ucom's cfg callbacks run on), with a real zeroed softc and lockinit(…, LK_CANRECURSE) exactly as ucycom_attach does (ucycom.c:214). The single interposed symbol is usbd_do_request_proc (usb_request.c:760): an evil-device model that sleeps timo ms ("WXFER") and returns USB_ERR_TIMEOUT (silent) or succeeds at timo-50 (slow-answer), logging each request's verbatim framing. Task queueing replicates ucom_open's production shape (usb_serial.c:829-872) and the user's tcsetattr shape (:1690-1693), both through the verbatim ucom_queue_command.

Measured (run.log, run.2.log β€” fresh guest, hz=100)

  • Phase A (first open + 1 tcsetattr + close; silent device): closer blocked 2094 / 2111 ms, 2 EP0 requests burned (cfg_open + cfg_param tasks; the first-open param task no-ops as in production).
  • Phase 2 (2 tcsetattrs): 2106 / 2097 ms β€” the rapid second tcsetattr coalesces into the pending sc_param_task double-buffer slot (production last-wins semantics); a sustained loop re-queues as tasks drain, extending the occupancy for as long as the attacker keeps looping (~1.1 s per processed task; sibling DF-5496 measured the same coalescing with a 5-burst).
  • Phase S (device answers at 950 ms β€” every request SUCCEEDS): 1987 / 1989 ms. This is the ucycom-specific point: with only one request per task, the family's "abort the chain on first error" mitigation is a no-op even when the device does fail, and covers nothing when it succeeds. The only in-file lever is the timeout itself.
  • Phase C (fix modeled on a second sed-fixed extraction of the same verbatim code, 0, 1000 β†’ 0, UCYCOM_CFG_TIMEOUT(250)): 638–661 ms.

Fix validation

  • fix.diff (UCYCOM_CFG_TIMEOUT 250): git apply --check clean on the audited repo; the patched production ucycom.c (md5 7044be95… built on host, spliced byte-identical into the guest tree) compiled -Werror-clean, 0 warnings, as the real in-tree-style ucycom.ko (fix_compile.log, RC=0).
  • Behavioral proof at mechanism level: phase C 2094 β†’ 638-661 ms.
  • Scope caveat: compile + mechanism level per the family evidence class; no physical crafted Earthmate exists on this guest.

Pass-2 negative results (this file, recorded for weight)

  • RX HID framing bounded: 64013 len = buf[1] (device, ≀255) and 63743 3-bit count are both clamped by actlen (ucycom.c:558-559); actlen ≀ frame len = sc_ilen ≀ 1026 = bufsize (attach clamp ucycom.c:247-254; usb_transfer.c:681).
  • TX 2-frame control write bounded by construction: frame0 must be 8 bytes (usb_transfer.c:1560-1565) and the data stage ≀ bufsize - 8 = 1026 (usb_transfer.c:681,718-726); sc_olen - offset β‰₯ 0 since sc_olen β‰₯ 2 enforced (ucycom.c:248); usbd_copy_in walks only allocated pages (usb_busdma.c:131-149).
  • sc_temp_cfg[5..31] sent to the device when sc_flen > 5 is zero-filled, not uninitialized: the newbus softc is kmalloc(…, M_ZERO) (subr_bus.c:1953). (And the recipient is the attacker's own device.)
  • Attach error path (goto detach β†’ ucycom_detach β†’ ucom_detach without ucom_attach having run) is safe: ucom_detach returns immediately unless UCOM_FLAG_ATTACHED is set (usb_serial.c:371-372).
  • usbd_req_get_hid_desc M_ZERO's its buffer and passes the device-declared length as both alloc size and request length (usb_hid.c:819-835): short transfers zero-pad, no uninit read reaches hid_report_size.
  • ucycom_pre_param whitelists speeds 600–57600 (ucycom.c:446-472); B0/0 β†’ EINVAL, so ucycom_cfg_write never sees a degenerate divisor (the chip takes the raw baud word; there is no division in the driver at all).
  • The 64013 TX length byte data[1] = actlen truncates >255-byte writes (protocol misframing only; requires a malicious HID descriptor declaring olen > 257 and only misframes the attacker's own device β€” no host impact).
  • Unplug-lifecycle/UAF concerns are DF-5320 (usb_serial.c core), not re-filed.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched reproduced

fix.diff is git-apply-able against the audited repo (git apply --check passes). The patched production driver (host-built md5 7044be95..., spliced byte-identical into the guest /usr/src after the guest's own patch(1) balked at the well-formed hunk) compiled -Werror-clean, 0 warnings, RC=0 as the in-tree-style ucycom.ko (fix_compile.log). Harness phase C models the exact change on a second sed-fixed verbatim extraction: closer block 2094/2111 ms -> 638-661 ms, and phase S shows why the timeout (not error propagation) is the lever for ucycom. Scope caveat: mechanism + compile level per the family evidence class; end-to-end with a physical crafted Earthmate is not possible on this guest.

fix.diff (sha256 38ce5578ca8566dbaa36b13caa684e3c7c11c0db5bedc5b778e97b70923a104f); fix_compile.log (RC=0); run.log/run.2.log 'UC5504[C]: closer: unblocked ELAPSED=638/639 ms' vs 'UC5504[A]: ... 2107/2111 ms'; VERDICT.md 'Fix validation' section
↓ fix.diffper-fix-DF-5504

Confirmed kernel references

Detail

Exploit chain

evil USB device with DeLorme Earthmate VID:PID enumerates as ucycom on insertion; a local user (any uid) opens /dev/cuaU* (cfg_open task ~1.1 s of taskqueue occupancy), then sustains a tcsetattr(2) loop: each processed cfg_param task pins the per-adapter ucom taskqueue ~1.1 s with the device silent; close(2) from any thread blocks SIGKILL-immune in usb_proc_mwait behind the occupied queue for as long as the loop runs; unplugging mid-cycle pins the per-bus USB hub event thread the same way via usb_proc_drain

Evidence (decisive lines)

run.log/run.2.log lines 'UC5504[A]: closer: unblocked ELAPSED=2107 ms', 'UC5504[S]: ... 1987 ms' (requests SUCCEED), 'UC5504[C]: ... 638 ms' (fix modeled), and the per-request framing lines 'EP0 -> bmRequestType=0x21 bRequest=0x09 (UR_SET_REPORT) ... timo=1000 ms ... discarded, ucycom.c:440-443'; extract.log shows the verbatim sed-extraction from the md5-gated guest tree; fix_compile.log shows the patched production ucycom.ko building -Werror-clean RC=0; VERDICT.md tables the chain shape with ucycom.c/usb_serial.c line cites and records the pass-2 negative results (RX/TX framing bounded, sc_temp_cfg zeroed via subr_bus.c:1953, attach-error detach safe via usb_serial.c:371-372)

PoC changes

No seed PoC existed; authored from scratch on the DF-5425 harness pattern at DF-5488 verbatim-extraction depth (driver cfg chain + usb_serial.c task wrappers sed-extracted md5-gated from the guest's own /usr/src; only usbd_do_request_proc is an evil-device model). Build fixups: kernel has no sys/termios.h (use usb_serial.h's tty.h), drop non-extracted static prototypes (-Wunused-function), forward-declare uc_sc for the sink, add usbhid.h; one extra phase (S: slow-answer-success) added to prove abort-on-error is structurally useless for single-request chains.

Verified recommended fix

Bound the cfg-path control timeout: UCYCOM_CFG_TIMEOUT 250 for the ucom_cfg_do_request in ucycom_cfg_write (ucycom.c:438); full diff in fix.diff

Verdict

Mechanism-level reproduction with production code verbatim: ucycom_cfg_write() (ucycom.c:412-444) issues one synchronous 1000 ms-timeout EP0 SET_REPORT(feature) per cfg callback (ucycom.c:438) and discards its usb_error_t ('failed ... (ignored)', ucycom.c:440-443). ucycom_cfg_open (ucycom.c:299-306) fires it on every first open(2) and ucycom_cfg_param (ucycom.c:474-507) on every tcsetattr(2) of an open port; the ucom core adds hz/10 per task (usb_serial.c:777,1636), so an evil DeLorme Earthmate (the only VID:PID, ucycom.c:177-179) that stays silent on the control data/status stage costs ~1.1 s of per-adapter ucom-taskqueue occupancy per task. close(2) is synchronous and uninterruptible behind that queue (ucom_close -> ucom_queue_command(ucom_cfg_close) -> usb_proc_mwait, usb_serial.c:671-672+955-957; usb_process.c:385-393), and unplug pins the USB hub event thread via ucom_detach -> usb_proc_drain (usb_serial.c:394). A harness built from the VERBATIM driver cfg chain and the VERBATIM usb_serial.c task wrappers (sed-extracted at build time from the guest's md5-gated /usr/src), running on the real usb_proc_create/msignal/mwait machinery, measured the closer blocked 2094/2111 ms behind first-open+1-tcsetattr (2 burned EP0 requests) and 1987/1989 ms when the evil device answers at 950 ms so every request SUCCEEDS -- the ucycom-specific point: with single-request chains the family's abort-on-error mitigation is a no-op even when requests fail and covers nothing when they succeed, so the timeout itself is the only in-file lever. A rapid tcsetattr burst coalesces into the sc_param_task double buffer (2106/2097 ms for 2 tcsetattrs) but a sustained loop keeps ~1.1 s/task of uninterruptible occupancy going for as long as the attacker loops. fix.diff (UCYCOM_CFG_TIMEOUT 250) is git-apply-clean on the audited repo, compiled the patched production ucycom.c -Werror-clean as the real ucycom.ko on the guest, and its modeled phase cut the closer block to 638-661 ms. No Cypress device model exists on the guest, hence the KLD-harness evidence class; availability-only impact, Low per family calibration (DF-5367/5383/5421/5424/5425/5449/5456/5476/5496).