[PATCH v7 1/2] media: uvcvideo: add UVC_QUIRK_CTRL_THROTTLE for fragile USB firmware

JP Hein
2026-04-10T00:28:58Z

Some UVC devices have firmware that locks up under sustained rapid
USB control transfers, crashing the xHCI host controller and taking
all USB devices on the bus with it.

The Razer Kiyo Pro (1532:0e05) is the first known example: approximately
25 rapid consecutive control transfers cause the firmware to stall an
endpoint.

Add UVC_QUIRK_CTRL_THROTTLE which rate-limits all USB control transfers
to 50ms intervals in __uvc_query_ctrl(), the lowest-level UVC control
transfer function, ensuring all callers are throttled including
uvc_set_video_ctrl() which bypasses uvc_query_ctrl().

The 50ms interval was determined experimentally: the device is stable
at this rate under sustained operation, while shorter intervals
eventually trigger the firmware bug.

Signed-off-by: JP Hein <jp@jphein.com>
---
drivers/media/usb/uvc/uvc_video.c | 20 ++++++++++++++++++++
drivers/media/usb/uvc/uvcvideo.h | 3 +++
2 files changed, 23 insertions(+)

diff --git a/drivers/media/usb/uvc/uvc_video.c b/drivers/media/usb/uvc/uvc_video.c
index a5013a7..1d1206f 100644
--- a/drivers/media/usb/uvc/uvc_video.c
+++ b/drivers/media/usb/uvc/uvc_video.c
@@ -36,6 +36,26 @@ static int __uvc_query_ctrl(struct uvc_device *dev, u8 query, u8 unit,
u8 type = USB_TYPE_CLASS | USB_RECIP_INTERFACE;
unsigned int pipe;

+ /*
+ * Rate-limit control transfers for devices with fragile firmware.
+ * The Razer Kiyo Pro locks up under sustained rapid control
+ * transfers (hundreds without delay), crashing the xHCI controller.
+ * Throttle in this low-level function to cover all callers,
+ * including uvc_set_video_ctrl() which bypasses uvc_query_ctrl().
+ */
+ if (dev->quirks & UVC_QUIRK_CTRL_THROTTLE) {
+ unsigned long min_interval = msecs_to_jiffies(50);
+
+ if (dev->last_ctrl_jiffies &&
+ time_before(jiffies,
+ dev->last_ctrl_jiffies + min_interval)) {
+ unsigned long wait = dev->last_ctrl_jiffies +
+ min_interval - jiffies;
+ msleep(jiffies_to_msecs(wait));
+ }
+ dev->last_ctrl_jiffies = jiffies;
+ }
+
pipe = (query & 0x80) ? usb_rcvctrlpipe(dev->udev, 0)
: usb_sndctrlpipe(dev->udev, 0);
type |= (query & 0x80) ? USB_DIR_IN : USB_DIR_OUT;
diff --git a/drivers/media/usb/uvc/uvcvideo.h b/drivers/media/usb/uvc/uvcvideo.h
index 757254f..31f2af5 100644
--- a/drivers/media/usb/uvc/uvcvideo.h
+++ b/drivers/media/usb/uvc/uvcvideo.h
@@ -78,6 +78,7 @@
#define UVC_QUIRK_INVALID_DEVICE_SOF 0x00010000
#define UVC_QUIRK_MJPEG_NO_EOF 0x00020000
#define UVC_QUIRK_MSXU_META 0x00040000
+#define UVC_QUIRK_CTRL_THROTTLE 0x00080000

/* Format flags */
#define UVC_FMT_FLAG_COMPRESSED 0x00000001
@@ -583,6 +584,8 @@ struct uvc_device {
struct usb_interface *intf;
unsigned long warnings;
u32 quirks;
+ /* UVC control transfer throttling (UVC_QUIRK_CTRL_THROTTLE) */
+ unsigned long last_ctrl_jiffies;
int intfnum;
char name[32];

--
2.43.0