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 makettyU*world-readable andcuaU*writable bydialoutgroup) equipped with a programmable USB device (Facedancer / Rubber Ducky class) that enumerates as one of the 80+ VID:PID pairs inuslcom_devs[](uslcom.c:208-293) and answers theGET_LATCHvendor request with a 0-length DATA1 packet β legal per USB 2.0 Β§8.5.3. The user issuesioctl(fd, USB_GET_GPIO)and observes a 4-byte int whose low byte is a fragment ofuslcom_ioctl's kernel stack frame (whatever sat at thelatchstack 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. LocalttyU/cuaUaccess. 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.
Recommended fix
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
sys/bus/u4b/serial/uslcom.c:622-639βUSB_GET_GPIO(uninitialisedlatch)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-NULLactlen- DF-1062 β same class of bug in
uchcom(broader impact, same framework root cause) - CWE-908 Use of Uninitialized Resource
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1063 Β· 1 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
fixedfix.diff applied + combined nativekernel build rc=0 (-Werror)
fix.diff applied + combined nativekernel build rc=0 (-Werror)
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
No comments yet.