ubser cfg-path stall: TIOCSBRK's single synchronous 1000 ms EP0 VENDOR_SET_BREAK request with discarded error pins the per-adapter ucom taskqueue 1 s per ioctl; close(2) blocks uninterruptibly behind it and unplug drains the same queue; sustained break loop = unbounded occupancy
Summary
The driver's ONLY cfg-path request: one sync 1000ms vendor request per TIOCSBRK with the error discarded - an evil BWCT adapter NAKs the data stage and every break ioctl eats a full second (a slow-answer variant stretches ~0.9s with every request SUCCEEDING, fully invisible); open and tcsetattr issue ZERO requests (no cfg_open/close/param callbacks). Production-code harness: closer blocked 914ms, 5 paced pairs occupied the taskqueue ~5.7s (extends without bound); 250ms-bound fix -> 91ms.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5515 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ub5515stall_harness.c | β | 16.2 KB | view raw | |
| Makefile | β | 136 B | β download | |
| build.sh | β | 124 B | view raw | |
| run.sh | β | 145 B | view raw | |
| build.log | β | 7.1 KB | view raw | |
| run.log | β | 4.3 KB | view raw | |
| run.2.log | β | 2.8 KB | view raw | |
| run.fixed.log | β | 2.8 KB | view raw | |
| fix_build.log | β | 827 B | view raw | |
| env.txt | β | 491 B | view raw | |
| fix.diff | β | 515 B | view raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 4.9 KB | view raw |
DF-5515 VERDICT β reproduced (mechanism level, production code)
Finding
ubser_cfg_set_break() (ubser.c:462-487) is the ONLY cfg-path request issuer
in the ubser driver: one synchronous EP0 vendor request
(ucom_cfg_do_request -> usbd_do_request_proc, usb_serial.h:220-221 /
usb_request.c:760-790) with a 1000 ms timeout (ubser.c:481) whose error
return is discarded (ubser.c:482-485, DPRINTFN only). It runs on the
per-adapter ucom USB process thread (usb_serial.c:322) which executes all cfg
tasks under sc_lock (usb_process.c:94,152).
Production request accounting (verified against the core):
- first open(2) issues 0 EP0 requests (no ucom_cfg_open/close/param
callbacks registered, ubser.c:175-184; the open-time line-state task computes
break-off -> set_break(sc,0) -> gated by if (onoff), ubser.c:470);
- TIOCSBRK (usb_serial.c:1190-1193 -> ucom_cfg_line_state, usb_serial.c:
1415-1417) issues 1 x 1000 ms request;
- TIOCCBRK issues 0 requests (the gate β DF-5517);
- close(2)/unplug cost is purely queue wait: usb_proc_mwait is an
uninterruptible wait (usb_serial.c:671-672; usb_process.c:364-393) and
usb_proc_drain on detach waits the same queue (usb_serial.c:394).
Reproduction (KLD harness, production code included)
The harness includes the unmodified production ubser.c (newbus
registration neutered; md5-gated by build.sh) and runs the real static
ubser_cfg_set_break() on a real usb_process taskqueue
(usb_proc_create with the production lock protocol, usb_serial.c:322).
The modeled EP0 sink reproduces the production requester's stall: it is
called with the taskqueue lock held and sleeps the caller-supplied timeout
("WXFER", usb_request.c:643 style) on a unique wchan, returning
USB_ERR_TIMEOUT (silent device) or success at timeout-100 ms (slow-answer
device) β both of which production ubser discards.
Measured (run.log / run.2.log, hz=100):
| phase | scenario | measured |
|---|---|---|
| A | open + TIOCSBRK + close, silent device | request 212->1215 ms; closer blocked 914 ms uninterruptibly behind it; 1 request |
| B | 5 SBRK/CBRK pairs paced 1.1 s (sustained loop) | 5 requests, taskqueue continuously occupied 205->5933 ms (~5.7 s); extends without bound while the loop runs |
| B2 | slow-answer device (request SUCCEEDS) | closer blocked 818 ms (799 ms on rerun) with no error visible anywhere β the invisible variant |
| C (fixed) | timeout 250 ms (fix.diff) | closer blocked 91 ms β stall GONE |
The close(2) block is uninterruptible (usb_proc_mwait cv_wait, usb_process.c:364-393 β no PCATCH/signal check), and unplug drains the same queue on the USB hub event thread (usb_serial.c:394), so an evil BWCT adapter stalls both a closing process and device removal by the same mechanism.
Local amplification is bounded by device node permissions (ttyU 0600 root:wheel, cuaU 0660 uucp:dialer, usb_serial.c:477-488): an unprivileged user outside group dialer cannot open the tty to issue TIOCSBRK. The evil device itself needs no privilege at all.
Fix validation
fix.diff (ubser.c:481: timeout 1000 -> 250 ms) applied to the guest's
/usr/src copy (guest patch(1), hunk #1 @477); harness rebuilt -Werror clean;
rerun: phase A closer ELAPSED 91 ms vs 914 ms baseline (run.fixed.log).
Guest tree restored and md5-verified (85afa08fa4f618c9288a6dca75d9e671).
git apply --check passes against the audited host repo.
Impact
Availability only (per-adapter taskqueue pin 1 s per TIOCSBRK, uninterruptible close(2), delayed unplug; sustained break loop = unbounded occupancy). Low per family calibration β this is the family minimum: exactly ONE request, and only on TIOCSBRK.
Honest scope notes
- Mechanism-level: no physical BWCT adapter exists for the guest; the EP0 sink timing (full-timeout NAK / just-under-timeout answer) is modeled, the taskqueue, lock protocol, driver callback and request parameters are production.
- fix_build.log holds the final link lines of the fixed build only (the baseline build.log is full).
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
fixedfix.diff applied to the guest's /usr/src ubser.c (guest patch(1), hunk #1 @477); same production-code harness rebuilt -Werror-clean and re-run: phase A closer ELAPSED 91 ms vs 914 ms baseline -- the stall is gone and the break request still completes for a live device in <250 ms. git apply --check passes on the audited repo; guest tree restored and md5-verified after validation.
fix.diff; fix_build.log (RC=0 link line); run.fixed.log ('UB5515[A]: closer: unblocked ELAPSED=91 ms'); VERDICT.md 'Fix validation' section
Confirmed kernel references
- sys/bus/u4b/serial/ubser.c:462-487
- sys/bus/u4b/serial/ubser.c:480-485
- sys/bus/u4b/serial/ubser.c:175-184
- sys/bus/u4b/serial/usb_serial.h:220-221
- sys/bus/u4b/serial/usb_serial.c:322
- sys/bus/u4b/serial/usb_serial.c:671-672
- sys/bus/u4b/serial/usb_serial.c:394
- sys/bus/u4b/serial/usb_serial.c:1356-1421
- sys/bus/u4b/usb_request.c:760-790
- sys/bus/u4b/usb_process.c:364-393
Detail
Exploit chain
attacker supplies an evil BWCT USB adapter (manufacturer string 'BWCT' + vendor-specific interface, ubser.c:215-217) -> ttyU/cuaU nodes created -> a tty opener (root or group dialer) issues TIOCSBRK -> ucom_cfg_line_state (usb_serial.c:1415-1417) runs ubser_cfg_set_break on the ucom taskqueue -> device NAKs the VENDOR_SET_BREAK data stage -> 1 s of serialized cfg-thread occupancy per ioctl; concurrent close(2) of any of the adapter's ttys blocks uninterruptibly behind the same queue; unplug (hub event thread) drains it too; a sustained >=1 s-paced break loop pins the taskqueue without bound
Evidence (decisive lines)
run.log: 'UB5515[A]: ... EP0 bRequest=0x02 (1000 ms timeout): device silent -- NAKs until timeout' + 'closer: unblocked ELAPSED=914 ms'; run.log phase B: 5 requests, line-state tasks 205->5933 ms continuous; run.2.log: 'ELAPSED=799 ms' (B2 stability); run.fixed.log: 'ELAPSED=91 ms' (fix validated); build.log: -Werror-clean production-code build
PoC changes
No seed PoC existed; authored from scratch (DF-5447 production-include + DF-5496 usb_process rig). Bring-up fixes: (1) header comment contained 'ttyU/cuaU' whose */ terminated the comment block -> parser cascade; (2) usbd_do_request_proc prototype already exists in usbdi_util.h:42 (visible via ubser.c:88) -- a manual pre-declaration is unnecessary and trips -Wredundant-decls; (3) pacing: UNPACED SBRK/CBRK bursts coalesce in the usb_proc double buffer -- pairs must be spaced >= request time (1.1 s) for the sustained-loop unbounded-occupancy claim.
Verified recommended fix
Bound the cfg-path request timeout: 1000 -> 250 ms at ubser.c:481 (full diff in fix.diff; validated: 914 ms -> 91 ms).
Verdict
Mechanism-level reproduction with production code verbatim: the unmodified ubser.c (md5-gated) is compiled into a KLD harness and the real static ubser_cfg_set_break() (ubser.c:462-487) runs on a real usb_process taskqueue (usb_proc_create, usb_serial.c:322) with only the EP0 sink modeled. Measured (hz=100): a silent evil BWCT adapter makes the single TIOCSBRK request eat its full 1000 ms timeout (error discarded, ubser.c:482-485) -- close(2) behind it blocked uninterruptibly (usb_proc_mwait, usb_serial.c:671-672) for 914 ms; five paced SBRK/CBRK pairs kept the per-adapter taskqueue continuously occupied 5.7 s (sustained loops extend without bound); the slow-answer variant SUCCEEDS at timeout-100 ms and blocks the closer 818 ms with no error visible anywhere. The driver's entire cfg surface is this ONE request (no cfg_open/close/param callbacks, ubser.c:175-184; open issues 0 requests), so this is the family minimum -- same shape as DF-5487 uvisor / DF-5504 ucycom. Unplug stalls identically via ucom_detach -> usb_proc_drain (usb_serial.c:394) on the USB hub event thread. Local amplification is bounded by tty permissions (0600/0660, usb_serial.c:477-488); the evil device needs nothing. fix.diff (timeout 1000 -> 250 ms at ubser.c:481) rebuilt and rerun on the guest: closer block 91 ms vs 914 ms -- stall GONE; tree restored md5-verified.
No comments yet.