Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH v4 1/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-04-15  7:31 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260415073142.1303505-1-vi@endrift.com>

This adds a new driver for the Switch 2 controllers. The Switch 2 uses an
unusual split-interface design such that input and rumble occur on the main
HID interface, but all other communication occurs over a "configuration"
interface. This is the case on both USB and Bluetooth, so this new driver
uses a split-driver design with the HID interface being the "main" driver
and the configuration interface is a secondary driver that looks up to the
HID interface, sharing resources on a common struct.

Due to using a non-standard pairing interface as well as Bluetooth
communications being extremely limited in the kernel, a custom interface
between userspace and the kernel will need to be designed, along with
bringup in BlueZ. That is beyond the scope of this initial patch, which
only contains the generic HID and USB configuration interface drivers.

This initial work supports general input for the Joy-Con 2, Pro Controller
2, and GameCube NSO controllers. IMU, rumble and battery support is not yet
present.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 MAINTAINERS                                   |    1 +
 drivers/hid/Kconfig                           |   11 +-
 drivers/hid/hid-ids.h                         |    4 +
 drivers/hid/hid-nintendo.c                    | 1194 ++++++++++++++++-
 drivers/hid/hid-nintendo.h                    |   72 +
 drivers/input/joystick/Kconfig                |   11 +
 drivers/input/joystick/Makefile               |    1 +
 drivers/input/joystick/nintendo-switch2-usb.c |  353 +++++
 8 files changed, 1637 insertions(+), 10 deletions(-)
 create mode 100644 drivers/hid/hid-nintendo.h
 create mode 100644 drivers/input/joystick/nintendo-switch2-usb.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 7b277d5bf3d1..4d1a28df5fd2 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18743,6 +18743,7 @@ F:	drivers/scsi/nsp32*
 
 NINTENDO HID DRIVER
 M:	Daniel J. Ogorchock <djogorchock@gmail.com>
+M:	Vicki Pfau <vi@endrift.com>
 L:	linux-input@vger.kernel.org
 S:	Maintained
 F:	drivers/hid/hid-nintendo*
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index c1d9f7c6a5f2..1a293a6c02c2 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -826,10 +826,13 @@ config HID_NINTENDO
 	depends on LEDS_CLASS
 	select POWER_SUPPLY
 	help
-	Adds support for the Nintendo Switch Joy-Cons, NSO, Pro Controller.
-	All controllers support bluetooth, and the Pro Controller also supports
-	its USB mode. This also includes support for the Nintendo Switch Online
-	Controllers which include the NES, Genesis, SNES, and N64 controllers.
+	Adds support for the Nintendo Switch Joy-Cons, NSO, Pro Controller, as
+	well as Nintendo Switch 2 Joy-Cons, Pro Controller, and NSO GameCube
+	controllers. All Switch controllers support bluetooth, and the Pro
+	Controller also supports its USB mode. This also includes support for
+	the Nintendo Switch Online Controllers which include the NES, Genesis,
+	SNES, and N64 controllers. Switch 2 controllers currently only support
+	USB mode.
 
 	To compile this driver as a module, choose M here: the
 	module will be called hid-nintendo.
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 4ab7640b119a..a794dad7980f 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1073,6 +1073,10 @@
 #define USB_DEVICE_ID_NINTENDO_SNESCON	0x2017
 #define USB_DEVICE_ID_NINTENDO_GENCON	0x201e
 #define USB_DEVICE_ID_NINTENDO_N64CON	0x2019
+#define USB_DEVICE_ID_NINTENDO_NS2_JOYCONR	0x2066
+#define USB_DEVICE_ID_NINTENDO_NS2_JOYCONL	0x2067
+#define USB_DEVICE_ID_NINTENDO_NS2_PROCON	0x2069
+#define USB_DEVICE_ID_NINTENDO_NS2_GCCON	0x2073
 
 #define USB_VENDOR_ID_NOVATEK		0x0603
 #define USB_DEVICE_ID_NOVATEK_PCT	0x0600
diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
index 29008c2cc530..ac84e32ed0bd 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -1,11 +1,13 @@
 // SPDX-License-Identifier: GPL-2.0+
 /*
- * HID driver for Nintendo Switch Joy-Cons and Pro Controllers
+ * HID driver for Nintendo Switch Joy-Cons and Pro Controllers, as well as
+ * Nintendo Switch 2 Joy-Cons, Pro Controller, and GameCube Controller
  *
  * Copyright (c) 2019-2021 Daniel J. Ogorchock <djogorchock@gmail.com>
  * Portions Copyright (c) 2020 Nadia Holmquist Pedersen <nadia@nhp.sh>
  * Copyright (c) 2022 Emily Strickland <linux@emily.st>
  * Copyright (c) 2023 Ryan McClelland <rymcclel@gmail.com>
+ * Copyright (c) 2026 Valve Software
  *
  * The following resources/projects were referenced for this driver:
  *   https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
@@ -13,6 +15,8 @@
  *   https://github.com/FrotBot/SwitchProConLinuxUSB
  *   https://github.com/MTCKC/ProconXInput
  *   https://github.com/Davidobot/BetterJoyForCemu
+ *   https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
+ *   https://github.com/ndeadly/switch2_controller_research
  *   hid-wiimote kernel hid driver
  *   hid-logitech-hidpp driver
  *   hid-sony driver
@@ -29,6 +33,7 @@
  */
 
 #include "hid-ids.h"
+#include "hid-nintendo.h"
 #include <linux/unaligned.h>
 #include <linux/delay.h>
 #include <linux/device.h>
@@ -41,6 +46,8 @@
 #include <linux/module.h>
 #include <linux/power_supply.h>
 #include <linux/spinlock.h>
