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

uvscom_cfg_read_status() returns uninitialized kernel stack on a successful SHORT vendor control-IN (core actlen=NULL zeroization gap) β€” return value currently has no consumer, latent DF-1062/2342-class hazard

Summary

uint8_t data[2] stays uninit when a control-IN succeeds short: the core zeroizes the short-read tail only when actlen!=NULL but ucom_cfg_do_request hardcodes NULL - return (data[0]|(data[1]<<8)) reads uninit stack. No consumer today (sole caller discards it; status fed from the intr pipe) but the NetBSD original uses exactly this read for modem status. One-line init fix (compile-validated).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5448 Β· 7 files
FileTypeDescriptionSize
README.md β€” 3.1 KB ↓ raw
VERDICT.md β€” 3.5 KB ↓ raw
fix.diff β€” 322 B view raw
fix_compile.log β€” 7.2 KB view raw
env.txt β€” 454 B view raw
manifest.json β€” 820 B view raw
verdict.json β€” 3.1 KB view raw

DF-5448 β€” uvscom_cfg_read_status() returns uninitialized kernel stack on a

successful short vendor control-IN response

The bug

sys/bus/u4b/serial/uvscom.c:714-734:

static uint16_t
uvscom_cfg_read_status(struct uvscom_softc *sc)
{
    struct usb_device_request req;
    usb_error_t err;
    uint8_t data[2];                    /* <-- UNINITIALIZED stack */

    req.bmRequestType = UT_READ_VENDOR_DEVICE;   /* 0xC0: device-to-host */
    req.bRequest = UVSCOM_READ_STATUS;
    USETW(req.wValue, 0);
    USETW(req.wIndex, 0);
    USETW(req.wLength, 2);

    err = ucom_cfg_do_request(sc->sc_udev, &sc->sc_ucom,
        &req, data, 0, 1000);
    if (err) {
        DPRINTFN(0, "device request failed, err=%s (ignored)\n", ...);
    }
    return (data[0] | (data[1] << 8));   /* <-- may be uninit */
}

Core semantics (sys/bus/u4b/usb_request.c:783-788, usbd_do_request_proc):

    /* on failure we zero the data */
    /* on short packet we zero the unused data */
    if ((len != 0) && (req->bmRequestType & UE_DIR_IN)) {
        if (err)
            memset(data, 0, len);          /* error: fully zeroed */
        else if (actlen && *actlen != len)
            memset(((uint8_t *)data) + *actlen, 0, len - *actlen);
    }

The short-read tail zeroization only happens when actlen != NULL β€” but ucom_cfg_do_request (sys/bus/u4b/serial/usb_serial.h:220-221) hardcodes actlen = NULL:

#define ucom_cfg_do_request(udev,com,req,ptr,flags,timo) \
    usbd_do_request_proc(udev,&(com)->sc_super->sc_tq,req,ptr,flags,NULL,timo)

So: - request fails (STALL/timeout): buffer zeroed β†’ return 0. Safe. - request succeeds with a short data stage β€” a control-IN transfer that ends in a 0-byte short packet leaves both bytes uninitialized; a 1-byte response leaves data[1] uninitialized (usb_request.c:662-665 copies only acttemp bytes, 680-681) β†’ return (data[0] | (data[1] << 8)) reads uninitialized kernel stack. Exactly the DF-1062/DF-2342 (uchcom) class, reached through the DF-5438 (ulpt)-style gap.

Impact

Currently none reaches userspace: the sole caller uvscom_cfg_open() (uvscom.c:640) discards the return value, and sc_lsr / sc_msr / sc_unit_status are fed only from the interrupt pipe (uvscom.c:425-446). The uninitialized value is computed and thrown away β€” a latent hazard, not a live leak: it becomes a real kernel-stack disclosure the moment any code consumes the return (the NetBSD original this driver was ported from uses exactly such a status read to update the modem-status state). Filed as hardening: initialize the buffer (defense in depth, same treatment the core already gives the error path).

Evidence

Static, code-certain (no SUNTAC device model exists for execution; the Info-severity class was not executed β€” see verdict.json status=untested). Path: uvscom.c:719,733 β†’ usb_serial.h:220-221 β†’ usb_request.c:662-665, 680-681, 783-788.

Fix

fix.diff β€” one line: initialize data. (Optionally the core macro could pass a real actlen; that is a ucom-wide change tracked with the family.)

Files

  • README.md, VERDICT.md, fix.diff, manifest.json, verdict.json
  • fix_compile.log β€” patched uvscom.ko compiles clean (combined guest build)
VERDICT.md
↓ download raw

DF-5448 VERDICT β€” uninitialized stack returned by uvscom_cfg_read_status()

on a successful short control-IN (NO CONSUMER β€” latent, Info)

Status: NOT EXECUTED (static, code-certain; Info-severity hardening class β€” no SUNTAC device model exists to exercise the short-read data stage). The defect is proven by direct source tracing below; the fix compiles clean on the guest (fix_compile.log, combined patched build RC=0).

