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

umct cfg callbacks chain sequential synchronous 1000 ms-timeout EP0 vendor requests with discarded errors: an evil MCT/Belkin/Sitecom adapter stretches the first-open cycle to ~4.0 s and every tcsetattr (any rate, incl. B0 β€” no branch) to ~2.1 s of uninterruptible ucom-taskqueue occupancy, blocking close(2) SIGKILL-immune behind the queue (measured 5.95 s with one queued tcsetattr, 8.05 s with a 5-burst; a sustained loop extends without bound) and stalling the USB hub event thread on unplug (user-amplifiable)

Summary

Every cfg step is a sync 1000ms EP0 write with the error discarded: first open = 3 requests, every tcsetattr = 2 + pause (no B0 branch - umct_pre_param accepts everything), close +1 - and cv_wait drops sc_lock so a sustained unpriv tcsetattr loop extends the SIGKILL-immune close block without bound (burst ceiling 2 queued per the msignal double buffer). Harness: closer blocked 3885/5952/8049ms; abort-on-error 3033ms, abort+250ms 834ms; 8/8-hunk fix compiles the patched production umct.ko. Zero control-IN reads (uninit class structurally absent) and correct actlen gate - memory-clean otherwise.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5496 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 8.2 KB ↓ raw
um5496stall_harness.c β€” 16.9 KB view raw
Makefile β€” 103 B ↓ download
build.sh β€” 144 B view raw
run.sh β€” 203 B view raw
build.log β€” 7.1 KB view raw
run.log β€” 9.8 KB view raw
fix_compile.log β€” 7.3 KB view raw
env.txt β€” 254 B view raw
fix.diff β€” 2.5 KB view raw
manifest.json β€” 3.2 KB view raw
verdict.json β€” 9.0 KB view raw

DF-5496 β€” umct(4) cfg-chain synchronous-EP0 stall (ucom family)

File: sys/bus/u4b/serial/umct.c (Magic Control Technology USB-RS232, Belkin F5U109/F5U409, Sitecom; ucom core) Class: cfg-stall family (cf. DF-5476 ubsa / DF-5421 uchcom / DF-5424 uftdi / DF-5425 uslcom / DF-5449 uvscom / DF-5456 umoscom / DF-5487 uvisor) Evidence class: KLD-harness-with-production-code (no MCT USB device model exists on the QEMU guest; the harness drives the real in-kernel usb_process taskqueue machinery that ucom's cfg callbacks, synchronous close and detach drain run on, with umct's exact production request counts and the production 1000 ms timeout).

Build (on the guest)

sh build.sh     # cd /usr/src/sys/bus/u4b/serial/um5496 && make
                # artifact: umct5496stall.ko  (-Werror clean)

Run (on the guest)

sh run.sh       # dmesg -c; kldload /root/df5496/umct5496stall.ko (~23 s,
                # all phases at MOD_LOAD); kldunload; dmesg | grep UM5496

Expected decisive output (measured 2026-10-06)

UM5496[A]: closer: unblocked  ELAPSED=3885 ms   (first open: 3-req
            line-state task + 1-req HUPCL DTR drop; first-open param
            bails !LL_READY)
UM5496[2]: closer: unblocked  ELAPSED=5952 ms   (+ 1 tcsetattr: 2 reqs
            + hz/10 pause)
UM5496[3]: closer: unblocked  ELAPSED=8049 ms   (+ burst of 5 tcsetattr
            coalesced to 2 param executions β€” production msignal
            double-buffer ceiling; a paced/sustained loop extends
            without bound until the abuser stops)
