Ring buffer out-of-bounds writes in the Linux USB audio drivers

Sinan Turunc Published 4 November 202626 min read
On this page

#Introduction

Last month I decided to do some vulnerability research on USB audio drivers in the Linux kernel. While carrying out static analysis, I found an interesting function in the snd-usb-audio driver, which is the kernel's driver for standard USB audio devices such as headsets and microphones. The function was retire_capture_urb(), which the driver calls each time a transfer of recorded audio arrives from the device so that it can copy every packet in that transfer into a ring buffer in kernel memory that the recording application reads from. A ring buffer is a fixed-size buffer used as a loop, so when the driver has written up to its last byte it continues writing from the first byte. When playback runs, copy_to_urb() does the reverse and copies audio from the ring buffer into the packets the driver sends to the device. In the other driver, snd-ua101, which drives the Edirol UA-101 and UA-1000 audio interfaces, the same logic is performed by two functions of its own, copy_capture_data() and copy_playback_data().

When writing memory-safe C, you want to be able to point to the bounds check that keeps the length of every memcpy() inside both the buffer it reads from and the buffer it writes to. What I noticed reading these four functions is that none of them has a bounds check, even though the USB audio device sets the size of each packet and the application sets the size of the ring buffer, so nothing ensures that a packet fits in the ring buffer before memcpy() is called. When I had the emulated device send 1023-byte packets while the application had asked for a 128-byte ring buffer, retire_capture_urb() wrote 767 bytes past the end of the ring buffer on the first packet. On the second packet, the length it passed to memcpy() wrapped to just under 4 GiB, and the kernel took a page fault.

I then wrote a patch for each driver so that no call to these functions copies more than the size of the ring buffer, and both patches are now in the kernel, merged in the 7.4 merge window :)

  • ALSA: usb-audio: Bound packet copies by the PCM buffer size (06be918062ad)
  • ALSA: ua101: Bound packet copies by the PCM buffer size (493a77a9f20f)

Both patches carry Cc: stable, which asks the stable maintainers to backport them to the long-term kernels. I also searched sound/usb and drivers/media/usb for other functions that copy packets into a ring buffer in the same way, and found the same kind of function in four more drivers, snd-usb-line6, snd-usb-usx2y, cx231xx-audio and em28xx-audio. When I tested them on the same kernel, the usx2y and cx231xx drivers overflowed their ring buffers as snd-usb-audio does. I reported all four results to the maintainers in the cover letter, but the two patches leave those drivers unchanged.

In this post I want to discuss how I found these bugs, how I reproduced them with an emulated USB device, and how I fixed, tested and submitted the patches for both drivers.

The post is split into seven sections. Firstly, the Background explains how a USB audio device sends packets and how the driver copies them into the ring buffer. Secondly, I show where the packet size and the ring buffer size come from. Thirdly, the Findings walk through each overflow, and the remaining sections cover how I reproduced and fixed the bugs, how I submitted the patches and what I learned from them. I used the following setup to reproduce and test the bugs.

  • Kernel under test: mainline 7.3-rc7 (2f2f7eeef8b0, x86_64) built with KASAN (the Kernel Address Sanitizer), and a 6.19-rc3 build without KASAN.
  • Virtual machine: QEMU, with the emulated device attached through vhci-hcd (CONFIG_USBIP_VHCI_HCD=y).
  • Emulated device: a Python USB/IP server with a UAC1 (USB Audio Class 1.0) profile for snd-usb-audio and one profile for each of the other five drivers.
  • Sound servers: a Fedora 42 root filesystem with PipeWire 1.4.11, WirePlumber 0.5.14 and PulseAudio 17.0.

#Background

#Isochronous transfers

Before I could see why these functions write past the end of the ring buffer, I had to understand how a USB audio device transfers audio to and from the host, so this section unpacks those mechanics first. The USB specification breaks a device into endpoints, and each one is a part of the device with its own address that either sends data to the host or receives data from it. Each endpoint has a number and a direction, IN for an endpoint that sends to the host and OUT for one that receives from it. Each endpoint also has one of four transfer types, control, bulk, interrupt or isochronous, and the type determines how the host controller schedules the endpoint's data on the USB bus that connects the device to the host. Audio uses isochronous transfers, which reserve time on that bus for the endpoint once every service interval, so samples arrive at a constant rate, but a packet that is lost is never retransmitted. The shortest service interval is 1 ms at full speed and 125 µs at high speed and above.

