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

uvisor cfg_close: one synchronous 1000 ms-timeout EP0 vendor control-IN with discarded error; an evil Handspring/Palm-VID:PID device stretches every close(2) to ~1 s of uninterruptible ucom-taskqueue occupancy and stalls the unplug (hub event thread) drain

Summary

The driver's ONLY cfg-path request - CLOSE_NOTIFICATION at 1000ms with the error discarded behind the uninterruptible close mwait: an evil Visor-VID adapter NAK-ing the data stage makes every close(2) block ~1s SIGKILL-immune and unplug stalls the hub event thread. Weakest member of the family stall set (open is free - cfg_open empty). Harness: closer blocked 1080/1093ms; 250ms-bound fix mechanism 346/363ms.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5487 Β· 13 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 4.6 KB ↓ raw
uvisor_cfgstall_harness.c β€” 8.5 KB view raw
Makefile β€” 133 B ↓ download
build.sh β€” 134 B view raw
run.sh β€” 139 B view raw
build.log β€” 7.1 KB view raw
run.log β€” 1.6 KB view raw
run.2.log β€” 219 B view raw
fix.diff β€” 1.1 KB view raw
fix_compile.log β€” 7.2 KB view raw
env.txt β€” 443 B view raw
verdict.json β€” 4.8 KB view raw

DF-5487 β€” uvisor cfg_close: one synchronous 1000 ms-timeout EP0 vendor

control-IN with discarded error; an evil Handspring/Palm-VID:PID device

stretches every close(2) to ~1 s of uninterruptible ucom-taskqueue occupancy

The chain (production code)

uvisor has the smallest cfg surface in the ucom serial family (uvisor.c:224-231): no ucom_cfg_param, no status/line callbacks, and uvisor_cfg_open() is EMPTY (uvisor.c:519-523). So open(2) costs only the core hz/10 pause (usb_serial.c:777) and zero EP0 requests, and tcsetattr costs nothing (core ucom_cfg_param returns before its pause when the driver callback is NULL, usb_serial.c:1623-1637).

The only EP0 request in the driver's cfg path is the close chain:

# task requests uvisor.c
1 ucom_close() β†’ ucom_shutdown() HUPCL β†’ line-state task 0 (no callbacks) usb_serial.c:694-696
2 ucom_cfg_close β†’ uvisor_cfg_close β†’ ucom_cfg_do_request(..., buffer, 0, 1000) 1Γ— CLOSE_NOTIFICATION vendor control-IN 529-545, req at 539-540

whose usb_error_t is discarded (DPRINTFN only, uvisor.c:541-544). ucom_cfg_do_request expands to usbd_do_request_proc(..., timo) (usb_serial.h:220-221), and close(2) is synchronous against the queue: ucom_queue_command() does usb_proc_mwait() for ucom_cfg_close (usb_serial.c:671-672, 955-957) β€” uninterruptible.

An evil Visor-VID:PID device (HANDSPRING 082d:0100 etc., uvisor.c:248-278) that NAKs the CLOSE_NOTIFICATION data stage makes the request eat its full 1000 ms timeout (usb_request.c:642-644 "WXFER" lksleep loop, 699-704), so:

  • every close(2) of /dev/ttyUxx blocks ~1 s, SIGKILL-immune (measured: closer behind a just-issued open: 1080/1093 ms);
  • unplug mid-cycle stalls the per-bus USB hub event thread the same way (uvisor_detach β†’ ucom_detach β†’ usb_proc_drain, usb_serial.c:394), plus the per-device enum lock each request holds (usb_request.c:467);
  • the block is repeatable at will (open+close cycles), but bounded at ~1 s per cycle β€” the weakest member of the family (DF-5476 ubsa ~10 s, DF-5456 umoscom ~14 s, DF-5449 uvscom ~6.1 s, all Low).

Evidence class

No Handspring/Palm VID:PID QEMU device model exists β€” KLD-harness-with- production-code (same class as DF-5367/5383/5421/5424/5425/5449/5456/ 5476): the harness drives the real in-kernel usb_process machinery (usb_proc_create / msignal / mwait / drain) that ucom's synchronous close and detach drain go through, with uvisor's production request counts (0 per open chain, 1 per close chain) and 1000 ms timeout. One timed-out EP0 request is modeled by its exact production cost: lksleep("WXFER", 1000 ms) (usb_request.c:642-644).

Reproduce

./build.sh   # in-tree kmod build at /usr/src/sys/bus/u4b/serial/hx5487, -Werror clean
./run.sh     # kldload; dmesg | grep HX5487

