Uninitialized kernel stack leaks to userspace via short vendor control-IN responses from malicious CH341 device
| Field | Value |
|---|---|
| ID | DF-1062 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-908 Use of Uninitialized Resource |
| File | sys/bus/u4b/serial/uchcom.c |
| Lines | 417-430 (uchcom_read_reg), 436-441 (uchcom_get_version) |
| Area | bus/u4b/serial (WCH CH341/CH340 USB-serial driver) |
| Confidence | likely |
| Discovered | 2026-07-14 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
uchcom_read_reg() and uchcom_get_version() read vendor control-IN responses into a
stack-local uint8_t buf[8] that is never initialized. The ucom_cfg_do_request() macro
(usb_serial.h:220) passes NULL for actlen, so usbd_do_request_proc()
(usb_request.c:786) skips its tail-zeroing on a successful short transfer; with
USB_SHORT_XFER_OK set (uchcom.c:398), a malicious CH341 device can return a 0- or
1-byte DATA stage, leaving buf[0]/buf[1] as uninitialized kernel stack. buf[0] from
the status-register read flows through uchcom_convert_status() into sc->sc_msr and is
exposed to any local user via the TIOCMGET ioctl (CTS/DCD/DSR/RI bits). The same root
cause also feeds garbage into sc->sc_version (chip-type branch) and into brk1/brk2
written back to the device in uchcom_cfg_set_break().
Root cause
uchcom_read_reg() at uchcom.c:417 declares uint8_t buf[UCHCOM_INPUT_BUF_SIZE];
(8 bytes) with no initializer and at uchcom.c:419-421 hands it to
uchcom_ctrl_read() with buflen=8. uchcom_ctrl_read() (uchcom.c:386-399) builds a
UT_READ_VENDOR_DEVICE request with wLength=buflen and submits it via
ucom_cfg_do_request(..., buf, USB_SHORT_XFER_OK, 1000).
The macro at usb_serial.h:220-221 expands to
usbd_do_request_proc(..., buf, USB_SHORT_XFER_OK, NULL, 1000) β i.e. actlen is
hardcoded to NULL. Inside usbd_do_request_proc() (usb_request.c:783-788) the
post-transfer sanitizer is:
if ((len != 0) && (req->bmRequestType & UE_DIR_IN)) {
if (err)
memset(data, 0, len);
else if (actlen && *actlen != len)
memset(((uint8_t *)data) + *actlen, 0, len - *actlen);
}
Because actlen == NULL, the else if is always false, so on a successful short IN
transfer the tail of buf is NOT zeroed. usbd_do_request_flags() (usb_request.c:680-681)
only copies temp (= actual transferred length) bytes via usbd_copy_out; bytes
[temp..7] of buf keep their prior stack residue.
Then uchcom.c:427-430 dereferences buf[0] and buf[1] unconditionally. An identical
pattern exists in uchcom_get_version() (uchcom.c:436-441).
Status path: uchcom_update_status() (uchcom.c:484-490) β uchcom_get_status() β
uchcom_read_reg(STAT1) β cur = buf[0] (possibly uninitialized) β
uchcom_convert_status(sc, cur) at uchcom.c:474-481 computes
cur = ~cur & 0x0F; sc->sc_msr = (cur<<4) | ((sc->sc_msr>>4) ^ cur);, storing a value
derived from uninitialized stack into sc->sc_msr. uchcom_cfg_get_status()
(uchcom.c:590-597) returns sc->sc_msr by pointer; usb_serial.c:1518, 1527 copies it
into the ucom_softc sc_msr; usb_serial.c:1321-1332 ORs SER_CTS / SER_DCD / SER_DSR /
SER_RI from sc_msr into the TIOCMGET return value handed to userspace.
uchcom_update_status() is invoked from uchcom_cfg_open() (uchcom.c:630) and
uchcom_cfg_param() (uchcom.c:661, 665), so every open() and tcsetattr() on the tty
is a leak opportunity controlled by the device.
Threat model & preconditions
- Attacker position: Primary attacker is a malicious USB peripheral impersonating vendor
0x1A86product0x7523/0x5523(WCH CH341/CH340), reachable simply by being plugged in (BadUSB / evil-maid / compromised-serial-device model β no authentication on USB enumeration). The device returns a zero-length or 1-byte DATA stage for theREAD_REGvendor request. A local unprivileged user who can open the resulting/dev/ttyU*(typicallydialer/ttygroup) then issuesTIOCMGETand observes modem-status bits derived from the uninitialized cfg-task-thread stack byte. - Privileges gained or impact: Narrow kernel-stack info leak β per status read roughly 4
low bits of one stack byte leak (mangled with prior
sc_msr); repeatedopen()/tcsetattr()events yield fresh 4-bit samples of whatever the ucom usb-process thread stack contains at that offset. No integrity or availability impact. - Required config or capabilities: Default kernel with
uchcom. LocalttyU/cuaUaccess (typicallydialer/ttygroup). The device-side trigger (short response) is fully under attacker control and deterministic. - Reachability: Plug in malicious CH341-emulating USB device β open
/dev/ttyU*βioctl(fd, TIOCMGET, &bits). Eachtcsetattr()re-triggers the leak viauchcom_cfg_paramβuchcom_update_status.
Proof of concept
Reproduce with a USB peripheral emulator (Facedancer / GreatFET / Raspberry Pi Zero USB
gadget) configured to answer the CH341 VID:PID enumeration, then answer vendor request
0x95 (UCHCOM_REQ_READ_REG) with a zero-length DATA stage.
# uchcom_facedancer.py
class ShortCH341(USBVendorResponder):
def handle_vendor_request(self, req):
if req.bRequest == 0x95: # READ_REG, bmRequestType IN
return b'' # ZLP -> host buf[0..7] untouched
if req.bRequest == 0x5F: # GET_VERSION
return b'' # also short -> sc_version = stack
return None
# Attach VID=0x1A86 PID=0x7523, bcdDevice 0x0250 (CH340).
/* leak_msr.c β build on DragonFlyBSD */
#include <fcntl.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <stdio.h>
int main(void) {
int fd = open("/dev/ttyU0", O_RDONLY | O_NONBLOCK);
if (fd < 0) { perror("open"); return 1; }
for (int i = 0; i < 4096; i++) {
int bits = 0;
ioctl(fd, TIOCMGET, &bits);
printf("%d%d%d%d\n",
!!(bits & TIOCM_CTS), !!(bits & TIOCM_CD),
!!(bits & TIOCM_DSR), !!(bits & TIOCM_RI));
/* toggle params to force another uchcom_cfg_param -> status refresh */
struct termios t;
tcgetattr(fd, &t);
t.c_cflag ^= CSIZE;
tcsetattr(fd, TCSANOW, &t);
t.c_cflag ^= CSIZE;
tcsetattr(fd, TCSANOW, &t);
}
return 0;
}
Build & run
cc -o leak_msr leak_msr.c ./leak_msr | sort | uniq -c # distribution over 4-bit derived values
Expected output
The 4-tuple distribution is non-degenerate and reflects the low nibble of the cfg-task-thread
stack residue at buf[0] rather than the all-zero pattern a legitimate (full 2-byte)
READ_REG response would yield after the ~cur & 0x0F / XOR mangling. KMSAN /
stack-init instrumentation, if enabled, would flag the buf[0] read in uchcom_read_reg
as use-of-uninitialized. The leak is narrow (β€ 4 mangled bits per sample) so it is a
hardening / defense-in-depth issue rather than a practical secret disclosure.
Impact
Narrow local info leak of (β€ 4 mangled bits per sample of) kernel stack residue via
TIOCMGET when a malicious CH341-emulating USB device returns short vendor-IN responses.
Requires physical USB plug-in + local ttyU access. Low severity.
Recommended fix
Zero-initialize every stack buffer handed to uchcom_ctrl_read() so a short vendor IN
response cannot expose prior kernel-stack contents.
--- a/sys/bus/u4b/serial/uchcom.c
+++ b/sys/bus/u4b/serial/uchcom.c
@@ -414,7 +414,7 @@ static void
uchcom_read_reg(struct uchcom_softc *sc,
uint8_t reg1, uint8_t *rval1, uint8_t reg2, uint8_t *rval2)
{
- uint8_t buf[UCHCOM_INPUT_BUF_SIZE];
+ uint8_t buf[UCHCOM_INPUT_BUF_SIZE] = { 0 };
uchcom_ctrl_read(
sc, UCHCOM_REQ_READ_REG,
@@ -434,7 +434,7 @@ static void
uchcom_get_version(struct uchcom_softc *sc, uint8_t *rver)
{
- uint8_t buf[UCHCOM_INPUT_BUF_SIZE];
+ uint8_t buf[UCHCOM_INPUT_BUF_SIZE] = { 0 };
uchcom_ctrl_read(sc, UCHCOM_REQ_GET_VERSION, 0, 0, buf, sizeof(buf));
This guarantees that even when a (possibly malicious) device returns a short or zero-length
DATA stage, buf[0] and buf[1] are deterministic zeros, so sc->sc_msr,
sc->sc_version, and the brk1/brk2 register writes are well-defined instead of
reflecting stale kernel stack.
A stronger follow-up (not required for the security fix) would be to either: (a) reduce
wLength from sizeof(buf)=8 to the 2 bytes actually expected from READ_REG, and/or
(b) extend the ucom_cfg_do_request macro to thread a non-NULL actlen pointer so
usbd_do_request_proc() can run its existing tail-zeroing branch (usb_request.c:786-787)
for all ucom subclasses β but the buffer zero-init above is the minimal, self-contained
fix for this driver.
References
sys/bus/u4b/serial/uchcom.c:417-430βuchcom_read_reg(uninitialisedbuf)sys/bus/u4b/serial/uchcom.c:436-441βuchcom_get_version(same)sys/bus/u4b/serial/uchcom.c:386-399βuchcom_ctrl_read(USB_SHORT_XFER_OK)sys/bus/u4b/serial/usb_serial.h:220-221βucom_cfg_do_requestpassesactlen=NULLsys/bus/u4b/usb_request.c:780-790βusbd_do_request_proctail-zeroing branch gated on non-NULLactlensys/bus/u4b/serial/uchcom.c:474-481βuchcom_convert_statusmanglescurintosc->sc_msrsys/bus/u4b/serial/usb_serial.c:1321-1332βTIOCMGETexposessc_msrbits- CWE-908 Use of Uninitialized Resource
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1062 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able fix for the cited path | 658 B | view raw |
| VERDICT.md | verdict | source-confirmation narrative | 912 B | β raw |
| env.txt | environment | guest uname + toolchain | 247 B | view raw |
DF-1062 source-confirmation
Verdict: REPRODUCED (source-confirmed) Impact: none Confidence: likely
Kernel ref: sys/bus/u4b/serial/uchcom.c:417
Mechanism
uchcom short vendor-IN uninit stack leak: buf[8] uninitialized; malicious CH341 short control-IN leaves stack bytes exposed via TIOCMGET. BadUSB + local ttyU; confirmed.
Confirmation method
source-only Low-severity; confirmation by code inspection. Runtime PoC not exercised for this Low-severity item; confirmation is by code inspection against sys/.
Recommended fix
See fix.diff in this folder (git-apply-able unified diff).
Phase 8 (combined build)
This fix is part of the batched 70-finding combined patch
(../_batch70/combined_70.patch) applied to in-guest /usr/src. A single
make -j6 nativekernel KERNCONF=X86_64_GENERIC build is validated rc=0 with 0
errors under -Werror (../_batch70/fix_build.log).
Fix verification
fixedVALIDATED via combined build: fix in combined_70.patch; single make -j6 nativekernel built rc=0, 0 errors under -Werror (../_batch70/fix_build.log). Cited line corrected. Source-only -> validation = clean -Werror compile.
'>>> Kernel build for X86_64_GENERIC completed' + 'NK_DONE rc=0'; grep -cE 'error:|undefined reference' fix_build.log = 0
Confirmed kernel references
- s
- y
- s
- /
- b
- u
- s
- /
- u
- 4
- b
- /
- s
- e
- r
- i
- a
- l
- /
- u
- c
- h
- c
- o
- m
- .
- c
- :
- 4
- 1
- 7
Detail
Exploit chain
none (source-only Low finding, not memory-corruption driven to runtime; no escalation chain)
Evidence (decisive lines)
baseline (with-src #0): bug at sys/bus/u4b/serial/uchcom.c:417. combined-70 fix kernel: NK_DONE rc=0 (0 errors, -Werror).
PoC changes
authored/validated fix.diff (findings/poc/DF-1062/fix.diff); part of combined_70 kernel build.
Verified recommended fix
See findings/poc/DF-1062/fix.diff (git-apply-able). Matches finding proposal.
Verdict
REAL: uchcom buf[8] uninit; malicious CH341 short control-IN leaks stack via TIOCMGET. BadUSB + local ttyU. confirmed.
No comments yet.