Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH 05/10] mailbox: apple: Add DockChannel FIFO controller
From: Michael Reeves via B4 Relay @ 2026-06-30 12:54 UTC (permalink / raw)
  To: 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
  Cc: asahi, linux-arm-kernel, linux-kernel, devicetree, iommu,
	linux-input, Michael Reeves
In-Reply-To: <20260630-apple-mtp-keyboard-final-v1-0-506d936a1707@gmail.com>

From: Michael Reeves <michael.reeves077@gmail.com>

DockChannel is a hardware FIFO used by Apple coprocessors for
low-latency byte-stream communication with the AP.

Add a mailbox controller that preallocates RX storage, tracks IRQ
enable state in software, and reports TX completion from the TX-empty
interrupt.

Reject messages larger than the FIFO and return -EBUSY while the
previous message is still pending. This keeps the provider usable for
future small-message clients such as serial transports without a TX
worker.

Co-developed-by: Hector Martin <marcan@marcan.st>
Signed-off-by: Hector Martin <marcan@marcan.st>
Signed-off-by: Michael Reeves <michael.reeves077@gmail.com>
---
 MAINTAINERS                               |   2 +
 drivers/mailbox/Kconfig                   |  12 +
 drivers/mailbox/Makefile                  |   2 +
 drivers/mailbox/apple-dockchannel.c       | 380 ++++++++++++++++++++++++++++++
 include/linux/mailbox/apple-dockchannel.h |  29 +++
 5 files changed, 425 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 1f3c2cdb6e19..ed68452c0ad6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2628,6 +2628,7 @@ F:	drivers/input/touchscreen/apple_z2.c
 F:	drivers/iommu/apple-dart.c
 F:	drivers/iommu/io-pgtable-dart.c
 F:	drivers/irqchip/irq-apple-aic.c
+F:	drivers/mailbox/apple-dockchannel.c
 F:	drivers/mfd/macsmc.c
 F:	drivers/nvme/host/apple.c
 F:	drivers/nvmem/apple-efuses.c
@@ -2646,6 +2647,7 @@ F:	drivers/video/backlight/apple_dwi_bl.c
 F:	drivers/watchdog/apple_wdt.c
 F:	include/dt-bindings/interrupt-controller/apple-aic.h
 F:	include/dt-bindings/pinctrl/apple.h
