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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5455 Β· 20 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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 the usbd_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 = 0 at :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 real umoscom_cfg_read/umoscom_cfg_get_status verbatim; 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 of val handed to the opaque call), then movzbl -0x9(%rbp),%eax β€” a genuine uninitialized stack load β€” feeding umoscom_cfg_get_status's four test/orb SER 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 (init val): patched production umoscom.ko builds -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

fixed
baseline reproduced→ patch + rebuild →patched reproduced

Compile 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)').
↓ fix.diffper-fix-DF-5455

Confirmed kernel references

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.