That covers isochronous transfers, so next I looked at how snd-usb-audio passes these packets to the host controller and receives them back, and found that it does this in batches of packets called USB request blocks (URBs). The kernel describes each one with a struct urb, which holds the endpoint the transfer uses in pipe, the buffer for its data in transfer_buffer and, in complete, the function the kernel calls once the transfer has finished. snd-usb-audio submits the URB to the host controller and the host controller carries out the transfer on the bus. The URB lists its packets in its iso_frame_desc array, with one entry per packet giving that packet's offset in the transfer buffer and its length. When recording, the device fills one of these packets every service interval, and the packet's entry holds how many bytes the device actually sent in actual_length. When the application is playing audio the device reads one packet every service interval instead. Once every packet in a URB has been transferred the host controller driver hands the URB back to snd-usb-audio, which for a recording stream is when retire_capture_urb() runs.

diagram
USB audio device
isochronous IN endpoint, one packet per service interval
|
v
host controller fills the URB's packets and records each
| actual_length in iso_frame_desc[]
v URB complete
snd_complete_urb()
|
v
retire_capture_urb() memcpy() each packet into the ring buffer
|
v
ring buffer runtime->dma_area, buffer_bytes long
|
v
application reads it through the PCM interface
USB deviceisochronous IN Host controllerfills the URB snd_complete_urb() retire_capture_urb()memcpy() each packet Ring bufferdma_area Application every service interval bytes = 1023set by the device (wMaxPacketSize) buffer_bytes = 128set by the application (hw_params) no check of bytes against buffer_bytes
Figure 1. The two sizes meet in one function, and nothing compares them.

The device also declares the largest packet it will send in the wMaxPacketSize field of its endpoint descriptor, which the host reads when the device is enumerated. Many simple USB microphones and headsets follow the UAC1 specification, and UAC1 adds a class-specific endpoint descriptor that can set the MaxPacketsOnly attribute (UAC_EP_CS_ATTR_FILL_MAX in the kernel), which indicates that every packet on that endpoint is full size, and this is how a full-speed device can make every packet 1023 bytes long.[9]

#The ring buffer

On the host the kernel's sound subsystem ALSA (Advanced Linux Sound Architecture) keeps the audio for each stream in a ring buffer. When recording snd-usb-audio writes at a position that moves forward through the ring buffer and once that position passes the last byte it wraps back to the first byte and the driver writes over audio the application has already read. ALSA calls each of these streams a pulse-code modulation (PCM) stream because it carries raw audio samples, and it keeps the stream's ring buffer in kernel memory at runtime->dma_area, the dma_area field of the struct snd_pcm_runtime that ALSA creates for every open stream. The job of the PCM layer is to sit between the driver and the application, so when recording the driver writes into the ring buffer and the PCM layer gives the application an interface to read from it, and when playing back the application writes through that interface and the driver reads from the ring buffer.

A sample is a single measurement of the sound wave for one channel that is stored as a number such as an 8-bit or 16-bit value, and what ALSA does is group one sample from every channel taken at the same instant into a frame. So a 16-bit stereo frame is four bytes while a frame of the 8-bit mono stream I used in my tests is a single byte, and ALSA measures the ring buffer in frames so that its sizes mean the same thing whatever the sample format. When an application such as my test client opens a stream it sets its hardware parameters, known as hw_params, and two of them, the period size and the number of periods, determine the size of the ring buffer, where a period is how many frames ALSA lets the driver write before it wakes the application. Multiply the two and you get the buffer size, which snd-usb-audio keeps in bytes as buffer_bytes.

#How retire_capture_urb() fills the ring buffer

With buffer_bytes defined, we can now look at how retire_capture_urb() in sound/usb/pcm.c uses it. For each packet in a completed URB, the function saves its current write position in the ring buffer, hwptr_done, as oldptr, moves hwptr_done forward by the packet's length, and then copies the packet into the ring buffer starting at oldptr. Because a packet can run past the last byte of the ring buffer, the function has to wrap back to the first byte, and this is how it does that in mainline 7.3-rc7.

sound/usb/pcm.cc
/* update the current pointer */
scoped_guard(spinlock_irqsave, &subs->lock) {
oldptr = subs->hwptr_done;
subs->hwptr_done += bytes;
if (subs->hwptr_done >= subs->buffer_bytes)
subs->hwptr_done -= subs->buffer_bytes;
...
}
/* copy a data chunk */
if (oldptr + bytes > subs->buffer_bytes) {
unsigned int bytes1 = subs->buffer_bytes - oldptr;
memcpy(runtime->dma_area + oldptr, cp, bytes1);
memcpy(runtime->dma_area, cp + bytes1, bytes - bytes1);
} else {
memcpy(runtime->dma_area + oldptr, cp, bytes);
}
diagram
ring buffer: buffer_bytes = 128, oldptr = 80, bytes = 100
offset 0 52 80 128
+---------+------------------+---------------------+
| part 2 | | part 1 |
| 52 B | | 48 B |
+---------+------------------+---------------------+
first memcpy(): 48 bytes from offset 80 up to the last byte
second memcpy(): 52 bytes from offset 0
hwptr_done = 80 + 100 - 128 = 52 (below 128, so the next packet is fine)

