Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH v9 0/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-07-01  6:56 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Vicki Pfau, Silvan Jegen

This series adds preliminary support for Switch 2 controllers using the
same split-driver model as previous versions. This is a minor iteration on
v7, fixing errors pointed out by sashiko.

At this point I'm pretty sure that what's left are imagined errors. It has
a lot of ideas of what can be run concurrently that I'm no longer convinced
of. If nothing obvious stands out, I'd like a review from a human before
fixing more nothings from sashiko.

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                    | 1670 ++++++++++++++++-
 drivers/hid/hid-nintendo.h                    |   72 +
 drivers/input/joystick/Kconfig                |   11 +
 drivers/input/joystick/Makefile               |    1 +
 drivers/input/joystick/nintendo-switch2-usb.c |  464 +++++
 8 files changed, 2201 insertions(+), 41 deletions(-)
 create mode 100644 drivers/hid/hid-nintendo.h
 create mode 100644 drivers/input/joystick/nintendo-switch2-usb.c

-- 
2.54.0


^ permalink raw reply

* [PATCH v9 2/3] HID: nintendo: Add rumble support for Switch 2 controllers
From: Vicki Pfau @ 2026-07-01  6:56 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Vicki Pfau, Silvan Jegen
In-Reply-To: <20260701065606.4169999-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 | 217 ++++++++++++++++++++++++++++++++++++-
 2 files changed, 218 insertions(+), 7 deletions(-)

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 19c77c323ec9..851eed76c236 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -859,10 +859,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 5aacb8e88b70..96463e1586bb 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -2989,6 +2989,7 @@ enum switch2_init_step {
 	NS2_INIT_READ_USER_SECONDARY_CALIB,
 	NS2_INIT_SET_FEATURE_MASK,
 	NS2_INIT_ENABLE_FEATURES,
+	NS2_INIT_ENABLE_RUMBLE,
 	NS2_INIT_GRIP_BUTTONS,
 	NS2_INIT_REPORT_FORMAT,
 	NS2_INIT_SET_PLAYER_LEDS,
@@ -3020,6 +3021,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;
@@ -3043,8 +3056,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;
+	uint64_t last_rumble_work;
+	struct delayed_work rumble_work;
+	uint8_t *rumble_buffer;
+#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);
 
@@ -3136,7 +3186,7 @@ static const uint8_t switch2_init_cmd_data[] = {
 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,
+	NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU | NS2_FEATURE_RUMBLE,
 	0x00, 0x00, 0x00
 };
 
@@ -3197,7 +3247,11 @@ static void switch2_controller_put(struct switch2_controller *ns2)
 static void switch2_kref_put(struct kref *refcount)
 {
 	struct switch2_controller *ns2 = container_of(refcount,
-		struct switch2_controller, refcount);
+					struct switch2_controller, refcount);
+
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	cancel_delayed_work_sync(&ns2->rumble_work);
+#endif
 
 	guard(mutex)(&switch2_controllers_lock);
 	list_del_init(&ns2->entry);
@@ -3205,6 +3259,125 @@ static void switch2_kref_put(struct kref *refcount)
 	kfree(ns2);
 }
 
+#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);
+	unsigned long flags;
+
+	if (effect->type != FF_RUMBLE)
+		return 0;
+
+	spin_lock_irqsave(&ns2->rumble_lock, flags);
+	if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
+		ns2->rumble.sd.amplitude = max(effect->u.rumble.strong_magnitude,
+			(uint16_t) (effect->u.rumble.weak_magnitude >> 1));
+	} else {
+		ns2->rumble.hd.hi_freq = RUMBLE_HI_FREQ;
+		ns2->rumble.hd.lo_freq = RUMBLE_LO_FREQ;
+		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;
+	}
+	spin_unlock_irqrestore(&ns2->rumble_lock, flags);
+
+	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 flags;
+	bool active;
+	int ret = 0;
+
+	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;
+	spin_unlock_irqrestore(&ns2->rumble_lock, flags);
+
+	if (active) {
+		unsigned long interval = msecs_to_jiffies(4);
+		uint64_t current_jiffies = get_jiffies_64();
+
+		if (!ns2->last_rumble_work)
+			ns2->last_rumble_work = current_jiffies;
+		else
+			ns2->last_rumble_work += interval;
+
+		/* Reschedule a little early to make sure the buffer never underruns */
+		interval += msecs_to_jiffies(2);
+		if (ns2->last_rumble_work + interval >= current_jiffies)
+			schedule_delayed_work(&ns2->rumble_work,
+				ns2->last_rumble_work + interval - current_jiffies);
+		else
+			schedule_delayed_work(&ns2->rumble_work, 0);
+	} else {
+		ns2->last_rumble_work = 0;
+	}
+
+	mutex_lock(&ns2->lock);
+	if (!ns2->hdev) {
+		cancel_delayed_work(&ns2->rumble_work);
+	} else {
+		ret = hid_hw_output_report(ns2->hdev, ns2->rumble_buffer, 64);
+		/*
+		 * Don't log on ENODEV, ESHUTDOWN, or EPROTO, which can happen
+		 * mid-hotplug. Also cancel any further work on ENODEV or
+		 * ESHUTDOWN as they're clear indications that the endpoint
+		 * is dead.
+		 */
+		if (ret == -ENODEV || ret == -ESHUTDOWN)
+			cancel_delayed_work(&ns2->rumble_work);
+		else if (ret < 0 && ret != -EPROTO)
+			hid_warn_ratelimited(ns2->hdev,
+				"Failed to send output report ret=%d\n", ret);
+	}
+	mutex_unlock(&ns2->lock);
+}
+#endif
+
 static int switch2_set_leds(struct switch2_controller *ns2)
 {
 	int i;
@@ -3275,6 +3448,9 @@ static void switch2_input_deref(struct input_dev *input)
 {
 	struct switch2_controller *ns2 = input_get_drvdata(input);
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	cancel_delayed_work(&ns2->rumble_work);
+#endif
 	kref_put(&ns2->refcount, switch2_kref_put);
 }
 
@@ -3346,6 +3522,20 @@ static int switch2_init_input(struct switch2_controller *ns2)
 		return -EINVAL;
 	}
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	ns2->rumble_buffer = devm_kzalloc(&input->dev, 64, GFP_KERNEL);
+	if (!ns2->rumble_buffer) {
+		input_free_device(input);
+		return -ENOMEM;
+	}
+	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)
@@ -3749,7 +3939,16 @@ 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_ENABLE_RUMBLE:
+		/*
+		 * It is unclear what this packet is supposed to be for, but it
+		 * appears to be needed for rumble to work reliably. The reply
+		 * data indicates it might be a query of some sort, but we
+		 * ignore the reply so long as it doesn't return an error.
+		 */
+		return ns2->cfg->send_command(0x11, 1, NULL, 0, ns2->cfg);
 	case NS2_INIT_GRIP_BUTTONS:
 		if (!switch2_ctlr_is_joycon(ns2->ctlr_type)) {
 			switch2_init_step_done(ns2, ns2->init_step);
@@ -3861,6 +4060,10 @@ int switch2_receive_command(struct switch2_controller *ns2,
 			switch2_init_step_done(ns2, NS2_INIT_GET_FIRMWARE_INFO);
 		}
 		break;
+	case 0x11:
+		if (header->subcommand == 1)
+			switch2_init_step_done(ns2, NS2_INIT_ENABLE_RUMBLE);
+		break;
 	default:
 		break;
 	}
@@ -3981,6 +4184,11 @@ static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id
 
 	switch2_leds_create(ns2);
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	spin_lock_init(&ns2->rumble_lock);
+	INIT_DELAYED_WORK(&ns2->rumble_work, switch2_rumble_work);
+#endif
+
 	ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
 	if (ret) {
 		hid_err(hdev, "hw_start failed %d\n", ret);
@@ -4026,6 +4234,9 @@ static void switch2_remove(struct hid_device *hdev)
 	hid_set_drvdata(hdev, NULL);
 	ida_free(&nintendo_player_id_allocator, ns2->player_id);
 	mutex_unlock(&ns2->lock);
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	cancel_delayed_work_sync(&ns2->rumble_work);
+#endif
 	kref_put(&ns2->refcount, switch2_kref_put);
 	hid_hw_close(hdev);
 	hid_hw_stop(hdev);
-- 
2.54.0


^ permalink raw reply related

* [PATCH v9 1/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-07-01  6:56 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Vicki Pfau, Silvan Jegen
In-Reply-To: <20260701065606.4169999-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                    | 1264 ++++++++++++++++-
 drivers/hid/hid-nintendo.h                    |   72 +
 drivers/input/joystick/Kconfig                |   11 +
 drivers/input/joystick/Makefile               |    1 +
 drivers/input/joystick/nintendo-switch2-usb.c |  464 ++++++
 8 files changed, 1818 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 4ecd282f8f52..778982ab298e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19051,6 +19051,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 f9bcaeb66385..19c77c323ec9 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -843,10 +843,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 1059922baaac..9ba62b8fb894 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1103,6 +1103,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 e7302ec01ff1..5aacb8e88b70 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:
@@ -2662,7 +2669,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);
@@ -2673,7 +2680,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;
@@ -2777,7 +2784,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;
@@ -2796,7 +2803,7 @@ static void nintendo_hid_remove(struct hid_device *hdev)
 	hid_hw_stop(hdev);
 }
 
-static int nintendo_hid_resume(struct hid_device *hdev)
+static int joycon_resume(struct hid_device *hdev)
 {
 	struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);
 	int ret;
@@ -2819,7 +2826,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);
 
@@ -2838,7 +2845,1194 @@ static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message)
 	return 0;
 }
 
+/*
+ * =============================================================================
+ * 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;
+	struct kref refcount;
+
+	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;
+	int init_retries;
+	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_retries = 0;
+	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 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) {
+			if (kref_get_unless_zero(&ns2->refcount))
+				return ns2;
+		}
+	}
+	ns2 = kzalloc(sizeof(*ns2), GFP_KERNEL);
+	if (!ns2)
+		return ERR_PTR(-ENOMEM);
+
+	kref_init(&ns2->refcount);
+	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;
+
+	rcu_read_lock();
+	input = rcu_dereference(ns2->input);
+	rcu_read_unlock();
+
+	rcu_assign_pointer(ns2->input, NULL);
+	synchronize_rcu();
+
+	ns2->init_step = 0;
+
+	if (input)
+		input_unregister_device(input);
+}
+
+static void switch2_kref_put(struct kref *refcount)
+{
+	struct switch2_controller *ns2 = container_of(refcount,
+		struct switch2_controller, refcount);
+
+	guard(mutex)(&switch2_controllers_lock);
+	list_del_init(&ns2->entry);
+	mutex_destroy(&ns2->lock);
+	kfree(ns2);
+}
+
+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 input_dev *input = to_input_dev(dev);
+	struct switch2_controller *ns2 = input_get_drvdata(input);
+
+	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 | LED_RETAIN_AT_SHUTDOWN;
+	}
+}
+
+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_input_ref(struct input_dev *input)
+{
+	struct switch2_controller *ns2 = input_get_drvdata(input);
+
+	kref_get(&ns2->refcount);
+
+	return 0;
+}
+
+static void switch2_input_deref(struct input_dev *input)
+{
+	struct switch2_controller *ns2 = input_get_drvdata(input);
+
+	kref_put(&ns2->refcount, switch2_kref_put);
+}
+
+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 = input_allocate_device();
+	if (!input)
+		return -ENOMEM;
+
+	input_set_drvdata(input, ns2);
+	input->open = switch2_input_ref;
+	input->close = switch2_input_deref;
+	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);
+		input_free_device(input);
+		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) {
+			dev_err(&input->dev, "Failed to allocate name for player %d LED; ret=%d\n",
+				i + 1, ret);
+			break;
+		}
+
+		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);
+			break;
+		}
+	}
+
+	rcu_assign_pointer(ns2->input, input);
+	synchronize_rcu();
+	return 0;
+}
+
+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);
+
+	if (zero != 232)
+		value /= (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;
+	put_unaligned_le32(address, &message[4]);
+	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;
+
+	if (ns2->init_retries > NS2_MAX_INIT_RETRIES) {
+		if (ns2->init_retries == NS2_MAX_INIT_RETRIES + 1) {
+			dev_err(ns2->cfg->dev, "Failed to configure controller\n");
+			ns2->init_retries++;
+		}
+		return -EIO;
+	}
+
+	ns2->init_retries++;
+	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);
+
+	mutex_lock(&ns2->lock);
+
+	header = (const struct switch2_cmd_header *)message;
+	if (!(header->flags & NS2_FLAG_OK)) {
+		dev_warn(ns2->cfg->dev, "Packet error %02x replying to command %x:%x",
+			header->flags, header->command, header->subcommand);
+		ret = -EIO;
+		goto exit;
+	}
+	message = &message[8];
+	length -= 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 < 8) {
+				ret = -EINVAL;
+				goto exit;
+			}
+			read_size = message[0];
+			read_address = get_unaligned_le32(&message[4]);
+			if (length < read_size + 8) {
+				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);
+
+	mutex_unlock(&ns2->lock);
+	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);
+	int ret = 0;
+
+	if (IS_ERR(ns2))
+		return PTR_ERR(ns2);
+
+	mutex_lock(&ns2->lock);
+	if (ns2->cfg) {
+		ret = -EBUSY;
+		goto out;
+	}
+	cfg->parent = ns2;
+	ns2->cfg = cfg;
+
+	if (ns2->hdev)
+		ret = switch2_init_controller(ns2);
+
+	if (ret < 0) {
+		cfg->parent = NULL;
+		ns2->cfg = NULL;
+	}
+
+out:
+	mutex_unlock(&ns2->lock);
+
+	if (ret < 0)
+		kref_put(&ns2->refcount, switch2_kref_put);
+	return ret;
+}
+EXPORT_SYMBOL_GPL(switch2_controller_attach_cfg);
+
+void switch2_controller_detach_cfg(struct switch2_controller *ns2)
+{
+	mutex_lock(&ns2->lock);
+	if (!ns2->cfg || WARN_ON(ns2 != ns2->cfg->parent)) {
+		mutex_unlock(&ns2->lock);
+		return;
+	}
+	ns2->cfg->parent = NULL;
+	ns2->cfg = NULL;
+	switch2_controller_put(ns2);
+	mutex_unlock(&ns2->lock);
+	kref_put(&ns2->refcount, switch2_kref_put);
+}
+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;
+	}
+
+	ns2 = switch2_get_controller(phys);
+	if (IS_ERR(ns2))
+		return PTR_ERR(ns2);
+
+	mutex_lock(&ns2->lock);
+	if (ns2->hdev) {
+		ret = -EBUSY;
+		hid_err(hdev,
+			"Second hdevs tried to claim same controller, first=%p vs second=%p\n",
+			ns2->hdev, hdev);
+		goto err_put;
+	}
+	ns2->hdev = hdev;
+	hid_set_drvdata(hdev, ns2);
+
+	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);
+
+	ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+	if (ret) {
+		hid_err(hdev, "hw_start failed %d\n", ret);
+		goto err_cleanup;
+	}
+
+	ret = hid_hw_open(hdev);
+	if (ret) {
+		hid_err(hdev, "hw_open failed %d\n", ret);
+		goto err_stop;
+	}
+
+	ret = 0;
+	if (ns2->cfg)
+		ret = switch2_init_controller(ns2);
+
+	if (!ret) {
+		mutex_unlock(&ns2->lock);
+		return 0;
+	}
+
+	hid_hw_close(hdev);
+err_stop:
+	hid_hw_stop(hdev);
+err_cleanup:
+	ida_free(&nintendo_player_id_allocator, ns2->player_id);
+	ns2->hdev = NULL;
+err_put:
+	switch2_controller_put(ns2);
+	mutex_unlock(&ns2->lock);
+	kref_put(&ns2->refcount, switch2_kref_put);
+
+	return ret;
+}
+
+static void switch2_remove(struct hid_device *hdev)
+{
+	struct switch2_controller *ns2 = hid_get_drvdata(hdev);
+
+	mutex_lock(&ns2->lock);
+	switch2_controller_put(ns2);
+	ns2->hdev = NULL;
+	hid_set_drvdata(hdev, NULL);
+	ida_free(&nintendo_player_id_allocator, ns2->player_id);
+	mutex_unlock(&ns2->lock);
+	kref_put(&ns2->refcount, switch2_kref_put);
+	hid_hw_close(hdev);
+	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,
@@ -2863,10 +4057,67 @@ static const struct hid_device_id nintendo_hid_devices[] = {
 			 USB_DEVICE_ID_NINTENDO_N64CON) },
 	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_HORI,
 			 USB_DEVICE_ID_HORI_WIRELESS_SWITCH_PAD) },
+	/* 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);
+}
+
+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);
+}
+
 static struct hid_driver nintendo_hid_driver = {
 	.name		= "nintendo",
 	.id_table	= nintendo_hid_devices,
@@ -2894,4 +4145,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..0a004918c1f1
--- /dev/null
+++ b/drivers/input/joystick/nintendo-switch2-usb.c
@@ -0,0 +1,464 @@
+// 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;
+
+enum switch2_urb_state {
+	NS2_URB_FREE,
+	NS2_URB_OUT,
+	NS2_URB_IN,
+};
+
+struct switch2_urb {
+	struct urb *urb;
+	uint8_t *data;
+	enum switch2_urb_state state;
+};
+
+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;
+	bool shutdown;
+	spinlock_t bulk_in_lock;
+
+	struct switch2_urb bulk_out[NS2_OUT_URBS];
+	struct usb_anchor bulk_out_anchor;
+	spinlock_t bulk_out_lock;
+
+	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->bulk_in[i].state = NS2_URB_IN;
+			continue;
+		}
+
+		if (ns2_usb->bulk_in[i].state != NS2_URB_FREE)
+			continue;
+
+		/*
+		 * We want exactly one bulk in URB scheduled at a time, so only
+		 * reschedule this immediately if nothing else is scheduled
+		 * currently.
+		 */
+		if (!usb_anchor_empty(&ns2_usb->bulk_in_anchor) || ns2_usb->shutdown)
+			continue;
+
+		ns2_urb = &ns2_usb->bulk_in[i];
+		if (!ns2_urb)
+			continue;
+
+		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->state = NS2_URB_OUT;
+		}
+	}
+	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);
+		break;
+	default:
+		dev_dbg(&ns2_usb->udev->dev, "unknown output urb status: %d\n", urb->status);
+		break;
+	}
+
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		if (ns2_usb->bulk_out[i].urb != urb)
+			continue;
+
+		ns2_usb->bulk_out[i].state = NS2_URB_FREE;
+		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].state != NS2_URB_FREE)
+			continue;
+
+		urb = &ns2_usb->bulk_out[i];
+		urb->state = NS2_URB_OUT;
+		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_KERNEL);
+	if (ret) {
+		if (ret != -ENODEV)
+			dev_warn(&ns2_usb->udev->dev, "failed to submit output urb: %i", ret);
+		spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
+		urb->state = NS2_URB_FREE;
+		spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
+		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;
+	int i;
+	unsigned long flags;
+
+	spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+	for (i = 0; i < NS2_IN_URBS; i++) {
+		urb = &ns2_usb->bulk_in[i];
+		if (urb->state != NS2_URB_IN)
+			continue;
+		spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+
+		if (ns2_usb->cfg.parent) {
+			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);
+		} else {
+			dev_err(&ns2_usb->udev->dev,
+				"Got message before controller is fully set up; discarding\n");
+		}
+
+		spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+		urb->state = NS2_URB_FREE;
+		/*
+		 * We want exactly one bulk in URB scheduled at a time, so only
+		 * reschedule this immediately if nothing else is scheduled
+		 * currently.
+		 */
+		if (!usb_anchor_empty(&ns2_usb->bulk_in_anchor) || ns2_usb->shutdown)
+			continue;
+
+		usb_anchor_urb(urb->urb, &ns2_usb->bulk_in_anchor);
+		err = usb_submit_urb(urb->urb, GFP_ATOMIC);
+		if (err) {
+			usb_unanchor_urb(urb->urb);
+			dev_dbg(&ns2_usb->udev->dev,
+				"failed to queue input urb: %d\n", err);
+		} else {
+			urb->state = NS2_URB_OUT;
+		}
+	}
+	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;
+	struct urb *urb;
+	uint8_t *data;
+	char phys[64];
+	int ret;
+	int i;
+	unsigned long flags;
+
+	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;
+
+	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);
+
+	ns2_usb->udev = udev;
+	for (i = 0; i < NS2_IN_URBS; i++) {
+		urb = usb_alloc_urb(0, GFP_KERNEL);
+		if (!urb) {
+			ret = -ENOMEM;
+			goto err_free_in;
+		}
+
+		data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
+			&urb->transfer_dma);
+		if (!data) {
+			usb_free_urb(urb);
+			ret = -ENOMEM;
+			goto err_free_in;
+		}
+
+		spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+		usb_fill_bulk_urb(urb, udev,
+			usb_rcvbulkpipe(udev, bulk_in->bEndpointAddress),
+			data, NS2_BULK_SIZE, switch2_bulk_in, ns2_usb);
+		urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+		ns2_usb->bulk_in[i].urb = urb;
+		ns2_usb->bulk_in[i].data = data;
+		spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+	}
+
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		urb = usb_alloc_urb(0, GFP_KERNEL);
+		if (!urb) {
+			ret = -ENOMEM;
+			goto err_free_out;
+		}
+
+		data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
+			&urb->transfer_dma);
+		if (!data) {
+			usb_free_urb(urb);
+			ret = -ENOMEM;
+			goto err_free_out;
+		}
+
+		spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
+		usb_fill_bulk_urb(urb, udev,
+			usb_sndbulkpipe(udev, bulk_out->bEndpointAddress),
+			data, NS2_BULK_SIZE, switch2_bulk_out, ns2_usb);
+		urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+		ns2_usb->bulk_out[i].urb = urb;
+		ns2_usb->bulk_out[i].data = data;
+		spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
+	}
+
+	usb_set_intfdata(intf, ns2_usb);
+
+	ns2_usb->cfg.dev = &ns2_usb->udev->dev;
+	ns2_usb->cfg.send_command = switch2_usb_send_cmd;
+
+	spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+	ns2_usb->bulk_in[0].state = NS2_URB_OUT;
+	usb_anchor_urb(ns2_usb->bulk_in[0].urb, &ns2_usb->bulk_in_anchor);
+	ret = usb_submit_urb(ns2_usb->bulk_in[0].urb, GFP_ATOMIC);
+	spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+
+	if (ret < 0)
+		goto err_free_out;
+
+	ret = switch2_controller_attach_cfg(phys, &ns2_usb->cfg);
+	if (ret < 0)
+		goto err_free_out;
+
+	return 0;
+
+err_free_out:
+	usb_kill_anchored_urbs(&ns2_usb->bulk_out_anchor);
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
+		urb = ns2_usb->bulk_out[i].urb;
+		data = ns2_usb->bulk_out[i].data;
+		if (!urb) {
+			spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
+			continue;
+		}
+
+		ns2_usb->bulk_out[i].urb = NULL;
+		ns2_usb->bulk_out[i].data = NULL;
+		spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
+
+		usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
+		usb_free_urb(urb);
+	}
+err_free_in:
+	spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+	ns2_usb->shutdown = true;
+	spin_unlock_irqrestore(&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++) {
+		spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+		urb = ns2_usb->bulk_in[i].urb;
+		data = ns2_usb->bulk_in[i].data;
+		if (!urb) {
+			spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+			continue;
+		}
+
+		ns2_usb->bulk_in[i].urb = NULL;
+		ns2_usb->bulk_in[i].data = NULL;
+		spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+
+		usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
+		usb_free_urb(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;
+	struct urb *urb;
+	uint8_t *data;
+	int i;
+
+	/* Prevent any further IN URBs from being scheduled */
+	spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+	ns2_usb->shutdown = true;
+	spin_unlock_irqrestore(&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++) {
+		spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+		urb = ns2_usb->bulk_in[i].urb;
+		data = ns2_usb->bulk_in[i].data;
+		ns2_usb->bulk_in[i].urb = NULL;
+		ns2_usb->bulk_in[i].data = NULL;
+		spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+
+		usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
+		usb_free_urb(urb);
+	}
+
+	/*
+	 * We need to detach *before* we kill the out URBs to make sure no
+	 * further URBs get scheduled by the HID endpoint in the meantime.
+	 */
+	switch2_controller_detach_cfg(ns2_usb->cfg.parent);
+
+	usb_kill_anchored_urbs(&ns2_usb->bulk_out_anchor);
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags);
+		urb = ns2_usb->bulk_out[i].urb;
+		data = ns2_usb->bulk_out[i].data;
+		ns2_usb->bulk_out[i].urb = NULL;
+		ns2_usb->bulk_out[i].data = NULL;
+		spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
+
+		usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
+		usb_free_urb(urb);
+	}
+}
+
+#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		= "nintendo-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.54.0