Expected baseline: HX5487[A]: closer: unblocked ELAPSED=~1100 ms (open-chain hz/10 pause + 1Γ—1000 ms cfg-close request). Expected fix mechanism (phase B, 250 ms cfg-path timeout bound): HX5487[B]: closer: unblocked ELAPSED=~350 ms.

Observed: A = 1080 / 1093 ms; B = 346 / 363 ms (run.log, run.2.log).

Fix

fix.diff bounds the cfg-path control timeout to 250 ms (UVISOR_CFG_TIMEOUT, same bound as the ubsa DF-5476 / uvscom DF-5449 family fixes). The patched production uvisor.ko compiles -Werror clean (fix_compile.log).

VERDICT.md
↓ download raw

DF-5487 VERDICT β€” REPRODUCED (mechanism-level, production machinery)

Finding: uvisor_cfg_close() issues one synchronous EP0 vendor control-IN (CLOSE_NOTIFICATION) at a 1000 ms timeout with the error discarded (uvisor.c:529-545), and ucom_close() waits for it uninterruptibly (usb_proc_mwait, usb_serial.c:671-672 + 955-957). An evil Handspring/Palm-VID:PID device (or a wedged real one) that NAKs the vendor data stage stretches every close(2) to ~1 s SIGKILL-immune and stalls the unplug (hub event thread) drain identically.

Reproduction (KLD-harness-with-production-code, family evidence class)

No QEMU Visor/Palm device model exists (-device usb-serial is a Netchip 0525:7701 that binds uftdi), so the harness runs the real in-kernel usb_process machinery β€” usb_proc_create / usb_proc_msignal / usb_proc_mwait / usb_proc_drain (usb_process.c) β€” that ucom's synchronous close and detach drain go through, with uvisor's exact production chain shape (0 requests + core hz/10 pause per open chain; 0-request HUPCL line task + 1Γ—1000 ms request per close chain). One timed-out EP0 request is modeled by its exact production cost (lksleep("WXFER", N ms), usb_request.c:642-644).

Measured (hz=100 guest, two runs each):

phase closer unblocked expected
A baseline (production chain) 1080 / 1093 ms ~1100 ms
B fix mechanism (250 ms bound) 346 / 363 ms ~350 ms

Decisive lines (run.log / run.2.log): HX5487[A]: closer: unblocked ELAPSED=1080 ms … =1093 ms HX5487[B]: closer: unblocked ELAPSED=346 ms … =363 ms

Task-level trace shows the exact production ordering: cfg-open task (EMPTY + hz/10 pause) β†’ line tasks (0 reqs) β†’ cfg-close task (1Γ—1000 ms, "error DISCARDED") with the closer blocked in usb_proc_mwait throughout.

Why this is the production behavior

  • uvisor_cfg_close() request: uvisor.c:539-540 (ucom_cfg_do_request(..., buffer, 0, 1000)), error path is DPRINTFN-only (uvisor.c:541-544).
  • ucom_cfg_do_request = usbd_do_request_proc(udev, &sc_super->sc_tq, req, ptr, flags, NULL, timo) (usb_serial.h:220-221) β€” synchronous control transfer on the ucom taskqueue.
  • A NAK-ing device makes the request eat the full timeout: usb_request.c:642-644 (while (usbd_transfer_pending(xfer)) lksleep(xfer, ..., "WXFER", hz)) + 699-704.
  • close(2) synchronicity: usb_serial.c:671-672 (if (fn == ucom_cfg_close) usb_proc_mwait(...)), reached from ucom_close() at usb_serial.c:955-957; mwait is cv_wait β€” uninterruptible, SIGKILL-immune (family-proven in DF-5421/5424/5425/5449/5456/5476).
  • Unplug: uvisor_detach (uvisor.c:356-368) β†’ ucom_detach β†’ usb_proc_drain(&ssc->sc_tq) (usb_serial.c:394) waits out the same request on the hub event thread; every request also serializes behind the per-device enum lock (usb_request.c:467).

Severity

Low β€” consistent with the family (weakest member: ~1 s per close cycle, bounded; requires an evil/malfunctioning Handspring/Palm-VID:PID device physically attached or passed through). No memory corruption, no info leak; impact is bounded uninterruptible taskqueue/thread occupancy (DoS), repeatable per open/close cycle and on unplug.

Fix validation

fix.diff adds UVISOR_CFG_TIMEOUT (250 ms) and uses it for the close notification. Validated two ways: 1. Harness phase B models the fixed chain: closer ELAPSED drops 1080→346 / 1093→363 ms. The multi-second→bounded occupancy change is the fix mechanism working. 2. The fully patched production uvisor.c compiled -Werror clean as a kmod on the guest (fix_compile.log, RC=0).