The function subtracts buffer_bytes from hwptr_done only once, and it splits the memcpy() into two calls, the first up to the last byte of the ring buffer and the second from the first byte, which I call a single wrap. Both steps assume that one packet is never larger than the ring buffer, so retire_capture_urb() is only correct when bytes is at most buffer_bytes.

#Packet size and buffer size

Having gone over the retire_capture_urb() function, let's look at where bytes and buffer_bytes come from, because the device sets one and the application sets the other. The size of each packet in bytes is held in bytes and comes from the packet's actual_length, which the device limits with the wMaxPacketSize it declares in its endpoint descriptor. snd-usb-audio keeps that limit as maxpacksize alongside curpacksize, which is the packet size it expects at the current sample rate, and when a device sets MaxPacketsOnly the driver sets curpacksize to maxpacksize so every packet arrives at the maximum size no matter what the sample rate is. The maximum packet size also depends on the speed the device connects at, and in the table below we can see the maximum for each speed.

speedmaximum packet
full speed (USB 1.1)1023 bytes
high speed (USB 2.0), with the packet multiplier3072 bytes
super speed (USB 3)up to 49152 bytes per interval

The size of the ring buffer in bytes is held in buffer_bytes and comes from the application instead. The only limits snd-usb-audio puts on it are three lower bounds that setup_hw_info() sets when the stream is opened, which are that a period holds at least 64 bytes, that a stream has at least two periods and that a period lasts at least one service interval. The 8 kHz mono stream of 8-bit samples I used in my tests meets all three with two periods of 64 frames each, and since each frame in that stream is one byte the application could ask for a ring buffer of only 128 bytes, which is less than an eighth of a single 1023-byte packet at full speed.

This means that every time retire_capture_urb() copies a packet, the function already holds both values it needs, the packet size that the device set in bytes and the ring buffer size that the application set in buffer_bytes, but nowhere in the function did I find a test such as if (bytes > subs->buffer_bytes) that checks one against the other before memcpy() is called. The only other place in the driver that could have caught this is setup_hw_info(), whose constraints could reject a ring buffer smaller than a packet when the application sets hw_params, but none of them uses the packet size either, so the check was never written anywhere in the snd-usb-audio driver.

diagram
USB audio device application
wMaxPacketSize = 1023 hw_params: period 64, periods 2
MaxPacketsOnly set
| |
v v
bytes = 1023 buffer_bytes = 128
\ /
\ /
v v
retire_capture_urb() -> memcpy(dma_area + oldptr, cp, ...)
no check of bytes against buffer_bytes

#Findings

#snd-usb-audio

Since no function checked bytes against buffer_bytes, I sent 1023-byte packets into the 128-byte ring buffer from the previous section to see what retire_capture_urb() would do. With oldptr at 0, the first memcpy() wrote 128 bytes and the second wrote the other 895 bytes from the first byte of the ring buffer, which carried the write 767 bytes past its end. The pointer update then subtracted 128 only once and left hwptr_done at 895, which is still not below buffer_bytes, so on the next packet bytes1 was buffer_bytes - oldptr, and 128 minus 895 as an unsigned int wraps to 4294966529. memcpy() then read past the end of the URB's transfer buffer and wrote past the end of the ring buffer, which ALSA allocates with vmalloc. The diagram below shows the two packets.

diagram
Packet 1: bytes = 1023, oldptr = 0, buffer_bytes = 128
dest offset 0 128 895
+------------+
first part | 128 bytes |
+------------+
+------------+-----------------------------------+
second part | 895 bytes, written from offset 0 |
+------------+-----------------------------------+
|<---- 767 bytes past the ring ---->|
(still inside the 1-page allocation)
pointer update: hwptr_done = 0 + 1023 - 128 = 895 (not below 128)
Packet 2: oldptr = 895
bytes1 = buffer_bytes - oldptr = 128 - 895 -> 4294966529 as unsigned int
Packet 1bytes = 1023, oldptr = 0, buffer_bytes = 128 ring buffer rest of the 1-page vmalloc allocation 0128895 first memcpy(): 128 bytes from offset 0 second memcpy(): the other 895 bytes, again from offset 0 767 bytes past the end of the ring buffer KASAN stays quiet: the write is still inside the page Packet 2hwptr_done = 0 + 1023 - 128 = 895, not below 128 oldptr = 895 bytes1 = 128 - 895 = 4294966529 as unsigned int: memcpy() runs off the end, page fault
Figure 2. Bytes drawn to scale. The ring buffer is 128 bytes; one full-speed packet is 1023.

KASAN did not report packet 1 because the 767-byte overflow stayed inside the page-sized allocation that holds the ring buffer, so it only reported packet 2. KASAN checks the source of a memcpy() before its destination, which meant the first fault it reported was the over-read of the URB's transfer buffer in slab memory, as the report below shows.