^ permalink raw reply related

* [PATCH v9 3/3] HID: nintendo: Add unified report format support
From: Vicki Pfau @ 2026-07-01  6:56 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Vicki Pfau, Silvan Jegen
In-Reply-To: <20260701065606.4169999-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 | 151 +++++++++++++++++++++++++++++++++++--
 1 file changed, 146 insertions(+), 5 deletions(-)

diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
index 96463e1586bb..b406e6cfd221 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -2872,6 +2872,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
@@ -3120,6 +3150,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,		},
@@ -3137,6 +3183,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,		},
@@ -3158,6 +3221,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,		},
@@ -3175,6 +3259,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
@@ -3787,11 +3888,51 @@ 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.
-		 */
+		if (size < 0x3f)
+			return -EINVAL;
+
+		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.54.0


^ permalink raw reply related

* Re: [PATCH v9 1/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: sashiko-bot @ 2026-07-01  7:12 UTC (permalink / raw)
  To: Vicki Pfau; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260701065606.4169999-2-vi@endrift.com>

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] Potential NULL pointer dereference in `switch2_handle_flash_read()` when receiving spontaneous or delayed USB messages.
- [High] Race condition in `switch2_remove()` clearing drvdata before hardware stop causes NULL pointer dereference.
- [High] Unconditional `ida_free()` with `U32_MAX` triggers a `BUG_ON` panic on probe failure or device removal.
- [High] Using `devm_led_classdev_register` with `&input->dev` causes a devres circular reference, leaking devices and causing a Use-After-Free.
- [High] The `-EBUSY` error path in `switch2_probe` erroneously destroys the active input device of the existing interface.
- [High] TOCTOU race condition in `switch2_usb_disconnect` causes UAF due to missing `usb_kill_urb`.
- [Medium] URB slot leak and stuck polling on transient USB errors, plus processing of invalid data.
--

commit ef162798bccd0977cafe2dea531f9e56194e0992
Author: Vicki Pfau <vi@endrift.com>
Subject: HID: nintendo: Add preliminary Switch 2 controller driver

This patch adds a new driver for Nintendo Switch 2 controllers, using a
split-driver design for HID and USB configuration interfaces. It brings
support for Joy-Con 2, Pro Controller 2, and GameCube NSO controllers via USB.

I found a few regressions while reviewing this patch.

> diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
> index e7302ec01ff1f..5aacb8e88b704 100644
> --- a/drivers/hid/hid-nintendo.c
> +++ b/drivers/hid/hid-nintendo.c
> @@ -2838,7 +2845,1194 @@ static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message)
> [ ... ]
> +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_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) {

[Severity: High]
Can this cause a NULL pointer dereference? If the USB configuration
interface receives a flash read reply before the HID interface finishes
probing, or if a delayed response arrives from a previous session,
ns2->hdev will be NULL when hid_dbg is called.

> +			hid_dbg(ns2->hdev, "Got factory primary stick calibration:\n");
> +			hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
> [ ... ]
> +static int switch2_init_input(struct switch2_controller *ns2)
> +{
> [ ... ]
> +	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) {
> +			dev_err(&input->dev, "Failed to allocate name for player %d LED; ret=%d\n",
> +				i + 1, ret);
> +			break;
> +		}
> +
> +		led->name = name;

[Severity: High]
Could this create a circular devres reference? The LED class devices hold
references to their parent (the input device). When the controller is
removed, the input device's devres cleanup won't run until its refcount
hits 0, which cannot happen because the LED devices still exist.

Since the led_classdev structures are embedded in ns2 (which is freed on
removal), might this cause a use-after-free when the leaked LED sysfs
nodes are later accessed?

> +		ret = devm_led_classdev_register(&input->dev, led);
> +		if (ret < 0) {
> +			dev_err(&input->dev, "Failed to register player %d LED; ret=%d\n",
> [ ... ]
> +static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id)
> +{
> [ ... ]
> +	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;
> [ ... ]
> +	hid_hw_close(hdev);
> +err_stop:
> +	hid_hw_stop(hdev);
> +err_cleanup:
> +	ida_free(&nintendo_player_id_allocator, ns2->player_id);
> +	ns2->hdev = NULL;

[Severity: High]
Does this error path break the already active controller? If a second HID
interface for the same physical controller probes and detects a conflict,
it returns -EBUSY and jumps to err_put.

However, unconditionally calling switch2_controller_put(ns2) here tears
down the input device for the existing, bound interface.

> +err_put:
> +	switch2_controller_put(ns2);
> +	mutex_unlock(&ns2->lock);
> +	kref_put(&ns2->refcount, switch2_kref_put);
> +
> +	return ret;
> +}
> +
> +static void switch2_remove(struct hid_device *hdev)
> +{
> +	struct switch2_controller *ns2 = hid_get_drvdata(hdev);
> +
> +	mutex_lock(&ns2->lock);
> +	switch2_controller_put(ns2);
> +	ns2->hdev = NULL;

[Severity: High]
Can this cause a race condition leading to a NULL pointer dereference?
The drvdata is set to NULL before hid_hw_stop() halts the hardware.

If an input interrupt triggers switch2_event() in this window, it will
retrieve a NULL pointer from hid_get_drvdata() and immediately dereference
it when calling rcu_dereference(ns2->input).

> +	hid_set_drvdata(hdev, NULL);

[Severity: High]
Does this trigger a panic if ida_alloc() failed during probe? If the probe
fails to allocate the ID, ns2->player_id remains U32_MAX.

Unconditionally calling ida_free() during removal passes this value, which
casts to -1 and will trigger the BUG_ON((int)id < 0) inside the ida_free()
implementation.

> +	ida_free(&nintendo_player_id_allocator, ns2->player_id);
> +	mutex_unlock(&ns2->lock);
> +	kref_put(&ns2->refcount, switch2_kref_put);
> +	hid_hw_close(hdev);
> +	hid_hw_stop(hdev);
> +}

> diff --git a/drivers/input/joystick/nintendo-switch2-usb.c b/drivers/input/joystick/nintendo-switch2-usb.c
> new file mode 100644
> index 0000000000000..0a004918c1f10
> --- /dev/null
> +++ b/drivers/input/joystick/nintendo-switch2-usb.c
> @@ -0,0 +1,464 @@
> [ ... ]
> +static void switch2_bulk_in(struct urb *urb)
> +{
> [ ... ]
> +	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;
> +

[Severity: Medium]
Does this leak the URB slot and process invalid data? If an URB completes
with an error, the schedule flag remains false, but the state is set to
NS2_URB_IN. If both IN URBs fail, the workqueue is never scheduled again.

If the workqueue is scheduled by the other URB, switch2_usb_message_in_work()
will process this failed URB without validating urb->status, passing
potentially garbage data to switch2_receive_command().

> +		if (ns2_usb->bulk_in[i].urb == urb) {
> +			ns2_usb->bulk_in[i].state = NS2_URB_IN;
> +			continue;
> +		}
> [ ... ]
> +static void switch2_usb_disconnect(struct usb_interface *intf)
> +{
> [ ... ]
> +	/* Prevent any further IN URBs from being scheduled */
> +	spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
> +	ns2_usb->shutdown = true;
> +	spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
> +

[Severity: High]
Is there a TOCTOU race condition here during disconnect? The USB core
unanchors URBs before invoking their completion handler.

If switch2_bulk_in() is running concurrently, usb_kill_anchored_urbs()
won't wait for it. cancel_work_sync() might return immediately if the
handler hasn't yet called schedule_work().

Could the handler then schedule work on the freed ns2_usb structure,
leading to a use-after-free?

> +	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++) {
> +		spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260701065606.4169999-1-vi@endrift.com?part=1

^ permalink raw reply

* Re: [PATCH v9 3/3] HID: nintendo: Add unified report format support
From: sashiko-bot @ 2026-07-01  7:12 UTC (permalink / raw)
  To: Vicki Pfau; +Cc: linux-input, dmitry.torokhov
In-Reply-To: <20260701065606.4169999-4-vi@endrift.com>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [Critical] LED class devices registered with devm_led_classdev_register() outlive their containing switch2_controller structure, leading to a UAF if accessed via sysfs after the device is unplugged.
--

commit 84beb320490de870df51e881a9a77c861e3838ca
Author: Vicki Pfau <vi@endrift.com>

HID: nintendo: Add unified report format support

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

> diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
> index 96463e1586bbd..b406e6cfd221f 100644
> --- a/drivers/hid/hid-nintendo.c
> +++ b/drivers/hid/hid-nintendo.c

[ ... ]

[Severity: Critical]
This is a pre-existing issue, but can this lead to a use-after-free when the
device is unplugged?

Looking at switch2_init_input(), LEDs are registered with a devm action tied to
input->dev:

drivers/hid/hid-nintendo.c:switch2_init_input() {
    ...
    ret = devm_led_classdev_register(&input->dev, led);
    ...
}

This means their teardown action is delayed until the input device's kobject is
released.

However, the ns2 structure containing the leds array is freed manually in
switch2_kref_put() upon device removal:

drivers/hid/hid-nintendo.c:switch2_kref_put() {
    ...
    kfree(ns2);
    ...
}

Because the LED sysfs device is a child of input->dev, it prevents input->dev
from being released until it is unregistered. This creates a circular dependency
where the devm cleanup is never triggered.

If a process keeps the LED sysfs file open, input->dev remains alive, but ns2
is freed. Does this leave the LED sysfs endpoints pointing to freed memory,
potentially executing function pointers from the freed ns2 structure when
writing to the LED sysfs brightness file?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260701065606.4169999-1-vi@endrift.com?part=3

^ permalink raw reply

* Re: [PATCH v9 2/3] HID: nintendo: Add rumble support for Switch 2 controllers
From: sashiko-bot @ 2026-07-01  7:17 UTC (permalink / raw)
  To: Vicki Pfau; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260701065606.4169999-3-vi@endrift.com>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Use-After-Free on `ns2->rumble_buffer` during device teardown due to devres lifecycle mismatch.
- [Medium] Rumble work scheduling math error adds 2ms instead of subtracting, causing it to run late and risk buffer underruns.
- [Medium] Pro Controller rumble motors receive identical signals, failing to control them individually as claimed.
--

commit 0899ea0ef032bf60e7abf4c07b73da1a806a6406
Author: Vicki Pfau <vi@endrift.com>
Date:   Wed Jul 1 06:56:03 2026 +0000

    HID: nintendo: Add rumble support for Switch 2 controllers
    
    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.

> diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
> index 5aacb8e88b704..96463e1586bbd 100644
> --- a/drivers/hid/hid-nintendo.c
> +++ b/drivers/hid/hid-nintendo.c
> @@ -3205,6 +3259,125 @@ static void switch2_kref_put(struct kref *refcount)
[ ... ]
> +static int switch2_play_effect(struct input_dev *dev, void *data, struct ff_effect *effect)
> +{
[ ... ]
> +	if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
> +		ns2->rumble.sd.amplitude = max(effect->u.rumble.strong_magnitude,
> +			(uint16_t) (effect->u.rumble.weak_magnitude >> 1));
> +	} else {
> +		ns2->rumble.hd.hi_freq = RUMBLE_HI_FREQ;
> +		ns2->rumble.hd.lo_freq = RUMBLE_LO_FREQ;
> +		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;
> +	}
> +	spin_unlock_irqrestore(&ns2->rumble_lock, flags);
> +
> +	schedule_delayed_work(&ns2->rumble_work, 0);
> +
> +	return 0;
> +}
> +
> +static void switch2_rumble_work(struct work_struct *work)
> +{
[ ... ]
> +	} 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]);
> +		}
> +	}