The path, line by line

  1. uvscom_cfg_read_status() β€” uvscom.c:714-734:
    uint8_t data[2];                                /* :719 UNINITIALIZED */
    req.bmRequestType = UT_READ_VENDOR_DEVICE;      /* :721 0xC0 = IN */
    USETW(req.wLength, 2);
    err = ucom_cfg_do_request(sc->sc_udev, &sc->sc_ucom,
        &req, data, 0, 1000);                       /* :727-728 */
    if (err) { DPRINTFN(0, "... (ignored)\n", ...); }
    return (data[0] | (data[1] << 8));              /* :733 possibly uninit */
  1. ucom_cfg_do_request β€” usb_serial.h:220-221 β€” expands to usbd_do_request_proc(udev, &sc_super->sc_tq, req, ptr, flags, NULL , timo) β€” the actlen out-parameter is hardcoded NULL.

  2. usbd_do_request_proc β€” usb_request.c:780-788:

    if ((len != 0) && (req->bmRequestType & UE_DIR_IN)) {
        if (err)
            memset(data, 0, len);                    /* error: zeroed */
        else if (actlen && *actlen != len)           /* actlen == NULL! */
            memset(((uint8_t *)data) + *actlen, 0, len - *actlen);
    }
  • request fails (STALL/timeout): buffer fully zeroed β†’ returns 0. SAFE.
  • request succeeds with a short data stage: the data-phase loop copies only acttemp bytes (usb_request.c:662-665 temp = acttemp, copy at 680-681) and returns err==0; the tail-zeroization branch is guarded by actlen && which is FALSE here (NULL) β†’ data[1] (1-byte reply) or both bytes (0-byte reply, a 0-length short packet terminates a control-IN data stage) remain uninitialized kernel stack, which line 733 reads and returns.

This is exactly the uchcom class (DF-1062/DF-2342, uninitialized kernel stack via short/malformed vendor control-IN) reached through the same core gap as DF-5438 (ulpt status callback).

Why Info and not Low

The sole caller discards the return: uvscom_cfg_open() uvscom.c:640 uvscom_cfg_read_status(sc); β€” value unused; sc_lsr/sc_msr/ sc_unit_status are fed exclusively from the interrupt pipe (uvscom.c:425-446). The uninitialized value is computed and thrown away: no kernel-memory disclosure, no control-flow impact today. It becomes a live DF-1062-class leak the moment any code consumes the return value (the NetBSD original uses this very status read to update modem-line state, so the porting-direction risk is real). Filed as hardening; one-line fix.

Fix

fix.diff: uint8_t data[2] = {0, 0}; β€” compiled clean as part of the combined patched uvscom.ko build on the guest (fix_compile.log, RC=0). Behavioral validation not possible without a device model (fix_status= not_testable for the runtime dimension; compile dimension validated).

Environment

env.txt: DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC, cc 8.3, guest left pristine.

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

not_testable
baseline no→ patch + rebuild →patched clean

Compile-dimension validated (patched production uvscom.ko builds -Werror-clean RC=0 on the guest, fix_compile.log); runtime dimension not testable without a SUNTAC device model -- the uninitialized-read path requires a crafted short control-IN data stage.

fix.diff (sha256 fc1e9be926f7708aec146584515283631b46210c6738ea17ae2147b2dac09d23); fix_compile.log (RC=0, combined patched build).
↓ fix.diffper-fix-DF-5448

Confirmed kernel references

Detail

Evidence (decisive lines)

Source trace in VERDICT.md: uvscom.c:719 (uninit data[2]), uvscom.c:733 (uninit read in return), usb_serial.h:220-221 (actlen=NULL), usb_request.c:783-788 (tail zeroization gated on actlen), usb_request.c:662-665+680-681 (short-copy semantics); uvscom.c:640 (return discarded -- why impact is none).

PoC changes

No PoC authored: executing the short-read data stage requires a USB device model for the SUNTAC VID:PID that does not exist on the QEMU guest; the defect is provable (and was proven) by direct source tracing of the exact core semantics.

Verified recommended fix

Initialize the read buffer in uvscom_cfg_read_status() (one line, full diff in fix.diff); optionally fix the core macro to pass actlen.

Verdict

Static, code-certain finding NOT executed (Info-severity hardening class; no SUNTAC VID:PID device model exists to exercise a short control-IN data stage -- same treatment as the DF-5438 ulpt sibling). uvscom_cfg_read_status() (uvscom.c:714-734) returns data[0]|(data[1]<<8) from a stack buffer that the core leaves uninitialized when a control-IN SUCCEEDS with a short data stage: usbd_do_request_proc() zeroizes the short-read tail only when actlen != NULL (usb_request.c:786-787) and ucom_cfg_do_request hardcodes actlen=NULL (usb_serial.h:220-221); the data-phase loop copies only acttemp bytes (usb_request.c:662-665, 680-681). A 1-byte reply leaves data[1] uninitialized, a 0-length data stage leaves both bytes uninitialized. Impact TODAY is none: the sole caller uvscom_cfg_open() (uvscom.c:640) discards the return value and sc_lsr/sc_msr/sc_unit_status are fed only from the interrupt pipe (uvscom.c:425-446) -- latent DF-1062/DF-2342-class hazard that turns into a kernel-stack disclosure the moment any code consumes the return value.