log
snd-usb-audio 2-1:1.0: invalid position: , pos = 895, buffer size = 128, period size = 64
BUG: KASAN: slab-out-of-bounds in retire_capture_urb+0x251/0x870
Read of size 4294966529 at addr ffff88800c1e2800 by task vhci_rx/82
retire_capture_urb+0x251/0x870
snd_complete_urb
__usb_hcd_giveback_urb
vhci_rx_loop

Playback failed the same way because copy_to_urb() has the same single wrap. prepare_playback_urb() calls it to fill each URB and one URB may hold a whole period of audio plus one more packet, which is more than a 128-byte ring buffer can hold, so this time the over-read came from the ring buffer itself at 0x37f (895) bytes into its vmalloc page, as the second report shows.

log
BUG: KASAN: vmalloc-out-of-bounds in copy_to_urb.isra.0+0x188/0x430
Read of size 4294966529 at addr ffffc9000004537f by task vhci_rx/79
prepare_playback_urb+0x98a/0x1fc0
The buggy address belongs to a 1-page vmalloc region starting at
0xffffc90000045000 allocated at __snd_dma_alloc_pages

Since I had already reproduced the bug on the latest mainline kernel with KASAN, I also wanted to check that it did not depend on KASAN, so I ran the same stream on a 6.19-rc3 build without it. The stream ended in a page fault in memcpy() called from retire_capture_urb(), and the register dump showed RDX holding 0xfffffd01, which is the wrapped bytes1.

#snd-ua101

With snd-usb-audio covered, I turned to snd-ua101, the second driver from the introduction, whose capture function copy_capture_data() in sound/usb/misc/ua101.c takes the frame count from the packet's actual_length and splits its memcpy() only once at the end of the ring buffer.

sound/usb/misc/ua101.cc
dest = runtime->dma_area + stream->buffer_pos * frame_bytes;
if (stream->buffer_pos + frames <= runtime->buffer_size) {
memcpy(dest, urb->transfer_buffer, frames * frame_bytes);
} else {
/* wrap around at end of ring buffer */
frames1 = runtime->buffer_size - stream->buffer_pos;
memcpy(dest, urb->transfer_buffer, frames1 * frame_bytes);
memcpy(runtime->dma_area,
urb->transfer_buffer + frames1 * frame_bytes,
(frames - frames1) * frame_bytes);
}

As we can see above, the capture side takes its frame count from the device, and on playback copy_playback_data() takes its frame count from the capture packets through the rate_feedback FIFO, which means the device sets the packet size in both directions.

On the ring buffer side set_stream_hw() allows a period as small as one byte with a minimum of two periods and a minimum period time of 1.5 service intervals, and at 44.1 kHz and high speed those limits allow a 9-frame period and an 18-frame ring buffer. My emulated device declared one channel and a 672-byte endpoint, which delivered 224 three-byte frames per packet into that 18-frame ring buffer, so after the first packet buffer_pos was 206 and frames1 wrapped on the next one as the reports below show.

diagram
Packet 1: frames = 224, buffer_pos = 0, buffer_size = 18 frames
frame 0 18 206
+--------+
part 1 | 18 |
+--------+
+--------+-------------------------------------+
part 2 | 206 frames, written from frame 0 |
+--------+-------------------------------------+
|<---- 188 frames past the ring ----->|
buffer_pos = 0 + 224 - 18 = 206 (not below 18)
Packet 2: frames1 = 18 - 206 -> wraps, a read of (18 - 206) x 3 = 4294966732 bytes
log
snd-ua101 2-1:1.0: invalid position: , pos = 206, buffer size = 18, period size = 9
BUG: KASAN: slab-out-of-bounds in capture_urb_complete+0x9eb/0xcb0
Read of size 4294966732 at addr ffff88800d73e7e0 by task vhci_rx/81
BUG: KASAN: vmalloc-out-of-bounds in playback_work+0x789/0xac0
Read of size 4294966732 at addr ffffc9000002626a by task kworker/0:1H/80

Both reports showed a read of 4294966732 bytes, which is 18 minus 206 frames times 3 bytes per frame reduced modulo 2^32, the wrapped length of the first memcpy().

#Other drivers with the same single wrap

After fixing the out-of-bounds copies in snd-usb-audio and snd-ua101, I searched sound/usb and drivers/media/usb for other functions with the same single wrap and found it in four more drivers. I built a USB/IP device profile for each one and ran one capture per driver on 7.3-rc7 with a ring buffer at or near the smallest the driver accepts, and the table below shows what happened.