[Severity: Medium]
Does this code accurately provide independent control of the two rumble motors?

The commit message and the comment above mention that the Pro Controller's left
and right rumble motors can be controlled individually.

However, in switch2_play_effect(), the strong and weak magnitudes are mixed
into a single switch2_hd_rumble structure (ns2->rumble.hd). 

Then in switch2_rumble_work(), this exact same structure is encoded into both
the left (offset 0x2) and right (offset 0x12) rumble buffers. 

Since both physical motors receive the exact same mixed signal (both high and 
low frequencies simultaneously), doesn't this result in identical rumble on 
both sides rather than individual control?

> +	ns2->rumble_seq = (ns2->rumble_seq + 1) & 0xF;
> +	spin_unlock_irqrestore(&ns2->rumble_lock, flags);
> +
> +	if (active) {
> +		unsigned long interval = msecs_to_jiffies(4);
> +		uint64_t current_jiffies = get_jiffies_64();
> +
> +		if (!ns2->last_rumble_work)
> +			ns2->last_rumble_work = current_jiffies;
> +		else
> +			ns2->last_rumble_work += interval;
> +
> +		/* Reschedule a little early to make sure the buffer never underruns */
> +		interval += msecs_to_jiffies(2);
> +		if (ns2->last_rumble_work + interval >= current_jiffies)
> +			schedule_delayed_work(&ns2->rumble_work,
> +				ns2->last_rumble_work + interval - current_jiffies);

[Severity: Medium]
Does this schedule the work early or late?

The comment mentions rescheduling slightly early to prevent buffer underruns.

Since interval starts at 4ms, adding 2ms to it with
interval += msecs_to_jiffies(2) makes the new interval 6ms. 

When scheduling for ns2->last_rumble_work + interval, this appears to target
a time 6ms after the last theoretical tick instead of the intended earlier
target (which would be 2ms after, or interval - 2ms).

Could this delay cause the work to run later than intended and lead to the 
buffer underruns the comment warns about?

[ ... ]
> @@ -3346,6 +3522,20 @@ static int switch2_init_input(struct switch2_controller *ns2)
[ ... ]
> +#if IS_ENABLED(CONFIG_NINTENDO_FF)
> +	ns2->rumble_buffer = devm_kzalloc(&input->dev, 64, GFP_KERNEL);
> +	if (!ns2->rumble_buffer) {
> +		input_free_device(input);
> +		return -ENOMEM;
> +	}

[Severity: High]
Can this lead to a use-after-free during device removal?

The rumble_buffer is allocated with devm_kzalloc() tied to the input device, 
so it gets freed when the input device is unregistered and its final reference
is dropped.

However, looking at the teardown sequence, if the input device is unregistered
(for example via switch2_controller_put() calling input_unregister_device())
before the delayed work is completely synced and canceled, the worker could run
and access the freed rumble_buffer.

Would it be safer to allocate this buffer without devm, or ensure the delayed
work is fully canceled prior to unregistering the input device?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260701065606.4169999-1-vi@endrift.com?part=2

^ permalink raw reply

* [PATCH v5] HID: logitech-hidpp: Add support for HID++ Multi-Platform feature (0x4531)
From: DevExalt @ 2026-07-01  7:37 UTC (permalink / raw)
  To: jikos, bentiss
  Cc: lains, hadess, linux-input, linux-kernel, sari.kreitem, hbarnor,
	Baraa Atta (Dev Exalt)
In-Reply-To: <20260625080807.74157-1-exalt.dev.team@gmail.com>

From: "Baraa Atta (Dev Exalt)" <exalt.dev.team@gmail.com>

Add support in the Logitech HID++ driver for the HID++ Multi-Platform
feature (0x4531), which enables HID++ devices to adjust their behavior
based on the host operating system.

This patch:
 * Adds device IDs for MX Keys S (046d) and Casa Keys (046d).
 * Introduces the per-device sysfs attribute "platform" to allow selecting
   a target platform.
 * Detects whether a device implements feature 0x4531.
 * Validates that the requested platform is supported by the device.
 * Applies the selected platform when valid.
 * Leaves the device unchanged when an unsupported platform is requested.

Supported values for the platform sysfs attribute:

 windows, winemb, linux, chrome, android,
 macos, ios, webos, tizen

TEST=Pair MX Keys S and Casa Keys over Bluetooth and verify:
 * Feature 0x4531 is detected.
 * Valid platform values written through sysfs are accepted and applied.
 * Invalid platform values result in no update.
 * Devices without 0x4531 retain default behavior.
 * Platform-specific key behavior is observed once applied.

Signed-off-by: Baraa Atta (Dev Exalt) <exalt.dev.team@gmail.com>
---
Changes in v2:
  * Replace the global hidpp_platform module parameter with a per-device
    sysfs attribute
  * Expose all platforms  supported by the HID++ Multi-Platform feature
  * Update documentation and testing description

Changes in v3:
  * Address Sashiko review comments.
  * Switch to devm_mutex_init() to handle mutex lifecycle management
    automatically.
  * Move hidpp_multiplatform_init() to the end of hidpp_probe() after
    hid_device_io_start() to guarantee that the hardware I/O loop is fully
    active when the sysfs attribute becomes visible.

Changes in v4:
  * Address Sashiko review comments.
  * Fix a potential use-after-destroy race condition during device unbind
    by replacing devm_device_add_group() with manual sysfs_create_group()
    management, ensuring the sysfs attribute is removed before the
    associated device resources and send_mutex are destroyed.

Changes in v5:
  * Address Sashiko review comments.
  * Fix a sub system design violation by removing the hardcoded 0xF client
    ID from the lower nibble of the FAP command macros.

 .../testing/sysfs-driver-hid-logitech-hidpp   |  29 ++
 drivers/hid/hid-ids.h                         |   2 +
 drivers/hid/hid-logitech-hidpp.c              | 399 ++++++++++++++++++
 drivers/hid/hid-quirks.c                      |   2 +
 4 files changed, 432 insertions(+)

diff --git a/Documentation/ABI/testing/sysfs-driver-hid-logitech-hidpp b/Documentation/ABI/testing/sysfs-driver-hid-logitech-hidpp
index d8f831f2d6b5..41f0fd8c2192 100644
--- a/Documentation/ABI/testing/sysfs-driver-hid-logitech-hidpp
+++ b/Documentation/ABI/testing/sysfs-driver-hid-logitech-hidpp
@@ -17,3 +17,32 @@ Description:
 		handling battery properties in the kernel. This way, upower can
 		add a udev rule to decide whether or not it should use the
 		internal unifying support or the generic kernel one.
+
+What:		/sys/bus/hid/drivers/logitech-hidpp-device/<dev>/platform
+Date:		Jul, 2026
+KernelVersion:	7.2
+Contact:	linux-input@vger.kernel.org
+Description:
+		(WO) This attribute is present only on Logitech HID++ 2.0 devices
+		that implement feature 0x4531 (Multi-Platform). It allows the host
+		to select which operating-system platform the device should emulate,
+		altering its key mapping and behaviour accordingly.
+
+		Writing one of the following platform names programs the device:
+
+		  ===========  ======================================================
+		  windows       Standard Windows key layout
+		  winemb        Windows Embedded key layout
+		  linux         Linux key layout
+		  chrome        ChromeOS key layout
+		  android       Android key layout
+		  macos         macOS key layout
+		  ios           iOS key layout
+		  webos         webOS key layout
+		  tizen         Tizen key layout
+		  ===========  ======================================================
+
+		Only platforms advertised by the device's own descriptors are
+		accepted. The input is case-insensitive. Writing an unknown
+		platform name returns -EINVAL; writing a valid name that the
+		device does not expose in its descriptors returns -EOPNOTSUPP.
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 1059922baaac..a6c2dea79397 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -913,6 +913,8 @@
 #define USB_DEVICE_ID_LOGITECH_T651	0xb00c
 #define USB_DEVICE_ID_LOGITECH_DINOVO_EDGE_KBD	0xb309
 #define USB_DEVICE_ID_LOGITECH_CASA_TOUCHPAD	0xbb00
+#define USB_DEVICE_ID_LOGITECH_CASA_KEYS_KEYBOARD	0xb371
+#define USB_DEVICE_ID_LOGITECH_MX_KEYS_S_KEYBOARD	0xb378
 #define USB_DEVICE_ID_LOGITECH_C007	0xc007
 #define USB_DEVICE_ID_LOGITECH_C077	0xc077
 #define USB_DEVICE_ID_LOGITECH_RECEIVER	0xc101
diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index 90b0184df777..39edb22b55e1 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -209,6 +209,9 @@ struct hidpp_device {
 	int hires_wheel_multiplier;
 	u8 hires_wheel_feature_index;
 
+	u8 multiplatform_feature_index;
+	struct mutex multiplatform_lock;
+
 	bool connected_once;
 };
 
@@ -4423,6 +4426,398 @@ static bool hidpp_application_equals(struct hid_device *hdev,
 	return report && report->application == application;
 }
 
+/* -------------------------------------------------------------------------- */
+/* 0x4531: Multi-Platform Support                                             */
+/* -------------------------------------------------------------------------- */
+
+/*
+ * Some Logitech devices expose the HID++ feature 0x4531 (Multi-Platform) allowing
+ * the host to specify which operating system platform to use on the device. Changing device's
+ * platform may alter the behavior of the device to match the specified platform.
+ *
+ * Devices that implement this feature expose a per-device sysfs attribute
+ * "platform". Writing one of (windows|winemb|linux|chrome|android|
+ * macos|ios|webos|tizen) selects the matching platform descriptor on the device.
+ */
+
+#define HIDPP_MULTIPLATFORM_FEAT_ID			0x4531
+#define HIDPP_MULTIPLATFORM_GET_FEATURE_INFO		0x00
+#define HIDPP_MULTIPLATFORM_GET_PLATFORM_DESCRIPTOR	0x10
+#define HIDPP_MULTIPLATFORM_SET_CURRENT_PLATFORM	0x30
+
+#define HIDPP_MULTIPLATFORM_PLATFORM_MASK_TIZEN		BIT(0)
+#define HIDPP_MULTIPLATFORM_PLATFORM_MASK_WINDOWS	BIT(8)
+#define HIDPP_MULTIPLATFORM_PLATFORM_MASK_WINEMB	BIT(9)
+#define HIDPP_MULTIPLATFORM_PLATFORM_MASK_LINUX		BIT(10)
+#define HIDPP_MULTIPLATFORM_PLATFORM_MASK_CHROME	BIT(11)
+#define HIDPP_MULTIPLATFORM_PLATFORM_MASK_ANDROID	BIT(12)
+#define HIDPP_MULTIPLATFORM_PLATFORM_MASK_MACOS		BIT(13)
+#define HIDPP_MULTIPLATFORM_PLATFORM_MASK_IOS		BIT(14)
+#define HIDPP_MULTIPLATFORM_PLATFORM_MASK_WEBOS		BIT(15)
+
+struct hidpp_platform_desc {
+	u8 plat_idx;
+	u8 desc_idx;
+	u16 plat_mask;
+};
+
+/*
+ * Platform names exposed through the "platform" sysfs attribute. The order of
+ * this array must stay in sync with multiplatform_masks[] below, as the index
+ * returned by sysfs_match_string() is used to look up the matching mask.
+ */
+static const char * const multiplatform_names[] = {
+	"windows", "winemb", "linux", "chrome",
+	"android", "macos", "ios", "webos", "tizen", NULL
+};
+
+static const u16 multiplatform_masks[] = {
+	HIDPP_MULTIPLATFORM_PLATFORM_MASK_WINDOWS,
+	HIDPP_MULTIPLATFORM_PLATFORM_MASK_WINEMB,
+	HIDPP_MULTIPLATFORM_PLATFORM_MASK_LINUX,
+	HIDPP_MULTIPLATFORM_PLATFORM_MASK_CHROME,
+	HIDPP_MULTIPLATFORM_PLATFORM_MASK_ANDROID,
+	HIDPP_MULTIPLATFORM_PLATFORM_MASK_MACOS,
+	HIDPP_MULTIPLATFORM_PLATFORM_MASK_IOS,
+	HIDPP_MULTIPLATFORM_PLATFORM_MASK_WEBOS,
+	HIDPP_MULTIPLATFORM_PLATFORM_MASK_TIZEN,
+};
+
+/**
+ * hidpp_multiplatform_errno() - Convert HID++ protocol error codes to Linux errno
+ * @err: HID++ protocol error code (positive) or Linux errno (negative or zero)
+ *
+ * Converts a HID++ protocol error code to the corresponding Linux errno. If @err is
+ * already a negative or zero Linux errno, it is returned unchanged. Otherwise, if @err
+ * is a positive HID++ error code, it is mapped to the appropriate negative Linux errno
+ * based on the HID++ specification error codes.
+ *
+ * This is used to ensure that functions interacting with the Multi-Platform feature can
+ * return consistent Linux error codes even when they encounter errors defined by the HID++
+ * protocol when the platform is set from the sysfs attribute.
+ *
+ * Return: Negative Linux errno corresponding to the HID++ error code, or @err if it is
+ * already a Linux errno.
+ */
+static int hidpp_multiplatform_errno(int err)
+{
+	if (err <= 0)
+		return err;
+
+	switch (err) {
+	case HIDPP20_ERROR_INVALID_ARGS:
+	case HIDPP20_ERROR_OUT_OF_RANGE:
+	case HIDPP20_ERROR_INVALID_FEATURE_INDEX:
+	case HIDPP20_ERROR_INVALID_FUNCTION_ID:
+		return -EINVAL;
+	case HIDPP20_ERROR_NOT_ALLOWED:
+		return -EPERM;
+	case HIDPP20_ERROR_BUSY:
+		return -EBUSY;
+	case HIDPP20_ERROR_UNSUPPORTED:
+		return -EOPNOTSUPP;
+	case HIDPP20_ERROR_HW_ERROR:
+	case HIDPP20_ERROR_UNKNOWN:
+	default:
+		return -EIO;
+	}
+}
+
+/**
+ * hidpp_multiplatform_get_num_pdesc() - Retrieve number of platform descriptors
+ * @hidpp: Pointer to the hidpp_device instance
+ * @feat_index: Feature index of the Multi-Platform feature
+ * @num_desc: Pointer to store the number of platform descriptors
+ *
+ * Retrieves the number of platform descriptors supported by the device through
+ * the Multi-Platform feature and stores it in @num_desc.
+ *
+ * Return: 0 on success, or a negative Linux errno on failure.
+ */
+static int hidpp_multiplatform_get_num_pdesc(struct hidpp_device *hidpp,
+					     u8 feat_index, u8 *num_desc)
+{
+	int ret;
+	struct hidpp_report response;
+	struct hid_device *hdev = hidpp->hid_dev;
+
+	ret = hidpp_send_fap_command_sync(hidpp, feat_index,
+					  HIDPP_MULTIPLATFORM_GET_FEATURE_INFO,
+					  NULL, 0, &response);
+	if (ret) {
+		hid_warn(hdev, "Multiplatform: GET_FEATURE_INFO failed (err=%d)", ret);
+		return hidpp_multiplatform_errno(ret);
+	}
+
+	*num_desc = response.fap.params[3];
+	hid_dbg(hdev, "Multiplatform: Device supports %d platform descriptors", *num_desc);
+
+	return 0;
+}
+
+/**
+ * hidpp_multiplatform_get_platform_desc() - Retrieve a platform descriptor entry
+ * @hidpp: Pointer to the hidpp_device instance
+ * @feat_index: Feature index of the Multi-Platform feature
+ * @platform_idx: Index of the platform descriptor to retrieve
+ * @pdesc: Pointer to store the retrieved platform descriptor
+ *
+ * Retrieves a single platform descriptor identified by @platform_idx from the
+ * device and stores the parsed descriptor fields in @pdesc.
+ *
+ * Return: 0 on success, or a negative Linux errno on failure.
+ */
+static int hidpp_multiplatform_get_platform_desc(struct hidpp_device *hidpp, u8 feat_index,
+						 u8 platform_idx, struct hidpp_platform_desc *pdesc)
+{
+	int ret;
+	struct hidpp_report response;
+	u8 params[1] = { platform_idx };
+	struct hid_device *hdev = hidpp->hid_dev;
+
+	ret = hidpp_send_fap_command_sync(hidpp, feat_index,
+					  HIDPP_MULTIPLATFORM_GET_PLATFORM_DESCRIPTOR,
+					  params, sizeof(params), &response);
+
+	if (ret) {
+		hid_warn(hdev,
+			 "Multiplatform: GET_PLATFORM_DESCRIPTOR failed for index %d (err=%d)",
+			 platform_idx, ret);
+		return hidpp_multiplatform_errno(ret);
+	}
+
+	pdesc->plat_idx = response.fap.params[0];
+	pdesc->desc_idx = response.fap.params[1];
+	pdesc->plat_mask = get_unaligned_be16(&response.fap.params[2]);
+
+	hid_dbg(hdev,
+		"Multiplatform: descriptor %d: plat_idx=%d, desc_idx=%d, plat_mask=0x%04x",
+		platform_idx, pdesc->plat_idx, pdesc->desc_idx, pdesc->plat_mask);
+
+	return 0;
+}
+
+/**
+ * hidpp_multiplatform_get_platform_index() - Find platform index for a mask
+ * @hidpp: Pointer to the hidpp_device instance
+ * @feat_index: Feature index of the Multi-Platform feature
+ * @plat_mask: Platform mask to search for
+ * @plat_index: Pointer to store the matched platform index
+ *
+ * Iterates through all platform descriptors exposed by the device via the
+ * Multi-Platform feature, retrieving each descriptor and comparing its
+ * platform mask to @plat_mask. A descriptor matches if its mask overlaps with
+ * the requested @plat_mask (i.e. (pdesc.plat_mask & plat_mask) is non-zero).
+ *
+ * When a matching descriptor is found, its platform index (plat_idx) is
+ * written to @plat_index and the function returns success.
+ *
+ * Return: 0 on success; -EOPNOTSUPP if the device exposes no descriptor
+ *         matching @plat_mask; or another negative Linux errno on transport
+ *         failure.
+ */
+static int hidpp_multiplatform_get_platform_index(struct hidpp_device *hidpp,
+						  u8 feat_index, u16 plat_mask,
+						  u8 *plat_index)
+{
+	int i;
+	int ret;
+	u8 num_desc;
+	struct hidpp_platform_desc pdesc;
+	struct hid_device *hdev = hidpp->hid_dev;
+
+	ret = hidpp_multiplatform_get_num_pdesc(hidpp, feat_index, &num_desc);
+	if (ret)
+		return ret;
+
+	for (i = 0; i < num_desc; i++) {
+		ret = hidpp_multiplatform_get_platform_desc(hidpp, feat_index, i, &pdesc);
+		if (ret)
+			return ret;
+
+		if (pdesc.plat_mask & plat_mask) {
+			*plat_index = pdesc.plat_idx;
+			hid_dbg(hdev,
+				"Multiplatform: Selected platform index %d for mask 0x%04x",
+				*plat_index, plat_mask);
+			return 0;
+		}
+	}
+
+	hid_dbg(hdev,
+		"Multiplatform: No matching platform descriptor for mask 0x%04x",
+		plat_mask);
+	return -EOPNOTSUPP;
+}
+
+/**
+ * hidpp_multiplatform_update_device_platform() - Update the device platform
+ * @hidpp: Pointer to the hidpp_device instance
+ * @feat_index: Feature index of the Multi-Platform feature
+ * @plat_index: Platform index to set on the device
+ *
+ * Sends the HID++ Multi-Platform 'SET_CURRENT_PLATFORM' command to the device to
+ * update its platform index to @plat_index.
+ *
+ * Return: 0 on success, or a negative Linux errno on failure.
+ */
+static int hidpp_multiplatform_update_device_platform(struct hidpp_device *hidpp,
+						      u8 feat_index, u8 plat_index)
+{
+	int ret;
+	struct hidpp_report response;
+	/* Byte 0 (hostIndex): 0xFF selects the current host. */
+	u8 params[2] = { 0xFF, plat_index };
+
+	ret = hidpp_send_fap_command_sync(hidpp, feat_index,
+					  HIDPP_MULTIPLATFORM_SET_CURRENT_PLATFORM,
+					  params, sizeof(params), &response);
+
+	if (ret)
+		hid_warn(hidpp->hid_dev,
+			 "Multiplatform: SET_CURRENT_PLATFORM failed for index %d (err=%d)",
+			 plat_index, ret);
+
+	return hidpp_multiplatform_errno(ret);
+}
+
+/**
+ * hidpp_multiplatform_set_platform() - Apply a platform to the device
+ * @hidpp: Pointer to the hidpp_device instance
+ * @mask: A single HIDPP_MULTIPLATFORM_PLATFORM_MASK_* bit to apply
+ *
+ * Looks up the device's platform descriptor whose platform mask matches @mask
+ * and instructs the device to switch to it via SET_CURRENT_PLATFORM.
+ *
+ * Return: 0 on success, -EOPNOTSUPP if the device does not implement feature
+ *         0x4531 or exposes no descriptor matching @mask, or another negative
+ *         Linux errno from the underlying HID++ command.
+ */
+static int hidpp_multiplatform_set_platform(struct hidpp_device *hidpp, u16 mask)
+{
+	u8 plat_index;
+	int ret;
+
+	if (!hidpp->multiplatform_feature_index)
+		return -EOPNOTSUPP;
+
+	ret = hidpp_multiplatform_get_platform_index(hidpp,
+			hidpp->multiplatform_feature_index, mask, &plat_index);
+	if (ret)
+		return ret;
+
+	ret = hidpp_multiplatform_update_device_platform(hidpp,
+			hidpp->multiplatform_feature_index, plat_index);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+/**
+ * platform_store() - Set the device platform based on user input
+ * @dev: Pointer to the device instance
+ * @attr: Pointer to the device attribute
+ * @buf: Buffer containing the platform name string
+ * @count: Size of the input buffer
+ *
+ * Parses the platform name from the input buffer, converts it to a platform mask,
+ * and applies it to the device using the HID++ Multi-Platform feature. The function
+ * handles errors gracefully, returning appropriate Linux errno values if the input
+ * is invalid or if the device does not support the requested platform.
+ *
+ * Return: Number of bytes consumed from the input buffer on success, or a negative
+ * Linux errno on failure.
+ */
+static ssize_t platform_store(struct device *dev,
+			      struct device_attribute *attr,
+			      const char *buf, size_t count)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct hidpp_device *hidpp = hid_get_drvdata(hdev);
+	char platform[16];
+	int idx;
+	int ret;
+
+	strscpy(platform, buf, sizeof(platform));
+	string_lower(platform, platform);
+
+	idx = sysfs_match_string(multiplatform_names, platform);
+	if (idx < 0)
+		return idx;
+
+	mutex_lock(&hidpp->multiplatform_lock);
+	ret = hidpp_multiplatform_set_platform(hidpp, multiplatform_masks[idx]);
+	mutex_unlock(&hidpp->multiplatform_lock);
+	if (ret)
+		return ret;
+
+	hid_dbg(hdev, "Multiplatform: Device platform set to '%s'\n",
+		multiplatform_names[idx]);
+
+	return count;
+}
+
+static DEVICE_ATTR_WO(platform);
+
+static struct attribute *multiplatform_attrs[] = {
+	&dev_attr_platform.attr,
+	NULL
+};
+
+static const struct attribute_group multiplatform_attribute_group = {
+	.attrs = multiplatform_attrs,
+};
+
+/**
+ * hidpp_multiplatform_init() - Initialize HID++ Multi-Platform support
+ * @hidpp: Pointer to the hidpp_device instance
+ *
+ * Checks if the device supports the HID++ Multi-Platform feature (0x4531) and, if so,
+ * initializes the hidpp_device structure to track the feature index and creates the
+ * corresponding sysfs attribute group for platform selection.
+ */
+static void hidpp_multiplatform_init(struct hidpp_device *hidpp)
+{
+	struct hid_device *hdev = hidpp->hid_dev;
+	u8 feat_index;
+	int ret;
+
+	ret = hidpp_root_get_feature(hidpp, HIDPP_MULTIPLATFORM_FEAT_ID, &feat_index);
+	if (ret)
+		return;
+
+	hidpp->multiplatform_feature_index = feat_index;
+
+	mutex_init(&hidpp->multiplatform_lock);
+
+	ret = sysfs_create_group(&hdev->dev.kobj, &multiplatform_attribute_group);
+	if (ret) {
+		hid_warn(hdev,
+			 "Multiplatform: Failed to create sysfs group (err=%d)\n", ret);
+		mutex_destroy(&hidpp->multiplatform_lock);
+		hidpp->multiplatform_feature_index = 0;
+	}
+}
+
+/**
+ * hidpp_multiplatform_cleanup() - Cleanup HID++ Multi-Platform support
+ * @hidpp: Pointer to the hidpp_device instance
+ * 
+ * Removes the sysfs attribute group for platform selection and destroys the mutex
+ * used for synchronizing access to the Multi-Platform feature.
+ * This function should be called during device removal or driver cleanup to ensure
+ * proper resource management.
+ */
+static void hidpp_multiplatform_cleanup(struct hidpp_device *hidpp)
+{
+	if (!hidpp->multiplatform_feature_index)
+		return;
+
+	sysfs_remove_group(&hidpp->hid_dev->dev.kobj, &multiplatform_attribute_group);
+	mutex_destroy(&hidpp->multiplatform_lock);
+}
+
 static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
 {
 	struct hidpp_device *hidpp;
@@ -4545,6 +4940,8 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
 		}
 	}
 
