umoscom_cfg_read() returns uninitialized kernel stack on failed/short vendor control-IN; umoscom_cfg_get_status maps bits 4-7 of the stale byte into TIOCMGET modem-status bits disclosed to unprivileged userspace (evil or wedged MCS7703)
Summary
uint8_t val uninitialized, error discarded: every failure mode (timeout/stall/cancel/zero-length/short) leaves the buffer untouched, and unlike the consumer-less uvscom twin this byte has a LIVE userspace consumer - cfg_get_status ORs msr bits into SER_CTS/DCD/RI/DSR -> sc_msr -> TIOCMGET copyout. Production-code KLD: 12/12 pattern-match across 6 patterns x 2 failure modes (0xff -> all four bits set), 3 loads 0 mismatches; fixed twin reports 0x00. Evil/wedged device precondition; 4 stale-stack bits per status refresh. val=0 fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5455 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 4.9 KB | β raw | |
| mo5455_harness.c | β | 9.4 KB | view raw | |
| mo5455_shim.c | β | 1.5 KB | view raw | |
| umoscom_prod_guarded.c | β | 17.9 KB | view raw | |
| prod_guard.diff | β | 995 B | view raw | |
| gen_guarded.py | β | 3.0 KB | view raw | |
| Makefile | β | 660 B | β download | |
| build.sh | β | 262 B | view raw | |
| run.sh | β | 156 B | view raw | |
| build.log | β | 7.9 KB | view raw | |
| build.werror_evidence.log | β | 1.6 KB | view raw | |
| run.log | β | 6.7 KB | view raw | |
| run.1.fold.log | β | 2.4 KB | view raw | |
| env.txt | β | 634 B | view raw | |
| fix.diff | β | 647 B | view raw | |
| fix_compile.log | β | 2.5 KB | view raw | |
| umoscomunleak.ko | β | 15.0 KB | β download | |
| manifest.json | β | 1.9 KB | view raw | |
| verdict.json | β | 5.7 KB | view raw |
DF-5455 β umoscom(4): uninitialized kernel stack returned by umoscom_cfg_read() on failed/short vendor control-IN, disclosed to userspace through TIOCMGET modem-status bits
Production file: sys/bus/u4b/serial/umoscom.c (MosChip MCS7703 USB serial,
ucom core). Evidence class: KLD-harness-with-production-code (no MCS7703
device model exists on the QEMU guest).
What the harness is
mo5455_harness.c #includes umoscom_prod_guarded.c β a byte-identical
copy of /usr/src/sys/bus/u4b/serial/umoscom.c except ONE guard hunk
(prod_guard.diff): the ucom_cfg_do_request macro is rerouted after its
usb_serial.h definition to mo_shim_request() (defined in the separate
TU mo5455_shim.c, mirroring production's opaque cross-module
usbd_do_request_proc call). The shim reproduces the traced
usbd_do_request_flags() failure semantics of an evil/silent MCS7703 β
timeout, and "successful" zero-length data stage β both of which leave the
caller's 1-byte buffer untouched. The DRIVER_MODULE registration is kept
(inert: no MCS7703 exists on the guest); the production cfg helpers are
called directly.
Build
sh build.sh # in-tree kmod build on the guest:
# /usr/src/sys/bus/u4b/serial/mo5455 + make
Run
sh run.sh # kldload umoscomunleak.ko x3, kldunload, dmesg | grep MO5455
Expected (decisive) output
MO5455: pat=0xff mode=timeout : PROD msr=0xf0 (predict 0xf0) TIOCM after ucom_totio=0741 MATCH MO5455: pat=0xa5 mode=0-len : PROD msr=0xa0 (predict 0xa0) TIOCM after ucom_totio=0501 MATCH MO5455: pat=0xXX FIXED (val=0) : msr=0x00 (clean) MO5455: RESULT: ... 12/12 pattern-match across failure modes -- UNINITIALIZED STACK DISCLOSED THROUGH MODEM-STATUS BITS
Bits 4..7 of the stale stack byte surface as TIOCM_CTS/TIOCM_DSR/
TIOCM_RI/TIOCM_CD exactly per the production mapping chain
(umoscom.c:511-521 β usb_serial.c:1527 β 1321-1332 β 1259-1270 β
TIOCMGET copyout at 1209-1211). Deterministic across 3 loads (36/36
matches, 0 mismatches). run.1.fold.log documents the optimizer-fold
artifact seen when the shim was same-TU inlinable (GCC folded the
provably-uninitialized load to constant 0 β why the shim now lives in its
own TU). build.werror_evidence.log shows the production-shaped
cross-TU build compiles with no diagnostic at all β the bug ships
silently; the disassembly (movzbl -0x9(%rbp),%eax after the opaque
call, feeding the four orb SER-bit translations) is in VERDICT.md.
Fix
fix.diff β initialize val in umoscom_cfg_read() (one line).
Runtime counterfactual in the harness (fixed twin under identical
poisoning: msr=0x00) plus patched umoscom.ko building -Werror-clean
(fix_compile.log, RC=0 β and the patched build passes WITH
-Wuninitialized armed).
DF-5455 VERDICT β reproduced (impact: leak, 4 bits of kernel stack per status refresh)
Finding
umoscom_cfg_read() (sys/bus/u4b/serial/umoscom.c:539-557) declares
uint8_t val; uninitialized, issues a 1-byte vendor control-IN through
ucom_cfg_do_request() (macro, usb_serial.h:220-221 β
usbd_do_request_proc(..., actlen=NULL, 1000)), and returns val
without checking the usb_error_t result β the request's return value is
discarded at both call sites (umoscom.c:551-556 writes nothing to val on
failure).
Core semantics traced in sys/bus/u4b/usb_request.c:
- timeout / stall / cancel: the WXFER wait loop breaks on
xfer->error(usb_request.c:642-650) before theusbd_copy_out()at :680-681 β the caller's buffer is never written; - zero-length data stage, clean status stage: SUCCESS return with
temp == 0, nothing copied (usb_request.c:666-683); - short (0-of-1 byte) data stage: umoscom passes
flags=0(no USB_SHORT_XFER_OK) βUSB_ERR_SHORT_XFERβ same no-copy break; - the macro hard-codes
actlen=NULL, so the core's only zeroization hook (*actlen = 0at :448-449) does not apply to the buffer.
Consumer chain (this is what separates it from the consumer-less uvscom sibling DF-5448):
umoscom_cfg_get_status() umoscom.c:495-522
msr = umoscom_cfg_read(UMOSCOM_MSR) :507 <-- uninit byte
*p_msr |= SER_CTS/DCD/RI/DSR from msr bits 0x10/0x80/0x40/0x20 :511-521
ucom_cfg_status_change() usb_serial.c:1515-1518 (new_msr starts 0)
sc->sc_msr = new_msr :1527
ucom_modem(tp,0,0) / TIOCMGET usb_serial.c:1321-1332, 1209-1211
ucom_totio(): SER_CTSβTIOCM_CTS, SER_DCDβTIOCM_CD,
SER_RIβTIOCM_RI, SER_DSRβTIOCM_DSR :1259-1270
copyout of the int to userspace
β bits 4..7 of one uninitialized kernel-stack byte are disclosed to an
unprivileged TIOCMGET reader, 4 bits per status refresh, whenever the
MSR vendor read fails (evil device: stay silent / stall / reply 0 bytes;
flaky device: timeout). Status refreshes are device-driven (interrupt
pipe β ucom_status_change, umoscom.c:681) and open-driven
(usb_serial.c:880), so an evil adapter controls the refresh rate.
Reproduction (KLD harness with production code, guest 6.5-DEVELOPMENT)
- The guarded production copy (1 hunk,
prod_guard.diff) compiles the realumoscom_cfg_read/umoscom_cfg_get_statusverbatim; only the EP0 transport is shimmed with the traced failure semantics. - Disassembly of the built module (
umoscom_cfg_read.constprop.2):lea -0x9(%rbp),%rsi(address ofvalhanded to the opaque call), thenmovzbl -0x9(%rbp),%eaxβ a genuine uninitialized stack load β feedingumoscom_cfg_get_status's fourtest/orbSER translations. - Stack poisoned with known patterns right before each production call:
12/12 pattern-matches per load, 3 loads, 0 mismatches
(
run.log): 0xffβSER 0xf0 (TIOCM 0741: CTS+DSR+RI+CD+LE), 0xa5β0xa0 (0501), 0x5aβ0x50 (0241), 0xf0/0x00/0x0f as predicted β for BOTH failure modes (timeout and zero-length success). - Counterfactual with the one-line fix (
uint8_t val = 0;), identical shim + poisoning: msr=0x00 in every case.
Two artifacts document the journey: run.1.fold.log (when the shim was
same-TU inlinable, GCC -O2 folded the provably-uninitialized load to a
constant 0 β the load never happened and results were all-zero; the shim
now lives in its own TU exactly like production's cross-module
usbd_do_request_proc) and build.werror_evidence.log (the
production-shaped cross-TU build produces no diagnostic at all β
-Wuninitialized only fires when GCC can see through the callee, which
is why this class ships).
Impact ceiling
4 bits per refresh of stale kernel stack from the ucom task thread's frame region (prior cfg tasks leave termios copies, request buffers, pointers there). Not a classic address leak by itself (top bits of a pointer are the usual take), low-bandwidth, evil-device or fault precondition. Severity: Low (CWE-457/CWE-908, kernleak bucket), consistent with family precedent (DF-1062, DF-2342) β but note this instance has a live userspace consumer, unlike DF-5448.
Fix validation
fix.diff(initval): patched productionumoscom.kobuilds -Werror-clean on the guest (fix_compile.log, RC=0) β with -Wuninitialized armed, i.e. the same-TU diagnostic that fires on the unfixed code is gone.- Runtime: the fixed twin path under identical poisoning reports zero
SER bits (36/36 clean across 3 loads,
run.log).
Not applied to sys/ (read-only audit target).
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
fixedCompile dimension: patched production umoscom.ko builds -Werror-clean RC=0 WITH -Wuninitialized armed (fix_compile.log -- the same-TU diagnostic that fires on unfixed code is gone). Runtime dimension: the fixed twin (verbatim cfg_read/cfg_get_status bodies with uint8_t val = 0;) runs under the identical shim + poisoning and reports msr=0x00 in all 36 cases while the unfixed production path pattern-matches 36/36 -- the one-line init provably closes the disclosure at the exact code point.
fix.diff (sha256 adc851f3357e395d8f1991719211c1866f7d86126ededaac567d8b9800050aeb); fix_compile.log (RC=0); run.log FIXED lines (36x 'msr=0x00 (clean)').
Confirmed kernel references
- sys/bus/u4b/serial/umoscom.c:539-557
- sys/bus/u4b/serial/umoscom.c:495-522
- sys/bus/u4b/serial/usb_serial.h:220-221
- sys/bus/u4b/usb_request.c:642-650
- sys/bus/u4b/usb_request.c:666-683
- sys/bus/u4b/serial/usb_serial.c:1515-1518
- sys/bus/u4b/serial/usb_serial.c:1527
- sys/bus/u4b/serial/usb_serial.c:1321-1332
- sys/bus/u4b/serial/usb_serial.c:1246-1273
- sys/bus/u4b/serial/usb_serial.c:1209-1211
Detail
Exploit chain
evil MCS7703 (0x9710:0x7703) is inserted; user opens /dev/ttyUx; device stays silent on the MSR/LSR vendor status reads (each times out after 1000 ms, buffer untouched); umoscom_cfg_get_status maps the stale stack byte's bits 4-7 into SER_* bits; ucom_cfg_status_change stores them into sc->sc_msr; unprivileged user calls TIOCMGET and reads TIOCM_CTS/CD/RI/DSR derived from uninitialized kernel stack; repeat to harvest stale-stack bits (harness proves the mapping deterministically with poisoned patterns).
Evidence (decisive lines)
run.log: 3 loads x 'RESULT: 12/12 pattern-match ... UNINITIALIZED STACK DISCLOSED THROUGH MODEM-STATUS BITS', 36 MATCH / 0 MISMATCH lines with per-pattern TIOCM values; fix counterfactual lines 'FIXED (val=0): msr=0x00 (clean)'; disassembly in VERDICT.md (umoscom_cfg_read.constprop.2: lea -0x9(%rbp),%rsi; callq; movzbl -0x9(%rbp),%eax feeding the four orb SER translations); build.werror_evidence.log (production-shaped cross-TU build emits no diagnostic -- why it ships); run.1.fold.log (same-TU optimizer-fold artifact, documented); fix_compile.log RC=0.
PoC changes
Seed concept executed as the family KLD-harness class: (1) production umoscom.c compiled in verbatim via a 1-hunk guarded copy (prod_guard.diff -- only the ucom_cfg_do_request macro is rerouted after its usb_serial.h definition; call sites byte-identical); (2) the failure-semantics shim lives in a SEPARATE TU (mo5455_shim.c) mirroring production's opaque cross-module usbd_do_request_proc -- when it was same-TU inlinable, GCC -O2 folded the provably-uninitialized load to constant 0 (run.1.fold.log artifact); (3) deterministic stack poisoning (recursive 256B pads x7) replaces the device/grooming precondition; (4) both failure modes exercised: timeout and successful zero-length data stage.
Verified recommended fix
Initialize the read buffer in umoscom_cfg_read() (one line, full diff in fix.diff); optionally fix the core macro to pass actlen.
Verdict
REPRODUCED with production code on the guest (KLD-harness-with-production-code class; no MCS7703 device model exists): umoscom_cfg_read() (umoscom.c:539-557) declares uint8_t val; uninitialized, issues a 1-byte vendor control-IN through ucom_cfg_do_request (usb_serial.h:220-221, actlen=NULL, 1000 ms) and returns val with the usb_error_t discarded. Traced core semantics (usb_request.c:642-650 timeout/stall/cancel break BEFORE the copy-out at :680-681; :666-683 zero-length data stage copies nothing) leave the buffer untouched in every failure mode. The harness compiles a byte-identical production copy (1 guard hunk rerouting only the request macro to a cross-TU shim with those exact failure semantics) and calls the real umoscom_cfg_get_status(): stack poisoned with known patterns, the stale byte's bits 4-7 surfaced as SER_CTS/SER_DCD/SER_RI/SER_DSR exactly per umoscom.c:511-521 -> sc->sc_msr (usb_serial.c:1527) -> ucom_modem (:1321-1332) -> ucom_totio (:1259-1270) -> TIOCMGET copyout (:1209-1211). 12/12 pattern-match per load across 6 patterns x 2 failure modes, 3 loads, 0 mismatches (0xff->0xf0/TIOCM 0741, 0xa5->0xa0/0501, 0x5a->0x50/0241); disassembly shows the genuine post-call movzbl -0x9(%rbx-frame),%eax load. Counterfactual with the one-line fix returns 0x00 under identical poisoning. Impact ceiling: 4 bits of stale ucom-task-thread kernel stack per status refresh, device-failure precondition (evil or wedged MCS7703) -- Low, kernleak bucket, but with a live userspace consumer unlike the uvscom DF-5448 sibling.
No comments yet.