+F:	include/linux/mailbox/apple-dockchannel.h
 F:	include/linux/mfd/macsmc.h
 F:	include/linux/soc/apple/*
 F:	include/uapi/drm/asahi_drm.h
diff --git a/drivers/mailbox/Kconfig b/drivers/mailbox/Kconfig
index 3062ee352f78..f1af76d19f1e 100644
--- a/drivers/mailbox/Kconfig
+++ b/drivers/mailbox/Kconfig
@@ -36,6 +36,18 @@ config ARM_MHU_V3
 	  that provides different means of transports: supported extensions
 	  will be discovered and possibly managed at probe-time.
 
+config APPLE_DOCKCHANNEL
+	tristate "Apple DockChannel FIFO mailbox"
+	depends on ARCH_APPLE || COMPILE_TEST
+	depends on HAS_IOMEM
+	depends on OF
+	help
+	  DockChannel is a hardware FIFO used on Apple Silicon SoCs for
+	  communication between the application processor and co-processors.
+	  This driver exposes DockChannel FIFOs through the mailbox framework.
+
+	  Say Y here if you have an M2 or later Apple MacBook.
+
 config AST2700_MBOX
 	tristate "ASPEED AST2700 IPC driver"
 	depends on ARCH_ASPEED || COMPILE_TEST
diff --git a/drivers/mailbox/Makefile b/drivers/mailbox/Makefile
index 944d8ea39f34..4f3405064269 100644
--- a/drivers/mailbox/Makefile
+++ b/drivers/mailbox/Makefile
@@ -11,6 +11,8 @@ obj-$(CONFIG_ARM_MHU_V2)	+= arm_mhuv2.o
 
 obj-$(CONFIG_ARM_MHU_V3)	+= arm_mhuv3.o
 
+obj-$(CONFIG_APPLE_DOCKCHANNEL)	+= apple-dockchannel.o
+
 obj-$(CONFIG_AST2700_MBOX)	+= ast2700-mailbox.o
 
 obj-$(CONFIG_CV1800_MBOX)	+= cv1800-mailbox.o
diff --git a/drivers/mailbox/apple-dockchannel.c b/drivers/mailbox/apple-dockchannel.c
new file mode 100644
index 000000000000..bae183db1307
--- /dev/null
+++ b/drivers/mailbox/apple-dockchannel.c
@@ -0,0 +1,380 @@
+// SPDX-License-Identifier: GPL-2.0-only OR MIT
+/*
+ * Apple DockChannel mailbox controller
+ *
+ * Copyright The Asahi Linux Contributors
+ *
+ * DockChannel is a byte FIFO used by Apple co-processors. This driver exposes a
+ * single FIFO pair as a Linux mailbox channel and moves payload bytes with PIO.
+ * There is no DMA involved, so relaxed MMIO accessors are sufficient for the
+ * FIFO accesses themselves.
+ */
+
+#include <linux/device.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/mailbox/apple-dockchannel.h>
+#include <linux/mailbox_controller.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/spinlock.h>
+#include <linux/unaligned.h>
+
+#define APPLE_DOCKCHANNEL_FIFO_SIZE	0x800
+
+#define IRQ_MASK			0x0
+#define IRQ_FLAG			0x4
+
+#define IRQ_TX				BIT(2)
+#define IRQ_RX				BIT(3)
+
+#define CONFIG_TX_THRESH		0x0
+#define CONFIG_RX_THRESH		0x4
+
+#define DATA_TX8			0x4
+#define DATA_TX32			0x10
+#define DATA_TX_FREE			0x14
+#define DATA_RX8			0x1c
+#define DATA_RX32			0x28
+#define DATA_RX_COUNT			0x2c
+
+struct apple_dockchannel {
+	struct device *dev;
+	struct mbox_controller controller;
+	struct mbox_chan chan;
+
+	void __iomem *irq_base;
+	void __iomem *config_base;
+	void __iomem *data_base;
+	int irq;
+
+	spinlock_t lock; /* protects IRQ mask and TX state */
+	u32 irq_mask;
+
+	const u8 *tx_buf;
+	size_t tx_len;
+	size_t tx_pos;
+	bool tx_active;
+
+	u8 rx_buf[APPLE_DOCKCHANNEL_FIFO_SIZE];
+};
+
+static void apple_dockchannel_irq_update(struct apple_dockchannel *dc,
+					 u32 bits, bool enable)
+{
+	if (enable)
+		dc->irq_mask |= bits;
+	else
+		dc->irq_mask &= ~bits;
+	writel_relaxed(dc->irq_mask, dc->irq_base + IRQ_MASK);
+}
+
+static void apple_dockchannel_irq_enable(struct apple_dockchannel *dc, u32 bits)
+{
+	/*
+	 * IRQ_FLAG is write-to-clear. Clear stale latched flags before
+	 * unmasking so the next interrupt reflects current FIFO state.
+	 */
+	writel_relaxed(bits, dc->irq_base + IRQ_FLAG);
+	apple_dockchannel_irq_update(dc, bits, true);
+}
+
+static void apple_dockchannel_irq_disable(struct apple_dockchannel *dc, u32 bits)
+{
+	apple_dockchannel_irq_update(dc, bits, false);
+}
+
+static bool apple_dockchannel_tx_empty(struct apple_dockchannel *dc)
+{
+	return readl_relaxed(dc->data_base + DATA_TX_FREE) ==
+	       APPLE_DOCKCHANNEL_FIFO_SIZE;
+}
+
+static void apple_dockchannel_write_pending(struct apple_dockchannel *dc)
+{
+	size_t left = dc->tx_len - dc->tx_pos;
+	const u8 *p = dc->tx_buf + dc->tx_pos;
+
+	while (left) {
+		size_t avail;
+		size_t block;
+
+		avail = readl_relaxed(dc->data_base + DATA_TX_FREE);
+		if (!avail)
+			break;
+
+		block = min(left, avail);
+
+		while (block >= sizeof(u32)) {
+			writel_relaxed(get_unaligned_le32(p),
+				       dc->data_base + DATA_TX32);
+			p += sizeof(u32);
+			left -= sizeof(u32);
+			block -= sizeof(u32);
+		}
+
+		while (block) {
+			writeb_relaxed(*p++, dc->data_base + DATA_TX8);
+			left--;
+			block--;
+		}
+	}
+
+	dc->tx_pos = dc->tx_len - left;
+}
+
+static void apple_dockchannel_read(struct apple_dockchannel *dc, void *buf,
+				   size_t count)
+{
+	u8 *p = buf;
+	size_t left = count;
+
+	while (left >= sizeof(u32)) {
+		put_unaligned_le32(readl_relaxed(dc->data_base + DATA_RX32), p);
+		p += sizeof(u32);
+		left -= sizeof(u32);
+	}
+
+	while (left) {
+		/*
+		 * The byte FIFO register returns the byte in bits [15:8] on
+		 * these instances.
+		 */
+		*p++ = readl_relaxed(dc->data_base + DATA_RX8) >> 8;
+		left--;
+	}
+}
+
+static int apple_dockchannel_send_data(struct mbox_chan *chan, void *data)
+{
+	struct apple_dockchannel *dc = chan->con_priv;
+	struct apple_dockchannel_msg *msg = data;
+	unsigned long flags;
+
+	if (!msg || !msg->data || !msg->len)
+		return -EINVAL;
+
+	if (msg->len > APPLE_DOCKCHANNEL_FIFO_SIZE)
+		return -EMSGSIZE;
+
+	spin_lock_irqsave(&dc->lock, flags);
+
+	if (dc->tx_active || !apple_dockchannel_tx_empty(dc)) {
+		spin_unlock_irqrestore(&dc->lock, flags);
+		return -EBUSY;
+	}
+
+	dc->tx_buf = msg->data;
+	dc->tx_len = msg->len;
+	dc->tx_pos = 0;
+	dc->tx_active = true;
+
+	apple_dockchannel_write_pending(dc);
+	writel_relaxed(APPLE_DOCKCHANNEL_FIFO_SIZE,
+		       dc->config_base + CONFIG_TX_THRESH);
+	apple_dockchannel_irq_enable(dc, IRQ_TX);
+
+	spin_unlock_irqrestore(&dc->lock, flags);
+
+	return 0;
+}
+
+static int apple_dockchannel_startup(struct mbox_chan *chan)
+{
+	struct apple_dockchannel *dc = chan->con_priv;
+	unsigned long flags;
+
+	spin_lock_irqsave(&dc->lock, flags);
+	/*
+	 * The mailbox framework has no per-client RX threshold. Use byte
+	 * granularity because UART-style DockChannel clients require it.
+	 */
+	writel_relaxed(1, dc->config_base + CONFIG_RX_THRESH);
+	apple_dockchannel_irq_enable(dc, IRQ_RX);
+	spin_unlock_irqrestore(&dc->lock, flags);
+
+	enable_irq(dc->irq);
+
+	return 0;
+}
+
+static void apple_dockchannel_shutdown(struct mbox_chan *chan)
+{
+	struct apple_dockchannel *dc = chan->con_priv;
+	unsigned long flags;
+
+	disable_irq(dc->irq);
+
+	spin_lock_irqsave(&dc->lock, flags);
+	apple_dockchannel_irq_disable(dc, IRQ_TX | IRQ_RX);
+	dc->tx_active = false;
+	spin_unlock_irqrestore(&dc->lock, flags);
+}
+
+static const struct mbox_chan_ops apple_dockchannel_mbox_ops = {
+	.send_data = apple_dockchannel_send_data,
+	.startup = apple_dockchannel_startup,
+	.shutdown = apple_dockchannel_shutdown,
+};
+
+static irqreturn_t apple_dockchannel_irq(int irq, void *data)
+{
+	struct apple_dockchannel *dc = data;
+	u32 flags;
+	u32 pending;
+	bool tx_done = false;
+
+	flags = readl_relaxed(dc->irq_base + IRQ_FLAG);
+
+	spin_lock(&dc->lock);
+
+	pending = flags & dc->irq_mask & (IRQ_TX | IRQ_RX);
+	if (!pending)
+		goto out_unlock_none;
+
+	if (pending & IRQ_TX) {
+		if (apple_dockchannel_tx_empty(dc)) {
+			apple_dockchannel_irq_disable(dc, IRQ_TX);
+			tx_done = dc->tx_active;
+			dc->tx_active = false;
+		} else {
+			pending &= ~IRQ_TX;
+		}
+	}
+
+	writel_relaxed(pending, dc->irq_base + IRQ_FLAG);
+
+	spin_unlock(&dc->lock);
+
+	if (tx_done)
+		mbox_chan_txdone(&dc->chan, 0);
+
+	if (pending & IRQ_RX)
+		return IRQ_WAKE_THREAD;
+
+	if (pending)
+		return IRQ_HANDLED;
+
+	return IRQ_NONE;
+
+out_unlock_none:
+	spin_unlock(&dc->lock);
+
+	if (flags & (IRQ_TX | IRQ_RX))
+		writel_relaxed(flags & (IRQ_TX | IRQ_RX),
+			       dc->irq_base + IRQ_FLAG);
+
+	return IRQ_NONE;
+}
+
+static irqreturn_t apple_dockchannel_irq_thread(int irq, void *data)
+{
+	struct apple_dockchannel *dc = data;
+
+	for (;;) {
+		struct apple_dockchannel_msg msg;
+		size_t avail;
+
+		avail = readl_relaxed(dc->data_base + DATA_RX_COUNT);
+		if (!avail)
+			break;
+
+		avail = min_t(size_t, avail, APPLE_DOCKCHANNEL_FIFO_SIZE);
+
+		apple_dockchannel_read(dc, dc->rx_buf, avail);
+
+		msg.data = dc->rx_buf;
+		msg.len = avail;
+		mbox_chan_received_data(&dc->chan, &msg);
+	}
+
+	return IRQ_HANDLED;
+}
+
+static struct mbox_chan *
+apple_dockchannel_of_xlate(struct mbox_controller *mbox,
+			   const struct of_phandle_args *spec)
+{
+	if (spec->args_count != 0)
+		return ERR_PTR(-EINVAL);
+
+	return &mbox->chans[0];
+}
+
+static int apple_dockchannel_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct apple_dockchannel *dc;
+	int ret;
+
+	dc = devm_kzalloc(dev, sizeof(*dc), GFP_KERNEL);
+	if (!dc)
+		return -ENOMEM;
+
+	dc->dev = dev;
+	spin_lock_init(&dc->lock);
+	platform_set_drvdata(pdev, dc);
+
+	dc->irq_base = devm_platform_ioremap_resource_byname(pdev, "irq");
+	if (IS_ERR(dc->irq_base))
+		return PTR_ERR(dc->irq_base);
+
+	dc->config_base = devm_platform_ioremap_resource_byname(pdev, "config");
+	if (IS_ERR(dc->config_base))
+		return PTR_ERR(dc->config_base);
+
+	dc->data_base = devm_platform_ioremap_resource_byname(pdev, "data");
+	if (IS_ERR(dc->data_base))
+		return PTR_ERR(dc->data_base);
+
+	writel_relaxed(0, dc->irq_base + IRQ_MASK);
+	writel_relaxed(~0, dc->irq_base + IRQ_FLAG);
+
+	dc->irq = platform_get_irq(pdev, 0);
+	if (dc->irq < 0)
+		return dc->irq;
+
+	ret = devm_request_threaded_irq(dev, dc->irq, apple_dockchannel_irq,
+					apple_dockchannel_irq_thread, IRQF_ONESHOT,
+					dev_name(dev), dc);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to request IRQ\n");
+
+	disable_irq(dc->irq);
+
+	dc->chan.con_priv = dc;
+	dc->controller.dev = dev;
+	dc->controller.ops = &apple_dockchannel_mbox_ops;
+	dc->controller.chans = &dc->chan;
+	dc->controller.num_chans = 1;
+	dc->controller.txdone_irq = true;
+	dc->controller.of_xlate = apple_dockchannel_of_xlate;
+
+	ret = devm_mbox_controller_register(dev, &dc->controller);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to register mailbox\n");
+
+	return 0;
+}
+
+static const struct of_device_id apple_dockchannel_of_match[] = {
+	{ .compatible = "apple,t8122-dockchannel" },
+	{ .compatible = "apple,t8112-dockchannel" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, apple_dockchannel_of_match);
+
+static struct platform_driver apple_dockchannel_driver = {
+	.driver = {
+		.name = "apple-dockchannel",
+		.of_match_table = apple_dockchannel_of_match,
+	},
+	.probe = apple_dockchannel_probe,
+};
+module_platform_driver(apple_dockchannel_driver);
+
+MODULE_DESCRIPTION("Apple DockChannel mailbox controller");
+MODULE_AUTHOR("Hector Martin <marcan@marcan.st>");
+MODULE_AUTHOR("Michael Reeves <michael.reeves077@gmail.com>");
+MODULE_LICENSE("Dual MIT/GPL");
diff --git a/include/linux/mailbox/apple-dockchannel.h b/include/linux/mailbox/apple-dockchannel.h
new file mode 100644
index 000000000000..04d2fc44f12f
--- /dev/null
+++ b/include/linux/mailbox/apple-dockchannel.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0-only OR MIT */
+/*
+ * Apple DockChannel mailbox message format.
+ *
+ * Copyright The Asahi Linux Contributors
+ */
+
+#ifndef _LINUX_MAILBOX_APPLE_DOCKCHANNEL_H_
+#define _LINUX_MAILBOX_APPLE_DOCKCHANNEL_H_
+
+#include <linux/types.h>
+
+/**
+ * struct apple_dockchannel_msg - DockChannel mailbox payload
+ * @data: Pointer to the byte stream payload
+ * @len: Number of payload bytes
+ *
+ * For TX, @data must remain valid until mbox_send_message() completes or the
+ * client receives tx_done in non-blocking mode.
+ *
+ * For RX, @data is owned by the controller and is valid only for the duration
+ * of the rx_callback.
+ */
+struct apple_dockchannel_msg {
+	void *data;
+	size_t len;
+};
+
+#endif /* _LINUX_MAILBOX_APPLE_DOCKCHANNEL_H_ */

-- 
2.51.2



^ permalink raw reply related

* [PATCH 06/10] soc: apple: rtkit: Add tracekit endpoint
From: Michael Reeves via B4 Relay @ 2026-06-30 12:54 UTC (permalink / raw)
  To: 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
  Cc: asahi, linux-arm-kernel, linux-kernel, devicetree, iommu,
	linux-input, Michael Reeves, Sasha Finkelstein
In-Reply-To: <20260630-apple-mtp-keyboard-final-v1-0-506d936a1707@gmail.com>

From: Sasha Finkelstein <fnkl.kernel@gmail.com>

The TraceKit endpoint is a system endpoint used by MTP, AOP, and
potentially other Apple RTKit coprocessors.

Start it automatically when it appears in the endpoint map, like the
other known system endpoints, to avoid warnings about an unknown
endpoint.

Signed-off-by: Sasha Finkelstein <fnkl.kernel@gmail.com>
Signed-off-by: Michael Reeves <michael.reeves077@gmail.com>
---
 drivers/soc/apple/rtkit.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c
index a3fdac8f6f06..1059b4bd8732 100644
--- a/drivers/soc/apple/rtkit.c
+++ b/drivers/soc/apple/rtkit.c
@@ -22,6 +22,7 @@ enum {
 	APPLE_RTKIT_EP_DEBUG = 3,
 	APPLE_RTKIT_EP_IOREPORT = 4,
 	APPLE_RTKIT_EP_OSLOG = 8,
+	APPLE_RTKIT_EP_TRACEKIT = 0xa,
 };
 
 #define APPLE_RTKIT_MGMT_TYPE GENMASK_ULL(59, 52)
@@ -191,6 +192,7 @@ static void apple_rtkit_management_rx_epmap(struct apple_rtkit *rtk, u64 msg)
 		case APPLE_RTKIT_EP_DEBUG:
 		case APPLE_RTKIT_EP_IOREPORT:
 		case APPLE_RTKIT_EP_OSLOG:
+		case APPLE_RTKIT_EP_TRACEKIT:
 			dev_dbg(rtk->dev,
 				"RTKit: Starting system endpoint 0x%02x\n", ep);
 			apple_rtkit_start_ep(rtk, ep);

-- 
2.51.2



^ permalink raw reply related

* [PATCH 08/10] HID: apple: Add DockChannel HID transport driver
From: Michael Reeves via B4 Relay @ 2026-06-30 12:54 UTC (permalink / raw)
  To: 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
  Cc: asahi, linux-arm-kernel, linux-kernel, devicetree, iommu,
	linux-input, Michael Reeves
In-Reply-To: <20260630-apple-mtp-keyboard-final-v1-0-506d936a1707@gmail.com>

From: Michael Reeves <michael.reeves077@gmail.com>

Apple MTP exposes internal keyboard and trackpad interfaces over a HID
transport carried by DockChannel.

Add a transport driver that boots the MTP RTKit coprocessor, exchanges
HID packets through the DockChannel mailbox, and registers child HID
interfaces from devicetree.

Co-developed-by: Hector Martin <marcan@marcan.st>
Signed-off-by: Hector Martin <marcan@marcan.st>
Signed-off-by: Michael Reeves <michael.reeves077@gmail.com>
---
 MAINTAINERS                         |    1 +
 drivers/hid/Kconfig                 |    2 +
 drivers/hid/Makefile                |    2 +
 drivers/hid/dockchannel/Kconfig     |   15 +
 drivers/hid/dockchannel/Makefile    |    3 +
 drivers/hid/dockchannel/apple-hid.c | 1130 +++++++++++++++++++++++++++++++++++
 6 files changed, 1153 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index ed68452c0ad6..0063276f0349 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2620,6 +2620,7 @@ F:	drivers/clk/clk-apple-nco.c
 F:	drivers/cpufreq/apple-soc-cpufreq.c
 F:	drivers/dma/apple-admac.c
 F:	drivers/gpio/gpio-macsmc.c
+F:	drivers/hid/dockchannel/
 F:	drivers/hwmon/macsmc-hwmon.c
 F:	drivers/pmdomain/apple/
 F:	drivers/i2c/busses/i2c-pasemi-core.c
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index f9bcaeb66385..f27cda601ede 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1488,6 +1488,8 @@ source "drivers/hid/surface-hid/Kconfig"
 
 source "drivers/hid/intel-thc-hid/Kconfig"
 
+source "drivers/hid/dockchannel/Kconfig"
+
 endif # HID
 
 # USB support may be used with HID disabled
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 23e6e3dd0c56..c9b4b1aff247 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -182,3 +182,5 @@ obj-$(CONFIG_AMD_SFH_HID)       += amd-sfh-hid/
 obj-$(CONFIG_SURFACE_HID_CORE)  += surface-hid/
 
 obj-$(CONFIG_INTEL_THC_HID)     += intel-thc-hid/
+
+obj-$(CONFIG_APPLE_DOCKCHANNEL_HID)	+= dockchannel/
diff --git a/drivers/hid/dockchannel/Kconfig b/drivers/hid/dockchannel/Kconfig
new file mode 100644
index 000000000000..fca09ef74403
--- /dev/null
+++ b/drivers/hid/dockchannel/Kconfig
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0-only OR MIT
+
+config APPLE_DOCKCHANNEL_HID
+	tristate "HID over Apple DockChannel"
+	depends on APPLE_DOCKCHANNEL
+	depends on APPLE_RTKIT
+	depends on HID
+	depends on INPUT
+	depends on OF
+	help
+	  This provides a HID transport layer over the Apple DockChannel
+	  mailbox interface. It is required to support the internal keyboard
+	  and trackpad on M2 and later MacBook models.
+
+	  Say Y here if you have an M2 or later Apple MacBook.
diff --git a/drivers/hid/dockchannel/Makefile b/drivers/hid/dockchannel/Makefile
new file mode 100644
index 000000000000..d1a82aa57a69
--- /dev/null
+++ b/drivers/hid/dockchannel/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only OR MIT
+
+obj-$(CONFIG_APPLE_DOCKCHANNEL_HID)	+= apple-hid.o
diff --git a/drivers/hid/dockchannel/apple-hid.c b/drivers/hid/dockchannel/apple-hid.c
new file mode 100644
index 000000000000..162fcfb5ab1c
--- /dev/null
+++ b/drivers/hid/dockchannel/apple-hid.c
@@ -0,0 +1,1130 @@
+// SPDX-License-Identifier: GPL-2.0-only OR MIT
+/*
+ * Apple DockChannel HID transport driver
+ *
+ * Copyright The Asahi Linux Contributors
+ */
+
+#include <linux/bitfield.h>
+#include <linux/completion.h>
+#include <linux/ctype.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/dma-mapping.h>
+#include <linux/hid.h>
+#include <linux/mailbox/apple-dockchannel.h>
+#include <linux/mailbox_client.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/slab.h>
+#include <linux/soc/apple/rtkit.h>
+#include <linux/spinlock.h>
+#include <linux/string.h>
+#include <linux/unaligned.h>
+#include <linux/workqueue.h>
+
+#define APPLE_ASC_CPU_CONTROL		0x44
+#define APPLE_ASC_CPU_CONTROL_RUN	BIT(4)
+
+#define COMMAND_TIMEOUT_MS	1000
+#define START_TIMEOUT_MS	2000
+
+#define MAX_INTERFACES		16
+
+#define DCHID_MAX_PAYLOAD	0xffff
+#define DCHID_CHECKSUM_LEN	4
+#define DCHID_RX_BUF_SIZE	(sizeof(struct dchid_hdr) + DCHID_MAX_PAYLOAD + \
+				 DCHID_CHECKSUM_LEN)
+
+#define DCHID_CHANNEL_CMD	0x11
+#define DCHID_CHANNEL_REPORT	0x12
+#define DCHID_CHECKSUM_SEED	0xffffffff
+
+struct dchid_hdr {
+	u8 hdr_len;
+	u8 channel;
+	__le16 length;
+	u8 seq;
+	u8 iface;
+	__le16 pad;
+} __packed;
+
+#define IFACE_COMM	0
+
+#define FLAGS_GROUP	GENMASK(7, 6)
+#define FLAGS_REQ	GENMASK(5, 0)
+
+#define REQ_SET_REPORT	0
+#define REQ_GET_REPORT	1
+
+struct dchid_subhdr {
+	u8 flags;
+	u8 unk;
+	__le16 length;
+	__le32 retcode;
+} __packed;
+
+#define EVENT_INIT	0xf0
+#define EVENT_READY	0xf1
+
+struct dchid_init_hdr {
+	u8 type;
+	u8 unk1;
+	u8 unk2;
+	u8 iface;
+	char name[16];
+	u8 more_packets;
+	u8 unkpad;
+} __packed;
+
+#define INIT_HID_DESCRIPTOR	0
+#define INIT_TERMINATOR		2
+#define INIT_PRODUCT_NAME	7
+
+#define CMD_RESET_INTERFACE	0x40
+#define CMD_RESET_INTERFACE_SUB	1
+#define CMD_ENABLE_INTERFACE	0xb4
+
+struct dchid_init_block_hdr {
+	__le16 type;
+	__le16 length;
+} __packed;
+
+#define STM_REPORT_ID		0x10
+#define STM_REPORT_SERIAL	0x11
+
+struct dchid_stm_id {
+	u8 unk;
+	__le16 vendor_id;
+	__le16 product_id;
+	__le16 version_number;
+	u8 unk2;
+	u8 unk3;
+	u8 keyboard_type;
+	u8 serial_length;
+	/* Serial follows, but we grab it with a different report. */
+} __packed;
+
+struct dchid_work {
+	struct work_struct work;
+	struct dchid_iface *iface;
+
+	struct dchid_hdr hdr;
+	u8 data[];
+};
+
+struct dchid_iface {
+	struct dchid_dev *dchid;
+	struct hid_device *hid;
+	struct workqueue_struct *wq;
+
+	bool creating;
+	struct work_struct create_work;
+
+	int index;
+	const char *name;
+	struct fwnode_handle *fwnode;
+
+	u8 tx_seq;
+	bool deferred;
+	bool starting;
+	bool open;
+	struct completion ready;
+
+	void *hid_desc;
+	size_t hid_desc_len;
+
+	/* Lock for command submission state below */
+	spinlock_t out_lock;
+	u32 out_flags;
+	int out_report;
+	u32 retcode;
+	void *resp_buf;
+	size_t resp_size;
+	struct completion out_complete;
+};
+
+struct dchid_dev {
+	struct device *dev;
+	struct mbox_client dc_mbox_client;
+	struct mbox_chan *dc_mbox;
+
+	struct apple_rtkit *rtk;
+	void __iomem *asc_base;
+	void __iomem *sram_base;
+	struct resource sram_res;
+
+	bool id_ready;
+	struct dchid_stm_id device_id;
+	char serial[64];
+
+	u8 *rx_buf;
+	size_t rx_len;
+
+	struct dchid_iface *comm;
+	struct mutex ifaces_lock; /* protects ifaces array */
+	struct dchid_iface *ifaces[MAX_INTERFACES];
+
+	/* Workqueue to asynchronously create HID devices */
+	struct workqueue_struct *new_iface_wq;
+};
+
+static void dchid_destroy_wq(void *data)
+{
+	struct workqueue_struct *wq = data;
+
+	destroy_workqueue(wq);
+}
+
+static void dchid_fwnode_release(void *data)
+{
+	fwnode_handle_put(data);
+}
+
+static void dchid_free_mbox(void *data)
+{
+	mbox_free_channel(data);
+}
+
+static u32 dchid_checksum(const void *data, size_t len)
+{
+	const u8 *p = data;
+	u32 sum = 0;
+	int i;
+
+	while (len >= sizeof(u32)) {
+		sum += get_unaligned_le32(p);
+		p += sizeof(u32);
+		len -= sizeof(u32);
+	}
+
+	if (len) {
+		u32 tmp = 0;
+
+		for (i = 0; i < len; i++)
+			tmp |= p[i] << (i * 8);
+		sum += tmp;
+	}
+
+	return sum;
+}
+
+static struct dchid_iface *
+dchid_get_interface(struct dchid_dev *dchid, int index, const char *name)
+{
+	struct dchid_iface *iface;
+	struct fwnode_handle *fwnode;
+	int ret;
+
+	if (index >= MAX_INTERFACES) {
+		dev_err(dchid->dev, "interface index %d out of range\n", index);
+		return NULL;
+	}
+
+	mutex_lock(&dchid->ifaces_lock);
+	if (dchid->ifaces[index]) {
+		iface = dchid->ifaces[index];
+		mutex_unlock(&dchid->ifaces_lock);
+		return iface;
+	}
+
+	iface = devm_kzalloc(dchid->dev, sizeof(*iface), GFP_KERNEL);
+	if (!iface) {
+		mutex_unlock(&dchid->ifaces_lock);
+		return NULL;
+	}
+
+	iface->index = index;
+	iface->name = devm_kstrdup(dchid->dev, name, GFP_KERNEL);
+	if (!iface->name) {
+		mutex_unlock(&dchid->ifaces_lock);
+		return NULL;
+	}
+
+	iface->dchid = dchid;
+	iface->out_report = -1;
+	init_completion(&iface->out_complete);
+	init_completion(&iface->ready);
+	spin_lock_init(&iface->out_lock);
+
+	iface->wq = alloc_ordered_workqueue("dchid-%s", 0, iface->name);
+	if (!iface->wq) {
+		mutex_unlock(&dchid->ifaces_lock);
+		return NULL;
+	}
+
+	ret = devm_add_action_or_reset(dchid->dev, dchid_destroy_wq, iface->wq);
+	if (ret) {
+		mutex_unlock(&dchid->ifaces_lock);
+		return NULL;
+	}
+
+	if (!strcmp(name, "comm")) {
+		/* Comm is not a HID subdevice */
+		dchid->ifaces[index] = iface;
+		mutex_unlock(&dchid->ifaces_lock);
+		return iface;
+	}
+
+	fwnode = device_get_named_child_node(dchid->dev, name);
+	if (fwnode) {
+		iface->fwnode = fwnode;
+		ret = devm_add_action_or_reset(dchid->dev, dchid_fwnode_release,
+					       iface->fwnode);
+		if (ret) {
+			mutex_unlock(&dchid->ifaces_lock);
+			return NULL;
+		}
+	} else {
+		iface->fwnode = dev_fwnode(dchid->dev);
+	}
+
+	dchid->ifaces[index] = iface;
+	mutex_unlock(&dchid->ifaces_lock);
+	return iface;
+}
+
+static int dchid_send(struct dchid_iface *iface, u32 flags, const void *msg,
+		      size_t size)
+{
+	struct dchid_dev *dchid = iface->dchid;
+	size_t payload_padded = round_up(size, sizeof(u32));
+	size_t total_len = sizeof(struct dchid_hdr) + sizeof(struct dchid_subhdr) +
+			   payload_padded + DCHID_CHECKSUM_LEN;
+	struct apple_dockchannel_msg dc_msg;
+	struct dchid_hdr *hdr;
+	struct dchid_subhdr *sub;
+	u32 *checksum_ptr;
+	u8 *buf;
+	int ret;
+
+	if (total_len > DCHID_RX_BUF_SIZE)
+		return -EINVAL;
+
+	buf = kzalloc(total_len, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	hdr = (struct dchid_hdr *)buf;
+	sub = (struct dchid_subhdr *)(buf + sizeof(*hdr));
+	checksum_ptr = (u32 *)(buf + total_len - DCHID_CHECKSUM_LEN);
+
+	hdr->hdr_len = sizeof(*hdr);
+	hdr->channel = DCHID_CHANNEL_CMD;
+	hdr->length = cpu_to_le16(payload_padded + sizeof(*sub));
+	hdr->seq = iface->tx_seq;
+	hdr->iface = iface->index;
+
+	sub->flags = (u8)flags;
+	sub->length = cpu_to_le16(size);
+
+	memcpy(buf + sizeof(*hdr) + sizeof(*sub), msg, size);
+
+	*checksum_ptr = 0xffffffff - dchid_checksum(buf, total_len - DCHID_CHECKSUM_LEN);
+
+	dc_msg.data = buf;
+	dc_msg.len = total_len;
+	ret = mbox_send_message(dchid->dc_mbox, &dc_msg);
+	kfree(buf);
+
+	return ret < 0 ? ret : 0;
+}
+
+static int dchid_cmd(struct dchid_iface *iface, u32 type, u32 req,
+		     void *data, size_t size, void *resp_buf, size_t resp_size)
+{
+	unsigned long flags;
+	int ret;
+	int report_id;
+	bool timed_out = false;
+	u32 out_flags;
+
+	if (size < 1)
+		return -EINVAL;
+
+	report_id = *(u8 *)data;
+	out_flags = FIELD_PREP(FLAGS_GROUP, type) | FIELD_PREP(FLAGS_REQ, req);
+
+	spin_lock_irqsave(&iface->out_lock, flags);
+
+	/* Only one command can be in flight per interface */
+	if (WARN_ON(iface->out_report != -1)) {
+		spin_unlock_irqrestore(&iface->out_lock, flags);
+		return -EBUSY;
+	}
+
+	iface->out_report = report_id;
+	iface->out_flags = out_flags;
+	iface->retcode = 0;
+	iface->resp_buf = resp_buf;
+	iface->resp_size = resp_size;
+	reinit_completion(&iface->out_complete);
+
+	spin_unlock_irqrestore(&iface->out_lock, flags);
+
+	ret = dchid_send(iface, out_flags, data, size);
+	if (ret < 0) {
+		spin_lock_irqsave(&iface->out_lock, flags);
+		iface->out_report = -1;
+		iface->resp_buf = NULL;
+		iface->resp_size = 0;
+		spin_unlock_irqrestore(&iface->out_lock, flags);
+		return ret;
+	}
+
+	if (!wait_for_completion_timeout(&iface->out_complete,
+					 msecs_to_jiffies(COMMAND_TIMEOUT_MS))) {
+		dev_err(iface->dchid->dev, "command 0x%x to iface %d (%s) timed out\n",
+			report_id, iface->index, iface->name);
+		timed_out = true;
+	}
+
+	spin_lock_irqsave(&iface->out_lock, flags);
+
+	if (timed_out && iface->out_report == report_id) {
+		ret = -ETIMEDOUT;
+	} else if (iface->retcode) {
+		dev_err(iface->dchid->dev,
+			"command 0x%x to iface %d (%s) failed with err 0x%x\n",
+			report_id, iface->index, iface->name, iface->retcode);
+		ret = -EIO;
+	} else {
+		ret = iface->resp_size;
+	}
+
+	iface->tx_seq++;
+	iface->out_report = -1;
+	iface->resp_buf = NULL;
+	iface->resp_size = 0;
+	spin_unlock_irqrestore(&iface->out_lock, flags);
+
+	return ret;
+}
+
+static int dchid_comm_cmd(struct dchid_dev *dchid, void *cmd, size_t size)
+{
+	return dchid_cmd(dchid->comm, HID_FEATURE_REPORT, REQ_SET_REPORT,
+			 cmd, size, NULL, 0);
+}
+
+static int dchid_enable_interface(struct dchid_iface *iface)
+{
+	u8 cmd[] = { CMD_ENABLE_INTERFACE, iface->index };
+
+	return dchid_comm_cmd(iface->dchid, cmd, sizeof(cmd));
+}
+
+static int dchid_reset_interface(struct dchid_iface *iface, int state)
+{
+	u8 cmd[] = { CMD_RESET_INTERFACE, CMD_RESET_INTERFACE_SUB, iface->index,
+		     (u8)state };
+
+	return dchid_comm_cmd(iface->dchid, cmd, sizeof(cmd));
+}
+
+static int dchid_start_interface(struct dchid_iface *iface)
+{
+	if (iface->starting)
+		return -EINPROGRESS;
+
+	dev_dbg(iface->dchid->dev, "starting interface %s\n", iface->name);
+
+	iface->starting = true;
+	dchid_reset_interface(iface, 0);
+	dchid_reset_interface(iface, 2);
+
+	return 0;
+}
+
+static int dchid_start(struct hid_device *hdev)
+{
+	return 0;
+}
+
+static int dchid_open(struct hid_device *hdev)
+{
+	struct dchid_iface *iface = hdev->driver_data;
+	int ret;
+
+	if (!completion_done(&iface->ready)) {
+		ret = dchid_start_interface(iface);
+		if (ret < 0)
+			return ret;
+
+		if (!wait_for_completion_timeout(&iface->ready,
+						 msecs_to_jiffies(START_TIMEOUT_MS))) {
+			dev_err(iface->dchid->dev, "iface %s start timed out\n",
+				iface->name);
+			return -ETIMEDOUT;
+		}
+	}
+
+	iface->open = true;
+	return 0;
+}
+
+static void dchid_close(struct hid_device *hdev)
+{
+	struct dchid_iface *iface = hdev->driver_data;
+
+	iface->open = false;
+}
+
+static int dchid_parse(struct hid_device *hdev)
+{
+	struct dchid_iface *iface = hdev->driver_data;
+
+	return hid_parse_report(hdev, iface->hid_desc, iface->hid_desc_len);
+}
+
+/* Note: buf excludes report number. */
+static int dchid_get_report_cmd(struct dchid_iface *iface, u8 reportnum,
+				void *buf, size_t len)
+{
+	int ret;
+
+	ret = dchid_cmd(iface, HID_FEATURE_REPORT, REQ_GET_REPORT, &reportnum, 1,
+			buf, len);
+
+	return ret <= 0 ? ret : ret - 1;
+}
+
+/* Note: buf includes report number. */
+static int dchid_set_report(struct dchid_iface *iface, void *buf, size_t len)
+{
+	return dchid_cmd(iface, HID_OUTPUT_REPORT, REQ_SET_REPORT, buf, len,
+			 NULL, 0);
+}
+
+static int dchid_raw_request(struct hid_device *hdev, unsigned char reportnum,
+			     __u8 *buf, size_t len, unsigned char rtype,
+			     int reqtype)
+{
+	struct dchid_iface *iface = hdev->driver_data;
+
+	switch (reqtype) {
+	case HID_REQ_GET_REPORT:
+		if (len < 1)
+			return -EINVAL;
+
+		buf[0] = reportnum;
+		return dchid_cmd(iface, rtype, REQ_GET_REPORT, &reportnum, 1,
+				 buf + 1, len - 1);
+	case HID_REQ_SET_REPORT:
+		return dchid_set_report(iface, buf, len);
+	default:
+		return -EIO;
+	}
+}
+
+static const struct hid_ll_driver dchid_ll = {
+	.start = dchid_start,
+	.open = dchid_open,
+	.close = dchid_close,
+	.parse = dchid_parse,
+	.raw_request = dchid_raw_request,
+};
+
+static void dchid_create_interface_work(struct work_struct *ws)
+{
+	struct dchid_iface *iface = container_of(ws, struct dchid_iface, create_work);
+	struct dchid_dev *dchid = iface->dchid;
+	struct hid_device *hid;
+	char cap_name[16];
+	int ret;
+
+	if (iface->hid) {
+		dev_warn(dchid->dev, "interface %s already created\n", iface->name);
+		goto done;
+	}
+
+	ret = dchid_enable_interface(iface);
+	if (ret < 0) {
+		dev_warn(dchid->dev, "failed to enable %s: %d\n", iface->name, ret);
+		goto done;
+	}
+
+	iface->deferred = false;
+
+	hid = hid_allocate_device();
+	if (IS_ERR(hid))
+		goto done;
+
+	strscpy(cap_name, iface->name, sizeof(cap_name));
+	if (cap_name[0])
+		cap_name[0] = toupper(cap_name[0]);
+	snprintf(hid->name, sizeof(hid->name), "Apple DockChannel %s", cap_name);
+
+	snprintf(hid->phys, sizeof(hid->phys), "%s.%d", dev_name(dchid->dev),
+		 iface->index);
+	strscpy(hid->uniq, dchid->serial, sizeof(hid->uniq));
+
+	hid->ll_driver = &dchid_ll;
+	hid->bus = BUS_HOST;
+	hid->vendor = le16_to_cpu(dchid->device_id.vendor_id);
+	hid->product = le16_to_cpu(dchid->device_id.product_id);
+	hid->version = le16_to_cpu(dchid->device_id.version_number);
+	hid->type = HID_TYPE_OTHER;
+	if (!strcmp(iface->name, "keyboard")) {
+		u32 country_code;
+
+		hid->group = HID_GROUP_APPLE_DOCKCHANNEL;
+
+		/*
+		 * The device provides no reliable way to get the keyboard
+		 * country code, so board devicetrees provide it instead,
+		 * filled by the bootloader.
+		 */
+		if (!fwnode_property_read_u32(iface->fwnode, "hid-country-code",
+					      &country_code))
+			hid->country = country_code;
+	}
+
+	hid->dev.parent = iface->dchid->dev;
+	hid->driver_data = iface;
+	iface->hid = hid;
+
+	ret = hid_add_device(hid);
+	if (ret < 0) {
+		iface->hid = NULL;
+		hid_destroy_device(hid);
+		dev_warn(iface->dchid->dev, "failed to register HID device %s\n",
+			 iface->name);
+	}
+
+done:
+	iface->creating = false;
+}
+
+static int dchid_create_interface(struct dchid_iface *iface)
+{
+	if (iface->creating)
+		return -EBUSY;
+
+	iface->creating = true;
+	INIT_WORK(&iface->create_work, dchid_create_interface_work);
+	return queue_work(iface->dchid->new_iface_wq, &iface->create_work);
+}
+
+static void dchid_handle_descriptor(struct dchid_iface *iface, void *hid_desc,
+				    size_t desc_len)
+{
+	u8 *rdesc;
+	int i;
+
+	if (iface->hid)
+		return;
+
+	rdesc = devm_kmemdup(iface->dchid->dev, hid_desc, desc_len,
+			     GFP_KERNEL);
+	if (!rdesc)
+		return;
+
+	/* Fix up oversized report sizes in DockChannel report descriptors */
+	if (desc_len >= 5) {
+		for (i = 0; i <= (int)desc_len - 5; i++) {
+			if (rdesc[i] == 0x76 && rdesc[i + 1] == 0x00 &&
+			    rdesc[i + 2] == 0x40 && rdesc[i + 3] == 0x95) {
+				u8 count = rdesc[i + 4];
+
+				if (count > 0 && count < 32) {
+					dev_info(iface->dchid->dev,
+						 "fixing up interface %s (%d) report size\n",
+						 iface->name, iface->index);
+					rdesc[i] = 0x75;
+					rdesc[i + 1] = 0x08;
+					rdesc[i + 2] = 0x96;
+					rdesc[i + 3] = 0x00;
+					rdesc[i + 4] = count * 8;
+				}
+			}
+		}
+	}
+
+	iface->hid_desc = rdesc;
+	iface->hid_desc_len = desc_len;
+}
+
+static void dchid_handle_ready(struct dchid_dev *dchid, void *data, size_t length)
+{
+	struct dchid_iface *iface;
+	u8 *pkt = data;
+	u8 index;
+	int i;
+	int ret;
+
+	if (length < 2)
+		return;
+
+	index = pkt[1];
+	if (index >= MAX_INTERFACES)
+		return;
+
+	iface = dchid->ifaces[index];
+	if (!iface)
+		return;
+
+	dev_dbg(dchid->dev, "interface %s is now ready\n", iface->name);
+	complete_all(&iface->ready);
+
+	/* When STM is ready, grab global device info */
+	if (!strcmp(iface->name, "stm")) {
+		ret = dchid_get_report_cmd(iface, STM_REPORT_ID, &dchid->device_id,
+					   sizeof(dchid->device_id));
+		if (ret < (int)sizeof(dchid->device_id)) {
+			dev_warn(iface->dchid->dev, "failed to get device ID from STM\n");
+			/* Fake it and keep going. Things might still work. */
+			memset(&dchid->device_id, 0, sizeof(dchid->device_id));
+		}
+
+		ret = dchid_get_report_cmd(iface, STM_REPORT_SERIAL, dchid->serial,
+					   sizeof(dchid->serial) - 1);
+		if (ret < 0) {
+			dev_warn(iface->dchid->dev, "failed to get serial from STM\n");
+			dchid->serial[0] = 0;
+		}
+
+		dchid->id_ready = true;
+		for (i = 0; i < MAX_INTERFACES; i++) {
+			if (!dchid->ifaces[i] || !dchid->ifaces[i]->deferred)
+				continue;
+			dchid_create_interface(dchid->ifaces[i]);
+		}
+	}
+}
+
+static void dchid_handle_init(struct dchid_dev *dchid, void *data, size_t length)
+{
+	struct dchid_init_hdr *hdr = data;
+	struct dchid_init_block_hdr *blk;
+	struct dchid_iface *iface;
+	u8 *p = data;
+
+	if (length < sizeof(*hdr))
+		return;
+
+	iface = dchid_get_interface(dchid, hdr->iface, hdr->name);
+	if (!iface)
+		return;
+
+	p += sizeof(*hdr);
+	length -= sizeof(*hdr);
+
+	while (length >= sizeof(*blk)) {
+		u16 blk_len;
+
+		blk = (struct dchid_init_block_hdr *)p;
+		p += sizeof(*blk);
+		length -= sizeof(*blk);
+
+		blk_len = le16_to_cpu(blk->length);
+		if (blk_len > length)
+			break;
+
+		switch (le16_to_cpu(blk->type)) {
+		case INIT_HID_DESCRIPTOR:
+			dchid_handle_descriptor(iface, p, blk_len);
+			break;
+		case INIT_PRODUCT_NAME:
+			if (blk_len > 0 && p[blk_len - 1] != 0)
+				dev_warn(dchid->dev, "unterminated product name for %s\n",
+					 iface->name);
+			break;
+		}
+
+		p += blk_len;
+		length -= blk_len;
+
+		if (le16_to_cpu(blk->type) == INIT_TERMINATOR)
+			break;
+	}
+
+	if (hdr->more_packets)
+		return;
+
+	/*
+	 * 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"))
+		dchid_create_interface(iface);
+	else
+		iface->deferred = true;
+}
+
+static void dchid_handle_event(struct dchid_dev *dchid, void *data, size_t length)
+{
+	u8 *p = data;
+
+	if (!length)
+		return;
+
+	switch (*p) {
+	case EVENT_INIT:
+		dchid_handle_init(dchid, data, length);
+		break;
+	case EVENT_READY:
+		dchid_handle_ready(dchid, data, length);
+		break;
+	}
+}
+
+static void dchid_handle_report(struct dchid_iface *iface, void *data, size_t length)
+{
+	if (!iface->hid || !iface->open)
+		return;
+
+	hid_input_report(iface->hid, HID_INPUT_REPORT, data, length, 1);
+}
+
+static void dchid_packet_work(struct work_struct *ws)
+{
+	struct dchid_work *work = container_of(ws, struct dchid_work, work);
+	struct dchid_subhdr *shdr = (void *)work->data;
+	struct dchid_dev *dchid = work->iface->dchid;
+	u16 hdr_len = le16_to_cpu(work->hdr.length);
+	u16 sub_len;
+	int type;
+	u8 *payload;
+
+	if (hdr_len < sizeof(*shdr)) {
+		dev_err(dchid->dev, "bad subheader length\n");
+		goto done;
+	}
+
+	sub_len = le16_to_cpu(shdr->length);
+	if (sub_len > hdr_len - sizeof(*shdr)) {
+		dev_err(dchid->dev, "bad subheader length\n");
+		goto done;
+	}
+
+	type = FIELD_GET(FLAGS_GROUP, shdr->flags);
+	payload = work->data + sizeof(*shdr);
+
+	switch (type) {
+	case HID_INPUT_REPORT:
+		if (work->hdr.iface == IFACE_COMM)
+			dchid_handle_event(dchid, payload, sub_len);
+		else
+			dchid_handle_report(work->iface, payload, sub_len);
+		break;
+	}
+
+done:
+	kfree(work);
+}
+
+static void dchid_handle_ack(struct dchid_iface *iface, struct dchid_hdr *hdr,
+			     void *data)
+{
+	struct dchid_subhdr *shdr = data;
+	u8 *payload = data + sizeof(*shdr);
+	u16 hdr_len = le16_to_cpu(hdr->length);
+	u16 sub_len = le16_to_cpu(shdr->length);
+	unsigned long flags;
+	bool complete_cmd = false;
+
+	if (hdr_len < sizeof(*shdr) || sub_len > hdr_len - sizeof(*shdr) ||
+	    sub_len < 1)
+		return;
+
+	spin_lock_irqsave(&iface->out_lock, flags);
+
+	if (shdr->flags == iface->out_flags && iface->tx_seq == hdr->seq &&
+	    iface->out_report == payload[0]) {
+		if (iface->resp_buf && iface->resp_size)
+			memcpy(iface->resp_buf, payload + 1,
+			       min_t(size_t, sub_len - 1, iface->resp_size));
+
+		iface->resp_size = sub_len;
+		iface->out_report = -1;
+		iface->retcode = le32_to_cpu(shdr->retcode);
+		complete_cmd = true;
+	}
+
+	spin_unlock_irqrestore(&iface->out_lock, flags);
+
+	if (complete_cmd)
+		complete(&iface->out_complete);
+}
+
+static void dchid_process_packet(struct dchid_dev *dchid, struct dchid_hdr *hdr,
+				 u8 *payload, size_t payload_len, u8 *packet,
+				 size_t packet_len)
+{
+	struct dchid_work *work;
+
+	if (dchid_checksum(packet, packet_len) != DCHID_CHECKSUM_SEED) {
+		dev_err_ratelimited(dchid->dev, "checksum error\n");
+		return;
+	}
+
+	if (payload_len < sizeof(struct dchid_subhdr))
+		return;
+
+	if (hdr->iface >= MAX_INTERFACES || !dchid->ifaces[hdr->iface])
+		return;
+
+	if (hdr->channel == DCHID_CHANNEL_CMD) {
+		dchid_handle_ack(dchid->ifaces[hdr->iface], hdr, payload);
+		return;
+	}
+
+	if (hdr->channel != DCHID_CHANNEL_REPORT)
+		return;
+
+	work = kzalloc(sizeof(*work) + payload_len, GFP_ATOMIC);
+	if (!work)
+		return;
+
+	work->hdr = *hdr;
+	work->iface = dchid->ifaces[hdr->iface];
+	memcpy(work->data, payload, payload_len);
+	INIT_WORK(&work->work, dchid_packet_work);
+
+	queue_work(work->iface->wq, &work->work);
+}
+
+static void dchid_consume_rx(struct dchid_dev *dchid)
+{
+	while (dchid->rx_len >= sizeof(struct dchid_hdr)) {
+		struct dchid_hdr *hdr = (struct dchid_hdr *)dchid->rx_buf;
+		size_t payload_len;
+		size_t packet_len;
+
+		if (hdr->hdr_len != sizeof(*hdr)) {
+			dev_err_ratelimited(dchid->dev, "bad header length %u\n",
+					    hdr->hdr_len);
+			dchid->rx_len = 0;
+			return;
+		}
+
+		payload_len = le16_to_cpu(hdr->length);
+		packet_len = sizeof(*hdr) + payload_len + DCHID_CHECKSUM_LEN;
+		if (packet_len > DCHID_RX_BUF_SIZE) {
+			dev_err_ratelimited(dchid->dev, "oversized packet %zu\n",
+					    packet_len);
+			dchid->rx_len = 0;
+			return;
+		}
+
+		if (dchid->rx_len < packet_len)
+			return;
+
+		dchid_process_packet(dchid, hdr, dchid->rx_buf + sizeof(*hdr),
+				     payload_len, dchid->rx_buf, packet_len);
+
+		dchid->rx_len -= packet_len;
+		memmove(dchid->rx_buf, dchid->rx_buf + packet_len, dchid->rx_len);
+	}
+}
+
+static void dchid_rx_callback(struct mbox_client *cl, void *mssg)
+{
+	struct dchid_dev *dchid = container_of(cl, struct dchid_dev, dc_mbox_client);
+	struct apple_dockchannel_msg *msg = mssg;
+
+	if (!msg || !msg->data || !msg->len)
+		return;
+
+	if (msg->len > DCHID_RX_BUF_SIZE - dchid->rx_len) {
+		dev_err_ratelimited(dchid->dev, "RX buffer overflow\n");
+		dchid->rx_len = 0;
+		return;
+	}
+
+	memcpy(dchid->rx_buf + dchid->rx_len, msg->data, msg->len);
+	dchid->rx_len += msg->len;
+
+	dchid_consume_rx(dchid);
+}
+
+static int dchid_rtkit_shmem_setup(void *cookie, struct apple_rtkit_shmem *bfr)
+{
+	struct dchid_dev *dchid = cookie;
+	struct resource res = {
+		.start = bfr->iova,
+		.end = bfr->iova + bfr->size - 1,
+		.name = "rtkit_map",
+	};
+
+	if (!bfr->iova) {
+		bfr->buffer = dma_alloc_coherent(dchid->dev, bfr->size,
+						 &bfr->iova, GFP_KERNEL);
+		if (!bfr->buffer)
+			return -ENOMEM;
+		return 0;
+	}
+
+	if (!dchid->sram_res.start)
+		return -EFAULT;
+
+	res.flags = dchid->sram_res.flags;
+	if (res.end < res.start || !resource_contains(&dchid->sram_res, &res))
+		return -EFAULT;
+
+	bfr->iomem = dchid->sram_base + (res.start - dchid->sram_res.start);
+	bfr->is_mapped = true;
+
+	return 0;
+}
+
+static void dchid_rtkit_shmem_destroy(void *cookie, struct apple_rtkit_shmem *bfr)
+{
+	struct dchid_dev *dchid = cookie;
+
+	if (bfr->buffer)
+		dma_free_coherent(dchid->dev, bfr->size, bfr->buffer, bfr->iova);
+}
+
+static const struct apple_rtkit_ops dchid_rtkit_ops = {
+	.shmem_setup = dchid_rtkit_shmem_setup,
+	.shmem_destroy = dchid_rtkit_shmem_destroy,
+};
+
+static int dchid_map_helper_cpu(struct platform_device *pdev, struct dchid_dev *dchid)
+{
+	struct resource *res;
+
+	dchid->asc_base = devm_platform_ioremap_resource_byname(pdev, "coproc-asc");
+	if (IS_ERR(dchid->asc_base))
+		return PTR_ERR(dchid->asc_base);
+
+	res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "coproc-sram");
+	if (!res)
+		return -EINVAL;
+
+	dchid->sram_res = *res;
+
+	dchid->sram_base = devm_ioremap_resource(&pdev->dev, res);
+	if (IS_ERR(dchid->sram_base))
+		return PTR_ERR(dchid->sram_base);
+
+	return 0;
+}
+
+static int dchid_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct dchid_dev *dchid;
+	int ret;
+
+	ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(44));
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to set DMA mask\n");
+
+	dchid = devm_kzalloc(dev, sizeof(*dchid), GFP_KERNEL);
+	if (!dchid)
+		return -ENOMEM;
+
+	dchid->rx_buf = devm_kmalloc(dev, DCHID_RX_BUF_SIZE, GFP_KERNEL);
+	if (!dchid->rx_buf)
+		return -ENOMEM;
+
+	dchid->dev = dev;
+	mutex_init(&dchid->ifaces_lock);
+	platform_set_drvdata(pdev, dchid);
+
+	ret = dchid_map_helper_cpu(pdev, dchid);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to map helper CPU\n");
+
+	dchid->dc_mbox_client.dev = dev;
+	dchid->dc_mbox_client.tx_block = true;
+	dchid->dc_mbox_client.rx_callback = dchid_rx_callback;
+
+	dchid->dc_mbox = mbox_request_channel_byname(&dchid->dc_mbox_client,
+						     "dockchannel");
+	if (IS_ERR(dchid->dc_mbox))
+		return dev_err_probe(dev, PTR_ERR(dchid->dc_mbox),
+				     "failed to request DockChannel mailbox\n");
+
+	ret = devm_add_action_or_reset(dev, dchid_free_mbox, dchid->dc_mbox);
+	if (ret)
+		return ret;
+
+	dchid->rtk = devm_apple_rtkit_init(dev, dchid, "asc", 0, &dchid_rtkit_ops);
+	if (IS_ERR(dchid->rtk))
+		return dev_err_probe(dev, PTR_ERR(dchid->rtk), "failed to init RTKit\n");
+
+	writel_relaxed(APPLE_ASC_CPU_CONTROL_RUN,
+		       dchid->asc_base + APPLE_ASC_CPU_CONTROL);
+
+	ret = apple_rtkit_wake(dchid->rtk);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to wake coprocessor\n");
+
+	dchid->new_iface_wq = alloc_ordered_workqueue("dchid-new", 0);
+	if (!dchid->new_iface_wq)
+		return dev_err_probe(dev, -ENOMEM, "failed to allocate workqueue\n");
+
+	ret = devm_add_action_or_reset(dev, dchid_destroy_wq, dchid->new_iface_wq);
+	if (ret)
+		return ret;
+
+	dchid->comm = dchid_get_interface(dchid, IFACE_COMM, "comm");
+	if (!dchid->comm)
+		return dev_err_probe(dev, -EIO, "failed to init comm interface\n");
+
+	return 0;
+}
+
+static void dchid_remove(struct platform_device *pdev)
+{
+	struct dchid_dev *dchid = platform_get_drvdata(pdev);
+	int i;
+
+	if (dchid->dc_mbox) {
+		devm_release_action(&pdev->dev, dchid_free_mbox, dchid->dc_mbox);
+		dchid->dc_mbox = NULL;
+	}
+
+	if (dchid->rtk && apple_rtkit_is_running(dchid->rtk))
+		apple_rtkit_quiesce(dchid->rtk);
+
+	if (dchid->asc_base)
+		writel_relaxed(0, dchid->asc_base + APPLE_ASC_CPU_CONTROL);
+
+	for (i = 0; i < MAX_INTERFACES; i++) {
+		struct dchid_iface *iface = dchid->ifaces[i];
+
+		if (!iface)
+			continue;
+
+		cancel_work_sync(&iface->create_work);
+		flush_workqueue(iface->wq);
+
+		if (iface->hid)
+			hid_destroy_device(iface->hid);
+	}
+
+	if (dchid->new_iface_wq)
+		flush_workqueue(dchid->new_iface_wq);
+}
+
+static const struct of_device_id dchid_of_match[] = {
+	{ .compatible = "apple,t8122-dockchannel-hid" },
+	{ .compatible = "apple,t8112-dockchannel-hid" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, dchid_of_match);
+
+static struct platform_driver dchid_platform_driver = {
+	.driver = {
+		.name = "dockchannel-hid",
+		.of_match_table = dchid_of_match,
+	},
+	.probe = dchid_probe,
+	.remove = dchid_remove,
+};
+module_platform_driver(dchid_platform_driver);
+
+MODULE_DESCRIPTION("Apple DockChannel HID transport driver");
+MODULE_AUTHOR("Hector Martin <marcan@marcan.st>");
+MODULE_AUTHOR("Michael Reeves <michael.reeves077@gmail.com>");
+MODULE_LICENSE("Dual MIT/GPL");

-- 
2.51.2



^ permalink raw reply related

* [PATCH 07/10] HID: apple: Add support for DockChannel HID keyboards
From: Michael Reeves via B4 Relay @ 2026-06-30 12:54 UTC (permalink / raw)
  To: 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
  Cc: asahi, linux-arm-kernel, linux-kernel, devicetree, iommu,
	linux-input, Michael Reeves
In-Reply-To: <20260630-apple-mtp-keyboard-final-v1-0-506d936a1707@gmail.com>

From: Michael Reeves <michael.reeves077@gmail.com>

DockChannel keyboards are registered as host-bus Apple HID devices
instead of USB or Bluetooth devices.

Match them in hid-apple, use the modern Magic Keyboard function-key
table, and fix up the oversized report-size descriptor pattern before
parsing.

Signed-off-by: Michael Reeves <michael.reeves077@gmail.com>
---
 drivers/hid/hid-apple.c | 139 ++++++++++++++++++++++++++++++++----------------
 include/linux/hid.h     |   1 +
 2 files changed, 94 insertions(+), 46 deletions(-)

diff --git a/drivers/hid/hid-apple.c b/drivers/hid/hid-apple.c
index bf7dd0fbf249..47c6ec09d5fa 100644
--- a/drivers/hid/hid-apple.c
+++ b/drivers/hid/hid-apple.c
@@ -390,6 +390,12 @@ static bool apple_is_omoton_kb066(struct hid_device *hdev)
 		strcmp(hdev->name, "Bluetooth Keyboard") == 0;
 }
 
+static bool apple_is_dockchannel_keyboard(struct hid_device *hdev)
+{
+	return hdev->bus == BUS_HOST &&
+	       hdev->group == HID_GROUP_APPLE_DOCKCHANNEL;
+}
+
 static inline void apple_setup_key_translation(struct input_dev *input,
 		const struct apple_key_translation *table)
 {
@@ -477,53 +483,57 @@ static int hidinput_apple_event(struct hid_device *hid, struct input_dev *input,
 		asc->fn_on = !!value;
 
 	if (real_fnmode) {
-		switch (hid->product) {
-		case USB_DEVICE_ID_APPLE_ALU_WIRELESS_ANSI:
-		case USB_DEVICE_ID_APPLE_ALU_WIRELESS_ISO:
-		case USB_DEVICE_ID_APPLE_ALU_WIRELESS_JIS:
-		case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2009_ANSI:
-		case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2009_ISO:
-		case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2009_JIS:
-		case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2011_ANSI:
-		case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2011_ISO:
-		case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2011_JIS:
-			table = magic_keyboard_alu_fn_keys;
-			break;
-		case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_2015:
-		case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_NUMPAD_2015:
-			table = magic_keyboard_2015_fn_keys;
-			break;
-		case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_2021:
-		case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_FINGERPRINT_2021:
-		case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_NUMPAD_2021:
-		case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_2024:
-		case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_FINGERPRINT_2024:
-		case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_NUMPAD_2024:
+		if (apple_is_dockchannel_keyboard(hid)) {
 			table = magic_keyboard_2021_and_2024_fn_keys;
-			break;
-		case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J132:
-		case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J213:
-		case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J680:
-		case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J680_ALT:
-			table = macbookpro_no_esc_fn_keys;
-			break;
-		case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J152F:
-		case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J214K:
-		case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J223:
-			table = macbookpro_dedicated_esc_fn_keys;
-			break;
-		case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J140K:
-		case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J230K:
-			table = apple_fn_keys;
-			break;
-		default:
-			if (hid->product >= USB_DEVICE_ID_APPLE_WELLSPRING4_ANSI &&
-			    hid->product <= USB_DEVICE_ID_APPLE_WELLSPRING4A_JIS)
-				table = macbookair_fn_keys;
-			else if (hid->product < 0x21d || hid->product >= 0x300)
-				table = powerbook_fn_keys;
-			else
+		} else {
+			switch (hid->product) {
+			case USB_DEVICE_ID_APPLE_ALU_WIRELESS_ANSI:
+			case USB_DEVICE_ID_APPLE_ALU_WIRELESS_ISO:
+			case USB_DEVICE_ID_APPLE_ALU_WIRELESS_JIS:
+			case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2009_ANSI:
+			case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2009_ISO:
+			case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2009_JIS:
+			case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2011_ANSI:
+			case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2011_ISO:
+			case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2011_JIS:
+				table = magic_keyboard_alu_fn_keys;
+				break;
+			case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_2015:
+			case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_NUMPAD_2015:
+				table = magic_keyboard_2015_fn_keys;
+				break;
+			case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_2021:
+			case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_FINGERPRINT_2021:
+			case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_NUMPAD_2021:
+			case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_2024:
+			case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_FINGERPRINT_2024:
+			case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_NUMPAD_2024:
+				table = magic_keyboard_2021_and_2024_fn_keys;
+				break;
+			case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J132:
+			case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J213:
+			case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J680:
+			case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J680_ALT:
+				table = macbookpro_no_esc_fn_keys;
+				break;
+			case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J152F:
+			case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J214K:
+			case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J223:
+				table = macbookpro_dedicated_esc_fn_keys;
+				break;
+			case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J140K:
+			case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J230K:
 				table = apple_fn_keys;
+				break;
+			default:
+				if (hid->product >= USB_DEVICE_ID_APPLE_WELLSPRING4_ANSI &&
+				    hid->product <= USB_DEVICE_ID_APPLE_WELLSPRING4A_JIS)
+					table = macbookair_fn_keys;
+				else if (hid->product < 0x21d || hid->product >= 0x300)
+					table = powerbook_fn_keys;
+				else
+					table = apple_fn_keys;
+			}
 		}
 
 		trans = apple_find_translation(table, code);
@@ -659,6 +669,7 @@ static void apple_battery_timer_tick(struct timer_list *t)
 /*
  * MacBook JIS keyboard has wrong logical maximum
  * Magic Keyboard JIS has wrong logical maximum
+ * Internal DockChannel keyboards can advertise oversized report sizes
  */
 static const __u8 *apple_report_fixup(struct hid_device *hdev, __u8 *rdesc,
 		unsigned int *rsize)
@@ -699,6 +710,27 @@ static const __u8 *apple_report_fixup(struct hid_device *hdev, __u8 *rdesc,
 		rdesc[3] = 0x06;
 	}
 
+	if (apple_is_dockchannel_keyboard(hdev) && *rsize >= 5) {
+		int i;
+
+		for (i = 0; i <= *rsize - 5; i++) {
+			if (rdesc[i] == 0x76 && rdesc[i + 1] == 0x00 &&
+			    rdesc[i + 2] == 0x40 && rdesc[i + 3] == 0x95) {
+				u8 count = rdesc[i + 4];
+
+				if (count > 0 && count < 32) {
+					hid_info(hdev,
+						 "fixing up DockChannel report size\n");
+					rdesc[i] = 0x75;
+					rdesc[i + 1] = 0x08;
+					rdesc[i + 2] = 0x96;
+					rdesc[i + 3] = 0x00;
+					rdesc[i + 4] = count * 8;
+				}
+			}
+		}
+	}
+
 	return rdesc;
 }
 
@@ -763,7 +795,7 @@ static int apple_input_configured(struct hid_device *hdev,
 	struct apple_sc *asc = hid_get_drvdata(hdev);
 
 	if (((asc->quirks & APPLE_HAS_FN) && !asc->fn_found) || apple_is_omoton_kb066(hdev)) {
-		hid_info(hdev, "Fn key not found (Apple Wireless Keyboard clone?), disabling Fn key handling\n");
+		hid_info(hdev, "Disabling function quirk for device without function key\n");
 		asc->quirks &= ~APPLE_HAS_FN;
 	}
 
@@ -1003,6 +1035,17 @@ static void apple_remove(struct hid_device *hdev)
 	hid_hw_stop(hdev);
 }
 
+static bool apple_match(struct hid_device *hdev, bool ignore_special_driver)
+{
+	if (ignore_special_driver)
+		return false;
+
+	if (hdev->group == HID_GROUP_APPLE_DOCKCHANNEL)
+		return apple_is_dockchannel_keyboard(hdev);
+
+	return true;
+}
+
 static const struct hid_device_id apple_devices[] = {
 	{ HID_USB_DEVICE(USB_VENDOR_ID_APPLE, USB_DEVICE_ID_APPLE_MIGHTYMOUSE),
 		.driver_data = APPLE_MIGHTYMOUSE | APPLE_INVERT_HWHEEL },
@@ -1224,6 +1267,9 @@ static const struct hid_device_id apple_devices[] = {
 		.driver_data = APPLE_HAS_FN | APPLE_ISO_TILDE_QUIRK },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_APPLE, USB_DEVICE_ID_APPLE_TOUCHBAR_BACKLIGHT),
 		.driver_data = APPLE_MAGIC_BACKLIGHT },
