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

uark cfg-chain synchronous-EP0 stall: uark_cfg_param chains six 1000 ms uninterruptible control writes whose errors are discarded β€” a silent/stalling ARK3116 (or unprivileged tcsetattr loop against one) parks the shared ucom taskqueue ~6.1 s per tcsetattr, blocking SIGKILL-immune close(2) behind it and stalling the USB hub event thread on unplug/detach drain (cfg-stall family, DF-5532 occupancy delta)

Summary

Every cfg step is a synchronous 1000ms EP0 vendor write whose usb_error_t is discarded at the only site, so no chain aborts: uark_cfg_param chains SIX writes per tcsetattr (~6.1s occupancy - 3x umct and the heaviest single-rate chain short of umcs' 28-request open), first open 1s, break toggles 1s each. close(2) waits uninterruptibly behind the queue (measured 7.0s after one tcsetattr, 13.0s after a 5-burst - coalesced to 2 executions); unplug stalls the hub event thread via usb_proc_drain. Production-machinery harness (real usb_process taskqueue, exact request counts/timeouts): 921/6995/13012ms phases; abort-on-error fix -> 2029ms, abort+250ms -> 561ms; fix.diff applies 6/6, patched uark.ko compiles -Werror clean. Divisor math (3000000+speed/2)/speed over [300,115200] fits uint16; zero control-INs anywhere so the uninit-return class is structurally absent.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5540 Β· 14 files
FileTypeDescriptionSize
uark5540stall_harness.c β€” 17.6 KB view raw
Makefile β€” 105 B ↓ download
build.sh β€” 159 B view raw
run.sh β€” 190 B view raw
build.log β€” 7.1 KB view raw
run.log β€” 9.8 KB view raw
run.2.log β€” 269 B view raw
fix_compile.log β€” 7.0 KB view raw
env.txt β€” 262 B view raw
fix.diff β€” 2.0 KB view raw
README.md β€” 3.0 KB ↓ raw
VERDICT.md β€” 8.3 KB ↓ raw
manifest.json β€” 2.7 KB view raw
verdict.json β€” 9.9 KB view raw

DF-5540 β€” uark(4) cfg-chain synchronous-EP0 stall (ucom family)

File: sys/bus/u4b/serial/uark.c (Arkmicro ARK3116 USB-RS232; ucom core) Class: cfg-stall family (cf. DF-5367 ufoma / DF-5383 umcs / DF-5407 uplcom / DF-5421 uchcom / DF-5424 uftdi / DF-5425 uslcom / DF-5449 uvscom / DF-5456 umoscom / DF-5476 ubsa / DF-5487 uvisor / DF-5496 umct / DF-5504 ucycom; root occupancy-gating mechanism DF-5532) Evidence class: KLD-harness-with-production-code (no Arkmicro ARK3116 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 uark'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/ua5540 && make
                # artifact: uark5540stall.ko  (-Werror clean)

Run (on the guest)

sh run.sh       # dmesg -c; kldload /root/df5540/uark5540stall.ko (~25 s,
                # all phases at MOD_LOAD); kldunload; dmesg | grep UA5540

Expected decisive output (measured 2026-10-07)

UA5540[A]: closer: unblocked  ELAPSED=921 ms   (first open: 1-request
            break-off line-state task -- ONLY set_break is registered
            among uark's line callbacks; param bails !LL_READY)
UA5540[2]: closer: unblocked  ELAPSED=6995 ms  (+ 1 tcsetattr: 6 reqs
            + hz/10 pause = 6.1 s -- heaviest single-rate chain in the
            audited family short of umcs' 28-request open/close)
UA5540[3]: closer: unblocked  ELAPSED=13012 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)
UA5540[B]: closer: unblocked  ELAPSED=2029 ms  (fix#1: abort on error)
UA5540[C]: closer: unblocked  ELAPSED=561 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. Stability re-run (run.2.log): 854 / 6950 / 12963 / 2034 / 564 ms.

Fix validation

fix.diff (6 hunks): error-returning uark_cfg_write(), chain aborts in uark_cfg_param() (all 6 steps checked), bounded cfg-path timeout 1000 β†’ 250 ms (UARK_CFG_TIMEOUT). Applies 6/6 clean (git apply --check on the audit tree + guest patch -p1) and the patched production uark.ko compiles with zero compiler diagnostics (fix_compile.log; the only "error" string in build logs is the literal -Werror flag).

Artifacts

  • uark5540stall_harness.c β€” the rig (self-contained KLD)
  • Makefile, build.sh, run.sh β€” exact build/run
  • build.log / run.log / run.2.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-5540 VERDICT β€” uark(4) cfg-chain synchronous-EP0 stall

Status: REPRODUCED (impact: dos; confidence: certain) Evidence class: KLD-harness-with-production-code on the QEMU guest (DragonFly 6.5-DEVELOPMENT, env.txt), family standard β€” no Arkmicro ARK3116 (6547:0232) device model exists in the rig.

Root cause (production code, line-accurate)

Every uark cfg callback step is a synchronous EP0 vendor control write (UARK_WRITE 0x40 / UARK_REQUEST 0xfe / wLength 0) issued through ucom_cfg_do_request() (uark.c:429-430 β†’ usb_serial.h:220-221 β†’ usbd_do_request_proc, usb_request.c:759-790) with a 1000 ms timeout, and the usb_error_t result is discarded at the only site there is (uark.c:431-434, "(ignored)"). No chain step ever aborts on device failure. A silent evil adapter makes each request eat its full 1000 ms timeout (usb_request.c:561 max_ticks, :643 "WXFER" sleep, :700-704 USB_ERR_TIMEOUT); a slow-but-answer-at-999 ms adapter makes every request succeed. All of this executes on the per-adapter ucom usb_process taskqueue (sc_tq, usb_serial.c:322) β€” the same queue whose synchronous usb_proc_mwait() close (usb_serial.c:671-672) and usb_proc_drain() detach (usb_serial.c:394) wait uninterruptibly (usb_process.c:364-393), and whose empty-queue-gated wakeup is the DF-5532 root defect.

Pass-2 production chain accounting (all verified against source)

  • First open(2) of /dev/ttyUx (ucom_open, usb_serial.c:801-903): param queued at :842 runs before cfg_open (:865) and bails (!LL_READY, :1626-1628) β€” 0 requests. uark registers no ucom_cfg_open (uark.c:138-148) β†’ cfg_open sets LL_READY with no hz/10 pause (:771-778). The coalesced first-open line-state task (:874-878 β†’ :1357-1420) computes its callback mask from what is registered (:1374-1381): uark registers only ucom_cfg_set_break among the line callbacks (uark.c:140; no set_dtr/set_rts/set_ring), so DTR+RTS+BREAK-off collapses to 1 Γ— 1000 ms (uark_cfg_set_break(sc,0) β†’ uark_cfg_write(sc,4,0x00), uark.c:414). Status task: cached bytes only β†’ 0 requests. β‡’ first open β‰ˆ 1.0 s taskqueue occupancy.
  • Every later tcsetattr (param task, LL_READY set; double-buffered usb_serial.c:1691-1693): uark_cfg_param (uark.c:348-396) chains six synchronous writes β€” uark.c:361 (reg3=0x83), :362 (reg0=div LSB), :363 (reg1=div MSB), :364 (reg3=0x03), :394 (reg3=0x00), :395 (reg3=LCR) β€” plus the core hz/10 pause (:1636) β‰ˆ 6.1 s per tcsetattr at ANY accepted rate (300..115200, uark.c:343; B0/0 and

    115200 are rejected in pre_param β€” there is no B0 variant). This is 3Γ— umct's per-tcsetattr cost (DF-5496) and the heaviest single-rate chain in the audited family short of umcs' 28-request open/close pair (DF-5383).

  • TIOCSBRK/TIOCCBRK β†’ line-state task β†’ uark_cfg_set_break (uark.c:407-415) β†’ 1 Γ— 1000 ms per toggle.
  • close(2): synchronous β€” ucom_close() β†’ ucom_shutdown() HUPCL DTR drop (usb_serial.c:694-696) is masked out (set_dtr NULL, :1374-1375) β†’ 0 requests of the closer's own; then cfg_close (no callback β†’ 0 requests) + usb_proc_mwait() uninterruptibly behind whatever is queued. Unplug mid-cycle stalls the USB hub event thread identically via ucom_detach β†’ usb_proc_drain (usb_serial.c:394).

What the harness measured (real usb_proc machinery, real timeouts)

Closer thread enters ~100–150 ms into the chain and blocks in the production usb_proc_mwait():

Phase Configuration Measured (run.log) Re-run (run.2.log)
A first open + close 921 ms 854 ms
A2 open + 1 tcsetattr + close 6995 ms 6950 ms
A3 open + 5-tcsetattr burst + close 13012 ms 12963 ms
B fix#1 abort-on-error 2029 ms 2034 ms
C fix#1+#2 abort + 250 ms 561 ms 564 ms

Every simulated request slept its full ~980–1000 ms (absolute START timestamps in run.log, e.g. phase A2: 1088/2070/3061/4061/5061/6050 ms). The A3 instrumentation shows the production usb_proc_msignal t==3 double-buffer coalescing ceiling (burst of 5 β†’ 2 param executions, usb_process.c:259-331); a paced or sustained tcsetattr loop keeps the taskqueue busy ~6.1 s per instance for as long as the abuser keeps queueing, and usb_proc_mwait's uninterruptible cv_wait drops sc_lock while asleep β€” the block extends without bound until the abuser stops.

Threat model / impact ceiling

  • Evil ARK3116-class adapter (or any uark device whose EP0 goes silent mid-operation): pins the per-adapter ucom cfg taskqueue 6.1 s per tcsetattr, ~1.0 s per open; the same user's close(2) is SIGKILL-immune-blocked behind it; unplug stalls the shared USB hub event thread up to the same duration per cycle (usb_serial.c:394).
  • Local unprivileged user holding the tty open can amplify: sustained tcsetattr loop keeps the queue occupied indefinitely; other opens/closes/ioctls of the same tty (single-port device) queue behind.
  • DoS only β€” no memory corruption, no info leak (see negatives).

Family precedent severity: Low (evil-USB precondition or seconds-scale local stall; cf. DF-5421/5424/5425/5449/5456/5476/5487/5496/5504).

Fix validation (family standard)

fix.diff (6 hunks): uark_cfg_write() returns usb_error_t with UARK_CFG_TIMEOUT (250 ms), uark_cfg_param() aborts the 6-write chain on the first failed step (5 of 6 steps checked; the final LCR write's return is irrelevant to chain length). Mechanism-level validation at runtime by harness phases B (2029 ms) and C (561 ms) vs baseline 6995 ms; the diff applies 6/6 clean (git apply --check on the audit tree; patch -p1 on the guest) and the patched production uark.ko compiles with zero compiler diagnostics (fix_compile.log). Guest /usr/src uark.c reverted after the compile; guest reset (with-src) performed.

Negative results this pass (pass-2 depth, classes hunted and killed)

  • No control-IN reads anywhere: uark_cfg_write is the only request helper and is write-only (uark.c:423-427, wLength 0, NULL data) β€” the DF-1062/2342/5412/5438/5448/5455/5489 short-control-IN uninit class is structurally absent.
  • Divisor math safe: data = (3000000 + speed/2)/speed with speed ∈ [300,115200] (uark.c:343,359) β†’ divisor ∈ [26,10000], fits uint16; LSB/MSB split (uark.c:362-363) matches the ARK3116 protocol (OpenBSD heritage). No B0 branch needed β€” B0 is rejected in pre_param (no uvscom DF-5447 inversion class).
  • Bulk endpoint buffer math bounded: write path sends only the actlen returned by ucom_get_data(..., UARK_BUF_SIZE, &actlen) (uark.c:260-263) ≀ bufsize 1024 (uark.c:56,123); read path sets frame len to usbd_xfer_max_len and ucom_put_data clamps (uark.c:290-294). uint32_t actlen vs int * prototype (uark.c:253, usbdi.h:542) is a benign same-width pun, value ≀ 1024.
  • Status bytes never written: sc_msr/sc_lsr (uark.c:91-92) are read (uark.c:403-404) but written by nothing β€” constant zero modem status; functional only (softc is zeroed β€” no uninit leak; no intr-endpoint short-packet consumption Γ  la ulpt DF-5438).
  • Attach/detach protocol safe: attach error paths fall into uark_detach β†’ ucom_detach no-ops unless ATTACHED (usb_serial.c: 371-372; ucom_attach self-cleans :344-348); teardown order ucom_detach β†’ usbd_transfer_unsetup β†’ lockuninit (uark.c:241-243) is the family-correct sequence. The DF-5320 unplug-UAF is a core usb_serial.c lifetime defect (already filed) β€” uark adds no per-driver delta. DF-5394 attach-race is a core usb_bus defect, N/A per-driver.
  • No synchronous EP0 from the bulk callback threads (umcs DF-5382 class absent); error re-submission only clears the device's own pipe stall (uark.c:267-272, 298-303).
  • open/close/poll methods: uark registers none beyond the family standard; uark_poll β†’ usbd_transfer_poll(sc->sc_xfer, UARK_N_TRANSFER) (uark.c:437-442) is the canonical form.

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 6995 ms to 2029 ms and phase C (abort + 250 ms bound) to 561 ms; the 6-hunk fix.diff (error-returning uark_cfg_write with UARK_CFG_TIMEOUT, chain aborts across the 6-write uark_cfg_param chain) applies 6/6 clean (git apply --check on the audit tree, patch -p1 on the guest) and compiles the patched production uark.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 b3aa7a97591b5fedc3de1fa5dc982c96ff00a9de4d252425bf00fb206caa143e); fix_compile.log (patched production uark.ko, zero compiler diagnostics; the only 'error' string is the literal -Werror flag); run.log phases B/C ELAPSED lines (2029 ms / 561 ms vs baseline 6995 ms).
↓ fix.diffper-fix-DF-5540

Confirmed kernel references

Detail

Exploit chain

evil adapter (Arkmicro ARK3116 6547:0232) inserted, or any uark device whose EP0 goes silent mid-operation; 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 ~1.0 s uninterruptibly; every tcsetattr adds ~6.1 s while a session holds the tty open (user-amplifiable: sustained loop keeps the tq busy 6.1 s per instance indefinitely; burst ceiling 2 queued instances); TIOCSBRK/TIOCCBRK add 1.0 s per toggle; close(2) blocks SIGKILL-immune behind the queue (measured 6.995 s / 13.012 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: UA5540[A]=921 ms, [2]=6995 ms, [3]=13012 ms (5-burst), [B]=2029 ms (fix abort), [C]=561 ms (fix abort+250ms), plus absolute per-request START timestamps showing every request sleeping its full ~980-1000 ms (e.g. phase A2: 1088/2070/3061/4061/5061/6050 ms) and the phase-A3 coalescing instrumentation (burst of 5 -> 2 param-task executions); run.2.log stability re-run (854/6950/12963/2034/564 ms); build.log (-Werror-clean in-tree kmod build); fix_compile.log (6/6 hunks, patched production uark.ko, zero compiler diagnostics -- only the literal -Werror flag matches 'error').

PoC changes

Seed concept executed as the DF-5496 umct family rig adapted to uark's exact production shapes: 1-request first-open line-state chain (only set_break registered among the line callbacks -- the mask at usb_serial.c:1374-1381 drops DTR/RTS/ring), 6-request tcsetattr chain at ANY accepted rate (B0 rejected in pre_param -- no umct-style no-B0-branch note needed beyond identity across rates), 0-request HUPCL close (set_dtr NULL -> masked, unlike umct's 1-request drop), 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 (uark.c:429-430), real usb_proc machinery, closer mirroring ucom_close's synchronous usb_proc_mwait entered ~100-150 ms into the chain, and the DF-5476 v2 fidelity lesson applied from the start (unique per-request wait channels). One build iteration: v1 had a parenthesized-nested-ternary typo that failed the on-guest -Werror build (build_failed) -- replaced with a static request-name table, rebuilt clean.

Verified recommended fix

Bound the cfg-path control timeout (UARK_CFG_TIMEOUT 250 ms) and abort the uark_cfg_param() 6-write chain on the first failed request via an error-returning uark_cfg_write() (full diff in fix.diff).

Verdict

REPRODUCED on the guest (KLD-harness-with-production-code class; no Arkmicro ARK3116 USB device model exists): every uark cfg-callback step is a synchronous EP0 vendor write via ucom_cfg_do_request with a 1000 ms timeout (uark.c:429-430) whose usb_error_t is discarded at the only site (uark.c:431-434, '(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 uark registers no ucom_cfg_open (:771-778 pause skipped), so first open issues only the coalesced line-state chain whose callback mask contains ONLY break (uark.c:140 registers set_break; set_dtr/set_rts/set_ring NULL, usb_serial.c:1374-1381) = 1 x 1000 ms (uark.c:414); measured closer block 921 ms. Every later tcsetattr re-queues the param task (double-buffered, usb_serial.c:1691-1693): uark_cfg_param chains SIX writes (uark.c:361 reg3=0x83, :362 div LSB, :363 div MSB, :364 reg3=0x03, :394 reg3=0x00, :395 reg3=LCR) + hz/10 core pause (:1636) = ~6.1 s -- IDENTICAL for every accepted rate 300..115200 (uark_pre_param uark.c:343 rejects B0/0 and >115200; there is no B0 branch) -- the heaviest single-rate chain in the audited family short of umcs' 28-request open/close pair (DF-5383) and 3x umct's (DF-5496); measured closer block 6995 ms (one tcsetattr) and 13012 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+955-957; the closer owns 0 requests of its own (HUPCL DTR drop masked by the absent set_dtr, usb_serial.c:694-696 + :1374-1375; no ucom_cfg_close registered). TIOCSBRK/TIOCCBRK cost 1 x 1000 ms per toggle (uark.c:407-415). 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 2029 ms (B), abort+250 ms bound 561 ms (C); patched production uark.ko compiles 6/6 hunks clean.