+#include <linux/usb.h>
+#include "usbhid/usbhid.h"
 
 /*
  * Reference the url below for the following HID report defines:
@@ -2614,7 +2621,7 @@ static int joycon_ctlr_handle_event(struct joycon_ctlr *ctlr, u8 *data,
 	return ret;
 }
 
-static int nintendo_hid_event(struct hid_device *hdev,
+static int joycon_event(struct hid_device *hdev,
 			      struct hid_report *report, u8 *raw_data, int size)
 {
 	struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
@@ -2625,7 +2632,7 @@ static int nintendo_hid_event(struct hid_device *hdev,
 	return joycon_ctlr_handle_event(ctlr, raw_data, size);
 }
 
-static int nintendo_hid_probe(struct hid_device *hdev,
+static int joycon_probe(struct hid_device *hdev,
 			    const struct hid_device_id *id)
 {
 	int ret;
@@ -2729,7 +2736,7 @@ static int nintendo_hid_probe(struct hid_device *hdev,
 	return ret;
 }
 
-static void nintendo_hid_remove(struct hid_device *hdev)
+static void joycon_remove(struct hid_device *hdev)
 {
 	struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
 	unsigned long flags;
@@ -2748,7 +2755,9 @@ static void nintendo_hid_remove(struct hid_device *hdev)
 	hid_hw_stop(hdev);
 }
 
-static int nintendo_hid_resume(struct hid_device *hdev)
+#ifdef CONFIG_PM
+
+static int joycon_resume(struct hid_device *hdev)
 {
 	struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
 	int ret;
@@ -2771,7 +2780,7 @@ static int nintendo_hid_resume(struct hid_device *hdev)
 	return ret;
 }
 
-static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message)
+static int joycon_suspend(struct hid_device *hdev, pm_message_t message)
 {
 	struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
 
@@ -2790,7 +2799,1120 @@ static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message)
 	return 0;
 }
 
+#endif
+
+/*
+ * =============================================================================
+ * Switch 2 support
+ * =============================================================================
+ */
+#define NS2_BTNR_B	BIT(0)
+#define NS2_BTNR_A	BIT(1)
+#define NS2_BTNR_Y	BIT(2)
+#define NS2_BTNR_X	BIT(3)
+#define NS2_BTNR_R	BIT(4)
+#define NS2_BTNR_ZR	BIT(5)
+#define NS2_BTNR_PLUS	BIT(6)
+#define NS2_BTNR_RS	BIT(7)
+
+#define NS2_BTNL_DOWN	BIT(0)
+#define NS2_BTNL_RIGHT	BIT(1)
+#define NS2_BTNL_LEFT	BIT(2)
+#define NS2_BTNL_UP	BIT(3)
+#define NS2_BTNL_L	BIT(4)
+#define NS2_BTNL_ZL	BIT(5)
+#define NS2_BTNL_MINUS	BIT(6)
+#define NS2_BTNL_LS	BIT(7)
+
+#define NS2_BTN3_C	BIT(4)
+#define NS2_BTN3_SR	BIT(6)
+#define NS2_BTN3_SL	BIT(7)
+
+#define NS2_BTN_JCR_HOME	BIT(0)
+#define NS2_BTN_JCR_GR		BIT(2)
+#define NS2_BTN_JCR_C		NS2_BTN3_C
+#define NS2_BTN_JCR_SR		NS2_BTN3_SR
+#define NS2_BTN_JCR_SL		NS2_BTN3_SL
+
+#define NS2_BTN_JCL_CAPTURE	BIT(0)
+#define NS2_BTN_JCL_GL		BIT(2)
+#define NS2_BTN_JCL_SR		NS2_BTN3_SR
+#define NS2_BTN_JCL_SL		NS2_BTN3_SL
+
+#define NS2_BTN_PRO_HOME	BIT(0)
+#define NS2_BTN_PRO_CAPTURE	BIT(1)
+#define NS2_BTN_PRO_GR		BIT(2)
+#define NS2_BTN_PRO_GL		BIT(3)
+#define NS2_BTN_PRO_C		NS2_BTN3_C
+
+#define NS2_BTN_GC_HOME		BIT(0)
+#define NS2_BTN_GC_CAPTURE	BIT(1)
+#define NS2_BTN_GC_C		NS2_BTN3_C
+
+#define NS2_TRIGGER_RANGE	4095
+#define NS2_AXIS_MIN		-32768
+#define NS2_AXIS_MAX		32767
+
+#define NS2_MAX_PLAYER_ID	8
+
+#define NS2_MAX_INIT_RETRIES	4
+
+#define NS2_FLASH_ADDR_SERIAL			0x13002
+#define NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB	0x130a8
+#define NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB	0x130e8
+#define NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB	0x13140
+#define NS2_FLASH_ADDR_USER_PRIMARY_CALIB	0x1fc040
+#define NS2_FLASH_ADDR_USER_SECONDARY_CALIB	0x1fc080
+
+#define NS2_FLASH_SIZE_SERIAL 0x10
+#define NS2_FLASH_SIZE_FACTORY_AXIS_CALIB 9
+#define NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB 2
+#define NS2_FLASH_SIZE_USER_AXIS_CALIB 11
+
+#define NS2_USER_CALIB_MAGIC 0xa1b2
+
+#define NS2_FEATURE_BUTTONS	BIT(0)
+#define NS2_FEATURE_ANALOG	BIT(1)
+#define NS2_FEATURE_IMU		BIT(2)
+#define NS2_FEATURE_MOUSE	BIT(4)
+#define NS2_FEATURE_RUMBLE	BIT(5)
+#define NS2_FEATURE_MAGNETO	BIT(7)
+
+enum switch2_subcmd_flash {
+	NS2_SUBCMD_FLASH_READ_BLOCK = 0x01,
+	NS2_SUBCMD_FLASH_WRITE_BLOCK = 0x02,
+	NS2_SUBCMD_FLASH_ERASE_BLOCK = 0x03,
+	NS2_SUBCMD_FLASH_READ = 0x04,
+	NS2_SUBCMD_FLASH_WRITE = 0x05,
+};
+
+enum switch2_subcmd_init {
+	NS2_SUBCMD_INIT_SELECT_REPORT = 0xa,
+	NS2_SUBCMD_INIT_USB = 0xd,
+};
+
+enum switch2_subcmd_feature_select {
+	NS2_SUBCMD_FEATSEL_GET_INFO = 0x1,
+	NS2_SUBCMD_FEATSEL_SET_MASK = 0x2,
+	NS2_SUBCMD_FEATSEL_CLEAR_MASK = 0x3,
+	NS2_SUBCMD_FEATSEL_ENABLE = 0x4,
+	NS2_SUBCMD_FEATSEL_DISABLE = 0x5,
+};
+
+enum switch2_subcmd_grip {
+	NS2_SUBCMD_GRIP_GET_INFO = 0x1,
+	NS2_SUBCMD_GRIP_ENABLE_BUTTONS = 0x2,
+	NS2_SUBCMD_GRIP_GET_INFO_EXT = 0x3,
+};
+
+enum switch2_subcmd_led {
+	NS2_SUBCMD_LED_P1 = 0x1,
+	NS2_SUBCMD_LED_P2 = 0x2,
+	NS2_SUBCMD_LED_P3 = 0x3,
+	NS2_SUBCMD_LED_P4 = 0x4,
+	NS2_SUBCMD_LED_ALL_ON = 0x5,
+	NS2_SUBCMD_LED_ALL_OFF = 0x6,
+	NS2_SUBCMD_LED_PATTERN = 0x7,
+	NS2_SUBCMD_LED_BLINK = 0x8,
+};
+
+enum switch2_subcmd_fw_info {
+	NS2_SUBCMD_FW_INFO_GET = 0x1,
+};
+
+enum switch2_ctlr_type {
+	NS2_CTLR_TYPE_JCL = 0x00,
+	NS2_CTLR_TYPE_JCR = 0x01,
+	NS2_CTLR_TYPE_PRO = 0x02,
+	NS2_CTLR_TYPE_GC = 0x03,
+};
+
+enum switch2_report_id {
+	NS2_REPORT_UNIFIED = 0x05,
+	NS2_REPORT_JCL = 0x07,
+	NS2_REPORT_JCR = 0x08,
+	NS2_REPORT_PRO = 0x09,
+	NS2_REPORT_GC = 0x0a,
+};
+
+enum switch2_init_step {
+	NS2_INIT_READ_SERIAL,
+	NS2_INIT_GET_FIRMWARE_INFO,
+	NS2_INIT_READ_FACTORY_PRIMARY_CALIB,
+	NS2_INIT_READ_FACTORY_SECONDARY_CALIB,
+	NS2_INIT_READ_FACTORY_TRIGGER_CALIB,
+	NS2_INIT_READ_USER_PRIMARY_CALIB,
+	NS2_INIT_READ_USER_SECONDARY_CALIB,
+	NS2_INIT_SET_FEATURE_MASK,
+	NS2_INIT_ENABLE_FEATURES,
+	NS2_INIT_GRIP_BUTTONS,
+	NS2_INIT_REPORT_FORMAT,
+	NS2_INIT_SET_PLAYER_LEDS,
+	NS2_INIT_INPUT,
+	NS2_INIT_FINISH,
+	NS2_INIT_DONE,
+};
+
+struct switch2_version_info {
+	uint8_t major;
+	uint8_t minor;
+	uint8_t patch;
+	uint8_t ctlr_type;
+	__le32 unk;
+	int8_t dsp_major;
+	int8_t dsp_minor;
+	int8_t dsp_patch;
+	int8_t dsp_type;
+};
+
+struct switch2_axis_calibration {
+	uint16_t neutral;
+	uint16_t negative;
+	uint16_t positive;
+};
+
+struct switch2_stick_calibration {
+	struct switch2_axis_calibration x;
+	struct switch2_axis_calibration y;
+};
+
+struct switch2_controller {
+	struct hid_device *hdev;
+	struct switch2_cfg_intf *cfg;
+
+	char name[64];
+	char phys[64];
+	struct list_head entry;
+	struct mutex lock;
+
+	enum switch2_ctlr_type ctlr_type;
+	enum switch2_init_step init_step;
+	struct input_dev __rcu *input;
+	char serial[NS2_FLASH_SIZE_SERIAL + 1];
+	struct switch2_version_info version;
+
+	struct switch2_stick_calibration stick_calib[2];
+	uint8_t lt_zero;
+	uint8_t rt_zero;
+
+	uint32_t player_id;
+	struct led_classdev leds[4];
+};
+
+static DEFINE_MUTEX(switch2_controllers_lock);
+static LIST_HEAD(switch2_controllers);
+
+struct switch2_ctlr_button_mapping {
+	uint32_t code;
+	int byte;
+	uint32_t bit;
+};
+
+static const struct switch2_ctlr_button_mapping ns2_left_joycon_button_mappings[] = {
+	{ BTN_DPAD_LEFT,	0, NS2_BTNL_LEFT,	},
+	{ BTN_DPAD_UP,		0, NS2_BTNL_UP,		},
+	{ BTN_DPAD_DOWN,	0, NS2_BTNL_DOWN,	},
+	{ BTN_DPAD_RIGHT,	0, NS2_BTNL_RIGHT,	},
+	{ BTN_TL,		0, NS2_BTNL_L,		},
+	{ BTN_TL2,		0, NS2_BTNL_ZL,		},
+	{ BTN_SELECT,		0, NS2_BTNL_MINUS,	},
+	{ BTN_THUMBL,		0, NS2_BTNL_LS,		},
+	{ KEY_RECORD,		1, NS2_BTN_JCL_CAPTURE,	},
+	{ BTN_GRIPR,		1, NS2_BTN_JCL_SL,	},
+	{ BTN_GRIPR2,		1, NS2_BTN_JCL_SR,	},
+	{ BTN_GRIPL,		1, NS2_BTN_JCL_GL,	},
+	{ /* sentinel */ },
+};
+
+static const struct switch2_ctlr_button_mapping ns2_right_joycon_button_mappings[] = {
+	{ BTN_SOUTH,	0, NS2_BTNR_A,		},
+	{ BTN_EAST,	0, NS2_BTNR_B,		},
+	{ BTN_NORTH,	0, NS2_BTNR_X,		},
+	{ BTN_WEST,	0, NS2_BTNR_Y,		},
+	{ BTN_TR,	0, NS2_BTNR_R,		},
+	{ BTN_TR2,	0, NS2_BTNR_ZR,		},
+	{ BTN_START,	0, NS2_BTNR_PLUS,	},
+	{ BTN_THUMBR,	0, NS2_BTNR_RS,		},
+	{ BTN_C,	1, NS2_BTN_JCR_C,	},
+	{ BTN_MODE,	1, NS2_BTN_JCR_HOME,	},
+	{ BTN_GRIPL2,	1, NS2_BTN_JCR_SL,	},
+	{ BTN_GRIPL,	1, NS2_BTN_JCR_SR,	},
+	{ BTN_GRIPR,	1, NS2_BTN_JCR_GR,	},
+	{ /* sentinel */ },
+};
+
+static const struct switch2_ctlr_button_mapping ns2_procon_mappings[] = {
+	{ BTN_SOUTH,	0, NS2_BTNR_A,		},
+	{ BTN_EAST,	0, NS2_BTNR_B,		},
+	{ BTN_NORTH,	0, NS2_BTNR_X,		},
+	{ BTN_WEST,	0, NS2_BTNR_Y,		},
+	{ BTN_TL,	1, NS2_BTNL_L,		},
+	{ BTN_TR,	0, NS2_BTNR_R,		},
+	{ BTN_TL2,	1, NS2_BTNL_ZL,		},
+	{ BTN_TR2,	0, NS2_BTNR_ZR,		},
+	{ BTN_SELECT,	1, NS2_BTNL_MINUS,	},
+	{ BTN_START,	0, NS2_BTNR_PLUS,	},
+	{ BTN_THUMBL,	1, NS2_BTNL_LS,		},
+	{ BTN_THUMBR,	0, NS2_BTNR_RS,		},
+	{ BTN_MODE,	2, NS2_BTN_PRO_HOME	},
+	{ KEY_RECORD,	2, NS2_BTN_PRO_CAPTURE	},
+	{ BTN_GRIPR,	2, NS2_BTN_PRO_GR	},
+	{ BTN_GRIPL,	2, NS2_BTN_PRO_GL	},
+	{ BTN_C,	2, NS2_BTN_PRO_C	},
+	{ /* sentinel */ },
+};
+
+static const struct switch2_ctlr_button_mapping ns2_gccon_mappings[] = {
+	{ BTN_SOUTH,	0, NS2_BTNR_A,		},
+	{ BTN_EAST,	0, NS2_BTNR_B,		},
+	{ BTN_NORTH,	0, NS2_BTNR_X,		},
+	{ BTN_WEST,	0, NS2_BTNR_Y,		},
+	{ BTN_TL2,	1, NS2_BTNL_L,		},
+	{ BTN_TR2,	0, NS2_BTNR_R,		},
+	{ BTN_TL,	1, NS2_BTNL_ZL,		},
+	{ BTN_TR,	0, NS2_BTNR_ZR,		},
+	{ BTN_SELECT,	1, NS2_BTNL_MINUS,	},
+	{ BTN_START,	0, NS2_BTNR_PLUS,	},
+	{ BTN_MODE,	2, NS2_BTN_GC_HOME	},
+	{ KEY_RECORD,	2, NS2_BTN_GC_CAPTURE	},
+	{ BTN_C,	2, NS2_BTN_GC_C		},
+	{ /* sentinel */ },
+};
+
+static const uint8_t switch2_init_cmd_data[] = {
+	/*
+	 * The last 6 bytes of this packet are the MAC address of
+	 * the console, but we don't need that for USB
+	 */
+	0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
+};
+
+static const uint8_t switch2_one_data[] = { 0x01, 0x00, 0x00, 0x00 };
+
+static const uint8_t switch2_feature_mask[] = {
+	NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU,
+	0x00, 0x00, 0x00
+};
+
+static void switch2_init_step_done(struct switch2_controller *ns2, enum switch2_init_step step)
+{
+	if (ns2->init_step != step)
+		return;
+
+	ns2->init_step++;
+}
+
+static inline bool switch2_ctlr_is_joycon(enum switch2_ctlr_type type)
+{
+	return type == NS2_CTLR_TYPE_JCL || type == NS2_CTLR_TYPE_JCR;
+}
+
+static int switch2_set_leds(struct switch2_controller *ns2)
+{
+	int i;
+	uint8_t message[8] = { 0 };
+
+	for (i = 0; i < JC_NUM_LEDS; i++)
+		message[0] |= (!!ns2->leds[i].brightness) << i;
+
+	if (!ns2->cfg)
+		return -ENOTCONN;
+	return ns2->cfg->send_command(NS2_CMD_LED, NS2_SUBCMD_LED_PATTERN,
+		&message, sizeof(message),
+		ns2->cfg);
+}
+
+static int switch2_player_led_brightness_set(struct led_classdev *led,
+					    enum led_brightness brightness)
+{
+	struct device *dev = led->dev->parent;
+	struct hid_device *hdev = to_hid_device(dev);
+	struct switch2_controller *ns2 = hid_get_drvdata(hdev);
+
+	if (!ns2)
+		return -ENODEV;
+
+	guard(mutex)(&ns2->lock);
+	return switch2_set_leds(ns2);
+}
+
+static void switch2_leds_create(struct switch2_controller *ns2)
+{
+	struct hid_device *hdev = ns2->hdev;
+	struct led_classdev *led;
+	int i;
+	int player_led_pattern;
+
+	player_led_pattern = ns2->player_id % JC_NUM_LED_PATTERNS;
+	hid_dbg(hdev, "assigned player %d led pattern", player_led_pattern + 1);
+
+	for (i = 0; i < JC_NUM_LEDS; i++) {
+		led = &ns2->leds[i];
+		led->brightness = joycon_player_led_patterns[player_led_pattern][i];
+		led->max_brightness = 1;
+		led->brightness_set_blocking = switch2_player_led_brightness_set;
+		led->flags = LED_CORE_SUSPENDRESUME | LED_HW_PLUGGABLE;
+	}
+}
+
+static void switch2_config_buttons(struct input_dev *idev,
+	const struct switch2_ctlr_button_mapping button_mappings[])
+{
+	const struct switch2_ctlr_button_mapping *button;
+
+	for (button = button_mappings; button->code; button++)
+		input_set_capability(idev, EV_KEY, button->code);
+}
+
+static int switch2_init_input(struct switch2_controller *ns2)
+{
+	struct input_dev *input;
+	struct hid_device *hdev = ns2->hdev;
+	int i;
+	int ret;
+
+	switch2_init_step_done(ns2, NS2_INIT_FINISH);
+
+	rcu_read_lock();
+	input = rcu_dereference(ns2->input);
+	rcu_read_unlock();
+
+	if (input)
+		return 0;
+
+	input = devm_input_allocate_device(&hdev->dev);
+	if (!input)
+		return -ENOMEM;
+
+	input_set_drvdata(input, ns2);
+	input->dev.parent = &hdev->dev;
+	input->id.bustype = hdev->bus;
+	input->id.vendor = hdev->vendor;
+	input->id.product = hdev->product;
+	input->id.version = hdev->version;
+	input->uniq = ns2->serial;
+	input->name = ns2->name;
+	input->phys = hdev->phys;
+
+	switch (ns2->ctlr_type) {
+	case NS2_CTLR_TYPE_JCL:
+		input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		switch2_config_buttons(input, ns2_left_joycon_button_mappings);
+		break;
+	case NS2_CTLR_TYPE_JCR:
+		input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		switch2_config_buttons(input, ns2_right_joycon_button_mappings);
+		break;
+	case NS2_CTLR_TYPE_GC:
+		input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_Z, 0, NS2_TRIGGER_RANGE, 32, 128);
+		input_set_abs_params(input, ABS_RZ, 0, NS2_TRIGGER_RANGE, 32, 128);
+		input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+		input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+		switch2_config_buttons(input, ns2_gccon_mappings);
+		break;
+	case NS2_CTLR_TYPE_PRO:
+		input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+		input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+		switch2_config_buttons(input, ns2_procon_mappings);
+		break;
+	default:
+		input_free_device(input);
+		return -EINVAL;
+	}
+
+	hid_info(ns2->hdev, "Firmware version %u.%u.%u (type %i)\n", ns2->version.major,
+		ns2->version.minor, ns2->version.patch, ns2->version.ctlr_type);
+	if (ns2->version.dsp_type >= 0)
+		hid_info(ns2->hdev, "DSP version %u.%u.%u\n", ns2->version.dsp_major,
+			ns2->version.dsp_minor, ns2->version.dsp_patch);
+
+	ret = input_register_device(input);
+	if (ret < 0) {
+		hid_err(ns2->hdev, "Failed to register input; ret=%d\n", ret);
+		return ret;
+	}
+
+	for (i = 0; i < JC_NUM_LEDS; i++) {
+		struct led_classdev *led = &ns2->leds[i];
+		char *name = devm_kasprintf(&input->dev, GFP_KERNEL, "%s:%s:%s",
+				      dev_name(&input->dev),
+				      "green",
+				      joycon_player_led_names[i]);
+
+		if (!name)
+			return -ENOMEM;
+
+		led->name = name;
+		ret = devm_led_classdev_register(&input->dev, led);
+		if (ret < 0) {
+			dev_err(&input->dev, "Failed to register player %d LED; ret=%d\n",
+				i + 1, ret);
+			input_unregister_device(input);
+			return ret;
+		}
+	}
+
+	rcu_assign_pointer(ns2->input, input);
+	synchronize_rcu();
+	return 0;
+}
+
+static struct switch2_controller *switch2_get_controller(const char *phys)
+{
+	struct switch2_controller *ns2;
+
+	guard(mutex)(&switch2_controllers_lock);
+	list_for_each_entry(ns2, &switch2_controllers, entry) {
+		if (strncmp(ns2->phys, phys, sizeof(ns2->phys)) == 0)
+			return ns2;
+	}
+	ns2 = kzalloc(sizeof(*ns2), GFP_KERNEL);
+	if (!ns2)
+		return ERR_PTR(-ENOMEM);
+
+	mutex_init(&ns2->lock);
+	INIT_LIST_HEAD(&ns2->entry);
+	list_add(&ns2->entry, &switch2_controllers);
+	strscpy(ns2->phys, phys, sizeof(ns2->phys));
+	return ns2;
+}
+
+static void switch2_controller_put(struct switch2_controller *ns2)
+{
+	struct input_dev *input;
+	bool do_free;
+
+	guard(mutex)(&switch2_controllers_lock);
+	mutex_lock(&ns2->lock);
+
+	rcu_read_lock();
+	input = rcu_dereference(ns2->input);
+	rcu_read_unlock();
+
+	rcu_assign_pointer(ns2->input, NULL);
+	synchronize_rcu();
+
+	ns2->init_step = 0;
+	do_free = !ns2->hdev && !ns2->cfg;
+	mutex_unlock(&ns2->lock);
+
+	if (input)
+		input_unregister_device(input);
+
+	if (do_free) {
+		list_del_init(&ns2->entry);
+		mutex_destroy(&ns2->lock);
+		kfree(ns2);
+	}
+}
+
+static bool switch2_parse_stick_calibration(struct switch2_stick_calibration *calib,
+	const uint8_t *data)
+{
+	static const uint8_t UNCALIBRATED[9] = {
+		0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
+	};
+	if (memcmp(UNCALIBRATED, data, sizeof(UNCALIBRATED)) == 0)
+		return false;
+
+	calib->x.neutral = data[0];
+	calib->x.neutral |= (data[1] & 0x0F) << 8;
+
+	calib->y.neutral = data[1] >> 4;
+	calib->y.neutral |= data[2] << 4;
+
+	calib->x.positive = data[3];
+	calib->x.positive |= (data[4] & 0x0F) << 8;
+
+	calib->y.positive = data[4] >> 4;
+	calib->y.positive |= data[5] << 4;
+
+	calib->x.negative = data[6];
+	calib->x.negative |= (data[7] & 0x0F) << 8;
+
+	calib->y.negative = data[7] >> 4;
+	calib->y.negative |= data[8] << 4;
+
+	return true;
+}
+
+static void switch2_handle_flash_read(struct switch2_controller *ns2, uint8_t size,
+	uint32_t address, const uint8_t *data)
+{
+	bool ok;
+
+	switch (address) {
+	case NS2_FLASH_ADDR_SERIAL:
+		if (size != NS2_FLASH_SIZE_SERIAL)
+			return;
+		memcpy(ns2->serial, data, size);
+		switch2_init_step_done(ns2, NS2_INIT_READ_SERIAL);
+		break;
+	case NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB:
+		if (size != NS2_FLASH_SIZE_FACTORY_AXIS_CALIB)
+			return;
+		switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_PRIMARY_CALIB);
+		ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], data);
+		if (ok) {
+			hid_dbg(ns2->hdev, "Got factory primary stick calibration:\n");
+			hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
+				ns2->stick_calib[0].x.negative,
+				ns2->stick_calib[0].x.neutral,
+				ns2->stick_calib[0].x.positive);
+			hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
+				ns2->stick_calib[0].y.negative,
+				ns2->stick_calib[0].y.neutral,
+				ns2->stick_calib[0].y.positive);
+		} else {
+			hid_dbg(ns2->hdev, "Factory primary stick calibration not present\n");
+		}
+		break;
+	case NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB:
+		if (size != NS2_FLASH_SIZE_FACTORY_AXIS_CALIB)
+			return;
+		switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_SECONDARY_CALIB);
+		ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], data);
+		if (ok) {
+			hid_dbg(ns2->hdev, "Got factory secondary stick calibration:\n");
+			hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
+				ns2->stick_calib[1].x.negative,
+				ns2->stick_calib[1].x.neutral,
+				ns2->stick_calib[1].x.positive);
+			hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
+				ns2->stick_calib[1].y.negative,
+				ns2->stick_calib[1].y.neutral,
+				ns2->stick_calib[1].y.positive);
+		} else {
+			hid_dbg(ns2->hdev, "Factory secondary stick calibration not present\n");
+		}
+		break;
+	case NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB:
+		if (size != NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB)
+			return;
+		switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_TRIGGER_CALIB);
+		if (data[0] != 0xFF && data[1] != 0xFF) {
+			ns2->lt_zero = data[0];
+			ns2->rt_zero = data[1];
+
+			hid_dbg(ns2->hdev, "Got factory trigger calibration:\n");
+			hid_dbg(ns2->hdev, "Left zero point: %i\n", ns2->lt_zero);
+			hid_dbg(ns2->hdev, "Right zero point: %i\n", ns2->rt_zero);
+		} else {
+			hid_dbg(ns2->hdev, "Factory trigger calibration not present\n");
+		}
+		break;
+	case NS2_FLASH_ADDR_USER_PRIMARY_CALIB:
+		if (size != NS2_FLASH_SIZE_USER_AXIS_CALIB)
+			return;
+		switch2_init_step_done(ns2, NS2_INIT_READ_USER_PRIMARY_CALIB);
+		if (__le16_to_cpu(*(__le16 *)data) != NS2_USER_CALIB_MAGIC) {
+			hid_dbg(ns2->hdev, "No user primary stick calibration present\n");
+			break;
+		}
+
+		ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], &data[2]);
+		if (ok) {
+			hid_dbg(ns2->hdev, "Got user primary stick calibration:\n");
+			hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
+				ns2->stick_calib[0].x.negative,
+				ns2->stick_calib[0].x.neutral,
+				ns2->stick_calib[0].x.positive);
+			hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
+				ns2->stick_calib[0].y.negative,
+				ns2->stick_calib[0].y.neutral,
+				ns2->stick_calib[0].y.positive);
+		} else {
+			hid_dbg(ns2->hdev, "No user primary stick calibration present\n");
+		}
+		break;
+	case NS2_FLASH_ADDR_USER_SECONDARY_CALIB:
+		if (size != NS2_FLASH_SIZE_USER_AXIS_CALIB)
+			return;
+		switch2_init_step_done(ns2, NS2_INIT_READ_USER_SECONDARY_CALIB);
+		if (__le16_to_cpu(*(__le16 *)data) != NS2_USER_CALIB_MAGIC) {
+			hid_dbg(ns2->hdev, "No user secondary stick calibration present\n");
+			break;
+		}
+
+		ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], &data[2]);
+		if (ok) {
+			hid_dbg(ns2->hdev, "Got user secondary stick calibration:\n");
+			hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
+				ns2->stick_calib[1].x.negative,
+				ns2->stick_calib[1].x.neutral,
+				ns2->stick_calib[1].x.positive);
+			hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
+				ns2->stick_calib[1].y.negative,
+				ns2->stick_calib[1].y.neutral,
+				ns2->stick_calib[1].y.positive);
+		} else {
+			hid_dbg(ns2->hdev, "No user secondary stick calibration present\n");
+		}
+		break;
+	}
+}
+
+static void switch2_report_buttons(struct input_dev *input, const uint8_t *bytes,
+	const struct switch2_ctlr_button_mapping button_mappings[])
+{
+	const struct switch2_ctlr_button_mapping *button;
+
+	for (button = button_mappings; button->code; button++)
+		input_report_key(input, button->code, bytes[button->byte] & button->bit);
+}
+
+static void switch2_report_axis(struct input_dev *input, struct switch2_axis_calibration *calib,
+	int axis, bool invert, int value)
+{
+	if (calib && calib->neutral && calib->negative && calib->positive) {
+		value -= calib->neutral;
+		value *= NS2_AXIS_MAX + 1;
+		if (value < 0)
+			value /= calib->negative;
+		else
+			value /= calib->positive;
+	} else {
+		value = (value - 2048) * 16;
+	}
+
+	if (invert)
+		value = -value;
+	input_report_abs(input, axis,
+		clamp(value, NS2_AXIS_MIN, NS2_AXIS_MAX));
+}
+
+static void switch2_report_stick(struct input_dev *input, struct switch2_stick_calibration *calib,
+	int x, bool invert_x, int y, bool invert_y, const uint8_t *data)
+{
+	switch2_report_axis(input, &calib->x, x, invert_x, data[0] | ((data[1] & 0x0F) << 8));
+	switch2_report_axis(input, &calib->y, y, invert_y, (data[1] >> 4) | (data[2] << 4));
+}
+
+static void switch2_report_trigger(struct input_dev *input, uint8_t zero, int abs, uint8_t data)
+{
+	int value = (NS2_TRIGGER_RANGE + 1) * (data - zero) / (232 - zero);
+
+	input_report_abs(input, abs, clamp(value, 0, NS2_TRIGGER_RANGE));
+}
+
+static int switch2_event(struct hid_device *hdev, struct hid_report *report, uint8_t *raw_data,
+	int size)
+{
+	struct switch2_controller *ns2 = hid_get_drvdata(hdev);
+	struct input_dev *input;
+
+	if (report->type != HID_INPUT_REPORT)
+		return 0;
+
+	if (size < 15)
+		return -EINVAL;
+
+	guard(rcu)();
+	input = rcu_dereference(ns2->input);
+
+	if (!input)
+		return 0;
+
+	switch (report->id) {
+	case NS2_REPORT_UNIFIED:
+		/*
+		 * TODO
+		 * This won't be sent unless the report type gets changed via command
+		 * 03-0A, but we should support it at some point regardless.
+		 */
+		break;
+	case NS2_REPORT_JCL:
+		switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
+			ABS_Y, true, &raw_data[6]);
+		switch2_report_buttons(input, &raw_data[3], ns2_left_joycon_button_mappings);
+		break;
+	case NS2_REPORT_JCR:
+		switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
+			ABS_Y, true, &raw_data[6]);
+		switch2_report_buttons(input, &raw_data[3], ns2_right_joycon_button_mappings);
+		break;
+	case NS2_REPORT_GC:
+		input_report_abs(input, ABS_HAT0X,
+			!!(raw_data[4] & NS2_BTNL_RIGHT) -
+			!!(raw_data[4] & NS2_BTNL_LEFT));
+		input_report_abs(input, ABS_HAT0Y,
+			!!(raw_data[4] & NS2_BTNL_DOWN) -
+			!!(raw_data[4] & NS2_BTNL_UP));
+		switch2_report_buttons(input, &raw_data[3], ns2_gccon_mappings);
+		switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
+			ABS_Y, true, &raw_data[6]);
+		switch2_report_stick(input, &ns2->stick_calib[1], ABS_RX, false,
+			ABS_RY, true, &raw_data[9]);
+		switch2_report_trigger(input, ns2->lt_zero, ABS_Z, raw_data[13]);
+		switch2_report_trigger(input, ns2->rt_zero, ABS_RZ, raw_data[14]);
+		break;
+	case NS2_REPORT_PRO:
+		input_report_abs(input, ABS_HAT0X,
+			!!(raw_data[4] & NS2_BTNL_RIGHT) -
+			!!(raw_data[4] & NS2_BTNL_LEFT));
+		input_report_abs(input, ABS_HAT0Y,
+			!!(raw_data[4] & NS2_BTNL_DOWN) -
+			!!(raw_data[4] & NS2_BTNL_UP));
+		switch2_report_buttons(input, &raw_data[3], ns2_procon_mappings);
+		switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
+			ABS_Y, true, &raw_data[6]);
+		switch2_report_stick(input, &ns2->stick_calib[1], ABS_RX, false,
+			ABS_RY, true, &raw_data[9]);
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	input_sync(input);
+	return 0;
+}
+
+static int switch2_features_enable(struct switch2_controller *ns2, int features)
+{
+	__le32 feature_bits = __cpu_to_le32(features);
+
+	if (!ns2->cfg)
+		return -ENOTCONN;
+	return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_ENABLE,
+		&feature_bits, sizeof(feature_bits),
+		ns2->cfg);
+}
+
+static int switch2_read_flash(struct switch2_controller *ns2, uint32_t address,
+	uint8_t size)
+{
+	uint8_t message[8] = { size, 0x7e };
+
+	if (!ns2->cfg)
+		return -ENOTCONN;
+	*(__le32 *)&message[4] = __cpu_to_le32(address);
+	return ns2->cfg->send_command(NS2_CMD_FLASH, NS2_SUBCMD_FLASH_READ, message,
+		sizeof(message), ns2->cfg);
+}
+
+static int switch2_set_player_id(struct switch2_controller *ns2, uint32_t player_id)
+{
+	int i;
+	int player_led_pattern = player_id % JC_NUM_LED_PATTERNS;
+
+	for (i = 0; i < JC_NUM_LEDS; i++)
+		ns2->leds[i].brightness = joycon_player_led_patterns[player_led_pattern][i];
+
+	return switch2_set_leds(ns2);
+}
+
+static int switch2_set_report_format(struct switch2_controller *ns2, enum switch2_report_id fmt)
+{
+	__le32 format_id = __cpu_to_le32(fmt);
+
+	if (!ns2->cfg)
+		return -ENOTCONN;
+	return ns2->cfg->send_command(NS2_CMD_INIT, NS2_SUBCMD_INIT_SELECT_REPORT,
+		&format_id, sizeof(format_id),
+		ns2->cfg);
+}
+
+static int switch2_init_controller(struct switch2_controller *ns2)
+{
+	if (ns2->init_step == NS2_INIT_DONE)
+		return 0;
+
+	if (!ns2->cfg)
+		return -ENOTCONN;
+
+	switch (ns2->init_step) {
+	case NS2_INIT_READ_SERIAL:
+		return switch2_read_flash(ns2, NS2_FLASH_ADDR_SERIAL,
+			NS2_FLASH_SIZE_SERIAL);
+	case NS2_INIT_GET_FIRMWARE_INFO:
+		return ns2->cfg->send_command(NS2_CMD_FW_INFO, NS2_SUBCMD_FW_INFO_GET,
+			NULL, 0, ns2->cfg);
+	case NS2_INIT_READ_FACTORY_PRIMARY_CALIB:
+		return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB,
+			NS2_FLASH_SIZE_FACTORY_AXIS_CALIB);
+	case NS2_INIT_READ_FACTORY_SECONDARY_CALIB:
+		if (switch2_ctlr_is_joycon(ns2->ctlr_type)) {
+			switch2_init_step_done(ns2, ns2->init_step);
+			return switch2_init_controller(ns2);
+		}
+		return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB,
+			NS2_FLASH_SIZE_FACTORY_AXIS_CALIB);
+	case NS2_INIT_READ_FACTORY_TRIGGER_CALIB:
+		if (ns2->ctlr_type != NS2_CTLR_TYPE_GC) {
+			switch2_init_step_done(ns2, ns2->init_step);
+			return switch2_init_controller(ns2);
+		}
+		return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB,
+			NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB);
+	case NS2_INIT_READ_USER_PRIMARY_CALIB:
+		return switch2_read_flash(ns2, NS2_FLASH_ADDR_USER_PRIMARY_CALIB,
+			NS2_FLASH_SIZE_USER_AXIS_CALIB);
+	case NS2_INIT_READ_USER_SECONDARY_CALIB:
+		if (switch2_ctlr_is_joycon(ns2->ctlr_type)) {
+			switch2_init_step_done(ns2, ns2->init_step);
+			return switch2_init_controller(ns2);
+		}
+		return switch2_read_flash(ns2, NS2_FLASH_ADDR_USER_SECONDARY_CALIB,
+			NS2_FLASH_SIZE_USER_AXIS_CALIB);
+	case NS2_INIT_SET_FEATURE_MASK:
+		return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_SET_MASK,
+			switch2_feature_mask, sizeof(switch2_feature_mask), ns2->cfg);
+	case NS2_INIT_ENABLE_FEATURES:
+		return switch2_features_enable(ns2, NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG);
+	case NS2_INIT_GRIP_BUTTONS:
+		if (!switch2_ctlr_is_joycon(ns2->ctlr_type)) {
+			switch2_init_step_done(ns2, ns2->init_step);
+			return switch2_init_controller(ns2);
+		}
+		return ns2->cfg->send_command(NS2_CMD_GRIP, NS2_SUBCMD_GRIP_ENABLE_BUTTONS,
+			switch2_one_data, sizeof(switch2_one_data),
+			ns2->cfg);
+	case NS2_INIT_REPORT_FORMAT:
+		switch (ns2->ctlr_type) {
+		case NS2_CTLR_TYPE_JCL:
+			return switch2_set_report_format(ns2, NS2_REPORT_JCL);
+		case NS2_CTLR_TYPE_JCR:
+			return switch2_set_report_format(ns2, NS2_REPORT_JCR);
+		case NS2_CTLR_TYPE_PRO:
+			return switch2_set_report_format(ns2, NS2_REPORT_PRO);
+		case NS2_CTLR_TYPE_GC:
+			return switch2_set_report_format(ns2, NS2_REPORT_GC);
+		default:
+			switch2_init_step_done(ns2, ns2->init_step);
+			return switch2_init_controller(ns2);
+		}
+	case NS2_INIT_SET_PLAYER_LEDS:
+		return switch2_set_player_id(ns2, ns2->player_id);
+	case NS2_INIT_INPUT:
+		return ns2->cfg->send_command(NS2_CMD_INIT, NS2_SUBCMD_INIT_USB,
+			switch2_init_cmd_data, sizeof(switch2_init_cmd_data), ns2->cfg);
+	case NS2_INIT_FINISH:
+		if (ns2->hdev)
+			return switch2_init_input(ns2);
+		break;
+	default:
+		WARN_ON_ONCE(1);
+		break;
+	}
+	return 0;
+}
+
+int switch2_receive_command(struct switch2_controller *ns2,
+	const uint8_t *message, size_t length)
+{
+	const struct switch2_cmd_header *header;
+	int ret = 0;
+
+	if (length < 8)
+		return -EINVAL;
+
+	print_hex_dump_debug("got cmd: ", DUMP_PREFIX_OFFSET, 16, 1, message, length, false);
+
+	guard(mutex)(&ns2->lock);
+
+	header = (const struct switch2_cmd_header *)message;
+	if (!(header->flags & NS2_FLAG_OK)) {
+		ret = -EIO;
+		goto exit;
+	}
+	message = &message[8];
+	switch (header->command) {
+	case NS2_CMD_FLASH:
+		if (header->subcommand == NS2_SUBCMD_FLASH_READ) {
+			uint8_t read_size;
+			uint32_t read_address;
+
+			if (length < 16) {
+				ret = -EINVAL;
+				goto exit;
+			}
+			read_size = message[0];
+			read_address = __le32_to_cpu(*(__le32 *)&message[4]);
+			if (length < read_size + 16) {
+				ret = -EINVAL;
+				goto exit;
+			}
+			switch2_handle_flash_read(ns2, read_size, read_address, &message[8]);
+		}
+		break;
+	case NS2_CMD_INIT:
+		if (header->subcommand == NS2_SUBCMD_INIT_USB)
+			switch2_init_step_done(ns2, NS2_INIT_INPUT);
+		else if (header->subcommand == NS2_SUBCMD_INIT_SELECT_REPORT)
+			switch2_init_step_done(ns2, NS2_INIT_REPORT_FORMAT);
+		break;
+	case NS2_CMD_GRIP:
+		if (header->subcommand == NS2_SUBCMD_GRIP_ENABLE_BUTTONS)
+			switch2_init_step_done(ns2, NS2_INIT_GRIP_BUTTONS);
+		break;
+	case NS2_CMD_LED:
+		if (header->subcommand == NS2_SUBCMD_LED_PATTERN)
+			switch2_init_step_done(ns2, NS2_INIT_SET_PLAYER_LEDS);
+		break;
+	case NS2_CMD_FEATSEL:
+		if (header->subcommand == NS2_SUBCMD_FEATSEL_SET_MASK)
+			switch2_init_step_done(ns2, NS2_INIT_SET_FEATURE_MASK);
+		else if (header->subcommand == NS2_SUBCMD_FEATSEL_ENABLE)
+			switch2_init_step_done(ns2, NS2_INIT_ENABLE_FEATURES);
+		break;
+	case NS2_CMD_FW_INFO:
+		if (header->subcommand == NS2_SUBCMD_FW_INFO_GET) {
+			if (length < sizeof(ns2->version)) {
+				ret = -EINVAL;
+				goto exit;
+			}
+			memcpy(&ns2->version, message, sizeof(ns2->version));
+			ns2->ctlr_type = ns2->version.ctlr_type;
+			switch2_init_step_done(ns2, NS2_INIT_GET_FIRMWARE_INFO);
+		}
+		break;
+	default:
+		break;
+	}
+
+exit:
+	if (ns2->init_step < NS2_INIT_DONE)
+		switch2_init_controller(ns2);
+
+	return ret;
+}
+EXPORT_SYMBOL_GPL(switch2_receive_command);
+
+int switch2_controller_attach_cfg(const char *phys, struct switch2_cfg_intf *cfg)
+{
+	struct switch2_controller *ns2 = switch2_get_controller(phys);
+
+	if (IS_ERR(ns2))
+		return PTR_ERR(ns2);
+
+	cfg->parent = ns2;
+
+	guard(mutex)(&ns2->lock);
+	WARN_ON(ns2->cfg);
+	ns2->cfg = cfg;
+
+	if (ns2->hdev)
+		return switch2_init_controller(ns2);
+	return 0;
+}
+EXPORT_SYMBOL_GPL(switch2_controller_attach_cfg);
+
+void switch2_controller_detach_cfg(struct switch2_controller *ns2)
+{
+	mutex_lock(&ns2->lock);
+	WARN_ON(ns2 != ns2->cfg->parent);
+	ns2->cfg = NULL;
+	mutex_unlock(&ns2->lock);
+	switch2_controller_put(ns2);
+}
+EXPORT_SYMBOL_GPL(switch2_controller_detach_cfg);
+
+static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+	struct switch2_controller *ns2;
+	struct usb_device *udev;
+	char phys[64];
+	int ret;
+
+	if (!hid_is_usb(hdev))
+		return -ENODEV;
+
+	udev = hid_to_usb_dev(hdev);
+	if (usb_make_path(udev, phys, sizeof(phys)) < 0)
+		return -EINVAL;
+
+	ret = hid_parse(hdev);
+	if (ret) {
+		hid_err(hdev, "parse failed %d\n", ret);
+		return ret;
+	}
+
+	ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+	if (ret) {
+		hid_err(hdev, "hw_start failed %d\n", ret);
+		return ret;
+	}
+
+	ret = hid_hw_open(hdev);
+	if (ret) {
+		hid_err(hdev, "hw_open failed %d\n", ret);
+		goto err_stop;
+	}
+
+	ns2 = switch2_get_controller(phys);
+	if (IS_ERR(ns2)) {
+		ret = PTR_ERR(ns2);
+		goto err_close;
+	}
+
+	guard(mutex)(&ns2->lock);
+	WARN_ON(ns2->hdev);
+	ns2->hdev = hdev;
+	switch (hdev->product | (hdev->vendor << 16)) {
+	default:
+		strscpy(ns2->name, hdev->name, sizeof(ns2->name));
+		break;
+	/* Some controllers have slightly wrong names so we override them */
+	case USB_DEVICE_ID_NINTENDO_NS2_JOYCONR | (USB_VENDOR_ID_NINTENDO << 16):
+		/* Missing the "2" in the name */
+		strscpy(ns2->name, "Nintendo Joy-Con 2 (R)", sizeof(ns2->name));
+		break;
+	case USB_DEVICE_ID_NINTENDO_NS2_GCCON | (USB_VENDOR_ID_NINTENDO << 16):
+		/* Has "Nintendo" in the name twice */
+		strscpy(ns2->name, "Nintendo GameCube Controller", sizeof(ns2->name));
+		break;
+	}
+
+	ns2->player_id = U32_MAX;
+	ret = ida_alloc(&nintendo_player_id_allocator, GFP_KERNEL);
+	if (ret < 0)
+		hid_warn(hdev, "Failed to allocate player ID, skipping; ret=%d\n", ret);
+	else
+		ns2->player_id = ret;
+
+	switch2_leds_create(ns2);
+
+	hid_set_drvdata(hdev, ns2);
+
+	if (ns2->cfg)
+		return switch2_init_controller(ns2);
+
+	return 0;
+
+err_close:
+	hid_hw_close(hdev);
+err_stop:
+	hid_hw_stop(hdev);
+
+	return ret;
+}
+
+static void switch2_remove(struct hid_device *hdev)
+{
+	struct switch2_controller *ns2 = hid_get_drvdata(hdev);
+
+	hid_hw_close(hdev);
+	mutex_lock(&ns2->lock);
+	WARN_ON(ns2->hdev != hdev);
+	ns2->hdev = NULL;
+	mutex_unlock(&ns2->lock);
+	ida_free(&nintendo_player_id_allocator, ns2->player_id);
+	switch2_controller_put(ns2);
+	hid_hw_stop(hdev);
+}
+
 static const struct hid_device_id nintendo_hid_devices[] = {
+	/* Switch devices */
 	{ HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
 			 USB_DEVICE_ID_NINTENDO_PROCON) },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