+	{ HID_DEVICE(BUS_HOST, HID_GROUP_APPLE_DOCKCHANNEL,
+		     HID_ANY_ID, HID_ANY_ID),
+		.driver_data = APPLE_HAS_FN | APPLE_ISO_TILDE_QUIRK },
 
 	{ }
 };
@@ -1232,6 +1278,7 @@ MODULE_DEVICE_TABLE(hid, apple_devices);
 static struct hid_driver apple_driver = {
 	.name = "apple",
 	.id_table = apple_devices,
+	.match = apple_match,
 	.report_fixup = apple_report_fixup,
 	.probe = apple_probe,
 	.remove = apple_remove,
diff --git a/include/linux/hid.h b/include/linux/hid.h
index 47dc0bc89fa4..0d40deec6295 100644
--- a/include/linux/hid.h
+++ b/include/linux/hid.h
@@ -441,6 +441,7 @@ struct hid_item {
 #define HID_GROUP_STEAM				0x0103
 #define HID_GROUP_LOGITECH_27MHZ_DEVICE		0x0104
 #define HID_GROUP_VIVALDI			0x0105
+#define HID_GROUP_APPLE_DOCKCHANNEL		0x0106
 
 /*
  * HID protocol status

-- 
2.51.2



^ permalink raw reply related

* [PATCH 04/10] dt-bindings: input: apple: Add DockChannel HID transport
From: Michael Reeves via B4 Relay @ 2026-06-30 12:54 UTC (permalink / raw)
  To: 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
  Cc: asahi, linux-arm-kernel, linux-kernel, devicetree, iommu,
	linux-input, Michael Reeves
In-Reply-To: <20260630-apple-mtp-keyboard-final-v1-0-506d936a1707@gmail.com>

From: Michael Reeves <michael.reeves077@gmail.com>

Apple internal keyboards and trackpads behind MTP are exposed through a
DockChannel HID transport.

Add the client binding tying together the RTKit ASC mailbox,
DockChannel mailbox, and MTP DART. The keyboard child can provide the
HID country code used by hid-apple to distinguish layout variants.

Signed-off-by: Michael Reeves <michael.reeves077@gmail.com>
---
 .../bindings/input/apple,dockchannel-hid.yaml      | 91 ++++++++++++++++++++++
 MAINTAINERS                                        |  1 +
 2 files changed, 92 insertions(+)

diff --git a/Documentation/devicetree/bindings/input/apple,dockchannel-hid.yaml b/Documentation/devicetree/bindings/input/apple,dockchannel-hid.yaml
new file mode 100644
index 000000000000..dbba4fc38971
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/apple,dockchannel-hid.yaml
@@ -0,0 +1,91 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/apple,dockchannel-hid.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Apple DockChannel HID Transport
+
+maintainers:
+  - Michael Reeves <michael.reeves077@gmail.com>
+
+description:
+  HID transport for keyboard and trackpad devices connected via the
+  DockChannel FIFO mailbox on Apple Silicon SoCs. The endpoint includes a
+  small RTKit coprocessor called MTP which must be booted before the HID
+  transport becomes available.
+
+properties:
+  compatible:
+    oneOf:
+      - items:
+          - const: apple,t8112-dockchannel-hid
+      - items:
+          - enum:
+              - apple,t6020-dockchannel-hid
+              - apple,t8122-dockchannel-hid
+          - const: apple,t8112-dockchannel-hid
+
+  reg:
+    items:
+      - description: Coprocessor ASC registers
+      - description: Coprocessor SRAM/mailbox registers
+
+  reg-names:
+    items:
+      - const: coproc-asc
+      - const: coproc-sram
+
+  mboxes:
+    items:
+      - description: ASC mailbox used for RTKit control
+      - description: DockChannel FIFO mailbox used for HID packets
+
+  mbox-names:
+    items:
+      - const: asc
+      - const: dockchannel
+
+  iommus:
+    maxItems: 1
+
+  keyboard:
+    type: object
+    properties:
+      hid-country-code:
+        $ref: /schemas/types.yaml#/definitions/uint32
+        description:
+          HID country code for the keyboard layout variant.
+
+    additionalProperties: false
+
+required:
+  - compatible
+  - reg
+  - reg-names
+  - mboxes
+  - mbox-names
+  - iommus
+
+additionalProperties: false
+
+examples:
+  - |
+    soc {
+        #address-cells = <2>;
+        #size-cells = <2>;
+
+        hid@24e400000 {
+            compatible = "apple,t8112-dockchannel-hid";
+            reg = <0x2 0x4e400000 0x0 0x4000>,
+                  <0x2 0x4ec00000 0x0 0x100000>;
+            reg-names = "coproc-asc", "coproc-sram";
+            mboxes = <&mtp_mbox>, <&mtp_dockchannel>;
+            mbox-names = "asc", "dockchannel";
+            iommus = <&mtp_dart 1>;
+
+            keyboard {
+                hid-country-code = <0>;
+            };
+        };
+    };
diff --git a/MAINTAINERS b/MAINTAINERS
index 741974f0f326..1f3c2cdb6e19 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2589,6 +2589,7 @@ F:	Documentation/devicetree/bindings/gpio/apple,smc-gpio.yaml
 F:	Documentation/devicetree/bindings/gpu/apple,agx.yaml
 F:	Documentation/devicetree/bindings/hwmon/apple,smc-hwmon.yaml
 F:	Documentation/devicetree/bindings/i2c/apple,i2c.yaml
+F:	Documentation/devicetree/bindings/input/apple,dockchannel-hid.yaml
 F:	Documentation/devicetree/bindings/input/touchscreen/apple,z2-multitouch.yaml
 F:	Documentation/devicetree/bindings/interrupt-controller/apple,*
 F:	Documentation/devicetree/bindings/iommu/apple,dart.yaml

-- 
2.51.2



^ permalink raw reply related

* [PATCH 03/10] dt-bindings: iommu: apple,dart: Add t8122 compatible
From: Michael Reeves via B4 Relay @ 2026-06-30 12:54 UTC (permalink / raw)
  To: 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
  Cc: asahi, linux-arm-kernel, linux-kernel, devicetree, iommu,
	linux-input, Michael Reeves
In-Reply-To: <20260630-apple-mtp-keyboard-final-v1-0-506d936a1707@gmail.com>

From: Michael Reeves <michael.reeves077@gmail.com>

The MTP block on t8122 has its own DART instance for coprocessor DMA.

Document the t8122 compatible with the existing t8110 fallback because
the programming model is shared.

Signed-off-by: Michael Reeves <michael.reeves077@gmail.com>
---
 Documentation/devicetree/bindings/iommu/apple,dart.yaml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Documentation/devicetree/bindings/iommu/apple,dart.yaml b/Documentation/devicetree/bindings/iommu/apple,dart.yaml
index 47ec7fa52c3a..e179199dbd3b 100644
--- a/Documentation/devicetree/bindings/iommu/apple,dart.yaml
+++ b/Documentation/devicetree/bindings/iommu/apple,dart.yaml
@@ -29,7 +29,9 @@ properties:
           - apple,t8110-dart
           - apple,t6000-dart
       - items:
-          - const: apple,t6020-dart
+          - enum:
+              - apple,t6020-dart
+              - apple,t8122-dart
           - const: apple,t8110-dart
 
   reg:

-- 
2.51.2



^ permalink raw reply related

* [PATCH 02/10] dt-bindings: mailbox: apple: Add DockChannel mailbox
From: Michael Reeves via B4 Relay @ 2026-06-30 12:54 UTC (permalink / raw)
  To: 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
  Cc: asahi, linux-arm-kernel, linux-kernel, devicetree, iommu,
	linux-input, Michael Reeves
In-Reply-To: <20260630-apple-mtp-keyboard-final-v1-0-506d936a1707@gmail.com>

From: Michael Reeves <michael.reeves077@gmail.com>

DockChannel is a FIFO and interrupt block used by Apple coprocessors
to exchange byte-stream traffic with the AP.

Describe it as a mailbox provider so HID and future serial clients can
use the common mailbox API.

Signed-off-by: Michael Reeves <michael.reeves077@gmail.com>
---
 .../bindings/mailbox/apple,dockchannel.yaml        | 75 ++++++++++++++++++++++
 MAINTAINERS                                        |  1 +
 2 files changed, 76 insertions(+)

diff --git a/Documentation/devicetree/bindings/mailbox/apple,dockchannel.yaml b/Documentation/devicetree/bindings/mailbox/apple,dockchannel.yaml
new file mode 100644
index 000000000000..4f326d8f3d5a
--- /dev/null
+++ b/Documentation/devicetree/bindings/mailbox/apple,dockchannel.yaml
@@ -0,0 +1,75 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/mailbox/apple,dockchannel.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Apple DockChannel FIFO Mailbox
+
+maintainers:
+  - Michael Reeves <michael.reeves077@gmail.com>
+
+description:
+  DockChannel is a hardware FIFO and interrupt block used on Apple SoCs for
+  low-latency byte stream communication with co-processors.
+
+properties:
+  compatible:
+    oneOf:
+      - items:
+          - const: apple,t8112-dockchannel
+      - items:
+          - enum:
+              - apple,t6020-dockchannel
+              - apple,t8122-dockchannel
+          - const: apple,t8112-dockchannel
+
+  reg:
+    items:
+      - description: Interrupt control registers
+      - description: FIFO configuration registers
+      - description: FIFO data registers
+
+  reg-names:
+    items:
+      - const: irq
+      - const: config
+      - const: data
+
+  interrupts:
+    maxItems: 1
+
+  "#mbox-cells":
+    const: 0
+
+  nonposted-mmio: true
+
+required:
+  - compatible
+  - reg
+  - reg-names
+  - interrupts
+  - "#mbox-cells"
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/interrupt-controller/apple-aic.h>
+    #include <dt-bindings/interrupt-controller/irq.h>
+
+    soc {
+        #address-cells = <2>;
+        #size-cells = <2>;
+
+        mailbox@24eb14000 {
+            compatible = "apple,t8112-dockchannel";
+            reg = <0x2 0x4eb14000 0x0 0x4000>,
+                  <0x2 0x4eb30000 0x0 0x4000>,
+                  <0x2 0x4eb34000 0x0 0x4000>;
+            reg-names = "irq", "config", "data";
+            interrupt-parent = <&aic>;
+            interrupts = <AIC_IRQ 850 IRQ_TYPE_LEVEL_HIGH>;
+            #mbox-cells = <0>;
+        };
+    };
diff --git a/MAINTAINERS b/MAINTAINERS
index 15011f5752a9..741974f0f326 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2594,6 +2594,7 @@ F:	Documentation/devicetree/bindings/interrupt-controller/apple,*
 F:	Documentation/devicetree/bindings/iommu/apple,dart.yaml
 F:	Documentation/devicetree/bindings/iommu/apple,sart.yaml
 F:	Documentation/devicetree/bindings/leds/backlight/apple,dwi-bl.yaml
+F:	Documentation/devicetree/bindings/mailbox/apple,dockchannel.yaml
 F:	Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml
 F:	Documentation/devicetree/bindings/mfd/apple,smc.yaml
 F:	Documentation/devicetree/bindings/net/bluetooth/brcm,bcm4377-bluetooth.yaml

-- 
2.51.2



^ permalink raw reply related

* [PATCH 01/10] dt-bindings: mailbox: Add Apple t8122 ASC mailbox
From: Michael Reeves via B4 Relay @ 2026-06-30 12:54 UTC (permalink / raw)
  To: 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
  Cc: asahi, linux-arm-kernel, linux-kernel, devicetree, iommu,
	linux-input, Michael Reeves
In-Reply-To: <20260630-apple-mtp-keyboard-final-v1-0-506d936a1707@gmail.com>

From: Michael Reeves <michael.reeves077@gmail.com>

The ASC mailbox on t8122 is compatible with the v4 mailbox block used
by other Apple Silicon SoCs.

Document the t8122 compatible in the v4 mailbox section so M3 device
trees can describe their coprocessor mailboxes.

Signed-off-by: Michael Reeves <michael.reeves077@gmail.com>
---
 Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml
index 28985cc62c25..946c909c6922 100644
--- a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml
+++ b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml
@@ -30,6 +30,7 @@ properties:
           - enum:
               - apple,t8103-asc-mailbox
               - apple,t8112-asc-mailbox
+              - apple,t8122-asc-mailbox
               - apple,t6000-asc-mailbox
               - apple,t6020-asc-mailbox
           - const: apple,asc-mailbox-v4

-- 
2.51.2



^ permalink raw reply related

* [PATCH 00/10] Add support for Apple Silicon DockChannel internal keyboards
From: Michael Reeves via B4 Relay @ 2026-06-30 12:54 UTC (permalink / raw)
  To: 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
  Cc: asahi, linux-arm-kernel, linux-kernel, devicetree, iommu,
	linux-input, Michael Reeves, Sasha Finkelstein

This series introduces support for the internal keyboards on Apple
Silicon M2 and M3 MacBook models.

On these platforms, built-in input devices are managed by a dedicated
coprocessor running an RTKit-based operating system. Communication
between this coprocessor and the main processor is carried out over a
low-latency hardware byte FIFO interface called DockChannel.

To support this input path, the series introduces a few new components:
- An apple-dockchannel mailbox driver to handle the low-level
  byte-stream FIFO.
- A DockChannel HID transport driver (apple-hid) that boots the
  coprocessor using the RTKit framework and encapsulates the HID
  protocol over the mailbox.
- Minor additions to the apple-rtkit and hid-apple drivers to support
  the TraceKit endpoint and integrate the keyboards into the existing
  input-quirks framework.
- Devicetree bindings and DTS updates to describe the nodes on M2 and
  M3 laptops.

The transport driver is based on an original out-of-tree implementation
by Hector Martin, but it has been significantly rewritten for upstream
inclusion to use the standard Linux mailbox framework and align better
with upstream HID design patterns and reduce reliance on hacks.

While the coprocessor manages both the keyboard and the trackpad, this
series only enables keyboard support. The keyboard can be initialised
without loading external firmware, whereas the trackpad requires
firmware. Trackpad support will be submitted in a subsequent series
once these base transport layers are established.

Tested on: MacBook Air M3 (J613).

Signed-off-by: Michael Reeves <michael.reeves077@gmail.com>
---
Michael Reeves (9):
      dt-bindings: mailbox: Add Apple t8122 ASC mailbox
      dt-bindings: mailbox: apple: Add DockChannel mailbox
      dt-bindings: iommu: apple,dart: Add t8122 compatible
      dt-bindings: input: apple: Add DockChannel HID transport
      mailbox: apple: Add DockChannel FIFO controller
      HID: apple: Add support for DockChannel HID keyboards
      HID: apple: Add DockChannel HID transport driver
      arm64: dts: apple: Add MTP DockChannel HID nodes
      arm64: dts: apple: Enable DockChannel HID on M2 and M3 laptops

Sasha Finkelstein (1):
      soc: apple: rtkit: Add tracekit endpoint

 .../bindings/input/apple,dockchannel-hid.yaml      |   91 ++
 .../devicetree/bindings/iommu/apple,dart.yaml      |    4 +-
 .../bindings/mailbox/apple,dockchannel.yaml        |   75 ++
 .../devicetree/bindings/mailbox/apple,mailbox.yaml |    1 +
 MAINTAINERS                                        |    5 +
 arch/arm64/boot/dts/apple/t602x-die0.dtsi          |   46 +
 arch/arm64/boot/dts/apple/t602x-j414-j416.dtsi     |   25 +
 arch/arm64/boot/dts/apple/t8112-j413.dts           |   20 +
 arch/arm64/boot/dts/apple/t8112-j415.dts           |   20 +
 arch/arm64/boot/dts/apple/t8112-j493.dts           |   22 +-
 arch/arm64/boot/dts/apple/t8112.dtsi               |   46 +
 arch/arm64/boot/dts/apple/t8122-j504.dts           |   22 +
 arch/arm64/boot/dts/apple/t8122-j613.dts           |   23 +
 arch/arm64/boot/dts/apple/t8122-j615.dts           |   23 +
 arch/arm64/boot/dts/apple/t8122.dtsi               |   47 +
 drivers/hid/Kconfig                                |    2 +
 drivers/hid/Makefile                               |    2 +
 drivers/hid/dockchannel/Kconfig                    |   15 +
 drivers/hid/dockchannel/Makefile                   |    3 +
 drivers/hid/dockchannel/apple-hid.c                | 1130 ++++++++++++++++++++
 drivers/hid/hid-apple.c                            |  139 ++-
 drivers/mailbox/Kconfig                            |   12 +
 drivers/mailbox/Makefile                           |    2 +
 drivers/mailbox/apple-dockchannel.c                |  380 +++++++
 drivers/soc/apple/rtkit.c                          |    2 +
 include/linux/hid.h                                |    1 +
 include/linux/mailbox/apple-dockchannel.h          |   29 +
 27 files changed, 2139 insertions(+), 48 deletions(-)
---
base-commit: dc59e4fea9d83f03bad6bddf3fa2e52491777482
change-id: 20260629-apple-mtp-keyboard-final-91cb5a6ff4fc

Best regards,
--  
Michael Reeves <michael.reeves077@gmail.com>



^ permalink raw reply

* Re: [PATCH v3] HID: logitech-hidpp: Add support for HID++ Multi-Platform feature (0x4531)
From: sashiko-bot @ 2026-06-30 10:13 UTC (permalink / raw)
  To: DevExalt; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260630094838.50471-1-exalt.dev.team@gmail.com>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Use-after-destroy of `hidpp->send_mutex` and operations on stopped hardware due to delayed sysfs group removal.
--

commit 55433f57873d505e62b905c239d0015edd306ae5
Author: Baraa Atta (Dev Exalt) <exalt.dev.team@gmail.com>

HID: logitech-hidpp: Add support for HID++ Multi-Platform feature (0x4531)

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.

> diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
> index 90b0184df777f..33904f3093803 100644
> --- a/drivers/hid/hid-logitech-hidpp.c
> +++ b/drivers/hid/hid-logitech-hidpp.c
[ ... ]
> @@ -4780,6 +4780,26 @@ 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;
>  
>  	ret = devm_mutex_init(&hdev->dev, &hidpp->multiplatform_lock);
>  	if (ret) {
>  		hid_warn(hdev,
>  			 "Multiplatform: Failed to init mutex (err=%d)\n", ret);
>  		hidpp->multiplatform_feature_index = 0;
>  		return;
>  	}
>  
>  	ret = devm_device_add_group(&hdev->dev, &multiplatform_attribute_group);

[Severity: High]
Does this create a use-after-destroy race condition during device unbind?

Because devm_device_add_group() is used, the sysfs group is scheduled for
removal during the devres phase, which runs after the driver's remove callback
returns.

In hidpp_remove(), the hardware state is explicitly torn down and the mutex is
destroyed:

drivers/hid/hid-logitech-hidpp.c:hidpp_remove() {
	...
	hid_hw_stop(hdev);
	cancel_work_sync(&hidpp->work);
	cancel_work_sync(&hidpp->reset_hi_res_work);
	mutex_destroy(&hidpp->send_mutex);
}

The sysfs attribute remains accessible to userspace during and shortly after
hidpp_remove() runs. A concurrent write to the platform sysfs attribute could
trigger platform_store(), which eventually attempts to lock the destroyed
hidpp->send_mutex and perform I/O on stopped hardware.

>  	if (ret) {
>  		hid_warn(hdev,
>  			 "Multiplatform: Failed to create sysfs group (err=%d)\n", ret);
>  		hidpp->multiplatform_feature_index = 0;
>  	}
>  }

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260630094838.50471-1-exalt.dev.team@gmail.com?part=1

^ permalink raw reply

* [PATCH v3] HID: logitech-hidpp: Add support for HID++ Multi-Platform feature (0x4531)
From: DevExalt @ 2026-06-30  9:48 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.
 
 .../testing/sysfs-driver-hid-logitech-hidpp   |  29 ++
 drivers/hid/hid-ids.h                         |   2 +
 drivers/hid/hid-logitech-hidpp.c              | 384 ++++++++++++++++++
 drivers/hid/hid-quirks.c                      |   2 +
 4 files changed, 417 insertions(+)

diff --git a/Documentation/ABI/testing/sysfs-driver-hid-logitech-hidpp b/Documentation/ABI/testing/sysfs-driver-hid-logitech-hidpp
index d8f831f2d6b5..9b963407a975 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:		Jun, 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..33904f309380 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,385 @@ 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		0x0F
+#define HIDPP_MULTIPLATFORM_GET_PLATFORM_DESCRIPTOR	0x1F
+#define HIDPP_MULTIPLATFORM_SET_CURRENT_PLATFORM	0x3F
+
+#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;
+
+	ret = devm_mutex_init(&hdev->dev, &hidpp->multiplatform_lock);
+	if (ret) {
+		hid_warn(hdev,
+			 "Multiplatform: Failed to init mutex (err=%d)\n", ret);
+		hidpp->multiplatform_feature_index = 0;
+		return;
+	}
+
+	ret = devm_device_add_group(&hdev->dev, &multiplatform_attribute_group);
+	if (ret) {
+		hid_warn(hdev,
+			 "Multiplatform: Failed to create sysfs group (err=%d)\n", ret);
+		hidpp->multiplatform_feature_index = 0;
+	}
+}
+
 static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
 {
 	struct hidpp_device *hidpp;
@@ -4545,6 +4927,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.
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] Input: zinitix: iterate contact slots by finger count
From: Linus Walleij @ 2026-06-30  9:11 UTC (permalink / raw)
  To: Thanh Nguyen, Nikita Travkin
  Cc: dmitry.torokhov, linux-input, linux-kernel, linus.walleij,
	timon37
In-Reply-To: <20260408051907.792-1-thanhnguyxn07@gmail.com>

Hi Thanh,

sorry for slow reply!

On Wed, Apr 8, 2026 at 6:19 AM Thanh Nguyen <thanhnguyxn07@gmail.com> wrote:
>
> On affected devices (for example Samsung A3 2015), the value in
> touch_event.finger_mask appears to behave as a count of reported slots
> rather than a bitmask. Using for_each_set_bit() can then skip valid
> contacts and break multitouch gestures.
>
> Keep filtering by SUB_BIT_EXIST to avoid reporting shadow contacts, but
> iterate from slot 0 up to min(finger_mask, MAX_SUPPORTED_FINGER_NUM).
> This follows the maintainer feedback to treat the field as a possible
> count while preserving the anti-shadow check.
>
> Fixes: e941dc13fd37 ("Input: zinitix - do not report shadow fingers")
> Link: https://bugzilla.kernel.org/show_bug.cgi?id=221278
> Signed-off-by: Thanh Nguyen <thanhnguyxn07@gmail.com>
> ---
> v2:
>  - Address maintainer feedback: do not revert e941dc13fd37.
>  - Keep SUB_BIT_EXIST filtering to avoid shadow contacts.
>  - Treat finger_mask as a slot-count bound and iterate 0..min(mask, max).

I can test this if need be, but maybe Nikita want to check it
first?

Yours,
Linus Walleij

^ permalink raw reply

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

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] The delay calculation in `switch2_rumble_work` causes an unbounded timer catch-up loop, flooding the USB bus with back-to-back output reports if the worker is delayed.
- [High] Sending an embedded, non-cacheline-aligned struct member (`ns2->rumble_buffer`) to `hid_hw_output_report()` can cause memory corruption during DMA.
- [High] The teardown sequence calls `hid_hw_stop()` before cancelling the rumble work and unregistering the input device, allowing output reports to be sent to a stopped device.
--

commit 6d9250862d78279cc28f8c0b3c447d3a7da2e69c
Author: Vicki Pfau <vi@endrift.com>

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 b9e2e708e28f3..a53c554a54ae3 100644
> --- a/drivers/hid/hid-nintendo.c
> +++ b/drivers/hid/hid-nintendo.c
[ ... ]
> @@ -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[64];

[Severity: High]
Does this pass an embedded, non-cacheline-aligned array to the DMA layer?

The USB DMA API requires that data buffers passed to it are dynamically
allocated (e.g., via kmalloc()) to ensure they are cache-line aligned.
Because rumble_buffer is embedded directly in struct switch2_controller, it
shares a cache line with other members like rumble_work.

Could this lead to memory corruption of adjacent struct members during
DMA cache invalidation on non-coherent architectures when passed to
hid_hw_output_report() in switch2_rumble_work()?

> +#endif
>  };
[ ... ]
> +static void switch2_rumble_work(struct work_struct *work)
> +{
[ ... ]
> +	if (active) {
> +		unsigned long interval = msecs_to_jiffies(2);
> +		uint64_t current_jiffies = get_jiffies_64();
> +
> +		if (!ns2->last_rumble_work)
> +			ns2->last_rumble_work = current_jiffies;
> +		else
> +			ns2->last_rumble_work += interval;
> +
> +		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);

[Severity: High]
Can this delay calculation result in an unbounded timer catch-up loop?

If the switch2_rumble_work() worker is delayed (e.g., due to system load
or slow synchronous transfers), last_rumble_work will fall behind
current_jiffies. The function appears to attempt to catch up by continually
adding the fixed 2ms interval to last_rumble_work and scheduling the next
execution with 0 delay.

For example, a 100ms stall might result in 50 back-to-back executions.
Could this flood the USB bus with zero-delay output reports, wasting CPU
resources and potentially degrading the device or bus?

> +	} 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);
[ ... ]
> @@ -4009,6 +4210,9 @@ static void switch2_remove(struct hid_device *hdev)
>  
>  	hid_hw_close(hdev);
>  	hid_hw_stop(hdev);
> +#if IS_ENABLED(CONFIG_NINTENDO_FF)
> +	cancel_delayed_work_sync(&ns2->rumble_work);
> +#endif
>  	mutex_lock(&ns2->lock);
>  	ida_free(&nintendo_player_id_allocator, ns2->player_id);
>  	WARN_ON(ns2->hdev != hdev);
> +	ns2->hdev = NULL;

[Severity: High]
Does this teardown sequence allow output reports to be sent to a stopped
device?

Here, hid_hw_stop() is called to stop the underlying HID transport,
followed by cancel_delayed_work_sync(&ns2->rumble_work). If the worker
thread was preempted just before acquiring ns2->lock in
switch2_rumble_work():

Worker thread:
    mutex_lock(&ns2->lock);
    // preempted before lock acquisition

Teardown:
    hid_hw_stop(hdev);
    cancel_delayed_work_sync(&ns2->rumble_work); // waits for worker

Worker thread resumes:
    if (!ns2->hdev) // hdev is still valid here, set to NULL later
        ...
    else
        ret = hid_hw_output_report(ns2->hdev, ns2->rumble_buffer, 64);

Could the worker thread then execute hid_hw_output_report() on the stopped
device, potentially causing undefined behavior in the transport drivers?

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

^ permalink raw reply

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

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] Missing lock release in URB error cleanup loops causes an immediate self-deadlock.
- [High] Incorrect teardown sequence in USB disconnect and probe error paths allows use-after-free of the controller and USB context.
- [High] Calling `ida_free()` with an unallocated ID (U32_MAX) on probe failure causes a BUG_ON / kernel panic.
- [High] Devres lifecycle violation for the input device and LEDs causes a severe Use-After-Free during teardown.
- [High] Overwriting the active configuration pointer if multiple matching USB interfaces are probed leads to a kernel panic on disconnect.
- [Medium] Type Confusion in LED brightness callback when casting an `input_dev->dev` pointer to a `hid_device`.
- [Medium] Early returns on unhandled URB error statuses permanently leak URB slots and fail to clean up state.
--

commit 7e33d97c8c5d7d9d33a04e0a368779204b06f73d
Author: Vicki Pfau <vi@endrift.com>

HID: nintendo: Add preliminary Switch 2 controller driver

This commit introduces a new driver for Nintendo Switch 2 controllers. It
implements a split-driver design with a main HID interface and a secondary
USB configuration interface. Support is included for basic input on Joy-Con 2,
Pro Controller 2, and GameCube NSO controllers.

> diff --git a/drivers/input/joystick/nintendo-switch2-usb.c b/drivers/input/joystick/nintendo-switch2-usb.c
> index 0000000000000..632af1a8638a7
> --- /dev/null
> +++ b/drivers/input/joystick/nintendo-switch2-usb.c
[ ... ]
> +static void switch2_bulk_out(struct urb *urb)
> +{
[ ... ]
> +	switch (urb->status) {
> +	case 0:
> +		break;
> +	case -ECONNRESET:
> +	case -ENOENT:
> +	case -ESHUTDOWN:
> +		dev_dbg(&ns2_usb->udev->dev, "shutting down output urb: %d\n", urb->status);
> +		return;
> +	default:
> +		dev_dbg(&ns2_usb->udev->dev, "unknown output urb status: %d\n", urb->status);
> +		return;
> +	}
> +
> +	for (i = 0; i < NS2_OUT_URBS; i++) {
> +		if (ns2_usb->bulk_out[i].urb != urb)

[Severity: Medium]
If the URB status falls into the default case, this function prints a debug
message and returns immediately. Does this skip the loop below that is supposed
to free the URB slot by setting its state to NS2_URB_FREE? If this occurs
multiple times, it seems all URB slots will remain permanently in-use, leading
to -ENOBUFS errors on future attempts.

[ ... ]
> +static int switch2_usb_probe(struct usb_interface *intf, const struct usb_device_id *id)
> +{
[ ... ]
> +	if (ret < 0) {
> +		cancel_work_sync(&ns2_usb->message_in_work);
> +		goto err_free_out;
> +	}
> +
> +	return 0;
> +
> +err_free_out:

[Severity: High]
In this error path, cancel_work_sync() is called before the IN URBs are
killed at err_free_in. If an IN URB completes immediately after
cancel_work_sync(), could it requeue the work? This would leave the work
active while the USB device context is being destroyed, potentially leading to
a use-after-free.

[ ... ]
> +	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)
> +			continue;
> +
> +		ns2_usb->bulk_out[i].urb = NULL;

[Severity: High]
When urb is NULL, this loop executes the continue statement while the 
bulk_out_lock is held. Will this cause the next iteration to attempt to
acquire the same spinlock and immediately deadlock the CPU?

[ ... ]
> +err_free_in:
> +	usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
> +	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)
> +			continue;
> +
> +		ns2_usb->bulk_in[i].urb = NULL;

[Severity: High]
Similarly, does this continue statement skip unlocking bulk_in_lock, causing
a deadlock on the next iteration?

[ ... ]
> +static void switch2_usb_disconnect(struct usb_interface *intf)
> +{
[ ... ]
> +	switch2_controller_detach_cfg(ns2_usb->cfg.parent);
> +
> +	usb_kill_anchored_urbs(&ns2_usb->bulk_out_anchor);
[ ... ]
> +	cancel_work_sync(&ns2_usb->message_in_work);
> +	usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);

[Severity: High]
Is this teardown sequence safe? switch2_controller_detach_cfg() drops the
refcount on the controller, potentially freeing it while message_in_work is
still active. Furthermore, cancel_work_sync() is called before killing the IN
URBs, which might allow the IN URB completion handler to reschedule the work
after we've attempted to cancel it. This seems to create multiple race windows
for a use-after-free.

> diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
> index e7302ec01ff1f..b9e2e708e28f3 100644
> --- a/drivers/hid/hid-nintendo.c
> +++ b/drivers/hid/hid-nintendo.c
[ ... ]
> +static int switch2_player_led_brightness_set(struct led_classdev *led,
> +					    enum led_brightness brightness)
> +{
> +	struct device *dev = led->dev->parent;
> +	struct hid_device *hdev = to_hid_device(dev);
> +	struct switch2_controller *ns2 = hid_get_drvdata(hdev);

[Severity: Medium]
The LEDs are registered with the input device as their parent, so
led->dev->parent points to the input_dev's device struct. Does using
to_hid_device() here perform an invalid cast? While hid_get_drvdata() might
accidentally work due to struct layout overlaps for the driver_data pointer,
casting an input_dev device to a hid_device looks like a strict-aliasing
violation.

[ ... ]
> +static int switch2_init_input(struct switch2_controller *ns2)
> +{
[ ... ]
> +	input = devm_input_allocate_device(&hdev->dev);
[ ... ]
> +		ret = devm_led_classdev_register(&input->dev, led);

[Severity: High]
The input device and LEDs are allocated using devm, tying their lifecycle to
the HID device. However, the physical memory for the leds array is embedded
inside the ns2 structure, which is managed via manual krefs and freed in
switch2_kref_put(). When the controller is removed, ns2 is freed immediately,
but the devres cleanup for the LEDs will run later when the HID device unbinds.
Could this lead to the LED core accessing the already-freed ns2 memory?

[ ... ]
> +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;
[ ... ]
> +err_cleanup:
> +	ida_free(&nintendo_player_id_allocator, ns2->player_id);

[Severity: High]
If ida_alloc() fails, ns2->player_id remains U32_MAX. If probe later fails
and jumps to err_cleanup, ida_free() is called unconditionally. Since
ida_free() expects a valid signed integer ID, passing U32_MAX (-1) will
trigger a BUG_ON. Should there be a check before freeing?

[ ... ]
> +static void switch2_remove(struct hid_device *hdev)
> +{
> +	struct switch2_controller *ns2 = hid_get_drvdata(hdev);
> +
> +	hid_hw_close(hdev);
> +	hid_hw_stop(hdev);
> +	mutex_lock(&ns2->lock);
> +	ida_free(&nintendo_player_id_allocator, ns2->player_id);

[Severity: High]
Similar to the probe error path, if the device was successfully probed but
ida_alloc() had initially failed, removing the device will call ida_free() on
U32_MAX, triggering a BUG_ON.

[ ... ]
> +int switch2_controller_attach_cfg(const char *phys, struct switch2_cfg_intf *cfg)
> +{
[ ... ]
> +	mutex_lock(&ns2->lock);
> +	WARN_ON(ns2->cfg);
> +	ns2->cfg = cfg;

[Severity: High]
If multiple USB interfaces from the same device match and trigger probes,
this function emits a warning but proceeds to overwrite ns2->cfg. This breaks
the one-to-one linkage. Should this return -EBUSY instead of overwriting?

[ ... ]
> +void switch2_controller_detach_cfg(struct switch2_controller *ns2)
> +{
> +	mutex_lock(&ns2->lock);

[Severity: High]
If the active configuration pointer was overwritten due to multiple interfaces,
when the first interface disconnects, it will clear the parent pointer on the
second interface's config. When the second interface then disconnects, it will
pass a NULL ns2 pointer to this function, causing an immediate NULL pointer
dereference when attempting to lock ns2->lock.

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

^ permalink raw reply

* Re: [PATCH v7 3/3] HID: nintendo: Add unified report format support
From: sashiko-bot @ 2026-06-30  8:59 UTC (permalink / raw)
  To: Vicki Pfau; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260630084838.2313379-4-vi@endrift.com>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The global HID report size check was incorrectly tightened to 17 bytes, silently dropping valid input reports for non-unified controllers.
--

commit 2414bf53574e3d2b6aee6b978aeb58f941052e21
Author: Vicki Pfau <vi@endrift.com>

HID: nintendo: Add unified report format support

This commit adds support for the unified report format for various Nintendo
controllers. It maps their respective buttons and axes to the new unified
formats.

> diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
> index a53c554a54ae3..4920c9691b08a 100644
> --- a/drivers/hid/hid-nintendo.c
> +++ b/drivers/hid/hid-nintendo.c

[ ... ]

> @@ -3765,7 +3866,7 @@ static int switch2_event(struct hid_device *hdev, struct hid_report *report, uin
>  	if (report->type != HID_INPUT_REPORT)
>  		return 0;
>  
> -	if (size < 15)
> +	if (size < 17)
>  		return -EINVAL;

[Severity: High]
Does tightening this global size check to 17 bytes silently drop valid input
reports for non-unified controllers?

Looking at the other cases in switch2_event(), such as NS2_REPORT_GC, the
driver previously validated against a 15-byte minimum because it only
accesses up to raw_data[14]. By unconditionally checking if the size is
less than 17 before evaluating the report ID, any 15-byte or 16-byte report
will hit this early return and be rejected with -EINVAL.

Should this size requirement be localized strictly to the NS2_REPORT_UNIFIED
branch to avoid breaking functionality for legacy controllers?

>  	guard(rcu)();
> @@ -3776,11 +3877,51 @@ static int switch2_event(struct hid_device *hdev, struct hid_report *report, uin
>  
>  	switch (report->id) {
>  	case NS2_REPORT_UNIFIED:

[ ... ]

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

^ permalink raw reply

* [PATCH v7 3/3] HID: nintendo: Add unified report format support
From: Vicki Pfau @ 2026-06-30  8:48 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Vicki Pfau, Silvan Jegen
In-Reply-To: <20260630084838.2313379-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 | 153 +++++++++++++++++++++++++++++++++++--
 1 file changed, 147 insertions(+), 6 deletions(-)

diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
index a53c554a54ae..4920c9691b08 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
@@ -3765,7 +3866,7 @@ static int switch2_event(struct hid_device *hdev, struct hid_report *report, uin
 	if (report->type != HID_INPUT_REPORT)
 		return 0;
 
-	if (size < 15)
+	if (size < 17)
 		return -EINVAL;
 
 	guard(rcu)();
@@ -3776,11 +3877,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.
-		 */
+		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:
+			if (size < 0x3f)
+				return -EINVAL;
+
+			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

* [PATCH v7 1/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-06-30  8:48 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Vicki Pfau, Silvan Jegen
In-Reply-To: <20260630084838.2313379-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                    | 1251 ++++++++++++++++-
 drivers/hid/hid-nintendo.h                    |   72 +
 drivers/input/joystick/Kconfig                |   11 +
 drivers/input/joystick/Makefile               |    1 +
 drivers/input/joystick/nintendo-switch2-usb.c |  443 ++++++
 8 files changed, 1784 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..b9e2e708e28f 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,1181 @@ 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) {
+			kref_get(&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 hid_device *hdev = to_hid_device(dev);
+	struct switch2_controller *ns2 = hid_get_drvdata(hdev);
+
+	if (!ns2)
+		return -ENODEV;
+
+	guard(mutex)(&ns2->lock);
+	return switch2_set_leds(ns2);
+}
+
+static void switch2_leds_create(struct switch2_controller *ns2)
+{
+	struct hid_device *hdev = ns2->hdev;
+	struct led_classdev *led;
+	int i;
+	int player_led_pattern;
+
+	player_led_pattern = ns2->player_id % JC_NUM_LED_PATTERNS;
+	hid_dbg(hdev, "assigned player %d led pattern", player_led_pattern + 1);
+
+	for (i = 0; i < JC_NUM_LEDS; i++) {
+		led = &ns2->leds[i];
+		led->brightness = joycon_player_led_patterns[player_led_pattern][i];
+		led->max_brightness = 1;
+		led->brightness_set_blocking = switch2_player_led_brightness_set;
+		led->flags = LED_CORE_SUSPENDRESUME | LED_HW_PLUGGABLE | 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 = devm_input_allocate_device(&hdev->dev);
+	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);
+
+	cfg->parent = ns2;
+
+	mutex_lock(&ns2->lock);
+	WARN_ON(ns2->cfg);
+	ns2->cfg = cfg;
+
+	if (ns2->hdev)
+		ret = switch2_init_controller(ns2);
+
+	if (ret < 0)
+		ns2->cfg = NULL;
+	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) {
+		mutex_unlock(&ns2->lock);
+		return;
+	}
+	WARN_ON(ns2 != ns2->cfg->parent);
+	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);
+	WARN_ON(ns2->hdev);
+	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;
+	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);
+
+	hid_hw_close(hdev);
+	hid_hw_stop(hdev);
+	mutex_lock(&ns2->lock);
+	ida_free(&nintendo_player_id_allocator, ns2->player_id);
+	WARN_ON(ns2->hdev != hdev);
+	ns2->hdev = NULL;
+	switch2_controller_put(ns2);
+	mutex_unlock(&ns2->lock);
+	kref_put(&ns2->refcount, switch2_kref_put);
+}
+
 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 +4044,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 +4132,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..632af1a8638a