driverdevice profilepacket into bufferresult
snd-usb-line6, line6_capture_copy()GuitarPort1024 bytes into 128invalid position: pos = 224, buffer size = 32, then the driver logs driver bug: len and stops copying; no KASAN report, because the ring is a whole page and the first copy stays inside it
snd-usb-usx2y, usx2y_urb_capt_retire()US-122, after the hwdep firmware load1024 bytes into 512KASAN: out-of-bounds in i_usx2y_urb_complete, read of size 18446744073709551104
cx231xx-audio, cx231xx_audio_isocirq()Pixelview Xcapture1024 bytes into 128KASAN: use-after-free in cx231xx_audio_isocirq, read of size 4294966528, position 224 against a 32-frame buffer
em28xx-audioem28603072 bytes into 2457630 s of capture, no report
DRIVERRESULTSTATUS snd-usb-audioOverflowedcapture and playbackFixed06be918062ad snd-ua101Overflowedcapture and playbackFixed493a77a9f20f snd-usb-usx2yOverflowed1024 bytes into 512Reported cx231xx-audioOverflowed1024 bytes into 128Reported snd-usb-line6Bad positionstopped copyingReported em28xx-audioSafeits smallest buffer is larger than any packetReported
Figure 3. Every status has a text label as well as a colour.

The usx2y and cx231xx drivers overflowed their ring buffers, and their read sizes were the same kind of wrapped length as in the two fixed drivers, 128 minus 896 modulo 2^32 for cx231xx and -512 held in a 64-bit type for usx2y. KASAN called the cx231xx access a use-after-free because the wrapped read reached a freed object. The line6 driver reached the same bad position, but its first overflow stayed inside the page-sized ring buffer and the driver then logged driver bug: len and stopped copying, so KASAN had nothing to report.

The em28xx driver was safe because of its buffer limits and not because of its copy function. Its period_bytes_min is 11059 and its periods_min is 2, which makes its smallest ring buffer 22118 bytes, while em28xx_audio_ep_packet_size() caps a high-speed endpoint at 8188 bytes, so no packet can be larger than the ring buffer. I reported all four results to the maintainers in the cover letter, but the two patches in this post do not change usx2y, cx231xx or line6.

#Reproduction

#Why a USB/IP device

To reproduce the bug, I needed a device that sends full-size isochronous packets and that was the most challenging part. The kernel's own USB device emulation dummy_hcd fails every isochronous transfer. syzkaller emulates devices through raw-gadget, which also runs on dummy_hcd, and its USB/IP mode sends arbitrary USB/IP packets rather than emulating a device with a streaming endpoint, so as far as I can tell no fuzzer has reached this path.[8] So as a hacky solution I used USB/IP to emulate the whole device instead, and the kernel under test imported it through vhci-hcd so the audio driver saw an ordinary USB device with a real isochronous endpoint.

diagram
+------------------------+ TCP 3240 +----------------------------+
| usbip-server.py | <--------------------> | QEMU, KASAN kernel |
| UAC1 mic/speaker | | vhci-hcd -> snd-usb-audio |
| wMaxPacketSize 1023 | | attach.c client.c |
| FILL_MAX, 1 ch 8 kHz | | period 64, periods 2 |
+------------------------+ +----------------------------+

#The test setup

The setup I built comes to about 580 lines of code across a USB/IP device server and two small C programs.

HOST usbip-server.pyemulated UAC1 device QEMU VM, mainline 7.3-rc7 with KASAN attach.cOP_REQ_IMPORT vhci-hcd snd-usb-audioKASAN on client.c PipeWire 1.4.11 PulseAudio 17.0 TCP
Figure 4. The emulated device reaches the driver the way a real one does, at any USB speed.

usbip-server.py emulates a UAC1 microphone or a speaker with --mode playback and it fills every packet it sends. It also has a profile for snd-ua101 and for each of the four drivers above with the IDs and interfaces that driver expects. The UAC1 profile I used for snd-usb-audio presents the smallest set of descriptors ALSA would accept, listed below.

  • an AudioControl interface;
  • one AudioStreaming interface with an idle and a streaming alternate setting;
  • a Type I format with one channel and 8-bit samples at one rate;
  • an isochronous endpoint with the chosen wMaxPacketSize;
  • a class-specific endpoint descriptor with bmAttributes set to 0x80 (MaxPacketsOnly).
usbip-server.pypython
bytes([9, 4, 1, 1, 1, 1, 2, 0, 0]), # AS interface 1 alt 1 (streaming)
bytes([7, 0x24, 1, link, 1, 0x01, 0x00]), # CS AS general: terminal link, PCM
bytes([11, 0x24, 2, 0x01, 1, 1, 8, 1]) + struct.pack("<I", self.rate)[:3], # type I: 1 ch, 8 bit
bytes([9, 5, ep_addr, 0x0D]) + struct.pack("<H", wmax) + bytes([1, 0, 0]), # isoc ep, synchronous
bytes([7, 0x25, 1, 0x80, 0, 0, 0]), # CS isoc data ep: MaxPacketsOnly

