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

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 0x1A86 product 0x7523 / 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 the READ_REG vendor request. A local unprivileged user who can open the resulting /dev/ttyU* (typically dialer / tty group) then issues TIOCMGET and 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); repeated open() / 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. Local ttyU / cuaU access (typically dialer / tty group). 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). Each tcsetattr() re-triggers the leak via uchcom_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.

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

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1062 Β· 3 files
FileTypeDescriptionSize
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
VERDICT.md verdict source-confirmation narrative
↓ download 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/.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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
↓ fix.diffDragonFly 6.5-DEVELOPMENT combined 70-finding fix kernel (built rc=0 -Werror 2026-07-23; not booted - source-only)

Confirmed kernel references

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.