--- /dev/null
+++ b/drivers/input/joystick/nintendo-switch2-usb.c
@@ -0,0 +1,443 @@
+// 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;
+	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))
+			continue;
+
+		ns2_urb = &ns2_usb->bulk_in[i];
+		usb_anchor_urb(ns2_urb->urb, &ns2_usb->bulk_in_anchor);
+		err = usb_submit_urb(ns2_urb->urb, GFP_ATOMIC);
+		if (err) {
+			usb_unanchor_urb(ns2_urb->urb);
+			dev_dbg(&ns2_usb->udev->dev, "failed to queue input urb: %d\n", err);
+		} else {
+			ns2_urb->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);
+		return;
+	default:
+		dev_dbg(&ns2_usb->udev->dev, "unknown output urb status: %d\n", urb->status);
+		return;
+	}
+
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		if (ns2_usb->bulk_out[i].urb != urb)
+			continue;
+
+		ns2_usb->bulk_out[i].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)) {
+			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) {
+		cancel_work_sync(&ns2_usb->message_in_work);
+		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)
+			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:
+	usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
+	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)
+			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;
+
+	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);
+	}
+
+	cancel_work_sync(&ns2_usb->message_in_work);
+	usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
+	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);
+	}
+}
+
+#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 v7 2/3] HID: nintendo: Add rumble support for Switch 2 controllers
From: Vicki Pfau @ 2026-06-30  8:48 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Vicki Pfau, Silvan Jegen
In-Reply-To: <20260630084838.2313379-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 | 210 ++++++++++++++++++++++++++++++++++++-
 2 files changed, 211 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 b9e2e708e28f..a53c554a54ae 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[64];