I wrote attach.c so the test kernel's initramfs would not need the usbip tool, and it sends the USB/IP import request OP_REQ_IMPORT itself and then passes the connected socket's file descriptor to vhci-hcd by writing it to /sys/devices/platform/vhci_hcd.0/attach.

client.c opens the card's PCM device directly and asks for the smallest legal buffer. It sets stop_threshold to LONG_MAX so that an XRUN does not stop the stream. An XRUN is the underrun or overrun on which ALSA normally stops a stream. The relevant part of its hw_params setup is below.

client.cc
interval_set(&IV(hw, SNDRV_PCM_HW_PARAM_CHANNELS), channels);
interval_set(&IV(hw, SNDRV_PCM_HW_PARAM_RATE), rate);
interval_set(&IV(hw, SNDRV_PCM_HW_PARAM_PERIOD_SIZE), period);
interval_set(&IV(hw, SNDRV_PCM_HW_PARAM_PERIODS), 2);
if (ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &hw) < 0) { perror("HW_PARAMS"); return 1; }
...
sw.stop_threshold = LONG_MAX; /* never stop on xrun */

For each test I booted the kernel under test in a QEMU virtual machine and attached the emulated device. I then started client.c as an unprivileged user in the audio group and checked the console for sanitiser reports. KASAN reported the out-of-bounds access within a second of the stream starting in every test. The server never reported an actual_length larger than the packet length the host requested so the failure did not depend on a malformed transfer.

#Testing with stock sound servers

If we take a look at the client.c program, it asks for the smallest buffer that the driver allows so it tests a 128-byte ring buffer that no full-speed packet of 1023 bytes can fit into, not the buffer size that the sound server on a normal desktop would choose. On a desktop, applications go through PipeWire or PulseAudio and those sound servers choose the buffer size themselves. So I tested the same device against a Fedora 42 root filesystem with its packaged sound servers as the same unprivileged user on the unpatched kernel and the table below shows the results.

sound serverbuffer it requestsfull speed, 1023 Bhigh speed, 3072 Bsuper speed, 49152 B
PipeWire 1.4.11 with WirePlumber 0.5.14the driver's maximum, capped at 2 s by snd-usb-audio (16000 bytes for this stream)fitsfitspw-record reproduces the capture report, pw-play the playback report
PulseAudio 17.0, timer scheduling off (tsched=0, its default inside a VM)four 25 ms fragments, 799 bytes hereparecord reproduces the capture reportnot runnot run
PulseAudio 17.0, timer scheduling on2 sfitsfitsnot run
Attack tree. A packet larger than the ring buffer needs both a device that sends large packets and one of three things: any user who can open the PCM device, PulseAudio 17.0 with timer scheduling off, or PipeWire 1.4.11 at super speed.
Figure 5. What it takes to reach the overflow.

At full and high speed the bug needed a small buffer, and any user who can open the PCM device could ask for one. Stock PulseAudio asked for one by itself when timer scheduling was off, which is its default inside a VM. Timer scheduling is the mode in which PulseAudio refills its buffer on a system timer instead of on the sound card's interrupts, and with it off PulseAudio falls back to small fixed fragments. At super speed an ordinary recording or playback application reached the bug in the PipeWire configuration I tested once the device was the default audio device. I did not test the three cells marked "not run" but the arithmetic predicts the same failure in all three, because PulseAudio's 799-byte buffer is smaller than one packet at high and super speed and its 2 s buffer of 16000 bytes is smaller than one packet at super speed.

These tests changed the cover letter more than any other because they replaced my assumptions about exposure with measurements.

#The fixes

#snd-usb-audio

The snd-usb-audio patch adds one check for capture and one for playback, so in retire_capture_urb() it truncates a packet larger than the ring buffer to buffer_bytes before the frame arithmetic and logs a ratelimited warning, and in prepare_playback_urb(), where the loop already stopped filling a URB at the URB's own size, it also stops once the URB would hold a whole ring buffer. The diff below only shows the changed lines and the full patch is commit 06be918062ad.

