[PATCH AUTOSEL 6.19-5.15] ALSA: usb-audio: Add sanity check for OOB writes at silencing

From: Sasha Levin

Date: Mon Feb 23 2026 - 07:40:11 EST


From: Takashi Iwai <tiwai@xxxxxxx>

[ Upstream commit fba2105a157fffcf19825e4eea498346738c9948 ]

At silencing the playback URB packets in the implicit fb mode before
the actual playback, we blindly assume that the received packets fit
with the buffer size. But when the setup in the capture stream
differs from the playback stream (e.g. due to the USB core limitation
of max packet size), such an inconsistency may lead to OOB writes to
the buffer, resulting in a crash.

For addressing it, add a sanity check of the transfer buffer size at
prepare_silent_urb(), and stop the data copy if the received data
overflows. Also, report back the transfer error properly from there,
too.

Note that this doesn't fix the root cause of the playback error
itself, but this merely covers the kernel Oops.

Link: https://bugzilla.kernel.org/show_bug.cgi?id=221076
Link: https://patch.msgid.link/20260216141209.1849200-4-tiwai@xxxxxxx
Signed-off-by: Takashi Iwai <tiwai@xxxxxxx>
Signed-off-by: Sasha Levin <sashal@xxxxxxxxxx>
---

LLM Generated explanations, may be completely bogus:

## Analysis

### What the commit fixes

This commit adds bounds checking to `prepare_silent_urb()` in the USB
audio driver to prevent **out-of-bounds memory writes** that cause a
**kernel panic/oops**. The bug occurs during implicit feedback mode when
silencing playback URB packets before actual playback begins. When the
capture stream has different (larger) packet sizes than the playback
stream (e.g., due to USB core max packet size limitations), the code
writes beyond the allocated transfer buffer, corrupting memory and
crashing the kernel.

### Bug severity

This is a **kernel crash/oops** triggered by real hardware. The bugzilla
report (bug #221076) documents:
- Hard system lockups / kernel panics on multiple platforms (Intel, AMD,
Raspberry Pi 5)
- Reproducible with specific USB audio devices at high sample rates
(352.8kHz/384kHz, 6-channel)
- The crash trace shows `memset_orig` called from `prepare_outbound_urb`
→ page fault on not-present page
- The regression was traced back to kernel 5.8.0, meaning it has
affected users for years

### Code change analysis

The fix is small (22 insertions, 17 deletions, single file) and
surgical:

1. **Return type change**: `prepare_silent_urb()` changes from `void` to
`int` to report errors
2. **Negative return check**: Checks if
`snd_usb_endpoint_next_packet_size()` returns a negative error code
before using the value
3. **Buffer bounds check**: `if (offs + length + extra >
ctx->buffer_size) break;` — stops copying if data would overflow the
buffer
4. **Error propagation**: Returns `-EPIPE` if nothing was written, `0`
on success
5. **Caller updated**: `prepare_outbound_urb()` now returns the error
from `prepare_silent_urb()` instead of ignoring it

### Stable kernel criteria assessment

- **Obviously correct**: Yes — adds bounds checking before memory
writes, a straightforward safety measure
- **Fixes a real bug**: Yes — kernel oops/panic with real hardware,
documented in bugzilla with crash traces
- **Small and contained**: Yes — 22 insertions/17 deletions in a single
file, no API changes
- **No new features**: Correct — purely defensive checks
- **Risk**: Very low — the check only prevents writing beyond the
buffer; normal operation is unchanged

### Dependencies

The commit modifies only `prepare_silent_urb()` and its caller in
`prepare_outbound_urb()`. The `ctx->buffer_size` field has been present
since the URB context structure was established. No prerequisite commits
appear needed.

### Verification

- Confirmed via the bugzilla report (bug #221076) that this is a real
user-reported crash with kernel oops trace showing `memset_orig` page
fault in `prepare_outbound_urb` call path
- The bug has been present since kernel 5.8.0 per user bisection,
meaning all stable trees are affected
- Explored `snd_usb_endpoint_next_packet_size()` — confirmed it can
return negative (`-EAGAIN`), which was not checked before this fix
- Confirmed `ctx->buffer_size` is set in `data_ep_set_params()` as
`maxsize * packets`, and the transfer buffer is allocated to that size
— so the bounds check is valid
- The commit is authored by Takashi Iwai (ALSA maintainer), providing
high confidence in correctness
- The commit message explicitly states it covers a kernel Oops — a clear
stable-worthy fix
- Commit touches only `sound/usb/endpoint.c` (1 file, small diff)

This is a textbook stable backport candidate: a small, surgical fix for
a kernel crash affecting real users, authored by the subsystem
maintainer, with low regression risk.

**YES**

sound/usb/endpoint.c | 39 ++++++++++++++++++++++-----------------
1 file changed, 22 insertions(+), 17 deletions(-)

diff --git a/sound/usb/endpoint.c b/sound/usb/endpoint.c
index 27ade2aa16f5a..1eaf52d1ae9c7 100644
--- a/sound/usb/endpoint.c
+++ b/sound/usb/endpoint.c
@@ -275,8 +275,8 @@ static inline bool has_tx_length_quirk(struct snd_usb_audio *chip)
return chip->quirk_flags & QUIRK_FLAG_TX_LENGTH;
}

-static void prepare_silent_urb(struct snd_usb_endpoint *ep,
- struct snd_urb_ctx *ctx)
+static int prepare_silent_urb(struct snd_usb_endpoint *ep,
+ struct snd_urb_ctx *ctx)
{
struct urb *urb = ctx->urb;
unsigned int offs = 0;
@@ -289,28 +289,34 @@ static void prepare_silent_urb(struct snd_usb_endpoint *ep,
extra = sizeof(packet_length);

for (i = 0; i < ctx->packets; ++i) {
- unsigned int offset;
- unsigned int length;
- int counts;
-
- counts = snd_usb_endpoint_next_packet_size(ep, ctx, i, 0);
- length = counts * ep->stride; /* number of silent bytes */
- offset = offs * ep->stride + extra * i;
- urb->iso_frame_desc[i].offset = offset;
+ int length;
+
+ length = snd_usb_endpoint_next_packet_size(ep, ctx, i, 0);
+ if (length < 0)
+ return length;
+ length *= ep->stride; /* number of silent bytes */
+ if (offs + length + extra > ctx->buffer_size)
+ break;
+ urb->iso_frame_desc[i].offset = offs;
urb->iso_frame_desc[i].length = length + extra;
if (extra) {
packet_length = cpu_to_le32(length);
- memcpy(urb->transfer_buffer + offset,
+ memcpy(urb->transfer_buffer + offs,
&packet_length, sizeof(packet_length));
+ offs += extra;
}
- memset(urb->transfer_buffer + offset + extra,
+ memset(urb->transfer_buffer + offs,
ep->silence_value, length);
- offs += counts;
+ offs += length;
}

- urb->number_of_packets = ctx->packets;
- urb->transfer_buffer_length = offs * ep->stride + ctx->packets * extra;
+ if (!offs)
+ return -EPIPE;
+
+ urb->number_of_packets = i;
+ urb->transfer_buffer_length = offs;
ctx->queued = 0;
+ return 0;
}

/*
@@ -332,8 +338,7 @@ static int prepare_outbound_urb(struct snd_usb_endpoint *ep,
if (data_subs && ep->prepare_data_urb)
return ep->prepare_data_urb(data_subs, urb, in_stream_lock);
/* no data provider, so send silence */
- prepare_silent_urb(ep, ctx);
- break;
+ return prepare_silent_urb(ep, ctx);

case SND_USB_ENDPOINT_TYPE_SYNC:
if (snd_usb_get_speed(ep->chip->dev) >= USB_SPEED_HIGH) {
--
2.51.0