+#endif
 };
 
+enum gc_rumble {
+	GC_RUMBLE_OFF = 0,
+	GC_RUMBLE_ON = 1,
+	GC_RUMBLE_STOP = 2,
+};
+
+/*
+ * The highest rumble level for "HD Rumble" is strong enough to potentially damage the controller,
+ * and also leaves your hands feeling like melted jelly, so we set a semi-arbitrary scaling factor
+ * to artificially limit the maximum for safety and comfort. It is currently unknown if the Switch
+ * 2 itself does something similar, but it's quite likely.
+ *
+ * This value must be between 0 and 1024, otherwise the math below will overflow.
+ */
+#define RUMBLE_MAX 450u
+
+/*
+ * Semi-arbitrary values used to simulate the "rumble" sensation of an eccentric rotating
+ * mass type haptic motor on the Switch 2 controllers' linear resonant actuator type haptics.
+ *
+ * The units used are unknown, but the values must be between 0 and 1023.
+ */
+#define RUMBLE_HI_FREQ 0x187
+#define RUMBLE_LO_FREQ 0x112
+
 static DEFINE_MUTEX(switch2_controllers_lock);
 static LIST_HEAD(switch2_controllers);
 
@@ -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
 };
 
@@ -3196,7 +3246,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);
@@ -3204,6 +3258,123 @@ 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(2);
+		uint64_t current_jiffies = get_jiffies_64();
+
+		if (!ns2->last_rumble_work)
+			ns2->last_rumble_work = current_jiffies;
+		else
+			ns2->last_rumble_work += interval;
+
+		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 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;
@@ -3345,6 +3516,15 @@ static int switch2_init_input(struct switch2_controller *ns2)
 		return -EINVAL;
 	}
 
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	input_set_capability(input, EV_FF, FF_RUMBLE);
+	ret = input_ff_create_memless(input, NULL, switch2_play_effect);
+	if (ret) {
+		input_free_device(input);
+		return ret;
+	}
+#endif
+
 	hid_info(ns2->hdev, "Firmware version %u.%u.%u (type %i)\n", ns2->version.major,
 		ns2->version.minor, ns2->version.patch, ns2->version.ctlr_type);
 	if (ns2->version.dsp_type >= 0)
@@ -3748,7 +3928,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);
@@ -3860,6 +4049,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;
 	}
@@ -3969,6 +4162,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);
@@ -3995,6 +4193,9 @@ static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id
 	hid_hw_stop(hdev);
 err_cleanup:
 	ida_free(&nintendo_player_id_allocator, ns2->player_id);
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	cancel_delayed_work(&ns2->rumble_work);
+#endif
 	ns2->hdev = NULL;
 	switch2_controller_put(ns2);
 	mutex_unlock(&ns2->lock);
@@ -4009,6 +4210,9 @@ static void switch2_remove(struct hid_device *hdev)
 
 	hid_hw_close(hdev);
 	hid_hw_stop(hdev);
+#if IS_ENABLED(CONFIG_NINTENDO_FF)
+	cancel_delayed_work_sync(&ns2->rumble_work);
+#endif
 	mutex_lock(&ns2->lock);
 	ida_free(&nintendo_player_id_allocator, ns2->player_id);
 	WARN_ON(ns2->hdev != hdev);
-- 
2.54.0


^ permalink raw reply related

* [PATCH v7 0/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-06-30  8:48 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 an iteration on v6,
fixing errors pointed out by sashiko as well as cleaning up some other
things like lock handling and cleanup ordering.

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                    | 1650 ++++++++++++++++-
 drivers/hid/hid-nintendo.h                    |   72 +
 drivers/input/joystick/Kconfig                |   11 +
 drivers/input/joystick/Makefile               |    1 +
 drivers/input/joystick/nintendo-switch2-usb.c |  443 +++++
 8 files changed, 2160 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

* Re: [PATCH 00/13] treewide: replace linux/gpio.h
From: Bartosz Golaszewski @ 2026-06-30  7:53 UTC (permalink / raw)
  To: Arnd Bergmann
  Cc: linux-gpio, Arnd Bergmann, Bartosz Golaszewski, Andrew Lunn,
	Sebastian Hesselbarth, Gregory Clement, Frank Li, Robert Jarzmik,
	Krzysztof Kozlowski, Greg Ungerer, Thomas Bogendoerfer,
	Hauke Mehrtens, Rafał Miłecki, Yoshinori Sato,
	John Paul Adrian Glaubitz, Linus Walleij, Dmitry Torokhov,
	Jakub Kicinski, Paolo Abeni, Dominik Brodowski, linux-kernel,
	linux-arm-kernel, linux-samsung-soc, patches, linux-m68k,
	linux-mips, linux-sh, linux-input, linux-media, netdev,
	linux-sunxi, linux-phy, linux-rockchip, linux-sound
In-Reply-To: <20260629132633.1300009-1-arnd@kernel.org>

On Mon, 29 Jun 2026 15:26:20 +0200, Arnd Bergmann <arnd@kernel.org> said:
> From: Arnd Bergmann <arnd@arndb.de>
>
> The linux/gpio.h header used to be the global definition for the gpio
> interfaces, with 1100 users back in linux-3.17. In linux-7.2, only about
> 130 of those remain, so this series cleans out the rest.
>
> In each subsystem, we can replace the header either with
> linux/gpio/consumer.h for users of the modern gpio descriptor interface,
> or linux/gpio/legacy.h for the few remaining users of the old number
> based interface.
>
> All patches in this series can get applied independently, so my
> preference would be for each subsystem maintainer to apply these
> directly, with the rest going into the gpio tree at some point.
>
> The final patch here obviously needs to wait for all the others
> to get merged first.
>
>       Arnd

Thanks for doing this Arnd!

For the series:

Acked-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>

^ permalink raw reply

* Re: [PATCH] Input: hynitron_cstxxx - validate touch count against buffer
From: sashiko-bot @ 2026-06-30  7:45 UTC (permalink / raw)
  To: Pengpeng Hou; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <2026063009041899.4-ccfa108-0047-Input-hynitron_cstxxx---val-pengpeng@iscas.ac.cn>

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

Pre-existing issues:
- [High] The driver passes stack-allocated memory to `i2c_transfer()` and `i2c_master_send()`, which can cause memory corruption or system crashes if the underlying I2C controller uses DMA.
--

commit c00eb5cc820dacf72d27095570836d29b4df9d2d
Author: Pengpeng Hou <pengpeng@iscas.ac.cn>

Input: hynitron_cstxxx - validate touch count against buffer

Bounds the device-reported touch count against the chip limit and buffer
layout before parsing touch slots.

> diff --git a/drivers/input/touchscreen/hynitron_cstxxx.c b/drivers/input/touchscreen/hynitron_cstxxx.c
> index 1d8ca90dcda69..6644272bc72a4 100644
> --- a/drivers/input/touchscreen/hynitron_cstxxx.c
> +++ b/drivers/input/touchscreen/hynitron_cstxxx.c
> @@ -313,6 +313,12 @@ static void cst3xx_touch_report(struct i2c_client *client)
>  		return;
>  
>  	touch_cnt = buf[5] & CST3XX_TOUCH_COUNT_MASK;

[Severity: High]
This isn't a bug introduced by this patch, but is it safe to use
stack-allocated memory for I2C transfers?

Looking at cst3xx_touch_report(), buf is allocated on the stack and passed
down to cst3xx_i2c_read_register():

drivers/input/touchscreen/hynitron_cstxxx.c:cst3xx_touch_report() {
        ...
        u8 buf[28];
        ...
        err = cst3xx_i2c_read_register(client, CST3XX_TOUCH_DATA_PART_REG,
                                       buf, 28);
        ...
}

The cst3xx_i2c_read_register() function also appears to allocate its register
address buffer on the stack:

drivers/input/touchscreen/hynitron_cstxxx.c:cst3xx_i2c_read_register() {
        ...
        __le16 buf = cpu_to_le16(reg);
        ...
                .buf = (u8 *)&buf,
        ...
}

If the underlying I2C adapter driver uses DMA to perform the transfer, mapping
stack memory for DMA can cause cache coherency issues, memory corruption, or
kernel panics, particularly when CONFIG_VMAP_STACK is enabled.

> +	if (touch_cnt > ts_data->chip->max_touch_num ||
> +	    (touch_cnt > 1 && touch_cnt * 5 + 2 >= sizeof(buf))) {
> +		dev_err(&client->dev, "cst3xx touch read failure\n");
> +		return;
> +	}
> +
>  	/*
>  	 * Check the check bit of the last touch slot. The check bit is
>  	 * always present after touch point 1 for valid data, and then

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/2026063009041899.4-ccfa108-0047-Input-hynitron_cstxxx---val-pengpeng@iscas.ac.cn?part=1

^ permalink raw reply

* Re: [PATCH] Input: cyapa - validate unsupported PIP response length
From: sashiko-bot @ 2026-06-30  7:38 UTC (permalink / raw)
  To: Pengpeng Hou; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <2026063008017969.1-ccfa108-0008-Input-cyapa---validate-unsu-pengpeng@iscas.ac.cn>

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

Pre-existing issues:
- [Critical] Stack buffer overflow in `cyapa_pip_irq_handler` when reading touch reports.
- [High] Stack out-of-bounds read in `cyapa_gen5_read_panel_scan_raw_data` during raw data parsing.
- [High] Stack out-of-bounds read and kernel infinite loop in `cyapa_gen5_read_idac_data`.
--

commit 577a902f2ff46d9f3cceff655a89f68889610a56
Author: Pengpeng Hou <pengpeng@iscas.ac.cn>

Input: cyapa - validate unsupported PIP response length

cyapa_sort_tsg_pip_app_resp_data() accepts PIP application
responses after checking PIP_MIN_APP_RESP_LENGTH bytes.  That covers
the application command byte at offset 4, but the unsupported-command
path also reads the status byte at PIP_RESP_STATUS_OFFSET.

A short response can advertise PIP_UNSUPPORTED_CMD_RESP_LENGTH in the
response header while the supplied buffer still contains only the generic
minimum response.  Validate the unsupported response length before reading
the status byte.

> diff --git a/drivers/input/mouse/cyapa_gen5.c b/drivers/input/mouse/cyapa_gen5.c
> index 59f6e97d54827..733da6d324331 100644
> --- a/drivers/input/mouse/cyapa_gen5.c
> +++ b/drivers/input/mouse/cyapa_gen5.c
> @@ -686,6 +686,7 @@ bool cyapa_sort_tsg_pip_app_resp_data(struct cyapa *cyapa,
>  		u8 *data, int len)
>  {
>  	struct cyapa_pip_cmd_states *pip = &cyapa->cmd_states.pip;
> +	u8 cmd_code;
>  	int resp_len;
>  
>  	if (!data || len < PIP_MIN_APP_RESP_LENGTH)

[ ... ]

>  		/* Read mutual global idac or local mutual/self PWC data. */
>  		offset += read_len;
>  		for (i = 10; i < (read_len + GEN5_RESP_DATA_STRUCTURE_OFFSET);
>  				i += *data_size) {
>  			value = cyapa_parse_structure_data(resp_data[9],
>  					&resp_data[i], *data_size);

[Severity: High]
This is a pre-existing issue, but does this loop in
cyapa_gen5_read_idac_data() risk an infinite loop or out-of-bounds read?

If a malicious I2C peripheral sets the data size (derived from resp_data[9])
to 0, wouldn't the loop fail to increment i and hang the CPU?

Additionally, doesn't blindly trusting read_len (derived from resp_data[7:8])
for the loop bound risk reading past the bounds of the 256-byte resp_data
stack array?

[ ... ]

> 		read_elements = get_unaligned_le16(&resp_data[7]);
> 		if (read_elements == 0)
> 			break;
> 
> 		data_size = (resp_data[9] & GEN5_PWC_DATA_ELEMENT_SIZE_MASK);
> 		offset += read_elements;
> 		if (read_elements) {
> 			for (i = GEN5_RESP_DATA_STRUCTURE_OFFSET;
> 			     i < (read_elements * data_size +
> 					GEN5_RESP_DATA_STRUCTURE_OFFSET);
> 			     i += data_size) {
> 				value = cyapa_parse_structure_data(resp_data[9],
> 						&resp_data[i], data_size);

[Severity: High]
This isn't a bug introduced by this patch, but does this risk an out-of-bounds
read on the kernel stack in cyapa_gen5_read_panel_scan_raw_data()?

If the device returns a large read_elements value, the loop condition appears
to allow cyapa_parse_structure_data() to read far beyond the bounds of the
256-byte resp_data stack array.

[ ... ]

> int cyapa_pip_irq_handler(struct cyapa *cyapa)
> {
> 	struct device *dev = &cyapa->client->dev;
> 	struct cyapa_pip_report_data report_data;
> 	unsigned int report_len;
> 	int ret;
> 
> 	if (!cyapa_is_pip_app_mode(cyapa)) {
> 		dev_err(dev, "invalid device state, gen=%d, state=0x%02x\n",
> 			cyapa->gen, cyapa->state);
> 		return -EINVAL;
> 	}
> 
> 	ret = cyapa_i2c_pip_read(cyapa, (u8 *)&report_data,
> 			PIP_RESP_LENGTH_SIZE);

[ ... ]

> 	/* Idle, no data for report. */
> 	if (report_len == PIP_RESP_LENGTH_SIZE)
> 		return 0;
> 
> 	ret = cyapa_i2c_pip_read(cyapa, (u8 *)&report_data, report_len);

[Severity: Critical]
This is a pre-existing issue, but could this lead to a stack buffer overflow
in cyapa_pip_irq_handler()?

The report_data structure is 127 bytes on the stack, but an untrusted
report_len from the device is used in cyapa_i2c_pip_read(). Since the read
wrapper only caps the length to 256 bytes (CYAPA_REG_MAP_SIZE), can't a
compromised peripheral supply a length that overflows the buffer and corrupts
the stack?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/2026063008017969.1-ccfa108-0008-Input-cyapa---validate-unsu-pengpeng@iscas.ac.cn?part=1

^ permalink raw reply

* [PATCH] Input: hynitron_cstxxx - validate touch count against buffer
From: Pengpeng Hou @ 2026-06-30  7:30 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input, linux-kernel, Pengpeng Hou

cst3xx_touch_report() uses the device-reported touch count to compute
the check-byte offset and to parse touch slots in a fixed 28-byte
buffer.

Bound the touch count against the chip limit and the buffer layout
before parsing.

Signed-off-by: Pengpeng Hou <pengpeng@iscas.ac.cn>
---
 drivers/input/touchscreen/hynitron_cstxxx.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/drivers/input/touchscreen/hynitron_cstxxx.c b/drivers/input/touchscreen/hynitron_cstxxx.c
index 1d8ca90..6644272 100644
--- a/drivers/input/touchscreen/hynitron_cstxxx.c
+++ b/drivers/input/touchscreen/hynitron_cstxxx.c
@@ -313,6 +313,12 @@ static void cst3xx_touch_report(struct i2c_client *client)
 		return;
 
 	touch_cnt = buf[5] & CST3XX_TOUCH_COUNT_MASK;
+	if (touch_cnt > ts_data->chip->max_touch_num ||
+	    (touch_cnt > 1 && touch_cnt * 5 + 2 >= sizeof(buf))) {
+		dev_err(&client->dev, "cst3xx touch read failure\n");
+		return;
+	}
+
 	/*
 	 * Check the check bit of the last touch slot. The check bit is
 	 * always present after touch point 1 for valid data, and then


^ permalink raw reply related

* [PATCH] Input: cyapa - validate unsupported PIP response length
From: Pengpeng Hou @ 2026-06-30  7:19 UTC (permalink / raw)
  To: Dmitry Torokhov
  Cc: open list:INPUT KEYBOARD, MOUSE, JOYSTICK, TOUCHSCREEN...,
	open list, Pengpeng Hou

cyapa_sort_tsg_pip_app_resp_data() accepts PIP application
responses after checking PIP_MIN_APP_RESP_LENGTH bytes.  That covers
the application command byte at offset 4, but the unsupported-command
path also reads the status byte at PIP_RESP_STATUS_OFFSET.

A short response can advertise PIP_UNSUPPORTED_CMD_RESP_LENGTH in the
response header while the supplied buffer still contains only the generic
minimum response.  Validate the unsupported response length before reading
the status byte.

Signed-off-by: Pengpeng Hou <pengpeng@iscas.ac.cn>
---
 drivers/input/mouse/cyapa_gen5.c | 21 ++++++++++++++-------
 1 file changed, 14 insertions(+), 7 deletions(-)

diff --git a/drivers/input/mouse/cyapa_gen5.c b/drivers/input/mouse/cyapa_gen5.c
index 59f6e97..733da6d 100644
--- a/drivers/input/mouse/cyapa_gen5.c
+++ b/drivers/input/mouse/cyapa_gen5.c
@@ -686,6 +686,7 @@ bool cyapa_sort_tsg_pip_app_resp_data(struct cyapa *cyapa,
 		u8 *data, int len)
 {
 	struct cyapa_pip_cmd_states *pip = &cyapa->cmd_states.pip;
+	u8 cmd_code;
 	int resp_len;
 
 	if (!data || len < PIP_MIN_APP_RESP_LENGTH)
@@ -694,13 +695,19 @@ bool cyapa_sort_tsg_pip_app_resp_data(struct cyapa *cyapa,
 	if (data[PIP_RESP_REPORT_ID_OFFSET] == PIP_APP_RESP_REPORT_ID &&
 			data[PIP_RESP_RSVD_OFFSET] == PIP_RESP_RSVD_KEY) {
 		resp_len = get_unaligned_le16(&data[PIP_RESP_LENGTH_OFFSET]);
-		if (GET_PIP_CMD_CODE(data[PIP_RESP_APP_CMD_OFFSET]) == 0x00 &&
-			resp_len == PIP_UNSUPPORTED_CMD_RESP_LENGTH &&
-			data[5] == pip->in_progress_cmd) {
-			/* Unsupported command code */
-			return false;
-		} else if (GET_PIP_CMD_CODE(data[PIP_RESP_APP_CMD_OFFSET]) ==
-				pip->in_progress_cmd) {
+		cmd_code = GET_PIP_CMD_CODE(data[PIP_RESP_APP_CMD_OFFSET]);
+		if (cmd_code == 0x00 &&
+		    resp_len == PIP_UNSUPPORTED_CMD_RESP_LENGTH) {
+			if (len < PIP_UNSUPPORTED_CMD_RESP_LENGTH)
+				return false;
+
+			if (data[PIP_RESP_STATUS_OFFSET] == pip->in_progress_cmd) {
+				/* Unsupported command code */
+				return false;
+			}
+		}
+
+		if (cmd_code == pip->in_progress_cmd) {
 			/* Correct command response received */
 			return true;
 		}


^ permalink raw reply related

* Re: [PATCH] HID: rmi: check report length before trimming sentinel bytes
From: sashiko-bot @ 2026-06-30  7:05 UTC (permalink / raw)
  To: Pengpeng Hou; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <2026063007016439.1-ccfa108-0006-HID-rmi-check-report-length-pengpeng@iscas.ac.cn>

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

Pre-existing issues:
- [High] `rmi_hid_write_block` writes data into a dynamically allocated heap buffer `data->writeReport` without verifying that the requested transfer length fits within the allocation.
- [High] `rmi_hid_read_block` trusts an attacker-controlled length field from the device report to read from the dynamically allocated `data->readReport` buffer, leading to an out-of-bounds read of kernel memory.
--

commit 5629c16e6055ea1ed7974ee398f60300987bc662
Author: Pengpeng Hou <pengpeng@iscas.ac.cn>

HID: rmi: check report length before trimming sentinel bytes

rmi_check_sanity() trims trailing 0xff sentinel bytes, but its loop
reads data[valid_size - 1] before checking that valid_size is non-zero.

Reverse the condition so the length is proved before the last byte is
inspected.

> diff --git a/drivers/hid/hid-rmi.c b/drivers/hid/hid-rmi.c
> index d4af17fdba467..2bd781f1e0f53 100644
> --- a/drivers/hid/hid-rmi.c
> +++ b/drivers/hid/hid-rmi.c
> @@ -365,7 +365,7 @@ static int rmi_check_sanity(struct hid_device *hdev, u8 *data, int size)
>  	 * such reports here.
>  	 */
>  
> -	while ((data[valid_size - 1] == 0xff) && valid_size > 0)
> +	while (valid_size > 0 && data[valid_size - 1] == 0xff)
>  		valid_size--;
>  
>  	return valid_size;

[Severity: High]
This is a pre-existing issue, but does this code overflow the dynamically
allocated data->writeReport buffer?

drivers/hid/hid-rmi.c:rmi_hid_write_block() {
    ...
	data->writeReport[2] = addr & 0xFF;
	data->writeReport[3] = (addr >> 8) & 0xFF;
	memcpy(&data->writeReport[4], buf, len);
    ...
}

A malicious HID device can present a small output_report_size in its HID
descriptor, which determines the size of the data->writeReport allocation.

By sending parameters that cause the RMI core to perform a large block write,
can the device force rmi_hid_write_block() to process a len that vastly
exceeds the allocated buffer size?

[Severity: High]
This isn't a bug introduced by this patch, but could this memcpy() result in
an out-of-bounds read from the data->readReport buffer?

drivers/hid/hid-rmi.c:rmi_hid_read_block() {
    ...
			read_input_count = data->readReport[1];
			memcpy(buf + bytes_read, &data->readReport[2],
				min(read_input_count, bytes_needed));
    ...
}

The function reads read_input_count directly from the incoming HID report
without validating it against the size of the data->readReport allocation
(data->input_report_size).

Could a malicious device specify a small input_report_size in its HID
descriptor but send a large read_input_count byte in the report, causing
the memcpy() to over-read the kernel heap?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/2026063007016439.1-ccfa108-0006-HID-rmi-check-report-length-pengpeng@iscas.ac.cn?part=1

^ permalink raw reply


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