Flow chart. A packet arrives and the driver asks whether bytes is larger than buffer_bytes. Before the fix, yes leads to a second copy that runs past the end of the ring buffer. After the fix, yes leads to copying at most buffer_bytes. If not, the driver asks whether oldptr plus bytes is larger than buffer_bytes: yes means two memcpy() calls, no means one.
Figure 6. How retire_capture_urb() copies a packet, before and after the fix.
06be918062addiff
@@ -1343,6 +1343,18 @@ static void retire_capture_urb(struct snd_usb_substream *subs,
bytes -= adj;
subs->stream_offset_adj -= adj;
}
+ /*
+ * A packet can be as large as the endpoint's maxpacksize, which
+ * the device chooses, while the ring buffer can be as small as
+ * two 64-byte periods. The hwptr_done wrap below handles one
+ * wrap only, so never take more than one buffer from a packet.
+ */
+ if (unlikely(bytes > subs->buffer_bytes)) {
+ dev_warn_ratelimited(&subs->dev->dev,
+ "%u-byte packet exceeds %u-byte buffer, truncating\n",
+ bytes, subs->buffer_bytes);
+ bytes = subs->buffer_bytes;
+ }
frames = bytes / stride;
@@ -1553,8 +1565,14 @@ static int prepare_playback_urb(struct snd_usb_substream *subs,
counts = snd_usb_endpoint_next_packet_size(ep, ctx, i, avail);
+ /*
+ * Bound by the ring buffer as well as by the URB:
+ * copy_to_urb() wraps hwptr_done once, so one URB must
+ * never carry more than the whole buffer.
+ */
if (counts < 0 ||
- (frames + counts) * stride > ctx->buffer_size)
+ (frames + counts) * stride > ctx->buffer_size ||
+ (frames + counts) * stride > subs->buffer_bytes)
break;

Once bytes can no longer be larger than buffer_bytes, retire_capture_urb() again meets the condition its single wrap depends on, because oldptr always stays below buffer_bytes so bytes1 cannot wrap, the rest of the packet bytes - bytes1 is at most oldptr and both memcpy() calls stay inside the ring buffer.

diagram
Packet 1 after the fix: bytes = 1023, buffer_bytes = 128
bytes > buffer_bytes -> warn "1023-byte packet exceeds 128-byte buffer, truncating"
bytes = 128
offset 0 128
+--------------------------------------+
| 128 bytes, one full ring buffer |
+--------------------------------------+
hwptr_done stays below 128, so bytes1 can never wrap

I also looked at adding a hardware constraint (period_bytes_min >= maxpacksize) that would have rejected the configuration at hw_params time. That constraint would also have denied small periods to every device that declares a large wMaxPacketSize, so I kept the checks inside retire_capture_urb() and prepare_playback_urb() because they only take effect when a packet is larger than the ring buffer.

I tested the patch on 7.3-rc7 with the same device and client.c for 30 s in each direction and KASAN reported nothing. Capture kept streaming and logged 1023-byte packet exceeds 128-byte buffer, truncating, while playback went into XRUN straight away because no URB could hold even one packet, which is what should happen when a device's packets cannot fit the ring buffer.

#snd-ua101

The snd-ua101 patch fixes capture the same way as the snd-usb-audio patch, by truncating frames to runtime->buffer_size before copy_capture_data() copies the packet, but playback took me three attempts.

My first version clamped frames inside copy_playback_data() and that was wrong, because playback_work() had already set urb->iso_frame_desc[0].length from the unclamped value, so the packet would have gone out partly filled with stale audio and runtime->delay would have counted frames the driver never copied. My second version shortened the packet instead, and that broke the URB because its declared length no longer matched the isochronous descriptor and vhci-hcd dropped the device.

The version that was applied keeps the packet length, copies at most one ring buffer of audio and fills the rest of the packet with silence, so the packet length, what copy_playback_data() copies and the delay accounting all agree. The diff below only shows the changed lines and the full patch is commit 493a77a9f20f.

493a77a9f20fdiff
@@ -285,13 +285,29 @@ static void playback_work(struct work_struct *work)
/* fill packet with data or silence */
urb->iso_frame_desc[0].length =
frames * ua->playback.frame_bytes;
- if (test_bit(ALSA_PLAYBACK_RUNNING, &ua->states))
+ if (test_bit(ALSA_PLAYBACK_RUNNING, &ua->states)) {
+ /*
+ * The packet size comes from the capture stream,
+ * i.e. from the device, while the ring buffer can
+ * be as small as two tiny periods. The single wrap
+ * in copy_playback_data() is only valid for
+ * frames <= buffer_size, so copy at most one
+ * buffer and send silence for the rest.
+ */
+ unsigned int copy_frames = min(frames,
+ ua->playback.substream->runtime->buffer_size);
+
do_period_elapsed |= copy_playback_data(&ua->playback,
urb,
- frames);
- else
+ copy_frames);
+ memset(urb->transfer_buffer +
+ copy_frames * ua->playback.frame_bytes, 0,
+ (frames - copy_frames) *
+ ua->playback.frame_bytes);
+ } else {
memset(urb->transfer_buffer, 0,
urb->iso_frame_desc[0].length);
+ }

I tested each version with capture and playback, and the final one ran both for 30 s with no KASAN report while hw_ptr advanced in both directions. My first version had already removed the out-of-bounds read but was still wrong, because the fault it left was in playback_work(), the function that calls copy_playback_data(). So a test that finishes without a KASAN report does not on its own prove that a fix is correct, and reading the calling function as well as the fixed one was worth the second build.