Negative results / what was checked and killed

  • No DF-5384-class "detach on never-attached" bug: ucom_detach() early- returns when !(ssc->sc_flag & UCOM_FLAG_ATTACHED) (usb_serial.c:376-377), so uvisor_attach's error-path uvisor_detach() (uvisor.c:351-353) is safe even when ucom_attach() never ran or failed internally (it calls ucom_detach() itself at usb_serial.c:346).
  • No cfg_param/status/line chains exist to stall (callback table uvisor.c:224-231) β€” tcsetattr is free for uvisor.
  • Guest left clean: harness dirs removed, production uvisor.c untouched (md5 28d0d390d06dd00f9dbfc03106b453a7 before and after).

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 applied to the guest's /usr/src copy: the production uvisor.ko compiled -Werror-clean RC=0 (fix_compile.log); harness phase B models the fixed driver's timeout bound: closer ELAPSED drops from ~1.08-1.09 s to ~0.35-0.36 s across two runs. The full-second uninterruptible occupancy is GONE under the fix mechanism.

fix.diff (sha256 f73a9c3d... is the harness; fix.diff itself verified by git apply --check on host + guest patch -p5); fix_compile.log (RC=0); run.log/run.2.log phase-B lines; VERDICT.md 'Fix validation'.
↓ fix.diffper-fix-DF-5487

Confirmed kernel references

Detail

Exploit chain

Evil USB adapter with a Handspring/Palm VID:PID from uvisor_devs[] (e.g. HANDSPRING 082d:0100, uvisor.c:254) enumerates as uvisor and NAKs the CLOSE_NOTIFICATION vendor data stage -> the single cfg-close request eats its full 1000 ms timeout -> every close(2) of /dev/ttyUxx blocks ~1 s SIGKILL-immune behind usb_proc_mwait; yanking the device mid-cycle stalls the per-bus USB hub event thread the same ~1 s via ucom_detach -> usb_proc_drain; repeatable per open/close cycle but bounded at ~1 s -- the weakest member of the family (DF-5476 ~10 s, DF-5456 ~14 s, DF-5449 ~6.1 s, all Low).

Evidence (decisive lines)

run.log/run.2.log: 'HX5487[A]: closer: unblocked ELAPSED=1080 ms' / '1093 ms' (baseline) and 'HX5487[B]: ... ELAPSED=346 ms' / '363 ms' (fix mechanism); task-level trace shows cfg-open (EMPTY, 0 reqs + hz/10 pause) -> line tasks (0 reqs) -> cfg-close (1 x 1000 ms, 'error DISCARDED') with the closer in usb_proc_mwait throughout; build.log: -Werror kmod build; fix_compile.log: patched production uvisor.ko RC=0.

PoC changes

No seed PoC existed; authored from scratch on the DF-5449 harness pattern with uvisor's production chain shape (0 requests per open chain, 1 per close chain, 1000 ms). One bring-up iteration: none needed beyond the pattern (first build -Werror clean).

Verified recommended fix

Bound the cfg-path control timeout to 250 ms (UVISOR_CFG_TIMEOUT) for the CLOSE_NOTIFICATION request (full diff in fix.diff).

Verdict

Mechanism-level reproduction with production machinery (same evidence class as the proven family findings DF-5367/5383/5421/5424/5425/5449/5456/5476): uvisor_cfg_close() issues the driver's ONLY cfg-path EP0 request -- one CLOSE_NOTIFICATION vendor control-IN at a 1000 ms timeout with the usb_error_t discarded (uvisor.c:529-545, request at 539-540, error at 541-544) -- and close(2) waits for it uninterruptibly (ucom_queue_command -> usb_proc_mwait, usb_serial.c:671-672 + 955-957). uvisor's cfg surface is the family's smallest: uvisor_cfg_open() is empty (uvisor.c:519-523, open chain = 0 requests + core hz/10 pause usb_serial.c:777) and there are no param/status/line callbacks (uvisor.c:224-231), so tcsetattr is free. Harness on the real usb_process taskqueue: a close(2) issued 50 ms behind an open blocks ELAPSED=1080/1093 ms across two runs (SIGKILL-immune) while the evil Visor NAKs the vendor data stage (WXFER cost, usb_request.c:642-644); unplug stalls the hub event thread identically via usb_proc_drain (usb_serial.c:394) plus the per-device enum lock (usb_request.c:467). Fix mechanism (UVISOR_CFG_TIMEOUT 250 ms bound) demonstrated at ELAPSED=346/363 ms and the fully patched production uvisor.ko compiled -Werror-clean RC=0 on the guest.