UM5496[B]: closer: unblocked  ELAPSED=3033 ms   (fix#1: abort on error)
UM5496[C]: closer: unblocked  ELAPSED=834 ms    (fix#1+#2: abort+250 ms)

Every simulated request sleeps its full ~980–1000 ms (absolute timestamps in run.log), i.e. each is the real in-kernel cost of an unanswered EP0 data/status stage.

Fix validation

fix.diff (8 hunks): error-returning umct_cfg_do_request(), chain aborts in umct_cfg_set_{break,dtr,rts}/umct_cfg_param, cfg-path timeout bounded 1000 β†’ 250 ms (UMCT_CFG_TIMEOUT). Applies 8/8 clean (git apply --check + guest patch -p1) and the patched production umct.ko compiles with zero compiler diagnostics (fix_compile.log; the only "error" string in the log is the literal -Werror flag).

Artifacts

  • um5496stall_harness.c β€” the rig (self-contained KLD)
  • Makefile, build.sh, run.sh β€” exact build/run
  • build.log / run.log / fix_compile.log β€” full untrimmed logs
  • env.txt β€” guest uname/compiler
  • fix.diff β€” git-apply-able unified diff (never applied to sys/)
  • VERDICT.md β€” full narrative; manifest.json + verdict.json
VERDICT.md
↓ download raw

DF-5496 VERDICT β€” umct cfg-chain synchronous-EP0 stall

Status: REPRODUCED (KLD-harness-with-production-code class; no MCT/Belkin/Sitecom USB serial device model exists on the QEMU guest). Impact: dos (local, evil-USB precondition; uninterruptible occupancy of the per-adapter ucom taskqueue; blocks close(2) SIGKILL-immune and the USB hub event thread on unplug). Confidence: certain (every simulated request is the real in-kernel cost of an unanswered EP0 data/status stage, on the real usb_process taskqueue, with production request counts and queueing order).

Root cause (path:line)

  • Every umct cfg step is a synchronous EP0 vendor write via ucom_cfg_do_request (umct.c:332-333 β†’ usb_serial.h:220-221 β†’ usbd_do_request_proc) with a 1000 ms timeout (umct.c:333). The usb_error_t is discarded at the only site there is (umct.c:334-337, "(ignored)"), so no chain ever aborts on failure.
  • An evil adapter that never answers the data/status stage makes each request eat its full timeout (usb_request.c:561 max_ticks, :643 "WXFER" sleep, :700-704 USB_ERR_TIMEOUT); an adapter answering at just-under-timeout makes every request succeed β€” identical occupancy, no error to propagate.
  • Chains run on the per-adapter ucom taskqueue (sc_super_ucom.sc_tq, usb_serial.c:322). close(2) is synchronous against it (ucom_close β†’ ucom_shutdown (HUPCL, one more request) β†’ ucom_queue_command(ucom_cfg_close) β†’ usb_proc_mwait, usb_serial.c:671-672 + 953-957; uninterruptible cv_wait, usb_process.c:364-393 β€” a plain cv_wait, no signal delivery, that drops sc_lock while asleep, so a concurrently-queueing tcsetattr loop extends the wait until it stops). Unplug stalls the USB hub event thread the same way (ucom_detach β†’ usb_proc_drain, usb_serial.c:394).

Production chain accounting (pass-2 precision)

The core queues, on first open (usb_serial.c:842 param, :870 start, :865 cfg_open, :870 start, :874-878 line-state, :880 status): the first param task runs before the cfg_open task and bails at usb_serial.c:1626-1628 (!UCOM_FLAG_LL_READY) β€” the first-open param is a no-op for every ucom driver. umct registers no ucom_cfg_open (umct.c:175-187), so the core's hz/10 open pause (usb_serial.c:777) does not run either, and umct_cfg_get_status reads cached bytes (umct.c:386-387; kept fresh by umct_intr_callback_sub, umct.c:360-361 β€” no EP0). What issues EP0 on first open is the single coalesced line-state task (usb_serial.c:1357-1420): DTR=1 + RTS=1 + BREAK=0 β†’ 3 Γ— 1000 ms (umct.c:413 set_dtr SET_MCR, :426 set_rts SET_MCR, :400 set_break(0) SET_LCR). Close adds the HUPCL DTR drop (usb_serial.c:694-696 β†’ umct.c:413): +1 Γ— 1000 ms.

Every later tcsetattr (param task double-buffered, usb_serial.c:1691-1693, LL_READY now set):

  • any rate β€” there is no B0 branch in umct_cfg_param (umct.c:465-504); umct_pre_param accepts everything (umct.c:459-463) and the same two registers are programmed for B0 as for any speed: SET_BAUD (umct.c:472) + SET_LCR (umct.c:503) = 2 Γ— 1000 ms + hz/10 core pause (usb_serial.c:1636) β‰ˆ 2.1 s per tcsetattr;
  • TIOCMSET (DTR+RTS) β†’ 2 Γ— 1000 ms; TIOCSBRK/TIOCCBRK β†’ 1 Γ— 1000 ms.

Amplification: the msignal double buffer COALESCES a rapid tcsetattr burst to at most 2 queued instances (usb_process.c:259-331, t==3 branch) β€” a burst of 5 measured 2 full param executions (phase A3) β€” but a paced or sustained tcsetattr loop keeps the taskqueue busy ~2.1 s per instance for as long as the abuser keeps queueing, and because usb_proc_mwait's cv_wait drops sc_lock while asleep, the closer cannot lock them out: close(2) blocks SIGKILL-immune until the abuser stops plus up to 2 Γ— 2.1 s residual + 1.0 s HUPCL. Measured with the closer entering ~150 ms into the chain: first open + close 3885 ms; +1 tcsetattr 5952 ms; +5-burst tcsetattr 8049 ms. Unplug mid-cycle pins the USB hub event thread identically (usb_serial.c:394) per cycle.

Harness and why it is faithful

um5496stall_harness.c replicates, on the real usb_process machinery: the exact first-open queue order including the LL_READY gate; the 3/2/1-request production chains (line-state / tcsetattr / HUPCL-close); the core's hz/10 param pause; the absent core open-pause (no callback); the synchronous closer (HUPCL line task queued via msignal, then usb_proc_msignal+usb_proc_mwait, entered ~150 ms into the chain like a real close(2) after open); per-request cost = lksleep("WXFER", USB_MS_TO_TICKS(timeout)) on a unique per-request wait channel (production sleeps on the unique xfer pointer, usb_request.c:643 β€” the DF-5476 harness v1 lesson carried here from day one); and the production double-buffer coalescing semantics (phase A3 burst of 5 β†’ 2 param executions, instrumented).

Fix validation

  • fix.diff (8 hunks): umct_cfg_do_request() returns usb_error_t; umct_cfg_set_break/set_dtr/set_rts abort on failure; umct_cfg_param aborts the chain on the first failed SET_BAUD; cfg-path timeout bounded UMCT_CFG_TIMEOUT = 250 ms (umct.c:333 was 1000).
  • Mechanism-level validation per family standard: harness phase B (abort-on-first-error) cuts the measured closer block from 5952 ms to 3033 ms and phase C (abort + 250 ms bound) to 834 ms (run.log); the 8-hunk fix.diff applies 8/8 clean (git apply --check on the audit tree; patch -p1 on the guest) and compiles the patched production umct.ko with zero compiler diagnostics on the guest (fix_compile.log, -Werror; the only "error" string in the log is the literal -Werror flag). A full patched-kernel boot is not required to observe a per-request timeout bound + early-abort behavior the harness exercises directly.

Honesty notes

  • First fix.diff draft used != USB_ERR_OK, which does not exist in this tree's usb.h (success is implicitly 0) β€” the patched module failed to compile; corrected to != 0 (family precedent, DF-5476) and rebuilt clean. Attempt count includes this.
  • No MCT hardware exists on the guest; the device's side of each request (never answering the data/status stage) is simulated by the documented in-kernel cost of exactly that condition (the WXFER timeout sleep), not by a USB device model.

Negative results this pass (why umct is otherwise clean)

  • No uninit-return class: umct issues ZERO control-IN reads anywhere β€” UMCT_GET_MSR/UMCT_GET_LCR (umct.c:72-75) are dead constants; status is served from intr-cached sc_lsr/sc_msr (umct.c:381-388). The DF-1062/2342/5448/5455 class cannot exist.
  • Correct intr length gate: umct_intr_callback_sub checks actlen < 2 before copying the 2-byte status payload (umct.c:353-358) β€” the ulpt DF-5438 class is absent. Read path passes actlen (device-controlled but bounded by the frame length) to ucom_put_data (umct.c:605-610); write path bounded by the core-computed sc_obufsize (umct.c:281, SITECOM clamp :283-287).
  • No sync EP0 from the USB callback thread (umcs DF-5382 class absent): all three xfer callbacks only copy data and resubmit.
  • Attach/detach error paths safe: both umct_attach failure branches (umct.c:258-262 transfer-setup, :290-292 ucom_attach) goto umct_detach; ucom_detach no-ops unless UCOM_FLAG_ATTACHED (usb_serial.c:371-372) and ucom_attach self-cleans partial failure (usb_serial.c:344-348); usbd_transfer_unsetup on NULL xfers is a no-op. No uether DF-5334 goto-detach panic class.
  • Endpoint-swap heuristic memory-safe (umct.c:269-279): swaps whole xfer pointers whose DMA buffers stay sized to their own endpoints; dispatch guarded by sc_swap_cb (umct.c:547-566); written once at attach before ucom_attach (no race window).
  • umct_calc_baud/pre_param: unsupported rates (B50..B200, B230400+, arbitrary speeds) silently program divisor 0 β€” a functional wrong-baud issue (Linux mct_u232 maps divisor 0 to 2400 baud), upstream-inherited, no security impact (not 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

Mechanism-level validation per family standard: harness phase B (abort-on-first-error) cuts the measured closer block from 5952 ms to 3033 ms and phase C (abort + 250 ms bound) to 834 ms; the 8-hunk fix.diff (error-returning umct_cfg_do_request, chain aborts in set_break/set_dtr/set_rts/cfg_param, bounded UMCT_CFG_TIMEOUT) applies 8/8 clean (git apply --check on the audit tree, patch -p1 on the guest) and compiles the patched production umct.ko with zero compiler warnings on the guest (fix_compile.log). A full patched-kernel boot is not required to observe a per-request timeout bound + early-abort behavior the harness exercises directly.

fix.diff (sha256 b54552212a69cf7decff27f5f8506a0407726ae5bd166a3d9b110e2a00b92f11); fix_compile.log (patched production umct.ko, zero compiler diagnostics; the only 'error' string is the literal -Werror flag); run.log phases B/C ELAPSED lines (3033 ms / 834 ms vs baseline 5952 ms).
↓ fix.diffper-fix-DF-5496

Confirmed kernel references

Detail

Exploit chain

evil adapter (MCT USB232 0711:0210, Sitecom USB232 6189:2068, D-Link DU-H3SP, Belkin F5U109/F5U409 050d:010a/0129) inserted; it never answers EP0 vendor data/status stages (or answers at just-under-timeout, making every request succeed with no error anywhere); first open(2) of /dev/ttyUx occupies the adapter's ucom taskqueue ~3.0 s uninterruptibly (+1.0 s HUPCL at close); every tcsetattr adds ~2.1 s while a session holds the tty open (user-amplifiable: sustained loop keeps the tq busy 2.1 s per instance indefinitely; burst ceiling 2 queued instances); close(2) blocks SIGKILL-immune behind the queue (measured 5.95 s / 8.05 s); pulling the device mid-cycle stalls the USB hub event thread up to the same duration per cycle.

Evidence (decisive lines)

run.log ELAPSED lines: UM5496[A]=3885 ms, [2]=5952 ms, [3]=8049 ms (5-burst), [B]=3033 ms (fix abort), [C]=834 ms (fix abort+250ms), plus absolute per-request START timestamps showing every request sleeping its full ~980 ms (e.g. phase A: 57/1038/2028/3049/4058 ms) and the phase-A3 coalescing instrumentation (burst of 5 -> 2 param-task executions); build.log (-Werror-clean in-tree kmod build); fix_compile.log (8/8 hunks, patched production umct.ko, zero compiler diagnostics -- only the literal -Werror flag matches 'error').

PoC changes

Seed concept executed as the DF-5476 ubsa family rig adapted to umct's exact production shapes: 3-request first-open line-state chain, 2-request tcsetattr chain (identical for B0 -- umct has no B0 branch, unlike ubsa's 7-request B0 variant), 1-request HUPCL close drop modeled as a dedicated tq task queued by the closer (production ucom_shutdown ordering), 0-request open/status/close tasks, the production LL_READY gate that makes the first-open param task bail (usb_serial.c:1626-1628), absent core open-pause (no callback), production 1000 ms timeout (umct.c:333), real usb_proc machinery, closer mirroring ucom_close's synchronous usb_proc_mwait entered ~150 ms into the chain, and the DF-5476 v2 fidelity lesson applied from the start (unique per-request wait channels). New this instance: an amplification phase (5-burst) instrumenting the production msignal double-buffer coalescing ceiling (2 executions), and the first fix.diff draft used != USB_ERR_OK which does not exist in this tree's usb.h -- caught by the on-guest compile (build_failed -> corrected to != 0 per family precedent, rebuilt clean).

Verified recommended fix

Bound the cfg-path control timeout (UMCT_CFG_TIMEOUT 250 ms) and abort the umct_cfg_param()/set_* chains on the first failed request via an error-returning umct_cfg_do_request() (full diff in fix.diff).

Verdict

REPRODUCED on the guest (KLD-harness-with-production-code class; no MCT USB232/Sitecom/Belkin F5U109/F5U409 device model exists): every umct cfg-callback step is a synchronous EP0 vendor write via ucom_cfg_do_request with a 1000 ms timeout (umct.c:332-333) whose usb_error_t is discarded at the only site (umct.c:334-337, '(ignored)'), so no chain aborts on device failure. Pass-2 accounting precision: the first-open cfg_param task is queued before ucom_cfg_open (usb_serial.c:842 vs :865) and bails at :1626-1628 (!LL_READY) -- first-open param is a no-op for every ucom driver -- and umct registers no ucom_cfg_open (:777 pause skipped), so first open issues only the coalesced line-state chain DTR+RTS+BREAK-off = 3 x 1000 ms (umct.c:413,:426,:400), plus the HUPCL DTR drop at close (usb_serial.c:694-696 -> umct.c:413, 1 x 1000 ms); measured closer block 3885 ms. Every later tcsetattr re-queues the param task (double-buffered, usb_serial.c:1691-1693): SET_BAUD(umct.c:472) + SET_LCR(umct.c:503) + hz/10 core pause (:1636) = ~2.1 s -- IDENTICAL for B0 and any rate (no B0 branch in umct_cfg_param; umct_pre_param accepts everything, umct.c:459-463); measured closer block 5952 ms (one tcsetattr) and 8049 ms (burst of 5, coalesced to 2 full param executions by the msignal double buffer, usb_process.c:259-331 -- a paced/sustained loop extends the block without bound because usb_proc_mwait's uninterruptible cv_wait drops sc_lock while asleep, usb_process.c:364-393) -- uninterruptible per usb_serial.c:671-672+953-957; umct registers no ucom_cfg_close so the cfg_close task itself is a 0-request no-op. Unplug mid-cycle pins the USB hub event thread identically via ucom_detach->usb_proc_drain (usb_serial.c:394). Each simulated request is the real in-kernel cost of an unanswered EP0 data/status stage (lksleep WXFER 1000 ms, usb_request.c:643/700-704) on the real usb_process taskqueue, per-request unique wait channel. Fix mechanisms validated at runtime: abort-on-first-error 3033 ms (B), abort+250 ms bound 834 ms (C); patched production umct.ko compiles 8/8 hunks clean.