#Process

#Threat model

Before submitting the patches I checked how the kernel classes bugs like these in Documentation/process/threat-model.rst, which puts issues that need physical access to the machine out of scope and lists "bugs whose reproduction requires hardware modification or emulation" among those it does not treat as vulnerabilities.[5] My reproduction used an emulated device attached through USB/IP, which only root can do, and triggering the same bugs with a real device would mean plugging it into the machine, so I treated these as regular bugs. That meant no security list, no embargo and no CVE request. Documentation/process/security-bugs.rst also asks that a report posted to a public list hold the reproducer back until the maintainers ask for it[6], so the cover letter left it out and this post likewise shows the descriptors and the client setup but not the server.

Treating them as regular bugs only changed how I submitted the patches and not how widely they apply, because every device that snd-usb-audio drives still goes through retire_capture_urb() or copy_to_urb(), and the single wrap in both functions predates the git history, so both patches carry Cc: stable for every maintained tree. Physical access is also a real attack path for this driver, because CVE-2024-53197, an out-of-bounds access in snd-usb-audio triggered by a malicious USB device, was one of the three kernel USB bugs in the Cellebrite exploit chain that Amnesty International's Security Lab documented in February 2025.[10]

#Submission

After working out how the kernel classes these bugs, I submitted both patches to Jaroslav Kysela, Takashi Iwai and Clemens Ladisch, who are the maintainers that scripts/get_maintainer.pl named, with the linux-sound and linux-kernel mailing lists in copy. The [PATCH 0/2] cover letter held the analysis, the KASAN reports, the sound-server table and the results for the other drivers, and the table below shows how the series moved from my first tests to the merge.

dateevent
16 to 17 Oct 2026reproduction on 7.3-rc7, fixes written and verified
18 Octsound-server tests on the Fedora 42 root
23 Octua101 playback fix reworked and re-verified
26 Octseries submitted to the maintainers
27 Octboth patches applied to the sound tree, for-linus
31 Octmerged by Linus in sound-fix-7.4-rc1 (e9954f3fbddd)
16-17 Oct18 Oct23 Oct26 Oct27 Oct31 Oct reproduced on 7.3-rc7,fixes written sound-server tests ua101 playback fixreworked series sent applied to thesound tree merged by Linuse9954f3fbddd
Figure 7. From the first reproduction to the merge in 15 days.

#Conclusion

In this post I discussed how I found two out-of-bounds copies in the Linux USB audio drivers, how I reproduced them with an emulated USB/IP device, and how I fixed, tested and submitted the patches that are now in the 7.4 kernel. Both bugs came down to who sets each size. The device declares the packet size and the application sets the ring buffer size, and retire_capture_urb() assumed that a packet always fits in the ring buffer, while the only constraints that could have checked one against the other are in a different function, setup_hw_info(). So I searched sound/usb and drivers/media/usb for other functions that copy packets sized by the device into a ring buffer sized by the application, which is how I found the four other drivers.

The other thing these bugs showed me was a gap in test coverage. syzkaller's device emulation runs on dummy_hcd which cannot carry isochronous transfers, so as far as I can tell the continuous fuzzing of the USB stack has not reached these functions, and my USB/IP device server took only a few hundred lines of Python to give the kernel a real endpoint at any speed. The drivers behind that gap are where I would look next.

If you maintain a driver that copies packets sized by the device into a buffer sized by the application, put a bounds check before the memcpy() and then send it a packet larger than the ring buffer to see what happens.

#References

  1. Commit 06be918062ad, ALSA: usb-audio: Bound packet copies by the PCM buffer size
  2. Commit 493a77a9f20f, ALSA: ua101: Bound packet copies by the PCM buffer size
  3. Merge e9954f3fbddd, sound-fix-7.4-rc1
  4. Series on the list, [PATCH v2 0/2] ALSA: usb-audio, ua101: oversized packet copies overrun PCM ring buffers
  5. Documentation/process/threat-model.rst ↑
  6. Documentation/process/security-bugs.rst ↑
  7. USB/IP, Documentation/usb/usbip_protocol.rst
  8. syzkaller, docs/linux/external_fuzzing_usbip.md (USB/IP fuzzing) ↑
  9. USB Device Class Definition for Audio Devices 1.0, section 4.6.1.2 (class-specific isochronous audio data endpoint descriptor, MaxPacketsOnly) ↑
  10. Amnesty International Security Lab, "Cellebrite zero-day exploit used to target phone of Serbian student activist" (February 2025) ↑

Sinan Turunc

Founder of nandsec. Researches operating system security, memory safety and embedded systems.

AboutMore by this author
Found an error? Email [email protected]. Corrections are noted under the title.