ubser can never clear a break: VENDOR_CLEAR_BREAK (ubser.c:102) is dead code β ubser_cfg_set_break only sends VENDOR_SET_BREAK inside 'if (onoff)', so TIOCCBRK and port close are silent no-ops and the device's line stays broken until power-cycle
Summary
Both break commands are defined but CLEAR is referenced nowhere: set_break has no else-leg, so TIOCCBRK and close-time line-state issue nothing - once set, the port's TX line stays in break until replug and every later write transmits garbage (one-way functional kill failing open). Production-code harness: full open->SBRK->CBRK->close lifecycle sends exactly {0x02}, CLEAR never observed; else-leg fix -> {0x02,0x03}. Upstream-inherited.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5517 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ub5517clearbreak_harness.c | β | 8.9 KB | view raw | |
| Makefile | β | 142 B | β download | |
| build.sh | β | 175 B | view raw | |
| run.sh | β | 147 B | view raw | |
| build.log | β | 7.1 KB | view raw | |
| run.log | β | 886 B | view raw | |
| run.2.log | β | 886 B | view raw | |
| run.fixed.log | β | 724 B | view raw | |
| fix_build.log | β | 113 B | view raw | |
| env.txt | β | 491 B | view raw | |
| fix.diff | β | 763 B | view raw | |
| VERDICT.md | β | 3.2 KB | β raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 4.4 KB | view raw |
DF-5517 VERDICT β reproduced (production-code request-sequence proof)
Finding
VENDOR_CLEAR_BREAK (0x03) is defined at ubser.c:102 and referenced
nowhere else in the driver (build.sh's grep: exactly 1 occurrence β the
define). ubser_cfg_set_break() (ubser.c:462-487) only issues a device
request inside if (onoff) (ubser.c:470) β the SET command (0x02) β and has
no else-leg: when the ucom core asks it to clear the break
(TIOCCBRK, usb_serial.c:1194-1197 -> ucom_cfg_line_state's clear pass,
usb_serial.c:1401-1403/1415-1417) it silently issues nothing.
Consequence: once a break is set on a BWCT port (TIOCSBRK), the device's line stays in break until the adapter is power-cycled; TIOCCBRK and port close are kernel-side no-ops. Every subsequent byte transmitted on that port is garbled at the far end (a break is a sustained space condition) β a one-way functional kill of the serial port for any tty opener.
Reproduction
The harness includes the unmodified production ubser.c (registration
neutered, md5-gated) and drives the real ubser_cfg_set_break() through
the real usb_process taskqueue with the exact ucom_cfg_line_state
arithmetic (usb_serial.c:1384-1420); the modeled EP0 sink only records the
bRequest values the production code asks to send.
Measured (run.log / run.2.log):
| phase | scenario | baseline | fixed |
|---|---|---|---|
| L | open -> TIOCSBRK -> TIOCCBRK -> close (full lifecycle) | { 0x02 } β CLEAR NEVER OBSERVED |
{ 0x02, 0x03 } |
| D | direct ubser_cfg_set_break(ucom,1); (ucom,0) on a live port |
{ 0x02 } |
{ 0x02, 0x03 } |
Static proof (build.sh, echoed into build.log): VENDOR_CLEAR_BREAK
occurs exactly once in ubser.c β its definition.
Fix validation
fix.diff (adds the else-leg mirroring the SET request shape: wValue=portno,
wIndex=iface, VENDOR_CLEAR_BREAK, bounded 250 ms timeout) applied to the
guest's /usr/src; harness rebuilt -Werror clean; rerun: both phases observe
0x03 (run.fixed.log). Guest tree restored and md5-verified
(85afa08fa4f618c9288a6dca75d9e671). git apply --check passes against the
audited host repo.
Impact
Functional correctness / availability of the port for a tty opener (root via ttyU 0600, group dialer via cuaU 0660, usb_serial.c:477-488); no kernel memory-safety impact; fails OPEN (line stuck broken) not closed. Low. Upstream-inherited defect (FreeBSD ubser.c has the identical shape) β worth fixing in both trees.
Honest scope notes
- No physical BWCT device exists on the guest: the harness proves the kernel never SENDS the clear command (request-sequence proof); the device-side effect (line stays broken) follows from the device protocol the constants describe (ubser.c:99-102 Vendor Interface Requests).
- The fixed else-leg keeps the driver's existing (odd but evidently
device-correct)
UT_READ_VENDOR_INTERFACErequest shape used by SET.
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 @482); same production-code harness rebuilt -Werror-clean and re-run: PHASE L = { 0x02, 0x03 } and PHASE D = { 0x02, 0x03 } -- VENDOR_CLEAR_BREAK OBSERVED, the clear path exists. git apply --check passes on the audited repo; guest tree restored and md5-verified.
fix.diff; fix_build.log (RC=0); run.fixed.log ('VENDOR_CLEAR_BREAK=0x03 OBSERVED' in both phases); VERDICT.md 'Fix validation' section
Confirmed kernel references
Detail
Exploit chain
any tty opener (root via ttyU 0600, group dialer via cuaU 0660, usb_serial.c:477-488) issues TIOCSBRK once -> VENDOR_SET_BREAK (0x02) reaches the device -> TIOCCBRK/close send nothing (ubser.c:470 gate, ubser.c:102 dead constant) -> the BWCT port's TX line remains in break indefinitely -> every later write(2) on the port transmits garbage until the adapter is physically replugged
Evidence (decisive lines)
run.log/run.2.log: 'PHASE L lifecycle request sequence = { 0x02 } (1 request(s); VENDOR_CLEAR_BREAK=0x03 NEVER OBSERVED)' and 'PHASE D direct set+clear request sequence = { 0x02 } (...)'; build.log grep proof 'VENDOR_CLEAR_BREAK occurrences in ubser.c: 1 (definition only: YES)'; run.fixed.log: both sequences = { 0x02, 0x03 } (0x03 OBSERVED)
PoC changes
No seed PoC existed; authored from scratch (DF-5447 production-include + DF-5515 rig). Bring-up fixes: (1) kernel TU has no memchr -- replaced with a scan loop; (2) neutered ubser_driver/ubser_devclass statics must be referenced (touch function) or -Wunused kills the build; (3) the direct-clear phase must run on a LIVE taskqueue so the sink's usb_proc_is_gone gate (usb_request.c:771) passes as in production.
Verified recommended fix
Add the missing else-leg to ubser_cfg_set_break() sending VENDOR_CLEAR_BREAK (wValue=portno) with a bounded 250 ms timeout (full diff in fix.diff; validated: clear now OBSERVED).
Verdict
Production-code request-sequence proof: the unmodified ubser.c (md5-gated) is compiled into a KLD harness and the real static ubser_cfg_set_break() runs on a real usb_process taskqueue with the exact ucom_cfg_line_state arithmetic (usb_serial.c:1384-1420). The full open -> TIOCSBRK -> TIOCCBRK -> close lifecycle sends exactly ONE device command, VENDOR_SET_BREAK (0x02); the clear command is NEVER sent on any path -- PHASE L sequence = { 0x02 }, PHASE D (direct set_break(1); set_break(0) on a live port) = { 0x02 }, VENDOR_CLEAR_BREAK (0x03) NEVER OBSERVED, because the request lives inside `if (onoff)' (ubser.c:470) with no else-leg and the constant at ubser.c:102 has exactly one occurrence in the file (its #define, grep proof in build.log). Consequence: once a break is set on a BWCT port, the device's line stays broken until the adapter is power-cycled; TIOCCBRK and close are silent kernel-side no-ops and all subsequent TX on the port is garbled (functional kill of the port, fails open). Upstream-inherited (FreeBSD ubser has the identical shape). fix.diff (else-leg sending VENDOR_CLEAR_BREAK, wValue=portno, bounded 250 ms) rebuilt and rerun on the guest: both phases observe { 0x02, 0x03 }; tree restored md5-verified.
No comments yet.