@@ -2813,10 +3935,69 @@ 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) },
+	/* Switch 2 devices */
+	{ HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
+			 USB_DEVICE_ID_NINTENDO_NS2_JOYCONL) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
+			 USB_DEVICE_ID_NINTENDO_NS2_JOYCONR) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
+			 USB_DEVICE_ID_NINTENDO_NS2_PROCON) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO,
+			 USB_DEVICE_ID_NINTENDO_NS2_GCCON) },
 	{ }
 };
 MODULE_DEVICE_TABLE(hid, nintendo_hid_devices);
 
+static bool nintendo_is_switch2(struct hid_device *hdev)
+{
+	return hdev->vendor == USB_VENDOR_ID_NINTENDO &&
+		hdev->product >= USB_DEVICE_ID_NINTENDO_NS2_JOYCONR;
+}
+
+static void nintendo_hid_remove(struct hid_device *hdev)
+{
+	if (nintendo_is_switch2(hdev))
+		switch2_remove(hdev);
+	else
+		joycon_remove(hdev);
+}
+
+static int nintendo_hid_event(struct hid_device *hdev,
+			      struct hid_report *report, u8 *raw_data, int size)
+{
+	if (nintendo_is_switch2(hdev))
+		return switch2_event(hdev, report, raw_data, size);
+	else
+		return joycon_event(hdev, report, raw_data, size);
+}
+
+static int nintendo_hid_probe(struct hid_device *hdev,
+			    const struct hid_device_id *id)
+{
+	if (nintendo_is_switch2(hdev))
+		return switch2_probe(hdev, id);
+	else
+		return joycon_probe(hdev, id);
+}
+
+#ifdef CONFIG_PM
+static int nintendo_hid_resume(struct hid_device *hdev)
+{
+	if (nintendo_is_switch2(hdev))
+		return 0;
+	else
+		return joycon_resume(hdev);
+}
+
+static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message)
+{
+	if (nintendo_is_switch2(hdev))
+		return 0;
+	else
+		return joycon_suspend(hdev, message);
+}
+#endif
+
 static struct hid_driver nintendo_hid_driver = {
 	.name		= "nintendo",
 	.id_table	= nintendo_hid_devices,
@@ -2844,4 +4025,5 @@ MODULE_LICENSE("GPL");
 MODULE_AUTHOR("Ryan McClelland <rymcclel@gmail.com>");
 MODULE_AUTHOR("Emily Strickland <linux@emily.st>");
 MODULE_AUTHOR("Daniel J. Ogorchock <djogorchock@gmail.com>");
+MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
 MODULE_DESCRIPTION("Driver for Nintendo Switch Controllers");
diff --git a/drivers/hid/hid-nintendo.h b/drivers/hid/hid-nintendo.h
new file mode 100644
index 000000000000..7aff22f30266
--- /dev/null
+++ b/drivers/hid/hid-nintendo.h
@@ -0,0 +1,72 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * HID driver for Nintendo Switch 2 controllers
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the following work:
+ *   https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
+ *   https://github.com/ndeadly/switch2_controller_research
+ */
+
+#ifndef __HID_NINTENDO_H
+#define __HID_NINTENDO_H
+
+#include <linux/bits.h>
+
+#define NS2_FLAG_OK	BIT(0)
+#define NS2_FLAG_NACK	BIT(2)
+
+enum switch2_cmd {
+	NS2_CMD_NFC = 0x01,
+	NS2_CMD_FLASH = 0x02,
+	NS2_CMD_INIT = 0x03,
+	NS2_CMD_GRIP = 0x08,
+	NS2_CMD_LED = 0x09,
+	NS2_CMD_VIBRATE = 0x0a,
+	NS2_CMD_BATTERY = 0x0b,
+	NS2_CMD_FEATSEL = 0x0c,
+	NS2_CMD_FW_UPD = 0x0d,
+	NS2_CMD_FW_INFO = 0x10,
+	NS2_CMD_BT_PAIR = 0x15,
+};
+
+enum switch2_direction {
+	NS2_DIR_IN = 0x00,
+	NS2_DIR_OUT = 0x90,
+};
+
+enum switch2_transport {
+	NS2_TRANS_USB = 0x00,
+	NS2_TRANS_BT = 0x01,
+};
+
+struct switch2_cmd_header {
+	uint8_t command;
+	uint8_t flags;
+	uint8_t transport;
+	uint8_t subcommand;
+	uint8_t unk1;
+	uint8_t length;
+	uint16_t unk2;
+};
+static_assert(sizeof(struct switch2_cmd_header) == 8);
+
+struct device;
+struct switch2_controller;
+struct switch2_cfg_intf {
+	struct switch2_controller *parent;
+	struct device *dev;
+
+	int (*send_command)(enum switch2_cmd command, uint8_t subcommand,
+		const void *message, size_t length,
+		struct switch2_cfg_intf *intf);
+};
+
+int switch2_controller_attach_cfg(const char *phys, struct switch2_cfg_intf *cfg);
+void switch2_controller_detach_cfg(struct switch2_controller *controller);
+
+int switch2_receive_command(struct switch2_controller *controller,
+	const uint8_t *message, size_t length);
+
+#endif
diff --git a/drivers/input/joystick/Kconfig b/drivers/input/joystick/Kconfig
index 7755e5b454d2..868262c6ccd9 100644
--- a/drivers/input/joystick/Kconfig
+++ b/drivers/input/joystick/Kconfig
@@ -422,4 +422,15 @@ config JOYSTICK_SEESAW
 	  To compile this driver as a module, choose M here: the module will be
 	  called adafruit-seesaw.
 
+config JOYSTICK_NINTENDO_SWITCH2_USB
+	tristate "Wired Nintendo Switch 2 controller support"
+	depends on HID_NINTENDO
+	depends on USB
+	help
+	  Say Y here if you want to enable support for wired Nintendo Switch 2
+	  controllers.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called nintendo-switch2-usb.
+
 endif
diff --git a/drivers/input/joystick/Makefile b/drivers/input/joystick/Makefile
index 9976f596a920..8f92900ae885 100644
--- a/drivers/input/joystick/Makefile
+++ b/drivers/input/joystick/Makefile
@@ -34,6 +34,7 @@ obj-$(CONFIG_JOYSTICK_SIDEWINDER)	+= sidewinder.o
 obj-$(CONFIG_JOYSTICK_SPACEBALL)	+= spaceball.o
 obj-$(CONFIG_JOYSTICK_SPACEORB)		+= spaceorb.o
 obj-$(CONFIG_JOYSTICK_STINGER)		+= stinger.o
+obj-$(CONFIG_JOYSTICK_NINTENDO_SWITCH2_USB)	+= nintendo-switch2-usb.o
 obj-$(CONFIG_JOYSTICK_TMDC)		+= tmdc.o
 obj-$(CONFIG_JOYSTICK_TURBOGRAFX)	+= turbografx.o
 obj-$(CONFIG_JOYSTICK_TWIDJOY)		+= twidjoy.o
diff --git a/drivers/input/joystick/nintendo-switch2-usb.c b/drivers/input/joystick/nintendo-switch2-usb.c
new file mode 100644
index 000000000000..ebd89d852e21
--- /dev/null
+++ b/drivers/input/joystick/nintendo-switch2-usb.c
@@ -0,0 +1,353 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * USB driver for Nintendo Switch 2 controllers configuration interface
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the following work:
+ *   https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
+ *   https://github.com/ndeadly/switch2_controller_research
+ */
+
+#include "../../hid/hid-ids.h"
+#include "../../hid/hid-nintendo.h"
+#include <linux/module.h>
+#include <linux/usb/input.h>
+
+#define NS2_BULK_SIZE 64
+#define NS2_IN_URBS 2
+#define NS2_OUT_URBS 4
+
+static struct usb_driver switch2_usb;
+
+struct switch2_urb {
+	struct urb *urb;
+	uint8_t *data;
+	bool active;
+};
+
+struct switch2_usb {
+	struct switch2_cfg_intf cfg;
+	struct usb_device *udev;
+
+	struct switch2_urb bulk_in[NS2_IN_URBS];
+	struct usb_anchor bulk_in_anchor;
+	spinlock_t bulk_in_lock;
+
+	struct switch2_urb bulk_out[NS2_OUT_URBS];
+	struct usb_anchor bulk_out_anchor;
+	spinlock_t bulk_out_lock;
+
+	int message_in;
+	struct work_struct message_in_work;
+};
+
+static void switch2_bulk_in(struct urb *urb)
+{
+	struct switch2_usb *ns2_usb = urb->context;
+	int i;
+	bool schedule = false;
+	unsigned long flags;
+
+	switch (urb->status) {
+	case 0:
+		schedule = true;
+		break;
+	case -ECONNRESET:
+	case -ENOENT:
+	case -ESHUTDOWN:
+		dev_dbg(&ns2_usb->udev->dev, "shutting down input urb: %d\n", urb->status);
+		return;
+	default:
+		dev_dbg(&ns2_usb->udev->dev, "unknown input urb status: %d\n", urb->status);
+		break;
+	}
+
+	spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+	for (i = 0; i < NS2_IN_URBS; i++) {
+		int err;
+		struct switch2_urb *ns2_urb;
+
+		if (ns2_usb->bulk_in[i].urb == urb) {
+			ns2_usb->message_in = i;
+			continue;
+		}
+
+		if (ns2_usb->bulk_in[i].active)
+			continue;
+
+		ns2_urb = &ns2_usb->bulk_in[i];
+		usb_anchor_urb(ns2_urb->urb, &ns2_usb->bulk_in_anchor);
+		err = usb_submit_urb(ns2_urb->urb, GFP_ATOMIC);
+		if (err) {
+			usb_unanchor_urb(ns2_urb->urb);
+			dev_dbg(&ns2_usb->udev->dev, "failed to queue input urb: %d\n", err);
+		} else {
+			ns2_urb->active = true;
+		}
+	}
+	spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+
+	if (schedule)
+		schedule_work(&ns2_usb->message_in_work);
+}
+
+static void switch2_bulk_out(struct urb *urb)
+{
+	struct switch2_usb *ns2_usb = urb->context;
+	int i;
+
+	guard(spinlock_irqsave)(&ns2_usb->bulk_out_lock);
+
+	switch (urb->status) {
+	case 0:
+		break;
+	case -ECONNRESET:
+	case -ENOENT:
+	case -ESHUTDOWN:
+		dev_dbg(&ns2_usb->udev->dev, "shutting down output urb: %d\n", urb->status);
+		return;
+	default:
+		dev_dbg(&ns2_usb->udev->dev, "unknown output urb status: %d\n", urb->status);
+		return;
+	}
+
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		if (ns2_usb->bulk_out[i].urb != urb)
+			continue;
+
+		ns2_usb->bulk_out[i].active = false;
+		break;
+	}
+}
+
+static int switch2_usb_send_cmd(enum switch2_cmd command, uint8_t subcommand,
+	const void *message, size_t size, struct switch2_cfg_intf *cfg)
+{
+	struct switch2_usb *ns2_usb = (struct switch2_usb *)cfg;
+	struct switch2_urb *urb = NULL;
+	int i;
+	int ret;
+	unsigned long flags;
+
+	struct switch2_cmd_header header = {
+		command, NS2_DIR_OUT | NS2_FLAG_OK, NS2_TRANS_USB, subcommand, 0, size
+	};
+
+	if (WARN_ON(size > 56))
+		return -EINVAL;
+
+	spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		if (ns2_usb->bulk_out[i].active)
+			continue;
+
+		urb = &ns2_usb->bulk_out[i];
+		urb->active = true;
+		break;
+	}
+	spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
+
+	if (!urb) {
+		dev_warn(&ns2_usb->udev->dev, "output queue full, dropping message\n");
+		return -ENOBUFS;
+	}
+
+	memcpy(urb->data, &header, sizeof(header));
+	if (message && size)
+		memcpy(&urb->data[8], message, size);
+	urb->urb->transfer_buffer_length = size + sizeof(header);
+
+	print_hex_dump_debug("sending cmd: ", DUMP_PREFIX_OFFSET, 16, 1, urb->data,
+		size + sizeof(header), false);
+
+	usb_anchor_urb(urb->urb, &ns2_usb->bulk_out_anchor);
+	ret = usb_submit_urb(urb->urb, GFP_ATOMIC);
+	if (ret) {
+		if (ret != -ENODEV)
+			dev_warn(&ns2_usb->udev->dev, "failed to submit output urb: %i", ret);
+		urb->active = false;
+		usb_unanchor_urb(urb->urb);
+		return ret;
+	}
+
+	return 0;
+}
+
+static void switch2_usb_message_in_work(struct work_struct *work)
+{
+	struct switch2_usb *ns2_usb = container_of(work, struct switch2_usb, message_in_work);
+	struct switch2_urb *urb;
+	int err;
+	unsigned long flags;
+
+	spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+	urb = &ns2_usb->bulk_in[ns2_usb->message_in];
+	spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+
+	err = switch2_receive_command(ns2_usb->cfg.parent, urb->urb->transfer_buffer,
+		urb->urb->actual_length);
+	if (err)
+		dev_dbg(&ns2_usb->udev->dev, "receive command failed: %d\n", err);
+
+	spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+	urb->active = false;
+	spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+}
+
+static int switch2_usb_probe(struct usb_interface *intf, const struct usb_device_id *id)
+{
+	struct switch2_usb *ns2_usb;
+	struct usb_device *udev;
+	struct usb_endpoint_descriptor *bulk_in, *bulk_out;
+	char phys[64];
+	int ret;
+	int i;
+
+	udev = interface_to_usbdev(intf);
+	if (usb_make_path(udev, phys, sizeof(phys)) < 0)
+		return -EINVAL;
+
+	ret = usb_find_common_endpoints(intf->cur_altsetting, &bulk_in, &bulk_out, NULL, NULL);
+	if (ret) {
+		dev_err(&intf->dev, "failed to find bulk EPs\n");
+		return ret;
+	}
+
+	ns2_usb = devm_kzalloc(&intf->dev, sizeof(*ns2_usb), GFP_KERNEL);
+	if (!ns2_usb)
+		return -ENOMEM;
+
+	ns2_usb->udev = udev;
+	for (i = 0; i < NS2_IN_URBS; i++) {
+		ns2_usb->bulk_in[i].urb = usb_alloc_urb(0, GFP_KERNEL);
+		if (!ns2_usb->bulk_in[i].urb) {
+			ret = -ENOMEM;
+			goto err_free_in;
+		}
+
+		ns2_usb->bulk_in[i].data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
+			&ns2_usb->bulk_in[i].urb->transfer_dma);
+		if (!ns2_usb->bulk_in[i].data) {
+			ret = -ENOMEM;
+			goto err_free_in;
+		}
+
+		usb_fill_bulk_urb(ns2_usb->bulk_in[i].urb, udev,
+			usb_rcvbulkpipe(udev, bulk_in->bEndpointAddress),
+			ns2_usb->bulk_in[i].data, NS2_BULK_SIZE, switch2_bulk_in, ns2_usb);
+		ns2_usb->bulk_in[i].urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+	}
+
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		ns2_usb->bulk_out[i].urb = usb_alloc_urb(0, GFP_KERNEL);
+		if (!ns2_usb->bulk_out[i].urb) {
+			ret = -ENOMEM;
+			goto err_free_out;
+		}
+
+		ns2_usb->bulk_out[i].data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
+			&ns2_usb->bulk_out[i].urb->transfer_dma);
+		if (!ns2_usb->bulk_out[i].data) {
+			ret = -ENOMEM;
+			goto err_free_out;
+		}
+
+		usb_fill_bulk_urb(ns2_usb->bulk_out[i].urb, udev,
+			usb_sndbulkpipe(udev, bulk_out->bEndpointAddress),
+			ns2_usb->bulk_out[i].data, NS2_BULK_SIZE, switch2_bulk_out, ns2_usb);
+		ns2_usb->bulk_out[i].urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+	}
+
+	ns2_usb->bulk_in[0].active = true;
+	ret = usb_submit_urb(ns2_usb->bulk_in[0].urb, GFP_ATOMIC);
+	if (ret < 0)
+		goto err_free_out;
+
+	init_usb_anchor(&ns2_usb->bulk_out_anchor);
+	spin_lock_init(&ns2_usb->bulk_out_lock);
+	init_usb_anchor(&ns2_usb->bulk_in_anchor);
+	spin_lock_init(&ns2_usb->bulk_in_lock);
+	INIT_WORK(&ns2_usb->message_in_work, switch2_usb_message_in_work);
+
+	usb_set_intfdata(intf, ns2_usb);
+
+	ns2_usb->cfg.dev = &ns2_usb->udev->dev;
+	ns2_usb->cfg.send_command = switch2_usb_send_cmd;
+
+	ret = switch2_controller_attach_cfg(phys, &ns2_usb->cfg);
+	if (ret < 0)
+		goto err_kill_urb;
+
+	return 0;
+
+err_kill_urb:
+	usb_kill_urb(ns2_usb->bulk_in[0].urb);
+err_free_out:
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_out[i].data,
+			ns2_usb->bulk_out[i].urb->transfer_dma);
+		usb_free_urb(ns2_usb->bulk_out[i].urb);
+	}
+err_free_in:
+	for (i = 0; i < NS2_IN_URBS; i++) {
+		usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_in[i].data,
+			ns2_usb->bulk_in[i].urb->transfer_dma);
+		usb_free_urb(ns2_usb->bulk_in[i].urb);
+	}
+	devm_kfree(&intf->dev, ns2_usb);
+
+	return ret;
+}
+
+static void switch2_usb_disconnect(struct usb_interface *intf)
+{
+	struct switch2_usb *ns2_usb = usb_get_intfdata(intf);
+	unsigned long flags;
+	int i;
+
+	spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
+	usb_kill_anchored_urbs(&ns2_usb->bulk_out_anchor);
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_out[i].data,
+			ns2_usb->bulk_out[i].urb->transfer_dma);
+		usb_free_urb(ns2_usb->bulk_out[i].urb);
+	}
+	spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
+
+	spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+	usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
+	cancel_work_sync(&ns2_usb->message_in_work);
+	for (i = 0; i < NS2_IN_URBS; i++) {
+		usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_in[i].data,
+			ns2_usb->bulk_in[i].urb->transfer_dma);
+		usb_free_urb(ns2_usb->bulk_in[i].urb);
+	}
+	spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+
+	switch2_controller_detach_cfg(ns2_usb->cfg.parent);
+}
+
+#define SWITCH2_CONTROLLER(vend, prod) \
+	USB_DEVICE_AND_INTERFACE_INFO(vend, prod, USB_CLASS_VENDOR_SPEC, 0, 0)
+
+static const struct usb_device_id switch2_usb_devices[] = {
+	{ SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONL) },
+	{ SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONR) },
+	{ SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_PROCON) },
+	{ SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_GCCON) },
+	{ }
+};
+MODULE_DEVICE_TABLE(usb, switch2_usb_devices);
+
+static struct usb_driver switch2_usb = {
+	.name		= "switch2",
+	.id_table	= switch2_usb_devices,
+	.probe		= switch2_usb_probe,
+	.disconnect	= switch2_usb_disconnect,
+};
+module_usb_driver(switch2_usb);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
+MODULE_DESCRIPTION("Driver for Nintendo Switch 2 Controllers");
-- 
2.53.0


^ permalink raw reply related

* [PATCH v4 2/3] HID: nintendo: Add rumble support for Switch 2 controllers
From: Vicki Pfau @ 2026-04-15  7:31 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260415073142.1303505-1-vi@endrift.com>

This adds rumble support for both the "HD Rumble" linear resonant actuator
type as used in the Joy-Cons and Pro Controller, as well as the eccentric
rotating mass type used in the GameCube controller. Note that since there's
currently no API for exposing full control of LRAs with evdev, it only
simulates a basic rumble for now.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/hid/Kconfig        |   8 +-
 drivers/hid/hid-nintendo.c | 179 ++++++++++++++++++++++++++++++++++++-
 2 files changed, 181 insertions(+), 6 deletions(-)

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 1a293a6c02c2..d8ce7451d857 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -842,10 +842,10 @@ config NINTENDO_FF
 	depends on HID_NINTENDO
 	select INPUT_FF_MEMLESS
 	help
-	Say Y here if you have a Nintendo Switch controller and want to enable
-	force feedback support for it. This works for both joy-cons, the pro
-	controller, and the NSO N64 controller. For the pro controller, both
-	rumble motors can be controlled individually.
+	Say Y here if you have a Nintendo Switch or Switch 2 controller and want
+	to enable force feedback support for it. This works for Joy-Cons, the Pro
+	Controllers, and the NSO N64 and GameCube controller. For the Pro
+	Controller, both rumble motors can be controlled individually.
 
 config HID_NTI
 	tristate "NTI keyboard adapters"
diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
index ac84e32ed0bd..d55186698deb 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -2976,6 +2976,18 @@ struct switch2_stick_calibration {
 	struct switch2_axis_calibration y;
 };
 
+struct switch2_hd_rumble {
+	uint16_t hi_freq : 10;
+	uint16_t hi_amp : 10;
+	uint16_t lo_freq : 10;
+	uint16_t lo_amp : 10;
+} __packed;
+
+struct switch2_erm_rumble {
+	uint16_t error;
+	uint16_t amplitude;
+};
+
 struct switch2_controller {
 	struct hid_device *hdev;
 	struct switch2_cfg_intf *cfg;
@@ -2997,8 +3009,45 @@ struct switch2_controller {
 
 	uint32_t player_id;
 	struct led_classdev leds[4];
+
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	spinlock_t rumble_lock;
+	uint8_t rumble_seq;
+	union {
+		struct switch2_hd_rumble hd;
+		struct switch2_erm_rumble sd;
+	} rumble;
+	unsigned long last_rumble_work;
+	struct delayed_work rumble_work;
+	uint8_t rumble_buffer[64];
+#endif
+};
+
+enum gc_rumble {
+	GC_RUMBLE_OFF = 0,
+	GC_RUMBLE_ON = 1,
+	GC_RUMBLE_STOP = 2,
 };
 
+/*
+ * The highest rumble level for "HD Rumble" is strong enough to potentially damage the controller,
+ * and also leaves your hands feeling like melted jelly, so we set a semi-arbitrary scaling factor
+ * to artificially limit the maximum for safety and comfort. It is currently unknown if the Switch
+ * 2 itself does something similar, but it's quite likely.
+ *
+ * This value must be between 0 and 1024, otherwise the math below will overflow.
+ */
+#define RUMBLE_MAX 450u
+
+/*
+ * Semi-arbitrary values used to simulate the "rumble" sensation of an eccentric rotating
+ * mass type haptic motor on the Switch 2 controllers' linear resonant actuator type haptics.
+ *
+ * The units used are unknown, but the values must be between 0 and 1023.
+ */
+#define RUMBLE_HI_FREQ 0x187
+#define RUMBLE_LO_FREQ 0x112
+
 static DEFINE_MUTEX(switch2_controllers_lock);
 static LIST_HEAD(switch2_controllers);
 
@@ -3088,9 +3137,12 @@ static const uint8_t switch2_init_cmd_data[] = {
 };
 
 static const uint8_t switch2_one_data[] = { 0x01, 0x00, 0x00, 0x00 };
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+static const uint8_t switch2_zero_data[] = { 0x00, 0x00, 0x00, 0x00 };
+#endif
 
 static const uint8_t switch2_feature_mask[] = {
-	NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU,
+	NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU | NS2_FEATURE_RUMBLE,
 	0x00, 0x00, 0x00
 };
 
@@ -3107,6 +3159,107 @@ static inline bool switch2_ctlr_is_joycon(enum switch2_ctlr_type type)
 	return type == NS2_CTLR_TYPE_JCL || type == NS2_CTLR_TYPE_JCR;
 }
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+static void switch2_encode_rumble(struct switch2_hd_rumble *rumble, uint8_t buffer[5])
+{
+	buffer[0] = rumble->hi_freq;
+	buffer[1] = (rumble->hi_freq >> 8) | (rumble->hi_amp << 2);
+	buffer[2] = (rumble->hi_amp >> 6) | (rumble->lo_freq << 4);
+	buffer[3] = (rumble->lo_freq >> 4) | (rumble->lo_amp << 6);
+	buffer[4] = rumble->lo_amp >> 2;
+}
+
+static int switch2_play_effect(struct input_dev *dev, void *data, struct ff_effect *effect)
+{
+	struct switch2_controller *ns2 = input_get_drvdata(dev);
+
+	if (effect->type != FF_RUMBLE)
+		return 0;
+
+	guard(spinlock_irqsave)(&ns2->rumble_lock);
+	if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
+		ns2->rumble.sd.amplitude = max(effect->u.rumble.strong_magnitude,
+			effect->u.rumble.weak_magnitude >> 1);
+	} else {
+		ns2->rumble.hd.hi_amp = effect->u.rumble.weak_magnitude * RUMBLE_MAX >> 16;
+		ns2->rumble.hd.lo_amp = effect->u.rumble.strong_magnitude * RUMBLE_MAX >> 16;
+	}
+
+	if (ns2->hdev)
+		schedule_delayed_work(&ns2->rumble_work, 0);
+
+	return 0;
+}
+
+static void switch2_rumble_work(struct work_struct *work)
+{
+	struct switch2_controller *ns2 = container_of(to_delayed_work(work),
+						      struct switch2_controller, rumble_work);
+	unsigned long current_ms = jiffies_to_msecs(get_jiffies_64());
+	unsigned long flags;
+	bool active;
+	int ret;
+
+	spin_lock_irqsave(&ns2->rumble_lock, flags);
+	ns2->rumble_buffer[0x1] = 0x50 | ns2->rumble_seq;
+	if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
+		ns2->rumble_buffer[0] = 3;
+		if (ns2->rumble.sd.amplitude == 0) {
+			ns2->rumble_buffer[2] = GC_RUMBLE_STOP;
+			ns2->rumble.sd.error = 0;
+			active = false;
+		} else {
+			if (ns2->rumble.sd.error < ns2->rumble.sd.amplitude) {
+				ns2->rumble_buffer[2] = GC_RUMBLE_ON;
+				ns2->rumble.sd.error += U16_MAX - ns2->rumble.sd.amplitude;
+			} else {
+				ns2->rumble_buffer[2] = GC_RUMBLE_OFF;
+				ns2->rumble.sd.error -= ns2->rumble.sd.amplitude;
+			}
+			active = true;
+		}
+	} else {
+		ns2->rumble_buffer[0] = 1;
+		switch2_encode_rumble(&ns2->rumble.hd, &ns2->rumble_buffer[0x2]);
+		active = ns2->rumble.hd.hi_amp || ns2->rumble.hd.lo_amp;
+		if (ns2->ctlr_type == NS2_CTLR_TYPE_PRO) {
+			/*
+			 * The Pro Controller contains separate LRAs on each
+			 * side that can be controlled individually.
+			 */
+			ns2->rumble_buffer[0] = 2;
+			ns2->rumble_buffer[0x11] = 0x50 | ns2->rumble_seq;
+			switch2_encode_rumble(&ns2->rumble.hd, &ns2->rumble_buffer[0x12]);
+		}
+	}
+	ns2->rumble_seq = (ns2->rumble_seq + 1) & 0xF;
+
+	if (active) {
+		unsigned long interval = msecs_to_jiffies(4);
+
+		if (!ns2->last_rumble_work)
+			ns2->last_rumble_work = current_ms;
+		else
+			ns2->last_rumble_work += interval;
+		schedule_delayed_work(&ns2->rumble_work,
+			ns2->last_rumble_work + interval - current_ms);
+	} else {
+		ns2->last_rumble_work = 0;
+	}
+	spin_unlock_irqrestore(&ns2->rumble_lock, flags);
+
+	if (!ns2->hdev) {
+		cancel_delayed_work(&ns2->rumble_work);
+		ret = -ENODEV;
+	} else {
+		ret = hid_hw_output_report(ns2->hdev, ns2->rumble_buffer, 64);
+	}
+
+	if (ret < 0)
+		hid_dbg(ns2->hdev, "Failed to send output report ret=%d\n", ret);
+}
+#endif
+
 static int switch2_set_leds(struct switch2_controller *ns2)
 {
 	int i;
@@ -3230,6 +3383,15 @@ static int switch2_init_input(struct switch2_controller *ns2)
 		return -EINVAL;
 	}
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	input_set_capability(input, EV_FF, FF_RUMBLE);
+	ret = input_ff_create_memless(input, NULL, switch2_play_effect);
+	if (ret) {
+		input_free_device(input);
+		return ret;
+	}
+#endif
+
 	hid_info(ns2->hdev, "Firmware version %u.%u.%u (type %i)\n", ns2->version.major,
 		ns2->version.minor, ns2->version.patch, ns2->version.ctlr_type);
 	if (ns2->version.dsp_type >= 0)
@@ -3309,6 +3471,10 @@ static void switch2_controller_put(struct switch2_controller *ns2)
 	if (input)
 		input_unregister_device(input);
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	cancel_delayed_work_sync(&ns2->rumble_work);
+#endif
+
 	if (do_free) {
 		list_del_init(&ns2->entry);
 		mutex_destroy(&ns2->lock);
@@ -3668,7 +3834,8 @@ static int switch2_init_controller(struct switch2_controller *ns2)
 		return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_SET_MASK,
 			switch2_feature_mask, sizeof(switch2_feature_mask), ns2->cfg);
 	case NS2_INIT_ENABLE_FEATURES:
-		return switch2_features_enable(ns2, NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG);
+		return switch2_features_enable(ns2, NS2_FEATURE_BUTTONS |
+			NS2_FEATURE_ANALOG | NS2_FEATURE_RUMBLE);
 	case NS2_INIT_GRIP_BUTTONS:
 		if (!switch2_ctlr_is_joycon(ns2->ctlr_type)) {
 			switch2_init_step_done(ns2, ns2->init_step);
@@ -3882,6 +4049,14 @@ static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id
 
 	switch2_leds_create(ns2);
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	if (ns2->ctlr_type != NS2_CTLR_TYPE_GC) {
+		ns2->rumble.hd.hi_freq = RUMBLE_HI_FREQ;
+		ns2->rumble.hd.lo_freq = RUMBLE_LO_FREQ;
+	}
+	spin_lock_init(&ns2->rumble_lock);
+	INIT_DELAYED_WORK(&ns2->rumble_work, switch2_rumble_work);
+#endif
 	hid_set_drvdata(hdev, ns2);
 
 	if (ns2->cfg)
-- 
2.53.0


^ permalink raw reply related

* [PATCH v4 3/3] HID: nintendo: Add unified report format support
From: Vicki Pfau @ 2026-04-15  7:31 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260415073142.1303505-1-vi@endrift.com>

This adds support for the "unified" report format that all controllers also
support, which has overlapping fields for like buttons and axes between
them.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/hid/hid-nintendo.c | 148 +++++++++++++++++++++++++++++++++++--
 1 file changed, 143 insertions(+), 5 deletions(-)

diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
index d55186698deb..1b9e46de60c2 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -2828,6 +2828,36 @@ static int joycon_suspend(struct hid_device *hdev, pm_message_t message)
 #define NS2_BTN3_SR	BIT(6)
 #define NS2_BTN3_SL	BIT(7)
 
+#define NS2_BTN_U1_Y	BIT(0)
+#define NS2_BTN_U1_X	BIT(1)
+#define NS2_BTN_U1_B	BIT(2)
+#define NS2_BTN_U1_A	BIT(3)
+#define NS2_BTN_U1_SR	BIT(4)
+#define NS2_BTN_U1_SL	BIT(5)
+#define NS2_BTN_U1_R	BIT(6)
+#define NS2_BTN_U1_ZR	BIT(7)
+
+#define NS2_BTN_U2_MINUS	BIT(0)
+#define NS2_BTN_U2_PLUS		BIT(1)
+#define NS2_BTN_U2_RS		BIT(2)
+#define NS2_BTN_U2_LS		BIT(3)
+#define NS2_BTN_U2_HOME		BIT(4)
+#define NS2_BTN_U2_CAPTURE	BIT(5)
+#define NS2_BTN_U2_C		BIT(6)
+
+#define NS2_BTN_U3_DOWN		BIT(0)
+#define NS2_BTN_U3_UP		BIT(1)
+#define NS2_BTN_U3_RIGHT	BIT(2)
+#define NS2_BTN_U3_LEFT		BIT(3)
+#define NS2_BTN_U3_SR		BIT(4)
+#define NS2_BTN_U3_SL		BIT(5)
+#define NS2_BTN_U3_L		BIT(6)
+#define NS2_BTN_U3_ZL		BIT(7)
+
+#define NS2_BTN_U4_GR		BIT(0)
+#define NS2_BTN_U4_GL		BIT(1)
+#define NS2_BTN_U4_HEADSET	BIT(5)
+
 #define NS2_BTN_JCR_HOME	BIT(0)
 #define NS2_BTN_JCR_GR		BIT(2)
 #define NS2_BTN_JCR_C		NS2_BTN3_C
@@ -3073,6 +3103,22 @@ static const struct switch2_ctlr_button_mapping ns2_left_joycon_button_mappings[
 	{ /* sentinel */ },
 };
 
+static const struct switch2_ctlr_button_mapping ns2_left_joycon_button_unified_mappings[] = {
+	{ BTN_DPAD_LEFT,	2, NS2_BTN_U3_LEFT,	},
+	{ BTN_DPAD_UP,		2, NS2_BTN_U3_UP,	},
+	{ BTN_DPAD_DOWN,	2, NS2_BTN_U3_DOWN,	},
+	{ BTN_DPAD_RIGHT,	2, NS2_BTN_U3_RIGHT,	},
+	{ BTN_TL,		2, NS2_BTN_U3_L,	},
+	{ BTN_TL2,		2, NS2_BTN_U3_ZL,	},
+	{ BTN_SELECT,		1, NS2_BTN_U2_MINUS,	},
+	{ BTN_THUMBL,		1, NS2_BTN_U2_LS,	},
+	{ KEY_RECORD,		1, NS2_BTN_U2_CAPTURE,	},
+	{ BTN_GRIPR,		2, NS2_BTN_U3_SL,	},
+	{ BTN_GRIPR2,		2, NS2_BTN_U3_SR,	},
+	{ BTN_GRIPL,		3, NS2_BTN_U4_GL,	},
+	{ /* sentinel */ },
+};
+
 static const struct switch2_ctlr_button_mapping ns2_right_joycon_button_mappings[] = {
 	{ BTN_SOUTH,	0, NS2_BTNR_A,		},
 	{ BTN_EAST,	0, NS2_BTNR_B,		},
@@ -3090,6 +3136,23 @@ static const struct switch2_ctlr_button_mapping ns2_right_joycon_button_mappings
 	{ /* sentinel */ },
 };
 
+static const struct switch2_ctlr_button_mapping ns2_right_joycon_button_unified_mappings[] = {
+	{ BTN_SOUTH,	0, NS2_BTN_U1_A,	},
+	{ BTN_EAST,	0, NS2_BTN_U1_B,	},
+	{ BTN_NORTH,	0, NS2_BTN_U1_X,	},
+	{ BTN_WEST,	0, NS2_BTN_U1_Y,	},
+	{ BTN_TR,	0, NS2_BTN_U1_R,	},
+	{ BTN_TR2,	0, NS2_BTN_U1_ZR	},
+	{ BTN_START,	1, NS2_BTN_U2_PLUS,	},
+	{ BTN_THUMBR,	1, NS2_BTN_U2_RS,	},
+	{ BTN_C,	1, NS2_BTN_U2_C,	},
+	{ BTN_MODE,	1, NS2_BTN_U2_HOME,	},
+	{ BTN_GRIPL2,	0, NS2_BTN_U1_SL,	},
+	{ BTN_GRIPL,	0, NS2_BTN_U1_SR,	},
+	{ BTN_GRIPR,	3, NS2_BTN_U4_GR,	},
+	{ /* sentinel */ },
+};
+
 static const struct switch2_ctlr_button_mapping ns2_procon_mappings[] = {
 	{ BTN_SOUTH,	0, NS2_BTNR_A,		},
 	{ BTN_EAST,	0, NS2_BTNR_B,		},
@@ -3111,6 +3174,27 @@ static const struct switch2_ctlr_button_mapping ns2_procon_mappings[] = {
 	{ /* sentinel */ },
 };
 
+static const struct switch2_ctlr_button_mapping ns2_procon_unified_mappings[] = {
+	{ BTN_SOUTH,	0, NS2_BTN_U1_A,	},
+	{ BTN_EAST,	0, NS2_BTN_U1_B,	},
+	{ BTN_NORTH,	0, NS2_BTN_U1_X,	},
+	{ BTN_WEST,	0, NS2_BTN_U1_Y,	},
+	{ BTN_TL,	2, NS2_BTN_U3_L,	},
+	{ BTN_TR,	0, NS2_BTN_U1_R,	},
+	{ BTN_TL2,	2, NS2_BTN_U3_ZL,	},
+	{ BTN_TR2,	0, NS2_BTN_U1_ZR,	},
+	{ BTN_SELECT,	1, NS2_BTN_U2_MINUS,	},
+	{ BTN_START,	1, NS2_BTN_U2_PLUS,	},
+	{ BTN_THUMBL,	1, NS2_BTN_U2_LS,	},
+	{ BTN_THUMBR,	1, NS2_BTN_U2_RS,	},
+	{ BTN_MODE,	1, NS2_BTN_U2_HOME	},
+	{ KEY_RECORD,	1, NS2_BTN_U2_CAPTURE	},
+	{ BTN_GRIPR,	3, NS2_BTN_U4_GR	},
+	{ BTN_GRIPL,	3, NS2_BTN_U4_GL	},
+	{ BTN_C,	1, NS2_BTN_U2_C		},
+	{ /* sentinel */ },
+};
+
 static const struct switch2_ctlr_button_mapping ns2_gccon_mappings[] = {
 	{ BTN_SOUTH,	0, NS2_BTNR_A,		},
 	{ BTN_EAST,	0, NS2_BTNR_B,		},
@@ -3128,6 +3212,23 @@ static const struct switch2_ctlr_button_mapping ns2_gccon_mappings[] = {
 	{ /* sentinel */ },
 };
 
+static const struct switch2_ctlr_button_mapping ns2_gccon_unified_mappings[] = {
+	{ BTN_SOUTH,	0, NS2_BTN_U1_A,	},
+	{ BTN_EAST,	0, NS2_BTN_U1_B,	},
+	{ BTN_NORTH,	0, NS2_BTN_U1_X,	},
+	{ BTN_WEST,	0, NS2_BTN_U1_Y,	},
+	{ BTN_TL2,	2, NS2_BTN_U3_L,	},
+	{ BTN_TR2,	0, NS2_BTN_U1_R,	},
+	{ BTN_TL,	2, NS2_BTN_U3_ZL	},
+	{ BTN_TR,	0, NS2_BTN_U1_ZR	},
+	{ BTN_SELECT,	1, NS2_BTN_U2_MINUS,	},
+	{ BTN_START,	1, NS2_BTN_U2_PLUS,	},
+	{ BTN_MODE,	1, NS2_BTN_U2_HOME	},
+	{ KEY_RECORD,	1, NS2_BTN_U2_CAPTURE	},
+	{ BTN_C,	1, NS2_BTN_U2_C		},
+	{ /* sentinel */ },
+};
+
 static const uint8_t switch2_init_cmd_data[] = {
 	/*
 	 * The last 6 bytes of this packet are the MAC address of
@@ -3691,11 +3792,48 @@ static int switch2_event(struct hid_device *hdev, struct hid_report *report, uin
 
 	switch (report->id) {
 	case NS2_REPORT_UNIFIED:
-		/*
-		 * TODO
-		 * This won't be sent unless the report type gets changed via command
-		 * 03-0A, but we should support it at some point regardless.
-		 */
+		switch (ns2->ctlr_type) {
+		case NS2_CTLR_TYPE_JCL:
+			switch2_report_stick(input, &ns2->stick_calib[0],
+				ABS_X, false, ABS_Y, true, &raw_data[11]);
+			switch2_report_buttons(input, &raw_data[5],
+				ns2_left_joycon_button_unified_mappings);
+			break;
+		case NS2_CTLR_TYPE_JCR:
+			switch2_report_stick(input, &ns2->stick_calib[0],
+				ABS_X, false, ABS_Y, true, &raw_data[14]);
+			switch2_report_buttons(input, &raw_data[5],
+				ns2_right_joycon_button_unified_mappings);
+			break;
+		case NS2_CTLR_TYPE_GC:
+			input_report_abs(input, ABS_HAT0X,
+				!!(raw_data[7] & NS2_BTN_U3_RIGHT) -
+				!!(raw_data[7] & NS2_BTN_U3_LEFT));
+			input_report_abs(input, ABS_HAT0Y,
+				!!(raw_data[7] & NS2_BTN_U3_DOWN) -
+				!!(raw_data[7] & NS2_BTN_U3_UP));
+			switch2_report_buttons(input, &raw_data[5], ns2_gccon_unified_mappings);
+			switch2_report_stick(input, &ns2->stick_calib[0],
+				ABS_X, false, ABS_Y, true, &raw_data[11]);
+			switch2_report_stick(input, &ns2->stick_calib[1],
+				ABS_RX, false, ABS_RY, true, &raw_data[14]);
+			switch2_report_trigger(input, ns2->lt_zero, ABS_Z, raw_data[0x3d]);
+			switch2_report_trigger(input, ns2->rt_zero, ABS_RZ, raw_data[0x3e]);
+			break;
+		case NS2_CTLR_TYPE_PRO:
+			input_report_abs(input, ABS_HAT0X,
+				!!(raw_data[7] & NS2_BTN_U3_RIGHT) -
+				!!(raw_data[7] & NS2_BTN_U3_LEFT));
+			input_report_abs(input, ABS_HAT0Y,
+				!!(raw_data[7] & NS2_BTN_U3_DOWN) -
+				!!(raw_data[7] & NS2_BTN_U3_UP));
+			switch2_report_buttons(input, &raw_data[5], ns2_procon_unified_mappings);
+			switch2_report_stick(input, &ns2->stick_calib[0],
+				ABS_X, false, ABS_Y, true, &raw_data[11]);
+			switch2_report_stick(input, &ns2->stick_calib[1],
+				ABS_RX, false, ABS_RY, true, &raw_data[14]);
+			break;
+		}
 		break;
 	case NS2_REPORT_JCL:
 		switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
-- 
2.53.0


^ permalink raw reply related

* [PATCH v4 0/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-04-15  7:31 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau

This series adds preliminary support for Switch 2 controllers using the
same split-driver model as previous versions. This is a minor iteration on
v3 (which was erroneously submitted as v2 again), fixing an issue where we
checked for NULL instead of handling an ERR_PTR, as well as a few typo
fixes.

Vicki Pfau (3):
  HID: nintendo: Add preliminary Switch 2 controller driver
  HID: nintendo: Add rumble support for Switch 2 controllers
  HID: nintendo: Add unified report format support

 MAINTAINERS                                   |    1 +
 drivers/hid/Kconfig                           |   19 +-
 drivers/hid/hid-ids.h                         |    4 +
 drivers/hid/hid-nintendo.c                    | 1507 ++++++++++++++++-
 drivers/hid/hid-nintendo.h                    |   72 +
 drivers/input/joystick/Kconfig                |   11 +
 drivers/input/joystick/Makefile               |    1 +
 drivers/input/joystick/nintendo-switch2-usb.c |  353 ++++
 8 files changed, 1954 insertions(+), 14 deletions(-)
 create mode 100644 drivers/hid/hid-nintendo.h
 create mode 100644 drivers/input/joystick/nintendo-switch2-usb.c

-- 
2.53.0


^ permalink raw reply

* Re: [BUG] list_add double add in cm109_input_open
From: Dmitry Torokhov @ 2026-04-15  4:03 UTC (permalink / raw)
  To: 许东洁; +Cc: linux-input, zhaoruilin22
In-Reply-To: <1fdc2218.4cc75.19d8f37b6dd.Coremail.xudongjie25@mails.ucas.ac.cn>

On Wed, Apr 15, 2026 at 11:38:06AM +0800, 许东洁 wrote:
> Hi,
> 
> While running fuzzing tests on 6.19.0-rc5, we hit a "list_add double add" bug triggered by the cm109 driver.
> 
> The root cause lies in how the driver handles multiple input devices. During probe, the driver registers two separate input devices (idev and keypad). Both devices share the same cm109_input_open() and cm109_input_close() callbacks. If both input devices are opened by user space, cm109_input_open() is called twice.

Your LLM hallucinated a bunch of stuff. Please refrain from reporting
any more "bugs" to us.

Thanks.

-- 
Dmitry

^ permalink raw reply

* [BUG] list_add double add in cm109_input_open
From: 许东洁 @ 2026-04-15  3:38 UTC (permalink / raw)
  To: dmitry.torokhov; +Cc: linux-input, zhaoruilin22

Hi,

While running fuzzing tests on 6.19.0-rc5, we hit a "list_add double add" bug triggered by the cm109 driver.

The root cause lies in how the driver handles multiple input devices. During probe, the driver registers two separate input devices (idev and keypad). Both devices share the same cm109_input_open() and cm109_input_close() callbacks. If both input devices are opened by user space, cm109_input_open() is called twice.

However, cm109_input_open() doesn't check if the URBs are already in flight. It simply sets dev->open = 1 and calls usb_submit_urb() unconditionally. Submitting the same URB twice causes the double list_add BUG in the USB core.

A possible fix would be to convert the dev->open variable from a boolean flag into a reference counter. We could increment it in cm109_input_open() and only submit the URBs if it transitions from 0 to 1, and decrement it in cm109_input_close(), killing the URBs only when the counter reaches 0.

We do not have a formal patch ready yet, but we are happy to help test any proposed fixes.

Crash log, call trace, and machine info are as follows:

[Machine Info]
QEMU emulator version 6.2.0
CPU: Intel(R) Xeon(R) Gold 5218 CPU @ 2.30GHz (4 cores)
Kernel Version: 6.19.0-rc5-00042-g944aacb68baf

[Crash Report & Call Trace]
input: CM109 USB driver as /devices/platform/dummy_hcd.2/usb3/3-1/3-1:0.0/input/input24 list_add double add: new=ffff888033606818, prev=ffff888033606818, next=ffff88802dac6078. ------------[ cut here ]------------ kernel BUG at lib/list_debug.c:35! Oops: invalid opcode: 0000 [#1] SMP KASAN NOPTI CPU: 0 UID: 0 PID: 840 Comm: kworker/0:2 Tainted: G             L      6.19.0-rc5-00042-g944aacb68baf #1 PREEMPT(full) Tainted: [L]=SOFTLOCKUP Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014 Workqueue: usb_hub_wq hub_event RIP: 0010:__list_add_valid_or_report+0x142/0x190 lib/list_debug.c:35 Code: 16 48 89 f1 48 c7 c7 c0 ba d2 8b 4c 89 e6 e8 b5 13 f1 fc 0f 0b 48 89 f2 4c 89 e1 4c 89 ee 48 c7 c7 40 bb d2 8b e8 9e 13 f1 fc <0f> 0b 48 RSP: 0018:ffffc9000562ecc0 EFLAGS: 00010086 RAX: 0000000000000058 RBX: ffff888033606800 RCX: ffffc9002335e000 RDX: 0000000000000000 RSI: ffffffff819ecb10 RDI: 0000000000000005 RBP: 0000000000000020 R08: 0000000000000001 R09: ffffed100c4c48f1 R10: 0000000080000002 R11: 00000000000a48c0 R12: ffff88802dac6078 R13: ffff888033606818 R14: ffff88802dac6080 R15: ffff888033606818 FS:  0000000000000000(0000) GS:ffff8880ced64000(0000) knlGS:0000000000000000 CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033 CR2: 00007fb35e7afe00 CR3: 000000003b6b8000 CR4: 0000000000752ef0 PKRU: 55555554 Call Trace: <TASK> __list_add_valid include/linux/list.h:96 [inline] __list_add include/linux/list.h:158 [inline] list_add_tail include/linux/list.h:191 [inline] usb_hcd_link_urb_to_ep+0x220/0x3a0 drivers/usb/core/hcd.c:1158 dummy_urb_enqueue+0x2b1/0x970 drivers/usb/gadget/udc/dummy_hcd.c:1288 usb_hcd_submit_urb+0x266/0x1fd0 drivers/usb/core/hcd.c:1546 usb_submit_urb+0x880/0x18f0 drivers/usb/core/urb.c:587 cm109_input_open+0x279/0x490 drivers/input/misc/cm109.c:566 input_open_device+0x245/0x3f0 drivers/input/input.c:601 kbd_connect+0x127/0x180 drivers/tty/vt/keyboard.c:1563 input_attach_handler+0x17b/0x260 drivers/input/input.c:994 input_register_device+0xa4f/0x1190 drivers/input/input.c:2378 cm109_usb_probe+0x128c/0x1880 drivers/input/misc/cm109.c:797 usb_probe_interface+0x30b/0xab0 drivers/usb/core/driver.c:396 call_driver_probe drivers/base/dd.c:581 [inline] really_probe+0x252/0xb20 drivers/base/dd.c:659 __driver_probe_device+0x1df/0x470 drivers/base/dd.c:801 driver_probe_device+0x4c/0x1a0 drivers/base/dd.c:831 __device_attach_driver+0x1db/0x340 drivers/base/dd.c:959 bus_for_each_drv+0x14b/0x1d0 drivers/base/bus.c:500 __device_attach+0x1ee/0x4f0 drivers/base/dd.c:1031 device_initial_probe+0xaa/0xc0 drivers/base/dd.c:1086 bus_probe_device+0x64/0x150 drivers/base/bus.c:574 device_add+0x1132/0x1930 drivers/base/core.c:3689 usb_set_configuration+0x1194/0x1c70 drivers/usb/core/message.c:2210 usb_generic_driver_probe+0xb7/0x110 drivers/usb/core/generic.c:250 usb_probe_device+0xed/0x400 drivers/usb/core/driver.c:291 call_driver_probe drivers/base/dd.c:581 [inline] really_probe+0x252/0xb20 drivers/base/dd.c:659 __driver_probe_device+0x1df/0x470 drivers/base/dd.c:801 driver_probe_device+0x4c/0x1a0 drivers/base/dd.c:831 __device_attach_driver+0x1db/0x340 drivers/base/dd.c:959 bus_for_each_drv+0x14b/0x1d0 drivers/base/bus.c:500 __device_attach+0x1ee/0x4f0 drivers/base/dd.c:1031 device_initial_probe+0xaa/0xc0 drivers/base/dd.c:1086 bus_probe_device+0x64/0x150 drivers/base/bus.c:574 device_add+0x1132/0x1930 drivers/base/core.c:3689 usb_new_device+0xd2c/0x1ac0 drivers/usb/core/hub.c:2695 hub_port_connect drivers/usb/core/hub.c:5567 [inline] hub_port_connect_change drivers/usb/core/hub.c:5707 [inline] port_event drivers/usb/core/hub.c:5871 [inline] hub_event+0x2efa/0x5020 drivers/usb/core/hub.c:5953 process_one_work+0x990/0x1af0 kernel/workqueue.c:3257 process_scheduled_works kernel/workqueue.c:3340 [inline] worker_thread+0x67e/0xe90 kernel/workqueue.c:3421 kthread+0x446/0x890 kernel/kthread.c:463 ret_from_fork+0x95b/0xae0 arch/x86/kernel/process.c:158 ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:246 </TASK>
Xu Dongjie
University of Chinese Academy of Sciences

^ permalink raw reply

* [PATCH] HID: sony: use dedicated raw_event() handlers in sony_raw_event()
From: Rosalie Wanders @ 2026-04-15  2:45 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Rosalie Wanders, linux-input, linux-kernel

This commit changes the way sony_raw_event() works by adding a function
pointer to a raw_event() handler in the sc struct instead of manually
checking the quirk in order to call the right function, this simplifies
the sony_raw_event() function alongside making the raw_event() handlers
more self-contained, thus making the code more readable.

The raw_event() handler should be configured using the new
sony_init_raw_event_handler() function in sony_input_configured(), where
we already check for quirks and apply device specific workarounds.

Signed-off-by: Rosalie Wanders <rosalie@mailbox.org>
---
This patch depends on the following patches:
- 'HID: sony: add missing size validation for SMK-Link remotes'
- 'HID: sony: add missing size validation for Rock Band 3 Pro instruments'

 drivers/hid/hid-sony.c | 143 +++++++++++++++++++++++++----------------
 1 file changed, 89 insertions(+), 54 deletions(-)

diff --git a/drivers/hid/hid-sony.c b/drivers/hid/hid-sony.c
index e75246d29e16..827cb00ff095 100644
--- a/drivers/hid/hid-sony.c
+++ b/drivers/hid/hid-sony.c
@@ -81,6 +81,7 @@
 #define SONY_FF_SUPPORT (SIXAXIS_CONTROLLER | MOTION_CONTROLLER)
 #define SONY_BT_DEVICE (SIXAXIS_CONTROLLER_BT | MOTION_CONTROLLER_BT | NAVIGATION_CONTROLLER_BT)
 #define NSG_MRXU_REMOTE (NSG_MR5U_REMOTE_BT | NSG_MR7U_REMOTE_BT)
+#define RB4_GUITAR_PS4 (RB4_GUITAR_PS4_USB | RB4_GUITAR_PS4_BT)
 
 #define MAX_LEDS 4
 #define NSG_MRXU_MAX_X 1667
@@ -534,6 +535,7 @@ struct sony_sc {
 	struct input_dev *sensor_dev;
 	struct led_classdev *leds[MAX_LEDS];
 	unsigned long quirks;
+	int (*raw_event)(struct sony_sc *sc, u8 *rd, int size);
 	struct work_struct state_worker;
 	void (*send_output_report)(struct sony_sc *sc);
 	struct power_supply *battery;
@@ -946,7 +948,7 @@ static const u8 *sony_report_fixup(struct hid_device *hdev, u8 *rdesc,
 	return rdesc;
 }
 
-static void sixaxis_parse_report(struct sony_sc *sc, u8 *rd, int size)
+static int sixaxis_raw_event(struct sony_sc *sc, u8 *rd, int size)
 {
 	static const u8 sixaxis_battery_capacity[] = { 0, 1, 25, 50, 75, 100 };
 	unsigned long flags;
@@ -955,6 +957,31 @@ static void sixaxis_parse_report(struct sony_sc *sc, u8 *rd, int size)
 	u8 battery_capacity;
 	int battery_status;
 
+	if (unlikely(size != 49 || rd[0] != 0x01))
+		return 0;
+
+	if (sc->quirks & SIXAXIS_CONTROLLER) {
+		/*
+		 * When connected via Bluetooth the Sixaxis occasionally sends
+		 * a report with the second byte 0xff and the rest zeroed.
+		 *
+		 * This report does not reflect the actual state of the
+		 * controller must be ignored to avoid generating false input
+		 * events.
+		 */
+		if (rd[1] == 0xff)
+			return -EINVAL;
+
+		/*
+		 * Sixaxis HID report has acclerometers/gyro with MSByte first, this
+		 * has to be BYTE_SWAPPED before passing up to joystick interface
+		 */
+		swap(rd[41], rd[42]);
+		swap(rd[43], rd[44]);
+		swap(rd[45], rd[46]);
+		swap(rd[47], rd[48]);
+	}
+
 	/*
 	 * The sixaxis is charging if the battery value is 0xee
 	 * and it is fully charged if the value is 0xef.
@@ -993,13 +1020,18 @@ static void sixaxis_parse_report(struct sony_sc *sc, u8 *rd, int size)
 
 		input_sync(sc->sensor_dev);
 	}
+
+	return 0;
 }
 
-static void nsg_mrxu_parse_report(struct sony_sc *sc, u8 *rd, int size)
+static int nsg_mrxu_raw_event(struct sony_sc *sc, u8 *rd, int size)
 {
 	int n, offset, relx, rely;
 	u8 active;
 
+	if (unlikely(size < 12 || rd[0] != 0x02))
+		return 0;
+
 	/*
 	 * The NSG-MRxU multi-touch trackpad data starts at offset 1 and
 	 *   the touch-related data starts at offset 2.
@@ -1067,10 +1099,33 @@ static void nsg_mrxu_parse_report(struct sony_sc *sc, u8 *rd, int size)
 	input_mt_sync_frame(sc->touchpad);
 
 	input_sync(sc->touchpad);
+	return 0;
+}
+
+static int rb3_pro_instrument_raw_event(struct sony_sc *sc, u8 *rd, int size)
+{
+	/* Rock Band 3 PS3 Pro instruments set rd[24] to 0xE0 when they're
+	 * sending full reports, and 0x02 when only sending navigation.
+	 */
+	if (size < 25 || rd[24] != 0x02)
+		return 0;
+
+	/* Only attempt to enable full report every 8 seconds */
+	if (time_after(jiffies, sc->rb3_pro_poke_jiffies)) {
+		sc->rb3_pro_poke_jiffies = jiffies + secs_to_jiffies(8);
+		rb3_pro_instrument_enable_full_report(sc);
+	}
+
+	return 0;
 }
 
-static void rb4_ps4_guitar_parse_report(struct sony_sc *sc, u8 *rd, int size)
+static int rb4_ps4_guitar_raw_event(struct sony_sc *sc, u8 *rd, int size)
 {
+	const int expected_size = (sc->quirks & RB4_GUITAR_PS4_BT) ? 78 : 64;
+
+	if (unlikely(size != expected_size || rd[0] != 0x01))
+		return 0;
+
 	/*
 	 * Rock Band 4 PS4 guitars have whammy and
 	 * tilt functionality, they're located at
@@ -1084,9 +1139,10 @@ static void rb4_ps4_guitar_parse_report(struct sony_sc *sc, u8 *rd, int size)
 	input_report_abs(sc->input_dev, ABS_RZ, rd[45]);
 
 	input_sync(sc->input_dev);
+	return 0;
 }
 
-static void rb4_ps5_guitar_parse_report(struct sony_sc *sc, u8 *rd, int size)
+static int rb4_ps5_guitar_raw_event(struct sony_sc *sc, u8 *rd, int size)
 {
 	u8 charging_status;
 	u8 battery_data;
@@ -1094,6 +1150,9 @@ static void rb4_ps5_guitar_parse_report(struct sony_sc *sc, u8 *rd, int size)
 	u8 battery_status;
 	unsigned long flags;
 
+	if (unlikely(size != 64 || rd[0] != 0x01))
+		return 0;
+
 	/*
 	 * Rock Band 4 PS5 guitars have whammy and
 	 * tilt functionality, they're located at
@@ -1138,62 +1197,19 @@ static void rb4_ps5_guitar_parse_report(struct sony_sc *sc, u8 *rd, int size)
 	spin_unlock_irqrestore(&sc->lock, flags);
 
 	input_sync(sc->input_dev);
+	return 0;
 }
 
 static int sony_raw_event(struct hid_device *hdev, struct hid_report *report,
 		u8 *rd, int size)
 {
 	struct sony_sc *sc = hid_get_drvdata(hdev);
+	int ret;
 
-	/*
-	 * Sixaxis HID report has acclerometers/gyro with MSByte first, this
-	 * has to be BYTE_SWAPPED before passing up to joystick interface
-	 */
-	if ((sc->quirks & SIXAXIS_CONTROLLER) && rd[0] == 0x01 && size == 49) {
-		/*
-		 * When connected via Bluetooth the Sixaxis occasionally sends
-		 * a report with the second byte 0xff and the rest zeroed.
-		 *
-		 * This report does not reflect the actual state of the
-		 * controller must be ignored to avoid generating false input
-		 * events.
-		 */
-		if (rd[1] == 0xff)
-			return -EINVAL;
-
-		swap(rd[41], rd[42]);
-		swap(rd[43], rd[44]);
-		swap(rd[45], rd[46]);
-		swap(rd[47], rd[48]);
-
-		sixaxis_parse_report(sc, rd, size);
-	} else if ((sc->quirks & MOTION_CONTROLLER_BT) && rd[0] == 0x01 && size == 49) {
-		sixaxis_parse_report(sc, rd, size);
-	} else if ((sc->quirks & NAVIGATION_CONTROLLER) && rd[0] == 0x01 && size == 49) {
-		sixaxis_parse_report(sc, rd, size);
-	} else if ((sc->quirks & NSG_MRXU_REMOTE) && rd[0] == 0x02 && size >= 12) {
-		nsg_mrxu_parse_report(sc, rd, size);
-		return 1;
-	} else if ((sc->quirks & RB4_GUITAR_PS4_USB) && rd[0] == 0x01 && size == 64) {
-		rb4_ps4_guitar_parse_report(sc, rd, size);
-		return 1;
-	} else if ((sc->quirks & RB4_GUITAR_PS4_BT) && rd[0] == 0x01 && size == 78) {
-		rb4_ps4_guitar_parse_report(sc, rd, size);
-		return 1;
-	} else if ((sc->quirks & RB4_GUITAR_PS5) && rd[0] == 0x01 && size == 64) {
-		rb4_ps5_guitar_parse_report(sc, rd, size);
-		return 1;
-	}
-
-	/* Rock Band 3 PS3 Pro instruments set rd[24] to 0xE0 when they're
-	 * sending full reports, and 0x02 when only sending navigation.
-	 */
-	if ((sc->quirks & RB3_PRO_INSTRUMENT) && size >= 25 && rd[24] == 0x02) {
-		/* Only attempt to enable full report every 8 seconds */
-		if (time_after(jiffies, sc->rb3_pro_poke_jiffies)) {
-			sc->rb3_pro_poke_jiffies = jiffies + secs_to_jiffies(8);
-			rb3_pro_instrument_enable_full_report(sc);
-		}
+	if (sc->raw_event) {
+		ret = sc->raw_event(sc, rd, size);
+		if (unlikely(ret < 0))
+			return ret;
 	}
 
 	if (sc->defer_initialization) {
@@ -1256,7 +1272,7 @@ static int sony_mapping(struct hid_device *hdev, struct hid_input *hi,
 	if (sc->quirks & DJH_TURNTABLE)
 		return djh_turntable_mapping(hdev, hi, field, usage, bit, max);
 
-	if (sc->quirks & (RB4_GUITAR_PS4_USB | RB4_GUITAR_PS4_BT))
+	if (sc->quirks & RB4_GUITAR_PS4)
 		return rb4_guitar_mapping(hdev, hi, field, usage, bit, max);
 
 	if (sc->quirks & RB4_GUITAR_PS5)
@@ -2110,6 +2126,12 @@ static void sony_release_device_id(struct sony_sc *sc)
 	}
 }
 
+static inline void sony_init_raw_event_handler(struct sony_sc *sc,
+				int (*raw_event)(struct sony_sc *, u8 *, int))
+{
+	sc->raw_event = raw_event;
+}
+
 static inline void sony_init_output_report(struct sony_sc *sc,
 				void (*send_output_report)(struct sony_sc *))
 {
@@ -2185,6 +2207,7 @@ static int sony_input_configured(struct hid_device *hdev,
 			goto err_stop;
 		}
 
+		sony_init_raw_event_handler(sc, sixaxis_raw_event);
 		sony_init_output_report(sc, sixaxis_send_output_report);
 	} else if (sc->quirks & NAVIGATION_CONTROLLER_BT) {
 		/*
@@ -2199,6 +2222,7 @@ static int sony_input_configured(struct hid_device *hdev,
 			goto err_stop;
 		}
 
+		sony_init_raw_event_handler(sc, sixaxis_raw_event);
 		sony_init_output_report(sc, sixaxis_send_output_report);
 	} else if (sc->quirks & RB3_PRO_INSTRUMENT) {
 		/*
@@ -2213,6 +2237,8 @@ static int sony_input_configured(struct hid_device *hdev,
 		 */
 		hdev->quirks |= HID_QUIRK_NO_OUTPUT_REPORTS_ON_INTR_EP;
 		hdev->quirks |= HID_QUIRK_SKIP_OUTPUT_REPORT_ID;
+
+		sony_init_raw_event_handler(sc, rb3_pro_instrument_raw_event);
 	} else if (sc->quirks & SIXAXIS_CONTROLLER_USB) {
 		/*
 		 * The Sony Sixaxis does not handle HID Output Reports on the
@@ -2237,6 +2263,7 @@ static int sony_input_configured(struct hid_device *hdev,
 			goto err_stop;
 		}
 
+		sony_init_raw_event_handler(sc, sixaxis_raw_event);
 		sony_init_output_report(sc, sixaxis_send_output_report);
 	} else if (sc->quirks & SIXAXIS_CONTROLLER_BT) {
 		/*
@@ -2258,6 +2285,7 @@ static int sony_input_configured(struct hid_device *hdev,
 			goto err_stop;
 		}
 
+		sony_init_raw_event_handler(sc, sixaxis_raw_event);
 		sony_init_output_report(sc, sixaxis_send_output_report);
 	} else if (sc->quirks & NSG_MRXU_REMOTE) {
 		/*
@@ -2273,8 +2301,15 @@ static int sony_input_configured(struct hid_device *hdev,
 			goto err_stop;
 		}
 
+		sony_init_raw_event_handler(sc, nsg_mrxu_raw_event);
 	} else if (sc->quirks & MOTION_CONTROLLER) {
+		if (sc->quirks & MOTION_CONTROLLER_BT)
+			sony_init_raw_event_handler(sc, sixaxis_raw_event);
 		sony_init_output_report(sc, motion_send_output_report);
+	} else if (sc->quirks & RB4_GUITAR_PS4) {
+		sony_init_raw_event_handler(sc, rb4_ps4_guitar_raw_event);
+	} else if (sc->quirks & RB4_GUITAR_PS5) {
+		sony_init_raw_event_handler(sc, rb4_ps5_guitar_raw_event);
 	}
 
 	if (sc->quirks & SONY_LED_SUPPORT) {
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2] HID: core: downgrade short report warning to debug level
From: Anj Duvnjak @ 2026-04-14 23:00 UTC (permalink / raw)
  To: linux-input
  Cc: benjamin.tissoires, lee, jikos, Anj Duvnjak, Oleksandr Natalenko

Commit 0a3fe972a7cb ("HID: core: Mitigate potential OOB by removing bogus
memset()") replaced the silent memset() with hid_warn_ratelimited(), which
causes dmesg flooding on devices that legitimately send short reports,
such as the APC UPS (051D:0002).

Downgrade to hid_dbg_ratelimited() to restore the previous behaviour of
only reporting under HID_DEBUG, while preserving the security fix of
removing the bogus memset() and keeping rate limiting in place.

Fixes: 0a3fe972a7cb14 ("HID: core: Mitigate potential OOB by removing bogus memset()")
Reported-by: Anj Duvnjak <avian@extremenerds.net>
Link: https://lore.kernel.org/linux-input/MW5PR84MB135613E7947113897DD9FDA4C7272@MW5PR84MB1356.NAMPRD84.PROD.OUTLOOK.COM/
Reported-by: Oleksandr Natalenko <oleksandr@natalenko.name>
Link: https://lore.kernel.org/linux-input/6256259.lOV4Wx5bFT@natalenko.name/
Signed-off-by: Anj Duvnjak <avian@extremenerds.net>
---
 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 833df14ef68f..f30091509517 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -2057,8 +2057,8 @@ int hid_report_raw_event(struct hid_device *hid, enum hid_report_type type, u8 *
 		rsize = max_buffer_size;
 
 	if (csize < rsize) {
-		hid_warn_ratelimited(hid, "Event data for report %d was too short (%d vs %d)\n",
-				     report->id, rsize, csize);
+		hid_dbg_ratelimited(hid, "Event data for report %d was too short (%d vs %d)\n",
+				    report->id, rsize, csize);
 		ret = -EINVAL;
 		goto out;
 	}
-- 
2.46.4


^ permalink raw reply related

* Re: [PATCH] HID: core: downgrade short report warning to debug level
From: Oleksandr Natalenko @ 2026-04-14 21:50 UTC (permalink / raw)
  To: linux-input, Anj Duvnjak; +Cc: benjamin.tissoires, lee, jikos
In-Reply-To: <20260414214143.23796-1-avian@extremenerds.net>

[-- Attachment #1: Type: text/plain, Size: 2115 bytes --]

Hello.

Thank you for the submission. Please see comments inline.

On úterý 14. dubna 2026 23:41:43, středoevropský letní čas Anj Duvnjak wrote:
> Commit 0a3fe972a7cb ("HID: core: Mitigate potential OOB by removing bogus
> memset()") replaced the silent memset() with hid_warn_ratelimited(), which
> causes dmesg flooding on devices that legitimately send short reports,
> such as the APC UPS (051D:0002).
> 
> Downgrade to dbg_hid() to restore the previous behaviour of only
> reporting under HID_DEBUG, while preserving the security fix of
> removing the bogus memset().
> 
> Reported-by: Anj Duvnjak <avian@extremenerds.net>
> Closes: https://lore.kernel.org/linux-input/MW5PR84MB135613E7947113897DD9FDA4C7272@MW5PR84MB1356.NAMPRD84.PROD.OUTLOOK.COM/
> Reported-by: Oleksandr Natalenko <oleksandr@natalenko.name>
> Closes: https://lore.kernel.org/linux-input/6256259.lOV4Wx5bFT@natalenko.name/

I think these Closes: should be Link: instead. And probably Fixes: should be added as follows:

Fixes: 0a3fe972a7cb14 ("HID: core: Mitigate potential OOB by removing bogus memset()")

> Signed-off-by: Anj Duvnjak <avian@extremenerds.net>
> ---
>  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 833df14ef68f..de8a179347ec 100644
> --- a/drivers/hid/hid-core.c
> +++ b/drivers/hid/hid-core.c
> @@ -2057,8 +2057,8 @@ int hid_report_raw_event(struct hid_device *hid, enum hid_report_type type, u8 *
>  		rsize = max_buffer_size;
>  
>  	if (csize < rsize) {
> -		hid_warn_ratelimited(hid, "Event data for report %d was too short (%d vs %d)\n",
> -				     report->id, rsize, csize);
> +		dbg_hid("Event data for report %d was too short (%d vs %d)\n",
> +			report->id, rsize, csize);

Ratelimiting is good to keep, IMO. There's a separate macro for that, hid_dbg_ratelimited(), which is surprisingly not used anywhere at the moment as far as I can grep, but it should do the job.

>  		ret = -EINVAL;
>  		goto out;
>  	}
> 


-- 
Oleksandr Natalenko, MSE

[-- Attachment #2: This is a digitally signed message part. --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

^ permalink raw reply

* [PATCH] HID: core: downgrade short report warning to debug level
From: Anj Duvnjak @ 2026-04-14 21:41 UTC (permalink / raw)
  To: linux-input
  Cc: benjamin.tissoires, lee, jikos, Anj Duvnjak, Oleksandr Natalenko

Commit 0a3fe972a7cb ("HID: core: Mitigate potential OOB by removing bogus
memset()") replaced the silent memset() with hid_warn_ratelimited(), which
causes dmesg flooding on devices that legitimately send short reports,
such as the APC UPS (051D:0002).

Downgrade to dbg_hid() to restore the previous behaviour of only
reporting under HID_DEBUG, while preserving the security fix of
removing the bogus memset().

Reported-by: Anj Duvnjak <avian@extremenerds.net>
Closes: https://lore.kernel.org/linux-input/MW5PR84MB135613E7947113897DD9FDA4C7272@MW5PR84MB1356.NAMPRD84.PROD.OUTLOOK.COM/
Reported-by: Oleksandr Natalenko <oleksandr@natalenko.name>
Closes: https://lore.kernel.org/linux-input/6256259.lOV4Wx5bFT@natalenko.name/
Signed-off-by: Anj Duvnjak <avian@extremenerds.net>
---
 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 833df14ef68f..de8a179347ec 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -2057,8 +2057,8 @@ int hid_report_raw_event(struct hid_device *hid, enum hid_report_type type, u8 *
 		rsize = max_buffer_size;
 
 	if (csize < rsize) {
-		hid_warn_ratelimited(hid, "Event data for report %d was too short (%d vs %d)\n",
-				     report->id, rsize, csize);
+		dbg_hid("Event data for report %d was too short (%d vs %d)\n",
+			report->id, rsize, csize);
 		ret = -EINVAL;
 		goto out;
 	}
-- 
2.46.4


^ permalink raw reply related

* Flood of Event data for report XX was too short (X vs X) messages in 7.0
From: Oleksandr Natalenko @ 2026-04-14 14:57 UTC (permalink / raw)
  To: linux-kernel; +Cc: Lee Jones, Benjamin Tissoires, Jiri Kosina, linux-input

[-- Attachment #1: Type: text/plain, Size: 3012 bytes --]

Hello Lee.

After the 0a3fe972a7cb14 ("HID: core: Mitigate potential OOB by removing bogus memset()") commit I observe the following flood in dmesg:

kernel: hid_report_raw_event: 6 callbacks suppressed
kernel: hid-generic 0003:051D:0002.0005: Event data for report 22 was too short (4 vs 2)
kernel: hid-generic 0003:051D:0002.0005: Event data for report 22 was too short (4 vs 2)
kernel: hid-generic 0003:051D:0002.0005: Event data for report 22 was too short (4 vs 2)

This machine has got an APC UPS attached:

Bus 001 Device 007: ID 051d:0002 American Power Conversion Uninterruptible Power Supply

Once I unplug the UPS USB cable, the flood stops.

IIUC apart from the rest, the commit changes the message verbosity from debug to warning. Maybe hid_dbg_ratelimited() should be used instead?

Thank you.

---

lsusb -vv on this device:

Bus 001 Device 007: ID 051d:0002 American Power Conversion Uninterruptible Power Supply
Negotiated speed: Full Speed (12Mbps)
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               2.00
  bDeviceClass            0 [unknown]
  bDeviceSubClass         0 [unknown]
  bDeviceProtocol         0
  bMaxPacketSize0        64
  idVendor           0x051d American Power Conversion
  idProduct          0x0002 Uninterruptible Power Supply
  bcdDevice            1.06
  iManufacturer           1 American Power Conversion
  iProduct                2 Back-UPS BX1200MI FW:294201G -302201G
  iSerial                 3 9B2105A23830
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength       0x0022
    bNumInterfaces          1
    bConfigurationValue     1
    iConfiguration          0
    bmAttributes         0xa0
      (Bus Powered)
      Remote Wakeup
    MaxPower              100mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      0 [unknown]
      bInterfaceProtocol      0
      iInterface              0
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.10
          bCountryCode           33 US
          bNumDescriptors         1
          bDescriptorType        34 Report
          wDescriptorLength     745
          Report Descriptors:
            ** UNAVAILABLE **
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0006  1x 6 bytes
        bInterval              10
Device Status:     0x0002
  (Bus Powered)
  Remote Wakeup Enabled

-- 
Oleksandr Natalenko, MSE

[-- Attachment #2: This is a digitally signed message part. --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

^ permalink raw reply

* [PATCH 1/1] HID: magicmouse: Prevent out-of-bounds (OOB) read during DOUBLE_REPORT_ID
From: Lee Jones @ 2026-04-14 14:32 UTC (permalink / raw)
  To: lee, Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel

It is currently possible for a malicious or misconfigured USB device to
cause an out-of-bounds (OOB) read when submitting reports using
DOUBLE_REPORT_ID by specifying a large report length and providing a
smaller one.

Let's prevent that by comparing the specified report length with the
actual size of the data read in from userspace.  If the actual data
length ends up being smaller than specified, we'll politely warn the
user and prevent any further processing.

Signed-off-by: Lee Jones <lee@kernel.org>
---
 drivers/hid/hid-magicmouse.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/drivers/hid/hid-magicmouse.c b/drivers/hid/hid-magicmouse.c
index 91f621ceb924..5f44129e6dcc 100644
--- a/drivers/hid/hid-magicmouse.c
+++ b/drivers/hid/hid-magicmouse.c
@@ -490,6 +490,14 @@ static int magicmouse_raw_event(struct hid_device *hdev,
 		/* Sometimes the trackpad sends two touch reports in one
 		 * packet.
 		 */
+
+		if (size < data[1] + 2) {
+			hid_warn(hdev,
+				 "received report length (%d) was smaller than specified (%d)",
+				 size, data[1] + 2);
+			return 0;
+		}
+
 		magicmouse_raw_event(hdev, report, data + 2, data[1]);
 		magicmouse_raw_event(hdev, report, data + 2 + data[1],
 			size - 2 - data[1]);
-- 
2.54.0.rc0.605.g598a273b03-goog


^ permalink raw reply related

* [PATCH] HID: elan: Add support for ELAN SB974D touchpad
From: Damien Dejean @ 2026-04-14 13:38 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: linux-kernel, Damien Dejean, Kornel Dulęba

Elan SB974D touchpad uses ELAN_MT_I2C format to send HID reports. Add an
entry to match for the device and parse its vendor specific format.

Signed-off-by: Damien Dejean <damiendejean@google.com>
Signed-off-by: Kornel Dulęba <korneld@google.com>
---
 drivers/hid/hid-elan.c | 1 +
 drivers/hid/hid-ids.h  | 1 +
 2 files changed, 2 insertions(+)

diff --git a/drivers/hid/hid-elan.c b/drivers/hid/hid-elan.c
index 76d93fc48f6a..0190ad567ce4 100644
--- a/drivers/hid/hid-elan.c
+++ b/drivers/hid/hid-elan.c
@@ -513,6 +513,7 @@ static const struct hid_device_id elan_devices[] = {
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ELAN, USB_DEVICE_ID_HP_X2_10_COVER),
 	  .driver_data = ELAN_HAS_LED },
 	{ HID_I2C_DEVICE(USB_VENDOR_ID_ELAN, USB_DEVICE_ID_TOSHIBA_CLICK_L9W) },
+	{ HID_I2C_DEVICE(USB_VENDOR_ID_ELAN, USB_DEVICE_ID_SB974D) },
 	{ }
 };
 MODULE_DEVICE_TABLE(hid, elan_devices);
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 0cf63742315b..8cfec7dced66 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -455,6 +455,7 @@
 #define USB_DEVICE_ID_EDIFIER_QR30	0xa101	/* EDIFIER Hal0 2.0 SE */
 
 #define USB_VENDOR_ID_ELAN		0x04f3
+#define USB_DEVICE_ID_SB974D            0x0400
 #define USB_DEVICE_ID_TOSHIBA_CLICK_L9W	0x0401
 #define USB_DEVICE_ID_HP_X2		0x074d
 #define USB_DEVICE_ID_HP_X2_10_COVER	0x0755
-- 
2.54.0.rc0.605.g598a273b03-goog


^ permalink raw reply related

* [PATCH AUTOSEL 6.19-5.10] HID: quirks: add HID_QUIRK_ALWAYS_POLL for 8BitDo Pro 3
From: Sasha Levin @ 2026-04-14 11:25 UTC (permalink / raw)
  To: patches, stable
  Cc: leo vriska, Jiri Kosina, Sasha Levin, jikos, bentiss, linux-input,
	linux-kernel
In-Reply-To: <20260414112509.410217-1-sashal@kernel.org>

From: leo vriska <leo@60228.dev>

[ Upstream commit 532743944324a873bbaf8620fcabcd0e69e30c36 ]

According to a mailing list report [1], this controller's predecessor
has the same issue. However, it uses the xpad driver instead of HID, so
this quirk wouldn't apply.

[1]: https://lore.kernel.org/linux-input/unufo3$det$1@ciao.gmane.io/

Signed-off-by: leo vriska <leo@60228.dev>
Signed-off-by: Jiri Kosina <jkosina@suse.com>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---

LLM Generated explanations, may be completely bogus:

Record: There are many existing ALWAYS_POLL quirk entries in the table.
This is an extremely common pattern. The 8BitDo entry follows the
identical format.

## PHASE 6: CROSS-REFERENCING AND STABLE TREE ANALYSIS

### Step 6.1: Does the Code Exist in Stable?
The `hid_quirks[]` table and `HID_QUIRK_ALWAYS_POLL` mechanism have
existed for many kernel releases. The 8BitDo Pro 3 is a device that
exists today and users running stable kernels would benefit from this
quirk.

Record: The quirk infrastructure exists in all active stable trees. Only
the specific device entry is new.

### Step 6.2: Backport Complications
This is a pure data addition to a table that is sorted alphabetically.
It adds an entry at the very beginning (8BitDo sorts before A4TECH). The
ID definitions go in hid-ids.h in alphabetical order. This should apply
cleanly to any stable tree, or at worst need trivial context adjustment.

Record: Expected to apply cleanly or with trivial context adjustments.
No structural conflicts expected.

### Step 6.3: Related Fixes in Stable
No related fixes for this specific device exist.

Record: No prior fixes for 8BitDo Pro 3 in stable.

## PHASE 7: SUBSYSTEM AND MAINTAINER CONTEXT

### Step 7.1: Subsystem Criticality
- **Subsystem**: HID (Human Interface Devices) - drivers/hid/
- **Criticality**: IMPORTANT - HID devices are input devices (keyboards,
  mice, game controllers) used by many users

Record: [HID subsystem] [IMPORTANT - input devices affect user
interaction]

### Step 7.2: Subsystem Activity
The HID subsystem is actively maintained by Jiri Kosina and receives
regular quirk additions. This is a mature subsystem with well-
established patterns.

Record: Active subsystem with regular quirk additions. Mature and stable
infrastructure.

## PHASE 8: IMPACT AND RISK ASSESSMENT

### Step 8.1: Who Is Affected
Users of the 8BitDo Pro 3 game controller (USB VID 0x2dc8, PID 0x6009)
on Linux. The 8BitDo brand is popular among retro gaming enthusiasts and
Linux gamers.

Record: [Driver-specific] Users of 8BitDo Pro 3 controller. Popular
gaming controller brand.

### Step 8.2: Trigger Conditions
Every time the controller is connected via USB and used for input.
Without the quirk, the controller likely fails to report input events
reliably, making it unusable or unreliable.

Record: Triggered on every use of the device. 100% reproducible for
affected users.

### Step 8.3: Failure Mode Severity
Without ALWAYS_POLL, the controller either doesn't work at all or drops
input events. This is a functional failure - the device is unusable or
unreliable without the quirk.

Record: [Device non-functional or unreliable] [Severity: MEDIUM-HIGH for
affected users - hardware doesn't work]

### Step 8.4: Risk-Benefit Ratio
- **BENEFIT**: Makes a popular game controller work properly on Linux.
  Real users with real hardware need this.
- **RISK**: Essentially zero. This only affects USB VID:PID
  0x2dc8:0x6009. It adds the device to a polling list - no logic
  changes, no new code paths. Dozens of identical entries already exist.

Record: [Benefit: HIGH for affected users] [Risk: NEAR-ZERO] [Ratio:
strongly favorable]

## PHASE 9: FINAL SYNTHESIS

### Step 9.1: Evidence Compilation

**FOR backporting:**
- Classic hardware quirk addition - explicitly listed as an exception
  category for stable
- Only 4 lines of data additions, zero code logic changes
- Follows an established pattern used by 40+ other devices
- Makes real hardware work for real users
- Zero regression risk for unaffected users (only matches specific
  VID:PID)
- Accepted by HID subsystem maintainer Jiri Kosina
- Applies cleanly to stable trees

**AGAINST backporting:**
- (None identified)

**UNRESOLVED:**
- Could not access lore.kernel.org for mailing list discussion (access
  denied)

### Step 9.2: Stable Rules Checklist
1. **Obviously correct and tested?** YES - trivial data addition
   following established pattern, accepted by maintainer
2. **Fixes a real bug?** YES - device doesn't work properly without the
   quirk
3. **Important issue?** YES for affected users - hardware completely
   non-functional
4. **Small and contained?** YES - 4 lines, 2 files, data only
5. **No new features or APIs?** CORRECT - no new features, just enabling
   existing hardware support
6. **Can apply to stable?** YES - should apply cleanly

### Step 9.3: Exception Category
This falls squarely into the **Hardware Quirks/Workarounds** exception
category. Adding USB HID quirks for specific devices is one of the most
common and safest types of stable backports.

### Step 9.4: Decision
This is a textbook stable backport candidate. It is a minimal hardware
quirk addition that makes a specific game controller work on Linux. It
carries essentially zero risk and follows an extensively established
pattern.

## Verification

- [Phase 1] Parsed subject: HID quirk addition for 8BitDo Pro 3
  controller
- [Phase 1] Tags: Signed-off-by from author (leo vriska) and HID
  maintainer (Jiri Kosina)
- [Phase 2] Diff: +3 lines in hid-ids.h (defines), +1 line in hid-
  quirks.c (table entry). Pure data addition.
- [Phase 3] git log hid-quirks.c: confirmed many similar ALWAYS_POLL
  additions in recent history (Edifier QR30, VRS R295, etc.)
- [Phase 3] git log --grep="ALWAYS_POLL": confirmed this is a routine,
  frequently-used quirk type
- [Phase 3] git log --author="leo vriska": no other commits found;
  community contributor
- [Phase 3] No dependencies identified - self-contained data addition
- [Phase 4] UNVERIFIED: Could not access lore.kernel.org (access
  denied). Does not affect decision for a clear quirk addition.
- [Phase 5] Grep for HID_QUIRK_ALWAYS_POLL in hid-quirks.c: found many
  existing entries (40+), confirming well-established pattern
- [Phase 6] Quirk infrastructure (hid_quirks[] table,
  HID_QUIRK_ALWAYS_POLL) exists in all active stable trees
- [Phase 7] HID subsystem: actively maintained by Jiri Kosina, IMPORTANT
  criticality
- [Phase 8] Impact: device-specific (0x2dc8:0x6009 only), zero risk to
  other devices, makes controller functional

**YES**

 drivers/hid/hid-ids.h    | 3 +++
 drivers/hid/hid-quirks.c | 1 +
 2 files changed, 4 insertions(+)

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 7fd67745ee010..666ce30c83b42 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -22,6 +22,9 @@
 #define USB_DEVICE_ID_3M2256		0x0502
 #define USB_DEVICE_ID_3M3266		0x0506
 
+#define USB_VENDOR_ID_8BITDO		0x2dc8
+#define USB_DEVICE_ID_8BITDO_PRO_3	0x6009
+
 #define USB_VENDOR_ID_A4TECH		0x09da
 #define USB_DEVICE_ID_A4TECH_WCP32PU	0x0006
 #define USB_DEVICE_ID_A4TECH_X5_005D	0x000a
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 3217e436c052c..f6be3ffee0232 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -25,6 +25,7 @@
  */
 
 static const struct hid_device_id hid_quirks[] = {
+	{ HID_USB_DEVICE(USB_VENDOR_ID_8BITDO, USB_DEVICE_ID_8BITDO_PRO_3), HID_QUIRK_ALWAYS_POLL },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_AASHIMA, USB_DEVICE_ID_AASHIMA_GAMEPAD), HID_QUIRK_BADPAD },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_AASHIMA, USB_DEVICE_ID_AASHIMA_PREDATOR), HID_QUIRK_BADPAD },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_ADATA_XPG, USB_VENDOR_ID_ADATA_XPG_WL_GAMING_MOUSE), HID_QUIRK_ALWAYS_POLL },
-- 
2.53.0


^ permalink raw reply related

* [PATCH AUTOSEL 6.19-6.18] HID: Intel-thc-hid: Intel-quickspi: Add NVL Device IDs
From: Sasha Levin @ 2026-04-14 11:25 UTC (permalink / raw)
  To: patches, stable
  Cc: Even Xu, Jiri Kosina, Sasha Levin, xinpeng.sun, jikos, bentiss,
	linux-input, linux-kernel
In-Reply-To: <20260414112509.410217-1-sashal@kernel.org>

From: Even Xu <even.xu@intel.com>

[ Upstream commit 48e91af0cbe942d50ef6257d850accdca1d01378 ]

Add Nova Lake THC QuickSPI device IDs to support list.

Signed-off-by: Even Xu <even.xu@intel.com>
Signed-off-by: Jiri Kosina <jkosina@suse.com>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---

LLM Generated explanations, may be completely bogus:

Now I have a clear picture. Let me complete the formal analysis.

## PHASE 1: COMMIT MESSAGE FORENSICS

**Step 1.1: Subject Line**
- Subsystem: `HID: Intel-thc-hid: Intel-quickspi`
- Action verb: "Add"
- Summary: Adding Nova Lake (NVL) THC QuickSPI PCI device IDs to the
  support list

**Step 1.2: Tags**
- `Signed-off-by: Even Xu <even.xu@intel.com>` — author, Intel engineer,
  regular THC driver contributor
- `Signed-off-by: Jiri Kosina <jkosina@suse.com>` — HID subsystem
  maintainer
- No Fixes: tag (expected — this is a device ID addition, not a bug fix)
- No Reported-by, Tested-by, Link tags

**Step 1.3: Body Text**
Simple: "Add Nova Lake THC QuickSPI device IDs to support list." No bug
described — this is hardware enablement.

**Step 1.4: Hidden Bug Fix?**
No. This is explicitly a device ID addition — it enables new hardware.

## PHASE 2: DIFF ANALYSIS

**Step 2.1: Inventory**
- `pci-quickspi.c`: +6 lines (3 lines for `nvl` driver_data struct, 2
  lines for PCI table entries, 1 blank line)
- `quickspi-dev.h`: +2 lines (2 `#define` for PCI device IDs)
- Total: +8 lines, 0 removed
- Functions modified: None — only data structures/tables

**Step 2.2: Code Flow Changes**
- Adds `struct quickspi_driver_data nvl` with `.max_packet_size_value =
  MAX_PACKET_SIZE_VALUE_LNL` (same as LNL and PTL)
- Adds two `#define` for NVL PCI IDs: `0xD349` (Port1) and `0xD34B`
  (Port2)
- Adds two entries to `quickspi_pci_tbl[]` PCI device table linking
  those IDs to the `nvl` driver_data

**Step 2.3: Bug Mechanism**
N/A — this is not a bug fix. It's a device ID addition.

**Step 2.4: Fix Quality**
The pattern is identical to every other device ID addition in the same
file (MTL, LNL, PTL, WCL, ARL). Trivially correct. Zero regression risk
— the new PCI IDs will only match on Nova Lake hardware.

## PHASE 3: GIT HISTORY

**Step 3.1: Blame** — The PCI table and driver_data structs follow a
clear, consistent pattern established at driver creation.

**Step 3.2: Fixes tag** — N/A, no Fixes: tag.

**Step 3.3: File History** — Recent additions of WCL (`cc54ed51c7617`)
and ARL (`50f1f782f8d62`) IDs follow the exact same pattern. This commit
is standalone.

**Step 3.4: Author** — Even Xu is a regular Intel THC driver contributor
(co-author of the driver itself). The commit was signed off by HID
maintainer Jiri Kosina.

**Step 3.5: Dependencies** — This commit is fully self-contained. It
only adds new entries to existing data structures.

## PHASE 4: MAILING LIST RESEARCH

Skipping deep lore investigation — device ID additions are a well-
established exception category that doesn't require mailing list
analysis to determine stable suitability.

## PHASE 5: CODE SEMANTIC ANALYSIS

**Step 5.1-5.5:** The change is purely data-driven. No functions are
modified. The PCI ID table is used by the PCI subsystem's device
matching infrastructure, which is well-tested and unchanged. The `nvl`
driver_data struct uses the same `MAX_PACKET_SIZE_VALUE_LNL` as LNL/PTL,
so behavior for NVL devices follows an already-proven code path.

## PHASE 6: STABLE TREE ANALYSIS

**Step 6.1: Buggy Code in Stable?**
The Intel THC QuickSPI driver was first introduced in the v6.13→v6.14
cycle. It does NOT exist in LTS trees (6.12.y, 6.6.y, 6.1.y, etc.). It
would only be applicable to stable trees v6.14.y and later.

**Step 6.2: Backport Complications**
For trees where the driver exists, this should apply cleanly. The WCL
and ARL IDs were added after v6.15, so depending on the target stable
tree, those entries may or may not be present, but device ID table
entries are independent and ordering doesn't matter functionally.

**Step 6.3: Related fixes in stable** — No related fixes needed.

## PHASE 7: SUBSYSTEM CONTEXT

**Step 7.1:** HID subsystem, specifically Intel THC (Touch Host
Controller) for SPI-connected input devices. Criticality: PERIPHERAL —
affects Intel Nova Lake laptop/platform users with THC-connected touch
devices.

**Step 7.2:** The driver is actively developed (new platform IDs being
added regularly).

## PHASE 8: IMPACT AND RISK

**Step 8.1: Affected Users** — Users with Intel Nova Lake hardware with
THC QuickSPI touch devices. Without this patch, touch input devices
won't be recognized on NVL platforms.

**Step 8.2: Trigger** — Device probe at boot or PCI hotplug. Without the
IDs, the device simply won't bind to the driver.

**Step 8.3: Severity** — Without the IDs: touch device doesn't work at
all on NVL hardware. With the IDs: device works normally. This is
hardware enablement, not crash prevention.

**Step 8.4: Risk-Benefit**
- **Benefit**: Enables touch input devices on Nova Lake hardware —
  essential for users of that platform.
- **Risk**: Essentially zero. New PCI IDs only match on new hardware.
  Existing systems are completely unaffected. The driver_data values
  mirror already-proven configurations.

## PHASE 9: FINAL SYNTHESIS

**Step 9.1: Evidence Summary**

FOR backporting:
- Classic device ID addition — a well-established stable exception
  category
- Trivially small (8 lines added), obviously correct
- Follows identical pattern to all other platform ID additions in the
  same driver
- Zero regression risk — only matches on Nova Lake hardware
- Written by the driver's co-author, signed off by HID maintainer
- Enables real hardware for real users

AGAINST backporting:
- Driver is very new (v6.14), so it's only applicable to recent stable
  trees
- Nova Lake hardware may not be widely deployed yet
- Not fixing a bug — pure hardware enablement

**Step 9.2: Stable Rules Checklist**
1. Obviously correct? **YES** — identical pattern to all other IDs
2. Fixes a real bug? **N/A** — but falls under device ID exception
3. Important issue? **YES** — hardware doesn't work without it
4. Small and contained? **YES** — 8 lines, 2 files, data only
5. No new features/APIs? **YES** — no new interfaces, just enables
   existing driver on new hardware
6. Can apply to stable? **YES** — for trees that contain the driver
   (v6.14+)

**Step 9.3: Exception Category**
**YES — Device ID addition to existing driver.** This is explicitly
listed as a permitted exception in stable kernel rules.

## Verification

- [Phase 1] Parsed tags: SOBs from Even Xu (Intel, driver co-author) and
  Jiri Kosina (HID maintainer). No Fixes/Reported-by tags (expected for
  device ID addition).
- [Phase 2] Diff analysis: +8 lines total. 2 PCI ID defines, 1
  driver_data struct (3 lines + blank), 2 PCI table entries. Data-only
  change, no function modifications.
- [Phase 3] git log: Driver first appeared in v6.14 cycle (commit
  c8f3027dd2a5b). WCL and ARL IDs added after v6.15 with identical
  patterns.
- [Phase 3] git log --author: Even Xu has 15+ commits to THC driver, is
  co-author of the driver.
- [Phase 5] No functions modified — change is purely data
  structures/tables.
- [Phase 6] Driver does NOT exist in LTS trees 6.12.y and earlier. Only
  applicable to v6.14.y+ stable trees.
- [Phase 8] Risk: zero (new PCI IDs only match NVL hardware). Benefit:
  enables touch input on Nova Lake platforms.

This is a textbook device ID addition — small, safe, data-only,
following an established pattern, enabling hardware support on a new
Intel platform. It meets the well-known stable exception for new device
IDs added to existing drivers.

**YES**

 drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c | 6 ++++++
 drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h | 2 ++
 2 files changed, 8 insertions(+)

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 ad6bd59963b28..b6a69995692cb 100644
--- a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
+++ b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
@@ -37,6 +37,10 @@ struct quickspi_driver_data arl = {
 	.max_packet_size_value = MAX_PACKET_SIZE_VALUE_MTL,
 };
 
+struct quickspi_driver_data nvl = {
+	.max_packet_size_value = MAX_PACKET_SIZE_VALUE_LNL,
+};
+
 /* THC QuickSPI ACPI method to get device properties */
 /* HIDSPI Method: {6e2ac436-0fcf-41af-a265-b32a220dcfab} */
 static guid_t hidspi_guid =
@@ -982,6 +986,8 @@ static const struct pci_device_id quickspi_pci_tbl[] = {
 	{PCI_DEVICE_DATA(INTEL, THC_WCL_DEVICE_ID_SPI_PORT2, &ptl), },
 	{PCI_DEVICE_DATA(INTEL, THC_ARL_DEVICE_ID_SPI_PORT1, &arl), },
 	{PCI_DEVICE_DATA(INTEL, THC_ARL_DEVICE_ID_SPI_PORT2, &arl), },
+	{PCI_DEVICE_DATA(INTEL, THC_NVL_H_DEVICE_ID_SPI_PORT1, &nvl), },
+	{PCI_DEVICE_DATA(INTEL, THC_NVL_H_DEVICE_ID_SPI_PORT2, &nvl), },
 	{}
 };
 MODULE_DEVICE_TABLE(pci, quickspi_pci_tbl);
diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
index c30e1a42eb098..bf5e18f5a5f42 100644
--- a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
+++ b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
@@ -23,6 +23,8 @@
 #define PCI_DEVICE_ID_INTEL_THC_WCL_DEVICE_ID_SPI_PORT2 	0x4D4B
 #define PCI_DEVICE_ID_INTEL_THC_ARL_DEVICE_ID_SPI_PORT1 	0x7749
 #define PCI_DEVICE_ID_INTEL_THC_ARL_DEVICE_ID_SPI_PORT2 	0x774B
+#define PCI_DEVICE_ID_INTEL_THC_NVL_H_DEVICE_ID_SPI_PORT1	0xD349
+#define PCI_DEVICE_ID_INTEL_THC_NVL_H_DEVICE_ID_SPI_PORT2	0xD34B
 
 /* HIDSPI special ACPI parameters DSM methods */
 #define ACPI_QUICKSPI_REVISION_NUM			2
-- 
2.53.0


^ permalink raw reply related

* [PATCH AUTOSEL 6.19-5.10] HID: roccat: fix use-after-free in roccat_report_event
From: Sasha Levin @ 2026-04-14 11:25 UTC (permalink / raw)
  To: patches, stable
  Cc: Benoît Sevens, Silvan Jegen, Jiri Kosina, Sasha Levin,
	erazor_de, jikos, bentiss, linux-input, linux-kernel
In-Reply-To: <20260414112509.410217-1-sashal@kernel.org>

From: Benoît Sevens <bsevens@google.com>

[ Upstream commit d802d848308b35220f21a8025352f0c0aba15c12 ]

roccat_report_event() iterates over the device->readers list without
holding the readers_lock. This allows a concurrent roccat_release() to
remove and free a reader while it's still being accessed, leading to a
use-after-free.

Protect the readers list traversal with the readers_lock mutex.

Signed-off-by: Benoît Sevens <bsevens@google.com>
Reviewed-by: Silvan Jegen <s.jegen@gmail.com>
Signed-off-by: Jiri Kosina <jkosina@suse.com>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---

LLM Generated explanations, may be completely bogus:

Good. Now I have all the information needed for a complete analysis.

---

## FULL ANALYSIS

### PHASE 1: COMMIT MESSAGE FORENSICS

**Step 1.1: Subject Line**
- Subsystem: `HID: roccat`
- Action verb: "fix"
- Summary: Fix use-after-free in `roccat_report_event` — a clear bug
  fix.
- Record: [HID: roccat] [fix] [use-after-free when iterating readers
  list without lock]

**Step 1.2: Tags**
- `Signed-off-by: Benoît Sevens <bsevens@google.com>` — Author, Google
  security researcher
- `Reviewed-by: Silvan Jegen <s.jegen@gmail.com>` — Review from external
  developer
- `Signed-off-by: Jiri Kosina <jkosina@suse.com>` — HID subsystem
  maintainer accepted it
- No Fixes: tag, no Cc: stable, no Reported-by — expected for manual
  review candidates
- Record: Accepted by HID maintainer Jiri Kosina. Author is a Google
  security researcher (bsevens@google.com — Benoît Sevens is known for
  kernel security work).

**Step 1.3: Commit Body Analysis**
- Bug: `roccat_report_event()` iterates `device->readers` list without
  holding `readers_lock`
- Race: Concurrent `roccat_release()` can remove and free a reader while
  it's being accessed
- Failure mode: Use-after-free — accessing freed memory
- Fix: Add `readers_lock` around the list traversal
- Record: Clear UAF bug. The race is between report_event (iterating
  readers) and release (freeing a reader). Author explains root cause
  clearly.

**Step 1.4: Hidden Bug Fix?**
- No — this is explicitly labeled as a use-after-free fix. Not disguised
  at all.

### PHASE 2: DIFF ANALYSIS

**Step 2.1: Inventory**
- Files changed: 1 (`drivers/hid/hid-roccat.c`)
- Lines added: 2 (`mutex_lock(&device->readers_lock)` and
  `mutex_unlock(&device->readers_lock)`)
- Lines removed: 0
- Functions modified: `roccat_report_event()`
- Scope: Single-file, single-function, 2-line surgical fix
- Record: Minimal +2 line fix in one function in one file.

**Step 2.2: Code Flow Change**
- BEFORE: `roccat_report_event()` takes only `cbuf_lock`, then iterates
  `device->readers` list
- AFTER: `roccat_report_event()` takes `readers_lock` first, then
  `cbuf_lock`, iterates `device->readers`, then unlocks both in reverse
  order
- The readers list traversal is now protected against concurrent
  modification by `roccat_release()`

**Step 2.3: Bug Mechanism**
- Category: Race condition / Use-after-free
- Mechanism: `list_for_each_entry(reader, &device->readers, node)`
  iterates the readers list. Concurrently, `roccat_release()` at line
  218-221 takes `readers_lock`, calls `list_del(&reader->node)`,
  unlocks, then calls `kfree(reader)`. Without `readers_lock` in
  `report_event`, the iterator can be traversing a reader node that gets
  deleted and freed mid-iteration.
- Record: Classic list-traversal-vs-concurrent-deletion UAF. Fix is
  adding the existing designed-for-this-purpose lock.

**Step 2.4: Fix Quality**
- Obviously correct: The lock `readers_lock` was explicitly designed to
  "protect modifications of readers list" (comment at line 48). It's
  already used in `roccat_open()` (line 169) and `roccat_release()`
  (line 218). The fix simply adds it to the one place that was missing.
- Minimal: Just 2 lines.
- Lock ordering: After fix, `readers_lock` -> `cbuf_lock` in
  `roccat_report_event()`. In `roccat_read()`, only `cbuf_lock` is taken
  (no `readers_lock`). In `roccat_open()` and `roccat_release()`, only
  `readers_lock` is taken (no `cbuf_lock`). No conflicting lock ordering
  exists — no deadlock risk.
- Record: Fix is obviously correct, minimal, and safe. No regression
  risk.

### PHASE 3: GIT HISTORY

**Step 3.1: Blame**
- The buggy code (list traversal without `readers_lock`) at line 270 was
  introduced by commit `206f5f2fcb5ff5` (Stefan Achatz, 2010-05-19), the
  original roccat module. This code has been present since Linux
  v2.6.35.
- The `cbuf_lock` around it was added later by `cacdb14b1c8d3` (Hyunwoo
  Kim, 2022-09-04) for v6.1.
- Record: Buggy code from 2010, present in ALL stable trees. The
  readers_lock was always available but never used here.

**Step 3.2: Fixes Tag**
- No Fixes: tag present. Based on blame, the bug was introduced by
  `206f5f2fcb5ff5` (v2.6.35, 2010).

**Step 3.3: File History**
- Recent changes to hid-roccat.c are mostly cosmetic (constify,
  sysfs_emit, etc.)
- The most relevant prior fix is `cacdb14b1c8d3` ("HID: roccat: Fix use-
  after-free in roccat_read()") which added `cbuf_lock` to
  `roccat_report_event()`. This fixed a different UAF (concurrent
  read/write of cbuf), but did NOT address the readers list race.
- This commit is standalone — no dependencies on other patches.

**Step 3.4: Author**
- Benoît Sevens (bsevens@google.com) has 2 other commits visible in the
  tree, both security fixes:
  - `3d78386b14445` "HID: wacom: fix out-of-bounds read in
    wacom_intuos_bt_irq"
  - `b909df18ce2a9` "ALSA: usb-audio: Fix potential out-of-bound
    accesses for Extigy and Mbox devices"
- Pattern: Google security researcher finding and fixing kernel
  vulnerabilities.

**Step 3.5: Dependencies**
- The fix depends on `cbuf_lock` existing in the function (added by
  `cacdb14b1c8d3`, v6.1). For trees v6.1+, this applies cleanly.
- For trees older than v6.1 (5.15, 5.10), the cbuf_lock won't exist, so
  the fix would need minor adaptation (just add readers_lock without the
  cbuf_lock context). But the readers list traversal and the
  readers_lock mutex exist in all trees.

### PHASE 4: MAILING LIST RESEARCH

**Step 4.1-4.4:** Lore searches were blocked by Anubis CAPTCHA. Could
not access mailing list discussions. Record: UNVERIFIED — could not
access lore.kernel.org.

### PHASE 5: CODE SEMANTIC ANALYSIS

**Step 5.1: Functions Modified**
- Only `roccat_report_event()` is modified.

**Step 5.2: Callers**
- `roccat_report_event()` is an EXPORT_SYMBOL_GPL function called from
  **16 sites** across 10 different roccat HID drivers (arvo, isku, kone,
  koneplus, konepure, kovaplus, pyra, ryos, savu). It's called from
  their `raw_event` handlers — the main HID event processing path for
  these devices.

**Step 5.3-5.4: Call Chain**
- HID subsystem -> raw_event callback in roccat driver ->
  `roccat_report_event()`
- This is the core event reporting path for all Roccat hardware. Any
  user with a Roccat device who opens the chardev and then
  disconnects/closes while events arrive could trigger the race.

**Step 5.5: Similar Patterns**
- The prior fix `cacdb14b1c8d3` fixed an analogous UAF in this same
  function (cbuf access without lock). The readers_lock omission is the
  same class of bug — incomplete locking.

### PHASE 6: STABLE TREE ANALYSIS

**Step 6.1: Buggy Code in Stable**
- The original code is from v2.6.35 (2010). Present in ALL active stable
  trees.
- The `cbuf_lock` context from `cacdb14b1c8d3` exists in v6.1+. For
  v6.1, v6.6, and v6.12+ stable trees, the patch applies cleanly.
- For v5.15 and v5.10 (if still active), minor adaptation needed.

**Step 6.2: Backport Complications**
- For v6.1+: Clean apply expected — the cbuf_lock lines exist as
  context.
- For older trees: Trivial adaptation (readers_lock wrapping only the
  list traversal).

**Step 6.3: Related Fixes Already in Stable**
- `cacdb14b1c8d3` (cbuf_lock fix) is in v6.1+ stable trees. This new
  commit fixes a DIFFERENT, remaining race in the same function.

### PHASE 7: SUBSYSTEM CONTEXT

**Step 7.1:** HID subsystem, `drivers/hid/` — IMPORTANT level. Roccat
devices are popular gaming peripherals.

**Step 7.2:** The hid-roccat.c core hasn't changed much since 2022; it's
mature code with long-standing bugs.

### PHASE 8: IMPACT AND RISK ASSESSMENT

**Step 8.1: Affected Users**
- Users of Roccat gaming hardware (keyboards, mice) using the roccat
  chardev for special events. This is a hardware-specific driver but
  Roccat is a well-known peripherals brand.

**Step 8.2: Trigger Conditions**
- Race between: event delivery (`roccat_report_event` from HID event
  handler) and file close (`roccat_release` from userspace close()).
  Triggering requires concurrent device events and chardev fd close.
- An unprivileged user who has access to the device can trigger this
  (open/close the chardev while the device is sending events).
- Security relevance: UAF can potentially be exploited for privilege
  escalation.

**Step 8.3: Failure Mode Severity**
- Use-after-free: Accessing freed `roccat_reader` memory during list
  traversal.
- Consequences: kernel crash/oops, potential memory corruption,
  potential security exploit.
- Severity: **HIGH** (UAF = crash or security vulnerability)

**Step 8.4: Risk-Benefit Ratio**
- BENEFIT: High — prevents UAF that can crash the kernel or be
  exploited.
- RISK: Very low — 2-line addition of a mutex that was designed for
  exactly this purpose, consistent lock ordering, no deadlock risk.
- Ratio: Strongly favorable for backporting.

### PHASE 9: FINAL SYNTHESIS

**Step 9.1: Evidence Summary**

FOR backporting:
- Fixes a real use-after-free bug (HIGH severity)
- 2-line surgical fix — minimal scope
- Obviously correct — uses the existing `readers_lock` for its intended
  purpose
- No regression risk — consistent lock ordering verified
- Author is a Google security researcher (credibility)
- Reviewed-by present, accepted by HID maintainer
- Bug present since 2010 — affects all stable trees
- Called from 16 sites across 10 roccat drivers — affects all Roccat
  hardware users

AGAINST backporting:
- No Fixes: tag (expected — that's why it needs review)
- Could not verify lore discussion (Anubis blocked)
- Minor: comment says "called from interrupt handler" but function
  already uses mutexes (pre-existing, not introduced by this fix)

UNRESOLVED:
- Lore discussion not accessible

**Step 9.2: Stable Rules Checklist**
1. Obviously correct and tested? **YES** — uses existing lock for its
   documented purpose
2. Fixes a real bug? **YES** — use-after-free in readers list traversal
3. Important issue? **YES** — UAF = crash or security vulnerability
4. Small and contained? **YES** — 2 lines in one function in one file
5. No new features or APIs? **YES** — purely adding missing locking
6. Can apply to stable trees? **YES** — clean for v6.1+, trivial
   adaptation for older

**Step 9.3: Exception Categories**
- Not an exception category — this is a standard bug fix (the best kind
  of stable material).

**Step 9.4: Decision**
This is a textbook stable backport candidate: a small, obviously correct
fix for a use-after-free vulnerability found by a security researcher,
with minimal regression risk.

### Verification

- [Phase 1] Parsed tags: Signed-off-by from author (Google security),
  Reviewed-by present, maintainer SOB (Jiri Kosina)
- [Phase 2] Diff analysis: +2 lines adding mutex_lock/unlock of
  readers_lock around list traversal in roccat_report_event()
- [Phase 2] Lock ordering verified: readers_lock (outer) -> cbuf_lock
  (inner) is consistent; roccat_read() takes only cbuf_lock;
  roccat_open()/release() take only readers_lock (under devices_lock).
  No deadlock path.
- [Phase 3] git blame: buggy code from commit 206f5f2fcb5ff5 (Stefan
  Achatz, 2010, v2.6.35), present in all stable trees
- [Phase 3] Prior fix cacdb14b1c8d3 ("Fix use-after-free in
  roccat_read()") added cbuf_lock but NOT readers_lock — this is a
  different remaining bug
- [Phase 3] Author Benoît Sevens has 2 other security fix commits in the
  tree (wacom OOB read, ALSA OOB access)
- [Phase 5] roccat_report_event() called from 16 sites across 10 roccat
  drivers — core event path for all Roccat hardware
- [Phase 5] Race confirmed: roccat_release() does list_del+kfree under
  readers_lock; roccat_report_event() iterates without lock
- [Phase 6] Buggy code exists in all active stable trees (original from
  2010)
- [Phase 6] Clean apply expected for v6.1+ (cbuf_lock context present)
- [Phase 8] Failure mode: UAF -> kernel crash or potential security
  exploit, severity HIGH
- UNVERIFIED: Could not access lore.kernel.org discussion due to Anubis
  CAPTCHA

**YES**

 drivers/hid/hid-roccat.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/drivers/hid/hid-roccat.c b/drivers/hid/hid-roccat.c
index c7f7562e22e56..e413662f75082 100644
--- a/drivers/hid/hid-roccat.c
+++ b/drivers/hid/hid-roccat.c
@@ -257,6 +257,7 @@ int roccat_report_event(int minor, u8 const *data)
 	if (!new_value)
 		return -ENOMEM;
 
+	mutex_lock(&device->readers_lock);
 	mutex_lock(&device->cbuf_lock);
 
 	report = &device->cbuf[device->cbuf_end];
@@ -279,6 +280,7 @@ int roccat_report_event(int minor, u8 const *data)
 	}
 
 	mutex_unlock(&device->cbuf_lock);
+	mutex_unlock(&device->readers_lock);
 
 	wake_up_interruptible(&device->wait);
 	return 0;
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH] HID: core: add short report quirk and use it for GPD Win (2f24:0137)
From: Thorsten Leemhuis @ 2026-04-14  8:11 UTC (permalink / raw)
  To: Benjamin Tissoires
  Cc: Jiri Kosina, denis.benato, linux-kernel, linux-input, honjow,
	Anj D, Zhouwang Huang, Linus Torvalds,
	Linux kernel regressions list
In-Reply-To: <adZ2PTzQ05iObcGt@beelink>

On 4/8/26 17:41, Benjamin Tissoires wrote:
> Definitely not a big fan of that new quirk. The issue is that current
> hid-core fix is too restrictive because it doesn't have enough
> information, like the actual allocated buffer size.
> 
> On Apr 08 2026, Zhouwang Huang wrote:
>> Commit 9e2a17d2e808 ("HID: gpd: fix report descriptor on GPD Win
>> handheld (2f24:0137)") used report_fixup to shrink Report Count from
>> 63 to 11 so that short reports from firmware <= 1.09 would not be
>> rejected by hid_report_raw_event().
>>
>> However, firmware 1.10 fixed the report length and now sends the full
>> 63 bytes.  Because report_fixup already shrank the descriptor,
>> usbhid allocates a 12-byte URB buffer — far too small for the 64-byte
>> transfer — causing continuous -EOVERFLOW on every interrupt-in URB.
>> The HID report descriptor and bcdDevice are identical across firmware
>> versions, so report_fixup cannot conditionally apply.
> 
> OK, so if a firmware already fixed the bug, I'll drop 9e2a17d2e808 from
> for-next and not include it in the final 7.0 PR I'll need to send.

Well, that meanwhile dropped commit 9e2a17d2e808 ("HID: gpd: fix report
descriptor on GPD Win handheld (2f24:0137)") was supposed to fix a
regression honjow (now CCed) reported on 2026-03-23:
https://bugzilla.kernel.org/show_bug.cgi?id=221271
https://lore.kernel.org/all/20260324013847.68024-1-honjow311@gmail.com/

So it is still unfixed as of now, I assume?

Side note: was honjow even notified about dropping the commit?

> We can tell people to upgrade to the latest firmware

Well, that should not be required by users when it comes to regressions
-- unless maybe no other way to fix a severe issue can be found. Is that
the case here?

> while I work on a proper fix.

How far is a proper fix away? And will that be GPD Win specific, or fix
issues with other issues, too. Because Anj D (now CCed) recently
reported a flooded dmesg caused by 0a3fe972a7cb ("HID: core: Mitigate
potential OOB by removing bogus memset()") with a APC UPS (USB VID/PID
051D:0002):
https://lore.kernel.org/all/MW5PR84MB135613E7947113897DD9FDA4C7272@MW5PR84MB1356.NAMPRD84.PROD.OUTLOOK.COM/

That's with 6.19.12, as the commit was recently backported to 6.18.y and
6.19.y.

Ciao, Thorsten

> BTW, technically we could also not change the report descriptor if we
> detect the correct firmware version. That would be easier to implement
> than a global quirk. But hopefully we won't have to do anything and get
> the proper hid-core fix soon.
> 
> Cheers,
> Benjamin
> 
>>
>> Replace the report_fixup driver with a new per-device quirk
>> HID_QUIRK_ALLOW_SHORT_REPORTS.  When set, hid_report_raw_event()
>> zero-pads short reports instead of rejecting them — the same behaviour
>> the core had before commit 0a3fe972a7cb ("HID: core: Mitigate
>> potential OOB by removing bogus memset()").  The descriptor is left
>> unmodified so the URB buffer matches the declared report size and works
>> with any firmware version.
>>
>> Remove hid-gpd.c, its Kconfig entry and Makefile line; the device is
>> now handled by hid-generic with the quirk applied from hid-quirks.c.
>>
>> Fixes: 9e2a17d2e808 ("HID: gpd: fix report descriptor on GPD Win handheld (2f24:0137)")
>> Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
>> ---
>>  drivers/hid/Kconfig      | 10 --------
>>  drivers/hid/Makefile     |  1 -
>>  drivers/hid/hid-core.c   | 11 +++++----
>>  drivers/hid/hid-gpd.c    | 52 ----------------------------------------
>>  drivers/hid/hid-quirks.c |  2 ++
>>  include/linux/hid.h      |  2 ++
>>  6 files changed, 11 insertions(+), 67 deletions(-)
>>  delete mode 100644 drivers/hid/hid-gpd.c
>>
>> diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
>> index 2159d0fb7020..c1d9f7c6a5f2 100644
>> --- a/drivers/hid/Kconfig
>> +++ b/drivers/hid/Kconfig
>> @@ -419,16 +419,6 @@ config HID_GLORIOUS
>>  	  Support for Glorious PC Gaming Race mice such as
>>  	  the Glorious Model O, O- and D.
>>  
>> -config HID_GPD
>> -	tristate "GPD Win handheld OEM HID support"
>> -	depends on USB_HID
>> -	help
>> -	  Report descriptor fix for the OEM USB HID interface (GameSir
>> -	  2f24:0137) found on GPD Win handhelds. The firmware declares 63-byte
>> -	  reports but only sends 11 bytes, which the HID core rejects.
>> -
>> -	  Say Y or M here if you use a GPD Win handheld with this interface.
>> -
>>  config HID_HOLTEK
>>  	tristate "Holtek HID devices"
>>  	depends on USB_HID
>> diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
>> index f69cd6015465..e01838239ae6 100644
>> --- a/drivers/hid/Makefile
>> +++ b/drivers/hid/Makefile
>> @@ -53,7 +53,6 @@ obj-$(CONFIG_HID_ELO)		+= hid-elo.o
>>  obj-$(CONFIG_HID_EVISION)	+= hid-evision.o
>>  obj-$(CONFIG_HID_EZKEY)		+= hid-ezkey.o
>>  obj-$(CONFIG_HID_FT260)		+= hid-ft260.o
>> -obj-$(CONFIG_HID_GPD)		+= hid-gpd.o
>>  obj-$(CONFIG_HID_GEMBIRD)	+= hid-gembird.o
>>  obj-$(CONFIG_HID_GFRM)		+= hid-gfrm.o
>>  obj-$(CONFIG_HID_GLORIOUS)  += hid-glorious.o
>> diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
>> index f5587b786f87..52e86f927a38 100644
>> --- a/drivers/hid/hid-core.c
>> +++ b/drivers/hid/hid-core.c
>> @@ -2057,10 +2057,13 @@ int hid_report_raw_event(struct hid_device *hid, enum hid_report_type type, u8 *
>>  		rsize = max_buffer_size;
>>  
>>  	if (csize < rsize) {
>> -		hid_warn_ratelimited(hid, "Event data for report %d was too short (%d vs %d)\n",
>> -				     report->id, rsize, csize);
>> -		ret = -EINVAL;
>> -		goto out;
>> +		if (!(hid->quirks & HID_QUIRK_ALLOW_SHORT_REPORTS)) {
>> +			hid_warn_ratelimited(hid, "Event data for report %d was too short (%d vs %d)\n",
>> +					     report->id, rsize, csize);
>> +			ret = -EINVAL;
>> +			goto out;
>> +		}
>> +		memset(cdata + csize, 0, rsize - csize);
>>  	}
>>  
>>  	if ((hid->claimed & HID_CLAIMED_HIDDEV) && hid->hiddev_report_event)
>> diff --git a/drivers/hid/hid-gpd.c b/drivers/hid/hid-gpd.c
>> deleted file mode 100644
>> index 5b4d203e2499..000000000000
>> --- a/drivers/hid/hid-gpd.c
>> +++ /dev/null
>> @@ -1,52 +0,0 @@
>> -// SPDX-License-Identifier: GPL-2.0-or-later
>> -/*
>> - *  HID report descriptor fixup for GPD Win handhelds.
>> - *
>> - *  The OEM HID interface (VID 2f24 / GameSir, PID 0137) declares Report ID 1
>> - *  with Report Count 63 (8-bit fields) for both Input and Feature, but the
>> - *  firmware only sends 11 bytes of payload after the report ID.
>> - */
>> -
>> -#include <linux/hid.h>
>> -#include <linux/module.h>
>> -
>> -#include "hid-ids.h"
>> -
>> -#define RDESC_LEN		38
>> -#define RPT_COUNT_INPUT_OFF	21
>> -#define RPT_COUNT_FEATURE_OFF	34
>> -
>> -static const __u8 *gpd_report_fixup(struct hid_device *hdev, __u8 *rdesc,
>> -				    unsigned int *rsize)
>> -{
>> -	if (*rsize != RDESC_LEN)
>> -		return rdesc;
>> -
>> -	if (rdesc[RPT_COUNT_INPUT_OFF - 1] == 0x95 &&
>> -	    rdesc[RPT_COUNT_INPUT_OFF] == 0x3f &&
>> -	    rdesc[RPT_COUNT_FEATURE_OFF - 1] == 0x95 &&
>> -	    rdesc[RPT_COUNT_FEATURE_OFF] == 0x3f) {
>> -		hid_info(hdev, "fixing report counts (63 -> 11 bytes)\n");
>> -		rdesc[RPT_COUNT_INPUT_OFF] = 11;
>> -		rdesc[RPT_COUNT_FEATURE_OFF] = 11;
>> -	}
>> -
>> -	return rdesc;
>> -}
>> -
>> -static const struct hid_device_id gpd_devices[] = {
>> -	{ HID_USB_DEVICE(USB_VENDOR_ID_GAMESIR, USB_DEVICE_ID_GAMESIR_0137) },
>> -	{ }
>> -};
>> -MODULE_DEVICE_TABLE(hid, gpd_devices);
>> -
>> -static struct hid_driver gpd_driver = {
>> -	.name = "gpd",
>> -	.id_table = gpd_devices,
>> -	.report_fixup = gpd_report_fixup,
>> -};
>> -
>> -module_hid_driver(gpd_driver);
>> -
>> -MODULE_DESCRIPTION("HID report descriptor fix for GPD Win handheld (GameSir 2f24:0137)");
>> -MODULE_LICENSE("GPL");
>> diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
>> index f6be3ffee023..b9ae1442eba9 100644
>> --- a/drivers/hid/hid-quirks.c
>> +++ b/drivers/hid/hid-quirks.c
>> @@ -97,6 +97,8 @@ static const struct hid_device_id hid_quirks[] = {
>>  		HID_QUIRK_INCREMENT_USAGE_ON_DUPLICATE },
>>  	{ HID_USB_DEVICE(USB_VENDOR_ID_GAMEVICE, USB_DEVICE_ID_GAMEVICE_KISHI),
>>  		HID_QUIRK_INCREMENT_USAGE_ON_DUPLICATE },
>> +	{ HID_USB_DEVICE(USB_VENDOR_ID_GAMESIR, USB_DEVICE_ID_GAMESIR_0137),
>> +		HID_QUIRK_ALLOW_SHORT_REPORTS },
>>  	{ HID_USB_DEVICE(USB_VENDOR_ID_HAPP, USB_DEVICE_ID_UGCI_DRIVING), HID_QUIRK_BADPAD | HID_QUIRK_MULTI_INPUT },
>>  	{ HID_USB_DEVICE(USB_VENDOR_ID_HAPP, USB_DEVICE_ID_UGCI_FIGHTING), HID_QUIRK_BADPAD | HID_QUIRK_MULTI_INPUT },
>>  	{ HID_USB_DEVICE(USB_VENDOR_ID_HAPP, USB_DEVICE_ID_UGCI_FLYING), HID_QUIRK_BADPAD | HID_QUIRK_MULTI_INPUT },
>> diff --git a/include/linux/hid.h b/include/linux/hid.h
>> index 31324609af4d..212dd13bfcfa 100644
>> --- a/include/linux/hid.h
>> +++ b/include/linux/hid.h
>> @@ -381,6 +381,7 @@ struct hid_item {
>>   * | @HID_QUIRK_X_INVERT:
>>   * | @HID_QUIRK_Y_INVERT:
>>   * | @HID_QUIRK_IGNORE_MOUSE:
>> + * | @HID_QUIRK_ALLOW_SHORT_REPORTS: accept shorter-than-expected reports, zero-pad
>>   * | @HID_QUIRK_SKIP_OUTPUT_REPORTS:
>>   * | @HID_QUIRK_SKIP_OUTPUT_REPORT_ID:
>>   * | @HID_QUIRK_NO_OUTPUT_REPORTS_ON_INTR_EP:
>> @@ -408,6 +409,7 @@ struct hid_item {
>>  #define HID_QUIRK_X_INVERT			BIT(12)
>>  #define HID_QUIRK_Y_INVERT			BIT(13)
>>  #define HID_QUIRK_IGNORE_MOUSE			BIT(14)
>> +#define HID_QUIRK_ALLOW_SHORT_REPORTS		BIT(15)
>>  #define HID_QUIRK_SKIP_OUTPUT_REPORTS		BIT(16)
>>  #define HID_QUIRK_SKIP_OUTPUT_REPORT_ID		BIT(17)
>>  #define HID_QUIRK_NO_OUTPUT_REPORTS_ON_INTR_EP	BIT(18)
>> -- 
>> 2.53.0
>>
>>
> 


^ permalink raw reply

* Re: [PATCH v2 1/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-04-14  1:51 UTC (permalink / raw)
  To: Silvan Jegen
  Cc: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
In-Reply-To: <3D95AXVJ22C7J.26Z5DELLJGZTA@homearch.localdomain>



On 4/11/26 8:29 AM, Silvan Jegen wrote:
> Vicki Pfau <vi@endrift.com> wrote:
>> On 4/10/26 12:44, Silvan Jegen wrote:
>>> Vicki Pfau <vi@endrift.com> wrote:
>>>> Replies inline
>>>>
>>>> On 4/8/26 12:51, Silvan Jegen wrote:
>>>>> Heyhey!
>>>>>
>>>>> Vicki Pfau <vi@endrift.com> wrote:
>>>>>> Hi,
>>>>>>
>>>>>> Replies inline
>>>>>>
>>>>>> On 4/2/26 12:09 PM, Silvan Jegen wrote:
>>>>>>> Hi
>>>>>>>
>>>>>>> Thanks for the patch!
>>>>>>>
>>>>>>> Just some comments and questions inline below.
>>>>>>>
>>>>>>> Vicki Pfau <vi@endrift.com> wrote:
>>>>>>>>
>>>>>>>> [...]
>>>>>>>>
>>>>>>>> +
>>>>>>>> +static int switch2_set_report_format(struct switch2_controller *ns2, enum switch2_report_id fmt)
>>>>>>>> +{
>>>>>>>> +	__le32 format_id = __cpu_to_le32(fmt);
>>>>>>>> +
>>>>>>>> +	if (!ns2->cfg)
>>>>>>>> +		return -ENOTCONN;
>>>>>>>> +	return ns2->cfg->send_command(NS2_CMD_INIT, NS2_SUBCMD_INIT_SELECT_REPORT,
>>>>>>>> +		&format_id, sizeof(format_id),
>>>>>>>> +		ns2->cfg);
>>>>>>>> +}
>>>>>>>> +
>>>>>>>> +static int switch2_init_controller(struct switch2_controller *ns2)
>>>>>>>
>>>>>>> This is now a recursive call while in v1 it wasn't. I think I preferred
>>>>>>> the non-recursive version as there was one place where init_step
>>>>>>> state was changed while now I am not sure where it happens (and whether
>>>>>>> there is a code path where we end up in an infinite recursion)
>>>>>>>
>>>>>>> What is the advantage of the recursive version compared to the
>>>>>>> non-recursive one?
>>>>>    >
>>>>>>
>>>>>> The old version incremented the step regardless of whether or not it
>>>>>> could confirm it had happened. Since the confirmation is now handled
>>>>>> with an external step, calling into switch2_init_step_done, the loop
>>>>>> condition would become somewhat complicated.
>>>>>> I replaced it with explicit tail calls since that make the
>>>>>> control flow simplier, and it is always matched with a call to
>>>>>> switch2_init_step_done to ensure that the state is always advanced. As
>>>>>
>>>>>   From what I can tell switch2_init_step_done currently only advances
>>>>> the state if the current state is the expected one. This seems fine,
>>>>> but it also means that if the state is not the expected one, the
>>>>> state is not advanced and the recursive call continues anyway (in the
>>>>> NS2_INIT_READ_USER_SECONDARY_CALIB case, for example). I assume this
>>>>> should never happen but if we end up in this case for some reason we
>>>>> will recurse forever.
>>>>
>>>> That's correct, and the only way it would happen forever is if there's a
>>>> bug. The same would be true in a loop version if it doesn't advance the
>>>> state properly either, fwiw, which happened during development of this
>>>> version. Regardless, I can reduce the chance of introducing such a bug
>>>> by passing ns2->init_step instead of a constant, so I'll make that
>>>> change in v4.
>>>>
>>>>>
>>>>> The same case also potentially calls switch2_read_flash and it isn't
>>>>> clear to me if this means that the initialisation is done (as there
>>>>> is no switch2_init_step_done call and we are not in the FINISH state
>>>>> either). There is also the possibility of switch2_read_flash calling
>>>>> switch2_init_controller again, which one then has to check ... (note
>>>>> that this is not the case here though)
>>>>
>>>> switch2_handle_flash_read will advance the state once it's verified that
>>>> the read actually happened. If the step failed for whatever reason, this
>>>> same codepath will retry the specific read, as the caller
>>>> (switch2_receive_command) will always call into switch2_init_controller
>>>> if setup isn't done. This is how the retry logic works.
>>>
>>> Ah, so the call chain looks something like the below?
>>>
>>> switch2_read_flash->
>>> switch2_usb_send_cmd->
>>> switch2_usb_message_in_work (?)->
>>> switch2_receive_command->
>>> switch2_handle_flash_read->
>>> switch2_init_step_done
>>
>> Yes, and then switch2_init_controller is called again at the end of 
>> switch2_receive_command
>>>
>>>>
>>>>>
>>>>> To me it seems like it would be clearer to do a `ns2->init_step++` and
>>>>> then `continue` to make the progress of the state more visible and to
>>>>> do an explicit `break` when we are supposed to stop the initialisation.
>>>>>
>>>>
>>>> The problem with the loop approach, in my opinion is due to the fact
>>>> that the loop is the *exception*, not the rule. The loop idiom makes it
>>>> look like a loop is expected. Further the ns2->init_step++ in the
>>>> previous version means that the verification does not occur, so in the
>>>> case of any sort of failure it'll plow ahead anyway instead of retrying.
>>>> The point of this approach is to avoid that.
>>>
>>> In my mind a while-loop like you mentioned it above would make the state
>>> changes more obvious (since they could all be done in the loop body), while
>>> still allowing for retries. Something like the below, perhaps (untested).
>>>
>>> while (ns2->init_step < NS2_INIT_DONE) {
>>> 	switch (ns2->init_step) {
>>> 		...
>>> 		case NS2_INIT_READ_FACTORY_TRIGGER_CALIB:
>>> 			if (ns2->ctlr_type != NS2_CTLR_TYPE_GC) {
>>> 				ns->init_step++
>>> 				continue;
>>> 			}
>>>
>>> 			ret = switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB,
>>> 			        NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB);		
>>> 			if (ret) {
>>> 				// if it makes sense to retry here
>>> 				continue;
>>> 			}
>>>
>>> 			ns->init_step++
>>> 			break;
>>>
>>> 		case ...
>>> 	}
>>> }
>>>
>>
>> This can't be done because the process is fundamentally asynchronous. 
>> We'd have to block on waiting for a reply to the USB packet, which is 
>> not a good idea. This is why switch2_receive_command calls into 
>> switch2_init_controller at the end: it's resuming where it left off. 
> 
> Ah, that wasn't clear to me either.
> 
> I assume having the code wait for the reply is not allowed because
> otherwise it would stall the whole bootup process (or will there be some
> sort of dedicated Kthread for this)?

I'm not sure there is a synchronous USB API at all, but if there is it should really only be used when absolutely necessary. If you can get away without blocking, do.

> 
> Are you aware of any documentation where I can read up on how the probing
> of USB HID devices work in the Linux Kernel?

This has nothing to do with HID, actually. The Switch 2 controllers have a proprietary side-channel interface that we need to poke at here to get it to do anything. It's called cfg in the code and over USB it uses a bulk interface, which is unusual. Presumably because the side-channel doesn't care about latency, whereas the HID interface does. Something similar happens over bluetooth where they're accessed through disparate attributes. The latter isn't handled here yet, though.

> 
> Thanks for the help!
> 
>> Each of those returns is a step that interacts with the hardware, and we 
>> need to wait for the hardware to reply.
>>
>> There is a potential weird interaction here whereby if we get an 
>> unprompted command and/or reply from the controller it will retry a step 
>> before it gets a reply for it, but in practice this doesn't happen. The 
>> controllers, as far as we know, only reply and never initiate any 
>> commands. Furthermore, all of these steps are also idempotent, so it's 
>> not a big deal of they get repeated erroneously.
> 
> Could there be another reply incoming while the driver is still processing
> the previous one? I assume at least at probing time that shouldn't be
> the case. I wouldn't expect an USB HID device to send unsolicited replies
> in general ...

Replies only come in one at a time. Unless we send multiple messages without waiting for a reply first, which this code is specifically designed *not* to do, I don't think it will happen in practice.

> 
> Cheers,
> Silvan


^ permalink raw reply

* Re: [PATCH v3 09/11] dt-bindings: input: Document hid-over-spi DT schema
From: Rob Herring @ 2026-04-13 22:34 UTC (permalink / raw)
  To: Conor Dooley, Dmitry Torokhov, Jingyuan Liang
  Cc: Jiri Kosina, Benjamin Tissoires, Jonathan Corbet, Mark Brown,
	Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
	Krzysztof Kozlowski, Conor Dooley, linux-input, linux-doc,
	linux-kernel, linux-spi, linux-trace-kernel, devicetree, hbarnor,
	tfiga, Dmitry Antipov, Jarrett Schultz
In-Reply-To: <20260410-sake-dollop-9f253ddb0749@spud>

On Fri, Apr 10, 2026 at 06:35:00PM +0100, Conor Dooley wrote:
> On Thu, Apr 09, 2026 at 10:16:46AM -0700, Dmitry Torokhov wrote:
> > On Thu, Apr 09, 2026 at 05:02:11PM +0100, Conor Dooley wrote:
> > > On Thu, Apr 02, 2026 at 01:59:46AM +0000, Jingyuan Liang wrote:
> > > > Documentation describes the required and optional properties for
> > > > implementing Device Tree for a Microsoft G6 Touch Digitizer that
> > > > supports HID over SPI Protocol 1.0 specification.
> > > > 
> > > > The properties are common to HID over SPI.
> > > > 
> > > > Signed-off-by: Dmitry Antipov <dmanti@microsoft.com>
> > > > Signed-off-by: Jarrett Schultz <jaschultz@microsoft.com>
> > > > Signed-off-by: Jingyuan Liang <jingyliang@chromium.org>
> > > > ---
> > > >  .../devicetree/bindings/input/hid-over-spi.yaml    | 126 +++++++++++++++++++++
> > > >  1 file changed, 126 insertions(+)
> > > > 
> > > > diff --git a/Documentation/devicetree/bindings/input/hid-over-spi.yaml b/Documentation/devicetree/bindings/input/hid-over-spi.yaml
> > > > new file mode 100644
> > > > index 000000000000..d1b0a2e26c32
> > > > --- /dev/null
> > > > +++ b/Documentation/devicetree/bindings/input/hid-over-spi.yaml
> > > > @@ -0,0 +1,126 @@
> > > > +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> > > > +%YAML 1.2
> > > > +---
> > > > +$id: http://devicetree.org/schemas/input/hid-over-spi.yaml#
> > > > +$schema: http://devicetree.org/meta-schemas/core.yaml#
> > > > +
> > > > +title: HID over SPI Devices
> > > > +
> > > > +maintainers:
> > > > +  - Benjamin Tissoires <benjamin.tissoires@redhat.com>
> > > > +  - Jiri Kosina <jkosina@suse.cz>
> > > 
> > > Why them and not you, the developers of the series?
> > > 
> > > > +
> > > > +description: |+
> > > > +  HID over SPI provides support for various Human Interface Devices over the
> > > > +  SPI bus. These devices can be for example touchpads, keyboards, touch screens
> > > > +  or sensors.
> > > > +
> > > > +  The specification has been written by Microsoft and is currently available
> > > > +  here: https://www.microsoft.com/en-us/download/details.aspx?id=103325
> > > > +
> > > > +  If this binding is used, the kernel module spi-hid will handle the
> > > > +  communication with the device and the generic hid core layer will handle the
> > > > +  protocol.
> > > 
> > > This is not relevant to the binding, please remove it.
> > > 
> > > > +
> > > > +allOf:
> > > > +  - $ref: /schemas/input/touchscreen/touchscreen.yaml#
> > > > +
> > > > +properties:
> > > > +  compatible:
> > > > +    oneOf:
> > > > +      - items:
> > > > +          - enum:
> > > > +              - microsoft,g6-touch-digitizer
> > > > +          - const: hid-over-spi
> > > > +      - description: Just "hid-over-spi" alone is allowed, but not recommended.
> > > > +        const: hid-over-spi
> > > 
> > > Why is it allowed but not recommended? Seems to me like we should
> > > require device-specific compatibles.
> > 
> > Why would we want to change the driver code to add a new compatible each
> > time a vendor decides to create a chip that is fully hid-spi-protocol
> > compliant? Or is the plan to still allow "hid-over-spi" fallback but
> > require device-specific compatible that will be ignored unless there is
> > device-specific quirk needed?

The plan is the latter case (the 1st entry up above). The comment is 
remove the 2nd entry (with 'Just "hid-over-spi" alone is allowed, but 
not recommended.').

> This has nothing to do with the driver, just the oddity of having a
> comment saying that not having a device specific compatible was
> permitted by not recommended in a binding. Requiring device-specific
> compatibles is the norm after all and a comment like this makes draws
> more attention to the fact that this is abnormal. Regardless of what the
> driver does, device-specific compatibles should be required.
> 
> > > > +
> > > > +  reg:
> > > > +    maxItems: 1
> > > > +
> > > > +  interrupts:
> > > > +    maxItems: 1
> > > > +
> > > > +  reset-gpios:
> > > > +    maxItems: 1
> > > > +    description:
> > > > +      GPIO specifier for the digitizer's reset pin (active low). The line must
> > > > +      be flagged with GPIO_ACTIVE_LOW.
> > > > +
> > > > +  vdd-supply:
> > > > +    description:
> > > > +      Regulator for the VDD supply voltage.
> > > > +
> > > > +  input-report-header-address:
> > > > +    $ref: /schemas/types.yaml#/definitions/uint32
> > > > +    minimum: 0
> > > > +    maximum: 0xffffff
> > > > +    description:
> > > > +      A value to be included in the Read Approval packet, listing an address of
> > > > +      the input report header to be put on the SPI bus. This address has 24
> > > > +      bits.
> > > > +
> > > > +  input-report-body-address:
> > > > +    $ref: /schemas/types.yaml#/definitions/uint32
> > > > +    minimum: 0
> > > > +    maximum: 0xffffff
> > > > +    description:
> > > > +      A value to be included in the Read Approval packet, listing an address of
> > > > +      the input report body to be put on the SPI bus. This address has 24 bits.
> > > > +
> > > > +  output-report-address:
> > > > +    $ref: /schemas/types.yaml#/definitions/uint32
> > > > +    minimum: 0
> > > > +    maximum: 0xffffff
> > > > +    description:
> > > > +      A value to be included in the Output Report sent by the host, listing an
> > > > +      address where the output report on the SPI bus is to be written to. This
> > > > +      address has 24 bits.
> > > > +
> > > > +  read-opcode:
> > > > +    $ref: /schemas/types.yaml#/definitions/uint8
> > > > +    description:
> > > > +      Value to be used in Read Approval packets. 1 byte.
> > > > +
> > > > +  write-opcode:
> > > > +    $ref: /schemas/types.yaml#/definitions/uint8
> > > > +    description:
> > > > +      Value to be used in Write Approval packets. 1 byte.
> > > 
> > > Why can none of these things be determined from the device's compatible?
> > > On the surface, they like the kinds of things that could/should be.
> > 
> > Why would we want to keep tables of these values in the kernel and again
> > have to update the driver for each new chip?
> 
> That's pretty normal though innit? It's what match data does.
> If someone wants to have properties that communicate data that
> can be determined from the compatible, they need to provide
> justification why it is being done.

IIRC, it was explained in prior versions the spec itself says these 
values vary by device. If we expect variation, then I think these 
properties are fine. But please capture the reasoning for them in this 
patch or we will just keep asking the same questions over and over. 

Rob

^ permalink raw reply

* [PATCH 2/2] Input: analog: Fix coding style - indentation and parenthesis spacing
From: Akash Sukhavasi @ 2026-04-13 22:19 UTC (permalink / raw)
  To: dmitry.torokhov; +Cc: linux-input, linux-kernel, Akash Sukhavasi
In-Reply-To: <20260413221928.21748-1-akash.sukhavasi@gmail.com>

The checkpatch.pl script reports minor whitespace and indentation
warnings in the analog joystick driver. Specifically, there is a
misaligned port->loop assignment using spaces instead of tabs, and
an extraneous space before a closing parenthesis in the
ANALOG_BTNS_TLR mask expression.

Signed-off-by: Akash Sukhavasi <akash.sukhavasi@gmail.com>
---
 drivers/input/joystick/analog.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/drivers/input/joystick/analog.c b/drivers/input/joystick/analog.c
index 6e1255a82..04fd5b119 100644
--- a/drivers/input/joystick/analog.c
+++ b/drivers/input/joystick/analog.c
@@ -330,7 +330,7 @@ static void analog_calibrate_timer(struct analog_port *port)
 			tx = t;
 	}
 
-        port->loop = tx / 50;
+	port->loop = tx / 50;
 }
 
 /*
@@ -490,7 +490,7 @@ static int analog_init_masks(struct analog_port *port)
 			| ((~analog[0].mask & ANALOG_HAT_FCS) << 4);
 
 	analog[0].mask &= ~(ANALOG_THROTTLE | ANALOG_RUDDER)
-			| (((~analog[0].mask & ANALOG_BTNS_TLR ) >> 10)
+			| (((~analog[0].mask & ANALOG_BTNS_TLR) >> 10)
 			&  ((~analog[0].mask & ANALOG_BTNS_TLR2) >> 12));
 
 	analog[1].mask = ((i >> 20) & 0xff) | ((i >> 12) & 0xf0000);
-- 
2.53.0


^ permalink raw reply related

* [PATCH 1/2] Input: analog: Fix coding style - trailing statements on same line
From: Akash Sukhavasi @ 2026-04-13 22:19 UTC (permalink / raw)
  To: dmitry.torokhov; +Cc: linux-input, linux-kernel, Akash Sukhavasi
In-Reply-To: <20260413221928.21748-1-akash.sukhavasi@gmail.com>

The checkpatch.pl script flags several errors where trailing
statements are placed on the same line as 'if', 'while', or 'for'
conditionals. This violates the kernel coding style and makes the
control flow harder to read.

Separate these single-line bodies onto their own dedicated lines to
conform with standard kernel formatting.

No functional change intended.

Signed-off-by: Akash Sukhavasi <akash.sukhavasi@gmail.com>
---
 drivers/input/joystick/analog.c | 45 +++++++++++++++++++++++----------
 1 file changed, 32 insertions(+), 13 deletions(-)

diff --git a/drivers/input/joystick/analog.c b/drivers/input/joystick/analog.c
index b6f7bce1c..6e1255a82 100644
--- a/drivers/input/joystick/analog.c
+++ b/drivers/input/joystick/analog.c
@@ -231,11 +231,13 @@ static int analog_button_read(struct analog_port *port, char saitek, char chf)
 
 	while ((~u & 0xf0) && (i < 16) && t) {
 		port->buttons |= 1 << analog_chf[(~u >> 4) & 0xf];
-		if (!saitek) return 0;
+		if (!saitek)
+			return 0;
 		udelay(ANALOG_SAITEK_DELAY);
 		t = strobe;
 		gameport_trigger(port->gameport);
-		while (((u = gameport_read(port->gameport)) & port->mask) && t) t--;
+		while (((u = gameport_read(port->gameport)) & port->mask) && t)
+			t--;
 		i++;
 	}
 
@@ -324,7 +326,8 @@ static void analog_calibrate_timer(struct analog_port *port)
 		local_irq_restore(flags);
 		udelay(i);
 		t = ktime_sub(t2, t1) - ktime_sub(t3, t2);
-		if (t < tx) tx = t;
+		if (t < tx)
+			tx = t;
 	}
 
         port->loop = tx / 50;
@@ -405,7 +408,8 @@ static int analog_init_device(struct analog_port *port, struct analog *analog, i
 				x = y;
 
 			if (analog->mask & ANALOG_SAITEK) {
-				if (i == 2) x = port->axes[i];
+				if (i == 2)
+					x = port->axes[i];
 				v = x - (x >> 2);
 				w = (x >> 4);
 			}
@@ -496,13 +500,25 @@ static int analog_init_masks(struct analog_port *port)
 
 	if (port->cooked) {
 
-		for (i = 0; i < 4; i++) max[i] = port->axes[i] << 1;
+		for (i = 0; i < 4; i++)
+			max[i] = port->axes[i] << 1;
+
+		if ((analog[0].mask & 0x7) == 0x7)
+			max[2] = (max[0] + max[1]) >> 1;
+
+		if ((analog[0].mask & 0xb) == 0xb)
+			max[3] = (max[0] + max[1]) >> 1;
+
+		if ((analog[0].mask & ANALOG_BTN_TL) &&
+		    !(analog[0].mask & ANALOG_BTN_TL2))
+			max[2] >>= 1;
+
+		if ((analog[0].mask & ANALOG_BTN_TR) &&
+		    !(analog[0].mask & ANALOG_BTN_TR2))
+			max[3] >>= 1;
 
-		if ((analog[0].mask & 0x7) == 0x7) max[2] = (max[0] + max[1]) >> 1;
-		if ((analog[0].mask & 0xb) == 0xb) max[3] = (max[0] + max[1]) >> 1;
-		if ((analog[0].mask & ANALOG_BTN_TL) && !(analog[0].mask & ANALOG_BTN_TL2)) max[2] >>= 1;
-		if ((analog[0].mask & ANALOG_BTN_TR) && !(analog[0].mask & ANALOG_BTN_TR2)) max[3] >>= 1;
-		if ((analog[0].mask & ANALOG_HAT_FCS)) max[3] >>= 1;
+		if ((analog[0].mask & ANALOG_HAT_FCS))
+			max[3] >>= 1;
 
 		gameport_calibrate(port->gameport, port->axes, max);
 	}
@@ -662,13 +678,16 @@ static void analog_parse_options(void)
 				analog_options[i] = analog_types[j].value;
 				break;
 			}
-		if (analog_types[j].name) continue;
+		if (analog_types[j].name)
+			continue;
 
 		analog_options[i] = simple_strtoul(js[i], &end, 0);
-		if (end != js[i]) continue;
+		if (end != js[i])
+			continue;
 
 		analog_options[i] = 0xff;
-		if (!strlen(js[i])) continue;
+		if (!strlen(js[i]))
+			continue;
 
 		printk(KERN_WARNING "analog.c: Bad config for port %d - \"%s\"\n", i, js[i]);
 	}
-- 
2.53.0


^ permalink raw reply related

* [PATCH 0/2] Input: analog: fix coding style violations
From: Akash Sukhavasi @ 2026-04-13 22:19 UTC (permalink / raw)
  To: dmitry.torokhov; +Cc: linux-input, linux-kernel, Akash Sukhavasi

This patch series resolves 15 errors and addresses several warnings 
reported by checkpatch.pl in the drivers/input/joystick/analog.c driver. 
The original code contained multiple instances of trailing statements 
on single-line conditionals, as well as minor indentation and spacing 
issues that are in violation of kernel coding style. 

This series breaks the cleanups into two logical parts:
1. Fixing trailing statements (moving them to their own lines).
2. Correcting indentation and parenthesis spacing.

Several warnings are intentionally left unaddressed, as they require 
semantic or functional changes outside the scope of this formatting 
cleanup:
- printk() to pr_warn()/pr_info() conversions
- simple_strtoul() to kstrtoul() conversion
- Filename strings embedded in printk() messages
- Quoted string split across lines
- Suspect indent on fail3 label

Tested with: scripts/checkpatch.pl --strict on the final patch.

Signed-off-by: Akash Sukhavasi <akash.sukhavasi@gmail.com>
---
Akash Sukhavasi (2):
  Input: analog: Fix coding style - trailing statements on same line
  Input: analog: Fix coding style - indentation and parenthesis spacing

 drivers/input/joystick/analog.c | 49 +++++++++++++++++++++++----------
 1 file changed, 34 insertions(+), 15 deletions(-)

-- 
2.53.0


^ permalink raw reply

* [PATCH 5.10 026/491] HID: Add HID_CLAIMED_INPUT guards in raw_event callbacks missing them
From: Greg Kroah-Hartman @ 2026-04-13 15:54 UTC (permalink / raw)
  To: stable
  Cc: Greg Kroah-Hartman, patches, Jiri Kosina, Benjamin Tissoires,
	Bastien Nocera, linux-input, stable
In-Reply-To: <20260413155819.042779211@linuxfoundation.org>

5.10-stable review patch.  If anyone has any objections, please let me know.

------------------

From: Greg Kroah-Hartman <gregkh@linuxfoundation.org>

commit ecfa6f34492c493a9a1dc2900f3edeb01c79946b upstream.

In commit 2ff5baa9b527 ("HID: appleir: Fix potential NULL dereference at
raw event handle"), we handle the fact that raw event callbacks
can happen even for a HID device that has not been "claimed" causing a
crash if a broken device were attempted to be connected to the system.

Fix up the remaining in-tree HID drivers that forgot to add this same
check to resolve the same issue.

Cc: Jiri Kosina <jikos@kernel.org>
Cc: Benjamin Tissoires <bentiss@kernel.org>
Cc: Bastien Nocera <hadess@hadess.net>
Cc: linux-input@vger.kernel.org
Cc: stable <stable@kernel.org>
Assisted-by: gkh_clanker_2000
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
---
 drivers/hid/hid-cmedia.c          |    2 +-
 drivers/hid/hid-creative-sb0540.c |    2 +-
 drivers/hid/hid-zydacron.c        |    2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

--- a/drivers/hid/hid-cmedia.c
+++ b/drivers/hid/hid-cmedia.c
@@ -57,7 +57,7 @@ static int cmhid_raw_event(struct hid_de
 {
 	struct cmhid *cm = hid_get_drvdata(hid);
 
-	if (len != CM6533_JD_RAWEV_LEN)
+	if (len != CM6533_JD_RAWEV_LEN || !(hid->claimed & HID_CLAIMED_INPUT))
 		goto out;
 	if (memcmp(data+CM6533_JD_SFX_OFFSET, ji_sfx, sizeof(ji_sfx)))
 		goto out;
--- a/drivers/hid/hid-creative-sb0540.c
+++ b/drivers/hid/hid-creative-sb0540.c
@@ -153,7 +153,7 @@ static int creative_sb0540_raw_event(str
 	u64 code, main_code;
 	int key;
 
-	if (len != 6)
+	if (len != 6 || !(hid->claimed & HID_CLAIMED_INPUT))
 		return 0;
 
 	/* From daemons/hw_hiddev.c sb0540_rec() in lirc */
--- a/drivers/hid/hid-zydacron.c
+++ b/drivers/hid/hid-zydacron.c
@@ -114,7 +114,7 @@ static int zc_raw_event(struct hid_devic
 	unsigned key;
 	unsigned short index;
 
-	if (report->id == data[0]) {
+	if (report->id == data[0] && (hdev->claimed & HID_CLAIMED_INPUT)) {
 
 		/* break keys */
 		for (index = 0; index < 4; index++) {



^ permalink raw reply

* [PATCH 5.15 050/570] HID: Add HID_CLAIMED_INPUT guards in raw_event callbacks missing them
From: Greg Kroah-Hartman @ 2026-04-13 15:53 UTC (permalink / raw)
  To: stable
  Cc: Greg Kroah-Hartman, patches, Jiri Kosina, Benjamin Tissoires,
	Bastien Nocera, linux-input, stable
In-Reply-To: <20260413155830.386096114@linuxfoundation.org>

5.15-stable review patch.  If anyone has any objections, please let me know.

------------------

From: Greg Kroah-Hartman <gregkh@linuxfoundation.org>

commit ecfa6f34492c493a9a1dc2900f3edeb01c79946b upstream.

In commit 2ff5baa9b527 ("HID: appleir: Fix potential NULL dereference at
raw event handle"), we handle the fact that raw event callbacks
can happen even for a HID device that has not been "claimed" causing a
crash if a broken device were attempted to be connected to the system.

Fix up the remaining in-tree HID drivers that forgot to add this same
check to resolve the same issue.

Cc: Jiri Kosina <jikos@kernel.org>
Cc: Benjamin Tissoires <bentiss@kernel.org>
Cc: Bastien Nocera <hadess@hadess.net>
Cc: linux-input@vger.kernel.org
Cc: stable <stable@kernel.org>
Assisted-by: gkh_clanker_2000
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
---
 drivers/hid/hid-cmedia.c          |    2 +-
 drivers/hid/hid-creative-sb0540.c |    2 +-
 drivers/hid/hid-zydacron.c        |    2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

--- a/drivers/hid/hid-cmedia.c
+++ b/drivers/hid/hid-cmedia.c
@@ -99,7 +99,7 @@ static int cmhid_raw_event(struct hid_de
 {
 	struct cmhid *cm = hid_get_drvdata(hid);
 
-	if (len != CM6533_JD_RAWEV_LEN)
+	if (len != CM6533_JD_RAWEV_LEN || !(hid->claimed & HID_CLAIMED_INPUT))
 		goto out;
 	if (memcmp(data+CM6533_JD_SFX_OFFSET, ji_sfx, sizeof(ji_sfx)))
 		goto out;
--- a/drivers/hid/hid-creative-sb0540.c
+++ b/drivers/hid/hid-creative-sb0540.c
@@ -153,7 +153,7 @@ static int creative_sb0540_raw_event(str
 	u64 code, main_code;
 	int key;
 
-	if (len != 6)
+	if (len != 6 || !(hid->claimed & HID_CLAIMED_INPUT))
 		return 0;
 
 	/* From daemons/hw_hiddev.c sb0540_rec() in lirc */
--- a/drivers/hid/hid-zydacron.c
+++ b/drivers/hid/hid-zydacron.c
@@ -114,7 +114,7 @@ static int zc_raw_event(struct hid_devic
 	unsigned key;
 	unsigned short index;
 
-	if (report->id == data[0]) {
+	if (report->id == data[0] && (hdev->claimed & HID_CLAIMED_INPUT)) {
 
 		/* break keys */
 		for (index = 0; index < 4; index++) {



^ permalink raw reply

* Re: [PATCH v5 4/4] Input: charlieplex_keypad: add GPIO charlieplex keypad
From: Hugo Villeneuve @ 2026-04-13 16:20 UTC (permalink / raw)
  To: Andy Shevchenko
  Cc: robin, andy, geert, robh, krzk+dt, conor+dt, dmitry.torokhov,
	hvilleneuve, mkorpershoek, matthias.bgg,
	angelogioacchino.delregno, lee, alexander.sverdlin, marek.vasut,
	akurz, devicetree, linux-kernel, linux-input, linux-arm-kernel,
	linux-mediatek
In-Reply-To: <abPXX1eWoq7C7J1R@ashevche-desk.local>

Hi Dmitry,

On Fri, 13 Mar 2026 11:22:39 +0200
Andy Shevchenko <andriy.shevchenko@intel.com> wrote:

> On Thu, Mar 12, 2026 at 02:00:58PM -0400, Hugo Villeneuve wrote:
> > 
> > Add support for GPIO-based charlieplex keypad, allowing to control
> > N^2-N keys using N GPIO lines.
> > 
> > Reuse matrix keypad keymap to simplify, even if there is no concept
> > of rows and columns in this type of keyboard.
> 
> LGTM,
> Reviewed-by: Andy Shevchenko <andriy.shevchenko@intel.com>

I was just wondering if this will go into v7.1, as I am not seing the
patch series in your input/next tree/branch for the moment? Let me know
if you need me to rebase it on v7.0.

-- 
Hugo Villeneuve

^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox