[PATCH v2] ALSA: usb-audio: reset a MIDI device whose input is stuck
From: Stjepan Poljak
Date: Tue Oct 27 2026 - 11:32:54 EST
Switching a bus-powered USB keyboard off and on can be too quick for the
hub to report a disconnect. The device comes back unconfigured at the old
address, and its input endpoint then fails forever:
- with -EPIPE, which snd_usbmidi_urb_error() logs and treats as
"continue", so the URBs are resubmitted at once and the log fills
with "urb status -32" every few milliseconds;
- or with -EPROTO/-ETIME/-EILSEQ, retried silently every 100 ms.
Either way the rawmidi device stays, reads never fail and no data ever
arrives, so the instrument goes silent until the cable is replugged.
Retry a stalled input endpoint through the error timer like the other
errors, and when input stalls, or keeps failing for 2 s, queue a USB
reset. Failing means that every retry fails: a successful URB, or a
quiet spell of over 500 ms between errors, ends the streak, so isolated
errors on an idle device never add up to a reset. snd-usb-audio has no
pre_reset or post_reset, so the reset unbinds and re-probes the card,
and userspace sees an ordinary unplug and replug. A real unplug normally
reaches disconnect() long before 2 s. If the host misses that as well,
as dwc_otg does behind a hub, the input stalls and the reset finds no
device, which ends in the same disconnect.
The queued reset is dropped without a word if usb_lock_device_for_reset()
fails (e.g. -EBUSY when the device lock is held for over a second), so
while input keeps failing, queue another one every 2 s. Keep that limit
per device rather than per endpoint, as several input endpoints may fail
at once, and a second reset queued while the first one runs would reset
the re-probed device again.
Output endpoints are left as they are.
Tested on a Raspberry Pi 3B+ (dwc_otg, Raspberry Pi 6.12.61 kernel) with
a Nektar Impact LX49+ (2467:2015) behind the on-board hub. Without this
patch, switching the keyboard off and on left it silent, with "urb
status -32" logged every few milliseconds. With it, each off/on cycle
logged a single "MIDI input not recovering (urb status -32), resetting
device", and the keyboard was disconnected, re-enumerated as soon as it
was back on and playing again, including two cycles 9 s apart and one
where it stayed off for 3 minutes. Unplugging the cable went the same
way: the input stalled, the reset found no device and disconnected it,
and plugging it back in enumerated it normally. No warnings while
playing normally over about three hours.
Also tested on x86_64 (xHCI, 7.0 kernel): there the host sees both the
off/on cycle and an unplug as ordinary disconnects, so no reset is
queued and input resumes after re-enumeration as before.
On both, three fake -EPROTO completions 10 s apart on the idle keyboard
(a pending input URB unlinked by test code, not part of this patch) were
retried and caused no reset.
Assisted-by: LLM
Signed-off-by: Stjepan Poljak <stjepan.poljak@xxxxxxxxx>
---
v2:
- A quiet spell of over 500 ms between input errors now starts a new
failing streak. In v1 only a successful URB ended one, and an idle
device completes none, so two isolated errors far apart counted as
failing for the whole time in between and reset the device. Reproduced
on x86_64 by faking -EPROTO completions 10 s apart on an idle
Impact LX49+: v1 reset the device on the second one ("streak" of
10240 ms), v2 retried all three without a reset. (Sashiko review)
- The 2 s reset limit is now per device rather than per input endpoint,
so a second failing endpoint can't queue another reset while the first
one runs. (Sashiko review) Not tested: this keyboard has a single
input endpoint.
- Retested with v2 on the Pi: four off/on cycles, each reset and
recovered, and the fake errors above. The unplug, 3-minute-off and
x86_64 off/on and unplug results are from v1; those paths only see
errors 100 ms apart or a stall, which v2 handles the same way.
v1: https://lore.kernel.org/linux-sound/20261026125751.1700779-1-stjepan.poljak@xxxxxxxxx/
sound/usb/midi.c | 56 +++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 55 insertions(+), 1 deletion(-)
diff --git a/sound/usb/midi.c b/sound/usb/midi.c
index e3d157554061..488dede19e45 100644
--- a/sound/usb/midi.c
+++ b/sound/usb/midi.c
@@ -70,6 +70,18 @@
*/
#define ERROR_DELAY_JIFFIES (HZ / 10)
+/*
+ * how long input may keep failing before we reset the device (a real unplug
+ * reaches disconnect() well before this), and the least time between resets
+ */
+#define RESET_DELAY_JIFFIES (2 * HZ)
+
+/*
+ * a stuck endpoint fails again on every retry; an error after a longer
+ * quiet spell starts a new streak
+ */
+#define ERROR_GAP_JIFFIES (HZ / 2)
+
#define OUTPUT_URBS 7
#define INPUT_URBS 7
@@ -99,6 +111,8 @@ struct snd_usb_midi {
const struct usb_protocol_ops *usb_protocol_ops;
struct list_head list;
struct timer_list error_timer;
+ unsigned long reset_at; /* when we last queued a reset */
+ u8 reset_queued;
spinlock_t disc_lock;
struct rw_semaphore disc_rwsem;
struct mutex mutex;
@@ -162,6 +176,9 @@ struct snd_usb_midi_in_endpoint {
bool in_sysex;
u8 last_cin;
u8 error_resubmit;
+ u8 failing; /* no successful URB since the last error */
+ unsigned long failing_since;
+ unsigned long last_error;
int current_port;
};
@@ -234,6 +251,40 @@ static void dump_urb(const char *type, const u8 *data, int length)
#define dump_urb(type, data, length) /* nothing */
#endif
+/*
+ * A device that was power cycled without the hub noticing (e.g. a bus
+ * powered keyboard switched off and on again) has lost its configuration:
+ * it stalls the input endpoint or stops answering, and resubmitting the
+ * URBs never recovers it. Reset it instead; snd-usb-audio has no
+ * pre_reset/post_reset, so this unbinds and re-probes the card.
+ * A queued reset is skipped if usb_lock_device_for_reset() fails, so if
+ * we're still here a while later, ask again.
+ */
+static void snd_usbmidi_in_error(struct snd_usb_midi_in_endpoint *ep,
+ int status)
+{
+ struct snd_usb_midi *umidi = ep->umidi;
+
+ if (!ep->failing ||
+ time_after(jiffies, ep->last_error + ERROR_GAP_JIFFIES))
+ ep->failing_since = jiffies;
+ ep->failing = 1;
+ ep->last_error = jiffies;
+ /* per device, as several endpoints may fail at once */
+ if (umidi->reset_queued &&
+ time_before(jiffies, umidi->reset_at + RESET_DELAY_JIFFIES))
+ return;
+ if (status != -EPIPE &&
+ time_before(jiffies, ep->failing_since + RESET_DELAY_JIFFIES))
+ return;
+ umidi->reset_queued = 1;
+ umidi->reset_at = jiffies;
+ dev_warn(&umidi->dev->dev,
+ "MIDI input not recovering (urb status %d), resetting device\n",
+ status);
+ usb_queue_reset_device(umidi->iface);
+}
+
/*
* Processes the data read from the device.
*/
@@ -242,13 +293,16 @@ static void snd_usbmidi_in_urb_complete(struct urb *urb)
struct snd_usb_midi_in_endpoint *ep = urb->context;
if (urb->status == 0) {
+ ep->failing = 0;
dump_urb("received", urb->transfer_buffer, urb->actual_length);
ep->umidi->usb_protocol_ops->input(ep, urb->transfer_buffer,
urb->actual_length);
} else {
- int err = snd_usbmidi_urb_error(urb);
+ /* a stalled endpoint stays stalled: don't resubmit at once */
+ int err = urb->status == -EPIPE ? -EPIPE : snd_usbmidi_urb_error(urb);
if (err < 0) {
if (err != -ENODEV) {
+ snd_usbmidi_in_error(ep, urb->status);
ep->error_resubmit = 1;
mod_timer(&ep->umidi->error_timer,
jiffies + ERROR_DELAY_JIFFIES);
base-commit: a4311e9447bbb219044db38897f95be112f46a8a
--
2.53.0