May 30, 2026

Recording 4K60 on Linux is now easy (my first kernel patch)

Recording 4K60 on Linux used to be a mess. As of Linux 6.19 you plug in an Elgato 4K X, open OBS, and get clean 4K60 with the in-kernel driver and no extra software. What fixed it is a small USB quirk i got into the kernel.

i needed this for my daughter Karolina’s YouTube channel, Karo’s Gaming World, which records clean 4K60 Nintendo gameplay. Shameless plug: if you like that kind of thing, leave her a sub. :)

4K capture on Linux has been painful in general. USB UVC capture devices often enumerate wrong or expose the wrong modes, and PCIe capture cards lean on out-of-tree, reverse-engineered drivers from GitHub that you build and maintain yourself. The Elgato 4K X is a USB UVC device, so it should work with the in-kernel uvcvideo driver and nothing else. It didn’t, because of the USB BOS descriptor.

The card hangs during enumeration at SuperSpeed Plus (10Gbps), drops to plain SuperSpeed (5Gbps), and re-attaches under a different product ID. On the slower link it only offers up to 4K30; its 4K60 modes come back only on the full 10Gbps link. Skipping the BOS request keeps it on SuperSpeed Plus, and 4K60 works on a mainline kernel with the in-tree driver.

Elgato 4K X

The first confusing part is that the Elgato 4K X reports a different idProduct depending on the speed it ends up at:

  • 0fd9:009b at SuperSpeed Plus (Gen 2x1, 10Gbps), the working state, 4K60
  • 0fd9:009c at plain SuperSpeed (5Gbps), after a failed BOS read forces a re-enumeration, capped at 4K30

The ID tells you whether the card stayed on the 10Gbps link or fell back.

Here is the failure at SuperSpeed Plus:

1[    3.284990] usb 2-2: new SuperSpeed Plus Gen 2x1 USB device number 2 using xhci_hcd
2[    8.574542] usb 2-2: unable to get BOS descriptor or descriptor too short
3[    8.600018] usb 2-2: unable to read config index 0 descriptor/start: -71
4[    8.600027] usb 2-2: can't read configurations, error -71
5[    8.998412] usb 2-2: Device not responding to setup address.
6[    9.422737] usb 2-2: device not accepting address 3, error -71
7[   10.990897] usb 2-2: new SuperSpeed USB device number 5 using xhci_hcd
8[   11.152244] usb 2-2: New USB device found, idVendor=0fd9, idProduct=009c

The link trains at 10Gbps, the kernel asks for the BOS descriptor, the device stops responding, and after a few seconds of retries it gives up and re-attaches as 009c at 5Gbps. The BOS read is the first transfer to fail; the config-descriptor and set-address errors after it are downstream of that, not separate problems.

The BOS descriptor#

BOS is the Binary Device Object Store. It carries the SuperSpeed and SuperSpeedPlus device capability descriptors, LPM support, and similar information. The kernel reads it early in enumeration via usb_get_bos_descriptor().

This particular device hangs on that request, but only at 10Gbps. After enumeration it answers BOS requests from lsusb normally. So it expects something to happen before it can produce the descriptor at SuperSpeed Plus, and Linux issues the request in a different order than the device firmware was tested against.

This is a device bug, not a Linux bug. The same card works on Windows, where enumeration happens to not hit the device in the order that triggers the hang. Nobody captured the exact sequence Windows uses, so the device-side root cause stays unknown. The BOS descriptor is also not essential to operate the device. The kernel reads it for SuperSpeedPlus capability and LPM information, so skipping it for one broken device costs nothing in practice. The quirk acknowledges that the device is at fault and works around it.

The fix#

Skip the BOS request for devices that can’t handle it. A new quirk flag, gated in usb_get_bos_descriptor():

1/* skip BOS descriptor request */
2#define USB_QUIRK_NO_BOS			BIT(17)
1int usb_get_bos_descriptor(struct usb_device *dev)
2{
3	...
4	if (dev->quirks & USB_QUIRK_NO_BOS) {
5		dev_dbg(ddev, "skipping BOS descriptor\n");
6		return -ENOMSG;
7	}
8	...

And the device in the quirk table:

1/* Elgato 4K X - BOS descriptor fetch hangs at SuperSpeed Plus */
2{ USB_DEVICE(0x0fd9, 0x009b), .driver_info = USB_QUIRK_NO_BOS },

The table entry matches 009b, the ID the device presents while it is attempting the SuperSpeed Plus enumeration. That is the only point where the quirk can act, because the BOS request happens during that attempt. With the request skipped, enumeration completes and the device stays at 10Gbps:

1[    3.297159] usb 2-2: new SuperSpeed Plus Gen 2x1 USB device number 2 using xhci_hcd
2[    3.354248] usb 2-2: skipping BOS descriptor
3[    3.432917] usb 2-2: New USB device found, idVendor=0fd9, idProduct=009b
4[    3.432927] usb 2-2: Product: Elgato 4K X

Upstreaming#

The main concern in review was that adding devices to a skip-the-spec quirk invites a pile of future entries for hardware that is doing something odd USB-IF testing doesn’t catch. That is fair, and it is also why the quirk is opt-in per device rather than a blanket behavior change. The pile did show up, because the same capture chip appears in a lot of cards under different vendor and product IDs. Other people have since extended the quirk: a batch of three from one contributor (ASUS TUF 4K PRO, Avermedia Live Gamer Ultra 2.1, UGREEN 35871), and the ezcap401 after that. Each is the same BOS hang at 10Gbps, fixed by one more line in the table. A card that hangs the same way but is not yet listed needs its IDs added there; there is no usbcore.quirks= boot-parameter form for NO_BOS, so it takes a one-line patch, not a runtime toggle.

This was my first kernel patch, and Greg Kroah-Hartman made it painless. He was patient and helpful throughout, and he bought an Elgato 4K X to reproduce the hang rather than take my word for it. Whose budget that came from, his own or his employer’s, does not matter; spending real money to verify a stranger’s first patch is more care than i expected. He took it from the first post to mainline and four stable trees in about a month. The kernel contribution process has a reputation for being hostile, and none of that matched my experience. Thanks, Greg.

Releases#

The commit is 2740ac33. It shipped in mainline Linux 6.19 and was backported to the 6.1, 6.6, 6.12, and 6.18 stable trees, so any kernel on a maintained stable branch has it.

Check that it is active on a running system. A capture card binds to the uvcvideo driver, so its USB device node is listed under that driver in sysfs, no vendor ID needed:

1$ ls /sys/bus/usb/drivers/uvcvideo/
22-1:1.0  2-1:1.1  bind  module  new_id  remove_id  uevent  unbind

2-1:1.0 is an interface; the USB device node is the part before the colon, 2-1. Read its quirk flags:

1$ cat /sys/bus/usb/devices/2-1/quirks
20x20000

0x20000 is BIT(17), the NO_BOS flag, so the quirk is applied. It reads the same for any of the affected cards. lsusb -t shows the device at 10000M (SuperSpeed Plus) with Driver=uvcvideo, and v4l2-ctl -d /dev/video0 --list-formats-ext lists 3840x2160 under YUYV, MJPG and NV12. OBS then offers the full resolution:

Elgato 4K X at 3840x2160 60fps in OBS

This is why Linux is great#

Elgato has not shipped a Linux fix; the card is sold for Windows and macOS. Everything that made it work on Linux came from outside the vendor. Someone filed the bug report, people on a Reddit thread worked out that the card stubbornly fell back to 5Gbps, i took it from there and wrote the quirk, and other people extended it to more cards afterward.

A quirk is not rocket science. It is a few lines and a device ID. But i did not have to wait for the vendor or a release cycle: the source was there, the bug was understood, and the fix was one patch. That is the part open source gets right. Anyone can make the thing a little bit better, and the next person builds on it. The credit here belongs to the community: the original bug reporter, the Reddit thread, and Greg.

References#