+	hidpp_multiplatform_init(hidpp);
+
 	/*
 	 * This relies on logi_dj_ll_close() being a no-op so that DJ connection
 	 * events will still be received.
@@ -4557,6 +4954,7 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
 hid_hw_open_fail:
 	hid_hw_stop(hdev);
 hid_hw_start_fail:
+	hidpp_multiplatform_cleanup(hidpp);
 	sysfs_remove_group(&hdev->dev.kobj, &ps_attribute_group);
 	cancel_work_sync(&hidpp->work);
 	mutex_destroy(&hidpp->send_mutex);
@@ -4570,6 +4968,7 @@ static void hidpp_remove(struct hid_device *hdev)
 	if (!hidpp)
 		return hid_hw_stop(hdev);
 
+	hidpp_multiplatform_cleanup(hidpp);
 	sysfs_remove_group(&hdev->dev.kobj, &ps_attribute_group);
 
 	hid_hw_stop(hdev);
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 57d8efdd9b89..577af0075e27 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -537,6 +537,8 @@ static const struct hid_device_id hid_have_special_driver[] = {
 #endif
 #if IS_ENABLED(CONFIG_HID_LOGITECH_HIDPP)
 	{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_LOGITECH_G920_WHEEL) },
+	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_LOGITECH_CASA_KEYS_KEYBOARD) },
+	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_LOGITECH_MX_KEYS_S_KEYBOARD) },
 #endif
 #if IS_ENABLED(CONFIG_HID_MAGICMOUSE)
 	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_APPLE, USB_DEVICE_ID_APPLE_MAGICMOUSE) },
-- 
2.34.1


^ permalink raw reply related

* Re: [PATCH bpf-next v4 0/3] HID: bpf: Fix hid_bpf_get_data() range check
From: Benjamin Tissoires @ 2026-07-01  8:31 UTC (permalink / raw)
  To: Jiri Kosina, bpf, linux-input, Yiyang Chen
  Cc: Shuah Khan, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Martin KaFai Lau, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Song Liu, Yonghong Song, Jiri Olsa, linux-kselftest, linux-kernel
In-Reply-To: <20260623062315.2694160-1-chenyy23@mails.tsinghua.edu.cn>

On Tue, 23 Jun 2026 06:23:12 +0000, Yiyang Chen wrote:
> hid_bpf_get_data() exposes a pointer into the HID-BPF context data when
> the caller-provided offset and size fit inside ctx->allocated_size.
> The helper currently checks that range with:
> 
>   rdwr_buf_size + offset > ctx->allocated_size
> 
> Since both operands are unsigned, a very large size can wrap the sum and
> make an out-of-range request look valid.
> 
> [...]

Applied to https://git.kernel.org/pub/scm/linux/kernel/git/hid/hid.git (for-7.2/upstream-fixes), thanks!

[1/3] HID: bpf: Fix hid_bpf_get_data() range check
      https://git.kernel.org/hid/hid/c/2d044049421d
[2/3] selftests/hid: Load only requested struct_ops maps
      https://git.kernel.org/hid/hid/c/5aad55011a37
[3/3] selftests/hid: Cover hid_bpf_get_data() size overflow
      https://git.kernel.org/hid/hid/c/eebbef7c468a

Cheers,
-- 
Benjamin Tissoires <bentiss@kernel.org>


^ permalink raw reply

* Re: [PATCH] [RFC] gpiolib: introduce gpio_name() helper
From: Arnd Bergmann @ 2026-07-01  8:31 UTC (permalink / raw)
  To: Geert Uytterhoeven
  Cc: Arnd Bergmann, Linus Walleij, Bartosz Golaszewski,
	Marcel Holtmann, MyungJoo Ham, Chanwoo Choi, Geert Uytterhoeven,
	Andy Shevchenko, Dmitry Torokhov, Ulf Hansson, linux-bluetooth,
	linux-kernel, open list:GPIO SUBSYSTEM, dri-devel, linux-i2c,
	linux-iio, linux-input, linux-mmc @ vger . kernel . org,
	linux-arm-kernel, linux-pm, linux-usb
In-Reply-To: <CAMuHMdWhv8i6tkmOJU_ee9LAV7mMcQHe9FXgqTHCjGiEMWvn8Q@mail.gmail.com>

On Tue, Jun 30, 2026, at 18:01, Geert Uytterhoeven wrote:
> On Mon, 29 Jun 2026 at 19:54, Arnd Bergmann <arnd@arndb.de> wrote:
>> On Mon, Jun 29, 2026, at 17:29, Geert Uytterhoeven wrote:

>> > Same results for instantiation using sysfs or configfs[1], although
>> > the latter does have optional support for specifying the name.
>>
>> I wonder how many of the other instances have the same problem
>> then. Would it be appropriate for gpiochip_fwd_desc_add() to set
>> a name itself to address this one?
>
> I don't think it would be appropriate for the GPIO aggregator to set
> that name.  What we want to print here (for debugging) is the physical
> GPIO that an aggregator's GPIO is mapped to, not some consumer or line
> name (which is not guaranteed to be unique).

Ok.

> E.g. "<chip-name>.<offset>" would be fine.  As gpiod_name() can only
> return a fixed string or an existing string, it can't return such a
> formatted string, though. And consumers don't have access to chip info?

The gpiod_hwgpio() function is exported to consumers, so they can
already print that instead of desc_to_gpio() if the local number
is sufficient. If we really care about the <chip-name> portion for
any of the debug prints, we could export another function that
returns maybe gpiod_to_chip(gpiod)->label or
dev_name(&gpiod_to_gpio_device(gpiod)->dev), which are both
constant strings we can print.

It looks like we can also replace most of the remaining callers
of desc_to_gpio() with gpiod_hwgpio().

       Arnd

^ permalink raw reply

* [linux-next:master] BUILD REGRESSION be5c93fa674f0fc3c8f359c2143abce6bbb422e6
From: kernel test robot @ 2026-07-01 10:12 UTC (permalink / raw)
  To: Andrew Morton
  Cc: Linux Memory Management List, apparmor, linux-arm-kernel,
	linux-input, linux-remoteproc, linux-rockchip, linux-usb,
	virtualization, Mark Brown

tree/branch: https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git master
branch HEAD: be5c93fa674f0fc3c8f359c2143abce6bbb422e6  Add linux-next specific files for 20260630

Error/Warning (recently discovered and may have been fixed):

    https://lore.kernel.org/oe-kbuild-all/202607010945.6lDjBFTU-lkp@intel.com
    https://lore.kernel.org/oe-kbuild-all/202607011210.PI7XDDb1-lkp@intel.com

    drivers/remoteproc/remoteproc_internal.h:131:5: error: incompatible integer to pointer conversion assigning to 'void *' from 'int' [-Wint-conversion]
    drivers/remoteproc/remoteproc_internal.h:131:7: error: call to undeclared function 'ioremap_prot'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
    drivers/remoteproc/remoteproc_internal.h:146:2: error: call to undeclared function 'iounmap'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
    mm/hugetlb.c:3095:4: warning: array subscript -1 is below array bounds of 'struct list_head[1]' [-Warray-bounds]

Unverified Error/Warning (likely false positive, kindly check if interested):

    https://lore.kernel.org/oe-kbuild/202607010826.2wLg1q08-lkp@intel.com
    https://lore.kernel.org/oe-kbuild/202607011451.icwPyCIk-lkp@intel.com

    drivers/hid/hid-asus.c:620 asus_kbd_backlight_work() error: calling 'spin_unlock_irqrestore()' with bogus flags
    drivers/i2c/busses/i2c-rk3x.c:1131 rk3x_i2c_xfer_common() error: calling 'spin_unlock_irqrestore()' with bogus flags
    drivers/rpmsg/rpmsg_char.c:236 rpmsg_eptdev_read_iter() error: calling 'spin_unlock_irqrestore()' with bogus flags
    drivers/usb/host/ehci-hub.c:1080 ehci_hub_control() error: calling 'spin_unlock_irqrestore()' with bogus flags
    drivers/usb/host/ehci-hub.c:691 ehci_hub_status_data() error: calling 'spin_unlock_irqrestore()' with bogus flags
    drivers/usb/host/ehci-sched.c:1290 itd_urb_transaction() error: calling 'spin_unlock_irqrestore()' with bogus flags
    drivers/usb/host/ehci-sched.c:2093 sitd_urb_transaction() error: calling 'spin_unlock_irqrestore()' with bogus flags
    drivers/virtio/virtio_input.c:333 virtinput_probe() error: calling 'spin_unlock_irqrestore()' with bogus flags
    drivers/virtio/virtio_input.c:56 virtinput_recv_events() error: calling 'spin_unlock_irqrestore()' with bogus flags
    security/apparmor/apparmorfs.c:590 policy_update() warn: passing zero to 'PTR_ERR'

Error/Warning ids grouped by kconfigs:

recent_errors
|-- s390-randconfig-002-20260701
|   |-- drivers-remoteproc-remoteproc_internal.h:error:call-to-undeclared-function-ioremap_prot-ISO-C99-and-later-do-not-support-implicit-function-declarations
|   |-- drivers-remoteproc-remoteproc_internal.h:error:call-to-undeclared-function-iounmap-ISO-C99-and-later-do-not-support-implicit-function-declarations
|   `-- drivers-remoteproc-remoteproc_internal.h:error:incompatible-integer-to-pointer-conversion-assigning-to-void-from-int
|-- sparc-randconfig-r131-20260701
|   `-- mm-hugetlb.c:warning:array-subscript-is-below-array-bounds-of-struct-list_head
`-- x86_64-randconfig-161-20260701
    |-- drivers-hid-hid-asus.c-asus_kbd_backlight_work()-error:calling-spin_unlock_irqrestore()-with-bogus-flags
    |-- drivers-i2c-busses-i2c-rk3x.c-rk3x_i2c_xfer_common()-error:calling-spin_unlock_irqrestore()-with-bogus-flags
    |-- drivers-rpmsg-rpmsg_char.c-rpmsg_eptdev_read_iter()-error:calling-spin_unlock_irqrestore()-with-bogus-flags
    |-- drivers-usb-host-ehci-hub.c-ehci_hub_control()-error:calling-spin_unlock_irqrestore()-with-bogus-flags
    |-- drivers-usb-host-ehci-hub.c-ehci_hub_status_data()-error:calling-spin_unlock_irqrestore()-with-bogus-flags
    |-- drivers-usb-host-ehci-sched.c-itd_urb_transaction()-error:calling-spin_unlock_irqrestore()-with-bogus-flags
    |-- drivers-usb-host-ehci-sched.c-sitd_urb_transaction()-error:calling-spin_unlock_irqrestore()-with-bogus-flags
    |-- drivers-virtio-virtio_input.c-virtinput_probe()-error:calling-spin_unlock_irqrestore()-with-bogus-flags
    |-- drivers-virtio-virtio_input.c-virtinput_recv_events()-error:calling-spin_unlock_irqrestore()-with-bogus-flags
    `-- security-apparmor-apparmorfs.c-policy_update()-warn:passing-zero-to-PTR_ERR

elapsed time: 823m

configs tested: 227
configs skipped: 6

tested configs:
alpha                             allnoconfig    gcc-16.1.0
alpha                            allyesconfig    gcc-16.1.0
alpha                               defconfig    gcc-16.1.0
arc                              allmodconfig    clang-23
arc                              allmodconfig    gcc-16.1.0
arc                               allnoconfig    gcc-16.1.0
arc                              allyesconfig    clang-23
arc                              allyesconfig    gcc-16.1.0
arc                                 defconfig    gcc-16.1.0
arc                   randconfig-001-20260701    gcc-12.5.0
arc                   randconfig-002-20260701    gcc-12.5.0
arm                               allnoconfig    clang-17
arm                               allnoconfig    gcc-16.1.0
arm                              allyesconfig    clang-23
arm                              allyesconfig    gcc-16.1.0
arm                                 defconfig    clang-23
arm                                 defconfig    gcc-16.1.0
arm                   randconfig-001-20260701    gcc-12.5.0
arm                   randconfig-002-20260701    gcc-12.5.0
arm                   randconfig-003-20260701    gcc-12.5.0
arm                   randconfig-004-20260701    gcc-12.5.0
arm64                            allmodconfig    clang-23
arm64                             allnoconfig    gcc-16.1.0
arm64                               defconfig    gcc-16.1.0
arm64                 randconfig-001-20260701    gcc-12.5.0
arm64                 randconfig-001-20260701    gcc-16.1.0
arm64                 randconfig-002-20260701    clang-17
arm64                 randconfig-002-20260701    gcc-12.5.0
arm64                 randconfig-003-20260701    gcc-12.5.0
arm64                 randconfig-004-20260701    clang-17
arm64                 randconfig-004-20260701    gcc-12.5.0
csky                             allmodconfig    gcc-16.1.0
csky                              allnoconfig    gcc-16.1.0
csky                                defconfig    gcc-16.1.0
csky                  randconfig-001-20260701    gcc-10.5.0
csky                  randconfig-001-20260701    gcc-12.5.0
csky                  randconfig-002-20260701    gcc-12.5.0
hexagon                          allmodconfig    clang-23
hexagon                          allmodconfig    gcc-16.1.0
hexagon                           allnoconfig    clang-23
hexagon                           allnoconfig    gcc-16.1.0
hexagon                             defconfig    clang-23
hexagon                             defconfig    gcc-16.1.0
hexagon                        randconfig-001    gcc-11.5.0
hexagon               randconfig-001-20260701    clang-23
hexagon               randconfig-001-20260701    gcc-11.5.0
hexagon                        randconfig-002    gcc-11.5.0
hexagon               randconfig-002-20260701    clang-17
hexagon               randconfig-002-20260701    gcc-11.5.0
i386                             allmodconfig    clang-22
i386                             allmodconfig    gcc-14
i386                              allnoconfig    gcc-14
i386                              allnoconfig    gcc-16.1.0
i386                             allyesconfig    clang-22
i386                             allyesconfig    gcc-14
i386        buildonly-randconfig-001-20260701    clang-22
i386        buildonly-randconfig-002-20260701    clang-22
i386        buildonly-randconfig-003-20260701    clang-22
i386        buildonly-randconfig-004-20260701    clang-22
i386        buildonly-randconfig-005-20260701    clang-22
i386        buildonly-randconfig-006-20260701    clang-22
i386                                defconfig    clang-22
i386                                defconfig    gcc-16.1.0
i386                  randconfig-001-20260701    clang-22
i386                  randconfig-002-20260701    clang-22
i386                  randconfig-003-20260701    clang-22
i386                  randconfig-004-20260701    clang-22
i386                  randconfig-005-20260701    clang-22
i386                  randconfig-006-20260701    clang-22
i386                  randconfig-007-20260701    clang-22
i386                  randconfig-011-20260701    gcc-14
i386                  randconfig-012-20260701    gcc-14
i386                  randconfig-013-20260701    gcc-14
i386                  randconfig-014-20260701    gcc-14
i386                  randconfig-015-20260701    gcc-14
i386                  randconfig-016-20260701    gcc-14
i386                  randconfig-017-20260701    gcc-14
loongarch                        allmodconfig    clang-19
loongarch                        allmodconfig    clang-23
loongarch                         allnoconfig    clang-20
loongarch                         allnoconfig    gcc-16.1.0
loongarch                           defconfig    clang-23
loongarch                      randconfig-001    gcc-11.5.0
loongarch             randconfig-001-20260701    clang-23
loongarch             randconfig-001-20260701    gcc-11.5.0
loongarch                      randconfig-002    gcc-11.5.0
loongarch             randconfig-002-20260701    clang-19
loongarch             randconfig-002-20260701    gcc-11.5.0
m68k                             allmodconfig    gcc-16.1.0
m68k                              allnoconfig    gcc-16.1.0
m68k                             allyesconfig    clang-23
m68k                             allyesconfig    gcc-16.1.0
m68k                                defconfig    clang-23
m68k                                defconfig    gcc-16.1.0
microblaze                        allnoconfig    gcc-16.1.0
microblaze                       allyesconfig    gcc-16.1.0
microblaze                          defconfig    clang-23
microblaze                          defconfig    gcc-16.1.0
mips                             allmodconfig    gcc-16.1.0
mips                              allnoconfig    gcc-16.1.0
mips                             allyesconfig    gcc-16.1.0
mips                            gpr_defconfig    clang-23
nios2                            allmodconfig    clang-20
nios2                            allmodconfig    gcc-11.5.0
nios2                             allnoconfig    clang-23
nios2                             allnoconfig    gcc-11.5.0
nios2                               defconfig    clang-23
nios2                               defconfig    gcc-11.5.0
nios2                          randconfig-001    gcc-11.5.0
nios2                 randconfig-001-20260701    gcc-11.5.0
nios2                          randconfig-002    gcc-11.5.0
nios2                 randconfig-002-20260701    gcc-11.5.0
nios2                 randconfig-002-20260701    gcc-8.5.0
openrisc                         allmodconfig    clang-20
openrisc                         allmodconfig    gcc-16.1.0
openrisc                          allnoconfig    clang-23
openrisc                          allnoconfig    gcc-16.1.0
openrisc                            defconfig    gcc-16.1.0
parisc                           allmodconfig    gcc-16.1.0
parisc                            allnoconfig    clang-23
parisc                            allnoconfig    gcc-16.1.0
parisc                           allyesconfig    clang-17
parisc                           allyesconfig    gcc-16.1.0
parisc                              defconfig    gcc-16.1.0
parisc                randconfig-001-20260701    clang-17
parisc                randconfig-001-20260701    gcc-13.4.0
parisc                randconfig-002-20260701    clang-17
parisc                randconfig-002-20260701    gcc-15.2.0
parisc64                            defconfig    clang-23
parisc64                            defconfig    gcc-16.1.0
powerpc                          allmodconfig    gcc-16.1.0
powerpc                           allnoconfig    clang-23
powerpc                           allnoconfig    gcc-16.1.0
powerpc               randconfig-001-20260701    clang-17
powerpc               randconfig-001-20260701    gcc-8.5.0
powerpc               randconfig-002-20260701    clang-17
powerpc               randconfig-002-20260701    clang-23
powerpc64             randconfig-001-20260701    clang-17
powerpc64             randconfig-002-20260701    clang-17
riscv                            allmodconfig    clang-23
riscv                             allnoconfig    clang-23
riscv                             allnoconfig    gcc-16.1.0
riscv                            allyesconfig    clang-23
riscv                               defconfig    gcc-16.1.0
riscv                 randconfig-001-20260701    clang-23
riscv                 randconfig-002-20260701    clang-23
s390                             allmodconfig    clang-17
s390                             allmodconfig    clang-23
s390                              allnoconfig    clang-23
s390                             allyesconfig    gcc-16.1.0
s390                                defconfig    gcc-16.1.0
s390                  randconfig-001-20260701    clang-23
s390                  randconfig-002-20260701    clang-23
sh                               allmodconfig    gcc-16.1.0
sh                                allnoconfig    clang-23
sh                                allnoconfig    gcc-16.1.0
sh                               allyesconfig    clang-17
sh                               allyesconfig    gcc-16.1.0
sh                                  defconfig    gcc-14
sh                    randconfig-001-20260701    clang-23
sh                    randconfig-002-20260701    clang-23
sparc                             allnoconfig    clang-23
sparc                             allnoconfig    gcc-16.1.0
sparc                               defconfig    gcc-16.1.0
sparc                 randconfig-001-20260701    gcc-13.4.0
sparc                 randconfig-002-20260701    gcc-13.4.0
sparc64                          allmodconfig    clang-20
sparc64                             defconfig    gcc-14
sparc64               randconfig-001-20260701    gcc-13.4.0
sparc64               randconfig-002-20260701    gcc-13.4.0
um                               allmodconfig    clang-17
um                                allnoconfig    clang-17
um                                allnoconfig    clang-23
um                               allyesconfig    gcc-14
um                               allyesconfig    gcc-16.1.0
um                                  defconfig    gcc-14
um                             i386_defconfig    gcc-14
um                    randconfig-001-20260701    gcc-13.4.0
um                    randconfig-002-20260701    gcc-13.4.0
um                           x86_64_defconfig    gcc-14
x86_64                           allmodconfig    clang-22
x86_64                            allnoconfig    clang-22
x86_64                            allnoconfig    clang-23
x86_64                           allyesconfig    clang-22
x86_64      buildonly-randconfig-001-20260701    clang-22
x86_64      buildonly-randconfig-001-20260701    gcc-14
x86_64      buildonly-randconfig-002-20260701    clang-22
x86_64      buildonly-randconfig-003-20260701    clang-22
x86_64      buildonly-randconfig-004-20260701    clang-22
x86_64      buildonly-randconfig-004-20260701    gcc-14
x86_64      buildonly-randconfig-005-20260701    clang-22
x86_64      buildonly-randconfig-006-20260701    clang-22
x86_64                              defconfig    gcc-14
x86_64                                  kexec    clang-22
x86_64                randconfig-001-20260701    clang-22
x86_64                randconfig-001-20260701    gcc-14
x86_64                randconfig-002-20260701    gcc-14
x86_64                randconfig-003-20260701    gcc-14
x86_64                randconfig-004-20260701    clang-22
x86_64                randconfig-004-20260701    gcc-14
x86_64                randconfig-005-20260701    gcc-14
x86_64                randconfig-006-20260701    gcc-14
x86_64                randconfig-011-20260701    gcc-14
x86_64                randconfig-012-20260701    gcc-14
x86_64                randconfig-013-20260701    gcc-14
x86_64                randconfig-014-20260701    gcc-14
x86_64                randconfig-015-20260701    gcc-14
x86_64                randconfig-016-20260701    gcc-14
x86_64                randconfig-071-20260701    gcc-14
x86_64                randconfig-072-20260701    gcc-14
x86_64                randconfig-073-20260701    gcc-14
x86_64                randconfig-074-20260701    gcc-14
x86_64                randconfig-075-20260701    gcc-14
x86_64                randconfig-076-20260701    gcc-14
x86_64                               rhel-9.4    clang-22
x86_64                           rhel-9.4-bpf    gcc-14
x86_64                          rhel-9.4-func    clang-22
x86_64                    rhel-9.4-kselftests    clang-22
x86_64                         rhel-9.4-kunit    gcc-14
x86_64                           rhel-9.4-ltp    gcc-14
x86_64                          rhel-9.4-rust    clang-22
xtensa                            allnoconfig    clang-23
xtensa                            allnoconfig    gcc-16.1.0
xtensa                           allyesconfig    clang-20
xtensa                           allyesconfig    gcc-16.1.0
xtensa                randconfig-001-20260701    gcc-13.4.0
xtensa                randconfig-002-20260701    gcc-13.4.0

--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

^ permalink raw reply

* Re: [PATCH 07/10] HID: apple: Add support for DockChannel HID keyboards
From: Michael Reeves @ 2026-07-01 14:01 UTC (permalink / raw)
  To: Sasha Finkelstein
  Cc: Sven Peter, Janne Grunau, Neal Gompa, Jassi Brar, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Hector Martin,
	Joerg Roedel (AMD), Will Deacon, Robin Murphy, Dmitry Torokhov,
	Jiri Kosina, Benjamin Tissoires, asahi, linux-arm-kernel,
	linux-kernel, devicetree, iommu, linux-input
In-Reply-To: <B22C8EFD-DEDE-49C1-AD8E-BAEB5C91B6A6@chaosmail.tech>

On Tue, Jun 30, 2026 at 11:41 PM Sasha Finkelstein <k@chaosmail.tech> wrote:
[...]
> It looks like this section is duplicated in the following commit (8).
> Is that correct?
>
Yes, it is duplicated, thank you for the pick up. I moved the fixup to
the transport layer driver (in the following commit 8), which works
better, but must have forgotten to delete it here.

I will correct this in v2.

Thank you again!

^ permalink raw reply

* Re: [PATCH] HID: asus: add new Asus EC hid device for keyboard backlight and FN HotKeys
From: Jiri Kosina @ 2026-07-01 14:12 UTC (permalink / raw)
  To: Alexandru Serdeliuc; +Cc: Benjamin Tissoires, linux-kernel, linux-input
In-Reply-To: <9fc25f84-b8b1-4ffa-a79e-ff95020ec45f@yahoo.com>

On Tue, 30 Jun 2026, Alexandru Serdeliuc wrote:

> Hi Jiri, Benjamin,
> 
> Enquiring if anyone has had a chance to look over this patch series from
> December?
> 
> To provide a bit of architectural context, this is a 100% pure HID client
> driver.
> 
> This laptop (Asus Zenbook A14 UX3407QA) runs on the Qualcomm ARM64 Snapdragon
> platform.
> 
> Because it is an ARM device, it does not use traditional legacy x86 ACPI/WMI
> code.
> 
> Instead, the Fn hotkeys and keyboard backlight controls are routed completely
> through standard vendor-specific HID reports (using Report ID 0x5A) over the
> existing I2C-HID transport layer.
> 
> It requires absolutely zero Device Tree modifications or external platform
> subsystem hooks to bind or function.
> 
> The driver matches strictly on Vendor/Product ID (0x0B05 / 0x0220) passed
> directly through the core HID subsystem.
> 
> I would highly appreciate any architectural feedback or a quick review!

Hi Alexandru,

first - sorry for not having handled this, it somehow fell in between 
cracks.

I have just one question -- wouldn't it be better to have this device 
handled as part of generic hid-asus driver? Giving too many Kconfig 
configurations for every specific model feels a little bit too 
overwhelming.

(yes, we'd need to have hid-asus depend on I2C ... I don't think that's a 
big issue).

Thanks, and sorry again,

-- 
Jiri Kosina
SUSE Labs


^ permalink raw reply

* Re: [PATCH 08/10] HID: apple: Add DockChannel HID transport driver
From: Michael Reeves @ 2026-07-01 14:32 UTC (permalink / raw)
  To: Yureka Lilian
  Cc: Sven Peter, Janne Grunau, Neal Gompa, Jassi Brar, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Hector Martin,
	Joerg Roedel (AMD), Will Deacon, Robin Murphy, Dmitry Torokhov,
	Jiri Kosina, Benjamin Tissoires, asahi, linux-arm-kernel,
	linux-kernel, devicetree, iommu, linux-input
In-Reply-To: <05d39c4d-8405-4fe7-a798-c8a9c92699ed@cyberchaos.dev>

Hi Yureka,

Thank you for the review and the feedback.

On Tue, Jun 30, 2026 at 11:21 PM Yureka Lilian <yureka@cyberchaos.dev> wrote:
[...]
> > +     strscpy(hid->uniq, dchid->serial, sizeof(hid->uniq));
> If the keyboard appeared before stm, dchip->serial might be uninitialized.
Yes, true, thank you for picking this up!
[...]
> > +     /*
> > +      * Prefer to enable STM first, since it provides device IDs. Some
> > +      * firmware versions do not expose STM, so let the keyboard start
> > +      * without it.
> > +      */
> > +     if (iface->dchid->id_ready || !strcmp(iface->name, "stm") ||
> > +         !strcmp(iface->name, "keyboard"))
>
> I specifically asked for a mechanism to let the keyboard probe even on
> devices which do not expose/have stm. Thanks for adding that!
> However, I think this might need some more sophisticated mechanism to
> decide whether the stm is still going to appear after the keyboard, or
> not at all. I'm not sure if there is a way to tell this at this point,
> or we need to add a timeout for the stm to appearing, which needs to
> expire before we create the other interfaces with fake serials.
>
I do not think there is a way to tell at this point, unfortunately.

