Skip to content

Instantly share code, notes, and snippets.

@sebyx07
Created July 30, 2026 00:00
Show Gist options
  • Select an option

  • Save sebyx07/a6615156bce73f521f2b05dec6a93f07 to your computer and use it in GitHub Desktop.

Select an option

Save sebyx07/a6615156bce73f521f2b05dec6a93f07 to your computer and use it in GitHub Desktop.
SiGma Micro 1c4f:0202 (MK10) USB keyboard freezes on Linux when a media key is pressed — root cause (firmware returns 15 of the 79 report-descriptor bytes it declares) and a DKMS driver fix

SiGma Micro 1c4f:0202 USB keyboard locks up on Linux when a media key is pressed

Sold under various names — mine is an MK10 — these cheap keyboards work fine on Windows but hard-freeze on Linux the first time you press Volume Up / Volume Down. The whole keyboard dies, not just the media keys, and the only way back is to physically unplug and replug it.

Tested on Zorin OS (GNOME, PipeWire) with kernel 7.1.5, but nothing here is distro-specific.

Symptom

  • Press a media key (volume, play/pause, …) → keyboard stops responding entirely.
  • Normal keys are dead too. Caps Lock LED stops toggling.
  • journalctl -k shows absolutely nothing at the moment of the freeze — no -71, no -110, no reset, no disconnect. The device just goes silent.
  • Unplug + replug is the only recovery. A software USB reset is not enough; the controller needs to be power-cycled.
  • Works perfectly on Windows.

Root cause

The keyboard exposes two HID interfaces:

Interface Purpose Status on Linux
1.0 Normal keys ✅ valid descriptor, works
1.1 Consumer control (media keys) ❌ rejected with -EINVAL

At boot, every enumeration logs this:

hid-generic 0003:1C4F:0202.0009: unknown main item tag 0x0     (x10, "54 callbacks suppressed")
hid-generic 0003:1C4F:0202.0009: unbalanced collection at end of report description
hid-generic 0003:1C4F:0202.0009: probe with driver hid-generic failed with error -22

The firmware lies about its report descriptor. Interface 1's HID descriptor declares wDescriptorLength = 79, but the device only ever returns the first 15 bytes:

05 0c        Usage Page (Consumer)
09 01        Usage (Consumer Control)
a1 01        Collection (Application)
85 01        Report ID (1)
19 00        Usage Minimum (0)
2a 80 03     Usage Maximum (0x0380)
15 00        Logical Minimum (0)
             <-- data ends here, 64 bytes short

usbhid hands the full declared length to the parser regardless of how much actually arrived, so the remaining 64 bytes are read as zero padding — that's the run of unknown main item tag 0x0. The Collection (Application) is never closed, so the parse fails with -EINVAL.

The freeze follows from that failure. With no HID driver bound to interface 1, hid_hw_start() is never called, so usbhid never submits an interrupt URB to endpoint 0x82. The endpoint is never polled. Press a media key and the device queues a report that nobody ever drains; its buffer fills, the MCU wedges, and it takes the other interface down with it.

That's why the whole keyboard dies, why the kernel logs nothing (the host never had an error — the device simply stopped talking), and why only a power cycle recovers it.

Why Windows is fine

Interface 1 also declares bInterfaceSubClass 1 (Boot Interface) / bInterfaceProtocol 1 (Keyboard). Windows falls back to the boot protocol and keeps draining 0x82 without ever caring that the report descriptor is malformed. Linux insists on parsing the report descriptor first and bails out — so the endpoint goes unpolled.

Reproducing the diagnosis

Confirm the short read (unbind first, or lsusb reports ** UNAVAILABLE ** because usbhid holds the interface):

echo "3-3:1.1" | sudo tee /sys/bus/usb/drivers/usbhid/unbind    # adjust to your bus-port
sudo lsusb -d 1c4f:0202 -v | grep -A25 "bInterfaceNumber *1"

You should see:

wDescriptorLength      79
Warning: incomplete report descriptor
Report Descriptor: (length is 15)

79 declared, 15 returned is the bug in one line.

The fix

A ~90-line HID driver that substitutes the complete descriptor the device meant to send. The first 15 bytes are byte-for-byte what the device actually returns; the rest is the natural completion — a single 16-bit array field (Usage Maximum is 0x0380, so it cannot be narrower) plus the closing End Collection.

See hid-mk10.c in this gist.

The driver deliberately matches on both the declared size and the 15-byte prefix, so a device with fixed firmware — or a differently broken revision — is left untouched.

Install (DKMS)

sudo mkdir -p /usr/src/hid-mk10-1.0
sudo cp hid-mk10.c Makefile dkms.conf /usr/src/hid-mk10-1.0/

sudo dkms add     -m hid-mk10 -v 1.0
sudo dkms build   -m hid-mk10 -v 1.0
sudo dkms install -m hid-mk10 -v 1.0

Then replug the keyboard (or modprobe hid-mk10). DKMS rebuilds it automatically on kernel updates, and MODULE_DEVICE_TABLE means it auto-loads by modalias at boot.

Building against a clang/LLVM-built kernel (XanMod and similar) needs LLVM=1, which the included Makefile passes. Check with:

grep CONFIG_CC_IS_CLANG /boot/config-$(uname -r)

If your kernel was built with GCC, drop LLVM=1 from the Makefile.

⚠️ Do not write a dkms.conf whose MAKE line can expand to an empty M=. An empty M= turns the invocation into an in-tree build inside your kernel headers directory and clobbers its generated config files (include/generated/autoconf.h, rustc_cfg). If that happens, sudo apt install --reinstall linux-headers-$(uname -r) puts it back. The dkms.conf here routes everything through the Makefile's KDIR to avoid the trap.

Verifying it worked

hid-mk10 0003:1C4F:0202.000D: replacing truncated consumer descriptor (79 bytes claimed, 15 returned)
input: Usb KeyBoard as .../3-3:1.1/0003:1C4F:0202.000D/input/input20

Interface 1 now gets a real input node — it never had one before. Confirm the media keycodes are present:

sudo libinput list-devices | grep -A5 "Usb KeyBoard"
# or check that something (your compositor) holds it open, which is what makes usbhid poll 0x82:
sudo lsof /dev/input/eventN

Then press Volume Up. The volume changes and the keyboard keeps working.

Credits

Diagnosed and fixed with Claude Code.

Licensed GPL-2.0-or-later, same as the kernel.

PACKAGE_NAME="hid-mk10"
PACKAGE_VERSION="1.0"
BUILT_MODULE_NAME[0]="hid-mk10"
DEST_MODULE_LOCATION[0]="/updates/dkms"
MAKE[0]="make KVER=${kernelver} KDIR=${kernel_source_dir}"
CLEAN="make clean KVER=${kernelver} KDIR=${kernel_source_dir}"
AUTOINSTALL="yes"
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* HID driver for SiGma Micro 1c4f:0202 keyboards (sold as "MK10" and others).
*
* The consumer-control interface of these boards advertises a 79-byte report
* descriptor but only ever returns the first 15 bytes of it. usbhid hands the
* full declared length to the parser regardless, so the tail is read as zero
* padding: the Application collection is never closed and hid-generic rejects
* the interface with -EINVAL ("unbalanced collection at end of report
* description", preceded by a run of "unknown main item tag 0x0").
*
* With no driver bound, the interface's interrupt endpoint is never polled.
* The first media keypress queues a report that nobody drains, which wedges the
* controller and takes the keyboard's other interface down with it until the
* device is physically re-plugged. Windows does not hit this because the
* interface also declares the boot keyboard subclass, so it keeps draining the
* endpoint without ever parsing the broken descriptor.
*
* Substitute the complete descriptor the device was meant to send.
*/
#include <linux/hid.h>
#include <linux/module.h>
#include <linux/string.h>
#include <linux/usb.h>
#define USB_VENDOR_ID_SIGMA_MICRO 0x1c4f
#define USB_DEVICE_ID_SIGMA_MICRO_MK10 0x0202
/* Length the device claims in its HID descriptor. */
#define MK10_CLAIMED_RDESC_SIZE 79
/* Length it actually returns; also the length of the prefix we match on. */
#define MK10_ACTUAL_RDESC_SIZE 15
/*
* The first MK10_ACTUAL_RDESC_SIZE bytes below are byte-for-byte what the
* device does send. The remainder is the natural completion of it: a single
* 16-bit array field (Usage Maximum is 0x0380, so the field cannot be narrower)
* and the closing End Collection.
*/
static const __u8 mk10_consumer_rdesc[] = {
0x05, 0x0c, /* Usage Page (Consumer) */
0x09, 0x01, /* Usage (Consumer Control) */
0xa1, 0x01, /* Collection (Application) */
0x85, 0x01, /* Report ID (1) */
0x19, 0x00, /* Usage Minimum (0) */
0x2a, 0x80, 0x03, /* Usage Maximum (0x0380) */
0x15, 0x00, /* Logical Minimum (0) */
0x26, 0x80, 0x03, /* Logical Maximum (0x0380) */
0x75, 0x10, /* Report Size (16) */
0x95, 0x01, /* Report Count (1) */
0x81, 0x00, /* Input (Data, Array, Absolute) */
0xc0 /* End Collection */
};
static const __u8 *mk10_report_fixup(struct hid_device *hdev, __u8 *rdesc,
unsigned int *rsize)
{
struct usb_interface *intf;
if (!hid_is_usb(hdev))
return rdesc;
intf = to_usb_interface(hdev->dev.parent);
/* Interface 0 is the ordinary keyboard; its descriptor is valid. */
if (intf->cur_altsetting->desc.bInterfaceNumber != 1)
return rdesc;
/*
* Only rewrite the descriptor we know is broken. Matching the prefix
* as well as the size means a device with fixed firmware, or a
* differently broken revision, is left untouched.
*/
if (*rsize != MK10_CLAIMED_RDESC_SIZE ||
memcmp(rdesc, mk10_consumer_rdesc, MK10_ACTUAL_RDESC_SIZE))
return rdesc;
hid_info(hdev,
"replacing truncated consumer descriptor (%u bytes claimed, %u returned)\n",
*rsize, MK10_ACTUAL_RDESC_SIZE);
*rsize = sizeof(mk10_consumer_rdesc);
return mk10_consumer_rdesc;
}
static const struct hid_device_id mk10_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_SIGMA_MICRO,
USB_DEVICE_ID_SIGMA_MICRO_MK10) },
{ }
};
MODULE_DEVICE_TABLE(hid, mk10_devices);
static struct hid_driver mk10_driver = {
.name = "hid-mk10",
.id_table = mk10_devices,
.report_fixup = mk10_report_fixup,
};
module_hid_driver(mk10_driver);
MODULE_DESCRIPTION("HID driver for SiGma Micro 1c4f:0202 (MK10) keyboards");
MODULE_LICENSE("GPL");
KVER ?= $(shell uname -r)
KDIR ?= /lib/modules/$(KVER)/build
obj-m := hid-mk10.o
all:
$(MAKE) -C $(KDIR) M=$(CURDIR) LLVM=1 modules
clean:
$(MAKE) -C $(KDIR) M=$(CURDIR) LLVM=1 clean
.PHONY: all clean
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment