From 15a0449c955c862045c17f35d9479791981e7812 Mon Sep 17 00:00:00 2001 From: Karl Cayme Date: Sat, 21 Mar 2026 20:42:49 +0800 Subject: [PATCH 01/39] HID: rakk: add support for Rakk Dasig X side buttons The Rakk Dasig X gaming mouse has a faulty HID report descriptor that declares USAGE_MAXIMUM=3 (buttons 1-3) while actually sending 5 button bits (REPORT_COUNT=5). This causes the kernel to ignore side buttons (buttons 4 and 5). Fix by patching the descriptor to set USAGE_MAXIMUM=5 in the report_fixup callback. The mouse uses Telink vendor ID 0x248a with three product IDs for USB direct (0xfb01), wireless dongle (0xfa02), and Bluetooth (0x8266) connection modes. All three variants have the same bug at byte offset 17. Suggested-by: Terry Junge Signed-off-by: Karl Cayme Signed-off-by: Jiri Kosina --- drivers/hid/Kconfig | 9 +++++ drivers/hid/Makefile | 1 + drivers/hid/hid-ids.h | 5 +++ drivers/hid/hid-rakk.c | 75 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 drivers/hid/hid-rakk.c diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index ff2f580b660b..0fdb794cea24 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -766,6 +766,15 @@ config HID_MEGAWORLD_FF Say Y here if you have a Mega World based game controller and want to have force feedback support for it. +config HID_RAKK + tristate "Rakk support" + help + Support for Rakk gaming peripherals. + + Fixes the HID report descriptor of the Rakk Dasig X mouse, + which declares USAGE_MAXIMUM=3 (buttons 1-3) while actually + sending 5 button bits. This causes side buttons to be ignored. + config HID_REDRAGON tristate "Redragon keyboards" help diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index 0597fd6a4ffd..dd7ba9db01ae 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -115,6 +115,7 @@ obj-$(CONFIG_HID_PLANTRONICS) += hid-plantronics.o obj-$(CONFIG_HID_PLAYSTATION) += hid-playstation.o obj-$(CONFIG_HID_PRIMAX) += hid-primax.o obj-$(CONFIG_HID_PXRC) += hid-pxrc.o +obj-$(CONFIG_HID_RAKK) += hid-rakk.o obj-$(CONFIG_HID_RAPOO) += hid-rapoo.o obj-$(CONFIG_HID_RAZER) += hid-razer.o obj-$(CONFIG_HID_REDRAGON) += hid-redragon.o diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 0cf63742315b..847889d63276 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -1401,6 +1401,11 @@ #define USB_DEVICE_ID_SYNAPTICS_ACER_SWITCH5_017 0x73f6 #define USB_DEVICE_ID_SYNAPTICS_ACER_SWITCH5 0x81a7 +#define USB_VENDOR_ID_TELINK 0x248a +#define USB_DEVICE_ID_TELINK_RAKK_DASIG_X 0xfb01 +#define USB_DEVICE_ID_TELINK_RAKK_DASIG_X_DONGLE 0xfa02 +#define USB_DEVICE_ID_TELINK_RAKK_DASIG_X_BT 0x8266 + #define USB_VENDOR_ID_TEXAS_INSTRUMENTS 0x2047 #define USB_DEVICE_ID_TEXAS_INSTRUMENTS_LENOVO_YOGA 0x0855 diff --git a/drivers/hid/hid-rakk.c b/drivers/hid/hid-rakk.c new file mode 100644 index 000000000000..1140058a76ca --- /dev/null +++ b/drivers/hid/hid-rakk.c @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * HID driver for Rakk devices + * + * Copyright (c) 2026 Karl Cayme + * + * The Rakk Dasig X gaming mouse has a faulty HID report descriptor that + * declares USAGE_MAXIMUM = 3 (buttons 1-3) while actually sending 5 button + * bits (REPORT_COUNT = 5). This causes the kernel to ignore side buttons + * (buttons 4 and 5). This driver fixes the descriptor so all 5 buttons + * are properly recognized across 3 modes (wired, dongle, and Bluetooth). + */ + +#include +#include +#include +#include "hid-ids.h" + +/* + * The faulty byte is at offset 17 in the report descriptor for all three + * connection modes (USB direct, wireless dongle, and Bluetooth). + * + * Bytes 16-17 are: 0x29 0x03 (USAGE_MAXIMUM = 3) + * The fix changes byte 17 to 0x05 (USAGE_MAXIMUM = 5). + * + * Original descriptor bytes 0-17: + * 05 01 09 02 a1 01 85 01 09 01 a1 00 05 09 19 01 29 03 + * ^^ + * Should be 0x05 to declare 5 buttons instead of 3. + */ +#define RAKK_RDESC_USAGE_MAX_OFFSET 17 +#define RAKK_RDESC_USAGE_MAX_ORIG 0x03 +#define RAKK_RDESC_USAGE_MAX_FIXED 0x05 +#define RAKK_RDESC_USB_SIZE 193 +#define RAKK_RDESC_DONGLE_SIZE 150 +#define RAKK_RDESC_BT_SIZE 89 + +static const __u8 *rakk_report_fixup(struct hid_device *hdev, __u8 *rdesc, + unsigned int *rsize) +{ + if (((*rsize == RAKK_RDESC_USB_SIZE && + hdev->product == USB_DEVICE_ID_TELINK_RAKK_DASIG_X) || + (*rsize == RAKK_RDESC_DONGLE_SIZE && + hdev->product == USB_DEVICE_ID_TELINK_RAKK_DASIG_X_DONGLE) || + (*rsize == RAKK_RDESC_BT_SIZE && + hdev->product == USB_DEVICE_ID_TELINK_RAKK_DASIG_X_BT)) && + rdesc[RAKK_RDESC_USAGE_MAX_OFFSET] == RAKK_RDESC_USAGE_MAX_ORIG) { + hid_info(hdev, "fixing Rakk Dasig X button count (3 -> 5)\n"); + rdesc[RAKK_RDESC_USAGE_MAX_OFFSET] = RAKK_RDESC_USAGE_MAX_FIXED; + } + + return rdesc; +} + +static const struct hid_device_id rakk_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_TELINK, + USB_DEVICE_ID_TELINK_RAKK_DASIG_X) }, + { HID_USB_DEVICE(USB_VENDOR_ID_TELINK, + USB_DEVICE_ID_TELINK_RAKK_DASIG_X_DONGLE) }, + { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_TELINK, + USB_DEVICE_ID_TELINK_RAKK_DASIG_X_BT) }, + { } +}; +MODULE_DEVICE_TABLE(hid, rakk_devices); + +static struct hid_driver rakk_driver = { + .name = "rakk", + .id_table = rakk_devices, + .report_fixup = rakk_report_fixup, +}; +module_hid_driver(rakk_driver); + +MODULE_DESCRIPTION("HID driver for Rakk Dasig X mouse - fix side button support"); +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Karl Cayme"); From bf29bafe3eaa8b8582407967755dbcd940a30287 Mon Sep 17 00:00:00 2001 From: Aaron Webster Date: Mon, 6 Apr 2026 21:40:08 -0700 Subject: [PATCH 02/39] HID: playstation: Add DualSense Edge extra button support The DualSense Edge controller (product ID 0x0df2) has four additional buttons compared to the standard DualSense: two front function buttons (Fn1 and Fn2) and two rear paddles (left and right). These are reported in bits 4-7 of buttons[2] in the input report. Map them to BTN_TRIGGER_HAPPY1 through BTN_TRIGGER_HAPPY4 so that userspace applications can use these extra inputs. An is_edge flag gates the extra button handling based on the product ID. Signed-off-by: Aaron Webster Signed-off-by: Jiri Kosina --- drivers/hid/hid-playstation.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/drivers/hid/hid-playstation.c b/drivers/hid/hid-playstation.c index c43caac20b61..bc8cb2b89fa5 100644 --- a/drivers/hid/hid-playstation.c +++ b/drivers/hid/hid-playstation.c @@ -112,6 +112,12 @@ struct ps_led_info { #define DS_BUTTONS2_TOUCHPAD BIT(1) #define DS_BUTTONS2_MIC_MUTE BIT(2) +/* DualSense Edge extra buttons in buttons[2], bits 4-7. */ +#define DS_EDGE_BUTTONS_FN1 BIT(4) +#define DS_EDGE_BUTTONS_FN2 BIT(5) +#define DS_EDGE_BUTTONS_LEFT_PADDLE BIT(6) +#define DS_EDGE_BUTTONS_RIGHT_PADDLE BIT(7) + /* Status fields of DualSense input report. */ #define DS_STATUS0_BATTERY_CAPACITY GENMASK(3, 0) #define DS_STATUS0_CHARGING GENMASK(7, 4) @@ -178,6 +184,9 @@ struct dualsense { struct input_dev *touchpad; struct input_dev *jack; + /* True if this is a DualSense Edge (product 0x0df2). */ + bool is_edge; + /* Update version is used as a feature/capability version. */ u16 update_version; @@ -1486,6 +1495,18 @@ static int dualsense_parse_report(struct ps_device *ps_dev, struct hid_report *r input_report_key(ds->gamepad, BTN_THUMBL, ds_report->buttons[1] & DS_BUTTONS1_L3); input_report_key(ds->gamepad, BTN_THUMBR, ds_report->buttons[1] & DS_BUTTONS1_R3); input_report_key(ds->gamepad, BTN_MODE, ds_report->buttons[2] & DS_BUTTONS2_PS_HOME); + + if (ds->is_edge) { + input_report_key(ds->gamepad, BTN_TRIGGER_HAPPY1, + ds_report->buttons[2] & DS_EDGE_BUTTONS_FN1); + input_report_key(ds->gamepad, BTN_TRIGGER_HAPPY2, + ds_report->buttons[2] & DS_EDGE_BUTTONS_FN2); + input_report_key(ds->gamepad, BTN_TRIGGER_HAPPY3, + ds_report->buttons[2] & DS_EDGE_BUTTONS_LEFT_PADDLE); + input_report_key(ds->gamepad, BTN_TRIGGER_HAPPY4, + ds_report->buttons[2] & DS_EDGE_BUTTONS_RIGHT_PADDLE); + } + input_sync(ds->gamepad); /* @@ -1785,6 +1806,7 @@ static struct ps_device *dualsense_create(struct hid_device *hdev) ds->use_vibration_v2 = ds->update_version >= DS_FEATURE_VERSION(2, 21); } else if (hdev->product == USB_DEVICE_ID_SONY_PS5_CONTROLLER_2) { ds->use_vibration_v2 = true; + ds->is_edge = true; } ret = ps_devices_list_add(ps_dev); @@ -1802,6 +1824,15 @@ static struct ps_device *dualsense_create(struct hid_device *hdev) ret = PTR_ERR(ds->gamepad); goto err; } + + /* Register DualSense Edge back paddle and Fn buttons. */ + if (ds->is_edge) { + input_set_capability(ds->gamepad, EV_KEY, BTN_TRIGGER_HAPPY1); + input_set_capability(ds->gamepad, EV_KEY, BTN_TRIGGER_HAPPY2); + input_set_capability(ds->gamepad, EV_KEY, BTN_TRIGGER_HAPPY3); + input_set_capability(ds->gamepad, EV_KEY, BTN_TRIGGER_HAPPY4); + } + /* Use gamepad input device name as primary device name for e.g. LEDs */ ps_dev->input_dev_name = dev_name(&ds->gamepad->dev); From 34b228b739f5799f434f7de35328e482d385588b Mon Sep 17 00:00:00 2001 From: Mahad Ibrahim Date: Fri, 10 Apr 2026 15:24:47 -0400 Subject: [PATCH 03/39] HID: usbhid: replace strlcat with better alternatives In preparation for the removal of the strlcat() API as per the KSPP, replace the string concatenation logic in hid-core, usbkbd, and usbmouse with struct seq_buf, which tracks the current write position and remaining space internally. The changes implemented include: - Replace device name and phys concatenation with seq_buf_puts(). - Include Struct seq_buf and its initialization. - Include header file of seq_buf. - Replace strlen() with seq_buf_used() on the string buffer which was tracked by seq_buf to increase speed. - Add size_t len in files which did not have it. - Use of strscpy with length in place of strlcat. Testing: This driver was compiled as a module as well as in-built in QEMU with the QEMU basic mouse, and QEMU basic keyboard. The testing was done in the following steps. - Add Hardware Mouse in QEMU checking the usbhid module. - Verify dmesg string name of mouse. - Blacklist hidusb module from auto-loading, and removing the module via rmmod. - Load usbmouse module, and reattach QEMU mouse. - Verify dmesg string name of mouse. - Repeat same procedure on usbkbd module. This aligns the driver with KSPP security guidelines. Link: https://github.com/KSPP/linux/issues/370 Signed-off-by: Mahad Ibrahim Signed-off-by: Jiri Kosina --- drivers/hid/usbhid/hid-core.c | 17 ++++++++++------- drivers/hid/usbhid/usbkbd.c | 15 ++++++++++----- drivers/hid/usbhid/usbmouse.c | 15 ++++++++++----- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/drivers/hid/usbhid/hid-core.c b/drivers/hid/usbhid/hid-core.c index fbbfc0f60829..4f9d01270c5d 100644 --- a/drivers/hid/usbhid/hid-core.c +++ b/drivers/hid/usbhid/hid-core.c @@ -27,6 +27,7 @@ #include #include #include +#include #include @@ -1366,6 +1367,7 @@ static int usbhid_probe(struct usb_interface *intf, const struct usb_device_id * struct usb_endpoint_descriptor *ep; struct usbhid_device *usbhid; struct hid_device *hid; + struct seq_buf hid_name; size_t len; int ret; @@ -1397,7 +1399,7 @@ static int usbhid_probe(struct usb_interface *intf, const struct usb_device_id * hid->vendor = le16_to_cpu(dev->descriptor.idVendor); hid->product = le16_to_cpu(dev->descriptor.idProduct); hid->version = le16_to_cpu(dev->descriptor.bcdDevice); - hid->name[0] = 0; + seq_buf_init(&hid_name, hid->name, sizeof(hid->name)); if (intf->cur_altsetting->desc.bInterfaceProtocol == USB_INTERFACE_PROTOCOL_MOUSE) hid->type = HID_TYPE_USBMOUSE; @@ -1405,22 +1407,23 @@ static int usbhid_probe(struct usb_interface *intf, const struct usb_device_id * hid->type = HID_TYPE_USBNONE; if (dev->manufacturer) - strscpy(hid->name, dev->manufacturer, sizeof(hid->name)); + seq_buf_puts(&hid_name, dev->manufacturer); if (dev->product) { if (dev->manufacturer) - strlcat(hid->name, " ", sizeof(hid->name)); - strlcat(hid->name, dev->product, sizeof(hid->name)); + seq_buf_puts(&hid_name, " "); + seq_buf_puts(&hid_name, dev->product); } - if (!strlen(hid->name)) + if (!seq_buf_used(&hid_name)) snprintf(hid->name, sizeof(hid->name), "HID %04x:%04x", le16_to_cpu(dev->descriptor.idVendor), le16_to_cpu(dev->descriptor.idProduct)); usb_make_path(dev, hid->phys, sizeof(hid->phys)); - strlcat(hid->phys, "/input", sizeof(hid->phys)); - len = strlen(hid->phys); + len = strnlen(hid->phys, sizeof(hid->phys)); + strscpy(hid->phys + len, "/input", sizeof(hid->phys) - len); + len = strnlen(hid->phys, sizeof(hid->phys)); if (len < sizeof(hid->phys) - 1) snprintf(hid->phys + len, sizeof(hid->phys) - len, "%d", intf->altsetting[0].desc.bInterfaceNumber); diff --git a/drivers/hid/usbhid/usbkbd.c b/drivers/hid/usbhid/usbkbd.c index 6b33e6ad0846..83d4df0d7a45 100644 --- a/drivers/hid/usbhid/usbkbd.c +++ b/drivers/hid/usbhid/usbkbd.c @@ -20,6 +20,7 @@ #include #include #include +#include /* * Version Information @@ -266,8 +267,10 @@ static int usb_kbd_probe(struct usb_interface *iface, struct usb_endpoint_descriptor *endpoint; struct usb_kbd *kbd; struct input_dev *input_dev; + struct seq_buf kbd_name; int i, pipe, maxp; int error = -ENOMEM; + size_t len; interface = iface->cur_altsetting; @@ -292,24 +295,26 @@ static int usb_kbd_probe(struct usb_interface *iface, kbd->usbdev = dev; kbd->dev = input_dev; spin_lock_init(&kbd->leds_lock); + seq_buf_init(&kbd_name, kbd->name, sizeof(kbd->name)); if (dev->manufacturer) - strscpy(kbd->name, dev->manufacturer, sizeof(kbd->name)); + seq_buf_puts(&kbd_name, dev->manufacturer); if (dev->product) { if (dev->manufacturer) - strlcat(kbd->name, " ", sizeof(kbd->name)); - strlcat(kbd->name, dev->product, sizeof(kbd->name)); + seq_buf_puts(&kbd_name, " "); + seq_buf_puts(&kbd_name, dev->product); } - if (!strlen(kbd->name)) + if (!seq_buf_used(&kbd_name)) snprintf(kbd->name, sizeof(kbd->name), "USB HIDBP Keyboard %04x:%04x", le16_to_cpu(dev->descriptor.idVendor), le16_to_cpu(dev->descriptor.idProduct)); usb_make_path(dev, kbd->phys, sizeof(kbd->phys)); - strlcat(kbd->phys, "/input0", sizeof(kbd->phys)); + len = strnlen(kbd->phys, sizeof(kbd->phys)); + strscpy(kbd->phys + len, "/input0", sizeof(kbd->phys) - len); input_dev->name = kbd->name; input_dev->phys = kbd->phys; diff --git a/drivers/hid/usbhid/usbmouse.c b/drivers/hid/usbhid/usbmouse.c index 7cc4f9558e5f..b3b2abeee614 100644 --- a/drivers/hid/usbhid/usbmouse.c +++ b/drivers/hid/usbhid/usbmouse.c @@ -18,6 +18,7 @@ #include #include #include +#include /* for apple IDs */ #ifdef CONFIG_USB_HID_MODULE @@ -110,8 +111,10 @@ static int usb_mouse_probe(struct usb_interface *intf, const struct usb_device_i struct usb_endpoint_descriptor *endpoint; struct usb_mouse *mouse; struct input_dev *input_dev; + struct seq_buf mouse_name; int pipe, maxp; int error = -ENOMEM; + size_t len; interface = intf->cur_altsetting; @@ -140,24 +143,26 @@ static int usb_mouse_probe(struct usb_interface *intf, const struct usb_device_i mouse->usbdev = dev; mouse->dev = input_dev; + seq_buf_init(&mouse_name, mouse->name, sizeof(mouse->name)); if (dev->manufacturer) - strscpy(mouse->name, dev->manufacturer, sizeof(mouse->name)); + seq_buf_puts(&mouse_name, dev->manufacturer); if (dev->product) { if (dev->manufacturer) - strlcat(mouse->name, " ", sizeof(mouse->name)); - strlcat(mouse->name, dev->product, sizeof(mouse->name)); + seq_buf_puts(&mouse_name, " "); + seq_buf_puts(&mouse_name, dev->product); } - if (!strlen(mouse->name)) + if (!seq_buf_used(&mouse_name)) snprintf(mouse->name, sizeof(mouse->name), "USB HIDBP Mouse %04x:%04x", le16_to_cpu(dev->descriptor.idVendor), le16_to_cpu(dev->descriptor.idProduct)); usb_make_path(dev, mouse->phys, sizeof(mouse->phys)); - strlcat(mouse->phys, "/input0", sizeof(mouse->phys)); + len = strnlen(mouse->phys, sizeof(mouse->phys)); + strscpy(mouse->phys + len, "/input0", sizeof(mouse->phys) - len); input_dev->name = mouse->name; input_dev->phys = mouse->phys; From ebf441556f373ba9dca8e853c8f5c00f13500ce4 Mon Sep 17 00:00:00 2001 From: Rosalie Wanders Date: Sat, 11 Apr 2026 17:53:47 +0200 Subject: [PATCH 04/39] HID: sony: use input_dev from sc struct in sony_init_ff() This commit makes sony_init_ff() use the input_dev from the sc struct, this simplifies the sony_init_ff() function. Signed-off-by: Rosalie Wanders Signed-off-by: Jiri Kosina --- drivers/hid/hid-sony.c | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/drivers/hid/hid-sony.c b/drivers/hid/hid-sony.c index b5e724676c1d..a66d1755654c 100644 --- a/drivers/hid/hid-sony.c +++ b/drivers/hid/hid-sony.c @@ -1856,18 +1856,8 @@ static int sony_play_effect(struct input_dev *dev, void *data, static int sony_init_ff(struct sony_sc *sc) { - struct hid_input *hidinput; - struct input_dev *input_dev; - - if (list_empty(&sc->hdev->inputs)) { - hid_err(sc->hdev, "no inputs found\n"); - return -ENODEV; - } - hidinput = list_entry(sc->hdev->inputs.next, struct hid_input, list); - input_dev = hidinput->input; - - input_set_capability(input_dev, EV_FF, FF_RUMBLE); - return input_ff_create_memless(input_dev, NULL, sony_play_effect); + input_set_capability(sc->input_dev, EV_FF, FF_RUMBLE); + return input_ff_create_memless(sc->input_dev, NULL, sony_play_effect); } #else @@ -2154,6 +2144,8 @@ static int sony_input_configured(struct hid_device *hdev, int append_dev_id; int ret; + sc->input_dev = hidinput->input; + ret = sony_set_device_id(sc); if (ret < 0) { hid_err(hdev, "failed to allocate the device id\n"); @@ -2314,7 +2306,6 @@ static int sony_input_configured(struct hid_device *hdev, goto err_close; } - sc->input_dev = hidinput->input; return 0; err_close: hid_hw_close(hdev); From f1bd44b9b62c6fbdaacdd5d115ebe3fe543fcfa1 Mon Sep 17 00:00:00 2001 From: Dave Carey Date: Mon, 13 Apr 2026 08:58:03 -0400 Subject: [PATCH 05/39] HID: multitouch: Fix Yoga Book 9 14IAH10 touchscreen misclassification The Lenovo Yoga Book 9 14IAH10 (83KJ) (17EF:6161) firmware includes a HID_DG_TOUCHPAD application collection designed for the Windows inbox HID driver's Win8 PTP touchpad mode. On Linux the HID_DG_TOUCHSCREEN collections provide the correct direct-touch interface. The presence of the touchpad collection causes hid-multitouch to misclassify the touchscreen nodes as indirect buttonpads, leaving them non-functional. Within the touchpad collection: - HID_UP_BUTTON usages trigger the touchscreen-with-buttons heuristic that sets INPUT_MT_POINTER on the touchscreen applications. - The HID_DG_TOUCHPAD application itself sets INPUT_MT_POINTER via mt_allocate_application(), propagating to all touchscreen nodes. - A HID_DG_BUTTONTYPE feature (report 0x51) returns MT_BUTTONTYPE_CLICKPAD, setting td->is_buttonpad = true for the entire device. Additionally, the firmware resets if any USB control request arrives while the CDC-ACM interface is initialising (~1.18 s after enumeration). The Win8 compliance blob (0xff00:0xc5) and Contact Count Max feature reports in the touchscreen collections trigger GET_REPORT calls at probe that hit this window. Surface Switch (0x57) and Button Switch (0x58) feature reports are sent by mt_set_modes() on every input-device open and close, repeatedly hitting this window throughout device lifetime. The firmware also leaves a persistent ghost contact in its contact buffer (contact ID 2, fixed coordinates, tip always asserted) on every enumeration. This ghost occupies a multitouch slot and prevents KWin from seeing a clean finger-lift, causing stuck touch state. The ghost is cleared when Input Mode is set via HID_REQ_SET_REPORT at probe. Fix using a report descriptor fixup in mt_report_fixup() and a class definition update: 1. Remove the entire HID_DG_TOUCHPAD application collection. Parsing HID short items from its header to the matching End Collection and closing the gap with memmove eliminates all three BUTTONPAD heuristics and the feature reports within the collection. 2. Neutralize the Win8 compliance blob feature reports remaining in the touchscreen collections by changing Usage Page 0xff00 to 0x0f00, preventing the case 0xff0000c5 branch in mt_feature_mapping() from issuing GET_REPORT. 3. Neutralize the Contact Count Max feature reports by changing usage 0x55 to 0x00; set maxcontacts = 10 in the class definition so the driver uses the correct contact limit without querying the device. 4. Neutralize Surface Switch (0x57) and Button Switch (0x58) feature report usages in the Device Configuration collection so mt_set_modes() does not issue HID_REQ_SET_REPORT for these on every input-device open/close. Input Mode (0x52) is intentionally left intact: the single HID_REQ_SET_REPORT at probe flushes the firmware's contact buffer and clears the persistent ghost contact. By probe time the cdc-acm driver has already satisfied the CDC-ACM init watchdog (~130 ms), so this request arrives safely after the reset window has closed. 5. Add MT_QUIRK_NOT_SEEN_MEANS_UP to the MT_CLS_YOGABOOK9I class so that contacts not present in a frame are released via INPUT_MT_DROP_UNUSED, preventing stale multitouch slots from lingering if the firmware omits a contact from a report. Signed-off-by: Dave Carey Tested-by: Dave Carey Signed-off-by: Jiri Kosina --- drivers/hid/hid-multitouch.c | 146 ++++++++++++++++++++++++++++++++++- 1 file changed, 145 insertions(+), 1 deletion(-) diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c index e82a3c4e5b44..ec04dbafbd99 100644 --- a/drivers/hid/hid-multitouch.c +++ b/drivers/hid/hid-multitouch.c @@ -443,11 +443,13 @@ static const struct mt_class mt_classes[] = { MT_QUIRK_CONTACT_CNT_ACCURATE, }, { .name = MT_CLS_YOGABOOK9I, - .quirks = MT_QUIRK_ALWAYS_VALID | + .quirks = MT_QUIRK_NOT_SEEN_MEANS_UP | + MT_QUIRK_ALWAYS_VALID | MT_QUIRK_FORCE_MULTI_INPUT | MT_QUIRK_SEPARATE_APP_REPORT | MT_QUIRK_HOVERING | MT_QUIRK_YOGABOOK9I, + .maxcontacts = 10, .export_all_inputs = true }, { .name = MT_CLS_EGALAX_P80H84, @@ -1566,6 +1568,144 @@ static int mt_event(struct hid_device *hid, struct hid_field *field, return 0; } +/* + * Yoga Book 9 14IAH10 descriptor fixup. + * + * The device includes a HID_DG_TOUCHPAD application collection designed for + * the Windows inbox HID driver's Win8 PTP touchpad mode. On Linux we want + * only the HID_DG_TOUCHSCREEN collections. The touchpad collection (and the + * HID_DG_BUTTONTYPE and Win8 compliance blob features it contains) must be + * removed so hid-multitouch does not misclassify the touchscreen nodes as + * indirect buttonpads. + * + * The firmware also resets if any USB control request is received while the + * CDC-ACM interface is initialising (~1.18 s after enumeration). Dropping + * the Win8 blob and Contact Count Max feature reports prevents the + * GET_REPORT calls that hid-multitouch issues at probe. + */ +static void mt_yogabook9_fixup(struct hid_device *hdev, __u8 *rdesc, + unsigned int *size) +{ + /* Usage Page (Digitizer), Usage (Touch Pad), Collection (Application) */ + static const __u8 tp_app_hdr[] = { 0x05, 0x0d, 0x09, 0x05, 0xa1, 0x01 }; + /* Vendor Usage Page 0xff00 (Win8 compliance blob header) */ + static const __u8 win8_page[] = { 0x06, 0x00, 0xff }; + /* Usage (Contact Count Max = 0x55) */ + static const __u8 ccmax_usage[] = { 0x09, 0x55 }; + unsigned int i; + + /* + * Step 1: find and remove the Touch Pad application collection. + * Walk HID short items from the collection header to its matching + * End Collection, then close the gap with memmove. + */ + for (i = 0; i + sizeof(tp_app_hdr) <= *size; i++) { + if (memcmp(rdesc + i, tp_app_hdr, sizeof(tp_app_hdr)) == 0) { + __u8 *start = rdesc + i; + __u8 *coll_end = NULL; + __u8 *p = start; + unsigned int drop; + int depth = 0; + + while (p < rdesc + *size) { + __u8 b = *p; + int ds = b & 3; + int item_len; + + if (b == 0xfe) { /* long item */ + if (p + 2 >= rdesc + *size) + break; + item_len = p[1] + 3; + } else { + item_len = (ds == 3) ? 5 : ds + 1; + } + if (p + item_len > rdesc + *size) + break; + + if ((b & 0xfc) == 0xa0) + depth++; /* Collection */ + else if (b == 0xc0) { + depth--; /* End Collection */ + if (depth == 0) { + coll_end = p; + break; + } + } + p += item_len; + } + + if (!coll_end) { + hid_err(hdev, + "Yoga Book 9: Touch Pad End Collection not found\n"); + break; + } + + drop = coll_end - start + 1; + memmove(start, coll_end + 1, rdesc + *size - coll_end - 1); + *size -= drop; + hid_dbg(hdev, + "Yoga Book 9: dropped Touch Pad collection (%u bytes)\n", + drop); + break; + } + } + + /* + * Step 2: neutralize Win8 compliance blob feature reports remaining + * in the touchscreen collections. Change Usage Page 0xff00 to 0x0f00 + * so the case 0xff0000c5 branch in mt_feature_mapping() is not reached + * and no GET_REPORT is issued. + */ + for (i = 0; i + sizeof(win8_page) <= *size; i++) { + if (memcmp(rdesc + i, win8_page, sizeof(win8_page)) == 0) { + rdesc[i + 2] = 0x0f; /* 0xff00 -> 0x0f00 */ + hid_dbg(hdev, + "Yoga Book 9: neutralized Win8 blob at offset %u\n", + i); + } + } + + /* + * Step 3: neutralize Contact Count Max feature reports. Change usage + * 0x55 (HID_DG_CONTACTMAX) to 0x00 so mt_feature_mapping() does not + * issue GET_REPORT. The class maxcontacts field provides the value. + */ + for (i = 0; i + sizeof(ccmax_usage) <= *size; i++) { + if (memcmp(rdesc + i, ccmax_usage, sizeof(ccmax_usage)) == 0) { + rdesc[i + 1] = 0x00; + hid_dbg(hdev, + "Yoga Book 9: neutralized ContactMax at offset %u\n", + i); + } + } + + /* + * Step 4: neutralize Surface Switch (0x57) and Button Switch (0x58) + * feature report usages in the Device Configuration collection. + * mt_set_modes() issues HID_REQ_SET_REPORT for these on every + * input-device open/close; those repeated control requests hit the + * firmware's CDC-ACM init window and trigger resets. + * + * Input Mode (0x52) is intentionally left intact. mt_set_modes() + * sends it once at probe to set the device into touchscreen mode, + * which flushes the firmware's contact buffer and clears a persistent + * ghost contact (cid 2, fixed coordinates) that otherwise appears on + * every enumeration. By probe time cdc_acm has already satisfied the + * CDC-ACM init watchdog (~130 ms), so the single SET_REPORT for Input + * Mode arrives safely after the reset window has closed. + */ + for (i = 0; i + 2 <= *size; i++) { + if (rdesc[i] == 0x09 && + (rdesc[i + 1] == 0x57 || + rdesc[i + 1] == 0x58)) { + hid_dbg(hdev, + "Yoga Book 9: neutralized set-modes usage 0x%02x at offset %u\n", + rdesc[i + 1], i); + rdesc[i + 1] = 0x00; + } + } +} + static const __u8 *mt_report_fixup(struct hid_device *hdev, __u8 *rdesc, unsigned int *size) { @@ -1595,6 +1735,10 @@ got: %x\n", } } + if (hdev->vendor == USB_VENDOR_ID_LENOVO && + hdev->product == USB_DEVICE_ID_LENOVO_YOGABOOK9I) + mt_yogabook9_fixup(hdev, rdesc, size); + return rdesc; } From 84910c459d65ab3a1aeac7f936169fb71334dd46 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 18 Apr 2026 21:26:20 -0700 Subject: [PATCH 06/39] HID: hid-oxp: Add OneXPlayer configuration driver Adds OneXPlayer HID configuration driver. In this initial driver patch, add the RGB interface for the first generation of HID based RGB control. This interface provides the following attributes: - brightness: provided by the LED core, this works in a fairly unique way on this device. The hardware accepts 5 brightness values (0-4), which affects the brightness of the multicolor and animated effects built into the MCU firmware. For monocolor settings, the device expects the hardware brightness value to be pushed to maximum, then we apply brightness adjustments mathematically based on % (0-100). This leads to some odd conversion as we need the brightness slider to reach the full range, but it has no affect when incrementing between the division points for other effects. - multi-intensity: provided by the LED core for red, green, and blue. - effect: Allows the MCU to set 19 individual effects. - effect_index: Lists the 19 valid effect names for the interface. - enabled: Allows the MCU to toggle the RGB interface on/off. - enabled_index: Lists the valid states for enabled. - speed: Allows the MCU to set the animation rate for the various effects. - speed_range: Lists the valid range of speed (0-9). The MCU also has a few odd quirks that make sending multiple synchronous events challenging. It will essentially freeze if it receives another message before it has finished processing the last command. It also will not reply if you wait on it using a completion. To get around this, we do a 200ms sleep inside a work queue thread and debounce all but the most recent message using a 50ms mod_delayed_work. This will cache the last write, queue the work, then return so userspace can release its write thread. The work queue is only used for brightness/multi-intensity as that is the path likely to receive rapid successive writes. Reviewed-by: Zhouwang Huang Tested-by: Zhouwang Huang Signed-off-by: Derek J. Clark Signed-off-by: Jiri Kosina --- MAINTAINERS | 6 + drivers/hid/Kconfig | 12 + drivers/hid/Makefile | 1 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-oxp.c | 652 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 674 insertions(+) create mode 100644 drivers/hid/hid-oxp.c diff --git a/MAINTAINERS b/MAINTAINERS index 8b721e8ad919..d317e2ca65c1 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19861,6 +19861,12 @@ S: Maintained F: drivers/mtd/nand/onenand/ F: include/linux/mtd/onenand*.h +ONEXPLAYER HID DRIVER +M: Derek J. Clark +L: linux-input@vger.kernel.org +S: Maintained +F: drivers/hid/hid-oxp.c + ONEXPLAYER PLATFORM EC DRIVER M: Antheas Kapenekakis M: Derek John Clark diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index ff2f580b660b..db6e1e0bca6e 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -896,6 +896,18 @@ config HID_ORTEK - Ortek WKB-2000 - Skycable wireless presenter +config HID_OXP + tristate "OneXPlayer handheld controller configuration support" + depends on USB_HID + depends on LEDS_CLASS + depends on LEDS_CLASS_MULTICOLOR + help + Say Y here if you would like to enable support for OneXPlayer handheld + devices that come with RGB LED rings around the joysticks and macro buttons. + + To compile this driver as a module, choose M here: the module will + be called hid-oxp. + config HID_PANTHERLORD tristate "Pantherlord/GreenAsia game controller" help diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index 0597fd6a4ffd..70c0e02b9383 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -98,6 +98,7 @@ obj-$(CONFIG_HID_NTI) += hid-nti.o obj-$(CONFIG_HID_NTRIG) += hid-ntrig.o obj-$(CONFIG_HID_NVIDIA_SHIELD) += hid-nvidia-shield.o obj-$(CONFIG_HID_ORTEK) += hid-ortek.o +obj-$(CONFIG_HID_OXP) += hid-oxp.o obj-$(CONFIG_HID_PRODIKEYS) += hid-prodikeys.o obj-$(CONFIG_HID_PANTHERLORD) += hid-pl.o obj-$(CONFIG_HID_PENMOUNT) += hid-penmount.o diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 0cf63742315b..589e4db940c7 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -1127,6 +1127,9 @@ #define USB_VENDOR_ID_NVIDIA 0x0955 #define USB_DEVICE_ID_NVIDIA_THUNDERSTRIKE_CONTROLLER 0x7214 +#define USB_VENDOR_ID_CRSC 0x1a2c +#define USB_DEVICE_ID_ONEXPLAYER_GEN1 0xb001 + #define USB_VENDOR_ID_ONTRAK 0x0a07 #define USB_DEVICE_ID_ONTRAK_ADU100 0x0064 diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c new file mode 100644 index 000000000000..f72bc74a7e6e --- /dev/null +++ b/drivers/hid/hid-oxp.c @@ -0,0 +1,652 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for OneXPlayer gamepad configuration devices. + * + * Copyright (c) 2026 Valve Corporation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hid-ids.h" + +#define OXP_PACKET_SIZE 64 + +#define GEN1_MESSAGE_ID 0xff + +#define GEN1_USAGE_PAGE 0xff01 + +enum oxp_function_index { + OXP_FID_GEN1_RGB_SET = 0x07, + OXP_FID_GEN1_RGB_REPLY = 0x0f, +}; + +static struct oxp_hid_cfg { + struct delayed_work oxp_rgb_queue; + struct led_classdev_mc *led_mc; + struct hid_device *hdev; + struct mutex cfg_mutex; /*ensure single synchronous output report*/ + u8 rgb_brightness; + u8 rgb_effect; + u8 rgb_speed; + u8 rgb_en; +} drvdata; + +enum oxp_feature_en_index { + OXP_FEAT_DISABLED, + OXP_FEAT_ENABLED, +}; + +static const char *const oxp_feature_en_text[] = { + [OXP_FEAT_DISABLED] = "false", + [OXP_FEAT_ENABLED] = "true", +}; + +enum oxp_rgb_effect_index { + OXP_UNKNOWN, + OXP_EFFECT_AURORA, + OXP_EFFECT_BIRTHDAY, + OXP_EFFECT_FLOWING, + OXP_EFFECT_CHROMA_1, + OXP_EFFECT_NEON, + OXP_EFFECT_CHROMA_2, + OXP_EFFECT_DREAMY, + OXP_EFFECT_WARM, + OXP_EFFECT_CYBERPUNK, + OXP_EFFECT_SEA, + OXP_EFFECT_SUNSET, + OXP_EFFECT_COLORFUL, + OXP_EFFECT_MONSTER, + OXP_EFFECT_GREEN, + OXP_EFFECT_BLUE, + OXP_EFFECT_YELLOW, + OXP_EFFECT_TEAL, + OXP_EFFECT_PURPLE, + OXP_EFFECT_FOGGY, + OXP_EFFECT_MONO_LIST, /* placeholder for effect_index_show */ +}; + +/* These belong to rgb_effect_index, but we want to hide them from + * rgb_effect_text + */ + +#define OXP_GET_PROPERTY 0xfc +#define OXP_SET_PROPERTY 0xfd +#define OXP_EFFECT_MONO_TRUE 0xfe /* actual index for monocolor */ + +static const char *const oxp_rgb_effect_text[] = { + [OXP_UNKNOWN] = "unknown", + [OXP_EFFECT_AURORA] = "aurora", + [OXP_EFFECT_BIRTHDAY] = "birthday_cake", + [OXP_EFFECT_FLOWING] = "flowing_light", + [OXP_EFFECT_CHROMA_1] = "chroma_popping", + [OXP_EFFECT_NEON] = "neon", + [OXP_EFFECT_CHROMA_2] = "chroma_breathing", + [OXP_EFFECT_DREAMY] = "dreamy", + [OXP_EFFECT_WARM] = "warm_sun", + [OXP_EFFECT_CYBERPUNK] = "cyberpunk", + [OXP_EFFECT_SEA] = "sea_foam", + [OXP_EFFECT_SUNSET] = "sunset_afterglow", + [OXP_EFFECT_COLORFUL] = "colorful", + [OXP_EFFECT_MONSTER] = "monster_woke", + [OXP_EFFECT_GREEN] = "green_breathing", + [OXP_EFFECT_BLUE] = "blue_breathing", + [OXP_EFFECT_YELLOW] = "yellow_breathing", + [OXP_EFFECT_TEAL] = "teal_breathing", + [OXP_EFFECT_PURPLE] = "purple_breathing", + [OXP_EFFECT_FOGGY] = "foggy_haze", + [OXP_EFFECT_MONO_LIST] = "monocolor", +}; + +struct oxp_gen_1_rgb_report { + u8 report_id; + u8 message_id; + u8 padding_2[2]; + u8 effect; + u8 enabled; + u8 speed; + u8 brightness; + u8 red; + u8 green; + u8 blue; +} __packed; + +static u16 get_usage_page(struct hid_device *hdev) +{ + return hdev->collection[0].usage >> 16; +} + +static int oxp_hid_raw_event_gen_1(struct hid_device *hdev, + struct hid_report *report, u8 *data, + int size) +{ + struct led_classdev_mc *led_mc = drvdata.led_mc; + struct oxp_gen_1_rgb_report *rgb_rep; + + if (data[1] != OXP_FID_GEN1_RGB_REPLY) + return 0; + + rgb_rep = (struct oxp_gen_1_rgb_report *)data; + /* Ensure we save monocolor as the list value */ + drvdata.rgb_effect = rgb_rep->effect == OXP_EFFECT_MONO_TRUE ? + OXP_EFFECT_MONO_LIST : + rgb_rep->effect; + drvdata.rgb_speed = rgb_rep->speed; + drvdata.rgb_en = rgb_rep->enabled == 0 ? OXP_FEAT_DISABLED : + OXP_FEAT_ENABLED; + drvdata.rgb_brightness = rgb_rep->brightness; + led_mc->led_cdev.brightness = rgb_rep->brightness / 4 * + led_mc->led_cdev.max_brightness; + /* If monocolor had less than 100% brightness on the previous boot, + * there will be no reliable way to determine the real intensity. + * Since intensity scaling is used with a hardware brightness set at max, + * our brightness will always look like 100%. Use the last set value to + * prevent successive boots from lowering the brightness further. + * Brightness will be "wrong" but the effect will remain the same visually. + */ + led_mc->subled_info[0].intensity = rgb_rep->red; + led_mc->subled_info[1].intensity = rgb_rep->green; + led_mc->subled_info[2].intensity = rgb_rep->blue; + + return 0; +} + +static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report, + u8 *data, int size) +{ + u16 up = get_usage_page(hdev); + + dev_dbg(&hdev->dev, "raw event data: [%*ph]\n", OXP_PACKET_SIZE, data); + + switch (up) { + case GEN1_USAGE_PAGE: + return oxp_hid_raw_event_gen_1(hdev, report, data, size); + default: + break; + } + + return 0; +} + +static int mcu_property_out(u8 *header, size_t header_size, u8 *data, + size_t data_size, u8 *footer, size_t footer_size) +{ + unsigned char *dmabuf __free(kfree) = kzalloc(OXP_PACKET_SIZE, GFP_KERNEL); + int ret; + + if (!dmabuf) + return -ENOMEM; + + if (header_size + data_size + footer_size > OXP_PACKET_SIZE) + return -EINVAL; + + guard(mutex)(&drvdata.cfg_mutex); + memcpy(dmabuf, header, header_size); + memcpy(dmabuf + header_size, data, data_size); + if (footer_size) + memcpy(dmabuf + OXP_PACKET_SIZE - footer_size, footer, footer_size); + + dev_dbg(&drvdata.hdev->dev, "raw data: [%*ph]\n", OXP_PACKET_SIZE, dmabuf); + + ret = hid_hw_output_report(drvdata.hdev, dmabuf, OXP_PACKET_SIZE); + if (ret < 0) + return ret; + + /* MCU takes 200ms to be ready for another command. */ + msleep(200); + return ret == OXP_PACKET_SIZE ? 0 : -EIO; +} + +static int oxp_gen_1_property_out(enum oxp_function_index fid, u8 *data, + u8 data_size) +{ + u8 header[] = { fid, GEN1_MESSAGE_ID }; + size_t header_size = ARRAY_SIZE(header); + + return mcu_property_out(header, header_size, data, data_size, NULL, 0); +} + +static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness) +{ + u16 up = get_usage_page(drvdata.hdev); + u8 *data; + + /* Always default to max brightness and use intensity scaling when in + * monocolor mode. + */ + switch (up) { + case GEN1_USAGE_PAGE: + data = (u8[4]) { OXP_SET_PROPERTY, enabled, speed, brightness }; + if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST) + data[3] = 0x04; + return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 4); + default: + return -ENODEV; + } +} + +static ssize_t oxp_rgb_status_show(void) +{ + u16 up = get_usage_page(drvdata.hdev); + u8 *data; + + switch (up) { + case GEN1_USAGE_PAGE: + data = (u8[1]) { OXP_GET_PROPERTY }; + return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1); + default: + return -ENODEV; + } +} + +static int oxp_rgb_color_set(void) +{ + u8 max_br = drvdata.led_mc->led_cdev.max_brightness; + u8 br = drvdata.led_mc->led_cdev.brightness; + u16 up = get_usage_page(drvdata.hdev); + u8 green, red, blue; + size_t size; + u8 *data; + int i; + + red = br * drvdata.led_mc->subled_info[0].intensity / max_br; + green = br * drvdata.led_mc->subled_info[1].intensity / max_br; + blue = br * drvdata.led_mc->subled_info[2].intensity / max_br; + + switch (up) { + case GEN1_USAGE_PAGE: + size = 55; + data = (u8[55]) { OXP_EFFECT_MONO_TRUE }; + + for (i = 0; i < (size - 1) / 3; i++) { + data[3 * i + 1] = red; + data[3 * i + 2] = green; + data[3 * i + 3] = blue; + } + return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, size); + default: + return -ENODEV; + } +} + +static int oxp_rgb_effect_set(u8 effect) +{ + u16 up = get_usage_page(drvdata.hdev); + u8 *data; + int ret; + + switch (effect) { + case OXP_EFFECT_AURORA: + case OXP_EFFECT_BIRTHDAY: + case OXP_EFFECT_FLOWING: + case OXP_EFFECT_CHROMA_1: + case OXP_EFFECT_NEON: + case OXP_EFFECT_CHROMA_2: + case OXP_EFFECT_DREAMY: + case OXP_EFFECT_WARM: + case OXP_EFFECT_CYBERPUNK: + case OXP_EFFECT_SEA: + case OXP_EFFECT_SUNSET: + case OXP_EFFECT_COLORFUL: + case OXP_EFFECT_MONSTER: + case OXP_EFFECT_GREEN: + case OXP_EFFECT_BLUE: + case OXP_EFFECT_YELLOW: + case OXP_EFFECT_TEAL: + case OXP_EFFECT_PURPLE: + case OXP_EFFECT_FOGGY: + switch (up) { + case GEN1_USAGE_PAGE: + data = (u8[1]) { effect }; + ret = oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1); + break; + default: + ret = -ENODEV; + } + break; + case OXP_EFFECT_MONO_LIST: + ret = oxp_rgb_color_set(); + break; + default: + return -EINVAL; + } + + if (ret) + return ret; + + drvdata.rgb_effect = effect; + + return 0; +} + +static ssize_t enabled_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + u8 val; + + ret = sysfs_match_string(oxp_feature_en_text, buf); + if (ret < 0) + return ret; + val = ret; + + ret = oxp_rgb_status_store(val, drvdata.rgb_speed, + drvdata.rgb_brightness); + if (ret) + return ret; + + drvdata.rgb_en = val; + return count; +} + +static ssize_t enabled_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int ret; + + ret = oxp_rgb_status_show(); + if (ret) + return ret; + + if (drvdata.rgb_en >= ARRAY_SIZE(oxp_feature_en_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", oxp_feature_en_text[drvdata.rgb_en]); +} +static DEVICE_ATTR_RW(enabled); + +static ssize_t enabled_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + size_t count = 0; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(oxp_feature_en_text); i++) + count += sysfs_emit_at(buf, count, "%s ", oxp_feature_en_text[i]); + + if (count) + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(enabled_index); + +static ssize_t effect_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + u8 val; + + ret = sysfs_match_string(oxp_rgb_effect_text, buf); + if (ret < 0) + return ret; + + val = ret; + + ret = oxp_rgb_status_store(drvdata.rgb_en, drvdata.rgb_speed, + drvdata.rgb_brightness); + if (ret) + return ret; + + ret = oxp_rgb_effect_set(val); + if (ret) + return ret; + + return count; +} + +static ssize_t effect_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int ret; + + ret = oxp_rgb_status_show(); + if (ret) + return ret; + + if (drvdata.rgb_effect >= ARRAY_SIZE(oxp_rgb_effect_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", oxp_rgb_effect_text[drvdata.rgb_effect]); +} + +static DEVICE_ATTR_RW(effect); + +static ssize_t effect_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + size_t count = 0; + unsigned int i; + + for (i = 1; i < ARRAY_SIZE(oxp_rgb_effect_text); i++) + count += sysfs_emit_at(buf, count, "%s ", oxp_rgb_effect_text[i]); + + if (count) + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(effect_index); + +static ssize_t speed_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + u8 val; + + ret = kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val > 9) + return -EINVAL; + + ret = oxp_rgb_status_store(drvdata.rgb_en, val, drvdata.rgb_brightness); + if (ret) + return ret; + + drvdata.rgb_speed = val; + return count; +} + +static ssize_t speed_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int ret; + + ret = oxp_rgb_status_show(); + if (ret) + return ret; + + if (drvdata.rgb_speed > 9) + return -EINVAL; + + return sysfs_emit(buf, "%hhu\n", drvdata.rgb_speed); +} +static DEVICE_ATTR_RW(speed); + +static ssize_t speed_range_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0-9\n"); +} +static DEVICE_ATTR_RO(speed_range); + +static void oxp_rgb_queue_fn(struct work_struct *work) +{ + unsigned int max_brightness = drvdata.led_mc->led_cdev.max_brightness; + unsigned int brightness = drvdata.led_mc->led_cdev.brightness; + u8 val = 4 * brightness / max_brightness; + int ret; + + if (drvdata.rgb_brightness != val) { + ret = oxp_rgb_status_store(drvdata.rgb_en, drvdata.rgb_speed, val); + if (ret) + dev_err(drvdata.led_mc->led_cdev.dev, + "Error: Failed to write RGB Status: %i\n", ret); + + drvdata.rgb_brightness = val; + } + + if (drvdata.rgb_effect != OXP_EFFECT_MONO_LIST) + return; + + ret = oxp_rgb_effect_set(drvdata.rgb_effect); + if (ret) + dev_err(drvdata.led_mc->led_cdev.dev, "Error: Failed to write RGB color: %i\n", + ret); +} + +static void oxp_rgb_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + led_cdev->brightness = brightness; + mod_delayed_work(system_wq, &drvdata.oxp_rgb_queue, msecs_to_jiffies(50)); +} + +static struct attribute *oxp_rgb_attrs[] = { + &dev_attr_effect.attr, + &dev_attr_effect_index.attr, + &dev_attr_enabled.attr, + &dev_attr_enabled_index.attr, + &dev_attr_speed.attr, + &dev_attr_speed_range.attr, + NULL, +}; + +static const struct attribute_group oxp_rgb_attr_group = { + .attrs = oxp_rgb_attrs, +}; + +static struct mc_subled oxp_rgb_subled_info[] = { + { + .color_index = LED_COLOR_ID_RED, + .intensity = 0x24, + .channel = 0x1, + }, + { + .color_index = LED_COLOR_ID_GREEN, + .intensity = 0x22, + .channel = 0x2, + }, + { + .color_index = LED_COLOR_ID_BLUE, + .intensity = 0x99, + .channel = 0x3, + }, +}; + +static struct led_classdev_mc oxp_cdev_rgb = { + .led_cdev = { + .name = "oxp:rgb:joystick_rings", + .color = LED_COLOR_ID_RGB, + .brightness = 0x64, + .max_brightness = 0x64, + .brightness_set = oxp_rgb_brightness_set, + }, + .num_colors = ARRAY_SIZE(oxp_rgb_subled_info), + .subled_info = oxp_rgb_subled_info, +}; + +static int oxp_cfg_probe(struct hid_device *hdev, u16 up) +{ + int ret; + + hid_set_drvdata(hdev, &drvdata); + mutex_init(&drvdata.cfg_mutex); + drvdata.hdev = hdev; + drvdata.led_mc = &oxp_cdev_rgb; + + INIT_DELAYED_WORK(&drvdata.oxp_rgb_queue, oxp_rgb_queue_fn); + ret = devm_led_classdev_multicolor_register(&hdev->dev, &oxp_cdev_rgb); + if (ret) + return dev_err_probe(&hdev->dev, ret, + "Failed to create RGB device\n"); + + ret = devm_device_add_group(drvdata.led_mc->led_cdev.dev, + &oxp_rgb_attr_group); + if (ret) + return dev_err_probe(drvdata.led_mc->led_cdev.dev, ret, + "Failed to create RGB configuration attributes\n"); + + ret = oxp_rgb_status_show(); + if (ret) + dev_warn(drvdata.led_mc->led_cdev.dev, + "Failed to query RGB initial state: %i\n", ret); + + return 0; +} + +static int oxp_hid_probe(struct hid_device *hdev, + const struct hid_device_id *id) +{ + int ret; + u16 up; + + ret = hid_parse(hdev); + if (ret) + return dev_err_probe(&hdev->dev, ret, "Failed to parse HID device\n"); + + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (ret) + return dev_err_probe(&hdev->dev, ret, "Failed to start HID device\n"); + + ret = hid_hw_open(hdev); + if (ret) { + hid_hw_stop(hdev); + return dev_err_probe(&hdev->dev, ret, "Failed to open HID device\n"); + } + + up = get_usage_page(hdev); + dev_dbg(&hdev->dev, "Got usage page %04x\n", up); + + switch (up) { + case GEN1_USAGE_PAGE: + ret = oxp_cfg_probe(hdev, up); + if (ret) { + hid_hw_close(hdev); + hid_hw_stop(hdev); + } + + return ret; + default: + return 0; + } +} + +static void oxp_hid_remove(struct hid_device *hdev) +{ + cancel_delayed_work(&drvdata.oxp_rgb_queue); + hid_hw_close(hdev); + hid_hw_stop(hdev); +} + +static const struct hid_device_id oxp_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_CRSC, USB_DEVICE_ID_ONEXPLAYER_GEN1) }, + {} +}; + +MODULE_DEVICE_TABLE(hid, oxp_devices); +static struct hid_driver hid_oxp = { + .name = "hid-oxp", + .id_table = oxp_devices, + .probe = oxp_hid_probe, + .remove = oxp_hid_remove, + .raw_event = oxp_hid_raw_event, +}; +module_hid_driver(hid_oxp); + +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("Driver for OneXPlayer HID Interfaces"); +MODULE_LICENSE("GPL"); From 252c4bf1d9316ee57f60cef93b9ce37da10307be Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 18 Apr 2026 21:26:21 -0700 Subject: [PATCH 07/39] HID: hid-oxp: Add Second Generation RGB Control Adds support for the second generation of RGB Control for OneXPlayer devices. The interface mirrors the first generation, with some differences to how messages are formatted. Some devices have both a GEN1 MCU for RGB control and a GEN2 MCU for button mapping. To avoid conflicts, quirk these devices to skip RGB setup for the GEN2_USAGE_PAGE. Reviewed-by: Zhouwang Huang Tested-by: Zhouwang Huang Signed-off-by: Derek J. Clark Signed-off-by: Jiri Kosina --- drivers/hid/Kconfig | 1 + drivers/hid/hid-ids.h | 3 + drivers/hid/hid-oxp.c | 151 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+) diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index db6e1e0bca6e..f60837e4ed52 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -901,6 +901,7 @@ config HID_OXP depends on USB_HID depends on LEDS_CLASS depends on LEDS_CLASS_MULTICOLOR + depends on DMI help Say Y here if you would like to enable support for OneXPlayer handheld devices that come with RGB LED rings around the joysticks and macro buttons. diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 589e4db940c7..93939e489d83 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -1130,6 +1130,9 @@ #define USB_VENDOR_ID_CRSC 0x1a2c #define USB_DEVICE_ID_ONEXPLAYER_GEN1 0xb001 +#define USB_VENDOR_ID_WCH 0x1a86 +#define USB_DEVICE_ID_ONEXPLAYER_GEN2 0xfe00 + #define USB_VENDOR_ID_ONTRAK 0x0a07 #define USB_DEVICE_ID_ONTRAK_ADU100 0x0064 diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c index f72bc74a7e6e..835de2118e3c 100644 --- a/drivers/hid/hid-oxp.c +++ b/drivers/hid/hid-oxp.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -24,12 +25,15 @@ #define OXP_PACKET_SIZE 64 #define GEN1_MESSAGE_ID 0xff +#define GEN2_MESSAGE_ID 0x3f #define GEN1_USAGE_PAGE 0xff01 +#define GEN2_USAGE_PAGE 0xff00 enum oxp_function_index { OXP_FID_GEN1_RGB_SET = 0x07, OXP_FID_GEN1_RGB_REPLY = 0x0f, + OXP_FID_GEN2_STATUS_EVENT = 0xb8, }; static struct oxp_hid_cfg { @@ -122,6 +126,22 @@ struct oxp_gen_1_rgb_report { u8 blue; } __packed; +struct oxp_gen_2_rgb_report { + u8 report_id; + u8 header_id; + u8 padding_2; + u8 message_id; + u8 padding_4[2]; + u8 enabled; + u8 speed; + u8 brightness; + u8 red; + u8 green; + u8 blue; + u8 padding_12[3]; + u8 effect; +} __packed; + static u16 get_usage_page(struct hid_device *hdev) { return hdev->collection[0].usage >> 16; @@ -162,6 +182,44 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev, return 0; } +static int oxp_hid_raw_event_gen_2(struct hid_device *hdev, + struct hid_report *report, u8 *data, + int size) +{ + struct led_classdev_mc *led_mc = drvdata.led_mc; + struct oxp_gen_2_rgb_report *rgb_rep; + + if (data[0] != OXP_FID_GEN2_STATUS_EVENT) + return 0; + + if (data[3] != OXP_GET_PROPERTY) + return 0; + + rgb_rep = (struct oxp_gen_2_rgb_report *)data; + /* Ensure we save monocolor as the list value */ + drvdata.rgb_effect = rgb_rep->effect == OXP_EFFECT_MONO_TRUE ? + OXP_EFFECT_MONO_LIST : + rgb_rep->effect; + drvdata.rgb_speed = rgb_rep->speed; + drvdata.rgb_en = rgb_rep->enabled == 0 ? OXP_FEAT_DISABLED : + OXP_FEAT_ENABLED; + drvdata.rgb_brightness = rgb_rep->brightness; + led_mc->led_cdev.brightness = rgb_rep->brightness / 4 * + led_mc->led_cdev.max_brightness; + /* If monocolor had less than 100% brightness on the previous boot, + * there will be no reliable way to determine the real intensity. + * Since intensity scaling is used with a hardware brightness set at max, + * our brightness will always look like 100%. Use the last set value to + * prevent successive boots from lowering the brightness further. + * Brightness will be "wrong" but the effect will remain the same visually. + */ + led_mc->subled_info[0].intensity = rgb_rep->red; + led_mc->subled_info[1].intensity = rgb_rep->green; + led_mc->subled_info[2].intensity = rgb_rep->blue; + + return 0; +} + static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) { @@ -172,6 +230,8 @@ static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report, switch (up) { case GEN1_USAGE_PAGE: return oxp_hid_raw_event_gen_1(hdev, report, data, size); + case GEN2_USAGE_PAGE: + return oxp_hid_raw_event_gen_2(hdev, report, data, size); default: break; } @@ -217,6 +277,18 @@ static int oxp_gen_1_property_out(enum oxp_function_index fid, u8 *data, return mcu_property_out(header, header_size, data, data_size, NULL, 0); } +static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, + u8 data_size) +{ + u8 header[] = { fid, GEN2_MESSAGE_ID, 0x01 }; + u8 footer[] = { GEN2_MESSAGE_ID, fid }; + size_t header_size = ARRAY_SIZE(header); + size_t footer_size = ARRAY_SIZE(footer); + + return mcu_property_out(header, header_size, data, data_size, footer, + footer_size); +} + static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness) { u16 up = get_usage_page(drvdata.hdev); @@ -231,6 +303,11 @@ static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness) if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST) data[3] = 0x04; return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 4); + case GEN2_USAGE_PAGE: + data = (u8[6]) { OXP_SET_PROPERTY, 0x00, 0x02, enabled, speed, brightness }; + if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST) + data[5] = 0x04; + return oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, 6); default: return -ENODEV; } @@ -245,6 +322,9 @@ static ssize_t oxp_rgb_status_show(void) case GEN1_USAGE_PAGE: data = (u8[1]) { OXP_GET_PROPERTY }; return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1); + case GEN2_USAGE_PAGE: + data = (u8[3]) { OXP_GET_PROPERTY, 0x00, 0x02 }; + return oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, 3); default: return -ENODEV; } @@ -275,6 +355,16 @@ static int oxp_rgb_color_set(void) data[3 * i + 3] = blue; } return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, size); + case GEN2_USAGE_PAGE: + size = 57; + data = (u8[57]) { OXP_EFFECT_MONO_TRUE, 0x00, 0x02 }; + + for (i = 1; i < size / 3; i++) { + data[3 * i] = red; + data[3 * i + 1] = green; + data[3 * i + 2] = blue; + } + return oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, size); default: return -ENODEV; } @@ -311,6 +401,10 @@ static int oxp_rgb_effect_set(u8 effect) data = (u8[1]) { effect }; ret = oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1); break; + case GEN2_USAGE_PAGE: + data = (u8[3]) { effect, 0x00, 0x02 }; + ret = oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, 3); + break; default: ret = -ENODEV; } @@ -559,6 +653,56 @@ static struct led_classdev_mc oxp_cdev_rgb = { .subled_info = oxp_rgb_subled_info, }; +struct quirk_entry { + bool hybrid_mcu; +}; + +static struct quirk_entry quirk_hybrid_mcu = { + .hybrid_mcu = true, +}; + +static const struct dmi_system_id oxp_hybrid_mcu_list[] = { + { + .ident = "OneXPlayer Apex", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"), + DMI_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER APEX"), + }, + .driver_data = &quirk_hybrid_mcu, + }, + { + .ident = "OneXPlayer G1 AMD", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"), + DMI_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER G1 A"), + }, + .driver_data = &quirk_hybrid_mcu, + }, + { + .ident = "OneXPlayer G1 Intel", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"), + DMI_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER G1 i"), + }, + .driver_data = &quirk_hybrid_mcu, + }, + {}, +}; + +static bool oxp_hybrid_mcu_device(void) +{ + const struct dmi_system_id *dmi_id; + struct quirk_entry *quirks; + + dmi_id = dmi_first_match(oxp_hybrid_mcu_list); + if (!dmi_id) + return false; + + quirks = dmi_id->driver_data; + + return quirks->hybrid_mcu; +} + static int oxp_cfg_probe(struct hid_device *hdev, u16 up) { int ret; @@ -566,6 +710,10 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up) hid_set_drvdata(hdev, &drvdata); mutex_init(&drvdata.cfg_mutex); drvdata.hdev = hdev; + + if (up == GEN2_USAGE_PAGE && oxp_hybrid_mcu_device()) + goto skip_rgb; + drvdata.led_mc = &oxp_cdev_rgb; INIT_DELAYED_WORK(&drvdata.oxp_rgb_queue, oxp_rgb_queue_fn); @@ -585,6 +733,7 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up) dev_warn(drvdata.led_mc->led_cdev.dev, "Failed to query RGB initial state: %i\n", ret); +skip_rgb: return 0; } @@ -613,6 +762,7 @@ static int oxp_hid_probe(struct hid_device *hdev, switch (up) { case GEN1_USAGE_PAGE: + case GEN2_USAGE_PAGE: ret = oxp_cfg_probe(hdev, up); if (ret) { hid_hw_close(hdev); @@ -634,6 +784,7 @@ static void oxp_hid_remove(struct hid_device *hdev) static const struct hid_device_id oxp_devices[] = { { HID_USB_DEVICE(USB_VENDOR_ID_CRSC, USB_DEVICE_ID_ONEXPLAYER_GEN1) }, + { HID_USB_DEVICE(USB_VENDOR_ID_WCH, USB_DEVICE_ID_ONEXPLAYER_GEN2) }, {} }; From 2f424f28fb39fa3afc1cd73d4191af0262016ea2 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 18 Apr 2026 21:26:22 -0700 Subject: [PATCH 08/39] HID: hid-oxp: Add Second Generation Gamepad Mode Switch Adds "gamepad_mode" attribute to second generation OneXPlayer configuration HID devices. This attribute initiates a mode shift in the device MCU that puts it into a state where all events are routed to an hidraw interface instead of the xpad evdev interface. This allows for debugging the hardware input mapping added in the next patch. Reviewed-by: Zhouwang Huang Tested-by: Zhouwang Huang Signed-off-by: Derek J. Clark Signed-off-by: Jiri Kosina --- drivers/hid/hid-oxp.c | 131 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c index 835de2118e3c..2504b56b8f8a 100644 --- a/drivers/hid/hid-oxp.c +++ b/drivers/hid/hid-oxp.c @@ -33,20 +33,33 @@ enum oxp_function_index { OXP_FID_GEN1_RGB_SET = 0x07, OXP_FID_GEN1_RGB_REPLY = 0x0f, + OXP_FID_GEN2_TOGGLE_MODE = 0xb2, OXP_FID_GEN2_STATUS_EVENT = 0xb8, }; static struct oxp_hid_cfg { struct delayed_work oxp_rgb_queue; + struct delayed_work oxp_mcu_init; struct led_classdev_mc *led_mc; struct hid_device *hdev; struct mutex cfg_mutex; /*ensure single synchronous output report*/ u8 rgb_brightness; + u8 gamepad_mode; u8 rgb_effect; u8 rgb_speed; u8 rgb_en; } drvdata; +enum oxp_gamepad_mode_index { + OXP_GP_MODE_XINPUT = 0x00, + OXP_GP_MODE_DEBUG = 0x03, +}; + +static const char *const oxp_gamepad_mode_text[] = { + [OXP_GP_MODE_XINPUT] = "xinput", + [OXP_GP_MODE_DEBUG] = "debug", +}; + enum oxp_feature_en_index { OXP_FEAT_DISABLED, OXP_FEAT_ENABLED, @@ -182,6 +195,30 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev, return 0; } +static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, u8 data_size); + +static void oxp_mcu_init_fn(struct work_struct *work) +{ + u8 gp_mode_data[3] = { OXP_GP_MODE_DEBUG, 0x01, 0x02 }; + int ret; + + /* Cycle the gamepad mode */ + ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, gp_mode_data, 3); + if (ret) + dev_err(&drvdata.hdev->dev, + "Error: Failed to set gamepad mode: %i\n", ret); + + /* Remainder only applies for xinput mode */ + if (drvdata.gamepad_mode == OXP_GP_MODE_DEBUG) + return; + + gp_mode_data[0] = OXP_GP_MODE_XINPUT; + ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, gp_mode_data, 3); + if (ret) + dev_err(&drvdata.hdev->dev, + "Error: Failed to set gamepad mode: %i\n", ret); +} + static int oxp_hid_raw_event_gen_2(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) @@ -192,6 +229,14 @@ static int oxp_hid_raw_event_gen_2(struct hid_device *hdev, if (data[0] != OXP_FID_GEN2_STATUS_EVENT) return 0; + /* Sent ~6s after resume event, indicating the MCU has fully reset. + * Re-apply our settings after this has been received. + */ + if (data[3] == OXP_EFFECT_MONO_TRUE) { + mod_delayed_work(system_wq, &drvdata.oxp_mcu_init, msecs_to_jiffies(50)); + return 0; + } + if (data[3] != OXP_GET_PROPERTY) return 0; @@ -289,6 +334,77 @@ static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, footer_size); } +static ssize_t gamepad_mode_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + u16 up = get_usage_page(drvdata.hdev); + u8 data[3] = { 0x00, 0x01, 0x02 }; + int ret = -EINVAL; + int i; + + if (up != GEN2_USAGE_PAGE) + return ret; + + for (i = 0; i < ARRAY_SIZE(oxp_gamepad_mode_text); i++) { + if (oxp_gamepad_mode_text[i] && sysfs_streq(buf, oxp_gamepad_mode_text[i])) { + ret = i; + break; + } + } + if (ret < 0) + return ret; + + data[0] = ret; + + ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, data, 3); + if (ret) + return ret; + + drvdata.gamepad_mode = data[0]; + + return count; +} + +static ssize_t gamepad_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "%s\n", oxp_gamepad_mode_text[drvdata.gamepad_mode]); +} +static DEVICE_ATTR_RW(gamepad_mode); + +static ssize_t gamepad_mode_index_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + ssize_t count = 0; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(oxp_gamepad_mode_text); i++) { + if (!oxp_gamepad_mode_text[i] || + oxp_gamepad_mode_text[i][0] == '\0') + continue; + + count += sysfs_emit_at(buf, count, "%s ", oxp_gamepad_mode_text[i]); + } + + if (count) + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(gamepad_mode_index); + +static struct attribute *oxp_cfg_attrs[] = { + &dev_attr_gamepad_mode.attr, + &dev_attr_gamepad_mode_index.attr, + NULL, +}; + +static const struct attribute_group oxp_cfg_attrs_group = { + .attrs = oxp_cfg_attrs, +}; + static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness) { u16 up = get_usage_page(drvdata.hdev); @@ -733,7 +849,21 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up) dev_warn(drvdata.led_mc->led_cdev.dev, "Failed to query RGB initial state: %i\n", ret); + /* Below features are only implemented in gen 2 */ + if (up != GEN2_USAGE_PAGE) + return 0; + skip_rgb: + drvdata.gamepad_mode = OXP_GP_MODE_XINPUT; + + INIT_DELAYED_WORK(&drvdata.oxp_mcu_init, oxp_mcu_init_fn); + mod_delayed_work(system_wq, &drvdata.oxp_mcu_init, msecs_to_jiffies(50)); + + ret = devm_device_add_group(&hdev->dev, &oxp_cfg_attrs_group); + if (ret) + return dev_err_probe(&hdev->dev, ret, + "Failed to attach configuration attributes\n"); + return 0; } @@ -778,6 +908,7 @@ static int oxp_hid_probe(struct hid_device *hdev, static void oxp_hid_remove(struct hid_device *hdev) { cancel_delayed_work(&drvdata.oxp_rgb_queue); + cancel_delayed_work(&drvdata.oxp_mcu_init); hid_hw_close(hdev); hid_hw_stop(hdev); } From e4c850a6e750ae9878e9a9329ba7863ab5b4a756 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 18 Apr 2026 21:26:23 -0700 Subject: [PATCH 09/39] HID: hid-oxp: Add Button Mapping Interface Adds button mapping interface for second generation OneXPlayer configuration HID interfaces. This interface allows the MCU to swap button mappings at the hardware level. The current state cannot be retrieved, and the mappings may have been modified in Windows prior, so we reset the button mapping at init and expose an attribute to allow userspace to do this again at any time. The interface requires two pages of button mapping data to be sent before the settings will take place. Since the MCU requires a 200ms delay after each message (total 400ms for these attributes) use the same debounce work queue method we used for RGB. This will allow for userspace or udev rules to rapidly map all buttons. The values will be cached before the final write is finally sent to the device. Reviewed-by: Zhouwang Huang Tested-by: Zhouwang Huang Signed-off-by: Derek J. Clark Signed-off-by: Jiri Kosina --- drivers/hid/hid-oxp.c | 568 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 568 insertions(+) diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c index 2504b56b8f8a..52002d4cbd0b 100644 --- a/drivers/hid/hid-oxp.c +++ b/drivers/hid/hid-oxp.c @@ -34,11 +34,147 @@ enum oxp_function_index { OXP_FID_GEN1_RGB_SET = 0x07, OXP_FID_GEN1_RGB_REPLY = 0x0f, OXP_FID_GEN2_TOGGLE_MODE = 0xb2, + OXP_FID_GEN2_KEY_STATE = 0xb4, OXP_FID_GEN2_STATUS_EVENT = 0xb8, }; +#define OXP_MAPPING_GAMEPAD 0x01 +#define OXP_MAPPING_KEYBOARD 0x02 + +struct oxp_button_data { + u8 mode; + u8 index; + u8 key_id; + u8 padding[2]; +} __packed; + +struct oxp_button_entry { + struct oxp_button_data data; + const char *name; +}; + +static const struct oxp_button_entry oxp_button_table[] = { + /* Gamepad Buttons */ + { { OXP_MAPPING_GAMEPAD, 0x01 }, "BTN_A" }, + { { OXP_MAPPING_GAMEPAD, 0x02 }, "BTN_B" }, + { { OXP_MAPPING_GAMEPAD, 0x03 }, "BTN_X" }, + { { OXP_MAPPING_GAMEPAD, 0x04 }, "BTN_Y" }, + { { OXP_MAPPING_GAMEPAD, 0x05 }, "BTN_LB" }, + { { OXP_MAPPING_GAMEPAD, 0x06 }, "BTN_RB" }, + { { OXP_MAPPING_GAMEPAD, 0x07 }, "BTN_LT" }, + { { OXP_MAPPING_GAMEPAD, 0x08 }, "BTN_RT" }, + { { OXP_MAPPING_GAMEPAD, 0x09 }, "BTN_START" }, + { { OXP_MAPPING_GAMEPAD, 0x0a }, "BTN_SELECT" }, + { { OXP_MAPPING_GAMEPAD, 0x0b }, "BTN_L3" }, + { { OXP_MAPPING_GAMEPAD, 0x0c }, "BTN_R3" }, + { { OXP_MAPPING_GAMEPAD, 0x0d }, "DPAD_UP" }, + { { OXP_MAPPING_GAMEPAD, 0x0e }, "DPAD_DOWN" }, + { { OXP_MAPPING_GAMEPAD, 0x0f }, "DPAD_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x10 }, "DPAD_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x11 }, "JOY_L_UP" }, + { { OXP_MAPPING_GAMEPAD, 0x12 }, "JOY_L_UP_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x13 }, "JOY_L_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x14 }, "JOY_L_DOWN_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x15 }, "JOY_L_DOWN" }, + { { OXP_MAPPING_GAMEPAD, 0x16 }, "JOY_L_DOWN_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x17 }, "JOY_L_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x18 }, "JOY_L_UP_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x19 }, "JOY_R_UP" }, + { { OXP_MAPPING_GAMEPAD, 0x1a }, "JOY_R_UP_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x1b }, "JOY_R_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x1c }, "JOY_R_DOWN_RIGHT" }, + { { OXP_MAPPING_GAMEPAD, 0x1d }, "JOY_R_DOWN" }, + { { OXP_MAPPING_GAMEPAD, 0x1e }, "JOY_R_DOWN_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x1f }, "JOY_R_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x20 }, "JOY_R_UP_LEFT" }, + { { OXP_MAPPING_GAMEPAD, 0x22 }, "BTN_GUIDE" }, + /* Keyboard Keys */ + { { OXP_MAPPING_KEYBOARD, 0x01, 0x5a }, "KEY_F1" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x5b }, "KEY_F2" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x5c }, "KEY_F3" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x5d }, "KEY_F4" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x5e }, "KEY_F5" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x5f }, "KEY_F6" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x60 }, "KEY_F7" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x61 }, "KEY_F8" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x62 }, "KEY_F9" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x63 }, "KEY_F10" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x64 }, "KEY_F11" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x65 }, "KEY_F12" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x66 }, "KEY_F13" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x67 }, "KEY_F14" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x68 }, "KEY_F15" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x69 }, "KEY_F16" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x6a }, "KEY_F17" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x6b }, "KEY_F18" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x6c }, "KEY_F19" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x6d }, "KEY_F20" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x6e }, "KEY_F21" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x6f }, "KEY_F22" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x70 }, "KEY_F23" }, + { { OXP_MAPPING_KEYBOARD, 0x01, 0x71 }, "KEY_F24" }, +}; + +enum oxp_joybutton_index { + BUTTON_A = 0x01, + BUTTON_B, + BUTTON_X, + BUTTON_Y, + BUTTON_LB, + BUTTON_RB, + BUTTON_LT, + BUTTON_RT, + BUTTON_START, + BUTTON_SELECT, + BUTTON_L3, + BUTTON_R3, + BUTTON_DUP, + BUTTON_DDOWN, + BUTTON_DLEFT, + BUTTON_DRIGHT, + BUTTON_M1 = 0x22, + BUTTON_M2, + /* These are unused currently, reserved for future devices */ + BUTTON_M3, + BUTTON_M4, + BUTTON_M5, + BUTTON_M6, +}; + +struct oxp_button_idx { + enum oxp_joybutton_index button_idx; + u8 mapping_idx; +} __packed; + +struct oxp_bmap_page_1 { + struct oxp_button_idx btn_a; + struct oxp_button_idx btn_b; + struct oxp_button_idx btn_x; + struct oxp_button_idx btn_y; + struct oxp_button_idx btn_lb; + struct oxp_button_idx btn_rb; + struct oxp_button_idx btn_lt; + struct oxp_button_idx btn_rt; + struct oxp_button_idx btn_start; +} __packed; + +struct oxp_bmap_page_2 { + struct oxp_button_idx btn_select; + struct oxp_button_idx btn_l3; + struct oxp_button_idx btn_r3; + struct oxp_button_idx btn_dup; + struct oxp_button_idx btn_ddown; + struct oxp_button_idx btn_dleft; + struct oxp_button_idx btn_dright; + struct oxp_button_idx btn_m1; + struct oxp_button_idx btn_m2; +} __packed; + static struct oxp_hid_cfg { struct delayed_work oxp_rgb_queue; + struct delayed_work oxp_btn_queue; + struct oxp_bmap_page_1 *bmap_1; + struct oxp_bmap_page_2 *bmap_2; struct delayed_work oxp_mcu_init; struct led_classdev_mc *led_mc; struct hid_device *hdev; @@ -50,6 +186,10 @@ static struct oxp_hid_cfg { u8 rgb_en; } drvdata; +#define OXP_FILL_PAGE_SLOT(page, btn) \ + { .button_idx = (page)->btn.button_idx, \ + .mapping_idx = (page)->btn.mapping_idx } + enum oxp_gamepad_mode_index { OXP_GP_MODE_XINPUT = 0x00, OXP_GP_MODE_DEBUG = 0x03, @@ -155,6 +295,10 @@ struct oxp_gen_2_rgb_report { u8 effect; } __packed; +struct oxp_attr { + u8 index; +}; + static u16 get_usage_page(struct hid_device *hdev) { return hdev->collection[0].usage >> 16; @@ -196,12 +340,19 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev, } static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, u8 data_size); +static int oxp_set_buttons(void); static void oxp_mcu_init_fn(struct work_struct *work) { u8 gp_mode_data[3] = { OXP_GP_MODE_DEBUG, 0x01, 0x02 }; int ret; + /* Re-apply the button mapping */ + ret = oxp_set_buttons(); + if (ret) + dev_err(&drvdata.hdev->dev, + "Error: Failed to set button mapping: %i\n", ret); + /* Cycle the gamepad mode */ ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, gp_mode_data, 3); if (ret) @@ -395,9 +546,408 @@ static ssize_t gamepad_mode_index_show(struct device *dev, } static DEVICE_ATTR_RO(gamepad_mode_index); +static void oxp_set_defaults_bmap_1(struct oxp_bmap_page_1 *bmap) +{ + bmap->btn_a.button_idx = BUTTON_A; + bmap->btn_a.mapping_idx = 0; + bmap->btn_b.button_idx = BUTTON_B; + bmap->btn_b.mapping_idx = 1; + bmap->btn_x.button_idx = BUTTON_X; + bmap->btn_x.mapping_idx = 2; + bmap->btn_y.button_idx = BUTTON_Y; + bmap->btn_y.mapping_idx = 3; + bmap->btn_lb.button_idx = BUTTON_LB; + bmap->btn_lb.mapping_idx = 4; + bmap->btn_rb.button_idx = BUTTON_RB; + bmap->btn_rb.mapping_idx = 5; + bmap->btn_lt.button_idx = BUTTON_LT; + bmap->btn_lt.mapping_idx = 6; + bmap->btn_rt.button_idx = BUTTON_RT; + bmap->btn_rt.mapping_idx = 7; + bmap->btn_start.button_idx = BUTTON_START; + bmap->btn_start.mapping_idx = 8; +} + +static void oxp_set_defaults_bmap_2(struct oxp_bmap_page_2 *bmap) +{ + bmap->btn_select.button_idx = BUTTON_SELECT; + bmap->btn_select.mapping_idx = 9; + bmap->btn_l3.button_idx = BUTTON_L3; + bmap->btn_l3.mapping_idx = 10; + bmap->btn_r3.button_idx = BUTTON_R3; + bmap->btn_r3.mapping_idx = 11; + bmap->btn_dup.button_idx = BUTTON_DUP; + bmap->btn_dup.mapping_idx = 12; + bmap->btn_ddown.button_idx = BUTTON_DDOWN; + bmap->btn_ddown.mapping_idx = 13; + bmap->btn_dleft.button_idx = BUTTON_DLEFT; + bmap->btn_dleft.mapping_idx = 14; + bmap->btn_dright.button_idx = BUTTON_DRIGHT; + bmap->btn_dright.mapping_idx = 15; + bmap->btn_m1.button_idx = BUTTON_M1; + bmap->btn_m1.mapping_idx = 48; /* KEY_F15 */ + bmap->btn_m2.button_idx = BUTTON_M2; + bmap->btn_m2.mapping_idx = 49; /* KEY_F16 */ +} + +static void oxp_page_fill_data(char *buf, const struct oxp_button_idx *buttons, + size_t len) +{ + size_t offset_increment = sizeof(u8) + sizeof(struct oxp_button_idx); + size_t offset = 5; + unsigned int i; + + for (i = 0; i < len; i++, offset += offset_increment) { + buf[offset] = (u8)buttons[i].button_idx; + memcpy(buf + offset + 1, + &oxp_button_table[buttons[i].mapping_idx].data, + sizeof(struct oxp_button_data)); + } +} + +static int oxp_set_buttons(void) +{ + u8 page_1[59] = { 0x02, 0x38, 0x20, 0x01, 0x01 }; + u8 page_2[59] = { 0x02, 0x38, 0x20, 0x02, 0x01 }; + u16 up = get_usage_page(drvdata.hdev); + int ret; + + if (up != GEN2_USAGE_PAGE) + return -EINVAL; + + const struct oxp_button_idx p1[] = { + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_a), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_b), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_x), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_y), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_lb), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_rb), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_lt), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_rt), + OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_start), + }; + + const struct oxp_button_idx p2[] = { + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_select), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_l3), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_r3), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_dup), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_ddown), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_dleft), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_dright), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_m1), + OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_m2), + }; + + oxp_page_fill_data(page_1, p1, ARRAY_SIZE(p1)); + oxp_page_fill_data(page_2, p2, ARRAY_SIZE(p2)); + + ret = oxp_gen_2_property_out(OXP_FID_GEN2_KEY_STATE, page_1, ARRAY_SIZE(page_1)); + if (ret) + return ret; + + return oxp_gen_2_property_out(OXP_FID_GEN2_KEY_STATE, page_2, ARRAY_SIZE(page_2)); +} + +static void oxp_reset_buttons(void) +{ + oxp_set_defaults_bmap_1(drvdata.bmap_1); + oxp_set_defaults_bmap_2(drvdata.bmap_2); +} + +static ssize_t reset_buttons_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + int val, ret; + + ret = kstrtoint(buf, 10, &val); + if (ret) + return ret; + + if (val != 1) + return -EINVAL; + + oxp_reset_buttons(); + ret = oxp_set_buttons(); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_WO(reset_buttons); + +static void oxp_btn_queue_fn(struct work_struct *work) +{ + int ret; + + ret = oxp_set_buttons(); + if (ret) + dev_err(&drvdata.hdev->dev, + "Error: Failed to write button mapping: %i\n", ret); +} + +static int oxp_button_idx_from_str(const char *buf) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(oxp_button_table); i++) + if (sysfs_streq(buf, oxp_button_table[i].name)) + return i; + + return -EINVAL; +} + +static ssize_t map_button_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count, u8 index) +{ + int idx; + + idx = oxp_button_idx_from_str(buf); + if (idx < 0) + return idx; + + switch (index) { + case BUTTON_A: + drvdata.bmap_1->btn_a.mapping_idx = idx; + break; + case BUTTON_B: + drvdata.bmap_1->btn_b.mapping_idx = idx; + break; + case BUTTON_X: + drvdata.bmap_1->btn_x.mapping_idx = idx; + break; + case BUTTON_Y: + drvdata.bmap_1->btn_y.mapping_idx = idx; + break; + case BUTTON_LB: + drvdata.bmap_1->btn_lb.mapping_idx = idx; + break; + case BUTTON_RB: + drvdata.bmap_1->btn_rb.mapping_idx = idx; + break; + case BUTTON_LT: + drvdata.bmap_1->btn_lt.mapping_idx = idx; + break; + case BUTTON_RT: + drvdata.bmap_1->btn_rt.mapping_idx = idx; + break; + case BUTTON_START: + drvdata.bmap_1->btn_start.mapping_idx = idx; + break; + case BUTTON_SELECT: + drvdata.bmap_2->btn_select.mapping_idx = idx; + break; + case BUTTON_L3: + drvdata.bmap_2->btn_l3.mapping_idx = idx; + break; + case BUTTON_R3: + drvdata.bmap_2->btn_r3.mapping_idx = idx; + break; + case BUTTON_DUP: + drvdata.bmap_2->btn_dup.mapping_idx = idx; + break; + case BUTTON_DDOWN: + drvdata.bmap_2->btn_ddown.mapping_idx = idx; + break; + case BUTTON_DLEFT: + drvdata.bmap_2->btn_dleft.mapping_idx = idx; + break; + case BUTTON_DRIGHT: + drvdata.bmap_2->btn_dright.mapping_idx = idx; + break; + case BUTTON_M1: + drvdata.bmap_2->btn_m1.mapping_idx = idx; + break; + case BUTTON_M2: + drvdata.bmap_2->btn_m2.mapping_idx = idx; + break; + default: + return -EINVAL; + } + mod_delayed_work(system_wq, &drvdata.oxp_btn_queue, msecs_to_jiffies(50)); + return count; +} + +static ssize_t map_button_show(struct device *dev, + struct device_attribute *attr, char *buf, + u8 index) +{ + u8 i; + + switch (index) { + case BUTTON_A: + i = drvdata.bmap_1->btn_a.mapping_idx; + break; + case BUTTON_B: + i = drvdata.bmap_1->btn_b.mapping_idx; + break; + case BUTTON_X: + i = drvdata.bmap_1->btn_x.mapping_idx; + break; + case BUTTON_Y: + i = drvdata.bmap_1->btn_y.mapping_idx; + break; + case BUTTON_LB: + i = drvdata.bmap_1->btn_lb.mapping_idx; + break; + case BUTTON_RB: + i = drvdata.bmap_1->btn_rb.mapping_idx; + break; + case BUTTON_LT: + i = drvdata.bmap_1->btn_lt.mapping_idx; + break; + case BUTTON_RT: + i = drvdata.bmap_1->btn_rt.mapping_idx; + break; + case BUTTON_START: + i = drvdata.bmap_1->btn_start.mapping_idx; + break; + case BUTTON_SELECT: + i = drvdata.bmap_2->btn_select.mapping_idx; + break; + case BUTTON_L3: + i = drvdata.bmap_2->btn_l3.mapping_idx; + break; + case BUTTON_R3: + i = drvdata.bmap_2->btn_r3.mapping_idx; + break; + case BUTTON_DUP: + i = drvdata.bmap_2->btn_dup.mapping_idx; + break; + case BUTTON_DDOWN: + i = drvdata.bmap_2->btn_ddown.mapping_idx; + break; + case BUTTON_DLEFT: + i = drvdata.bmap_2->btn_dleft.mapping_idx; + break; + case BUTTON_DRIGHT: + i = drvdata.bmap_2->btn_dright.mapping_idx; + break; + case BUTTON_M1: + i = drvdata.bmap_2->btn_m1.mapping_idx; + break; + case BUTTON_M2: + i = drvdata.bmap_2->btn_m2.mapping_idx; + break; + default: + return -EINVAL; + } + + if (i >= ARRAY_SIZE(oxp_button_table)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", oxp_button_table[i].name); +} + +static ssize_t button_mapping_options_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t count = 0; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(oxp_button_table); i++) + count += sysfs_emit_at(buf, count, "%s ", oxp_button_table[i].name); + + if (count) + buf[count - 1] = '\n'; + + return count; +} +static DEVICE_ATTR_RO(button_mapping_options); + +#define OXP_DEVICE_ATTR_RW(_name, _group) \ + static ssize_t _name##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return _group##_store(dev, attr, buf, count, _name.index); \ + } \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + return _group##_show(dev, attr, buf, _name.index); \ + } \ + static DEVICE_ATTR_RW(_name) + +static struct oxp_attr button_a = { BUTTON_A }; +OXP_DEVICE_ATTR_RW(button_a, map_button); + +static struct oxp_attr button_b = { BUTTON_B }; +OXP_DEVICE_ATTR_RW(button_b, map_button); + +static struct oxp_attr button_x = { BUTTON_X }; +OXP_DEVICE_ATTR_RW(button_x, map_button); + +static struct oxp_attr button_y = { BUTTON_Y }; +OXP_DEVICE_ATTR_RW(button_y, map_button); + +static struct oxp_attr button_lb = { BUTTON_LB }; +OXP_DEVICE_ATTR_RW(button_lb, map_button); + +static struct oxp_attr button_rb = { BUTTON_RB }; +OXP_DEVICE_ATTR_RW(button_rb, map_button); + +static struct oxp_attr button_lt = { BUTTON_LT }; +OXP_DEVICE_ATTR_RW(button_lt, map_button); + +static struct oxp_attr button_rt = { BUTTON_RT }; +OXP_DEVICE_ATTR_RW(button_rt, map_button); + +static struct oxp_attr button_start = { BUTTON_START }; +OXP_DEVICE_ATTR_RW(button_start, map_button); + +static struct oxp_attr button_select = { BUTTON_SELECT }; +OXP_DEVICE_ATTR_RW(button_select, map_button); + +static struct oxp_attr button_l3 = { BUTTON_L3 }; +OXP_DEVICE_ATTR_RW(button_l3, map_button); + +static struct oxp_attr button_r3 = { BUTTON_R3 }; +OXP_DEVICE_ATTR_RW(button_r3, map_button); + +static struct oxp_attr button_d_up = { BUTTON_DUP }; +OXP_DEVICE_ATTR_RW(button_d_up, map_button); + +static struct oxp_attr button_d_down = { BUTTON_DDOWN }; +OXP_DEVICE_ATTR_RW(button_d_down, map_button); + +static struct oxp_attr button_d_left = { BUTTON_DLEFT }; +OXP_DEVICE_ATTR_RW(button_d_left, map_button); + +static struct oxp_attr button_d_right = { BUTTON_DRIGHT }; +OXP_DEVICE_ATTR_RW(button_d_right, map_button); + +static struct oxp_attr button_m1 = { BUTTON_M1 }; +OXP_DEVICE_ATTR_RW(button_m1, map_button); + +static struct oxp_attr button_m2 = { BUTTON_M2 }; +OXP_DEVICE_ATTR_RW(button_m2, map_button); + static struct attribute *oxp_cfg_attrs[] = { + &dev_attr_button_a.attr, + &dev_attr_button_b.attr, + &dev_attr_button_d_down.attr, + &dev_attr_button_d_left.attr, + &dev_attr_button_d_right.attr, + &dev_attr_button_d_up.attr, + &dev_attr_button_l3.attr, + &dev_attr_button_lb.attr, + &dev_attr_button_lt.attr, + &dev_attr_button_m1.attr, + &dev_attr_button_m2.attr, + &dev_attr_button_mapping_options.attr, + &dev_attr_button_r3.attr, + &dev_attr_button_rb.attr, + &dev_attr_button_rt.attr, + &dev_attr_button_select.attr, + &dev_attr_button_start.attr, + &dev_attr_button_x.attr, + &dev_attr_button_y.attr, &dev_attr_gamepad_mode.attr, &dev_attr_gamepad_mode_index.attr, + &dev_attr_reset_buttons.attr, NULL, }; @@ -821,6 +1371,8 @@ static bool oxp_hybrid_mcu_device(void) static int oxp_cfg_probe(struct hid_device *hdev, u16 up) { + struct oxp_bmap_page_1 *bmap_1; + struct oxp_bmap_page_2 *bmap_2; int ret; hid_set_drvdata(hdev, &drvdata); @@ -854,6 +1406,21 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up) return 0; skip_rgb: + bmap_1 = devm_kzalloc(&hdev->dev, sizeof(struct oxp_bmap_page_1), GFP_KERNEL); + if (!bmap_1) + return dev_err_probe(&hdev->dev, -ENOMEM, + "Unable to allocate button map page 1\n"); + + bmap_2 = devm_kzalloc(&hdev->dev, sizeof(struct oxp_bmap_page_2), GFP_KERNEL); + if (!bmap_2) + return dev_err_probe(&hdev->dev, -ENOMEM, + "Unable to allocate button map page 2\n"); + + drvdata.bmap_1 = bmap_1; + drvdata.bmap_2 = bmap_2; + oxp_reset_buttons(); + INIT_DELAYED_WORK(&drvdata.oxp_btn_queue, oxp_btn_queue_fn); + drvdata.gamepad_mode = OXP_GP_MODE_XINPUT; INIT_DELAYED_WORK(&drvdata.oxp_mcu_init, oxp_mcu_init_fn); @@ -908,6 +1475,7 @@ static int oxp_hid_probe(struct hid_device *hdev, static void oxp_hid_remove(struct hid_device *hdev) { cancel_delayed_work(&drvdata.oxp_rgb_queue); + cancel_delayed_work(&drvdata.oxp_btn_queue); cancel_delayed_work(&drvdata.oxp_mcu_init); hid_hw_close(hdev); hid_hw_stop(hdev); From 99bde1dfe878e990655142a2eddd187e84735198 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Sat, 18 Apr 2026 21:26:24 -0700 Subject: [PATCH 10/39] HID: hid-oxp: Add Vibration Intensity Attribute Adds attribute for setting the rumble intensity level. This setting must be re-applied after the gamepad mode is set as doing so resets this to the default value. Reviewed-by: Zhouwang Huang Tested-by: Zhouwang Huang Signed-off-by: Derek J. Clark Signed-off-by: Jiri Kosina --- drivers/hid/hid-oxp.c | 78 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c index 52002d4cbd0b..20a54f337220 100644 --- a/drivers/hid/hid-oxp.c +++ b/drivers/hid/hid-oxp.c @@ -34,6 +34,7 @@ enum oxp_function_index { OXP_FID_GEN1_RGB_SET = 0x07, OXP_FID_GEN1_RGB_REPLY = 0x0f, OXP_FID_GEN2_TOGGLE_MODE = 0xb2, + OXP_FID_GEN2_RUMBLE_SET = 0xb3, OXP_FID_GEN2_KEY_STATE = 0xb4, OXP_FID_GEN2_STATUS_EVENT = 0xb8, }; @@ -181,6 +182,7 @@ static struct oxp_hid_cfg { struct mutex cfg_mutex; /*ensure single synchronous output report*/ u8 rgb_brightness; u8 gamepad_mode; + u8 rumble_intensity; u8 rgb_effect; u8 rgb_speed; u8 rgb_en; @@ -266,6 +268,11 @@ static const char *const oxp_rgb_effect_text[] = { [OXP_EFFECT_MONO_LIST] = "monocolor", }; +enum oxp_rumble_side_index { + OXP_RUMBLE_LEFT = 0x00, + OXP_RUMBLE_RIGHT, +}; + struct oxp_gen_1_rgb_report { u8 report_id; u8 message_id; @@ -341,6 +348,7 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev, static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, u8 data_size); static int oxp_set_buttons(void); +static int oxp_rumble_intensity_set(u8 intensity); static void oxp_mcu_init_fn(struct work_struct *work) { @@ -368,6 +376,12 @@ static void oxp_mcu_init_fn(struct work_struct *work) if (ret) dev_err(&drvdata.hdev->dev, "Error: Failed to set gamepad mode: %i\n", ret); + + /* Set vibration level */ + ret = oxp_rumble_intensity_set(drvdata.rumble_intensity); + if (ret) + dev_err(&drvdata.hdev->dev, + "Error: Failed to set rumble intensity: %i\n", ret); } static int oxp_hid_raw_event_gen_2(struct hid_device *hdev, @@ -514,6 +528,14 @@ static ssize_t gamepad_mode_store(struct device *dev, drvdata.gamepad_mode = data[0]; + if (drvdata.gamepad_mode == OXP_GP_MODE_DEBUG) + return count; + + /* Re-apply rumble settings as switching gamepad mode will override */ + ret = oxp_rumble_intensity_set(drvdata.rumble_intensity); + if (ret) + return ret; + return count; } @@ -857,6 +879,59 @@ static ssize_t button_mapping_options_show(struct device *dev, } static DEVICE_ATTR_RO(button_mapping_options); +static int oxp_rumble_intensity_set(u8 intensity) +{ + u8 header[15] = { 0x02, 0x38, 0x02, 0xe3, 0x39, 0xe3, 0x39, 0xe3, + 0x39, 0x01, intensity, 0x05, 0xe3, 0x39, 0xe3 }; + u8 footer[9] = { 0x39, 0xe3, 0x39, 0xe3, 0xe3, 0x02, 0x04, 0x39, 0x39 }; + size_t footer_size = ARRAY_SIZE(footer); + size_t header_size = ARRAY_SIZE(header); + u8 data[59] = { 0x0 }; + size_t data_size = ARRAY_SIZE(data); + + memcpy(data, header, header_size); + memcpy(data + data_size - footer_size, footer, footer_size); + + return oxp_gen_2_property_out(OXP_FID_GEN2_RUMBLE_SET, data, data_size); +} + +static ssize_t rumble_intensity_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + int ret; + u8 val; + + ret = kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val < 0 || val > 5) + return -EINVAL; + + ret = oxp_rumble_intensity_set(val); + if (ret) + return ret; + + drvdata.rumble_intensity = val; + + return count; +} + +static ssize_t rumble_intensity_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "%i\n", drvdata.rumble_intensity); +} +static DEVICE_ATTR_RW(rumble_intensity); + +static ssize_t rumble_intensity_range_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0-5\n"); +} +static DEVICE_ATTR_RO(rumble_intensity_range); + #define OXP_DEVICE_ATTR_RW(_name, _group) \ static ssize_t _name##_store(struct device *dev, \ struct device_attribute *attr, \ @@ -948,6 +1023,8 @@ static struct attribute *oxp_cfg_attrs[] = { &dev_attr_gamepad_mode.attr, &dev_attr_gamepad_mode_index.attr, &dev_attr_reset_buttons.attr, + &dev_attr_rumble_intensity.attr, + &dev_attr_rumble_intensity_range.attr, NULL, }; @@ -1422,6 +1499,7 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up) INIT_DELAYED_WORK(&drvdata.oxp_btn_queue, oxp_btn_queue_fn); drvdata.gamepad_mode = OXP_GP_MODE_XINPUT; + drvdata.rumble_intensity = 5; INIT_DELAYED_WORK(&drvdata.oxp_mcu_init, oxp_mcu_init_fn); mod_delayed_work(system_wq, &drvdata.oxp_mcu_init, msecs_to_jiffies(50)); From 08918b98b02b5537c43cf4d591bfd8591cc3eb3b Mon Sep 17 00:00:00 2001 From: James Clark Date: Mon, 18 May 2026 10:03:09 +0100 Subject: [PATCH 11/39] selftests/hid: Remove unused LLD variable This file was mostly copied from selftests/bpf/Makefile, but the LLD variable is not used here. Also, this copied block didn't get the same fixes as the original one did later. Remove it to avoid confusion and so future fixes don't have to be in two places. Signed-off-by: James Clark Signed-off-by: Benjamin Tissoires --- tools/testing/selftests/hid/Makefile | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tools/testing/selftests/hid/Makefile b/tools/testing/selftests/hid/Makefile index 50ec9e0406ab..96071b4800e8 100644 --- a/tools/testing/selftests/hid/Makefile +++ b/tools/testing/selftests/hid/Makefile @@ -105,13 +105,6 @@ $(MAKE_DIRS): $(call msg,MKDIR,,$@) $(Q)mkdir -p $@ -# LLVM's ld.lld doesn't support all the architectures, so use it only on x86 -ifeq ($(SRCARCH),x86) -LLD := lld -else -LLD := ld -endif - DEFAULT_BPFTOOL := $(HOST_SCRATCH_DIR)/sbin/bpftool TEST_GEN_PROGS_EXTENDED += $(DEFAULT_BPFTOOL) From 857e71cb0a538b1660743a4267a1e789575f7966 Mon Sep 17 00:00:00 2001 From: Nikhil Chatterjee Date: Sat, 25 Apr 2026 20:18:19 -0700 Subject: [PATCH 12/39] HID: bpf: Add Huion Inspiroy Frego M button quirk The Huion Inspiroy Frego M pen report descriptor exposes the second side button as Secondary Tip Switch instead of Secondary Barrel Switch. This makes userspace see the control as the wrong pen button. Add a HID-BPF report descriptor fixup for the Bluetooth 256c:8251 device and USB 256c:2012 L610 variant. The fixup matches the expected pen descriptor and rewrites the offending usage from Secondary Tip Switch to Secondary Barrel Switch. Tested by building the HID-BPF object with: make -C drivers/hid/bpf/progs Huion__Inspiroy-Frego-M.bpf.o Signed-off-by: Nikhil Chatterjee Signed-off-by: Benjamin Tissoires --- .../bpf/progs/Huion__Inspiroy-Frego-M.bpf.c | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 drivers/hid/bpf/progs/Huion__Inspiroy-Frego-M.bpf.c diff --git a/drivers/hid/bpf/progs/Huion__Inspiroy-Frego-M.bpf.c b/drivers/hid/bpf/progs/Huion__Inspiroy-Frego-M.bpf.c new file mode 100644 index 000000000000..e6ba2295dc77 --- /dev/null +++ b/drivers/hid/bpf/progs/Huion__Inspiroy-Frego-M.bpf.c @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "vmlinux.h" +#include "hid_bpf.h" +#include "hid_bpf_helpers.h" +#include + +/* + * Huion Inspiroy Frego M Pen Tablet + * Model L610 + * 256c:8251 (Bluetooth) + * 256c:2012 (USB) + */ +#define VID_HUION 0x256C +#define PID_INSPIROY_FREGO_M 0x8251 +#define PID_L610 0x2012 + +#define PEN_RDESC_SIZE 125 +#define SECONDARY_SWITCH_OFFSET 17 + +HID_BPF_CONFIG( + HID_DEVICE(BUS_BLUETOOTH, HID_GROUP_GENERIC, VID_HUION, PID_INSPIROY_FREGO_M), + HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_HUION, PID_L610) +); + +/* + * The pen descriptor reports the second side button as Secondary Tip Switch + * instead of Secondary Barrel Switch. + * + * Relevant part of the original pen report descriptor: + * + * 0x09, 0x42, // Usage (Tip Switch) 12 + * 0x09, 0x44, // Usage (Barrel Switch) 14 + * 0x09, 0x43, // Usage (Secondary Tip Switch) 16 <- change to 0x5a + * 0x09, 0x3c, // Usage (Invert) 18 + * 0x09, 0x45, // Usage (Eraser) 20 + * 0x15, 0x00, // Logical Minimum (0) 22 + * 0x25, 0x01, // Logical Maximum (1) 24 + */ +SEC(HID_BPF_RDESC_FIXUP) +int BPF_PROG(fix_secondary_barrel_rdesc, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, HID_MAX_DESCRIPTOR_SIZE /* size */); + + if (!data) + return 0; /* EPERM check */ + + if (hctx->size != PEN_RDESC_SIZE) + return 0; + + if (data[0] != 0x05 || data[1] != 0x0d || /* Usage Page (Digitizers) */ + data[2] != 0x09 || data[3] != 0x02 || /* Usage (Pen) */ + data[16] != 0x09 || + data[SECONDARY_SWITCH_OFFSET] != 0x43) /* Secondary Tip Switch */ + return 0; + + data[SECONDARY_SWITCH_OFFSET] = 0x5a; + + return 0; +} + +HID_BPF_OPS(fix_secondary_barrel) = { + .hid_rdesc_fixup = (void *)fix_secondary_barrel_rdesc, +}; + +SEC("syscall") +int probe(struct hid_bpf_probe_args *ctx) +{ + ctx->retval = ctx->rdesc_size != PEN_RDESC_SIZE; + if (ctx->retval) { + ctx->retval = -EINVAL; + return 0; + } + + if (ctx->rdesc[0] != 0x05 || ctx->rdesc[1] != 0x0d || /* Usage Page (Digitizers) */ + ctx->rdesc[2] != 0x09 || ctx->rdesc[3] != 0x02 || /* Usage (Pen) */ + ctx->rdesc[16] != 0x09 || + ctx->rdesc[SECONDARY_SWITCH_OFFSET] != 0x43) { /* Secondary Tip Switch */ + ctx->retval = -EINVAL; + return 0; + } + + ctx->retval = 0; + + return 0; +} + +char _license[] SEC("license") = "GPL"; From 12b7731995ca577d86e02196e99ba9c126f47282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Thu, 26 Mar 2026 15:03:48 +0100 Subject: [PATCH 13/39] HID: wiimote: Fix table layout and whitespace errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some tab characters snuck into the data layout table for turntable extensions, which resulted in the table only looking right at a tabstop of 4, which is uncommon in the kernel. Change them to the equivalent amount of spaces, which should look correct in any editor. While at it, also fix the other whitespace errors (trailing spaces at end of line) introduced in the same commit. Fixes: 05086f3db530b3 ("HID: wiimote: Add support for the DJ Hero turntable") Reviewed-by: David Rheinsberg Signed-off-by: J. Neuschäfer Signed-off-by: Benjamin Tissoires --- drivers/hid/hid-wiimote-modules.c | 58 +++++++++++++++---------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/drivers/hid/hid-wiimote-modules.c b/drivers/hid/hid-wiimote-modules.c index dbccdfa63916..dccb78bb3afd 100644 --- a/drivers/hid/hid-wiimote-modules.c +++ b/drivers/hid/hid-wiimote-modules.c @@ -2403,7 +2403,7 @@ static const struct wiimod_ops wiimod_guitar = { .in_ext = wiimod_guitar_in_ext, }; -/* +/* * Turntable * DJ Hero came with a Turntable Controller that was plugged in * as an extension. @@ -2439,15 +2439,15 @@ static const __u16 wiimod_turntable_map[] = { static void wiimod_turntable_in_ext(struct wiimote_data *wdata, const __u8 *ext) { __u8 be, cs, sx, sy, ed, rtt, rbg, rbr, rbb, ltt, lbg, lbr, lbb, bp, bm; - /* + /* * Byte | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | *------+------+-----+-----+-----+-----+------+------+--------+ - * 0 | RTT<4:3> | SX <5:0> | - * 1 | RTT<2:1> | SY <5:0> | + * 0 | RTT<4:3> | SX <5:0> | + * 1 | RTT<2:1> | SY <5:0> | *------+------+-----+-----+-----+-----+------+------+--------+ * 2 |RTT<0>| ED<4:3> | CS<3:0> | RTT<5> | *------+------+-----+-----+-----+-----+------+------+--------+ - * 3 | ED<2:0> | LTT<4:0> | + * 3 | ED<2:0> | LTT<4:0> | *------+------+-----+-----+-----+-----+------+------+--------+ * 4 | 0 | 0 | LBR | B- | 0 | B+ | RBR | LTT<5> | *------+------+-----+-----+-----+-----+------+------+--------+ @@ -2458,20 +2458,20 @@ static void wiimod_turntable_in_ext(struct wiimote_data *wdata, const __u8 *ext) * With Motion+ enabled, it will look like this: * Byte | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | *------+------+-----+-----+-----+-----+------+------+--------+ - * 1 | RTT<4:3> | SX <5:1> | 0 | - * 2 | RTT<2:1> | SY <5:1> | 0 | + * 1 | RTT<4:3> | SX <5:1> | 0 | + * 2 | RTT<2:1> | SY <5:1> | 0 | *------+------+-----+-----+-----+-----+------+------+--------+ * 3 |RTT<0>| ED<4:3> | CS<3:0> | RTT<5> | *------+------+-----+-----+-----+-----+------+------+--------+ - * 4 | ED<2:0> | LTT<4:0> | + * 4 | ED<2:0> | LTT<4:0> | *------+------+-----+-----+-----+-----+------+------+--------+ * 5 | 0 | 0 | LBR | B- | 0 | B+ | RBR | XXXX | *------+------+-----+-----+-----+-----+------+------+--------+ * 6 | LBB | 0 | RBG | BE | LBG | RBB | XXXX | XXXX | *------+------+-----+-----+-----+-----+------+------+--------+ */ - - be = !(ext[5] & 0x10); + + be = !(ext[5] & 0x10); cs = ((ext[2] & 0x1e)); sx = ext[0] & 0x3f; sy = ext[1] & 0x3f; @@ -2499,32 +2499,32 @@ static void wiimod_turntable_in_ext(struct wiimote_data *wdata, const __u8 *ext) input_report_abs(wdata->extension.input, ABS_HAT1X, ltt); input_report_abs(wdata->extension.input, ABS_HAT2X, cs); input_report_abs(wdata->extension.input, ABS_HAT3X, ed); - input_report_key(wdata->extension.input, - wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_G_RIGHT], + input_report_key(wdata->extension.input, + wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_G_RIGHT], rbg); input_report_key(wdata->extension.input, wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_R_RIGHT], rbr); - input_report_key(wdata->extension.input, - wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_B_RIGHT], + input_report_key(wdata->extension.input, + wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_B_RIGHT], rbb); - input_report_key(wdata->extension.input, - wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_G_LEFT], + input_report_key(wdata->extension.input, + wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_G_LEFT], lbg); - input_report_key(wdata->extension.input, - wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_R_LEFT], + input_report_key(wdata->extension.input, + wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_R_LEFT], lbr); - input_report_key(wdata->extension.input, - wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_B_LEFT], + input_report_key(wdata->extension.input, + wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_B_LEFT], lbb); - input_report_key(wdata->extension.input, - wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_EUPHORIA], + input_report_key(wdata->extension.input, + wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_EUPHORIA], be); - input_report_key(wdata->extension.input, - wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_PLUS], + input_report_key(wdata->extension.input, + wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_PLUS], bp); - input_report_key(wdata->extension.input, - wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_MINUS], + input_report_key(wdata->extension.input, + wiimod_turntable_map[WIIMOD_TURNTABLE_KEY_MINUS], bm); input_sync(wdata->extension.input); @@ -2557,7 +2557,7 @@ static void wiimod_turntable_close(struct input_dev *dev) static int wiimod_turntable_probe(const struct wiimod_ops *ops, struct wiimote_data *wdata) { - int ret, i; + int ret, i; wdata->extension.input = input_allocate_device(); if (!wdata->extension.input) @@ -2594,9 +2594,9 @@ static int wiimod_turntable_probe(const struct wiimod_ops *ops, input_set_abs_params(wdata->extension.input, ABS_HAT1X, -8, 8, 0, 0); input_set_abs_params(wdata->extension.input, - ABS_HAT2X, 0, 31, 1, 1); + ABS_HAT2X, 0, 31, 1, 1); input_set_abs_params(wdata->extension.input, - ABS_HAT3X, 0, 7, 0, 0); + ABS_HAT3X, 0, 7, 0, 0); ret = input_register_device(wdata->extension.input); if (ret) goto err_free; From eda0f9e5708786c1d0dfc582f3b127708dfbf835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig=20=28The=20Capable=20Hub=29?= Date: Tue, 19 May 2026 18:04:20 +0200 Subject: [PATCH 14/39] HID: i2c-hid-of: Use named initializers for struct i2c_device_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While being less compact, using named initializers allows to more easily see which members of the structs are assigned which value without having to lookup the declaration of the struct. And it's also more robust against changes to the struct definition. This patch doesn't modify the compiled array, only its representation in source form benefits. The former was confirmed with x86 and arm64 builds. Signed-off-by: Uwe Kleine-König (The Capable Hub) Signed-off-by: Benjamin Tissoires --- drivers/hid/i2c-hid/i2c-hid-of.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/hid/i2c-hid/i2c-hid-of.c b/drivers/hid/i2c-hid/i2c-hid-of.c index 57379b77e977..59393d71ddb9 100644 --- a/drivers/hid/i2c-hid/i2c-hid-of.c +++ b/drivers/hid/i2c-hid/i2c-hid-of.c @@ -144,8 +144,8 @@ MODULE_DEVICE_TABLE(of, i2c_hid_of_match); #endif static const struct i2c_device_id i2c_hid_of_id_table[] = { - { "hid" }, - { "hid-over-i2c" }, + { .name = "hid" }, + { .name = "hid-over-i2c" }, { } }; MODULE_DEVICE_TABLE(i2c, i2c_hid_of_id_table); From 4de4b8a5ddc2c5a2e0f62c72864caab7027ea67a Mon Sep 17 00:00:00 2001 From: "Pawel Zalewski (The Capable Hub)" Date: Mon, 18 May 2026 17:06:24 +0100 Subject: [PATCH 15/39] HID: hid-belkin: clean up usage of 'driver_data' The module is storing an integer inside the drvdata pointer, which is confusing, lets fix this and set the whole of 'hid_device_id' struct as the drvdata and then simply use its integer 'driver_data' field for quirks, which shall make the code cleaner, type-safe, consistent and more readable. This makes the cast to (void *) during storage a bit safer (just to suppress the const qualifier warning) and the cast to (unsigned long) during retrieval is removed. Signed-off-by: Pawel Zalewski (The Capable Hub) Signed-off-by: Benjamin Tissoires --- drivers/hid/hid-belkin.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/hid/hid-belkin.c b/drivers/hid/hid-belkin.c index 75aaed35ee9f..84695115d37b 100644 --- a/drivers/hid/hid-belkin.c +++ b/drivers/hid/hid-belkin.c @@ -27,7 +27,8 @@ static int belkin_input_mapping(struct hid_device *hdev, struct hid_input *hi, struct hid_field *field, struct hid_usage *usage, unsigned long **bit, int *max) { - unsigned long quirks = (unsigned long)hid_get_drvdata(hdev); + const struct hid_device_id *id = hid_get_drvdata(hdev); + unsigned long quirks = id->driver_data; if ((usage->hid & HID_USAGE_PAGE) != HID_UP_CONSUMER || !(quirks & BELKIN_WKBD)) @@ -48,7 +49,7 @@ static int belkin_probe(struct hid_device *hdev, const struct hid_device_id *id) unsigned long quirks = id->driver_data; int ret; - hid_set_drvdata(hdev, (void *)quirks); + hid_set_drvdata(hdev, (void *)id); ret = hid_parse(hdev); if (ret) { From 73e784ddf895416ec03d3760300f6050324cfe52 Mon Sep 17 00:00:00 2001 From: "Pawel Zalewski (The Capable Hub)" Date: Mon, 18 May 2026 17:06:25 +0100 Subject: [PATCH 16/39] HID: hid-cypress: clean up usage of 'driver_data' The module is storing an integer inside the drvdata pointer, which is confusing - furthermore this integer is mutable. When its value is changed it is set again using the 'hid_set_drvdata' API within the 'cp_event' function. Let's fix this, create and allocate the 'cp_device' struct that is then set as the drvdata and then simply use its integer 'quirks' field for storing the quirks, which shall make the code cleaner, type-safe, consistent and more readable. This makes the cast to (void *) during storage unnecessary and the cast to (unsigned long) during retrieval is also removed. Signed-off-by: Pawel Zalewski (The Capable Hub) Signed-off-by: Benjamin Tissoires --- drivers/hid/hid-cypress.c | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/drivers/hid/hid-cypress.c b/drivers/hid/hid-cypress.c index 98548201feec..f18fddc176d0 100644 --- a/drivers/hid/hid-cypress.c +++ b/drivers/hid/hid-cypress.c @@ -25,6 +25,10 @@ #define VA_INVAL_LOGICAL_BOUNDARY 0x08 +struct cp_device { + unsigned long quirks; +}; + /* * Some USB barcode readers from cypress have usage min and usage max in * the wrong order @@ -70,7 +74,8 @@ static __u8 *va_logical_boundary_fixup(struct hid_device *hdev, __u8 *rdesc, static const __u8 *cp_report_fixup(struct hid_device *hdev, __u8 *rdesc, unsigned int *rsize) { - unsigned long quirks = (unsigned long)hid_get_drvdata(hdev); + const struct cp_device *cp_device = hid_get_drvdata(hdev); + unsigned long quirks = cp_device->quirks; if (quirks & CP_RDESC_SWAPPED_MIN_MAX) rdesc = cp_rdesc_fixup(hdev, rdesc, rsize); @@ -84,7 +89,8 @@ static int cp_input_mapped(struct hid_device *hdev, struct hid_input *hi, struct hid_field *field, struct hid_usage *usage, unsigned long **bit, int *max) { - unsigned long quirks = (unsigned long)hid_get_drvdata(hdev); + const struct cp_device *cp_device = hid_get_drvdata(hdev); + unsigned long quirks = cp_device->quirks; if (!(quirks & CP_2WHEEL_MOUSE_HACK)) return 0; @@ -100,22 +106,21 @@ static int cp_input_mapped(struct hid_device *hdev, struct hid_input *hi, static int cp_event(struct hid_device *hdev, struct hid_field *field, struct hid_usage *usage, __s32 value) { - unsigned long quirks = (unsigned long)hid_get_drvdata(hdev); + struct cp_device *cp_device = hid_get_drvdata(hdev); if (!(hdev->claimed & HID_CLAIMED_INPUT) || !field->hidinput || - !usage->type || !(quirks & CP_2WHEEL_MOUSE_HACK)) + !usage->type || !(cp_device->quirks & CP_2WHEEL_MOUSE_HACK)) return 0; if (usage->hid == 0x00090005) { if (value) - quirks |= CP_2WHEEL_MOUSE_HACK_ON; + cp_device->quirks |= CP_2WHEEL_MOUSE_HACK_ON; else - quirks &= ~CP_2WHEEL_MOUSE_HACK_ON; - hid_set_drvdata(hdev, (void *)quirks); + cp_device->quirks &= ~CP_2WHEEL_MOUSE_HACK_ON; return 1; } - if (usage->code == REL_WHEEL && (quirks & CP_2WHEEL_MOUSE_HACK_ON)) { + if (usage->code == REL_WHEEL && (cp_device->quirks & CP_2WHEEL_MOUSE_HACK_ON)) { struct input_dev *input = field->hidinput->input; input_event(input, usage->type, REL_HWHEEL, value); @@ -127,10 +132,17 @@ static int cp_event(struct hid_device *hdev, struct hid_field *field, static int cp_probe(struct hid_device *hdev, const struct hid_device_id *id) { - unsigned long quirks = id->driver_data; int ret; + struct cp_device *cp_device; - hid_set_drvdata(hdev, (void *)quirks); + cp_device = devm_kzalloc(&hdev->dev, sizeof(*cp_device), GFP_KERNEL); + + if (!cp_device) + return -ENOMEM; + + cp_device->quirks = id->driver_data; + + hid_set_drvdata(hdev, cp_device); ret = hid_parse(hdev); if (ret) { From 0b8bb8c3c913b11bb1d4d34603212b531ecd8354 Mon Sep 17 00:00:00 2001 From: "Pawel Zalewski (The Capable Hub)" Date: Mon, 18 May 2026 17:06:26 +0100 Subject: [PATCH 17/39] HID: hid-gfrm: clean up usage of 'driver_data' The module is storing an integer inside the drvdata pointer, which is confusing, lets fix this and set the whole of 'hid_device_id' struct as the drvdata and then simply use its integer 'driver_data' field for quirks, which shall make the code cleaner, type-safe, consistent and more readable. This makes the cast to (void *) during storage a bit safer (just to suppress the const qualifier warning) and the cast to (unsigned long) during retrieval is removed. Signed-off-by: Pawel Zalewski (The Capable Hub) Signed-off-by: Benjamin Tissoires --- drivers/hid/hid-gfrm.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/drivers/hid/hid-gfrm.c b/drivers/hid/hid-gfrm.c index d2a56bf92b41..f7cc754de84e 100644 --- a/drivers/hid/hid-gfrm.c +++ b/drivers/hid/hid-gfrm.c @@ -28,7 +28,8 @@ static int gfrm_input_mapping(struct hid_device *hdev, struct hid_input *hi, struct hid_field *field, struct hid_usage *usage, unsigned long **bit, int *max) { - unsigned long hdev_type = (unsigned long) hid_get_drvdata(hdev); + const struct hid_device_id *id = hid_get_drvdata(hdev); + unsigned long hdev_type = id->driver_data; if (hdev_type == GFRM100) { if (usage->hid == (HID_UP_CONSUMER | 0x4)) { @@ -50,7 +51,8 @@ static int gfrm_input_mapping(struct hid_device *hdev, struct hid_input *hi, static int gfrm_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) { - unsigned long hdev_type = (unsigned long) hid_get_drvdata(hdev); + const struct hid_device_id *id = hid_get_drvdata(hdev); + unsigned long hdev_type = id->driver_data; int ret = 0; if (hdev_type != GFRM100) @@ -99,7 +101,7 @@ static int gfrm_probe(struct hid_device *hdev, const struct hid_device_id *id) { int ret; - hid_set_drvdata(hdev, (void *) id->driver_data); + hid_set_drvdata(hdev, (void *)id); ret = hid_parse(hdev); if (ret) From b11dfa6cc3c8dcbe2687c74fe4de9a39a8123d74 Mon Sep 17 00:00:00 2001 From: "Pawel Zalewski (The Capable Hub)" Date: Mon, 18 May 2026 17:06:27 +0100 Subject: [PATCH 18/39] HID: hid-ite: clean up usage of 'driver_data' The module is storing an integer inside the drvdata pointer, which is confusing, lets fix this and set the whole of 'hid_device_id' struct as the drvdata and then simply use its integer 'driver_data' field for quirks, which shall make the code cleaner, type-safe, consistent and more readable. This makes the cast to (void *) during storage a bit safer (just to suppress the const qualifier warning) and the cast to (unsigned long) during retrieval is removed. Signed-off-by: Pawel Zalewski (The Capable Hub) Signed-off-by: Benjamin Tissoires --- drivers/hid/hid-ite.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/drivers/hid/hid-ite.c b/drivers/hid/hid-ite.c index 8e42780a2663..63908f24b524 100644 --- a/drivers/hid/hid-ite.c +++ b/drivers/hid/hid-ite.c @@ -15,7 +15,9 @@ static const __u8 *ite_report_fixup(struct hid_device *hdev, __u8 *rdesc, unsigned int *rsize) { - unsigned long quirks = (unsigned long)hid_get_drvdata(hdev); + + const struct hid_device_id *id = hid_get_drvdata(hdev); + unsigned long quirks = id->driver_data; if (quirks & QUIRK_TOUCHPAD_ON_OFF_REPORT) { /* For Acer Aspire Switch 10 SW5-012 keyboard-dock */ @@ -44,7 +46,8 @@ static int ite_input_mapping(struct hid_device *hdev, int *max) { - unsigned long quirks = (unsigned long)hid_get_drvdata(hdev); + const struct hid_device_id *id = hid_get_drvdata(hdev); + unsigned long quirks = id->driver_data; if ((quirks & QUIRK_TOUCHPAD_ON_OFF_REPORT) && (usage->hid & HID_USAGE_PAGE) == 0x00880000) { @@ -94,7 +97,7 @@ static int ite_probe(struct hid_device *hdev, const struct hid_device_id *id) { int ret; - hid_set_drvdata(hdev, (void *)id->driver_data); + hid_set_drvdata(hdev, (void *)id); ret = hid_open_report(hdev); if (ret) From 3a1e4e77e3eea841f58b48086d6ee5bd7c1da87e Mon Sep 17 00:00:00 2001 From: Harrison Vanderbyl Date: Fri, 29 May 2026 11:16:15 +1000 Subject: [PATCH 19/39] hid: Pen battery quirk for Surface Pro 12in The pen setup for this device uses bluetooth for communicating battery levels and status instead of reporting it over i2c. Without this quirk, the device either reports an extra, broken phantom battery, or hangs. Signed-off-by: Harrison Vanderbyl Acked-by: Jiri Kosina Signed-off-by: Benjamin Tissoires --- drivers/hid/hid-ids.h | 1 + drivers/hid/hid-input.c | 2 ++ 2 files changed, 3 insertions(+) diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 0cf63742315b..d16f55479786 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -459,6 +459,7 @@ #define USB_DEVICE_ID_HP_X2 0x074d #define USB_DEVICE_ID_HP_X2_10_COVER 0x0755 #define I2C_DEVICE_ID_CHROMEBOOK_TROGDOR_POMPOM 0x2F81 +#define I2C_DEVICE_ID_SURFACE_PRO_12IN 0x4376 #define USB_VENDOR_ID_ELECOM 0x056e #define USB_DEVICE_ID_ELECOM_BM084 0x0061 diff --git a/drivers/hid/hid-input.c b/drivers/hid/hid-input.c index d73cfa2e73d3..61ecd840d0bd 100644 --- a/drivers/hid/hid-input.c +++ b/drivers/hid/hid-input.c @@ -395,6 +395,8 @@ static const struct hid_device_id hid_battery_quirks[] = { HID_BATTERY_QUIRK_AVOID_QUERY }, { HID_I2C_DEVICE(USB_VENDOR_ID_ELAN, I2C_DEVICE_ID_CHROMEBOOK_TROGDOR_POMPOM), HID_BATTERY_QUIRK_AVOID_QUERY }, + { HID_I2C_DEVICE(USB_VENDOR_ID_ELAN, I2C_DEVICE_ID_SURFACE_PRO_12IN), + HID_BATTERY_QUIRK_IGNORE }, /* * Elan HID touchscreens seem to all report a non present battery, * set HID_BATTERY_QUIRK_IGNORE for all Elan I2C and USB HID devices. From 6b3014ec0e9a390ca563030b2d7689921f0daef5 Mon Sep 17 00:00:00 2001 From: Jinmo Yang Date: Fri, 29 May 2026 02:59:45 +0900 Subject: [PATCH 20/39] HID: wacom: fix slab-out-of-bounds write in wacom_wac_queue_insert wacom_wac_queue_insert() calls kfifo_skip() in a loop when the kfifo doesn't have enough space for the incoming report. If the kfifo is empty, kfifo_skip() reads stale data left in the kmalloc'd buffer via __kfifo_peek_n() and interprets it as a record length, advancing fifo->out by that garbage value. This corrupts the internal kfifo state, causing kfifo_unused() to return a value much larger than the actual buffer size, which bypasses __kfifo_in_r()'s guard: if (len + recsize > kfifo_unused(fifo)) return 0; kfifo_copy_in() then performs an out-of-bounds memcpy, writing up to 3842 bytes past the 256-byte buffer. Add a !kfifo_is_empty() condition to the while loop so kfifo_skip() is never called on an empty fifo, and check the return value of kfifo_in() to reject reports that are too large for the fifo. Suggested-by: Dmitry Torokhov Fixes: 5e013ad20689 ("HID: wacom: Remove static WACOM_PKGLEN_MAX limit") Cc: stable@vger.kernel.org Signed-off-by: Jinmo Yang Reviewed-by: Dmitry Torokhov Signed-off-by: Benjamin Tissoires --- drivers/hid/wacom_sys.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/drivers/hid/wacom_sys.c b/drivers/hid/wacom_sys.c index 2220168bf116..fac75c202453 100644 --- a/drivers/hid/wacom_sys.c +++ b/drivers/hid/wacom_sys.c @@ -54,7 +54,7 @@ static void wacom_wac_queue_insert(struct hid_device *hdev, { bool warned = false; - while (kfifo_avail(fifo) < size) { + while (kfifo_avail(fifo) < size && !kfifo_is_empty(fifo)) { if (!warned) hid_warn(hdev, "%s: kfifo has filled, starting to drop events\n", __func__); warned = true; @@ -62,7 +62,9 @@ static void wacom_wac_queue_insert(struct hid_device *hdev, kfifo_skip(fifo); } - kfifo_in(fifo, raw_data, size); + if (!kfifo_in(fifo, raw_data, size)) + hid_warn_ratelimited(hdev, "%s: report is too large (%d)\n", + __func__, size); } static void wacom_wac_queue_flush(struct hid_device *hdev, From 55f1ad573e34abf9a0443c34bc5a63d74edba7d7 Mon Sep 17 00:00:00 2001 From: Jinmo Yang Date: Mon, 1 Jun 2026 22:41:23 +0900 Subject: [PATCH 21/39] HID: wacom: use GFP_ATOMIC in wacom_wac_queue_flush() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wacom_wac_queue_flush() is called via the .raw_event callback (wacom_raw_event → wacom_wac_pen_serial_enforce → wacom_wac_queue_flush). For USB HID devices, this callback is invoked from hid_irq_in(), which is a URB completion handler running in atomic context. Using GFP_KERNEL in this path can sleep, leading to a "scheduling while atomic" bug. Use GFP_ATOMIC instead. The existing code already handles allocation failure by skipping the fifo entry and continuing. Reported-by: Sashiko-bot Fixes: 5e013ad20689 ("HID: wacom: Remove static WACOM_PKGLEN_MAX limit") Cc: stable@vger.kernel.org Reviewed-by: Dmitry Torokhov Signed-off-by: Jinmo Yang Signed-off-by: Benjamin Tissoires --- drivers/hid/wacom_sys.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hid/wacom_sys.c b/drivers/hid/wacom_sys.c index fac75c202453..3a06be1e163a 100644 --- a/drivers/hid/wacom_sys.c +++ b/drivers/hid/wacom_sys.c @@ -76,7 +76,7 @@ static void wacom_wac_queue_flush(struct hid_device *hdev, unsigned int count; int err; - buf = kzalloc(size, GFP_KERNEL); + buf = kzalloc(size, GFP_ATOMIC); if (!buf) { kfifo_skip(fifo); continue; From cb605d48dac95b3b1258c2dcdd2d2c3617bee092 Mon Sep 17 00:00:00 2001 From: Jinmo Yang Date: Mon, 1 Jun 2026 22:41:24 +0900 Subject: [PATCH 22/39] HID: wacom: use cleanup.h for wacom_wac_queue_flush() buffer management Use __free(kfree) cleanup facility for the temporary buffer in wacom_wac_queue_flush() to simplify error paths and ensure the buffer is freed automatically when it goes out of scope. Signed-off-by: Jinmo Yang Signed-off-by: Benjamin Tissoires --- drivers/hid/wacom_sys.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/drivers/hid/wacom_sys.c b/drivers/hid/wacom_sys.c index 3a06be1e163a..3e3bcf695296 100644 --- a/drivers/hid/wacom_sys.c +++ b/drivers/hid/wacom_sys.c @@ -72,11 +72,10 @@ static void wacom_wac_queue_flush(struct hid_device *hdev, { while (!kfifo_is_empty(fifo)) { int size = kfifo_peek_len(fifo); - u8 *buf; + u8 *buf __free(kfree) = kzalloc(size, GFP_ATOMIC); unsigned int count; int err; - buf = kzalloc(size, GFP_ATOMIC); if (!buf) { kfifo_skip(fifo); continue; @@ -89,7 +88,6 @@ static void wacom_wac_queue_flush(struct hid_device *hdev, // to flush seems reasonable enough, however. hid_warn(hdev, "%s: removed fifo entry with unexpected size\n", __func__); - kfree(buf); continue; } err = hid_report_raw_event(hdev, HID_INPUT_REPORT, buf, size, size, false); @@ -97,8 +95,6 @@ static void wacom_wac_queue_flush(struct hid_device *hdev, hid_warn(hdev, "%s: unable to flush event due to error %d\n", __func__, err); } - - kfree(buf); } } From f22a5db8a7d38152556f230d6d68e59dbc27971b Mon Sep 17 00:00:00 2001 From: Rosen Penev Date: Sat, 30 May 2026 17:01:50 -0700 Subject: [PATCH 23/39] HID: logitech-hidpp: remove excess kernel-doc member in hidpp_scroll_counter The @dev member described in the kernel-doc does not exist in the struct. Remove the stale entry. Fixes: 0610430e3dea ("HID: logitech-hidpp: add input_device ptr to struct hidpp_device") Assisted-by: opencode:big-pickle Signed-off-by: Rosen Penev Signed-off-by: Benjamin Tissoires --- drivers/hid/hid-logitech-hidpp.c | 1 - 1 file changed, 1 deletion(-) diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c index ccbf28869a96..1990ba5b26ea 100644 --- a/drivers/hid/hid-logitech-hidpp.c +++ b/drivers/hid/hid-logitech-hidpp.c @@ -164,7 +164,6 @@ struct hidpp_battery { /** * struct hidpp_scroll_counter - Utility class for processing high-resolution * scroll events. - * @dev: the input device for which events should be reported. * @wheel_multiplier: the scalar multiplier to be applied to each wheel event * @remainder: counts the number of high-resolution units moved since the last * low-resolution event (REL_WHEEL or REL_HWHEEL) was sent. Should From 426e5846eba75feaf1c9c6c119cb153610192da1 Mon Sep 17 00:00:00 2001 From: Rafael Passos Date: Tue, 2 Jun 2026 00:05:19 -0300 Subject: [PATCH 24/39] HID: Input: Add battery list cleanup with devm action The batteries list (hdev->batteries) is not cleaned up during hidinput_disconnect(), but struct hid_battery entries are allocated with devm_kzalloc. When a driver is unbound (e.g. during devicereprobe), devm frees those entries while their list_head nodesremain dangling in hdev->batteries, which persists across rebinds. Link: https://lore.kernel.org/all/20260602011949.2825852-1-rafael@rcpassos.me/ Fixes: 4a58ae85c3f9 ("HID: input: Add support for multiple batteries per device") Signed-off-by: Rafael Passos Acked-by: Lucas Zampieri Signed-off-by: Benjamin Tissoires --- drivers/hid/hid-input.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/drivers/hid/hid-input.c b/drivers/hid/hid-input.c index d73cfa2e73d3..c7b8c4ff7a33 100644 --- a/drivers/hid/hid-input.c +++ b/drivers/hid/hid-input.c @@ -519,6 +519,13 @@ static struct hid_battery *hidinput_find_battery(struct hid_device *dev, return NULL; } +static void hidinput_cleanup_battery(void *res) +{ + struct hid_battery *bat = res; + + list_del(&bat->list); +} + static int hidinput_setup_battery(struct hid_device *dev, unsigned report_type, struct hid_field *field, bool is_percentage) { @@ -610,6 +617,12 @@ static int hidinput_setup_battery(struct hid_device *dev, unsigned report_type, power_supply_powers(bat->ps, &dev->dev); list_add_tail(&bat->list, &dev->batteries); + + error = devm_add_action_or_reset(&dev->dev, + hidinput_cleanup_battery, bat); + if (error) + return error; + return 0; err_free_name: From 73fde0cbff7d9d618591774a12c23434232752c1 Mon Sep 17 00:00:00 2001 From: Manish Khadka Date: Fri, 15 May 2026 23:30:11 +0545 Subject: [PATCH 25/39] HID: hid-lenovo-go: cancel cfg_setup work in hid_go_cfg_remove() hid_go_cfg_probe() initialises drvdata.go_cfg_setup and schedules it to run 2 ms later: INIT_DELAYED_WORK(&drvdata.go_cfg_setup, &cfg_setup); schedule_delayed_work(&drvdata.go_cfg_setup, msecs_to_jiffies(2)); cfg_setup() dereferences drvdata.hdev to issue MCU command requests. hid_go_cfg_remove() tears down sysfs and stops the HID device, but never drains the delayed work. If the device is unbound within the 2 ms scheduling delay (a probe failure rolling back via remove, or a fast rmmod after probe), the work fires after hid_destroy_device() has dropped its reference and released the underlying hdev struct, leaving cfg_setup() with a stale drvdata.hdev pointer. Mirror the sibling driver hid-lenovo-go-s.c, whose hid_gos_cfg_remove() already calls cancel_delayed_work_sync() on its analogous work, and drain go_cfg_setup at the top of hid_go_cfg_remove(). The cancel must come before guard(mutex)(&drvdata.cfg_mutex) because cfg_setup() acquires that mutex; reversing the order would deadlock. Fixes: d69ccfcbc955 ("HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver") Cc: stable@vger.kernel.org Signed-off-by: Manish Khadka Signed-off-by: Jiri Kosina --- drivers/hid/hid-lenovo-go.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c index e0c9d5ec9451..318b1152ff8b 100644 --- a/drivers/hid/hid-lenovo-go.c +++ b/drivers/hid/hid-lenovo-go.c @@ -2405,6 +2405,15 @@ static int hid_go_cfg_probe(struct hid_device *hdev, static void hid_go_cfg_remove(struct hid_device *hdev) { + /* + * cfg_setup is scheduled from hid_go_cfg_probe() with a 2 ms delay + * and dereferences drvdata.hdev. Drain it here before tearing + * down so the workqueue cannot run after hid_destroy_device()'s + * put_device() has released the underlying hdev and dereference + * a stale drvdata.hdev pointer. + */ + cancel_delayed_work_sync(&drvdata.go_cfg_setup); + guard(mutex)(&drvdata.cfg_mutex); sysfs_remove_groups(&hdev->dev.kobj, top_level_attr_groups); hid_hw_close(hdev); From c0be05f68a2145d566a44805f22896f141316cf1 Mon Sep 17 00:00:00 2001 From: Danny Kaehn Date: Wed, 20 May 2026 11:13:06 -0500 Subject: [PATCH 26/39] HID: cp2112: Add fwnode support Support describing the CP2112's I2C and GPIO interfaces in firmware. Bindings between the firmware nodes and the functions of the device are distinct between ACPI and DeviceTree. For ACPI, the i2c_adapter will use the child with _ADR equal to Zero and the gpio_chip will use the child with _ADR equal to One. For DeviceTree, the i2c_adapter will use the child with name "i2c", but the gpio_chip will share a firmware node with the CP2112. Signed-off-by: Danny Kaehn Reviewed-by: Andy Shevchenko Reviewed-by: Bartosz Golaszewski Tested-by: Jacky Huang Signed-off-by: Jiri Kosina --- drivers/hid/hid-cp2112.c | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/drivers/hid/hid-cp2112.c b/drivers/hid/hid-cp2112.c index 803b883ae875..e960fc988058 100644 --- a/drivers/hid/hid-cp2112.c +++ b/drivers/hid/hid-cp2112.c @@ -29,6 +29,18 @@ #include #include "hid-ids.h" +/** + * enum cp2112_child_acpi_cell_addrs - Child ACPI addresses for CP2112 sub-functions + * Note that the enum values are explicitly defined, as this defines the interface + * between ACPI and Linux + * @CP2112_I2C_ADR: Address for I2C node + * @CP2112_GPIO_ADR: Address for GPIO node + */ +enum cp2112_child_acpi_cell_addrs { + CP2112_I2C_ADR = 0, + CP2112_GPIO_ADR = 1, +}; + #define CP2112_REPORT_MAX_LENGTH 64 #define CP2112_GPIO_CONFIG_LENGTH 5 #define CP2112_GPIO_GET_LENGTH 2 @@ -1208,7 +1220,10 @@ static int cp2112_probe(struct hid_device *hdev, const struct hid_device_id *id) struct cp2112_device *dev; u8 buf[3]; struct cp2112_smbus_config_report config; + struct fwnode_handle *cp2112_fwnode; + struct fwnode_handle *child; struct gpio_irq_chip *girq; + u32 addr; int ret; dev = devm_kzalloc(&hdev->dev, sizeof(*dev), GFP_KERNEL); @@ -1226,6 +1241,28 @@ static int cp2112_probe(struct hid_device *hdev, const struct hid_device_id *id) return ret; } + cp2112_fwnode = dev_fwnode(&hdev->dev); + if (is_acpi_device_node(cp2112_fwnode)) { + fwnode_for_each_child_node(cp2112_fwnode, child) { + ret = acpi_get_local_address(ACPI_HANDLE_FWNODE(child), &addr); + if (ret) + continue; + + switch (addr) { + case CP2112_I2C_ADR: + device_set_node(&dev->adap.dev, child); + break; + case CP2112_GPIO_ADR: + dev->gc.fwnode = child; + break; + } + } + } else if (is_of_node(cp2112_fwnode)) { + child = fwnode_get_named_child_node(cp2112_fwnode, "i2c"); + device_set_node(&dev->adap.dev, child); + fwnode_handle_put(child); + } + ret = hid_parse(hdev); if (ret) { hid_err(hdev, "parse failed\n"); From efab84c398c17d2575e8a308c243915baae3affa Mon Sep 17 00:00:00 2001 From: Danny Kaehn Date: Wed, 20 May 2026 11:13:07 -0500 Subject: [PATCH 27/39] HID: cp2112: Configure I2C bus speed from firmware Now that the I2C adapter on the CP2112 can have an associated firmware node, set the bus speed based on firmware configuration Reviewed-by: Andy Shevchenko Signed-off-by: Danny Kaehn Signed-off-by: Jiri Kosina --- drivers/hid/hid-cp2112.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/hid/hid-cp2112.c b/drivers/hid/hid-cp2112.c index e960fc988058..04379db93571 100644 --- a/drivers/hid/hid-cp2112.c +++ b/drivers/hid/hid-cp2112.c @@ -1223,6 +1223,7 @@ static int cp2112_probe(struct hid_device *hdev, const struct hid_device_id *id) struct fwnode_handle *cp2112_fwnode; struct fwnode_handle *child; struct gpio_irq_chip *girq; + struct i2c_timings timings; u32 addr; int ret; @@ -1308,6 +1309,9 @@ static int cp2112_probe(struct hid_device *hdev, const struct hid_device_id *id) goto err_power_normal; } + i2c_parse_fw_timings(&dev->adap.dev, &timings, true); + + config.clock_speed = cpu_to_be32(timings.bus_freq_hz); config.retry_time = cpu_to_be16(1); ret = cp2112_hid_output(hdev, (u8 *)&config, sizeof(config), From df72cacda33f097a6cf891134a411e5fe953b276 Mon Sep 17 00:00:00 2001 From: Vishnu Sankar Date: Fri, 22 May 2026 14:06:31 +0900 Subject: [PATCH 28/39] HID: lenovo: Add support for ThinkPad X13 Folio keyboard Add USB ID support for the ThinkPad X13 detachable keyboard. The Keyboard uses the same HID raw event protocol as the ThinkPad X12 Gen 2. The functionality stays the same with X12 Gen 2 Keyboards. Also declare KEY_PERFORMANCE capability in lenovo_input_configured() for X13 detachable, allowing userspace to discover the key via evdev capability bits. Signed-off-by: Vishnu Sankar Signed-off-by: Jiri Kosina --- drivers/hid/hid-ids.h | 1 + drivers/hid/hid-lenovo.c | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 426ff78c1c03..8e876a3a6476 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -874,6 +874,7 @@ #define USB_DEVICE_ID_LENOVO_X1_TAB3 0x60b5 #define USB_DEVICE_ID_LENOVO_X12_TAB 0x60fe #define USB_DEVICE_ID_LENOVO_X12_TAB2 0x61ae +#define USB_DEVICE_ID_LENOVO_X13_TAB 0x62af #define USB_DEVICE_ID_LENOVO_YOGABOOK9I 0x6161 #define USB_DEVICE_ID_LENOVO_OPTICAL_USB_MOUSE_600E 0x600e #define USB_DEVICE_ID_LENOVO_PIXART_USB_MOUSE_608D 0x608d diff --git a/drivers/hid/hid-lenovo.c b/drivers/hid/hid-lenovo.c index c11957ae8b77..c86a54d9b2b2 100644 --- a/drivers/hid/hid-lenovo.c +++ b/drivers/hid/hid-lenovo.c @@ -505,6 +505,7 @@ static int lenovo_input_mapping(struct hid_device *hdev, usage, bit, max); case USB_DEVICE_ID_LENOVO_X12_TAB: case USB_DEVICE_ID_LENOVO_X12_TAB2: + case USB_DEVICE_ID_LENOVO_X13_TAB: case USB_DEVICE_ID_LENOVO_X1_TAB: case USB_DEVICE_ID_LENOVO_X1_TAB2: case USB_DEVICE_ID_LENOVO_X1_TAB3: @@ -621,6 +622,7 @@ static ssize_t attr_fn_lock_store(struct device *dev, break; case USB_DEVICE_ID_LENOVO_X12_TAB: case USB_DEVICE_ID_LENOVO_X12_TAB2: + case USB_DEVICE_ID_LENOVO_X13_TAB: case USB_DEVICE_ID_LENOVO_TP10UBKBD: case USB_DEVICE_ID_LENOVO_X1_TAB: case USB_DEVICE_ID_LENOVO_X1_TAB2: @@ -792,9 +794,10 @@ static int lenovo_raw_event(struct hid_device *hdev, * Lenovo TP X12 Tab KBD's Fn+XX is HID raw data defined. Report ID is 0x03 * e.g.: Raw data received for MIC mute is 0x00020003. */ - if (unlikely((hdev->product == USB_DEVICE_ID_LENOVO_X12_TAB - || hdev->product == USB_DEVICE_ID_LENOVO_X12_TAB2) - && size >= 4 && report->id == 0x03)) + if (unlikely((hdev->product == USB_DEVICE_ID_LENOVO_X12_TAB || + hdev->product == USB_DEVICE_ID_LENOVO_X12_TAB2 || + hdev->product == USB_DEVICE_ID_LENOVO_X13_TAB) && + size >= 4 && report->id == 0x03)) return lenovo_raw_event_TP_X12_tab(hdev, get_unaligned_le32(data)); return 0; @@ -878,6 +881,7 @@ static int lenovo_event(struct hid_device *hdev, struct hid_field *field, return lenovo_event_cptkbd(hdev, field, usage, value); case USB_DEVICE_ID_LENOVO_X12_TAB: case USB_DEVICE_ID_LENOVO_X12_TAB2: + case USB_DEVICE_ID_LENOVO_X13_TAB: case USB_DEVICE_ID_LENOVO_TP10UBKBD: case USB_DEVICE_ID_LENOVO_X1_TAB: case USB_DEVICE_ID_LENOVO_X1_TAB2: @@ -1162,6 +1166,7 @@ static int lenovo_led_brightness_set(struct led_classdev *led_cdev, break; case USB_DEVICE_ID_LENOVO_X12_TAB: case USB_DEVICE_ID_LENOVO_X12_TAB2: + case USB_DEVICE_ID_LENOVO_X13_TAB: case USB_DEVICE_ID_LENOVO_TP10UBKBD: case USB_DEVICE_ID_LENOVO_X1_TAB: case USB_DEVICE_ID_LENOVO_X1_TAB2: @@ -1356,7 +1361,8 @@ static int lenovo_probe_tp10ubkbd(struct hid_device *hdev) */ data->fn_lock = !(hdev->product == USB_DEVICE_ID_LENOVO_X12_TAB || - hdev->product == USB_DEVICE_ID_LENOVO_X12_TAB2); + hdev->product == USB_DEVICE_ID_LENOVO_X12_TAB2 || + hdev->product == USB_DEVICE_ID_LENOVO_X13_TAB); lenovo_led_set_tp10ubkbd(hdev, TP10UBKBD_FN_LOCK_LED, data->fn_lock); @@ -1403,6 +1409,7 @@ static int lenovo_probe(struct hid_device *hdev, break; case USB_DEVICE_ID_LENOVO_X12_TAB: case USB_DEVICE_ID_LENOVO_X12_TAB2: + case USB_DEVICE_ID_LENOVO_X13_TAB: case USB_DEVICE_ID_LENOVO_TP10UBKBD: case USB_DEVICE_ID_LENOVO_X1_TAB: case USB_DEVICE_ID_LENOVO_X1_TAB2: @@ -1491,6 +1498,7 @@ static void lenovo_remove(struct hid_device *hdev) break; case USB_DEVICE_ID_LENOVO_X12_TAB: case USB_DEVICE_ID_LENOVO_X12_TAB2: + case USB_DEVICE_ID_LENOVO_X13_TAB: case USB_DEVICE_ID_LENOVO_TP10UBKBD: case USB_DEVICE_ID_LENOVO_X1_TAB: case USB_DEVICE_ID_LENOVO_X1_TAB2: @@ -1518,6 +1526,9 @@ static int lenovo_input_configured(struct hid_device *hdev, hi->input->propbit); } break; + case USB_DEVICE_ID_LENOVO_X13_TAB: + input_set_capability(hi->input, EV_KEY, KEY_PERFORMANCE); + break; } return 0; @@ -1552,6 +1563,8 @@ static const struct hid_device_id lenovo_devices[] = { USB_VENDOR_ID_LENOVO, USB_DEVICE_ID_LENOVO_X12_TAB) }, { HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, USB_VENDOR_ID_LENOVO, USB_DEVICE_ID_LENOVO_X12_TAB2) }, + { HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, + USB_VENDOR_ID_LENOVO, USB_DEVICE_ID_LENOVO_X13_TAB) }, { HID_DEVICE(BUS_I2C, HID_GROUP_GENERIC, USB_VENDOR_ID_ITE, I2C_DEVICE_ID_ITE_LENOVO_YOGA_SLIM_7X_KEYBOARD) }, { } From 4b0f556deb9af714dd87c309dc125f32098aeb9d Mon Sep 17 00:00:00 2001 From: Vishnu Sankar Date: Fri, 22 May 2026 14:06:32 +0900 Subject: [PATCH 29/39] HID: lenovo: Use KEY_PERFORMANCE capability for ThinkPad X12 Tab Gen 2 The X12 Tab Gen 2 emits KEY_PERFORMANCE via Fn+F8 through the raw event handler but never declared the capability via input_set_capability(). This prevents userspace tools from discovering the key through evdev capability bits. Signed-off-by: Vishnu Sankar Signed-off-by: Jiri Kosina --- drivers/hid/hid-lenovo.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/hid/hid-lenovo.c b/drivers/hid/hid-lenovo.c index c86a54d9b2b2..3976d7b53b14 100644 --- a/drivers/hid/hid-lenovo.c +++ b/drivers/hid/hid-lenovo.c @@ -1526,6 +1526,7 @@ static int lenovo_input_configured(struct hid_device *hdev, hi->input->propbit); } break; + case USB_DEVICE_ID_LENOVO_X12_TAB2: case USB_DEVICE_ID_LENOVO_X13_TAB: input_set_capability(hi->input, EV_KEY, KEY_PERFORMANCE); break; From d0ff08d946c83b51359a8063c41e9f5af067e628 Mon Sep 17 00:00:00 2001 From: Matteo Croce Date: Sat, 23 May 2026 12:55:45 +0200 Subject: [PATCH 30/39] HID: core: demote warning to debug level The log level for short messages was changed from debug to warning, flooding syslog on systems with devices that regularly send short reports, in my case an UPS: $ dmesg |grep -c 'Event data for report .* was too short' 35 Demote it back to debug level. Fixes: 0a3fe972a7cb ("HID: core: Mitigate potential OOB by removing bogus memset()") Signed-off-by: Matteo Croce Signed-off-by: Jiri Kosina --- drivers/hid/hid-core.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c index b3596851c719..3c3884684914 100644 --- a/drivers/hid/hid-core.c +++ b/drivers/hid/hid-core.c @@ -2072,8 +2072,8 @@ int hid_report_raw_event(struct hid_device *hid, enum hid_report_type type, u8 * rsize = max_buffer_size; if (bsize < rsize) { - hid_warn_ratelimited(hid, "Event data for report %d was too short (%d vs %ld)\n", - report->id, rsize, bsize); + hid_dbg_ratelimited(hid, "Event data for report %d was too short (%d vs %ld)\n", + report->id, rsize, bsize); return -EINVAL; } From ec2612b8ad9e642596db011dd8b6568ef1edeaa1 Mon Sep 17 00:00:00 2001 From: Myeonghun Pak Date: Thu, 4 Jun 2026 13:56:58 +0900 Subject: [PATCH 31/39] HID: wacom: stop hardware after post-start probe failures wacom_parse_and_register() starts HID hardware before registering inputs and initializing pad LEDs/remotes. Those later steps can fail, but their error paths currently release Wacom resources without stopping the HID hardware. Route post-hid_hw_start() failures through hid_hw_stop() before releasing driver resources. This issue was identified during our ongoing static-analysis research while reviewing kernel code. Fixes: c1d6708bf0d3 ("HID: wacom: Do not register input devices until after hid_hw_start") Cc: stable@vger.kernel.org Co-developed-by: Ijae Kim Signed-off-by: Ijae Kim Signed-off-by: Myeonghun Pak Reviewed-by: Dmitry Torokhov Signed-off-by: Jiri Kosina --- drivers/hid/wacom_sys.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/drivers/hid/wacom_sys.c b/drivers/hid/wacom_sys.c index 3e3bcf695296..0eafa483b7f7 100644 --- a/drivers/hid/wacom_sys.c +++ b/drivers/hid/wacom_sys.c @@ -2460,16 +2460,16 @@ static int wacom_parse_and_register(struct wacom *wacom, bool wireless) error = wacom_register_inputs(wacom); if (error) - goto fail; + goto fail_hw_stop; if (wacom->wacom_wac.features.device_type & WACOM_DEVICETYPE_PAD) { error = wacom_initialize_leds(wacom); if (error) - goto fail; + goto fail_hw_stop; error = wacom_initialize_remotes(wacom); if (error) - goto fail; + goto fail_hw_stop; } if (!wireless) { @@ -2483,14 +2483,14 @@ static int wacom_parse_and_register(struct wacom *wacom, bool wireless) cancel_delayed_work_sync(&wacom->init_work); _wacom_query_tablet_data(wacom); error = -ENODEV; - goto fail_quirks; + goto fail_hw_stop; } if (features->device_type & WACOM_DEVICETYPE_WL_MONITOR) { error = hid_hw_open(hdev); if (error) { hid_err(hdev, "hw open failed\n"); - goto fail_quirks; + goto fail_hw_stop; } } @@ -2499,7 +2499,7 @@ static int wacom_parse_and_register(struct wacom *wacom, bool wireless) return 0; -fail_quirks: +fail_hw_stop: hid_hw_stop(hdev); fail: wacom_release_resources(wacom); From b251598b8bf37300510868f739a79e07800d41ce Mon Sep 17 00:00:00 2001 From: Oleg Makarenko Date: Tue, 9 Jun 2026 19:00:27 +0300 Subject: [PATCH 32/39] HID: pidff: Use correct effect type in effect update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When updating an existing effect, the effect type from the last created effect was sent to the device instead of the updated one. This caused incorrect reports when a game creates multiple different effects and updates only one that is not the last created. Fixes FFB in multiple games that create multiple simultaneous effects (Forza Horizon 5/6). Fixes: 224ee88fe395 ("Input: add force feedback driver for PID devices") Cc: stable@vger.kernel.org Tested-by: Oliver Roundtree Co-developed-by: Ryno Kotzé Signed-off-by: Ryno Kotzé Signed-off-by: Oleg Makarenko Signed-off-by: Jiri Kosina --- drivers/hid/usbhid/hid-pidff.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hid/usbhid/hid-pidff.c b/drivers/hid/usbhid/hid-pidff.c index c45f182d0448..5f4395f7c645 100644 --- a/drivers/hid/usbhid/hid-pidff.c +++ b/drivers/hid/usbhid/hid-pidff.c @@ -522,7 +522,7 @@ static void pidff_set_effect_report(struct pidff_device *pidff, pidff->set_effect[PID_EFFECT_BLOCK_INDEX].value[0] = pidff->block_load[PID_EFFECT_BLOCK_INDEX].value[0]; pidff->set_effect_type->value[0] = - pidff->create_new_effect_type->value[0]; + pidff_get_effect_type_id(pidff, effect); pidff_set_duration(&pidff->set_effect[PID_DURATION], effect->replay.length); From ef257b8be9776915ca468bae6c91e31757e69734 Mon Sep 17 00:00:00 2001 From: Dave Carey Date: Thu, 14 May 2026 15:32:58 -0400 Subject: [PATCH 33/39] HID: multitouch: Honor ContactCount for Yoga Book 9 to suppress ghost contacts The INGENIC 17EF:6161 firmware on the Lenovo Yoga Book 9 14IAH10 does not clear stale contact slots when fingers are lifted. Each HID report contains up to 10 finger slots, but only the first ContactCount slots represent valid contacts; the remaining slots retain TipSwitch=1 with positions from previous touches. Raw HID capture confirms this: across a 60-second capture with repeated multi-finger gestures, 90% of frames had more TipSwitch=1 slots than the reported ContactCount. The ContactCount field itself is always accurate. Add MT_QUIRK_CONTACT_CNT_ACCURATE to the MT_CLS_YOGABOOK9I class so the driver stops processing slots once ContactCount valid contacts have been consumed, discarding the stale ghost entries per HID specification section 17. MT_QUIRK_NOT_SEEN_MEANS_UP (already in the class) ensures that any slot skipped by this guard is released via INPUT_MT_DROP_UNUSED at frame sync. Signed-off-by: Dave Carey Tested-by: Dave Carey Signed-off-by: Jiri Kosina --- drivers/hid/hid-multitouch.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c index ec04dbafbd99..507c34f4aa2d 100644 --- a/drivers/hid/hid-multitouch.c +++ b/drivers/hid/hid-multitouch.c @@ -445,6 +445,7 @@ static const struct mt_class mt_classes[] = { { .name = MT_CLS_YOGABOOK9I, .quirks = MT_QUIRK_NOT_SEEN_MEANS_UP | MT_QUIRK_ALWAYS_VALID | + MT_QUIRK_CONTACT_CNT_ACCURATE | MT_QUIRK_FORCE_MULTI_INPUT | MT_QUIRK_SEPARATE_APP_REPORT | MT_QUIRK_HOVERING | From 9146038120a6e5b2ba872515ed2097e4c285602d Mon Sep 17 00:00:00 2001 From: Hector Zelaya Date: Wed, 27 May 2026 10:01:32 -0600 Subject: [PATCH 34/39] HID: nintendo: add support for HORI Wireless Switch Pad Add support for the HORI Wireless Switch Pad (vendor 0x0f0d, product 0x00f6), a licensed third-party Nintendo Switch Pro Controller. The controller reports controller type 0x06 (vs 0x03 for first-party Pro Controllers) and has the following quirks: - SPI flash calibration data is incompatible; use default stick calibration values instead. - X and Y button bits are swapped compared to first-party controllers; add a dedicated button mapping table. - Rumble and IMU enable may timeout (no vibration motor in hardware); treat as non-fatal for licensed controllers. Tested over Bluetooth on NixOS with kernel 7.0.5 and 7.0.10: - All 14 buttons map correctly - Player LED sets on connect - Sticks report correctly with default calibration - IMU/gyro data streams at 60Hz - D-pad reports on ABS_HAT0X/HAT0Y Device information: Bluetooth name: Lic Pro Controller Bluetooth HID: 0005:0F0D:00F6 Assisted-by: Kiro:Auto [Amazon Kiro IDE] Signed-off-by: Hector Zelaya Reviewed-by: Joshua Peisach Signed-off-by: Jiri Kosina --- drivers/hid/hid-ids.h | 3 ++ drivers/hid/hid-nintendo.c | 80 ++++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 426ff78c1c03..c508bc3f02da 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -683,6 +683,9 @@ #define USB_DEVICE_ID_HARMONIX_WII_RB3_KEYBOARD 0x3330 #define USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_KEYBOARD_MODE 0x3338 +#define USB_VENDOR_ID_HORI 0x0f0d +#define USB_DEVICE_ID_HORI_WIRELESS_SWITCH_PAD 0x00f6 + #define USB_VENDOR_ID_HP 0x03f0 #define USB_PRODUCT_ID_HP_ELITE_PRESENTER_MOUSE_464A 0x464a #define USB_PRODUCT_ID_HP_LOGITECH_OEM_USB_OPTICAL_MOUSE_0A4A 0x0a4a diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c index 29008c2cc530..2d37ddeffdb6 100644 --- a/drivers/hid/hid-nintendo.c +++ b/drivers/hid/hid-nintendo.c @@ -316,6 +316,7 @@ enum joycon_ctlr_type { JOYCON_CTLR_TYPE_JCL = 0x01, JOYCON_CTLR_TYPE_JCR = 0x02, JOYCON_CTLR_TYPE_PRO = 0x03, + JOYCON_CTLR_TYPE_LIC_PRO = 0x06, JOYCON_CTLR_TYPE_NESL = 0x09, JOYCON_CTLR_TYPE_NESR = 0x0A, JOYCON_CTLR_TYPE_SNES = 0x0B, @@ -433,6 +434,25 @@ static const struct joycon_ctlr_button_mapping procon_button_mappings[] = { { /* sentinel */ }, }; +/* Licensed Pro Controllers (e.g. HORI) swap X/Y bits in the report */ +static const struct joycon_ctlr_button_mapping lic_procon_button_mappings[] = { + { BTN_EAST, JC_BTN_A, }, + { BTN_SOUTH, JC_BTN_B, }, + { BTN_NORTH, JC_BTN_Y, }, + { BTN_WEST, JC_BTN_X, }, + { BTN_TL, JC_BTN_L, }, + { BTN_TR, JC_BTN_R, }, + { BTN_TL2, JC_BTN_ZL, }, + { BTN_TR2, JC_BTN_ZR, }, + { BTN_SELECT, JC_BTN_MINUS, }, + { BTN_START, JC_BTN_PLUS, }, + { BTN_THUMBL, JC_BTN_LSTICK, }, + { BTN_THUMBR, JC_BTN_RSTICK, }, + { BTN_MODE, JC_BTN_HOME, }, + { BTN_Z, JC_BTN_CAP, }, + { /* sentinel */ }, +}; + static const struct joycon_ctlr_button_mapping nescon_button_mappings[] = { { BTN_SOUTH, JC_BTN_A, }, { BTN_EAST, JC_BTN_B, }, @@ -695,7 +715,8 @@ static inline bool joycon_type_is_right_joycon(struct joycon_ctlr *ctlr) static inline bool joycon_type_is_procon(struct joycon_ctlr *ctlr) { - return ctlr->ctlr_type == JOYCON_CTLR_TYPE_PRO; + return ctlr->ctlr_type == JOYCON_CTLR_TYPE_PRO || + ctlr->ctlr_type == JOYCON_CTLR_TYPE_LIC_PRO; } static inline bool joycon_type_is_snescon(struct joycon_ctlr *ctlr) @@ -1710,7 +1731,10 @@ static void joycon_parse_report(struct joycon_ctlr *ctlr, joycon_report_left_stick(ctlr, rep); joycon_report_right_stick(ctlr, rep); joycon_report_dpad(ctlr, rep); - joycon_report_buttons(ctlr, rep, procon_button_mappings); + if (ctlr->ctlr_type == JOYCON_CTLR_TYPE_LIC_PRO) + joycon_report_buttons(ctlr, rep, lic_procon_button_mappings); + else + joycon_report_buttons(ctlr, rep, procon_button_mappings); } else if (joycon_type_is_any_nescon(ctlr)) { joycon_report_dpad(ctlr, rep); joycon_report_buttons(ctlr, rep, nescon_button_mappings); @@ -2156,7 +2180,10 @@ static int joycon_input_create(struct joycon_ctlr *ctlr) joycon_config_left_stick(ctlr->input); joycon_config_right_stick(ctlr->input); joycon_config_dpad(ctlr->input); - joycon_config_buttons(ctlr->input, procon_button_mappings); + if (ctlr->ctlr_type == JOYCON_CTLR_TYPE_LIC_PRO) + joycon_config_buttons(ctlr->input, lic_procon_button_mappings); + else + joycon_config_buttons(ctlr->input, procon_button_mappings); } else if (joycon_type_is_any_nescon(ctlr)) { joycon_config_dpad(ctlr->input); joycon_config_buttons(ctlr->input, nescon_button_mappings); @@ -2503,13 +2530,30 @@ static int joycon_init(struct hid_device *hdev) if (joycon_has_joysticks(ctlr)) { /* get controller calibration data, and parse it */ - ret = joycon_request_calibration(ctlr); - if (ret) { + if (ctlr->ctlr_type == JOYCON_CTLR_TYPE_LIC_PRO) { /* - * We can function with default calibration, but it may be - * inaccurate. Provide a warning, and continue on. + * Licensed controllers may have incompatible SPI flash + * layouts. Use default calibration values. */ - hid_warn(hdev, "Analog stick positions may be inaccurate\n"); + hid_info(hdev, "using default cal for licensed controller\n"); + joycon_use_default_calibration(hdev, + &ctlr->left_stick_cal_x, + &ctlr->left_stick_cal_y, + "left", 0); + joycon_use_default_calibration(hdev, + &ctlr->right_stick_cal_x, + &ctlr->right_stick_cal_y, + "right", 0); + } else { + ret = joycon_request_calibration(ctlr); + if (ret) { + /* + * We can function with default calibration, but + * it may be inaccurate. Provide a warning, and + * continue on. + */ + hid_warn(hdev, "Analog stick positions may be inaccurate\n"); + } } } @@ -2527,8 +2571,13 @@ static int joycon_init(struct hid_device *hdev) /* Enable the IMU */ ret = joycon_enable_imu(ctlr); if (ret) { - hid_err(hdev, "Failed to enable the IMU; ret=%d\n", ret); - goto out_unlock; + if (ctlr->ctlr_type == JOYCON_CTLR_TYPE_LIC_PRO) { + hid_dbg(hdev, "IMU enable failed for licensed controller, continuing\n"); + ret = 0; + } else { + hid_err(hdev, "Failed to enable the IMU; ret=%d\n", ret); + goto out_unlock; + } } } @@ -2543,8 +2592,13 @@ static int joycon_init(struct hid_device *hdev) /* Enable rumble */ ret = joycon_enable_rumble(ctlr); if (ret) { - hid_err(hdev, "Failed to enable rumble; ret=%d\n", ret); - goto out_unlock; + if (ctlr->ctlr_type == JOYCON_CTLR_TYPE_LIC_PRO) { + hid_dbg(hdev, "rumble enable failed for licensed controller, continuing\n"); + ret = 0; + } else { + hid_err(hdev, "Failed to enable rumble; ret=%d\n", ret); + goto out_unlock; + } } } @@ -2813,6 +2867,8 @@ static const struct hid_device_id nintendo_hid_devices[] = { USB_DEVICE_ID_NINTENDO_GENCON) }, { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_N64CON) }, + { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_HORI, + USB_DEVICE_ID_HORI_WIRELESS_SWITCH_PAD) }, { } }; MODULE_DEVICE_TABLE(hid, nintendo_hid_devices); From db0a0768d09273aadadeb76730cd658d720333a4 Mon Sep 17 00:00:00 2001 From: Tianchu Chen Date: Fri, 29 May 2026 13:42:47 +0000 Subject: [PATCH 35/39] HID: hid-goodix-spi: validate report size to prevent stack buffer overflow goodix_hid_set_raw_report() builds a protocol frame in a 128-byte stack buffer (tmp_buf), writing an 11-12 byte header followed by the caller-supplied report data. The HID core caps report size at HID_MAX_BUFFER_SIZE (16384) by default, while the driver does not set hid_ll_driver.max_buffer_size and performs no bounds checking before copying the payload: memcpy(tmp_buf + tx_len, buf, len); A hidraw SET_REPORT ioctl with a report larger than ~116 bytes overflows the stack buffer. Add a size check after constructing the header, rejecting reports that would exceed the buffer capacity. Discovered by Atuin - Automated Vulnerability Discovery Engine. Fixes: 75e16c8ce283 ("HID: hid-goodix: Add Goodix HID-over-SPI driver") Cc: stable@vger.kernel.org Signed-off-by: Tianchu Chen Reviewed-by: Dmitry Torokhov Signed-off-by: Jiri Kosina --- drivers/hid/hid-goodix-spi.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/hid/hid-goodix-spi.c b/drivers/hid/hid-goodix-spi.c index 80c0288a3a38..288cb827e9d6 100644 --- a/drivers/hid/hid-goodix-spi.c +++ b/drivers/hid/hid-goodix-spi.c @@ -520,6 +520,9 @@ static int goodix_hid_set_raw_report(struct hid_device *hid, memcpy(tmp_buf + tx_len, args, args_len); tx_len += args_len; + if (tx_len + len > sizeof(tmp_buf)) + return -EINVAL; + memcpy(tmp_buf + tx_len, buf, len); tx_len += len; From 63a694c51bf120a37550890b8e7736b4888985e9 Mon Sep 17 00:00:00 2001 From: Carlos Llamas Date: Sat, 6 Jun 2026 18:15:52 +0000 Subject: [PATCH 36/39] HID: uhid: convert to hid_safe_input_report() Commit 0a3fe972a7cb ("HID: core: Mitigate potential OOB by removing bogus memset()"), added a check in hid_report_raw_event() to reject reports if the received data size is smaller than expected. This was intended to prevent OOB errors by no longer allowing zeroing-out of shorter reports due to the lack of buffer size information. However, this leads to regressions in hid_report_raw_event(), where shorter than expected reports are rejected, even though their buffers are sufficiently large to be zero-padded. To solve this issue, Benjamin introduced a safer alternative in commit 206342541fc8 ("HID: core: introduce hid_safe_input_report()"), which forwards the buffer size and allows hid_report_raw_event() to safely zero-pad the data. Convert uhid to use hid_safe_input_report() and pass UHID_DATA_MAX as the buffer size. This prevents the reported regressions [1], allowing hid core to zero-pad the shorter reports safely as expected. Cc: stable@vger.kernel.org Fixes: 0a3fe972a7cb ("HID: core: Mitigate potential OOB by removing bogus memset()") Closes: https://lore.kernel.org/all/ahsh0UtTX6e0ZeHa@google.com/ [1] Signed-off-by: Carlos Llamas Reviewed-by: Lee Jones Closes: https://lore.kernel.org/all/ahsh0UtTX6e0ZeHa@google.com/ Signed-off-by: Jiri Kosina --- drivers/hid/uhid.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/hid/uhid.c b/drivers/hid/uhid.c index 524b53a3c87b..37b60c3aaf66 100644 --- a/drivers/hid/uhid.c +++ b/drivers/hid/uhid.c @@ -595,8 +595,8 @@ static int uhid_dev_input(struct uhid_device *uhid, struct uhid_event *ev) if (!READ_ONCE(uhid->running)) return -EINVAL; - hid_input_report(uhid->hid, HID_INPUT_REPORT, ev->u.input.data, - min_t(size_t, ev->u.input.size, UHID_DATA_MAX), 0); + hid_safe_input_report(uhid->hid, HID_INPUT_REPORT, ev->u.input.data, UHID_DATA_MAX, + min_t(size_t, ev->u.input.size, UHID_DATA_MAX), 0); return 0; } @@ -606,8 +606,8 @@ static int uhid_dev_input2(struct uhid_device *uhid, struct uhid_event *ev) if (!READ_ONCE(uhid->running)) return -EINVAL; - hid_input_report(uhid->hid, HID_INPUT_REPORT, ev->u.input2.data, - min_t(size_t, ev->u.input2.size, UHID_DATA_MAX), 0); + hid_safe_input_report(uhid->hid, HID_INPUT_REPORT, ev->u.input2.data, UHID_DATA_MAX, + min_t(size_t, ev->u.input2.size, UHID_DATA_MAX), 0); return 0; } From b7799c3b6aa8f08dd9918eb4637af4ac21cf90f2 Mon Sep 17 00:00:00 2001 From: "Danny D." Date: Tue, 2 Jun 2026 00:18:28 +0300 Subject: [PATCH 37/39] HID: intel-thc-hid: intel-quickspi: reset touch IC on system resume On the Surface Pro 10 (Meteor Lake) the touchscreen stops working after a suspend/resume cycle and only recovers after a reboot. The driver logs "GET_DEVICE_INFO: recv failed: -11" on resume. This platform suspends through s2idle: /sys/power/mem_sleep exposes "[s2idle]" as the only state, there is no "deep"/S3 entry at all. The touch IC nonetheless loses power across that s2idle suspend, the same way it does across hibernation. quickspi_resume() only re-selects the THC port, restores interrupts and DMA and sends a HIDSPI_ON command, assuming the touch IC kept its power and state. When it has actually lost power the HIDSPI_ON command is never acknowledged and the descriptor read fails, leaving the touchscreen dead until the module is reloaded. quickspi_restore() already handles this for hibernation by reconfiguring the THC SPI/LTR settings and running reset_tic() to re-enumerate the device. Make quickspi_resume() do the same when the device is not a wake source. A wake-enabled device keeps its power and state across suspend, so it stays on the light restore path: resetting it would discard a pending wake touch event and break wake-on-touch. The non-wake path mirrors the existing quickspi_restore() sequence, including enabling interrupts before reset_tic(), so it introduces no new ordering relative to code already in the driver. This change has been validated on a Surface Pro 10 running the linux-surface kernel across multiple s2idle suspend/resume cycles; it has not been tested on a mainline build. Closes: https://github.com/linux-surface/linux-surface/issues/1799 Signed-off-by: Danny D. Reviewed-by: Even Xu Signed-off-by: Jiri Kosina --- .../intel-quickspi/pci-quickspi.c | 62 +++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c index f669235f1883..4ae2e1718b30 100644 --- a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c +++ b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c @@ -784,20 +784,72 @@ static int quickspi_resume(struct device *device) if (ret) return ret; + /* + * A wake-enabled device keeps its power and state across suspend, so + * only restore the THC context. Resetting it here would discard a + * pending wake touch event and break wake-on-touch. + */ + if (device_may_wakeup(qsdev->dev)) { + thc_interrupt_config(qsdev->thc_hw); + + thc_interrupt_enable(qsdev->thc_hw, true); + + ret = thc_dma_configure(qsdev->thc_hw); + if (ret) + return ret; + + return thc_interrupt_quiesce(qsdev->thc_hw, false); + } + + /* + * Otherwise the touch IC may have lost power across suspend. On + * platforms that suspend through s2idle (for example the Surface Pro + * 10, whose firmware exposes s2idle as the only mem_sleep state) the + * IC loses power the same way it does across hibernation. A plain + * HIDSPI_ON is then not acknowledged and the descriptor read fails, so + * re-enumerate the device through the full reset flow already used by + * quickspi_restore(). + */ + thc_spi_input_output_address_config(qsdev->thc_hw, + qsdev->input_report_hdr_addr, + qsdev->input_report_bdy_addr, + qsdev->output_report_addr); + + ret = thc_spi_read_config(qsdev->thc_hw, qsdev->spi_freq_val, + qsdev->spi_read_io_mode, + qsdev->spi_read_opcode, + qsdev->spi_packet_size); + if (ret) + return ret; + + ret = thc_spi_write_config(qsdev->thc_hw, qsdev->spi_freq_val, + qsdev->spi_write_io_mode, + qsdev->spi_write_opcode, + qsdev->spi_packet_size, + qsdev->performance_limit); + if (ret) + return ret; + thc_interrupt_config(qsdev->thc_hw); thc_interrupt_enable(qsdev->thc_hw, true); + /* The touch IC may have lost power, reset it to recover */ + ret = reset_tic(qsdev); + if (ret) + return ret; + ret = thc_dma_configure(qsdev->thc_hw); if (ret) return ret; - ret = thc_interrupt_quiesce(qsdev->thc_hw, false); - if (ret) - return ret; + thc_ltr_config(qsdev->thc_hw, + qsdev->active_ltr_val, + qsdev->low_power_ltr_val); - if (!device_may_wakeup(qsdev->dev)) - return quickspi_set_power(qsdev, HIDSPI_ON); + thc_change_ltr_mode(qsdev->thc_hw, THC_LTR_MODE_ACTIVE); + + qsdev->state = QUICKSPI_ENABLED; return 0; } From f0866517be9345d8245d32b722574b8aecccb348 Mon Sep 17 00:00:00 2001 From: Lauri Saurus Date: Mon, 18 May 2026 19:28:50 +0000 Subject: [PATCH 38/39] HID: logitech-hidpp: sync wheel multiplier on wheel mode changes The hid-logitech-hidpp driver enables high resolution scrolling on device connect for capable HID++ 2.0 devices. Driver also reads the wheel capability and caches the returned high resolution wheel scroll multiplier, that is used for scroll scaling when handling wheel scroll events. Wheel mode can also be set externally through HID++ requests, which can leave the cached multiplier stale and cause incorrect scroll scaling. If external SetWheelMode HID++ request sets the mode to low resolution, the cached multiplier is not updated accordingly. This causes extremely slow scrolling since driver expects multiple wheel scroll events per detent but is only getting one. The fix listens for HID++ SetWheelMode request responses and updates the wheel scroll multiplier based on the set high resolution scroll mode. The fix has been tested with Logitech G502X lightspeed mouse. Signed-off-by: Lauri Saurus Signed-off-by: Jiri Kosina --- drivers/hid/hid-logitech-hidpp.c | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c index 1990ba5b26ea..70ba1a5e40d8 100644 --- a/drivers/hid/hid-logitech-hidpp.c +++ b/drivers/hid/hid-logitech-hidpp.c @@ -206,6 +206,9 @@ struct hidpp_device { u8 wireless_feature_index; + int hires_wheel_multiplier; + u8 hires_wheel_feature_index; + bool connected_once; }; @@ -3709,6 +3712,7 @@ static int hi_res_scroll_enable(struct hidpp_device *hidpp) multiplier = 1; } + hidpp->hires_wheel_multiplier = multiplier; hidpp->vertical_wheel_counter.wheel_multiplier = multiplier; hid_dbg(hidpp->hid_dev, "wheel multiplier = %d\n", multiplier); return 0; @@ -3719,6 +3723,7 @@ static int hidpp_initialize_hires_scroll(struct hidpp_device *hidpp) int ret; unsigned long capabilities; + hidpp->hires_wheel_feature_index = 0xff; capabilities = hidpp->capabilities; if (hidpp->protocol_major >= 2) { @@ -3728,6 +3733,7 @@ static int hidpp_initialize_hires_scroll(struct hidpp_device *hidpp) &feature_index); if (!ret) { hidpp->capabilities |= HIDPP_CAPABILITY_HIDPP20_HI_RES_WHEEL; + hidpp->hires_wheel_feature_index = feature_index; hid_dbg(hidpp->hid_dev, "Detected HID++ 2.0 hi-res scroll wheel\n"); return 0; } @@ -3750,6 +3756,31 @@ static int hidpp_initialize_hires_scroll(struct hidpp_device *hidpp) return 0; } +static int hidpp20_hires_wheel_raw_event(struct hidpp_device *hidpp, + u8 *data, int size) +{ + if (hidpp->hires_wheel_feature_index == 0xff) + return 0; + + if (size < 5) + return 0; + + if (data[0] != REPORT_ID_HIDPP_LONG || + data[2] != hidpp->hires_wheel_feature_index) + return 0; + + if ((data[3] & 0xf0) == CMD_HIRES_WHEEL_SET_WHEEL_MODE) { + u8 mode = data[4]; + bool hires = (mode & 0x02) != 0; + int new_multiplier = (hires && hidpp->hires_wheel_multiplier > 0) + ? hidpp->hires_wheel_multiplier : 1; + hidpp->vertical_wheel_counter.wheel_multiplier = new_multiplier; + return 1; + } + + return 0; +} + /* -------------------------------------------------------------------------- */ /* Generic HID++ devices */ /* -------------------------------------------------------------------------- */ @@ -3946,6 +3977,12 @@ static int hidpp_raw_hidpp_event(struct hidpp_device *hidpp, u8 *data, return ret; } + if (hidpp->capabilities & HIDPP_CAPABILITY_HIDPP20_HI_RES_WHEEL) { + ret = hidpp20_hires_wheel_raw_event(hidpp, data, size); + if (ret != 0) + return ret; + } + return 0; } From 6df6b1f2c49678211f65647c300bc51dda02893b Mon Sep 17 00:00:00 2001 From: Jiri Kosina Date: Fri, 12 Jun 2026 17:48:22 +0200 Subject: [PATCH 39/39] HID: hidpp: fix potential UAF in hidpp_connect_event() If input_register_device() fails, we call input_free_device(), but keep stale pointer to the old device in hidpp->input, which could potentially lead to UAF. Fix that by resetting it to NULL before returning from hidpp_connect_event(). Reported-by: zdi-disclosures@trendmicro.com Signed-off-by: Jiri Kosina --- drivers/hid/hid-logitech-hidpp.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c index ccbf28869a96..d8e86b6ccf37 100644 --- a/drivers/hid/hid-logitech-hidpp.c +++ b/drivers/hid/hid-logitech-hidpp.c @@ -4295,6 +4295,7 @@ static void hidpp_connect_event(struct work_struct *work) ret = input_register_device(input); if (ret) { + hidpp->input = NULL; input_free_device(input); return; }