I think the best way to resolve this would be to implement a delayed work
item that is scheduled during probe. If STM appears and initalises before
the timer (I'm not sure how long it should be right now, I will prototype and
experiment) we cancel the delayed work, get the real serial, and spin up
the keyboard. If the timeout expires without seeing the STM interface, we
assume the platform does not have one, mark id_ready as true, and
proceed to init the keyboard.

I will work on this and experiment to see if it's the best solution or
if there's
any others as well as to find the ideal timeout, and submit as part of v2.
[...]

Thanks again,
Michael

^ permalink raw reply

* Re: [PATCH 02/10] dt-bindings: mailbox: apple: Add DockChannel mailbox
From: Michael Reeves @ 2026-07-01 14:35 UTC (permalink / raw)
  To: Conor Dooley
  Cc: Sven Peter, Janne Grunau, Neal Gompa, Jassi Brar, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Hector Martin,
	Joerg Roedel (AMD), Will Deacon, Robin Murphy, Dmitry Torokhov,
	Jiri Kosina, Benjamin Tissoires, asahi, linux-arm-kernel,
	linux-kernel, devicetree, iommu, linux-input
In-Reply-To: <20260630-tattling-pacify-3b990261b8b5@spud>

On Wed, Jul 1, 2026 at 3:07 AM Conor Dooley <conor@kernel.org> wrote:
[...]
> > +++ b/Documentation/devicetree/bindings/mailbox/apple,dockchannel.yaml
>
> Please name this file matching the t8112 compatible (since that's your
> fallback).
> Otherwise, this looks good.
> Reviewed-by: Conor Dooley <conor.dooley@microchip.com>
>
> pw-bot: changes-requested
Thank you, will change the name in v2.

^ permalink raw reply

* Re: [PATCH 04/10] dt-bindings: input: apple: Add DockChannel HID transport
From: Michael Reeves @ 2026-07-01 14:36 UTC (permalink / raw)
  To: Conor Dooley
  Cc: Sven Peter, Janne Grunau, Neal Gompa, Jassi Brar, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Hector Martin,
	Joerg Roedel (AMD), Will Deacon, Robin Murphy, Dmitry Torokhov,
	Jiri Kosina, Benjamin Tissoires, asahi, linux-arm-kernel,
	linux-kernel, devicetree, iommu, linux-input
In-Reply-To: <20260630-halves-magnesium-856f9c7d60b2@spud>

On Wed, Jul 1, 2026 at 3:08 AM Conor Dooley <conor@kernel.org> wrote:
[...]
> > +++ b/Documentation/devicetree/bindings/input/apple,dockchannel-hid.yaml
>
> Same thing here about the filename. Looks good otherwise, so please
> change that.
> pw-bot: changes-requested
>
> Reviewed-by: Conor Dooley <conor.dooley@microchip.com>
>
> Thanks,
> Conor.
Thanks again, will also adjust this name in v2.

^ permalink raw reply

* [PATCH 1/2] HID: multitouch: fix out-of-bounds bit access on mt_io_flags
From: Trung Nguyen @ 2026-07-01 17:13 UTC (permalink / raw)
  To: bentiss, jikos; +Cc: linux-input, linux-kernel, Trung Nguyen, stable
In-Reply-To: <CAO-hwJLS6uAX__aGkmpcDQ8v6GJaHGrL+TOYXVRX6jvjzNW+wg@mail.gmail.com>

mt_io_flags is a single unsigned long, but mt_process_slot(),
mt_release_pending_palms() and mt_release_contacts() use it as a
per-slot bitmap indexed by the slot number. That slot number is only
bounded by td->maxcontacts, which is taken from the device's
ContactCountMaximum feature report and can be up to 255, not by
BITS_PER_LONG.

As a result, a multitouch device that advertises a large contact count
makes set_bit()/clear_bit() operate past the mt_io_flags word and
corrupt the adjacent members of struct mt_device. The sticky-fingers
release timer is the easiest way to reach this. mt_release_contacts()
runs

	for (i = 0; i < mt->num_slots; i++)
		clear_bit(i, &td->mt_io_flags);

with num_slots == maxcontacts. For maxcontacts around 250 the loop
clears the bits that overlap td->applications.next, zeroing that list
head, and the list_for_each_entry() that immediately follows then
dereferences NULL. The kernel panics from timer (softirq) context. On a
KASAN build this shows up as a general protection fault in
mt_release_contacts() with a null-ptr-deref at offset 0x58, which is
offsetof(struct mt_application, num_received).

The state is reachable from an untrusted USB or Bluetooth HID
multitouch device; no local privileges are required.

Store the per-slot active state in a separately allocated bitmap sized
for maxcontacts, the same pattern already used for pending_palm_slots,
and keep only MT_IO_FLAGS_RUNNING in mt_io_flags. The two
"mt_io_flags & MT_IO_SLOTS_MASK" arming checks become
bitmap_empty(td->active_slots, td->maxcontacts).

Move MT_IO_FLAGS_RUNNING back to bit 0. It was bumped to bit 32 by the
same commit to leave the low byte for the slot bits; with the slot bits
gone it fits in bit 0 again, which also keeps it within the unsigned
long on 32-bit.

Fixes: 46f781e0d151 ("HID: multitouch: fix sticky fingers")
Cc: stable@vger.kernel.org
Signed-off-by: Trung Nguyen <trungnh@cystack.net>
---
 drivers/hid/hid-multitouch.c | 32 ++++++++++++++++++++------------
 1 file changed, 20 insertions(+), 12 deletions(-)

diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
index 0495152091e3..edb37b4c867e 100644
--- a/drivers/hid/hid-multitouch.c
+++ b/drivers/hid/hid-multitouch.c
@@ -31,6 +31,7 @@
  * [1] https://gitlab.freedesktop.org/libevdev/hid-tools
  */
 
+#include <linux/bitmap.h>
 #include <linux/bits.h>
 #include <linux/device.h>
 #include <linux/hid.h>
@@ -97,8 +98,7 @@ enum report_mode {
 	TOUCHPAD_REPORT_ALL = TOUCHPAD_REPORT_BUTTONS | TOUCHPAD_REPORT_CONTACTS,
 };
 
-#define MT_IO_SLOTS_MASK		GENMASK(7, 0) /* reserve first 8 bits for slot tracking */
-#define MT_IO_FLAGS_RUNNING		32
+#define MT_IO_FLAGS_RUNNING		0
 
 static const bool mtrue = true;		/* default for true */
 static const bool mfalse;		/* default for false */
@@ -174,10 +174,9 @@ struct mt_device {
 	struct timer_list release_timer;	/* to release sticky fingers */
 	struct hid_haptic_device *haptic;	/* haptic related configuration */
 	struct hid_device *hdev;	/* hid_device we're attached to */
-	unsigned long mt_io_flags;	/* mt flags (MT_IO_FLAGS_RUNNING)
-					 * first 8 bits are reserved for keeping the slot
-					 * states, this is fine because we only support up
-					 * to 250 slots (MT_MAX_MAXCONTACT)
+	unsigned long mt_io_flags;	/* mt flags (MT_IO_FLAGS_RUNNING) */
+	unsigned long *active_slots;	/* bitmap of slots with an active
+					 * contact, sized for maxcontacts
 					 */
 	__u8 inputmode_value;	/* InputMode HID feature value */
 	__u8 maxcontacts;
@@ -1036,7 +1035,7 @@ static void mt_release_pending_palms(struct mt_device *td,
 
 	for_each_set_bit(slotnum, app->pending_palm_slots, td->maxcontacts) {
 		clear_bit(slotnum, app->pending_palm_slots);
-		clear_bit(slotnum, &td->mt_io_flags);
+		clear_bit(slotnum, td->active_slots);
 
 		input_mt_slot(input, slotnum);
 		input_mt_report_slot_inactive(input);
@@ -1247,9 +1246,9 @@ static int mt_process_slot(struct mt_device *td, struct input_dev *input,
 		input_event(input, EV_ABS, ABS_MT_TOUCH_MAJOR, major);
 		input_event(input, EV_ABS, ABS_MT_TOUCH_MINOR, minor);
 
-		set_bit(slotnum, &td->mt_io_flags);
+		set_bit(slotnum, td->active_slots);
 	} else {
-		clear_bit(slotnum, &td->mt_io_flags);
+		clear_bit(slotnum, td->active_slots);
 	}
 
 	return 0;
@@ -1384,7 +1383,7 @@ static void mt_touch_report(struct hid_device *hid,
 	 * defect.
 	 */
 	if (app->quirks & MT_QUIRK_STICKY_FINGERS) {
-		if (td->mt_io_flags & MT_IO_SLOTS_MASK)
+		if (!bitmap_empty(td->active_slots, td->maxcontacts))
 			mod_timer(&td->release_timer,
 				  jiffies + msecs_to_jiffies(100));
 		else
@@ -1443,6 +1442,15 @@ static int mt_touch_input_configured(struct hid_device *hdev,
 	if (td->is_pressurepad)
 		__set_bit(INPUT_PROP_PRESSUREPAD, input->propbit);
 
+	if (!td->active_slots) {
+		td->active_slots = devm_kcalloc(&td->hdev->dev,
+						BITS_TO_LONGS(td->maxcontacts),
+						sizeof(long),
+						GFP_KERNEL);
+		if (!td->active_slots)
+			return -ENOMEM;
+	}
+
 	app->pending_palm_slots = devm_kcalloc(&hi->input->dev,
 					       BITS_TO_LONGS(td->maxcontacts),
 					       sizeof(long),
@@ -2062,7 +2070,7 @@ static void mt_release_contacts(struct hid_device *hid)
 			for (i = 0; i < mt->num_slots; i++) {
 				input_mt_slot(input_dev, i);
 				input_mt_report_slot_inactive(input_dev);
-				clear_bit(i, &td->mt_io_flags);
+				clear_bit(i, td->active_slots);
 			}
 			input_mt_sync_frame(input_dev);
 			input_sync(input_dev);
@@ -2085,7 +2093,7 @@ static void mt_expired_timeout(struct timer_list *t)
 	 */
 	if (test_and_set_bit_lock(MT_IO_FLAGS_RUNNING, &td->mt_io_flags))
 		return;
-	if (td->mt_io_flags & MT_IO_SLOTS_MASK)
+	if (!bitmap_empty(td->active_slots, td->maxcontacts))
 		mt_release_contacts(hdev);
 	clear_bit_unlock(MT_IO_FLAGS_RUNNING, &td->mt_io_flags);
 }
-- 
2.45.1.windows.1


^ permalink raw reply related

* [PATCH 2/2] selftests/hid: multitouch: test a large ContactCountMaximum
From: Trung Nguyen @ 2026-07-01 17:13 UTC (permalink / raw)
  To: bentiss, jikos; +Cc: linux-input, linux-kernel, Trung Nguyen
In-Reply-To: <20260701171320.16367-1-trungnh@cystack.net>

Add a regression test for the out-of-bounds bit operations on
struct mt_device.mt_io_flags.

A HID multitouch device can advertise a ContactCountMaximum far larger
than the number of contacts a single report describes, up to 255. The
driver used to keep the per-slot active state in the bits of a single
unsigned long and index set_bit()/clear_bit() by the slot number, so such
a device drove those operations out of bounds. The sticky-fingers release
timer made it fatal: mt_release_contacts() cleared one bit per slot and
overwrote the adjacent members of struct mt_device.

The new device advertises a ContactCountMaximum of 250 while exposing only
a few finger collections (a large contact count cannot be expressed with
one finger collection per contact within the HID descriptor size limit).
The test sends a single contact and lets the 100ms sticky-fingers timer
release it. A kernel without the fix panics in mt_release_contacts(); a
fixed kernel reports the release cleanly.

Signed-off-by: Trung Nguyen <trungnh@cystack.net>
---
 .../selftests/hid/tests/test_multitouch.py    | 114 ++++++++++++++++++
 1 file changed, 114 insertions(+)

diff --git a/tools/testing/selftests/hid/tests/test_multitouch.py b/tools/testing/selftests/hid/tests/test_multitouch.py
index fa4fb2054bd4..7897340118b4 100644
--- a/tools/testing/selftests/hid/tests/test_multitouch.py
+++ b/tools/testing/selftests/hid/tests/test_multitouch.py
@@ -513,6 +513,79 @@ class SmartTechDigitizer(Digitizer):
         return absinfo is not None and absinfo.resolution == 3
 
 
+class MinWin8TSParallelBigContactMax(Digitizer):
+    """A parallel Win8 touchscreen that advertises a ContactCountMaximum much
+    larger than the number of contacts it actually reports.
+
+    Such firmware makes the driver allocate that many input slots (up to 255)
+    while the input report only carries a few contacts. This is what used to
+    drive the per-slot bit operations on mt_io_flags out of bounds. The number
+    of contacts a HID report can describe is limited by the descriptor size,
+    so a large ContactCountMaximum can only be expressed this way, decoupled
+    from the number of finger collections."""
+
+    def __init__(self, n_fingers=5, contact_max=250):
+        self.phys_max = 120, 90
+        rdesc_finger_str = f"""
+            Usage Page (Digitizers)
+            Usage (Finger)
+            Collection (Logical)
+             Report Size (1)
+             Report Count (1)
+             Logical Minimum (0)
+             Logical Maximum (1)
+             Usage (Tip Switch)
+             Input (Data,Var,Abs)
+             Report Size (7)
+             Logical Maximum (127)
+             Input (Cnst,Var,Abs)
+             Report Size (8)
+             Logical Maximum (255)
+             Usage (Contact Id)
+             Input (Data,Var,Abs)
+             Report Size (16)
+             Unit Exponent (-1)
+             Unit (SILinear: cm)
+             Logical Maximum (4095)
+             Physical Minimum (0)
+             Physical Maximum ({self.phys_max[0]})
+             Usage Page (Generic Desktop)
+             Usage (X)
+             Input (Data,Var,Abs)
+             Physical Maximum ({self.phys_max[1]})
+             Usage (Y)
+             Input (Data,Var,Abs)
+            End Collection
+"""
+        rdesc_str = f"""
+           Usage Page (Digitizers)
+           Usage (Touch Screen)
+           Collection (Application)
+            Report ID (1)
+            {rdesc_finger_str * n_fingers}
+            Unit Exponent (-4)
+            Unit (SILinear: s)
+            Logical Maximum (65535)
+            Physical Maximum (65535)
+            Usage Page (Digitizers)
+            Usage (Scan Time)
+            Input (Data,Var,Abs)
+            Report Size (8)
+            Logical Maximum (255)
+            Usage (Contact Count)
+            Input (Data,Var,Abs)
+            Report ID (2)
+            Logical Maximum ({contact_max})
+            Usage (Contact Max)
+            Feature (Data,Var,Abs)
+          End Collection
+          {Digitizer.msCertificationBlob(68)}
+"""
+        super().__init__(
+            f"uhid test parallel big contact max {contact_max}", rdesc_str
+        )
+
+
 class BaseTest:
     class TestMultitouch(base.BaseTestCase.TestUhid):
         kernel_modules = [KERNEL_MODULE]
@@ -1735,6 +1808,47 @@ class TestMinWin8TSParallel(BaseTest.TestWin8Multitouch):
         return MinWin8TSParallel(10)
 
 
+class TestMinWin8TSParallelBigContactMax(base.BaseTestCase.TestUhid):
+    """Regression test for the out-of-bounds bit operations on
+    struct mt_device.mt_io_flags.
+
+    A Win8 touchscreen may advertise a ContactCountMaximum much larger than
+    the number of contacts it reports. The driver used to keep the per-slot
+    active state in the bits of a single unsigned long while indexing
+    set_bit()/clear_bit() by the slot number, so such a device drove those bit
+    operations out of bounds. The sticky-fingers release timer made it fatal:
+    mt_release_contacts() cleared one bit per slot, overwrote the adjacent
+    struct mt_device members and panicked the kernel.
+
+    Send a single contact, let the 100ms sticky-fingers timer release it, and
+    check that the kernel reports the release cleanly instead of crashing."""
+
+    kernel_modules = [KERNEL_MODULE]
+
+    def create_device(self):
+        return MinWin8TSParallelBigContactMax()
+
+    def test_sticky_fingers_release_big_contact_max(self):
+        uhdev = self.uhdev
+        evdev = uhdev.get_evdev()
+
+        assert evdev.num_slots == uhdev.max_contacts
+
+        t0 = Touch(1, 5, 10)
+        r = uhdev.event([t0])
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == 0
+
+        # do not release the contact; the sticky-fingers timer must do it
+        # after 100ms, which is where the out-of-bounds release used to hit
+        time.sleep(0.2)
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 0) in events
+        assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == -1
+
+
 class TestMinWin8TSHybrid(BaseTest.TestWin8Multitouch):
     def create_device(self):
         return MinWin8TSHybrid()
-- 
2.45.1.windows.1


^ permalink raw reply related

* Re: [PATCH 1/2] HID: multitouch: fix out-of-bounds bit access on mt_io_flags
From: Benjamin Tissoires @ 2026-07-01 19:12 UTC (permalink / raw)
  To: jikos, Trung Nguyen; +Cc: linux-input, linux-kernel, stable
In-Reply-To: <20260701171320.16367-1-trungnh@cystack.net>

On Thu, 02 Jul 2026 00:13:19 +0700, Trung Nguyen wrote:
> mt_io_flags is a single unsigned long, but mt_process_slot(),
> mt_release_pending_palms() and mt_release_contacts() use it as a
> per-slot bitmap indexed by the slot number. That slot number is only
> bounded by td->maxcontacts, which is taken from the device's
> ContactCountMaximum feature report and can be up to 255, not by
> BITS_PER_LONG.
> 
> [...]

Applied to https://git.kernel.org/pub/scm/linux/kernel/git/hid/hid.git (for-7.2/upstream-fixes), thanks!

[1/2] HID: multitouch: fix out-of-bounds bit access on mt_io_flags
      https://git.kernel.org/hid/hid/c/8813b0612275
[2/2] selftests/hid: multitouch: test a large ContactCountMaximum
      https://git.kernel.org/hid/hid/c/b6eb022890c7

Cheers,
-- 
Benjamin Tissoires <bentiss@kernel.org>


^ permalink raw reply

* [PATCH] Input: focaltech - fix array out-of-bounds in focaltech_process_rel_packet
From: Richard Davies @ 2026-07-01 19:09 UTC (permalink / raw)
  To: linux-input, Dmitry Torokhov
  Cc: Mathias Gottschlag, Hans de Goede, Richard Davies

Make finger2 (and also finger1) unsigned, so that if the finger index in
the packet is 0 then subtracting 1 creates an array index which overflows
above the existing check for FOC_MAX_FINGERS, as the existing comment says
it should, instead of writing to state->fingers[-1].

Fixes: 05be1d079ec0 ("Input: psmouse - support for the FocalTech PS/2 protocol extensions")
Signed-off-by: Richard Davies <richard@arachsys.com>
---
 drivers/input/mouse/focaltech.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/input/mouse/focaltech.c b/drivers/input/mouse/focaltech.c
index 43f9939b7c63..d3ad4af5aa09 100644
--- a/drivers/input/mouse/focaltech.c
+++ b/drivers/input/mouse/focaltech.c
@@ -197,7 +197,7 @@ static void focaltech_process_rel_packet(struct psmouse *psmouse,
 {
 	struct focaltech_data *priv = psmouse->private;
 	struct focaltech_hw_state *state = &priv->state;
-	int finger1, finger2;
+	unsigned int finger1, finger2;
 
 	state->pressed = packet[0] >> 7;
 	finger1 = ((packet[0] >> 4) & 0x7) - 1;
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH] Input: focaltech - fix array out-of-bounds in focaltech_process_rel_packet
From: Richard Davies @ 2026-07-01 19:28 UTC (permalink / raw)
  To: linux-input, Dmitry Torokhov; +Cc: Mathias Gottschlag, Hans de Goede
In-Reply-To: <20260701190932.14960-1-richard@arachsys.com>

Richard Davies wrote:
>Make finger2 (and also finger1) unsigned, so that if the finger index in
>the packet is 0 then subtracting 1 creates an array index which overflows
>above the existing check for FOC_MAX_FINGERS, as the existing comment says
>it should, instead of writing to state->fingers[-1].

Some further context for my patch...

I get errors such as the following on my laptop running Ubuntu 26.04 LTS:

[   52.422376] ------------[ cut here ]------------
[   52.422381] UBSAN: array-index-out-of-bounds in /build/linux-IJm0IA/linux-7.0.0/drivers/input/mouse/focaltech.c:221:17
[   52.422386] index -1 is out of range for type 'focaltech_finger_state [5]'
[   52.422389] CPU: 3 UID: 0 PID: 0 Comm: swapper/3 Tainted: G S                  7.0.0-27-generic #27-Ubuntu PREEMPT(lazy) 
[   52.422392] Tainted: [S]=CPU_OUT_OF_SPEC
[   52.422393] Hardware name: ASUSTeK COMPUTER INC. N550JK/N550JK, BIOS N550JK.208 09/26/2014
[   52.422395] Call Trace:
[   52.422396]  <IRQ>
[   52.422399]  show_stack+0x49/0x60
[   52.422405]  dump_stack_lvl+0x5f/0x90
[   52.422409]  dump_stack+0x10/0x18
[   52.422410]  ubsan_epilogue+0x9/0x39
[   52.422416]  __ubsan_handle_out_of_bounds.cold+0x50/0x55
[   52.422421]  focaltech_process_packet+0x541/0x560 [psmouse]
[   52.422435]  focaltech_process_byte+0x23/0x30 [psmouse]
[   52.422443]  psmouse_handle_byte+0x19/0x70 [psmouse]
[   52.422450]  psmouse_receive_byte+0x8d/0x300 [psmouse]
[   52.422456]  ps2_interrupt+0xa1/0x110
[   52.422462]  serio_interrupt+0x4b/0xb0
[   52.422464]  i8042_handle_data+0x189/0x370
[   52.422466]  ? timekeeping_adjust+0x1e/0x180
[   52.422469]  ? __note_gp_changes+0x1f3/0x270
[   52.422473]  ? sched_balance_domains+0xd9/0x380
[   52.422475]  i8042_interrupt+0x15/0x60
[   52.422478]  __handle_irq_event_percpu+0x59/0x230
[   52.422481]  handle_irq_event+0x36/0x90
[   52.422484]  handle_edge_irq+0xd3/0x1a0
[   52.422487]  __common_interrupt+0x50/0x160
[   52.422489]  ? irq_enter_rcu+0x75/0x90
[   52.422492]  common_interrupt+0xb0/0xe0
[   52.422495]  </IRQ>
[   52.422496]  <TASK>
[   52.422497]  asm_common_interrupt+0x27/0x40
[   52.422499] RIP: 0010:cpuidle_enter_state+0xca/0x700
[   52.422502] Code: 00 e8 ca 91 dd fe e8 15 ee ff ff 49 89 c5 0f 1f 44 00 00 31 ff e8 06 7f db fe 80 7d d0 00 0f 85 c6 01 00 00 fb 0f 1f 44 00 00 <45> 85 e4 0f 88 3e 02 00 00 4d 63 fc 49 83 ff 0a 0f 83 1d 05 00 00
[   52.422503] RSP: 0018:ffffd20bc00f3e00 EFLAGS: 00000246
[   52.422505] RAX: 0000000000000000 RBX: ffff8ef0a6dbd6c0 RCX: 0000000000000000
[   52.422507] RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000
[   52.422507] RBP: ffffd20bc00f3e50 R08: 0000000000000000 R09: 0000000000000000
[   52.422508] R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000005
[   52.422509] R13: 0000000c349ddabc R14: ffffffffb1d54c40 R15: 0000000000000005
[   52.422511]  ? tick_nohz_stop_tick+0x5e/0x260
[   52.422516]  cpuidle_enter+0x30/0x50
[   52.422520]  call_cpuidle+0x21/0x50
[   52.422523]  cpuidle_idle_call+0x16b/0x1f0
[   52.422526]  do_idle+0x94/0xf0
[   52.422528]  cpu_startup_entry+0x29/0x30
[   52.422529]  start_secondary+0x125/0x180
[   52.422532]  ? soft_restart_cpu+0x14/0x14
[   52.422534]  common_startup_64+0x13e/0x141
[   52.422537]  </TASK>
[   52.422537] ---[ end trace ]---

These no longer occur when this patch is applied.

I found similar reports from other users at
https://bugs.launchpad.net/ubuntu/+source/linux/+bug/2125250

I found a related review comment when this code was first written at
https://lore.kernel.org/linux-input/20141111171554.GB27720@dtor-ws/

>Fixes: 05be1d079ec0 ("Input: psmouse - support for the FocalTech PS/2 protocol extensions")
>Signed-off-by: Richard Davies <richard@arachsys.com>
>---
> drivers/input/mouse/focaltech.c | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
>diff --git a/drivers/input/mouse/focaltech.c b/drivers/input/mouse/focaltech.c
>index 43f9939b7c63..d3ad4af5aa09 100644
>--- a/drivers/input/mouse/focaltech.c
>+++ b/drivers/input/mouse/focaltech.c
>@@ -197,7 +197,7 @@ static void focaltech_process_rel_packet(struct psmouse *psmouse,
> {
> 	struct focaltech_data *priv = psmouse->private;
> 	struct focaltech_hw_state *state = &priv->state;
>-	int finger1, finger2;
>+	unsigned int finger1, finger2;
>
> 	state->pressed = packet[0] >> 7;
> 	finger1 = ((packet[0] >> 4) & 0x7) - 1;
>-- 
>2.53.0

^ permalink raw reply

* Re: [PATCH] Input: focaltech - fix array out-of-bounds in focaltech_process_rel_packet
From: sashiko-bot @ 2026-07-01 19:32 UTC (permalink / raw)
  To: Richard Davies; +Cc: linux-input, dmitry.torokhov
In-Reply-To: <20260701190932.14960-1-richard@arachsys.com>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [Medium] Unsigned coordinate wrapping causes the cursor to incorrectly jump to the opposite edge when processing relative movements.
--

commit 7c58aa99b660b4d96ec349d279fd307555360fd9
Author: Richard Davies <richard@arachsys.com>

Input: focaltech - fix array out-of-bounds in focaltech_process_rel_packet

Make finger2 (and also finger1) unsigned, so that if the finger index in
the packet is 0 then subtracting 1 creates an array index which overflows
above the existing check for FOC_MAX_FINGERS, as the existing comment says
it should, instead of writing to state->fingers[-1].

> diff --git a/drivers/input/mouse/focaltech.c b/drivers/input/mouse/focaltech.c
> index 43f9939b7c63c..d3ad4af5aa09d 100644
> --- a/drivers/input/mouse/focaltech.c
> +++ b/drivers/input/mouse/focaltech.c
> @@ -197,7 +197,7 @@ static void focaltech_process_rel_packet(struct psmouse *psmouse,
>  {
>  	struct focaltech_data *priv = psmouse->private;
>  	struct focaltech_hw_state *state = &priv->state;
> -	int finger1, finger2;
> +	unsigned int finger1, finger2;
>  
>  	state->pressed = packet[0] >> 7;
>  	finger1 = ((packet[0] >> 4) & 0x7) - 1;

[Severity: Medium]
This isn't a bug introduced by this patch, but does the unsigned type of the
finger coordinates cause them to incorrectly wrap around when applying
relative movements?

When processing relative packets:

drivers/input/mouse/focaltech.c:focaltech_process_rel_packet() {
    ...
		state->fingers[finger1].x += (s8)packet[1];
		state->fingers[finger1].y += (s8)packet[2];
    ...
}

Because x and y are defined as unsigned int, subtracting a value that drops
the coordinate below 0 causes an integer underflow, wrapping it to a large
positive number.

Then, when clamping the coordinates:

drivers/input/mouse/focaltech.c:focaltech_report_state() {
    ...
			clamped_x = clamp(finger->x, 0U, priv->x_max);
			clamped_y = clamp(finger->y, 0U, priv->y_max);
    ...
}

The underflowed unsigned value is incorrectly clamped to the maximum bound
instead of 0. This could cause the user's cursor to jump erratically to the
opposite edge of the screen when dragging a finger near the left or bottom
edge of the touchpad.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260701190932.14960-1-richard@arachsys.com?part=1

^ permalink raw reply

* Re: [PATCH 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
From: Elliot Douglas @ 2026-07-01 22:32 UTC (permalink / raw)
  To: Bastien Nocera; +Cc: linux-input, lains, jikos, bentiss, linux-kernel
In-Reply-To: <CAGt6S1r6ep_L-SdEPd7hcJ92zcBM1oj1NxOD25qjuE6iuGw8Ng@mail.gmail.com>

Just wanted to poke on this thread again, Benjamin or Bastien, what is needed to
push this forward or should I send the v2 at this point?

Thanks,
Elliot

On Wed, Jun 17, 2026 at 6:16 PM Elliot Douglas <edouglas7358@gmail.com> wrote:
>
> Thanks, that makes sense.
>
> For Solaar, this is not continuously forced. The kernel only programs
> temporary diversion when the device connects. Solaar can still issue HID++
> commands through hidraw, so if Solaar changes reporting for the same controls
> afterwards, the last writer wins.
>
> If Solaar takes over those controls for custom actions, the kernel would stop
> receiving the diverted button notifications for normal evdev reporting until
> the kernel diverts the controls again, for example after reconnect. While the
> controls remain diverted, hidraw clients should still receive the raw HID++
> reports.
>
> I have addressed the inline comments locally for v2:
> - replaced the profile/count wrapper with NULL-terminated mapping arrays
> - cached the selected mapping pointer in struct hidpp_device
>
> I'll wait for Benjamin's input to send Patch v2.
>
>
> On Wed, Jun 17, 2026 at 3:28 AM Bastien Nocera <hadess@hadess.net> wrote:
> >
> > On Sat, 2026-06-13 at 10:51 -0700, Elliot Douglas wrote:
> > > Some Logitech HID++ 2.0 mice can report diverted reprogrammable
> > > controls through HID++ feature 0x1b04, SpecialKeysMseButtons /
> > > REPROG_CONTROLS_V4, instead of the normal HID mouse report.
> > >
> > > Add a quirk-gated event path for those controls. The handler temporarily
> > > diverts verified per-product controls, parses divertedButtonsEvent as the
> > > current pressed-control list, and reports the corresponding evdev key state
> > > for every mapped control.
> > >
> > > Keep the control mappings in per-product profiles so adding support for
> > > another mouse does not change the evdev capabilities advertised by
> > > already-supported devices.
> >
> > How does this forced setting work/clash with the programmable buttons
> > in Solaar?
> >
> > I've added some inline comments below.
> >
> > >
> > > Documentation for feature 0x1b04 describes divertedButtonsEvent as a list
> > > of currently pressed diverted buttons, which is the event format handled
> > > here.
> > >
> > > Link: https://lekensteyn.nl/files/logitech/x1b04_specialkeysmsebuttons.html
> > >
> > > Signed-off-by: Elliot Douglas <edouglas7358@gmail.com>
> > > ---
> > >  drivers/hid/hid-logitech-hidpp.c | 215 +++++++++++++++++++++++++++++++
> > >  1 file changed, 215 insertions(+)
> > >
> > > diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
> > > index 70ba1a5e40d8..24c9cfaa4f37 100644
> > > --- a/drivers/hid/hid-logitech-hidpp.c
> > > +++ b/drivers/hid/hid-logitech-hidpp.c
> > > @@ -76,6 +76,7 @@ MODULE_PARM_DESC(disable_tap_to_click,
> > >  #define HIDPP_QUIRK_HI_RES_SCROLL_1P0                BIT(28)
> > >  #define HIDPP_QUIRK_WIRELESS_STATUS          BIT(29)
> > >  #define HIDPP_QUIRK_RESET_HI_RES_SCROLL              BIT(30)
> > > +#define HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS       BIT(31)
> > >
> > >  /* These are just aliases for now */
> > >  #define HIDPP_QUIRK_KBD_SCROLL_WHEEL HIDPP_QUIRK_HIDPP_WHEELS
> > > @@ -205,6 +206,7 @@ struct hidpp_device {
> > >       struct hidpp_scroll_counter vertical_wheel_counter;
> > >
> > >       u8 wireless_feature_index;
> > > +     u8 reprog_controls_feature_index;
> > >
> > >       int hires_wheel_multiplier;
> > >       u8 hires_wheel_feature_index;
> > > @@ -3601,6 +3603,209 @@ static int hidpp10_extra_mouse_buttons_raw_event(struct hidpp_device *hidpp,
> > >       return 1;
> > >  }
> > >
> > > +/* -------------------------------------------------------------------------- */
> > > +/* HID++2.0 reprogrammable controls                                           */
> > > +/* -------------------------------------------------------------------------- */
> > > +
> > > +#define HIDPP_PAGE_REPROG_CONTROLS_V4                        0x1b04
> > > +
> > > +#define HIDPP_REPROG_CONTROLS_GET_COUNT                      0x00
> > > +#define HIDPP_REPROG_CONTROLS_GET_CID_INFO           0x10
> > > +#define HIDPP_REPROG_CONTROLS_SET_CONTROL_REPORTING  0x30
> > > +
> > > +#define HIDPP_REPROG_CONTROLS_FLAG_MOUSE             BIT(0)
> > > +#define HIDPP_REPROG_CONTROLS_FLAG_DIVERT            BIT(5)
> > > +
> > > +#define HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED     BIT(0)
> > > +#define HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT        BIT(1)
> > > +
> > > +#define HIDPP_REPROG_CONTROLS_EVENT_DIVERTED         0x00
> > > +
> > > +struct hidpp_reprog_control_mapping {
> > > +     u16 control;
> > > +     u16 code;
> > > +};
> > > +
> > > +struct hidpp_reprog_controls_profile {
> > > +     const struct hidpp_reprog_control_mapping *mappings;
> >
> > probably needs a __counted_by(), or maybe as it's static, it might be
> > better to not require an intermediate struct, and return a NULL-
> > terminated array instead.
> >
> > > +     unsigned int mapping_count;
> > > +};
> > > +
> > > +static const struct hidpp_reprog_controls_profile *
> > > +hidpp20_reprog_controls_get_profile(struct hidpp_device *hidpp)
> > > +{
> > > +     return NULL;
> > > +}
> > > +
> > > +static int hidpp20_reprog_controls_get_count(struct hidpp_device *hidpp)
> > > +{
> > > +     struct hidpp_report response;
> > > +     u8 feature_index = hidpp->reprog_controls_feature_index;
> > > +     u8 cmd = HIDPP_REPROG_CONTROLS_GET_COUNT;
> > > +     int ret;
> > > +
> > > +     ret = hidpp_send_fap_command_sync(hidpp, feature_index, cmd, NULL, 0,
> > > +                                       &response);
> > > +     if (ret > 0)
> > > +             return -EPROTO;
> > > +     if (ret)
> > > +             return ret;
> > > +
> > > +     return response.fap.params[0];
> > > +}
> > > +
> > > +static int hidpp20_reprog_controls_get_cid_info(struct hidpp_device *hidpp,
> > > +                                             u8 index, u16 *control,
> > > +                                             u8 *flags)
> > > +{
> > > +     struct hidpp_report response;
> > > +     u8 feature_index = hidpp->reprog_controls_feature_index;
> > > +     u8 cmd = HIDPP_REPROG_CONTROLS_GET_CID_INFO;
> > > +     int ret;
> > > +
> > > +     ret = hidpp_send_fap_command_sync(hidpp, feature_index, cmd, &index,
> > > +                                       sizeof(index), &response);
> > > +     if (ret > 0)
> > > +             return -EPROTO;
> > > +     if (ret)
> > > +             return ret;
> > > +
> > > +     *control = get_unaligned_be16(&response.fap.params[0]);
> > > +     *flags = response.fap.params[4];
> > > +
> > > +     return 0;
> > > +}
> > > +
> > > +static bool hidpp20_reprog_controls_find_control(struct hidpp_device *hidpp,
> > > +                                              u16 control)
> > > +{
> > > +     int count, ret;
> > > +     u16 cid;
> > > +     u8 flags;
> > > +     int i;
> > > +
> > > +     count = hidpp20_reprog_controls_get_count(hidpp);
> > > +     if (count < 0)
> > > +             return false;
> > > +
> > > +     for (i = 0; i < count; i++) {
> > > +             ret = hidpp20_reprog_controls_get_cid_info(hidpp, i, &cid,
> > > +                                                        &flags);
> > > +             if (ret)
> > > +                     return false;
> > > +
> > > +             if (cid == control)
> > > +                     return (flags & HIDPP_REPROG_CONTROLS_FLAG_MOUSE) &&
> > > +                            (flags & HIDPP_REPROG_CONTROLS_FLAG_DIVERT);
> > > +     }
> > > +
> > > +     return false;
> > > +}
> > > +
> > > +static int hidpp20_reprog_controls_set_control_reporting(struct hidpp_device *hidpp,
> > > +                                                      u16 control, u8 flags)
> > > +{
> > > +     struct hidpp_report response;
> > > +     u8 params[5];
> > > +
> > > +     put_unaligned_be16(control, &params[0]);
> > > +     params[2] = flags;
> > > +     put_unaligned_be16(control, &params[3]);
> > > +
> > > +     return hidpp_send_fap_command_sync(hidpp,
> > > +                                        hidpp->reprog_controls_feature_index,
> > > +                                        HIDPP_REPROG_CONTROLS_SET_CONTROL_REPORTING,
> > > +                                        params, sizeof(params), &response);
> > > +}
> > > +
> > > +static void hidpp20_reprog_controls_connect(struct hidpp_device *hidpp)
> > > +{
> > > +     const struct hidpp_reprog_controls_profile *profile;
> > > +     u8 flags = HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED |
> > > +                HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT;
> > > +     unsigned int i;
> > > +
> > > +     if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS))
> > > +             return;
> > > +
> > > +     profile = hidpp20_reprog_controls_get_profile(hidpp);
> >
> > Could the profile be cached in the hidpp_device struct?
> >
> > > +     if (!profile)
> > > +             return;
> > > +
> > > +     if (hidpp_root_get_feature(hidpp, HIDPP_PAGE_REPROG_CONTROLS_V4,
> > > +                                &hidpp->reprog_controls_feature_index))
> > > +             return;
> > > +
> > > +     for (i = 0; i < profile->mapping_count; i++) {
> > > +             u16 control = profile->mappings[i].control;
> > > +
> > > +             if (!hidpp20_reprog_controls_find_control(hidpp, control))
> > > +                     continue;
> > > +
> > > +             hidpp20_reprog_controls_set_control_reporting(hidpp, control, flags);
> > > +     }
> > > +}
> > > +
> > > +static int hidpp20_reprog_controls_raw_event(struct hidpp_device *hidpp,
> > > +                                          u8 *data, int size)
> > > +{
> > > +     const struct hidpp_reprog_controls_profile *profile;
> > > +     const struct hidpp_reprog_control_mapping *mapping;
> > > +     struct hidpp_report *report = (struct hidpp_report *)data;
> > > +     u16 controls[4];
> > > +     bool pressed;
> > > +     unsigned int i, j;
> > > +
> > > +     if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS) ||
> > > +         !hidpp->input ||
> > > +         hidpp->reprog_controls_feature_index == 0xff)
> > > +             return 0;
> > > +
> > > +     profile = hidpp20_reprog_controls_get_profile(hidpp);
> > > +     if (!profile)
> > > +             return 0;
> > > +
> > > +     if (size < HIDPP_REPORT_LONG_LENGTH ||
> > > +         report->fap.feature_index != hidpp->reprog_controls_feature_index ||
> > > +         report->fap.funcindex_clientid != HIDPP_REPROG_CONTROLS_EVENT_DIVERTED)
> > > +             return 0;
> > > +
> > > +     for (i = 0; i < ARRAY_SIZE(controls); i++)
> > > +             controls[i] = get_unaligned_be16(&report->fap.params[i * 2]);
> > > +
> > > +     for (i = 0; i < profile->mapping_count; i++) {
> > > +             mapping = &profile->mappings[i];
> > > +             pressed = false;
> > > +
> > > +             for (j = 0; j < ARRAY_SIZE(controls); j++) {
> > > +                     if (controls[j] == mapping->control) {
> > > +                             pressed = true;
> > > +                             break;
> > > +                     }
> > > +             }
> > > +
> > > +             input_report_key(hidpp->input, mapping->code, pressed);
> > > +     }
> > > +
> > > +     input_sync(hidpp->input);
> > > +
> > > +     return 1;
> > > +}
> > > +
> > > +static void hidpp20_reprog_controls_populate_input(struct hidpp_device *hidpp,
> > > +                                                struct input_dev *input_dev)
> > > +{
> > > +     const struct hidpp_reprog_controls_profile *profile;
> > > +     unsigned int i;
> > > +
> > > +     profile = hidpp20_reprog_controls_get_profile(hidpp);
> > > +     if (!profile)
> > > +             return;
> > > +
> > > +     for (i = 0; i < profile->mapping_count; i++)
> > > +             input_set_capability(input_dev, EV_KEY, profile->mappings[i].code);
> > > +}
> > > +
> > >  static void hidpp10_extra_mouse_buttons_populate_input(
> > >                       struct hidpp_device *hidpp, struct input_dev *input_dev)
> > >  {
> > > @@ -3859,6 +4064,9 @@ static void hidpp_populate_input(struct hidpp_device *hidpp,
> > >
> > >       if (hidpp->quirks & HIDPP_QUIRK_HIDPP_EXTRA_MOUSE_BTNS)
> > >               hidpp10_extra_mouse_buttons_populate_input(hidpp, input);
> > > +
> > > +     if (hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS)
> > > +             hidpp20_reprog_controls_populate_input(hidpp, input);
> > >  }
> > >
> > >  static int hidpp_input_configured(struct hid_device *hdev,
> > > @@ -3971,6 +4179,10 @@ static int hidpp_raw_hidpp_event(struct hidpp_device *hidpp, u8 *data,
> > >                       return ret;
> > >       }
> > >
> > > +     ret = hidpp20_reprog_controls_raw_event(hidpp, data, size);
> > > +     if (ret != 0)
> > > +             return ret;
> > > +
> > >       if (hidpp->quirks & HIDPP_QUIRK_HIDPP_CONSUMER_VENDOR_KEYS) {
> > >               ret = hidpp10_consumer_keys_raw_event(hidpp, data, size);
> > >               if (ret != 0)
> > > @@ -4264,6 +4476,8 @@ static void hidpp_connect_event(struct work_struct *work)
> > >                       return;
> > >       }
> > >
> > > +     hidpp20_reprog_controls_connect(hidpp);
> > > +
> > >       if (hidpp->quirks & HIDPP_QUIRK_HIDPP_CONSUMER_VENDOR_KEYS) {
> > >               ret = hidpp10_consumer_keys_connect(hidpp);
> > >               if (ret)
> > > @@ -4436,6 +4650,7 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
> > >       hidpp->hid_dev = hdev;
> > >       hidpp->name = hdev->name;
> > >       hidpp->quirks = id->driver_data;
> > > +     hidpp->reprog_controls_feature_index = 0xff;
> > >       hid_set_drvdata(hdev, hidpp);
> > >
> > >       ret = hid_parse(hdev);

^ permalink raw reply

* [PATCH v10 0/3] HID: nintendo: Add preliminary Switch 2 controller
From: Vicki Pfau @ 2026-07-02  3:41 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Vicki Pfau, Silvan Jegen

This series adds preliminary support for Switch 2 controllers using the
same split-driver model as previous versions. This is a minor iteration on
v9, fixing errors pointed out by sashiko and moving both the led structs
and the rumble work to be devm-allocated based on the input device.

I think several of the concurrency issues pointed out by sashiko are
impossible (e.g. a HID device receiving events concurrently with the remove
handler), otherwise there are some major issues with lots and lots of
drivers.

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                    | 1681 ++++++++++++++++-
 drivers/hid/hid-nintendo.h                    |   72 +
 drivers/input/joystick/Kconfig                |   11 +
 drivers/input/joystick/Makefile               |    1 +
 drivers/input/joystick/nintendo-switch2-usb.c |  468 +++++
 8 files changed, 2216 insertions(+), 41 deletions(-)
 create mode 100644 drivers/hid/hid-nintendo.h
 create mode 100644 drivers/input/joystick/nintendo-switch2-usb.c

-- 
2.54.0


^ permalink raw reply

* [PATCH v10 2/3] HID: nintendo: Add rumble support for Switch 2 controllers
From: Vicki Pfau @ 2026-07-02  3:41 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Vicki Pfau, Silvan Jegen
In-Reply-To: <20260702034106.1498056-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 | 214 ++++++++++++++++++++++++++++++++++++-
 2 files changed, 216 insertions(+), 6 deletions(-)

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 19c77c323ec9..851eed76c236 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -859,10 +859,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 f82034decb9e..71a04c438271 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -37,6 +37,7 @@
 #include <linux/unaligned.h>
 #include <linux/delay.h>
 #include <linux/device.h>
+#include <linux/devm-helpers.h>
 #include <linux/kernel.h>
 #include <linux/hid.h>
 #include <linux/idr.h>
@@ -2989,6 +2990,7 @@ enum switch2_init_step {
 	NS2_INIT_READ_USER_SECONDARY_CALIB,
 	NS2_INIT_SET_FEATURE_MASK,
 	NS2_INIT_ENABLE_FEATURES,
+	NS2_INIT_ENABLE_RUMBLE,
 	NS2_INIT_GRIP_BUTTONS,
 	NS2_INIT_REPORT_FORMAT,
 	NS2_INIT_INPUT,
@@ -3020,6 +3022,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;
@@ -3043,8 +3057,45 @@ struct switch2_controller {
 
 	uint32_t player_id;
 	struct led_classdev *leds;
+
+#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;
+	uint64_t last_rumble_work;
+	struct delayed_work rumble_work;
+	uint8_t *rumble_buffer;
+#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);
 
@@ -3136,7 +3187,7 @@ static const uint8_t switch2_init_cmd_data[] = {
 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,
+	NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU | NS2_FEATURE_RUMBLE,
 	0x00, 0x00, 0x00
 };
 
@@ -3209,6 +3260,125 @@ static void switch2_kref_put(struct kref *refcount)
 	kfree(ns2);
 }
 
+#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);
+	unsigned long flags;
+
+	if (effect->type != FF_RUMBLE)
+		return 0;
+
+	spin_lock_irqsave(&ns2->rumble_lock, flags);
+	if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
+		ns2->rumble.sd.amplitude = max(effect->u.rumble.strong_magnitude,
+			(uint16_t) (effect->u.rumble.weak_magnitude >> 1));
+	} else {
+		ns2->rumble.hd.hi_freq = RUMBLE_HI_FREQ;
+		ns2->rumble.hd.lo_freq = RUMBLE_LO_FREQ;
+		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;
+	}
+	spin_unlock_irqrestore(&ns2->rumble_lock, flags);
+
+	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 flags;
+	bool active;
+	int ret = 0;
+
+	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;
+	spin_unlock_irqrestore(&ns2->rumble_lock, flags);
+
+	if (active) {
+		unsigned long interval = msecs_to_jiffies(4);
+		uint64_t current_jiffies = get_jiffies_64();
+
+		if (!ns2->last_rumble_work)
+			ns2->last_rumble_work = current_jiffies;
+		else
+			ns2->last_rumble_work += interval;
+
+		/* Reschedule a little early to make sure the buffer never underruns */
+		interval -= msecs_to_jiffies(2);
+		if (ns2->last_rumble_work + interval >= current_jiffies)
+			schedule_delayed_work(&ns2->rumble_work,
+				ns2->last_rumble_work + interval - current_jiffies);
+		else
+			schedule_delayed_work(&ns2->rumble_work, 0);
+	} else {
+		ns2->last_rumble_work = 0;
+	}
+
+	mutex_lock(&ns2->lock);
+	if (!ns2->hdev) {
+		cancel_delayed_work(&ns2->rumble_work);
+	} else {
+		ret = hid_hw_output_report(ns2->hdev, ns2->rumble_buffer, 64);
+		/*
+		 * Don't log on ENODEV, ESHUTDOWN, or EPROTO, which can happen
+		 * mid-hotplug. Also cancel any further work on ENODEV or
+		 * ESHUTDOWN as they're clear indications that the endpoint
+		 * is dead.
+		 */
+		if (ret == -ENODEV || ret == -ESHUTDOWN)
+			cancel_delayed_work(&ns2->rumble_work);
+		else if (ret < 0 && ret != -EPROTO)
+			hid_warn_ratelimited(ns2->hdev,
+				"Failed to send output report ret=%d\n", ret);
+	}
+	mutex_unlock(&ns2->lock);
+}
+#endif
+
 static int switch2_set_leds(struct switch2_controller *ns2)
 {
 	int i;
@@ -3332,6 +3502,26 @@ static int switch2_init_input(struct switch2_controller *ns2)
 		return -EINVAL;
 	}
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	ns2->rumble_buffer = devm_kzalloc(&input->dev, 64, GFP_KERNEL);
+	if (!ns2->rumble_buffer) {
+		input_free_device(input);
+		return -ENOMEM;
+	}
+	ret = devm_delayed_work_autocancel(&input->dev, &ns2->rumble_work, switch2_rumble_work);
+	if (ret < 0) {
+		input_free_device(input);
+		return ret;
+	}
+
+	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)
@@ -3764,7 +3954,16 @@ 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_ENABLE_RUMBLE:
+		/*
+		 * It is unclear what this packet is supposed to be for, but it
+		 * appears to be needed for rumble to work reliably. The reply
+		 * data indicates it might be a query of some sort, but we
+		 * ignore the reply so long as it doesn't return an error.
+		 */
+		return ns2->cfg->send_command(0x11, 1, NULL, 0, ns2->cfg);
 	case NS2_INIT_GRIP_BUTTONS:
 		if (!switch2_ctlr_is_joycon(ns2->ctlr_type)) {
 			switch2_init_step_done(ns2, ns2->init_step);
@@ -3876,6 +4075,10 @@ int switch2_receive_command(struct switch2_controller *ns2,
 			switch2_init_step_done(ns2, NS2_INIT_GET_FIRMWARE_INFO);
 		}
 		break;
+	case 0x11:
+		if (header->subcommand == 1)
+			switch2_init_step_done(ns2, NS2_INIT_ENABLE_RUMBLE);
+		break;
 	default:
 		break;
 	}
@@ -3995,6 +4198,10 @@ static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id
 	else
 		ns2->player_id = ret;
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	spin_lock_init(&ns2->rumble_lock);
+#endif
+
 	ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
 	if (ret) {
 		hid_err(hdev, "hw_start failed %d\n", ret);
@@ -4038,6 +4245,9 @@ static void switch2_remove(struct hid_device *hdev)
 	ns2->hdev = NULL;
 	ida_free(&nintendo_player_id_allocator, ns2->player_id);
 	mutex_unlock(&ns2->lock);
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	cancel_delayed_work_sync(&ns2->rumble_work);
+#endif
 	kref_put(&ns2->refcount, switch2_kref_put);
 	hid_hw_close(hdev);
 	hid_hw_stop(hdev);
-- 
2.54.0


^ permalink raw reply related


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