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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5517 Β· 14 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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_INTERFACE request 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

fixed
baseline reproduced→ patch + rebuild →patched reproduced

fix.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
↓ fix.diffper-fix-DF-5517

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.