Linux Input/HID development
 help / color / mirror / Atom feed
* Re: [PATCH 6.18 000/188] 6.18.32-rc1 review
From: Sasha Levin @ 2026-05-19 19:25 UTC (permalink / raw)
  To: gregkh
  Cc: Sasha Levin, achill, akpm, broonie, conor, f.fainelli, hargar,
	jonathanh, linux-kernel, linux, lkft-triage, patches, patches,
	pavel, rwarsow, shuah, sr, stable, sudipm.mukherjee, torvalds,
	Miguel Ojeda, Jiri Kosina, Benjamin Tissoires, linux-input,
	Johan Hovold, Nathan Chancellor
In-Reply-To: <20260516020430.110135-1-ojeda@kernel.org>

On Sat, May 16, 2026 at 04:04:30AM +0200, Miguel Ojeda wrote:
> Via arm32 I see:
>
>     drivers/hid/hid-core.c:2050:29: error: format specifies type 'long' but the argument has type 'size_t' (aka 'unsigned int') [-Werror,-Wformat]
>      2049 |                 hid_warn_ratelimited(hid, "Event data for report %d is incorrect (%d vs %ld)\n",
>           |                                                                                         ~~~
>           |                                                                                         %zu
>      2050 |                                      report->id, csize, bsize);
>           |                                                         ^~~~~
>
> It is also reproducible in mainline, though. Cc'ing a few folks...

Nathan's mainline fix (4d3a2a466b8d "HID: core: Fix size_t specifier in
hid_report_raw_event()") has been queued for 6.18.y and 7.0.y.

--
Thanks,
Sasha

^ permalink raw reply

* Re: [PATCH] HID: logitech-hidpp: Add support for newer Bluetooth keyboards
From: Alain Michaud @ 2026-05-19 18:46 UTC (permalink / raw)
  To: Filipe Laíns; +Cc: jikos, bentiss, hadess, ogay, linux-input, linux-kernel
In-Reply-To: <64842eb18284bf49ab1e283b0fed1e6bdea7037d.camel@riseup.net>

On Tue, May 19, 2026 at 2:38 PM Filipe Laíns <lains@riseup.net> wrote:
>
> On Tue, 2026-05-12 at 13:22 +0000, Alain Michaud wrote:
> > Add product IDs (PIDs) for several newer Logitech Bluetooth keyboards
> > to the hidpp_devices matching table, enabling full HID++ support for
> > them.
> >
> > The added keyboards are:
> > - Logitech Signature K650 & B2B
> > - Logitech Pebble Keys 2 K380S
> > - Logitech Casa Pop-Up Desk & B2B
> > - Logitech Wave Keys & B2B
> > - Logitech Signature Slim K950 & B2B
> > - Logitech MX Keys S & B2B
> > - Logitech Keys-To-Go 2
> > - Logitech Pop Icon Keys
> > - Logitech MX Keys Mini & B2B
> > - Logitech Signature Slim Solar+ K980 B2B
> > - Logitech Bluetooth Keyboard K250/K251
> > - Logitech Signature Comfort K880 & B2B
>
> Hi Alain,
>
> Did you actually verify the functionality on each device, or did you just update
> the device ID list for new releases?
The later.  Not all the functionality may be available across these
devices.  However, these will definitely work with the Multi-Platform
feature patch currently under review.
>
> Cheers,
> Filipe Laíns

^ permalink raw reply

* Re: [PATCH] HID: logitech-hidpp: Add support for newer Bluetooth keyboards
From: Filipe Laíns @ 2026-05-19 18:38 UTC (permalink / raw)
  To: Alain Michaud, jikos, bentiss; +Cc: hadess, ogay, linux-input, linux-kernel
In-Reply-To: <20260512132244.2194556-1-alainmichaud@google.com>

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

On Tue, 2026-05-12 at 13:22 +0000, Alain Michaud wrote:
> Add product IDs (PIDs) for several newer Logitech Bluetooth keyboards
> to the hidpp_devices matching table, enabling full HID++ support for
> them.
> 
> The added keyboards are:
> - Logitech Signature K650 & B2B
> - Logitech Pebble Keys 2 K380S
> - Logitech Casa Pop-Up Desk & B2B
> - Logitech Wave Keys & B2B
> - Logitech Signature Slim K950 & B2B
> - Logitech MX Keys S & B2B
> - Logitech Keys-To-Go 2
> - Logitech Pop Icon Keys
> - Logitech MX Keys Mini & B2B
> - Logitech Signature Slim Solar+ K980 B2B
> - Logitech Bluetooth Keyboard K250/K251
> - Logitech Signature Comfort K880 & B2B

Hi Alain,

Did you actually verify the functionality on each device, or did you just update
the device ID list for new releases?

Cheers,
Filipe Laíns

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

^ permalink raw reply

* Mismatched product ids for the ASUS ROG RAIKIRI PRO
From: Justin Opini @ 2026-05-19 17:37 UTC (permalink / raw)
  To: dmitry.torokhov; +Cc: linux-input, linux-kernel, torvalds
In-Reply-To: <Zjb8RrOFLHFSk_Gv@google.com>

Hey Dimity,

I had a question on the product id,
{ 0x0b05, 0x1abb, "ASUS ROG RAIKIRI PRO", 0, XTYPE_XBOXONE },

This doesn’t match both my devices so I was wondering if somehow we have similar named devices with need for different product ids or if there was a mistake in the original commit?

My relevant entries from lsusb are 
Bus 001 Device 002: ID 0b05:1a3c ASUSTek Computer, Inc. ROG RAIKIRI PRO
Bus 001 Device 003: ID 0b05:1a3c ASUSTek Computer, Inc. ROG RAIKIRI PRO

On my system I have ended up using this udev rule for quite some time
ACTION=="add", ATTRS{idVendor}=="0b05", ATTRS{idProduct}=="1a3c", GROUP="input", MODE="0660", TAG+="uaccess", TAG+="seat", RUN+="/bin/sh -c 'echo 0b05 1a3c > /sys/bus/usb/drivers/xpad/new_id'"

Regards,
Justin

^ permalink raw reply

* [PATCH v1] HID: i2c-hid-of: Use named initializers for struct i2c_device_id
From: Uwe Kleine-König (The Capable Hub) @ 2026-05-19 16:04 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires; +Cc: linux-input, linux-kernel

While being less compact, using named initializers allows to more easily
see which members of the structs are assigned which value without having
to lookup the declaration of the struct. And it's also more robust
against changes to the struct definition.

This patch doesn't modify the compiled array, only its representation in
source form benefits. The former was confirmed with x86 and arm64
builds.

Signed-off-by: Uwe Kleine-König (The Capable Hub) <u.kleine-koenig@baylibre.com>
---
Hello,

this patch is part of a bigger quest to use named initializers for
mainly struct i2c_device_id::driver_data to be able to modify
i2c_device_id. See e.g.
https://lore.kernel.org/all/20260518111203.639603-2-u.kleine-koenig@baylibre.com/
for the details.

This patch here isn't critical for this quest, as this driver doesn't
make use of .driver_data, so apart from the better readability this is
only about consistency with other subsystems.

This is the only i2c driver under drivers/hid, so this is the only patch
needed to adapt the whole subsystem to the new style for initializing
i2c_device_id arrays.

Best regards
Uwe

 drivers/hid/i2c-hid/i2c-hid-of.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/drivers/hid/i2c-hid/i2c-hid-of.c b/drivers/hid/i2c-hid/i2c-hid-of.c
index 57379b77e977..59393d71ddb9 100644
--- a/drivers/hid/i2c-hid/i2c-hid-of.c
+++ b/drivers/hid/i2c-hid/i2c-hid-of.c
@@ -144,8 +144,8 @@ MODULE_DEVICE_TABLE(of, i2c_hid_of_match);
 #endif
 
 static const struct i2c_device_id i2c_hid_of_id_table[] = {
-	{ "hid" },
-	{ "hid-over-i2c" },
+	{ .name = "hid" },
+	{ .name = "hid-over-i2c" },
 	{ }
 };
 MODULE_DEVICE_TABLE(i2c, i2c_hid_of_id_table);

base-commit: 254f49634ee16a731174d2ae34bc50bd5f45e731
-- 
2.47.3


^ permalink raw reply related

* Re: [PATCH v3 0/4] HID: Proper fix for OOM in hid-core
From: Benjamin Tissoires @ 2026-05-19 14:00 UTC (permalink / raw)
  To: Greg Kroah-Hartman
  Cc: Lee Jones, Jiri Kosina, Filipe Laíns, Bastien Nocera,
	Ping Cheng, Jason Gerecke, Viresh Kumar, Johan Hovold, Alex Elder,
	Icenowy Zheng, linux-input, linux-kernel, greybus-dev,
	linux-staging, linux-usb, stable
In-Reply-To: <2026051937-hefty-registry-37b2@gregkh>

On May 19 2026, Greg Kroah-Hartman wrote:
> On Tue, May 19, 2026 at 02:46:13PM +0200, Benjamin Tissoires wrote:
> > On May 19 2026, Lee Jones wrote:
> > > On Tue, 12 May 2026, Lee Jones wrote:
> > > 
> > > > On Wed, 06 May 2026, Lee Jones wrote:
> > > > 
> > > > > On Mon, 04 May 2026, Benjamin Tissoires wrote:
> > > > > 
> > > > > > Commit 0a3fe972a7cb ("HID: core: Mitigate potential OOB by removing
> > > > > > bogus memset()") enforced the provided data to be at least the size of
> > > > > > the declared buffer in the report descriptor to prevent a buffer
> > > > > > overflow.
> > > > > > 
> > > > > > We only had corner cases of malicious devices exposing the OOM because
> > > > > > in most cases, the buffer provided by the transport layer needs to be
> > > > > > allocated at probe time and is large enough to handle all the possible
> > > > > > reports.
> > > > > > 
> > > > > > However, the patch from above, which enforces the spec a little bit more
> > > > > > introduced both regressions for devices not following the spec (not
> > > > > > necesserally malicious), but also a stream of errors for those devices.
> > > > > > 
> > > > > > Let's revert to the old behavior by giving more information to HID core
> > > > > > to be able to decide whether it can or not memset the rest of the buffer
> > > > > > to 0 and continue the processing.
> > > > > > 
> > > > > > Note that the first commit makes an API change, but the callers are
> > > > > > relatively limited, so it should be fine on its own. The second patch
> > > > > > can't really make the same kind of API change because we have too many
> > > > > > callers in various subsystems. We can switch them one by one to the safe
> > > > > > approach when needed.
> > > > > > 
> > > > > > The last 2 patches are small cleanups I initially put together with the
> > > > > > 2 first patches, but they can be applied on their own and don't need to
> > > > > > be pulled in stable like the first 2.
> > > > > > 
> > > > > > Cheers,
> > > > > > Benjamin
> > > > > > 
> > > > > > Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>
> > > > > > ---
> > > > > > Changes in v3:
> > > > > > - fixed ghib -> ghid in greybus
> > > > > > - fixed i386 size_t debug size reported by kernel-bot
> > > > > > - Link to v2: https://lore.kernel.org/r/20260416-wip-fix-core-v2-0-be92570e5627@kernel.org
> > > > > > 
> > > > > > Changes in v2:
> > > > > > - added a small blurb explaining the difference between the safe and the
> > > > > >   non safe version of hid_safe_input_report
> > > > > > - Link to v1: https://lore.kernel.org/r/20260415-wip-fix-core-v1-0-ed3c4c823175@kernel.org
> > > > > > 
> > > > > > ---
> > > > > > Benjamin Tissoires (4):
> > > > > >       HID: pass the buffer size to hid_report_raw_event
> > > > > >       HID: core: introduce hid_safe_input_report()
> > > > > >       HID: multitouch: use __free(kfree) to clean up temporary buffers
> > > > > >       HID: wacom: use __free(kfree) to clean up temporary buffers
> > > > > > 
> > > > > >  drivers/hid/bpf/hid_bpf_dispatch.c |  6 ++--
> > > > > >  drivers/hid/hid-core.c             | 67 ++++++++++++++++++++++++++++++--------
> > > > > >  drivers/hid/hid-gfrm.c             |  4 +--
> > > > > >  drivers/hid/hid-logitech-hidpp.c   |  2 +-
> > > > > >  drivers/hid/hid-multitouch.c       | 18 ++++------
> > > > > >  drivers/hid/hid-primax.c           |  2 +-
> > > > > >  drivers/hid/hid-vivaldi-common.c   |  2 +-
> > > > > >  drivers/hid/i2c-hid/i2c-hid-core.c |  7 ++--
> > > > > >  drivers/hid/usbhid/hid-core.c      | 11 ++++---
> > > > > >  drivers/hid/wacom_sys.c            | 46 +++++++++-----------------
> > > > > >  drivers/staging/greybus/hid.c      |  2 +-
> > > > > >  include/linux/hid.h                |  6 ++--
> > > > > >  include/linux/hid_bpf.h            | 14 +++++---
> > > > > >  13 files changed, 109 insertions(+), 78 deletions(-)
> > > > > 
> > > > > What's the plan for this set Benjamin? -rcs or -next?
> > > > 
> > > > Are there any updates on this set please?
> > > > 
> > > > FYI, this set is still important to us.
> > > > 
> > > > Ideally, if all is well, it would go into the -rcs for v7.1.
> > > 
> > > I'm still actively tracking these.
> > > 
> > > It looks like Mark has been reverting them from -next and I'm getting
> > > complaints from the Stable folks that they are causing build errors.
> > > 
> > >   drivers/hid/hid-core.c: In function 'hid_safe_input_report':
> > >   drivers/hid/hid-core.c:2195:16: error: too many arguments to function '__hid_input_report'
> > >     2195 |         return __hid_input_report(hid, type, data, bufsize, size, interrupt, 0,
> > > 
> > > Are you folks still working on this set?
> > 
> > Well, everything is in Linus' tree:
> > 
> > not yet in a released rc (taken yesterday by Linus directly):
> > 
> > 4d3a2a466b8d HID: core: Fix size_t specifier in hid_report_raw_event()
> > 
> > Already in 7.1-rc4:
> > 
> > 206342541fc8 HID: core: introduce hid_safe_input_report()
> > 2c85c61d1332 HID: pass the buffer size to hid_report_raw_event
> > 
> > Not sure why the patches don't apply to stable, but from an upstream
> > subsystem point of view, everything is in order.
> 
> We dropped them from stable because of the build breakage :(
> 

If that was just the i386 size_t issue, then it has been fixed in Linus'
tree. Could you try retaking them? Please?

Cheers,
Benjamin

^ permalink raw reply

* Re: [BUG] ALSA: usb-audio: use-after-free in snd_dualsense_ih_match during rapid USB reconnect
From: Takashi Iwai @ 2026-05-19 13:44 UTC (permalink / raw)
  To: Sean Brar
  Cc: linux-input, linux-sound, roderick.colenbrander, jikos, bentiss,
	perex, tiwai
In-Reply-To: <909876aa-7498-4ca4-b3a6-6618fbc6209d@seanbrar.com>

On Mon, 11 May 2026 23:58:30 +0200,
Sean Brar wrote:
> 
> A rapid USB disconnect/reconnect cycle on a DualSense controller
> (054c:0ce6) triggers a use-after-free in snd_dualsense_ih_match()
> (sound/usb/mixer_quirks.c), resulting in a general protection fault
> and leaving the USB subsystem in an unrecoverable state requiring a
> hard reboot.
> 
> Kernel version: 7.0.5-arch1-1 (also reproduced the trigger on
> 7.0.3-arch1-2; affected code in sound/usb/mixer_quirks.c is
> unchanged on current mainline)
> 
> Hardware:
>   - Sony DualSense Wireless Controller (054c:0ce6, bcdDevice=1.00)
>   - hw_version=0x00000711, fw_version=0x0110002a
>   - Host: Gigabyte B550 AORUS PRO AC, BIOS F17 03/22/2024
> 
> Trigger condition:
> 
> When connected to a degraded USB port (intermittent electrical
> contact), the DualSense enters a rapid disconnect/reconnect cycle.
> snd_usb_audio times out (ETIMEDOUT, -110) querying mixer controls
> during probe, each timeout triggers a USB reset, and the device
> reconnects every 1–2 seconds. This is not a driver bug, it's the
> expected kernel behavior when USB communication is unreliable, but
> it creates a race window for the use-after-free described below.
> 
> The bug:
> 
> In sound/usb/mixer_quirks.c, snd_dualsense_ih_match() is invoked as
> an input handler match callback from input_register_device() during
> ps_probe() (hid_playstation). At line 575, it obtains a pointer to
> the USB device struct:
> 
>     snd_dev = mei->info.head.mixer->chip->dev;
> 
> It then uses this pointer in dev_warn() calls at lines 579 and 585:
> 
>     dev_warn(&snd_dev->dev, "Failed to get input dev path\n");
>     dev_warn(&snd_dev->dev, "Failed to get USB dev path\n");
> 
> No reference is taken on snd_dev. If the USB device is concurrently
> disconnected and freed while snd_dualsense_ih_match() is executing,
> snd_dev becomes a dangling pointer. The dev_warn() call dereferences
> snd_dev->dev.kobj.name via dev_vprintk_emit() → strnlen(), faulting
> on the freed memory.
> 
> The OOPS offset (snd_dualsense_ih_match.cold+0xf/0x2b) corresponds
> to the first dev_warn() at line 579. The compiler placed both error
> branches in a cold section, and the offset is consistent with the
> earlier branch.
> 
> OOPS:
> 
>   Oops: general protection fault, probably for non-canonical address
> 0x441f0ffa1e0ff3: 0000 [#1] SMP NOPTI
>   CPU: 14 UID: 0 PID: 108 Comm: kworker/14:0 Not tainted 7.0.5-arch1-1
> #1 PREEMPT(full)
>   Hardware name: Gigabyte Technology Co., Ltd. B550 AORUS PRO AC/B550
> AORUS PRO AC, BIOS F17 03/22/2024
>   Workqueue: usb_hub_wq hub_event
>   RIP: 0010:strnlen+0x29/0x40
>   RSP: 0018:ffffce3f40537378 EFLAGS: 00010202
>   RAX: 00441f0ffa1e0ff3 RBX: 00441f0ffa1e0ff3 RCX: 0000000000000000
>   RDX: 00441f0ffa1e1003 RSI: 0000000000000010 RDI: 00441f0ffa1e0ff3
>   RBP: ffffce3f40537408 R08: 0000000000000000 R09: ffffce3f40537478
>   R10: 0000000000000004 R11: ffffffffc20ebe12 R12: ffff8943511c80b0
>   R13: ffff8943511c80b0 R14: ffff894345daf028 R15: ffffce3f40537418
>   FS:  0000000000000000(0000) GS:ffff8962144e7000(0000)
> knlGS:0000000000000000
>   CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
>   CR2: 000055e7f9dc3c18 CR3: 0000000135ffe000 CR4: 0000000000f50ef0
> 
> Stack trace:
> 
>   Call Trace:
>    <TASK>
>    dev_vprintk_emit+0x70/0x1b0
>    dev_printk_emit+0x61/0x7b
>    __dev_printk+0x2d/0x70
>    _dev_warn+0x7f/0x99
>    snd_dualsense_ih_match.cold+0xf/0x2b [snd_usb_audio
> a3e71fdbdb8c7ccfdfdb57dadc854b1d1e18445c]
>    input_attach_handler.isra.0+0x4b/0xa0
>    input_register_device.cold+0xf8/0x1f0
>    ps_probe+0xf79/0x10dc [hid_playstation
> 0cc5feea5231aa914fe418c8a06b14588cd5f064]
>    hid_device_probe+0x1b8/0x270
>    hid_add_device+0xcd/0x130
>    usbhid_probe+0x49b/0x6c0
>    usb_probe_interface+0xf8/0x2f0
>    usb_set_configuration+0x738/0x920
>    usb_generic_driver_probe+0x4a/0x70
>    usb_probe_device+0x44/0x170
>    usb_new_device.cold+0x154/0x3fd
>    hub_event+0x129d/0x1ad0
>    process_one_work+0x19c/0x3a0
>    worker_thread+0x1b1/0x310
>    kthread+0xe1/0x120
>    ret_from_fork+0x2bc/0x350
>    ret_from_fork_asm+0x1a/0x30
>    </TASK>
>   ---[ end trace 0000000000000000 ]---
> 
> Analysis:
> 
> The race is between two concurrent paths:
> 
>   1. Probe: hub_event → usb_new_device → ... → ps_probe →
>      input_register_device → input_attach_handler →
>      snd_dualsense_ih_match (accesses USB device struct at line 575)
> 
>   2. Disconnect: hub_event → usb_disconnect → ... → USB device
>      struct freed
> 
> snd_dualsense_ih_match() retrieves snd_dev via the mixer→chip→dev
> chain at line 575 without taking a reference. A concurrent disconnect
> can free the USB device struct before the subsequent dev_warn() call
> dereferences it. The faulting RDI value (0x441f0ffa1e0ff3) is
> non-canonical and consistent with SLUB freed-object poisoning,
> confirming a use-after-free.
> 
> Impact:
> 
> After the OOPS, the USB subsystem is left in an unrecoverable state:
> all USB ports stop enumerating devices (including ports not involved
> in the reconnect cycle), and a clean shutdown hangs in USB driver
> teardown. A hard reset is required. The trigger condition (rapid USB
> reconnection due to a degraded port) is a normal hardware failure
> mode.
> 
> Workaround:
> 
>   # /etc/modprobe.d/dualsense.conf
>   options snd_usb_audio ignore_ctl_error=1
> 
> This suppresses the mixer control timeouts that drive the reconnect
> cycle, preventing the race window from opening. DualSense headphone
> jack audio controls may not function correctly with this option.
> 
> Steps to reproduce:
> 
>   Disconnect/reconnect loop (the trigger condition):
>   1. Load hid_playstation and snd_usb_audio (default on standard
>      desktop kernels)
>   2. Connect a DualSense (054c:0ce6) via USB to a port with
>      unreliable electrical contact
>   3. Observe rapid disconnect/reconnect in dmesg -w, with device
>      number incrementing each cycle
> 
>   The OOPS:
>   The OOPS was observed after the loop ran for several minutes. Exact
>   timing-dependent reproduction beyond triggering the loop is not
>   confirmed; this was observed once and not retested to avoid the
>   unrecoverable USB hang.
> 
> Kernel .config (7.0.5-arch1-1):
> https://gist.github.com/seanbrar/d97a577efc3f48098d300105c4de398b
> dmesg with OOPS (7.0.5-arch1-1):
> https://gist.github.com/seanbrar/994ed882ca6f25b93e4763a8906d0fd5
> dmesg with reconnect loop (7.0.3-arch1-2):
> https://gist.github.com/seanbrar/188a75bf69fffe0d089f7aa82c6aebb6

Thanks for the report.

As this is involved with two individual devices, I wonder the bug is
triggered at which timing -- has the sound interface been already
disconnected (or being disconnected) while the input is being probed?

In such a case, does the change below help?


Takashi

-- 8< --
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -705,14 +705,15 @@ static int snd_dualsense_resume_jack(struct usb_mixer_elem_list *list)
 	return 0;
 }
 
-static void snd_dualsense_mixer_elem_free(struct snd_kcontrol *kctl)
+static void snd_dualsense_mixer_free(struct usb_mixer_interface *mixer)
 {
-	struct dualsense_mixer_elem_info *mei = snd_kcontrol_chip(kctl);
+	struct dualsense_mixer_elem_info *mei = mixer->private_data;
 
-	if (mei->ih.event)
+	if (mei && mei->ih.event) {
 		input_unregister_handler(&mei->ih);
-
-	snd_usb_mixer_elem_free(kctl);
+		mei->ih.event = NULL;
+	}
+	mixer->private_data = NULL;
 }
 
 static int snd_dualsense_jack_create(struct usb_mixer_interface *mixer,
@@ -744,7 +745,7 @@ static int snd_dualsense_jack_create(struct usb_mixer_interface *mixer,
 	}
 
 	strscpy(kctl->id.name, name, sizeof(kctl->id.name));
-	kctl->private_free = snd_dualsense_mixer_elem_free;
+	kctl->private_free = snd_usb_mixer_elem_free;
 
 	err = snd_usb_mixer_add_control(&mei->info.head, kctl);
 	if (err)
@@ -774,6 +775,9 @@ static int snd_dualsense_jack_create(struct usb_mixer_interface *mixer,
 		dev_warn(&mixer->chip->dev->dev,
 			 "Could not register input handler: %d\n", err);
 		mei->ih.event = NULL;
+	} else {
+		mixer->private_free = snd_dualsense_mixer_free;
+		mixer->private_data = mei;
 	}
 
 	return 0;

^ permalink raw reply

* [RFC PATCH] HID: core: quiesce input in hid_hw_stop() to prevent use-after-free
From: Philipp Weber @ 2026-05-19 13:00 UTC (permalink / raw)
  To: bentiss, jikos
  Cc: eadavis, linux-input, linux-kernel, syzkaller-bugs,
	syzbot+9eebf5f6544c5e873858
In-Reply-To: <69eed7e0.a00a0220.7773.0026.GAE@google.com>

A driver's probe calls hid_device_io_start() to enable input delivery,
then fails at a later initialization step and unwinds via hid_hw_stop().
The unwind frees struct hidraw via hidraw_disconnect() while in-flight
HID reports may still be running on another CPU, dereferencing the
freed object through hidraw_report_event(). syzbot reports the
resulting use-after-free for the corsair-psu HID driver.

Edward Adam Davis posted a per-driver fix for corsair-psu that adds
an explicit hid_device_io_stop() before hid_hw_stop() in the probe
error path ("hwmon: prevent packets from going to driver for probe",
2026-04-28). Auditing the tree shows 15 drivers call
hid_device_io_start(); 7 also call hid_device_io_stop() and 8 do not:

  drivers calling hid_device_io_start() without a matching
  hid_device_io_stop() before hid_hw_stop():
    drivers/hwmon/corsair-psu.c       (fix posted by Edward)
    drivers/hwmon/corsair-cpro.c
    drivers/hwmon/nzxt-kraken3.c
    drivers/hwmon/nzxt-smart2.c
    drivers/hwmon/gigabyte_waterforce.c
    drivers/hid/hid-logitech-dj.c
    drivers/hid/hid-nintendo.c
    drivers/hid/hid-mcp2221.c

Roughly half of all callers of the API are exposed. Centralize the
quiesce in hid_hw_stop() so callers do not have to remember the
matching stop: if a driver has left hdev->io_started true on entry,
call hid_device_io_stop() before hid_disconnect().

For the 7 drivers that already call hid_device_io_stop() correctly,
hdev->io_started is false on entry, the guard short-circuits, and
behavior is unchanged.

No Fixes: tag because the affected drivers gained their
hid_device_io_start() calls independently over years; the bug is a
class-wide API misuse rather than a regression from one commit.

Reported-by: syzbot+9eebf5f6544c5e873858@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=9eebf5f6544c5e873858
Signed-off-by: Philipp Weber <kernel@phwe.de>
---
 drivers/hid/hid-core.c | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index 41a79e43c82b..6b024118d983 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -2440,9 +2440,16 @@ EXPORT_SYMBOL_GPL(hid_hw_start);
  *
  * This is usually called from remove function or from probe when something
  * failed and hid_hw_start was called already.
+ *
+ * If the caller enabled HID input via hid_device_io_start() and is unwinding
+ * without an explicit hid_device_io_stop(), quiesce input first so that
+ * in-flight reports cannot reach handlers (e.g. hidraw_report_event) whose
+ * backing objects hid_disconnect() is about to free.
  */
 void hid_hw_stop(struct hid_device *hdev)
 {
+	if (hdev->io_started)
+		hid_device_io_stop(hdev);
 	hid_disconnect(hdev);
 	hdev->ll_driver->stop(hdev);
 }

base-commit: ab5fce87a778cb780a05984a2ca448f2b41aafbf
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH v3 0/4] HID: Proper fix for OOM in hid-core
From: Greg Kroah-Hartman @ 2026-05-19 12:48 UTC (permalink / raw)
  To: Benjamin Tissoires
  Cc: Lee Jones, Jiri Kosina, Filipe Laíns, Bastien Nocera,
	Ping Cheng, Jason Gerecke, Viresh Kumar, Johan Hovold, Alex Elder,
	Icenowy Zheng, linux-input, linux-kernel, greybus-dev,
	linux-staging, linux-usb, stable
In-Reply-To: <agxbD6k60vQYrJ6T@beelink>

On Tue, May 19, 2026 at 02:46:13PM +0200, Benjamin Tissoires wrote:
> On May 19 2026, Lee Jones wrote:
> > On Tue, 12 May 2026, Lee Jones wrote:
> > 
> > > On Wed, 06 May 2026, Lee Jones wrote:
> > > 
> > > > On Mon, 04 May 2026, Benjamin Tissoires wrote:
> > > > 
> > > > > Commit 0a3fe972a7cb ("HID: core: Mitigate potential OOB by removing
> > > > > bogus memset()") enforced the provided data to be at least the size of
> > > > > the declared buffer in the report descriptor to prevent a buffer
> > > > > overflow.
> > > > > 
> > > > > We only had corner cases of malicious devices exposing the OOM because
> > > > > in most cases, the buffer provided by the transport layer needs to be
> > > > > allocated at probe time and is large enough to handle all the possible
> > > > > reports.
> > > > > 
> > > > > However, the patch from above, which enforces the spec a little bit more
> > > > > introduced both regressions for devices not following the spec (not
> > > > > necesserally malicious), but also a stream of errors for those devices.
> > > > > 
> > > > > Let's revert to the old behavior by giving more information to HID core
> > > > > to be able to decide whether it can or not memset the rest of the buffer
> > > > > to 0 and continue the processing.
> > > > > 
> > > > > Note that the first commit makes an API change, but the callers are
> > > > > relatively limited, so it should be fine on its own. The second patch
> > > > > can't really make the same kind of API change because we have too many
> > > > > callers in various subsystems. We can switch them one by one to the safe
> > > > > approach when needed.
> > > > > 
> > > > > The last 2 patches are small cleanups I initially put together with the
> > > > > 2 first patches, but they can be applied on their own and don't need to
> > > > > be pulled in stable like the first 2.
> > > > > 
> > > > > Cheers,
> > > > > Benjamin
> > > > > 
> > > > > Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>
> > > > > ---
> > > > > Changes in v3:
> > > > > - fixed ghib -> ghid in greybus
> > > > > - fixed i386 size_t debug size reported by kernel-bot
> > > > > - Link to v2: https://lore.kernel.org/r/20260416-wip-fix-core-v2-0-be92570e5627@kernel.org
> > > > > 
> > > > > Changes in v2:
> > > > > - added a small blurb explaining the difference between the safe and the
> > > > >   non safe version of hid_safe_input_report
> > > > > - Link to v1: https://lore.kernel.org/r/20260415-wip-fix-core-v1-0-ed3c4c823175@kernel.org
> > > > > 
> > > > > ---
> > > > > Benjamin Tissoires (4):
> > > > >       HID: pass the buffer size to hid_report_raw_event
> > > > >       HID: core: introduce hid_safe_input_report()
> > > > >       HID: multitouch: use __free(kfree) to clean up temporary buffers
> > > > >       HID: wacom: use __free(kfree) to clean up temporary buffers
> > > > > 
> > > > >  drivers/hid/bpf/hid_bpf_dispatch.c |  6 ++--
> > > > >  drivers/hid/hid-core.c             | 67 ++++++++++++++++++++++++++++++--------
> > > > >  drivers/hid/hid-gfrm.c             |  4 +--
> > > > >  drivers/hid/hid-logitech-hidpp.c   |  2 +-
> > > > >  drivers/hid/hid-multitouch.c       | 18 ++++------
> > > > >  drivers/hid/hid-primax.c           |  2 +-
> > > > >  drivers/hid/hid-vivaldi-common.c   |  2 +-
> > > > >  drivers/hid/i2c-hid/i2c-hid-core.c |  7 ++--
> > > > >  drivers/hid/usbhid/hid-core.c      | 11 ++++---
> > > > >  drivers/hid/wacom_sys.c            | 46 +++++++++-----------------
> > > > >  drivers/staging/greybus/hid.c      |  2 +-
> > > > >  include/linux/hid.h                |  6 ++--
> > > > >  include/linux/hid_bpf.h            | 14 +++++---
> > > > >  13 files changed, 109 insertions(+), 78 deletions(-)
> > > > 
> > > > What's the plan for this set Benjamin? -rcs or -next?
> > > 
> > > Are there any updates on this set please?
> > > 
> > > FYI, this set is still important to us.
> > > 
> > > Ideally, if all is well, it would go into the -rcs for v7.1.
> > 
> > I'm still actively tracking these.
> > 
> > It looks like Mark has been reverting them from -next and I'm getting
> > complaints from the Stable folks that they are causing build errors.
> > 
> >   drivers/hid/hid-core.c: In function 'hid_safe_input_report':
> >   drivers/hid/hid-core.c:2195:16: error: too many arguments to function '__hid_input_report'
> >     2195 |         return __hid_input_report(hid, type, data, bufsize, size, interrupt, 0,
> > 
> > Are you folks still working on this set?
> 
> Well, everything is in Linus' tree:
> 
> not yet in a released rc (taken yesterday by Linus directly):
> 
> 4d3a2a466b8d HID: core: Fix size_t specifier in hid_report_raw_event()
> 
> Already in 7.1-rc4:
> 
> 206342541fc8 HID: core: introduce hid_safe_input_report()
> 2c85c61d1332 HID: pass the buffer size to hid_report_raw_event
> 
> Not sure why the patches don't apply to stable, but from an upstream
> subsystem point of view, everything is in order.

We dropped them from stable because of the build breakage :(

thanks,

greg k-h

^ permalink raw reply

* Re: [PATCH v3 0/4] HID: Proper fix for OOM in hid-core
From: Benjamin Tissoires @ 2026-05-19 12:46 UTC (permalink / raw)
  To: Lee Jones
  Cc: Jiri Kosina, Filipe Laíns, Bastien Nocera, Ping Cheng,
	Jason Gerecke, Viresh Kumar, Johan Hovold, Alex Elder,
	Greg Kroah-Hartman, Icenowy Zheng, linux-input, linux-kernel,
	greybus-dev, linux-staging, linux-usb, stable
In-Reply-To: <20260519111723.GU305027@google.com>

On May 19 2026, Lee Jones wrote:
> On Tue, 12 May 2026, Lee Jones wrote:
> 
> > On Wed, 06 May 2026, Lee Jones wrote:
> > 
> > > On Mon, 04 May 2026, Benjamin Tissoires wrote:
> > > 
> > > > Commit 0a3fe972a7cb ("HID: core: Mitigate potential OOB by removing
> > > > bogus memset()") enforced the provided data to be at least the size of
> > > > the declared buffer in the report descriptor to prevent a buffer
> > > > overflow.
> > > > 
> > > > We only had corner cases of malicious devices exposing the OOM because
> > > > in most cases, the buffer provided by the transport layer needs to be
> > > > allocated at probe time and is large enough to handle all the possible
> > > > reports.
> > > > 
> > > > However, the patch from above, which enforces the spec a little bit more
> > > > introduced both regressions for devices not following the spec (not
> > > > necesserally malicious), but also a stream of errors for those devices.
> > > > 
> > > > Let's revert to the old behavior by giving more information to HID core
> > > > to be able to decide whether it can or not memset the rest of the buffer
> > > > to 0 and continue the processing.
> > > > 
> > > > Note that the first commit makes an API change, but the callers are
> > > > relatively limited, so it should be fine on its own. The second patch
> > > > can't really make the same kind of API change because we have too many
> > > > callers in various subsystems. We can switch them one by one to the safe
> > > > approach when needed.
> > > > 
> > > > The last 2 patches are small cleanups I initially put together with the
> > > > 2 first patches, but they can be applied on their own and don't need to
> > > > be pulled in stable like the first 2.
> > > > 
> > > > Cheers,
> > > > Benjamin
> > > > 
> > > > Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>
> > > > ---
> > > > Changes in v3:
> > > > - fixed ghib -> ghid in greybus
> > > > - fixed i386 size_t debug size reported by kernel-bot
> > > > - Link to v2: https://lore.kernel.org/r/20260416-wip-fix-core-v2-0-be92570e5627@kernel.org
> > > > 
> > > > Changes in v2:
> > > > - added a small blurb explaining the difference between the safe and the
> > > >   non safe version of hid_safe_input_report
> > > > - Link to v1: https://lore.kernel.org/r/20260415-wip-fix-core-v1-0-ed3c4c823175@kernel.org
> > > > 
> > > > ---
> > > > Benjamin Tissoires (4):
> > > >       HID: pass the buffer size to hid_report_raw_event
> > > >       HID: core: introduce hid_safe_input_report()
> > > >       HID: multitouch: use __free(kfree) to clean up temporary buffers
> > > >       HID: wacom: use __free(kfree) to clean up temporary buffers
> > > > 
> > > >  drivers/hid/bpf/hid_bpf_dispatch.c |  6 ++--
> > > >  drivers/hid/hid-core.c             | 67 ++++++++++++++++++++++++++++++--------
> > > >  drivers/hid/hid-gfrm.c             |  4 +--
> > > >  drivers/hid/hid-logitech-hidpp.c   |  2 +-
> > > >  drivers/hid/hid-multitouch.c       | 18 ++++------
> > > >  drivers/hid/hid-primax.c           |  2 +-
> > > >  drivers/hid/hid-vivaldi-common.c   |  2 +-
> > > >  drivers/hid/i2c-hid/i2c-hid-core.c |  7 ++--
> > > >  drivers/hid/usbhid/hid-core.c      | 11 ++++---
> > > >  drivers/hid/wacom_sys.c            | 46 +++++++++-----------------
> > > >  drivers/staging/greybus/hid.c      |  2 +-
> > > >  include/linux/hid.h                |  6 ++--
> > > >  include/linux/hid_bpf.h            | 14 +++++---
> > > >  13 files changed, 109 insertions(+), 78 deletions(-)
> > > 
> > > What's the plan for this set Benjamin? -rcs or -next?
> > 
> > Are there any updates on this set please?
> > 
> > FYI, this set is still important to us.
> > 
> > Ideally, if all is well, it would go into the -rcs for v7.1.
> 
> I'm still actively tracking these.
> 
> It looks like Mark has been reverting them from -next and I'm getting
> complaints from the Stable folks that they are causing build errors.
> 
>   drivers/hid/hid-core.c: In function 'hid_safe_input_report':
>   drivers/hid/hid-core.c:2195:16: error: too many arguments to function '__hid_input_report'
>     2195 |         return __hid_input_report(hid, type, data, bufsize, size, interrupt, 0,
> 
> Are you folks still working on this set?

Well, everything is in Linus' tree:

not yet in a released rc (taken yesterday by Linus directly):

4d3a2a466b8d HID: core: Fix size_t specifier in hid_report_raw_event()

Already in 7.1-rc4:

206342541fc8 HID: core: introduce hid_safe_input_report()
2c85c61d1332 HID: pass the buffer size to hid_report_raw_event

Not sure why the patches don't apply to stable, but from an upstream
subsystem point of view, everything is in order.

I still have to resend the last 2 patches, but they are not fixing
anything, just nice to have.

Cheers,
Benjamin

^ permalink raw reply

* Re: [PATCH v3 0/4] HID: Proper fix for OOM in hid-core
From: Lee Jones @ 2026-05-19 11:17 UTC (permalink / raw)
  To: Benjamin Tissoires
  Cc: Jiri Kosina, Filipe Laíns, Bastien Nocera, Ping Cheng,
	Jason Gerecke, Viresh Kumar, Johan Hovold, Alex Elder,
	Greg Kroah-Hartman, Icenowy Zheng, linux-input, linux-kernel,
	greybus-dev, linux-staging, linux-usb, stable
In-Reply-To: <20260512101723.GU305027@google.com>

On Tue, 12 May 2026, Lee Jones wrote:

> On Wed, 06 May 2026, Lee Jones wrote:
> 
> > On Mon, 04 May 2026, Benjamin Tissoires wrote:
> > 
> > > Commit 0a3fe972a7cb ("HID: core: Mitigate potential OOB by removing
> > > bogus memset()") enforced the provided data to be at least the size of
> > > the declared buffer in the report descriptor to prevent a buffer
> > > overflow.
> > > 
> > > We only had corner cases of malicious devices exposing the OOM because
> > > in most cases, the buffer provided by the transport layer needs to be
> > > allocated at probe time and is large enough to handle all the possible
> > > reports.
> > > 
> > > However, the patch from above, which enforces the spec a little bit more
> > > introduced both regressions for devices not following the spec (not
> > > necesserally malicious), but also a stream of errors for those devices.
> > > 
> > > Let's revert to the old behavior by giving more information to HID core
> > > to be able to decide whether it can or not memset the rest of the buffer
> > > to 0 and continue the processing.
> > > 
> > > Note that the first commit makes an API change, but the callers are
> > > relatively limited, so it should be fine on its own. The second patch
> > > can't really make the same kind of API change because we have too many
> > > callers in various subsystems. We can switch them one by one to the safe
> > > approach when needed.
> > > 
> > > The last 2 patches are small cleanups I initially put together with the
> > > 2 first patches, but they can be applied on their own and don't need to
> > > be pulled in stable like the first 2.
> > > 
> > > Cheers,
> > > Benjamin
> > > 
> > > Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>
> > > ---
> > > Changes in v3:
> > > - fixed ghib -> ghid in greybus
> > > - fixed i386 size_t debug size reported by kernel-bot
> > > - Link to v2: https://lore.kernel.org/r/20260416-wip-fix-core-v2-0-be92570e5627@kernel.org
> > > 
> > > Changes in v2:
> > > - added a small blurb explaining the difference between the safe and the
> > >   non safe version of hid_safe_input_report
> > > - Link to v1: https://lore.kernel.org/r/20260415-wip-fix-core-v1-0-ed3c4c823175@kernel.org
> > > 
> > > ---
> > > Benjamin Tissoires (4):
> > >       HID: pass the buffer size to hid_report_raw_event
> > >       HID: core: introduce hid_safe_input_report()
> > >       HID: multitouch: use __free(kfree) to clean up temporary buffers
> > >       HID: wacom: use __free(kfree) to clean up temporary buffers
> > > 
> > >  drivers/hid/bpf/hid_bpf_dispatch.c |  6 ++--
> > >  drivers/hid/hid-core.c             | 67 ++++++++++++++++++++++++++++++--------
> > >  drivers/hid/hid-gfrm.c             |  4 +--
> > >  drivers/hid/hid-logitech-hidpp.c   |  2 +-
> > >  drivers/hid/hid-multitouch.c       | 18 ++++------
> > >  drivers/hid/hid-primax.c           |  2 +-
> > >  drivers/hid/hid-vivaldi-common.c   |  2 +-
> > >  drivers/hid/i2c-hid/i2c-hid-core.c |  7 ++--
> > >  drivers/hid/usbhid/hid-core.c      | 11 ++++---
> > >  drivers/hid/wacom_sys.c            | 46 +++++++++-----------------
> > >  drivers/staging/greybus/hid.c      |  2 +-
> > >  include/linux/hid.h                |  6 ++--
> > >  include/linux/hid_bpf.h            | 14 +++++---
> > >  13 files changed, 109 insertions(+), 78 deletions(-)
> > 
> > What's the plan for this set Benjamin? -rcs or -next?
> 
> Are there any updates on this set please?
> 
> FYI, this set is still important to us.
> 
> Ideally, if all is well, it would go into the -rcs for v7.1.

I'm still actively tracking these.

It looks like Mark has been reverting them from -next and I'm getting
complaints from the Stable folks that they are causing build errors.

  drivers/hid/hid-core.c: In function 'hid_safe_input_report':
  drivers/hid/hid-core.c:2195:16: error: too many arguments to function '__hid_input_report'
    2195 |         return __hid_input_report(hid, type, data, bufsize, size, interrupt, 0,

Are you folks still working on this set?

-- 
Lee Jones

^ permalink raw reply

* Re: [PATCH 1/1] HID: wacom: Fix OOB write in wacom_hid_set_device_mode()
From: Lee Jones @ 2026-05-19 11:13 UTC (permalink / raw)
  To: Ping Cheng
  Cc: Ping Cheng, Jason Gerecke, Jiri Kosina, Benjamin Tissoires,
	linux-input, linux-kernel, stable
In-Reply-To: <CAF8JNhKTMpT3CGq_oDqaGVygqXK0jjvrvjxbAWUerqtWzdB9+Q@mail.gmail.com>

On Wed, 13 May 2026, Ping Cheng wrote:

> On Wed, May 13, 2026 at 1:05 AM Lee Jones <lee@kernel.org> wrote:
> >
> > wacom_hid_set_device_mode() currently assumes that the HID_DG_INPUTMODE
> > usage is always located in the first field (field[0]) of the feature report.
> > However, a device can specify HID_DG_INPUTMODE in a different field.
> >
> > If HID_DG_INPUTMODE is in a field other than the first one and the first
> > field has a report_count smaller than the usage_index of HID_DG_INPUTMODE,
> > this leads to an out-of-bounds write to r->field[0]->value.
> >
> > Fix this by storing the field index of HID_DG_INPUTMODE in 'struct
> > hid_data' during feature mapping.  In wacom_hid_set_device_mode(), use
> > this stored field index to access the correct field and add bounds
> > checks to ensure both the field index and the value index are within
> > valid ranges before writing.
> >
> > Cc: stable@vger.kernel.org
> > Fixes: 5ae6e89f7409 ("HID: wacom: implement the finger part of the HID generic handling")
> > Signed-off-by: Lee Jones <lee@kernel.org>
> 
> Patch looks sensible to me. Thank you for your effort, Lee!
> 
> Tested-by: Ping Cheng <ping.cheng@wacom.com>
> Reviewed-by: Ping Cheng <ping.cheng@wacom.com>

Thank you Ping, I appreciate your review.

HID folks - any movement on this please?

-- 
Lee Jones

^ permalink raw reply

* Re: [PATCH] i2c: add sanity check for input SMBus data length
From: Wolfram Sang @ 2026-05-19  9:43 UTC (permalink / raw)
  To: Edward Adam Davis
  Cc: syzbot+64ca69977b37604cd6d9, bentiss, jikos, linux-i2c,
	linux-input, linux-kernel, michael.zaidman, syzkaller-bugs
In-Reply-To: <tencent_9443A8C45A37693763A6D7D3658367896405@qq.com>

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

Hi,

thanks for your patch!

On Tue, Jan 20, 2026 at 09:47:02PM +0800, Edward Adam Davis wrote:
> The value passed to block[0] in the user-constructed data is too large,
> exceeding the length that data for SMBus messages can accommodate. This
> triggered the out-of-bounds access reported by syzbot [1].
> 
> Adding relevant data size checks in the smbus ioctl can prevent this
> out-of-bounds access.
> 
> [1]
> BUG: KASAN: stack-out-of-bounds in ft260_smbus_write+0x19b/0x2f0 drivers/hid/hid-ft260.c:486
> Read of size 42 at addr ffffc90003427d81 by task syz.2.65/6119
> Call Trace:
>  ft260_smbus_write+0x19b/0x2f0 drivers/hid/hid-ft260.c:486
>  ft260_smbus_xfer+0x22c/0x640 drivers/hid/hid-ft260.c:736
> 

Did you look for a suitable Fixes tag?

> Reported-by: syzbot+64ca69977b37604cd6d9@syzkaller.appspotmail.com
> Closes: https://syzkaller.appspot.com/bug?extid=64ca69977b37604cd6d9
> Signed-off-by: Edward Adam Davis <eadavis@qq.com>
> ---
>  drivers/i2c/i2c-dev.c | 8 ++++++++
>  1 file changed, 8 insertions(+)
> 
> diff --git a/drivers/i2c/i2c-dev.c b/drivers/i2c/i2c-dev.c
> index e9577f920286..6725a49d6921 100644
> --- a/drivers/i2c/i2c-dev.c
> +++ b/drivers/i2c/i2c-dev.c
> @@ -378,6 +378,14 @@ static noinline int i2cdev_ioctl_smbus(struct i2c_client *client,
>  	    (read_write == I2C_SMBUS_WRITE)) {
>  		if (copy_from_user(&temp, data, datasize))
>  			return -EFAULT;
> +
> +		if (temp.block[0] > datasize) {
> +			dev_dbg(&client->adapter->dev,
> +				"user input data size (%u) is too big "
> +				"in ioctl I2C_SMBUS.\n",

Strings stay in one line, please, even if they break the line length.

> +				temp.block[0]);
> +			return -EINVAL;
> +		}
>  	}
>  	if (size == I2C_SMBUS_I2C_BLOCK_BROKEN) {
>  		/* Convert old I2C block commands to the new

Happy hacking,

   Wolfram


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

^ permalink raw reply

* Re: [PATCH] ARM: move Risc PC-specific <asm/hardware/iomd.h> header into mach-rpc
From: Helge Deller @ 2026-05-19  6:51 UTC (permalink / raw)
  To: Ethan Nelson-Moore, linux-arm-kernel, linux-i2c, linux-input,
	linux-fbdev
  Cc: Russell King, Andi Shyti, Dmitry Torokhov, Kees Cook
In-Reply-To: <20260510031100.255248-1-enelsonmoore@gmail.com>

On 5/10/26 05:10, Ethan Nelson-Moore wrote:
> The <asm/hardware/iomd.h> header is specific to the IOMD chip used on
> the Risc PC. Move it into mach-rpc to avoid polluting asm/hardware/
> with machine-specific headers.
> 
> Also take the opportunity to remove a comment with the file path from
> the header, which is bad style.
> 
> Signed-off-by: Ethan Nelson-Moore <enelsonmoore@gmail.com>
> ---
>   MAINTAINERS                                                     | 1 -
>   arch/arm/mach-rpc/dma.c                                         | 2 +-
>   arch/arm/{include/asm/hardware => mach-rpc/include/mach}/iomd.h | 2 --
>   arch/arm/mach-rpc/irq.c                                         | 2 +-
>   arch/arm/mach-rpc/riscpc.c                                      | 2 +-
>   arch/arm/mach-rpc/time.c                                        | 2 +-
>   drivers/i2c/busses/i2c-acorn.c                                  | 2 +-
>   drivers/input/mouse/rpcmouse.c                                  | 2 +-
>   drivers/input/serio/rpckbd.c                                    | 2 +-

>   drivers/video/fbdev/acornfb.h                                   | 2 +-

Regarding the fbdev change:
Acked-by: Helge Deller <deller@gmx.de>

I assume this patch is pushed via the arm tree?

Helge

^ permalink raw reply

* RE: [PATCH v3 0/9] iio: introduce devm_ API for hid sensro setup and cleanup
From: Zhang, Lixu @ 2026-05-19  5:22 UTC (permalink / raw)
  To: Sanjay Chitroda
  Cc: jikos@kernel.org, jic23@kernel.org,
	srinivas.pandruvada@linux.intel.com, Lechner, David,
	nuno.sa@analog.com, andy@kernel.org, sakari.ailus@linux.intel.com,
	linux-input@vger.kernel.org, linux-iio@vger.kernel.org,
	linux-kernel@vger.kernel.org
In-Reply-To: <agoi55wCo//eY1YQ@hu-ckantibh-hyd.qualcomm.com>

>-----Original Message-----
>From: Sanjay Chitroda <sanjayembeddedse@gmail.com>
>Sent: Monday, May 18, 2026 4:20 AM
>To: Zhang, Lixu <lixu.zhang@intel.com>
>Cc: jikos@kernel.org; jic23@kernel.org; srinivas.pandruvada@linux.intel.com;
>Lechner, David <dlechner@baylibre.com>; nuno.sa@analog.com;
>andy@kernel.org; sakari.ailus@linux.intel.com; linux-input@vger.kernel.org;
>linux-iio@vger.kernel.org; linux-kernel@vger.kernel.org
>Subject: Re: [PATCH v3 0/9] iio: introduce devm_ API for hid sensro setup and
>cleanup
>
>On Thu, May 14, 2026 at 02:47:52AM +0000, Zhang, Lixu wrote:
>> Tested-by: Zhang Lixu <lixu.zhang@intel.com>
>
>Hi Zhang,
>
>Thanks for the testing and validaiton.
>I hope you validated the complete series, including the unbind flow for the
>drivers using devm API.

Hi Sanjay,

Yes, I validated the complete series. The unbind flow was tested as Srinivas previously
suggested - performing the unbind while iio-sensor-proxy has an open session. No
regression issues were observed.

Since iio-sensor-proxy does not typically open a gyro session, I applied the same changes
from hid-sensor-gyro-3d to hid-sensor-accel-3d and tested the unbind flow on that driver
as well. Again, no regression issues were found.

Thanks,
Lixu

>
>Hi Jonathan,
>
>How should we take this series forward? should I extend the series to cover
>remaining HID IIO drivers for this devm API, or this series can applied to IIO tree
>first and sent followup patches on top of that?
>
>Thanks, Sanjay
>
>>
>> >-----Original Message-----
>> >From: Sanjay Chitroda <sanjayembeddedse@gmail.com>
>> >Sent: Saturday, May 9, 2026 6:11 PM
>> >To: jikos@kernel.org; jic23@kernel.org;
>> >srinivas.pandruvada@linux.intel.com
>> >Cc: Lechner, David <dlechner@baylibre.com>; nuno.sa@analog.com;
>> >andy@kernel.org; sanjayembeddedse@gmail.com;
>> >sakari.ailus@linux.intel.com; linux-input@vger.kernel.org; linux-
>> >iio@vger.kernel.org; linux-kernel@vger.kernel.org
>> >Subject: [PATCH v3 0/9] iio: introduce devm_ API for hid sensro setup
>> >and cleanup
>> >
>> >From: Sanjay Chitroda <sanjayembeddedse@gmail.com>
>> >
>> >Key highlights:
>> >- Prepare change as pre-requisite for devm conversion for HID IIO
>> >  drivers by removing redundant argument
>> >- Add devm API to setup trigger and clenaup resource using
>> >  devm_add_action_or_reset()
>> >- few cleanup and prepratory changes before updating driver for devm_
>> >- few sample driver update using devm conversion to auto release
>> >resource
>> >
>> >changes in v3:
>> >- Added cleanup and prepratory changes before adding devm_ API
>> >  conversion based on self review: 0002, 0004, 0006, 0007 and 0008
>> >- Address andy's review comment on commit message and coding style
>> >- v2 series -> https://lore.kernel.org/all/20260429175918.2541914-1-
>> >sanjayembedded@gmail.com/
>> >changes in v2:
>> >- Following input from Jonathan and Andy, squash initial patch v1
>> >  series in single change as individual change should not break
>> >anything
>> >- Add devm API support and two driver using the same
>> >- v1 series -> https://lore.kernel.org/all/20260428071613.1134053-1-
>> >sanjayembedded@gmail.com/
>> >
>> >Testing:
>> >  - Compiled with W=1
>> >  - Build-tested on QEMU x86_64
>> >
>> >Based on further feedback and reviews, I would extend this series to
>> >convert all HID IIO driver to use devm_* API.
>> >
>> >Thanks,
>> >Sanjay Chitroda
>> >
>> >
>> >Sanjay Chitroda (9):
>> >  iio: hid-sensors: drop redundant iio_dev argument
>> >  iio: hid-sensors: cleanup codestyle warning
>> >  iio: hid-sensors: introduce device managed API
>> >  iio: gyro: hid-sensor-gyro-3d: cleanup codestyle warning
>> >  iio: gyro: hid-sensor-gyro-3d: drop hid_sensor_remove_trigger() using
>> >    devm API
>> >  iio: humidity: hid-sensor-humidity: cleanup codestyle check
>> >  iio: humidity: hid-sensor-humidity: use common device for devres
>> >  iio: humidity: hid-sensor-humidity: use local struct device
>> >  iio: humidity: hid-sensor-humidity: drop hid_sensor_remove_trigger()
>> >    using devm API
>> >
>> > drivers/iio/accel/hid-sensor-accel-3d.c       |  4 +-
>> > .../common/hid-sensors/hid-sensor-trigger.c   | 24 +++++++-
>> > .../common/hid-sensors/hid-sensor-trigger.h   |  5 +-
>> > drivers/iio/gyro/hid-sensor-gyro-3d.c         | 16 ++---
>> > drivers/iio/humidity/hid-sensor-humidity.c    | 61 +++++++++----------
>> > drivers/iio/light/hid-sensor-als.c            |  4 +-
>> > drivers/iio/light/hid-sensor-prox.c           |  4 +-
>> > drivers/iio/magnetometer/hid-sensor-magn-3d.c |  4 +-
>> >drivers/iio/orientation/hid-sensor-incl-3d.c  |  4 +-
>> >drivers/iio/orientation/hid- sensor-rotation.c |  4 +-  .../position/hid-sensor-
>custom-intel-hinge.c  |  4 +-
>> > drivers/iio/pressure/hid-sensor-press.c       |  4 +-
>> > .../iio/temperature/hid-sensor-temperature.c  |  4 +-
>> > 13 files changed, 78 insertions(+), 64 deletions(-)
>> >
>> >
>> >base-commit: 39b80c5c9830d12d2d6531059001301c4265322a
>> >--
>> >2.34.1
>> >
>>

^ permalink raw reply

* Re: [PATCH v6 4/4] HID: hid-msi: Add Rumble Intensity Attributes
From: sashiko-bot @ 2026-05-19  0:22 UTC (permalink / raw)
  To: Derek J. Clark; +Cc: linux-input, dmitry.torokhov
In-Reply-To: <20260518222935.1802071-5-derekjohn.clark@gmail.com>

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] State machine desynchronization and data race on `profile_pending` leads to corrupted rumble intensity reads.
--

commit 85612b755f6822082f71e029c33a01200dc4046f
Author: Derek J. Clark <derekjohn.clark@gmail.com>

    HID: hid-msi: Add Rumble Intensity Attributes
    
    Adds intensity adjustment for the left and right rumble motors.

> diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
> index 4d267b896da72..339f256b60dbe 100644
> --- a/drivers/hid/hid-msi.c
> +++ b/drivers/hid/hid-msi.c

[ ... ]

> @@ -882,6 +901,142 @@ static ssize_t button_mapping_options_show(struct device *dev,
>  }
>  static DEVICE_ATTR_RO(button_mapping_options);

[ ... ]

> +static ssize_t rumble_intensity_left_show(struct device *dev,
> +					  struct device_attribute *attr,
> +					  char *buf)
> +{
> +	u8 data[4] = { 0x01, (rumble_addr[0] >> 8) & 0xff, rumble_addr[0] & 0xff, 0x01 };
> +	struct hid_device *hdev = to_hid_device(dev);
> +	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
> +	int ret;
> +	u8 val;
> +
> +	if (!drvdata->gamepad_registered)
> +		return -ENODEV;
> +
> +	guard(mutex)(&drvdata->profile_mutex);
> +	drvdata->profile_pending = CLAW_RUMBLE_LEFT_PENDING;
> +	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data,
> +				    ARRAY_SIZE(data), 25);
> +	if (ret) {
> +		drvdata->profile_pending = CLAW_NO_PENDING;
> +		return ret;
> +	}

Can the profile_pending state become desynchronized if a hardware read times
out?

If claw_hw_output_report() times out, it sets
drvdata->profile_pending = CLAW_NO_PENDING and returns.

If a subsequent call to rumble_intensity_right_show() sets
drvdata->profile_pending = CLAW_RUMBLE_RIGHT_PENDING, and then the delayed
hardware ACK from the first left request arrives, claw_profile_event() will
misattribute the response.

It will read the left motor's intensity into drvdata->rumble_intensity_right,
corrupting the state:

	case CLAW_RUMBLE_RIGHT_PENDING:
		scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
			drvdata->rumble_intensity_right = cmd_rep->data[4];

Additionally, claw_profile_event() clears
drvdata->profile_pending = CLAW_NO_PENDING locklessly at the end, which
could race with other process contexts accessing it under profile_mutex.

Should claw_profile_event() validate the memory address in the response payload
to ensure it belongs to the pending request, and synchronize accesses to
profile_pending?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260518222935.1802071-1-derekjohn.clark@gmail.com?part=4

^ permalink raw reply

* [PATCH v3] platform/x86/lenovo: Add Yoga Book 9 keyboard dock detection driver
From: Dave Carey @ 2026-05-19  0:02 UTC (permalink / raw)
  To: ilpo.jarvinen
  Cc: pithenrich2d, mpearson-lenovo, derekjohn.clark, W_Armin,
	platform-driver-x86, linux-input, linux-kernel, dmitry.torokhov,
	Dave Carey
In-Reply-To: <20260517150224.50191-1-carvsdriver@gmail.com>

The Lenovo Yoga Book 9 14IAH10 ships with a detachable Bluetooth keyboard
that magnetically attaches to the bottom (secondary) screen in one of two
positions.  The Embedded Controller tracks the attachment state in a 2-bit
field called BKBD and signals changes via WMI event GUID
806BD2A2-177B-481D-BFB5-3BA0BB4A2285 (notify ID 0xEB on the WM10 ACPI
device).

The current BKBD state is read via a separate WMI query GUID
E7F300FA-21CD-4003-ADAC-2696135982E6 (WQAF method), which returns an
8-byte buffer: bytes [0..3] hold the LFID constant 0x00060000 and bytes
[4..7] hold the BKBD value.

BKBD encoding:
  0 = keyboard detached
  1 = keyboard docked on top half of bottom screen
  2 = keyboard docked on bottom half of bottom screen
  3 = reserved (not observed in practice)

Both GUIDs are children of the same ACPI device (WM10), so both are
matched by a single WMI driver.  The query device pointer is stored in a
module-level variable protected by a mutex; the event device uses
wmidev_block_query() via the stored pointer rather than the deprecated
global wmi_query_block().  get_device()/put_device() bracket each use of
the stored pointer so probe/remove races cannot produce a use-after-free.

This driver:
  - Registers as a WMI driver on both the event and query GUIDs.
  - Queries BKBD state synchronously on probe and on each WMI
    notification.
  - Sets the initial SW_TABLET_MODE bit before input_register_device()
    via __set_bit() so userspace always reads the correct state on first
    open.
  - Reports SW_TABLET_MODE=1 when detached, SW_TABLET_MODE=0 when docked
    in either position (a physical keyboard is present in both cases).
  - Exposes the raw BKBD value via a read-only sysfs attribute
    "keyboard_position" for use by userspace (e.g. to distinguish between
    the two docked positions for different UI layouts).  The attribute is
    registered per-device via devm_device_add_groups() in the event-device
    probe path only; the query device has no priv and no sysfs groups.

Tested on: Lenovo Yoga Book 9 14IAH10 (model 83KJ), kernel 6.19.

Signed-off-by: Dave Carey <carvsdriver@gmail.com>
---
 .../testing/sysfs-driver-lenovo-yb9-kbdock    |  21 ++
 MAINTAINERS                                   |   6 +
 drivers/platform/x86/lenovo/Kconfig           |  14 ++
 drivers/platform/x86/lenovo/Makefile          |   1 +
 drivers/platform/x86/lenovo/yb9-kbdock.c      | 270 ++++++++++++++++++
 5 files changed, 312 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-driver-lenovo-yb9-kbdock
 create mode 100644 MAINTAINERS
 create mode 100644 drivers/platform/x86/lenovo/yb9-kbdock.c

diff --git a/Documentation/ABI/testing/sysfs-driver-lenovo-yb9-kbdock b/Documentation/ABI/testing/sysfs-driver-lenovo-yb9-kbdock
new file mode 100644
index 0000000..bb57690
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-lenovo-yb9-kbdock
@@ -0,0 +1,21 @@
+What:		/sys/bus/wmi/drivers/lenovo-yb9-kbdock/<guid>/keyboard_position
+Date:		April 2026
+KernelVersion:	6.10
+Contact:	Dave Carey <carvsdriver@gmail.com>
+Description:
+		Read-only attribute reporting the current keyboard dock position
+		as reported by the Embedded Controller on the Lenovo Yoga Book 9
+		14IAH10.
+
+		Possible values:
+
+		==  ============================================================
+		0   detached  — keyboard is not docked to any screen
+		1   top-half  — keyboard docked on the top half of the bottom screen
+		2   bottom-half — keyboard docked on the bottom half of the bottom screen
+		==  ============================================================
+
+		The value is formatted as "<n> (<name>)\n", e.g. "1 (top-half)\n".
+
+		SW_TABLET_MODE input events are also emitted: 0 when the keyboard
+		is docked (either position), 1 when detached.
diff --git a/MAINTAINERS b/MAINTAINERS
new file mode 100644
index 0000000..cb765b4
--- /dev/null
+++ b/MAINTAINERS
@@ -0,0 +1,6 @@
+LENOVO YOGA BOOK 9 KEYBOARD DOCK DRIVER
+M:	Dave Carey <carvsdriver@gmail.com>
+L:	platform-driver-x86@vger.kernel.org
+S:	Maintained
+F:	Documentation/ABI/testing/sysfs-driver-lenovo-yb9-kbdock
+F:	drivers/platform/x86/lenovo/yb9-kbdock.c
diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
index 9c48487..938b361 100644
--- a/drivers/platform/x86/lenovo/Kconfig
+++ b/drivers/platform/x86/lenovo/Kconfig
@@ -43,6 +43,20 @@ config LENOVO_WMI_CAMERA
 	  To compile this driver as a module, choose M here: the module
 	  will be called lenovo-wmi-camera.

+config LENOVO_YB9_KBDOCK
+	tristate "Lenovo Yoga Book 9 keyboard dock detection"
+	depends on ACPI_WMI
+	depends on DMI
+	depends on INPUT
+	help
+	  Say Y here to enable keyboard dock detection on the Lenovo Yoga Book 9
+	  14IAH10.  The detachable Bluetooth keyboard magnetically attaches to
+	  either screen; this driver reports SW_TABLET_MODE input events based
+	  on the attachment state and exposes the raw position in sysfs.
+
+	  To compile this driver as a module, choose M here: the module will be
+	  called lenovo-yb9-kbdock.
+
 config LENOVO_YMC
 	tristate "Lenovo Yoga Tablet Mode Control"
 	depends on ACPI_WMI
diff --git a/drivers/platform/x86/lenovo/Makefile b/drivers/platform/x86/lenovo/Makefile
index 7b2128e..2842d7d 100644
--- a/drivers/platform/x86/lenovo/Makefile
+++ b/drivers/platform/x86/lenovo/Makefile
@@ -8,6 +8,7 @@ obj-$(CONFIG_THINKPAD_LMI)	+= think-lmi.o
 obj-$(CONFIG_THINKPAD_ACPI)	+= thinkpad_acpi.o

 lenovo-target-$(CONFIG_LENOVO_WMI_HOTKEY_UTILITIES)	+= wmi-hotkey-utilities.o
+lenovo-target-$(CONFIG_LENOVO_YB9_KBDOCK)	+= yb9-kbdock.o
 lenovo-target-$(CONFIG_LENOVO_YMC)	+= ymc.o
 lenovo-target-$(CONFIG_YOGABOOK)	+= yogabook.o
 lenovo-target-$(CONFIG_YT2_1380)	+= yoga-tab2-pro-1380-fastcharger.o
diff --git a/drivers/platform/x86/lenovo/yb9-kbdock.c b/drivers/platform/x86/lenovo/yb9-kbdock.c
new file mode 100644
index 0000000..0000000
--- /dev/null
+++ b/drivers/platform/x86/lenovo/yb9-kbdock.c
@@ -0,0 +1,270 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Lenovo Yoga Book 9 keyboard-dock detection
+ *
+ * Reports SW_TABLET_MODE based on keyboard attachment state and exposes the
+ * raw dock position via sysfs.
+ *
+ * Copyright (C) 2026 Dave Carey <carvsdriver@gmail.com>
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/acpi.h>
+#include <linux/bitfield.h>
+#include <linux/cleanup.h>
+#include <linux/dmi.h>
+#include <linux/input.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/wmi.h>
+
+/*
+ * WM10 ACPI device (_UID "GMZN") exposes two relevant WMI GUIDs:
+ *   YB9_KBDOCK_EVENT_GUID — notify ID 0xEB fires on attachment state change.
+ *   YB9_KBDOCK_QUERY_GUID — object "AF" (WQAF), returns an 8-byte buffer
+ *                           whose upper four bytes hold the BKBD value.
+ *
+ * BKBD encoding:
+ *   0 (BKBD_DETACHED)    — keyboard detached       → SW_TABLET_MODE = 1
+ *   1 (BKBD_TOP_HALF)    — docked, top half        → SW_TABLET_MODE = 0
+ *   2 (BKBD_BOTTOM_HALF) — docked, bottom half     → SW_TABLET_MODE = 0
+ *   3                    — reserved; treated as an error
+ */
+#define YB9_KBDOCK_EVENT_GUID		"806BD2A2-177B-481D-BFB5-3BA0BB4A2285"
+#define YB9_KBDOCK_QUERY_GUID		"E7F300FA-21CD-4003-ADAC-2696135982E6"
+#define YB9_KBDOCK_QUERY_INSTANCE	0
+
+#define BKBD_DETACHED		0
+#define BKBD_TOP_HALF		1
+#define BKBD_BOTTOM_HALF	2
+#define BKBD_MASK		GENMASK(1, 0)
+
+/* Distinguish the two GUIDs via the id_table context field. */
+enum yb9_guid_type { YB9_GUID_EVENT, YB9_GUID_QUERY };
+
+/*
+ * Both GUIDs are children of the same ACPI device (WM10).  Store the query
+ * WMI device globally so the event-device probe and notify path can reach it
+ * via wmidev_block_query().  Protected by yb9_query_lock during probe/remove.
+ */
+static struct wmi_device *yb9_query_wdev;
+static DEFINE_MUTEX(yb9_query_lock);
+
+struct yb9_kbdock_priv {
+	struct input_dev *input_dev;
+	unsigned int bkbd;
+};
+
+/* Returns 0–2 on success, -errno on error. */
+static int yb9_kbdock_query(struct wmi_device *event_wdev,
+			     struct wmi_device *query_wdev)
+{
+	u32 bkbd;
+
+	union acpi_object *obj __free(kfree) =
+		wmidev_block_query(query_wdev, YB9_KBDOCK_QUERY_INSTANCE);
+	if (!obj) {
+		dev_warn(&event_wdev->dev, "WQAF query returned NULL\n");
+		return -EIO;
+	}
+
+	/*
+	 * WQAF returns an 8-byte buffer: bytes [0..3] = LFID (0x00060000),
+	 * bytes [4..7] = BKBD value.  Guard against short buffers.
+	 */
+	if (obj->type == ACPI_TYPE_BUFFER && obj->buffer.length >= 8)
+		memcpy(&bkbd, obj->buffer.pointer + 4, sizeof(bkbd));
+	else if (obj->type == ACPI_TYPE_INTEGER)
+		bkbd = obj->integer.value;
+	else {
+		dev_warn(&event_wdev->dev,
+			 "WQAF: unexpected result type %d len %u\n",
+			 obj->type, obj->type == ACPI_TYPE_BUFFER ? obj->buffer.length : 0);
+		return -EIO;
+	}
+
+	bkbd = FIELD_GET(BKBD_MASK, bkbd);
+	if (bkbd == 3) {
+		dev_warn(&event_wdev->dev, "BKBD value 3 is reserved\n");
+		return -EINVAL;
+	}
+
+	return bkbd;
+}
+
+static void yb9_kbdock_update(struct wmi_device *wdev)
+{
+	struct yb9_kbdock_priv *priv = dev_get_drvdata(&wdev->dev);
+	struct wmi_device *qwdev;
+	int tablet_mode;
+	int bkbd;
+
+	mutex_lock(&yb9_query_lock);
+	qwdev = yb9_query_wdev;
+	if (qwdev)
+		get_device(&qwdev->dev);
+	mutex_unlock(&yb9_query_lock);
+	if (!qwdev)
+		return;
+
+	bkbd = yb9_kbdock_query(wdev, qwdev);
+	put_device(&qwdev->dev);
+	if (bkbd < 0)
+		return;
+
+	priv->bkbd = bkbd;
+	tablet_mode = (bkbd == BKBD_DETACHED) ? 1 : 0;
+
+	input_report_switch(priv->input_dev, SW_TABLET_MODE, tablet_mode);
+	input_sync(priv->input_dev);
+
+	dev_dbg(&wdev->dev, "BKBD=%u tablet_mode=%d\n", bkbd, tablet_mode);
+}
+
+static void yb9_kbdock_notify(struct wmi_device *wdev, union acpi_object *data)
+{
+	yb9_kbdock_update(wdev);
+}
+
+static ssize_t keyboard_position_show(struct device *dev,
+				      struct device_attribute *attr, char *buf)
+{
+	static const char * const names[] = {
+		"detached", "top-half", "bottom-half",
+	};
+	struct yb9_kbdock_priv *priv = dev_get_drvdata(dev);
+	unsigned int bkbd = priv->bkbd;
+
+	if (WARN_ON_ONCE(bkbd >= ARRAY_SIZE(names)))
+		return -EINVAL;
+	return sysfs_emit(buf, "%u (%s)\n", bkbd, names[bkbd]);
+}
+static DEVICE_ATTR_RO(keyboard_position);
+
+static struct attribute *yb9_kbdock_attrs[] = {
+	&dev_attr_keyboard_position.attr,
+	NULL,
+};
+
+static const struct attribute_group yb9_kbdock_group = {
+	.attrs = yb9_kbdock_attrs,
+};
+
+static const struct dmi_system_id yb9_kbdock_dmi_table[] = {
+	{
+		/* Lenovo Yoga Book 9 14IAH10 */
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR,   "LENOVO"),
+			DMI_MATCH(DMI_PRODUCT_NAME, "83KJ"),
+		},
+	},
+	{ }
+};
+
+static int yb9_kbdock_probe(struct wmi_device *wdev, const void *ctx)
+{
+	enum yb9_guid_type type = (enum yb9_guid_type)(uintptr_t)ctx;
+	struct yb9_kbdock_priv *priv;
+	struct input_dev *input_dev;
+	struct wmi_device *qwdev;
+	int bkbd_init;
+	int err;
+
+	if (type == YB9_GUID_QUERY) {
+		if (!dmi_check_system(yb9_kbdock_dmi_table))
+			return -ENODEV;
+		mutex_lock(&yb9_query_lock);
+		yb9_query_wdev = wdev;
+		mutex_unlock(&yb9_query_lock);
+		return 0;
+	}
+
+	if (!dmi_check_system(yb9_kbdock_dmi_table))
+		return -ENODEV;
+
+	mutex_lock(&yb9_query_lock);
+	qwdev = yb9_query_wdev;
+	if (qwdev)
+		get_device(&qwdev->dev);
+	mutex_unlock(&yb9_query_lock);
+	if (!qwdev)
+		return -EPROBE_DEFER;
+
+	priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv) {
+		put_device(&qwdev->dev);
+		return -ENOMEM;
+	}
+
+	input_dev = devm_input_allocate_device(&wdev->dev);
+	if (!input_dev) {
+		put_device(&qwdev->dev);
+		return -ENOMEM;
+	}
+
+	input_dev->name = "Lenovo Yoga Book 9 keyboard dock switch";
+	input_dev->phys = YB9_KBDOCK_EVENT_GUID "/input0";
+	input_dev->id.bustype = BUS_HOST;
+	input_dev->dev.parent = &wdev->dev;
+	input_set_capability(input_dev, EV_SW, SW_TABLET_MODE);
+
+	priv->input_dev = input_dev;
+	dev_set_drvdata(&wdev->dev, priv);
+
+	/*
+	 * Query the initial dock state and preset the switch bit before
+	 * input_register_device() so userspace never sees SW_TABLET_MODE = 0
+	 * for a detached keyboard on first open.
+	 */
+	bkbd_init = yb9_kbdock_query(wdev, qwdev);
+	put_device(&qwdev->dev);
+	if (bkbd_init >= 0) {
+		priv->bkbd = bkbd_init;
+		if (bkbd_init == BKBD_DETACHED)
+			__set_bit(SW_TABLET_MODE, input_dev->sw);
+	}
+
+	err = input_register_device(input_dev);
+	if (err) {
+		dev_err(&wdev->dev, "failed to register input device: %d\n", err);
+		return err;
+	}
+
+	err = devm_device_add_group(&wdev->dev, &yb9_kbdock_group);
+	if (err) {
+		dev_err(&wdev->dev, "failed to add sysfs group: %d\n", err);
+		return err;
+	}
+
+	return 0;
+}
+
+static void yb9_kbdock_remove(struct wmi_device *wdev)
+{
+	mutex_lock(&yb9_query_lock);
+	if (wdev == yb9_query_wdev)
+		yb9_query_wdev = NULL;
+	mutex_unlock(&yb9_query_lock);
+}
+
+static const struct wmi_device_id yb9_kbdock_wmi_id_table[] = {
+	{ .guid_string = YB9_KBDOCK_EVENT_GUID, .context = (void *)YB9_GUID_EVENT },
+	{ .guid_string = YB9_KBDOCK_QUERY_GUID, .context = (void *)YB9_GUID_QUERY },
+	{ }
+};
+MODULE_DEVICE_TABLE(wmi, yb9_kbdock_wmi_id_table);
+
+static struct wmi_driver yb9_kbdock_driver = {
+	.driver = {
+		.name = "lenovo-yb9-kbdock",
+	},
+	.id_table = yb9_kbdock_wmi_id_table,
+	.probe    = yb9_kbdock_probe,
+	.remove   = yb9_kbdock_remove,
+	.notify   = yb9_kbdock_notify,
+};
+module_wmi_driver(yb9_kbdock_driver);
+
+MODULE_AUTHOR("Dave Carey <carvsdriver@gmail.com>");
+MODULE_DESCRIPTION("Lenovo Yoga Book 9 keyboard dock detection");
+MODULE_LICENSE("GPL");
--
2.53.0


^ permalink raw reply related

* Re: [PATCH v6 3/4] HID: hid-msi: Add RGB control interface
From: sashiko-bot @ 2026-05-18 23:56 UTC (permalink / raw)
  To: Derek J. Clark; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260518222935.1802071-4-derekjohn.clark@gmail.com>

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Critical] Use-After-Free and NULL pointer dereference risks during RGB interface teardown.
- [High] Use-After-Free due to incorrect workqueue cancellation order.
- [Medium] Lifecycle mismatch of sysfs groups causes kernfs warnings on device removal.
- [Medium] Gamepad initialization is fully disabled if the RGB configuration read fails.
--

commit 1d7a0aec4b983c2c64bceb50fe253ae0e8e94c94
Author: Derek J. Clark <derekjohn.clark@gmail.com>
HID: hid-msi: Add RGB control interface

This commit adds an RGB control interface for MSI Claw devices, parsing
profile and effect data to mirror the RGB effects available in Windows.

> diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
> index e58d35dba5b40..4d267b896da72 100644
> --- a/drivers/hid/hid-msi.c
> +++ b/drivers/hid/hid-msi.c

[ ... ]

> @@ -846,16 +1372,44 @@ static void cfg_setup_fn(struct work_struct *work)
>  		return;
>  	}
>  
> -	/* Add sysfs attributes after we get the device state */
> -	ret = devm_device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
> +	ret = claw_read_rgb_config(drvdata->hdev);
>  	if (ret) {
>  		dev_err(&drvdata->hdev->dev,
> -			"Failed to setup device, can't create gamepad attrs: %d\n", ret);
> +			"Failed to setup device, can't read RGB config: %d\n", ret);
>  		return;
>  	}

If claw_read_rgb_config() fails, it returns early and skips registering the 
gamepad attributes. Should core gamepad functionality (like button mapping)
break if the cosmetic RGB feature fails to initialize?

> -	drvdata->gamepad_registered = true;
> +
> +	/* Add sysfs attributes after we get the device state */
> +	if (!drvdata->gamepad_registered) {
> +		ret = devm_device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
> +		if (ret) {
> +			dev_err(&drvdata->hdev->dev,
> +				"Failed to setup device, can't create gamepad attrs: %d\n", ret);
> +			return;
> +		}
> +		drvdata->gamepad_registered = true;
> +	}
> +
> +	/* Add and enable RGB interface once we have the device state */
> +	if (!drvdata->rgb_registered) {
> +		ret = devm_led_classdev_multicolor_register(&drvdata->hdev->dev, &drvdata->led_mc);
> +		if (ret) {
> +			dev_err(&drvdata->hdev->dev,
> +				"Failed to setup device, can't create led device: %d\n", ret);
> +			return;
> +		}
> +
> +		ret = devm_device_add_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group);

Will this trigger a kernfs warning on device removal? 

The LED class device is unregistered via devres first, which calls 
device_del() and implicitly removes all sysfs entries. This makes the 
subsequent devres removal of claw_rgb_attr_group fail to find the directory, 
triggering a "can not remove '...', no directory" warning.

Would it be safer to assign this group to drvdata->led_mc.led_cdev.groups
before registration instead of using devm_device_add_group()?

> +		if (ret) {
> +			dev_err(&drvdata->hdev->dev,
> +				"Failed to setup device, can't create led attributes: %d\n", ret);
> +			return;
> +		}
> +		drvdata->rgb_registered = true;
> +	}
>  
>  	kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE);
> +	kobject_uevent(&drvdata->led_mc.led_cdev.dev->kobj, KOBJ_CHANGE);
>  }

[ ... ]

> @@ -979,10 +1553,14 @@ static void claw_remove(struct hid_device *hdev)
>  		return;
>  	}
>  
> +	/* Block writes to brightness/multi_intensity during teardown */
> +	drvdata->led_mc.led_cdev.brightness_set = NULL;

Can this race with __led_set_brightness() in the LED subsystem? If the LED 
core checks the brightness_set pointer and finds it valid, but gets preempted
while this runs without locking, could it then jump to a NULL pointer?

>  	cancel_delayed_work_sync(&drvdata->cfg_setup);
>  	cancel_delayed_work_sync(&drvdata->cfg_resume);

Since cfg_resume_fn() schedules cfg_setup, is it possible for cfg_setup to 
be re-armed here? 

If cfg_resume is currently running, cancelling cfg_setup first does nothing, 
and then cfg_resume will queue cfg_setup again before completing. This could 
leave cfg_setup pending after the driver data is freed.

> +	cancel_delayed_work_sync(&drvdata->rgb_queue);
>  
>  	drvdata->gamepad_registered = false;
> +	drvdata->rgb_enabled = false;
>  
>  	hid_hw_close(hdev);
>  }

Because drvdata->rgb_registered remains true here, and the RGB sysfs 
attributes are devres-managed (meaning they persist until after claw_remove
returns), could a concurrent userspace write pass the rgb_registered check 
and call mod_delayed_work()? 

This would re-arm rgb_queue after it was cancelled, leading to a 
use-after-free when drvdata is eventually freed. Should 
drvdata->rgb_registered = false be set before cancelling the workqueue?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260518222935.1802071-1-derekjohn.clark@gmail.com?part=3

^ permalink raw reply

* Re: [PATCH v6 2/4] HID: hid-msi: Add M-key mapping attributes
From: sashiko-bot @ 2026-05-18 23:29 UTC (permalink / raw)
  To: Derek J. Clark; +Cc: linux-input, dmitry.torokhov
In-Reply-To: <20260518222935.1802071-3-derekjohn.clark@gmail.com>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Data race and state desynchronization in `profile_pending` allows delayed ACKs to corrupt profile data.
- [High] Race window in `claw_hw_output_report()` causes spurious command timeouts.
- [High] Use-After-Free during module teardown due to incorrect delayed work cancellation order.
--

commit d45c51fc226fdd946c2d70a93c254be199300dc7
Author: Derek J. Clark <derekjohn.clark@gmail.com>
Subject: HID: hid-msi: Add M-key mapping attributes

This commit introduces M-key mapping attributes for the MSI Claw device.
It adds the ability to read and write profile mappings for two back buttons
(M1 and M2) via sysfs, using custom HID commands to communicate with the
hardware.

> diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
> index c79eb0bfeb776..e58d35dba5b40 100644
> --- a/drivers/hid/hid-msi.c
> +++ b/drivers/hid/hid-msi.c

[ ... ]

>  struct claw_drvdata {
>  	/* MCU General Variables */
> +	enum claw_profile_ack_pending profile_pending;
>  	struct completion send_cmd_complete;
>  	struct delayed_work cfg_resume;
>  	struct delayed_work cfg_setup;

This problem wasn't introduced by this patch, but does the cancellation order
of these delayed works cause a use-after-free regression during module
teardown?

In cfg_resume_fn(), cfg_setup is unconditionally queued via
schedule_delayed_work(). During module removal, if
cancel_delayed_work_sync(&drvdata->cfg_setup) is executed before
cancel_delayed_work_sync(&drvdata->cfg_resume), and cfg_resume_fn is
actively executing:

The first cancellation would return immediately because cfg_setup is not
yet pending. cfg_resume_fn would then queue cfg_setup. The second
cancellation would wait for cfg_resume_fn to finish.

Once the removal completes and drvdata is freed by devres, could the
newly queued cfg_setup fire 500ms later and dereference the freed
drvdata pointer?

[ ... ]

> +static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep)
> +{
> +	u8 *codes;
> +	int i;
> +
> +	switch (drvdata->profile_pending) {
> +	case CLAW_M1_PENDING:
> +	case CLAW_M2_PENDING:
> +		codes = (drvdata->profile_pending == CLAW_M1_PENDING) ?
> +			drvdata->m1_codes : drvdata->m2_codes;
> +		for (i = 0; i < CLAW_KEYS_MAX; i++)
> +			codes[i] = (cmd_rep->data[6 + i]);
> +		break;
> +	default:
> +		dev_dbg(&drvdata->hdev->dev,
> +			"Got profile event without changes pending from command: %x\n",
> +			cmd_rep->cmd);
> +		return -EINVAL;
> +	}
> +	drvdata->profile_pending = CLAW_NO_PENDING;
> +
> +	return 0;
> +}

Is there a data race and potential state desynchronization with
profile_pending here?

drvdata->profile_pending is written under profile_mutex in process context
in claw_buttons_show(), but it is read and cleared locklessly here in the
atomic HID event handler.

If a request (like an M1 read) times out, profile_pending is cleared by the
sysfs thread. If a subsequent request then sets profile_pending to
CLAW_M2_PENDING, could a concurrently arriving delayed ACK for the original
M1 read cause this lockless handler to mistakenly parse the M1 payload into
drvdata->m2_codes?

This would reset profile_pending and prematurely wake the waiting thread,
corrupting the profile data.

[ ... ]

> +static int claw_buttons_store(struct device *dev, const char *buf, u8 mkey_idx)
> +{

[ ... ]

> +set_buttons:
> +	scoped_guard(mutex, &drvdata->rom_mutex) {
> +		ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
> +					    data, len, 25);

This problem wasn't introduced by this patch, but is there a race window in
claw_hw_output_report() that could cause spurious command timeouts?

Looking at claw_hw_output_report(), drvdata->waiting_for_ack is set to
true inside a cmd_lock critical section. Immediately after releasing the
spinlock, it calls reinit_completion(&drvdata->send_cmd_complete).

If the hardware responds exceptionally quickly, or if a delayed ACK from a
previous timed-out command arrives in the small window between the spinlock
release and the reinit_completion() call:

Could the interrupt handler clear waiting_for_ack and call complete(),
only for the main thread to immediately erase the completion event with
reinit_completion()?

This would cause wait_for_completion_interruptible_timeout() to block and
incorrectly return -ETIMEDOUT to userspace.

Should reinit_completion() be called before setting waiting_for_ack?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260518222935.1802071-1-derekjohn.clark@gmail.com?part=2

^ permalink raw reply

* Re: [PATCH v6 1/4] HID: hid-msi: Add MSI Claw configuration driver
From: sashiko-bot @ 2026-05-18 22:57 UTC (permalink / raw)
  To: Derek J. Clark; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260518222935.1802071-2-derekjohn.clark@gmail.com>

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Inverted registration check in `gamepad_mode_show` breaks functionality and allows access after device removal.
- [High] TOCTOU race in sysfs attributes leading to Use-After-Free during device teardown.
- [High] Race condition with `reinit_completion` and unprotected state on error paths in `claw_hw_output_report`.
- [High] Incorrect cancellation order of interdependent delayed works causes a Use-After-Free.
- [High] Brittle endpoint check in `msi_remove` bypasses teardown and causes unbalanced `hid_hw_close`.
--

commit bae529985c447afb063f1f68fb0ebbfec812a32f
Author: Derek J. Clark <derekjohn.clark@gmail.com>
Subject: HID: hid-msi: Add MSI Claw configuration driver

This commit adds a configuration HID driver for the MSI Claw series of
handheld PC's. It provides an initial driver outline and attributes for
changing the gamepad mode, M-key behavior, and handling resets.

> diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
> new file mode 100644
> index 0000000000000..c79eb0bfeb776
> --- /dev/null
> +++ b/drivers/hid/hid-msi.c

[ ... ]

> +static int claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data,
> +				 size_t len, unsigned int timeout)
> +{
> +	unsigned char *dmabuf __free(kfree) = NULL;
> +	u8 header[] = { CLAW_OUTPUT_REPORT_ID, 0, 0, 0x3c, index };
> +	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
> +	size_t header_size = ARRAY_SIZE(header);
> +	int ret;
> +
> +	if (header_size + len > CLAW_PACKET_SIZE)
> +		return -EINVAL;
> +
> +	/* We can't use a devm_alloc reusable buffer without side effects during suspend */
> +	dmabuf = kzalloc(CLAW_PACKET_SIZE, GFP_KERNEL);
> +	if (!dmabuf)
> +		return -ENOMEM;
> +
> +	memcpy(dmabuf, header, header_size);
> +	if (data && len)
> +		memcpy(dmabuf + header_size, data, len);
> +
> +	guard(mutex)(&drvdata->cfg_mutex);
> +	if (timeout) {
> +		scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
> +			drvdata->waiting_cmd = index;
> +			drvdata->waiting_for_ack = true;
> +			drvdata->cmd_status = -ETIMEDOUT;
> +		}
> +		reinit_completion(&drvdata->send_cmd_complete);

Could this race with an incoming ACK? Since reinit_completion() is called
outside cmd_lock after setting waiting_for_ack = true, an ACK arriving in
this window could call complete(), which would then be overwritten, causing
a command timeout.

> +	}
> +
> +	dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
> +		CLAW_PACKET_SIZE, dmabuf);
> +
> +	ret = hid_hw_output_report(hdev, dmabuf, CLAW_PACKET_SIZE);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = ret == CLAW_PACKET_SIZE ? 0 : -EIO;
> +	if (ret)
> +		return ret;

If hid_hw_output_report() fails or returns a partial write, the function
returns early without resetting waiting_for_ack to false. Could this corrupt
the state machine and cause stray ACKs to trigger spurious completions later?

[ ... ]

> +static ssize_t gamepad_mode_show(struct device *dev,
> +				 struct device_attribute *attr, char *buf)
> +{
> +	struct hid_device *hdev = to_hid_device(dev);
> +	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
> +	int ret, i;
> +
> +	if (drvdata->gamepad_registered)
> +		return -ENODEV;

Should this be if (!drvdata->gamepad_registered)? Without the logical NOT,
this prevents reading the mode during normal operation and allows access
during teardown, which could lead to a use-after-free since the sysfs
attribute can be accessed after device removal.

[ ... ]

> +static ssize_t mkeys_function_store(struct device *dev, struct device_attribute *attr,
> +				    const char *buf, size_t count)
> +{
> +	struct hid_device *hdev = to_hid_device(dev);
> +	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
> +	int i, ret = -EINVAL;
> +	u8 data[2];
> +
> +	if (!drvdata->gamepad_registered)
> +		return -ENODEV;

Is there a race condition here during device teardown? The check for
gamepad_registered happens locklessly before cfg_mutex is acquired in
claw_hw_output_report().

If claw_remove() executes concurrently, sets gamepad_registered to false,
and closes the hardware, could this result in calling hid_hw_output_report()
on a stopped device and causing a use-after-free when the sysfs thread
resumes?

[ ... ]

> +static void claw_remove(struct hid_device *hdev)
> +{
> +	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
> +
> +	if (!drvdata) {
> +		hid_hw_close(hdev);
> +		return;
> +	}
> +
> +	cancel_delayed_work_sync(&drvdata->cfg_setup);
> +	cancel_delayed_work_sync(&drvdata->cfg_resume);

Should cfg_resume be cancelled before cfg_setup? Since cfg_resume_fn() can
schedule cfg_setup, cancelling cfg_setup first might allow cfg_resume_fn()
to queue it again. If this happens, the newly scheduled cfg_setup work could
access freed drvdata memory after claw_remove() finishes.

> +
> +	drvdata->gamepad_registered = false;
> +
> +	hid_hw_close(hdev);
> +}
> +
> +static void msi_remove(struct hid_device *hdev)
> +{
> +	int ret;
> +	u8 ep;
> +
> +	ret = get_endpoint_address(hdev);
> +	if (ret <= 0)
> +		goto hw_stop;
> +
> +	ep = ret;
> +	if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
> +		claw_remove(hdev);

Is it safe to rely on get_endpoint_address() during removal?

If userspace modifies the USB alternate setting via usbfs while the driver
is bound, the endpoint could change, causing claw_remove() to be skipped
and leaving the workqueues running, leading to a use-after-free.

Conversely, if the endpoint didn't match during probe but does match during
removal, claw_remove() might be called with a NULL drvdata and execute
hid_hw_close() without a prior hid_hw_open(), underflowing ll_open_count.

[ ... ]

> +static int claw_suspend(struct hid_device *hdev)
> +{
> +	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
> +
> +	if (!drvdata)
> +		return -ENODEV;
> +
> +	cancel_delayed_work_sync(&drvdata->cfg_setup);
> +	cancel_delayed_work_sync(&drvdata->cfg_resume);

Does this have the same cancellation order issue as claw_remove(), where
cfg_resume_fn() could reschedule cfg_setup after it has been cancelled?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260518222935.1802071-1-derekjohn.clark@gmail.com?part=1

^ permalink raw reply

* [PATCH v6 4/4] HID: hid-msi: Add Rumble Intensity Attributes
From: Derek J. Clark @ 2026-05-18 22:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
	Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260518222935.1802071-1-derekjohn.clark@gmail.com>

Adds intensity adjustment for the left and right rumble motors.

Claude was used during the reverse-engineering data gathering for this
feature done by Zhouwang Huang. As the code had already been affected,
I used Claude to create the initial framing for the feature, then did
manual cleanup of the _show and _store functions afterwards to fix bugs
and keep the coding style consistent. Claude was also used as an initial
reviewer of this patch.

Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz
    config.
  - Add spinlock_irqsave for read/write access on rumble_intensity
    variables.
  - Gate all attribute show/store functions with gamepad_registered.
v5:
  - Remove mkey related changes.
v2:
  - Use pending_profile and sync to rom mutexes.
---
 drivers/hid/hid-msi.c | 167 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 167 insertions(+)

diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
index 4d267b896da72..339f256b60dbe 100644
--- a/drivers/hid/hid-msi.c
+++ b/drivers/hid/hid-msi.c
@@ -78,6 +78,8 @@ enum claw_profile_ack_pending {
 	CLAW_M1_PENDING,
 	CLAW_M2_PENDING,
 	CLAW_RGB_PENDING,
+	CLAW_RUMBLE_LEFT_PENDING,
+	CLAW_RUMBLE_RIGHT_PENDING,
 };
 
 enum claw_key_index {
@@ -265,6 +267,11 @@ static const u16 button_mapping_addr_new[] = {
 static const u16 rgb_addr_old = 0x01fa;
 static const u16 rgb_addr_new = 0x024a;
 
+static const u16 rumble_addr[] = {
+	0x0022,  /* left  */
+	0x0023,  /* right */
+};
+
 struct claw_command_report {
 	u8 report_id;
 	u8 padding[2];
@@ -317,9 +324,13 @@ struct claw_drvdata {
 	enum claw_gamepad_mode_index gamepad_mode;
 	u8 m1_codes[CLAW_KEYS_MAX];
 	u8 m2_codes[CLAW_KEYS_MAX];
+	u8 rumble_intensity_right;
+	u8 rumble_intensity_left;
 	bool gamepad_registered;
+	spinlock_t rumble_lock; /* lock for rumble_intensity read/write */
 	spinlock_t mode_lock; /* Lock for mode data read/write */
 	const u16 *bmap_addr;
+	bool rumble_support;
 	bool bmap_support;
 
 	/* RGB Variables */
@@ -409,6 +420,14 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_
 		}
 
 		break;
+	case CLAW_RUMBLE_LEFT_PENDING:
+		scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+			drvdata->rumble_intensity_left = cmd_rep->data[4];
+		break;
+	case CLAW_RUMBLE_RIGHT_PENDING:
+		scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+			drvdata->rumble_intensity_right = cmd_rep->data[4];
+		break;
 	default:
 		dev_dbg(&drvdata->hdev->dev,
 			"Got profile event without changes pending from command: %x\n",
@@ -882,6 +901,142 @@ static ssize_t button_mapping_options_show(struct device *dev,
 }
 static DEVICE_ATTR_RO(button_mapping_options);
 
+static ssize_t rumble_intensity_left_store(struct device *dev,
+					   struct device_attribute *attr,
+					   const char *buf, size_t count)
+{
+	u8 data[] = { 0x01, (rumble_addr[0] >> 8) & 0xff, rumble_addr[0] & 0xff, 0x01, 0x00 };
+	struct hid_device *hdev = to_hid_device(dev);
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	u8 val;
+	int ret;
+
+	if (!drvdata->gamepad_registered)
+		return -ENODEV;
+
+	ret = kstrtou8(buf, 10, &val);
+	if (ret)
+		return ret;
+
+	if (val > 100)
+		return -EINVAL;
+
+	data[4] = val;
+
+	guard(mutex)(&drvdata->rom_mutex);
+	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+				    data, ARRAY_SIZE(data), 25);
+	if (ret)
+		return ret;
+
+	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 25);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t rumble_intensity_left_show(struct device *dev,
+					  struct device_attribute *attr,
+					  char *buf)
+{
+	u8 data[4] = { 0x01, (rumble_addr[0] >> 8) & 0xff, rumble_addr[0] & 0xff, 0x01 };
+	struct hid_device *hdev = to_hid_device(dev);
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	int ret;
+	u8 val;
+
+	if (!drvdata->gamepad_registered)
+		return -ENODEV;
+
+	guard(mutex)(&drvdata->profile_mutex);
+	drvdata->profile_pending = CLAW_RUMBLE_LEFT_PENDING;
+	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data,
+				    ARRAY_SIZE(data), 25);
+	if (ret) {
+		drvdata->profile_pending = CLAW_NO_PENDING;
+		return ret;
+	}
+
+	scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+		val = drvdata->rumble_intensity_left;
+
+	return sysfs_emit(buf, "%u\n", val);
+}
+static DEVICE_ATTR_RW(rumble_intensity_left);
+
+static ssize_t rumble_intensity_right_store(struct device *dev,
+					    struct device_attribute *attr,
+					    const char *buf, size_t count)
+{
+	u8 data[] = { 0x01, (rumble_addr[1] >> 8) & 0xff, rumble_addr[1] & 0xff, 0x01, 0x00 };
+	struct hid_device *hdev = to_hid_device(dev);
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	u8 val;
+	int ret;
+
+	if (!drvdata->gamepad_registered)
+		return -ENODEV;
+
+	ret = kstrtou8(buf, 10, &val);
+	if (ret)
+		return ret;
+
+	if (val > 100)
+		return -EINVAL;
+
+	data[4] = val;
+
+	guard(mutex)(&drvdata->rom_mutex);
+	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+				    data, ARRAY_SIZE(data), 25);
+	if (ret)
+		return ret;
+
+	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 25);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t rumble_intensity_right_show(struct device *dev,
+					   struct device_attribute *attr,
+					   char *buf)
+{
+	u8 data[4] = { 0x01, (rumble_addr[1] >> 8) & 0xff, rumble_addr[1] & 0xff, 0x01 };
+	struct hid_device *hdev = to_hid_device(dev);
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	int ret;
+	u8 val;
+
+	if (!drvdata->gamepad_registered)
+		return -ENODEV;
+
+	guard(mutex)(&drvdata->profile_mutex);
+	drvdata->profile_pending = CLAW_RUMBLE_RIGHT_PENDING;
+	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data,
+				    ARRAY_SIZE(data), 25);
+	if (ret) {
+		drvdata->profile_pending = CLAW_NO_PENDING;
+		return ret;
+	}
+
+	scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+		val = drvdata->rumble_intensity_right;
+
+	return sysfs_emit(buf, "%u\n", val);
+}
+static DEVICE_ATTR_RW(rumble_intensity_right);
+
+static ssize_t rumble_intensity_range_show(struct device *dev,
+					   struct device_attribute *attr,
+					   char *buf)
+{
+	return sysfs_emit(buf, "0-100\n");
+}
+static DEVICE_ATTR_RO(rumble_intensity_range);
+
 static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
 					    int n)
 {
@@ -902,6 +1057,12 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu
 	    attr == &dev_attr_reset.attr)
 		return attr->mode;
 
+	/* Hide rumble attrs if not supported */
+	if (attr == &dev_attr_rumble_intensity_left.attr ||
+	    attr == &dev_attr_rumble_intensity_right.attr ||
+	    attr == &dev_attr_rumble_intensity_range.attr)
+		return drvdata->rumble_support ? attr->mode : 0;
+
 	/* Hide button mapping attrs if it isn't supported */
 	return drvdata->bmap_support ? attr->mode : 0;
 }
@@ -915,6 +1076,9 @@ static struct attribute *claw_gamepad_attrs[] = {
 	&dev_attr_mkeys_function.attr,
 	&dev_attr_mkeys_function_index.attr,
 	&dev_attr_reset.attr,
+	&dev_attr_rumble_intensity_left.attr,
+	&dev_attr_rumble_intensity_right.attr,
+	&dev_attr_rumble_intensity_range.attr,
 	NULL,
 };
 
@@ -1430,6 +1594,7 @@ static void claw_features_supported(struct claw_drvdata *drvdata)
 		drvdata->bmap_support = true;
 		if (minor >= 0x66) {
 			drvdata->bmap_addr = button_mapping_addr_new;
+			drvdata->rumble_support = true;
 			drvdata->rgb_addr = rgb_addr_new;
 		} else {
 			drvdata->bmap_addr = button_mapping_addr_old;
@@ -1441,6 +1606,7 @@ static void claw_features_supported(struct claw_drvdata *drvdata)
 	if ((major == 0x02 && minor >= 0x17) || major >= 0x03) {
 		drvdata->bmap_support = true;
 		drvdata->bmap_addr = button_mapping_addr_new;
+		drvdata->rumble_support = true;
 		drvdata->rgb_addr = rgb_addr_new;
 		return;
 	}
@@ -1488,6 +1654,7 @@ static int claw_probe(struct hid_device *hdev, u8 ep)
 	spin_lock_init(&drvdata->cmd_lock);
 	spin_lock_init(&drvdata->mode_lock);
 	spin_lock_init(&drvdata->frame_lock);
+	spin_lock_init(&drvdata->rumble_lock);
 	init_completion(&drvdata->send_cmd_complete);
 	INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
 	INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
-- 
2.53.0


^ permalink raw reply related

* [PATCH v6 3/4] HID: hid-msi: Add RGB control interface
From: Derek J. Clark @ 2026-05-18 22:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
	Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260518222935.1802071-1-derekjohn.clark@gmail.com>

Adds RGB control interface for MSI Claw devices. The MSI Claw uses a
fairly unique RGB interface. It has 9 total zones (4 per joystick ring
and 1 for the ABXY buttons), and supports up to 8 sequential frames of
RGB zone data. Each frame is written to a specific area of MCU memory by
the profile command, the value of which changes based on the firmware of
the device. Unlike other devices (such as the Legion Go or the OneXPlayer
devices), there are no hard coded effects built into the MCU. Instead,
the basic effects are provided as a series of frame data. I have
mirrored the effects available in Windows in this driver, while keeping
the effect names consistent with the Lenovo drivers for the effects that
are similar.

Initial reverse-engineering and implementation of this feature was done
by Zhouwang Huang. I refactored the overall format to conform to kernel
driver best practices and style guides. Claude was used as an initial
reviewer of this patch.

Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz
    config.
  - Gate all attribute show/store functions with rgb_registered,
    enabling use of devm_device_add_group.
v5:
  - Move adding the RGB device into cfg_setup to prevent led core
    attributes from being written to prior to setup completing.
  - Ensure frame_lock is properly init.
  - Change variable names in RGB functions from frame and zone to f and
    z respectively to fit all scoped_guard actions in 100 columns.
v4:
  - Fix frame_calc validity check to use >=.
  - USe spinlock instead of mutex in raw_event and related attribute
    _store function.
  - Ensure delayed work is canceled in suspend & canceled before sysfs
    attribute removal.
v3:
  - Add mutex for read/write of rgb frame data.
  - Remove setting rgb_frame_count when reading rgb profiles as it always
    returns garbage data.
  - Ensure rgb_speed is getting drvdata from a valid lookup (not hdev).
v2:
  - Use pending_profile mutex
---
 drivers/hid/hid-msi.c | 596 +++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 587 insertions(+), 9 deletions(-)

diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
index e58d35dba5b40..4d267b896da72 100644
--- a/drivers/hid/hid-msi.c
+++ b/drivers/hid/hid-msi.c
@@ -21,6 +21,7 @@
 #include <linux/device.h>
 #include <linux/hid.h>
 #include <linux/kobject.h>
+#include <linux/led-class-multicolor.h>
 #include <linux/leds.h>
 #include <linux/module.h>
 #include <linux/mutex.h>
@@ -44,6 +45,10 @@
 
 #define CLAW_KEYS_MAX		5
 
+#define CLAW_RGB_ZONES		9
+#define CLAW_RGB_MAX_FRAMES	8
+#define CLAW_RGB_FRAME_OFFSET	0x24
+
 enum claw_command_index {
 	CLAW_COMMAND_TYPE_READ_PROFILE =		0x04,
 	CLAW_COMMAND_TYPE_READ_PROFILE_ACK =		0x05,
@@ -72,6 +77,7 @@ enum claw_profile_ack_pending {
 	CLAW_NO_PENDING,
 	CLAW_M1_PENDING,
 	CLAW_M2_PENDING,
+	CLAW_RGB_PENDING,
 };
 
 enum claw_key_index {
@@ -230,6 +236,22 @@ static const struct {
 	{ 0xff, "DISABLED" },
 };
 
+enum claw_rgb_effect_index {
+	CLAW_RGB_EFFECT_MONOCOLOR,
+	CLAW_RGB_EFFECT_BREATHE,
+	CLAW_RGB_EFFECT_CHROMA,
+	CLAW_RGB_EFFECT_RAINBOW,
+	CLAW_RGB_EFFECT_FROSTFIRE,
+};
+
+static const char * const claw_rgb_effect_text[] = {
+	[CLAW_RGB_EFFECT_MONOCOLOR] =	"monocolor",
+	[CLAW_RGB_EFFECT_BREATHE] =	"breathe",
+	[CLAW_RGB_EFFECT_CHROMA] =	"chroma",
+	[CLAW_RGB_EFFECT_RAINBOW] =	"rainbow",
+	[CLAW_RGB_EFFECT_FROSTFIRE] =	"frostfire",
+};
+
 static const u16 button_mapping_addr_old[] = {
 	0x007a,  /* M1 */
 	0x011f,  /* M2 */
@@ -240,6 +262,9 @@ static const u16 button_mapping_addr_new[] = {
 	0x0164,  /* M2 */
 };
 
+static const u16 rgb_addr_old = 0x01fa;
+static const u16 rgb_addr_new = 0x024a;
+
 struct claw_command_report {
 	u8 report_id;
 	u8 padding[2];
@@ -248,6 +273,28 @@ struct claw_command_report {
 	u8 data[59];
 } __packed;
 
+struct rgb_zone {
+	u8 red;
+	u8 green;
+	u8 blue;
+};
+
+struct rgb_frame {
+	struct rgb_zone zone[CLAW_RGB_ZONES];
+};
+
+struct rgb_report {
+	u8 profile;
+	__be16 read_addr;
+	u8 frame_bytes;
+	u8 padding;
+	u8 frame_count;
+	u8 state; /* Always 0x09 */
+	u8 speed;
+	u8 brightness;
+	struct rgb_frame zone_data;
+} __packed;
+
 struct claw_drvdata {
 	/* MCU General Variables */
 	enum claw_profile_ack_pending profile_pending;
@@ -274,6 +321,18 @@ struct claw_drvdata {
 	spinlock_t mode_lock; /* Lock for mode data read/write */
 	const u16 *bmap_addr;
 	bool bmap_support;
+
+	/* RGB Variables */
+	struct rgb_frame rgb_frames[CLAW_RGB_MAX_FRAMES];
+	enum claw_rgb_effect_index rgb_effect;
+	struct led_classdev_mc led_mc;
+	struct delayed_work rgb_queue;
+	spinlock_t frame_lock; /* lock for rgb_frames read/write */
+	bool rgb_registered;
+	u8 rgb_frame_count;
+	bool rgb_enabled;
+	u8 rgb_speed;
+	u16 rgb_addr;
 };
 
 static int get_endpoint_address(struct hid_device *hdev)
@@ -307,8 +366,11 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
 
 static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep)
 {
-	u8 *codes;
-	int i;
+	struct rgb_report *frame;
+	u16 rgb_addr, read_addr;
+	u8 *codes, f_idx;
+	u16 frame_calc;
+	int i, ret = 0;
 
 	switch (drvdata->profile_pending) {
 	case CLAW_M1_PENDING:
@@ -318,6 +380,35 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_
 		for (i = 0; i < CLAW_KEYS_MAX; i++)
 			codes[i] = (cmd_rep->data[6 + i]);
 		break;
+	case CLAW_RGB_PENDING:
+		frame = (struct rgb_report *)cmd_rep->data;
+		rgb_addr = drvdata->rgb_addr;
+		read_addr = be16_to_cpu(frame->read_addr);
+		frame_calc = (read_addr - rgb_addr) / CLAW_RGB_FRAME_OFFSET;
+		if (frame_calc >= CLAW_RGB_MAX_FRAMES) {
+			dev_err(&drvdata->hdev->dev, "Got unsupported frame index: %x\n",
+				frame_calc);
+			drvdata->profile_pending = CLAW_NO_PENDING;
+			return -EINVAL;
+		}
+		f_idx = frame_calc;
+
+		scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+			memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data,
+			       sizeof(struct rgb_frame));
+
+			/* Only use frame 0 for remaining variable assignment */
+			if (f_idx != 0)
+				break;
+
+			drvdata->rgb_speed = frame->speed;
+			drvdata->led_mc.led_cdev.brightness = frame->brightness;
+			drvdata->led_mc.subled_info[0].intensity = frame->zone_data.zone[0].red;
+			drvdata->led_mc.subled_info[1].intensity = frame->zone_data.zone[0].green;
+			drvdata->led_mc.subled_info[2].intensity = frame->zone_data.zone[0].blue;
+		}
+
+		break;
 	default:
 		dev_dbg(&drvdata->hdev->dev,
 			"Got profile event without changes pending from command: %x\n",
@@ -326,7 +417,7 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_
 	}
 	drvdata->profile_pending = CLAW_NO_PENDING;
 
-	return 0;
+	return ret;
 }
 
 static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report,
@@ -832,6 +923,441 @@ static const struct attribute_group claw_gamepad_attr_group = {
 	.is_visible = claw_gamepad_attr_is_visible,
 };
 
+/* Read RGB config from device */
+static int claw_read_rgb_config(struct hid_device *hdev)
+{
+	u8 data[4] = { 0x01, 0x00, 0x00, CLAW_RGB_FRAME_OFFSET };
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	u16 read_addr = drvdata->rgb_addr;
+	size_t len = ARRAY_SIZE(data);
+	int ret, i;
+
+	if (!drvdata->rgb_addr)
+		return -ENODEV;
+
+	/* Loop through all 8 pages of RGB data */
+	guard(mutex)(&drvdata->profile_mutex);
+	for (i = 0; i < 8; i++) {
+		drvdata->profile_pending = CLAW_RGB_PENDING;
+		data[1] = (read_addr >> 8) & 0xff;
+		data[2] = read_addr & 0x00ff;
+		ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 25);
+		if (ret) {
+			drvdata->profile_pending = CLAW_NO_PENDING;
+			return ret;
+		}
+		read_addr += CLAW_RGB_FRAME_OFFSET;
+	}
+
+	return 0;
+}
+
+/* Send RGB configuration to device */
+static int claw_write_rgb_state(struct claw_drvdata *drvdata)
+{
+	struct rgb_report report = { 0x01, 0x0000, CLAW_RGB_FRAME_OFFSET, 0x00,
+			drvdata->rgb_frame_count, 0x09, drvdata->rgb_speed,
+			drvdata->led_mc.led_cdev.brightness };
+	u16 write_addr = drvdata->rgb_addr;
+	size_t len = sizeof(report);
+	int f, ret;
+
+	if (!drvdata->rgb_addr)
+		return -ENODEV;
+
+	if (!drvdata->rgb_frame_count)
+		return -EINVAL;
+
+	guard(mutex)(&drvdata->rom_mutex);
+	/* Loop through (up to) 8 pages of RGB data */
+	for (f = 0; f < drvdata->rgb_frame_count; f++) {
+		scoped_guard(spinlock_irqsave, &drvdata->frame_lock)
+			report.zone_data = drvdata->rgb_frames[f];
+
+		/* Set the MCU address to write the frame data to */
+		report.read_addr = cpu_to_be16(write_addr);
+
+		/* Serialize the rgb_report and write it to MCU */
+		ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+					    (u8 *)&report, len, 25);
+		if (ret)
+			return ret;
+
+		/* Increment the write addr by the offset for the next frame */
+		write_addr += CLAW_RGB_FRAME_OFFSET;
+	}
+
+	ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 25);
+
+	return ret;
+}
+
+/* Fill all zones with the same color */
+static void claw_frame_fill_solid(struct rgb_frame *frame, struct rgb_zone zone)
+{
+	int z;
+
+	for (z = 0; z < CLAW_RGB_ZONES; z++)
+		frame->zone[z] = zone;
+}
+
+/* Apply solid effect (1 frame, no color) */
+static int claw_apply_disabled(struct claw_drvdata *drvdata)
+{
+	struct rgb_zone off = { 0x00, 0x00, 0x00};
+
+	scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+		drvdata->rgb_frame_count = 1;
+		claw_frame_fill_solid(&drvdata->rgb_frames[0], off);
+	}
+
+	return claw_write_rgb_state(drvdata);
+}
+
+/* Apply solid effect (1 frame, all zones same color) */
+static int claw_apply_monocolor(struct claw_drvdata *drvdata)
+{
+	struct mc_subled *subleds = drvdata->led_mc.subled_info;
+	struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity,
+				 subleds[2].intensity };
+
+	scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+		drvdata->rgb_frame_count = 1;
+		claw_frame_fill_solid(&drvdata->rgb_frames[0], zone);
+	}
+
+	return claw_write_rgb_state(drvdata);
+}
+
+/* Apply breathe effect (2 frames: color -> off) */
+static int claw_apply_breathe(struct claw_drvdata *drvdata)
+{
+	struct mc_subled *subleds = drvdata->led_mc.subled_info;
+	struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity,
+				 subleds[2].intensity };
+	static const struct rgb_zone off = { 0, 0, 0 };
+
+	scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+		drvdata->rgb_frame_count = 2;
+		claw_frame_fill_solid(&drvdata->rgb_frames[0], zone);
+		claw_frame_fill_solid(&drvdata->rgb_frames[1], off);
+	}
+
+	return claw_write_rgb_state(drvdata);
+}
+
+/* Apply chroma effect (6 frames: rainbow cycle, all zones sync) */
+static int claw_apply_chroma(struct claw_drvdata *drvdata)
+{
+	static const struct rgb_zone colors[] = {
+		{255,   0,   0},  /* red     */
+		{255, 255,   0},  /* yellow  */
+		{  0, 255,   0},  /* green   */
+		{  0, 255, 255},  /* cyan    */
+		{  0,   0, 255},  /* blue    */
+		{255,   0, 255},  /* magenta */
+	};
+	u8 frame_count = ARRAY_SIZE(colors);
+	int f;
+
+	scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+		drvdata->rgb_frame_count = frame_count;
+
+		for (f = 0; f < frame_count; f++)
+			claw_frame_fill_solid(&drvdata->rgb_frames[f], colors[f]);
+	}
+
+	return claw_write_rgb_state(drvdata);
+}
+
+/* Apply rainbow effect (4 frames: rotating colors around joysticks) */
+static int claw_apply_rainbow(struct claw_drvdata *drvdata)
+{
+	static const struct rgb_zone colors[] = {
+		{255,   0,   0},  /* red   */
+		{  0, 255,   0},  /* green */
+		{  0, 255, 255},  /* cyan  */
+		{  0,   0, 255},  /* blue  */
+	};
+	u8 frame_count = ARRAY_SIZE(colors);
+	int f, z;
+
+	scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+		drvdata->rgb_frame_count = frame_count;
+
+		for (f = 0; f < frame_count; f++) {
+			for (z = 0; z < 4; z++) {
+				drvdata->rgb_frames[f].zone[z]     = colors[(z + f) % 4];
+				drvdata->rgb_frames[f].zone[z + 4] = colors[(z + f) % 4];
+			}
+			drvdata->rgb_frames[f].zone[8] = colors[f];
+		}
+	}
+
+	return claw_write_rgb_state(drvdata);
+}
+
+/*
+ * Apply frostfire effect (4 frames: fire vs ice rotating)
+ * Right joystick: fire red -> dark -> ice blue -> dark (clockwise)
+ * Left joystick: ice blue -> dark -> fire red -> dark (counter-clockwise)
+ * ABXY: fire red -> dark -> ice blue -> dark
+ */
+static int claw_apply_frostfire(struct claw_drvdata *drvdata)
+{
+	static const struct rgb_zone colors[] = {
+		{255,   0,   0},  /* fire red */
+		{  0,   0,   0},  /* dark     */
+		{  0,   0, 255},  /* ice blue */
+		{  0,   0,   0},  /* dark     */
+	};
+	u8 frame_count = ARRAY_SIZE(colors);
+	int f, z;
+
+	scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+		drvdata->rgb_frame_count = frame_count;
+
+		for (f = 0; f < frame_count; f++) {
+			for (z = 0; z < 4; z++) {
+				drvdata->rgb_frames[f].zone[z]     = colors[(z + f) % 4];
+				drvdata->rgb_frames[f].zone[z + 4] = colors[(z - f + 6) % 4];
+			}
+			drvdata->rgb_frames[f].zone[8] = colors[f];
+		}
+	}
+
+	return claw_write_rgb_state(drvdata);
+}
+
+/* Apply current state to device */
+static int claw_apply_rgb_state(struct claw_drvdata *drvdata)
+{
+	if (!drvdata->rgb_enabled)
+		return claw_apply_disabled(drvdata);
+
+	switch (drvdata->rgb_effect) {
+	case CLAW_RGB_EFFECT_MONOCOLOR:
+		return claw_apply_monocolor(drvdata);
+	case CLAW_RGB_EFFECT_BREATHE:
+		return claw_apply_breathe(drvdata);
+	case CLAW_RGB_EFFECT_CHROMA:
+		return claw_apply_chroma(drvdata);
+	case CLAW_RGB_EFFECT_RAINBOW:
+		return claw_apply_rainbow(drvdata);
+	case CLAW_RGB_EFFECT_FROSTFIRE:
+		return claw_apply_frostfire(drvdata);
+	default:
+		dev_err(drvdata->led_mc.led_cdev.dev,
+			"No supported rgb_effect selected\n");
+		return -EINVAL;
+	}
+}
+
+static void claw_rgb_queue_fn(struct work_struct *work)
+{
+	struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+	struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, rgb_queue);
+	int ret;
+
+	if (!drvdata->rgb_registered)
+		return;
+
+	ret = claw_apply_rgb_state(drvdata);
+	if (ret)
+		dev_err(drvdata->led_mc.led_cdev.dev,
+			"Failed to apply RGB state: %d\n", ret);
+}
+
+static ssize_t effect_store(struct device *dev,
+			    struct device_attribute *attr,
+			    const char *buf, size_t count)
+{
+	struct led_classdev *led_cdev = dev_get_drvdata(dev);
+	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+	int ret;
+
+	if (!drvdata->rgb_registered)
+		return -ENODEV;
+
+	ret = sysfs_match_string(claw_rgb_effect_text, buf);
+	if (ret < 0)
+		return ret;
+
+	drvdata->rgb_effect = ret;
+	mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+
+	return count;
+}
+
+static ssize_t effect_show(struct device *dev,
+			   struct device_attribute *attr, char *buf)
+{
+	struct led_classdev *led_cdev = dev_get_drvdata(dev);
+	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+
+	if (!drvdata->rgb_registered)
+		return -ENODEV;
+
+	if (drvdata->rgb_effect >= ARRAY_SIZE(claw_rgb_effect_text))
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%s\n", claw_rgb_effect_text[drvdata->rgb_effect]);
+}
+
+static DEVICE_ATTR_RW(effect);
+
+static ssize_t effect_index_show(struct device *dev,
+				 struct device_attribute *attr, char *buf)
+{
+	int i, count = 0;
+
+	for (i = 0; i < ARRAY_SIZE(claw_rgb_effect_text); i++)
+		count += sysfs_emit_at(buf, count, "%s ", claw_rgb_effect_text[i]);
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(effect_index);
+
+static ssize_t enabled_store(struct device *dev,
+			     struct device_attribute *attr,
+			     const char *buf, size_t count)
+{
+	struct led_classdev *led_cdev = dev_get_drvdata(dev);
+	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+	bool val;
+	int ret;
+
+	if (!drvdata->rgb_registered)
+		return -ENODEV;
+
+	ret = kstrtobool(buf, &val);
+	if (ret)
+		return ret;
+
+	drvdata->rgb_enabled = val;
+	mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+
+	return count;
+}
+
+static ssize_t enabled_show(struct device *dev,
+			    struct device_attribute *attr, char *buf)
+{
+	struct led_classdev *led_cdev = dev_get_drvdata(dev);
+	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+
+	if (!drvdata->rgb_registered)
+		return -ENODEV;
+
+	return sysfs_emit(buf, "%s\n", drvdata->rgb_enabled ? "true" : "false");
+}
+static DEVICE_ATTR_RW(enabled);
+
+static ssize_t enabled_index_show(struct device *dev,
+				  struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "true false\n");
+}
+static DEVICE_ATTR_RO(enabled_index);
+
+static ssize_t speed_store(struct device *dev, struct device_attribute *attr,
+			   const char *buf, size_t count)
+{
+	struct led_classdev *led_cdev = dev_get_drvdata(dev);
+	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+	unsigned int val, speed;
+	int ret;
+
+	if (!drvdata->rgb_registered)
+		return -ENODEV;
+
+	ret = kstrtouint(buf, 10, &val);
+	if (ret)
+		return ret;
+
+	if (val > 20)
+		return -EINVAL;
+
+	/* 0 is fastest, invert value for intuitive userspace speed */
+	speed = 20 - val;
+
+	drvdata->rgb_speed = speed;
+	mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+
+	return count;
+}
+
+static ssize_t speed_show(struct device *dev, struct device_attribute *attr,
+			  char *buf)
+{
+	struct led_classdev *led_cdev = dev_get_drvdata(dev);
+	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+	u8 speed = 20 - drvdata->rgb_speed;
+
+	if (!drvdata->rgb_registered)
+		return -ENODEV;
+
+	return sysfs_emit(buf, "%u\n", speed);
+}
+static DEVICE_ATTR_RW(speed);
+
+static ssize_t speed_range_show(struct device *dev,
+				struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "0-20\n");
+}
+static DEVICE_ATTR_RO(speed_range);
+
+static void claw_led_brightness_set(struct led_classdev *led_cdev,
+				    enum led_brightness _brightness)
+{
+	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+
+	if (!drvdata->rgb_registered)
+		return;
+
+	mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+}
+
+static struct attribute *claw_rgb_attrs[] = {
+	&dev_attr_effect.attr,
+	&dev_attr_effect_index.attr,
+	&dev_attr_enabled.attr,
+	&dev_attr_enabled_index.attr,
+	&dev_attr_speed.attr,
+	&dev_attr_speed_range.attr,
+	NULL,
+};
+
+static const struct attribute_group claw_rgb_attr_group = {
+	.attrs = claw_rgb_attrs,
+};
+
+static struct mc_subled claw_rgb_subled_info[] = {
+	{
+		.color_index = LED_COLOR_ID_RED,
+		.channel = 0x1,
+	},
+	{
+		.color_index = LED_COLOR_ID_GREEN,
+		.channel = 0x2,
+	},
+	{
+		.color_index = LED_COLOR_ID_BLUE,
+		.channel = 0x3,
+	},
+};
+
 static void cfg_setup_fn(struct work_struct *work)
 {
 	struct delayed_work *dwork = container_of(work, struct delayed_work, work);
@@ -846,16 +1372,44 @@ static void cfg_setup_fn(struct work_struct *work)
 		return;
 	}
 
-	/* Add sysfs attributes after we get the device state */
-	ret = devm_device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
+	ret = claw_read_rgb_config(drvdata->hdev);
 	if (ret) {
 		dev_err(&drvdata->hdev->dev,
-			"Failed to setup device, can't create gamepad attrs: %d\n", ret);
+			"Failed to setup device, can't read RGB config: %d\n", ret);
 		return;
 	}
-	drvdata->gamepad_registered = true;
+
+	/* Add sysfs attributes after we get the device state */
+	if (!drvdata->gamepad_registered) {
+		ret = devm_device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
+		if (ret) {
+			dev_err(&drvdata->hdev->dev,
+				"Failed to setup device, can't create gamepad attrs: %d\n", ret);
+			return;
+		}
+		drvdata->gamepad_registered = true;
+	}
+
+	/* Add and enable RGB interface once we have the device state */
+	if (!drvdata->rgb_registered) {
+		ret = devm_led_classdev_multicolor_register(&drvdata->hdev->dev, &drvdata->led_mc);
+		if (ret) {
+			dev_err(&drvdata->hdev->dev,
+				"Failed to setup device, can't create led device: %d\n", ret);
+			return;
+		}
+
+		ret = devm_device_add_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group);
+		if (ret) {
+			dev_err(&drvdata->hdev->dev,
+				"Failed to setup device, can't create led attributes: %d\n", ret);
+			return;
+		}
+		drvdata->rgb_registered = true;
+	}
 
 	kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE);
+	kobject_uevent(&drvdata->led_mc.led_cdev.dev->kobj, KOBJ_CHANGE);
 }
 
 static void cfg_resume_fn(struct work_struct *work)
@@ -874,18 +1428,24 @@ static void claw_features_supported(struct claw_drvdata *drvdata)
 
 	if (major == 0x01) {
 		drvdata->bmap_support = true;
-		if (minor >= 0x66)
+		if (minor >= 0x66) {
 			drvdata->bmap_addr = button_mapping_addr_new;
-		else
+			drvdata->rgb_addr = rgb_addr_new;
+		} else {
 			drvdata->bmap_addr = button_mapping_addr_old;
+			drvdata->rgb_addr = rgb_addr_old;
+		}
 		return;
 	}
 
 	if ((major == 0x02 && minor >= 0x17) || major >= 0x03) {
 		drvdata->bmap_support = true;
 		drvdata->bmap_addr = button_mapping_addr_new;
+		drvdata->rgb_addr = rgb_addr_new;
 		return;
 	}
+
+	drvdata->rgb_addr = rgb_addr_old;
 }
 
 static int claw_probe(struct hid_device *hdev, u8 ep)
@@ -900,6 +1460,7 @@ static int claw_probe(struct hid_device *hdev, u8 ep)
 		return -ENOMEM;
 
 	drvdata->gamepad_mode = CLAW_GAMEPAD_MODE_XINPUT;
+	drvdata->rgb_enabled = true;
 	drvdata->hdev = hdev;
 	drvdata->ep = ep;
 
@@ -910,14 +1471,27 @@ static int claw_probe(struct hid_device *hdev, u8 ep)
 	if (!drvdata->bmap_support)
 		dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n");
 
+	drvdata->led_mc.led_cdev.name = "msi_claw:rgb:joystick_rings";
+	drvdata->led_mc.led_cdev.brightness = 0x50;
+	drvdata->led_mc.led_cdev.max_brightness = 0x64;
+	drvdata->led_mc.led_cdev.color = LED_COLOR_ID_RGB;
+	drvdata->led_mc.led_cdev.brightness_set = claw_led_brightness_set;
+	drvdata->led_mc.num_colors = 3;
+	drvdata->led_mc.subled_info = devm_kmemdup(&hdev->dev, claw_rgb_subled_info,
+						   sizeof(claw_rgb_subled_info), GFP_KERNEL);
+	if (!drvdata->led_mc.subled_info)
+		return -ENOMEM;
+
 	mutex_init(&drvdata->cfg_mutex);
 	mutex_init(&drvdata->profile_mutex);
 	mutex_init(&drvdata->rom_mutex);
 	spin_lock_init(&drvdata->cmd_lock);
 	spin_lock_init(&drvdata->mode_lock);
+	spin_lock_init(&drvdata->frame_lock);
 	init_completion(&drvdata->send_cmd_complete);
 	INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
 	INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
+	INIT_DELAYED_WORK(&drvdata->rgb_queue, &claw_rgb_queue_fn);
 
 	/* For control interface: open the HID transport for sending commands. */
 	ret = hid_hw_open(hdev);
@@ -979,10 +1553,14 @@ static void claw_remove(struct hid_device *hdev)
 		return;
 	}
 
+	/* Block writes to brightness/multi_intensity during teardown */
+	drvdata->led_mc.led_cdev.brightness_set = NULL;
 	cancel_delayed_work_sync(&drvdata->cfg_setup);
 	cancel_delayed_work_sync(&drvdata->cfg_resume);
+	cancel_delayed_work_sync(&drvdata->rgb_queue);
 
 	drvdata->gamepad_registered = false;
+	drvdata->rgb_enabled = false;
 
 	hid_hw_close(hdev);
 }
-- 
2.53.0


^ permalink raw reply related

* [PATCH v6 2/4] HID: hid-msi: Add M-key mapping attributes
From: Derek J. Clark @ 2026-05-18 22:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
	Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260518222935.1802071-1-derekjohn.clark@gmail.com>

Adds attributes that allow for remapping the M-keys with up to 5 values
when in macro mode. There are 2 mappable buttons on the rear of the
device, M1 on the right and M2 on the left. When mapped, the events will
fire from one of three event devices: gamepad buttons will fire from the
device handled by xpad, while keyboard and mouse events will fire from
respectively typed evdevs provided by the input core. Names of each
mapping have been kept as close to the event that will fire from the evdev
as possible, with context added to the ABS_ events on the direction of the
movement.

Initial reverse-engineering and implementation of this feature was done
by Zhouwang Huang. I refactored the overall format to conform to kernel
driver best practices and style guides. Claude was used as an initial
reviewer of this patch.

Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz
    config.
  - Gate all attribute show/store functions with gamepad_registered.
  - Remove duplicated argv_free macro.
v5:
  - Ensure adding "DISABLED" key to valid entries is done in the correct
    patch.
  - Re-enable sending an empty string to clear button mappings in
    addition to setting DISABLED.
v4:
  - Change dev_warn to dev_dbg in claw_profile_event.
  - use __free with DEFINE_FREE macro for argv instead of manually
    running argv_free, cleaining up scoped_guard goto.
v3:
  - Use scoped_guard where necessary.
v2:
  - Add mutex for SYNC_TO_ROM commands to ensure every SYNC is completed
    before more data is written to the MCU volatile memory.
  - Add mutex for profile_pending to ensure every profile action
    response is  serialized to the generating command.
---
 drivers/hid/hid-msi.c | 415 +++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 414 insertions(+), 1 deletion(-)

diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
index c79eb0bfeb776..e58d35dba5b40 100644
--- a/drivers/hid/hid-msi.c
+++ b/drivers/hid/hid-msi.c
@@ -42,6 +42,8 @@
 #define CLAW_DINPUT_CFG_INTF_IN	0x82
 #define CLAW_XINPUT_CFG_INTF_IN	0x83
 
+#define CLAW_KEYS_MAX		5
+
 enum claw_command_index {
 	CLAW_COMMAND_TYPE_READ_PROFILE =		0x04,
 	CLAW_COMMAND_TYPE_READ_PROFILE_ACK =		0x05,
@@ -66,6 +68,17 @@ static const char * const claw_gamepad_mode_text[] = {
 	[CLAW_GAMEPAD_MODE_DESKTOP] =	"desktop",
 };
 
+enum claw_profile_ack_pending {
+	CLAW_NO_PENDING,
+	CLAW_M1_PENDING,
+	CLAW_M2_PENDING,
+};
+
+enum claw_key_index {
+	CLAW_KEY_M1,
+	CLAW_KEY_M2,
+};
+
 enum claw_mkeys_function_index {
 	CLAW_MKEY_FUNCTION_MACRO,
 	CLAW_MKEY_FUNCTION_DISABLED,
@@ -78,6 +91,155 @@ static const char * const claw_mkeys_function_text[] = {
 	[CLAW_MKEY_FUNCTION_COMBO] =	"combination",
 };
 
+static const struct {
+	u8 code;
+	const char *name;
+} claw_button_mapping_key_map[] = {
+	/* Gamepad buttons */
+	{ 0x01, "ABS_HAT0Y_UP" },
+	{ 0x02, "ABS_HAT0Y_DOWN" },
+	{ 0x03, "ABS_HAT0X_LEFT" },
+	{ 0x04, "ABS_HAT0X_RIGHT" },
+	{ 0x05, "BTN_TL" },
+	{ 0x06, "BTN_TR" },
+	{ 0x07, "BTN_THUMBL" },
+	{ 0x08, "BTN_THUMBR" },
+	{ 0x09, "BTN_SOUTH" },
+	{ 0x0a, "BTN_EAST" },
+	{ 0x0b, "BTN_NORTH" },
+	{ 0x0c, "BTN_WEST" },
+	{ 0x0d, "BTN_MODE" },
+	{ 0x0e, "BTN_SELECT" },
+	{ 0x0f, "BTN_START" },
+	{ 0x13, "BTN_TL2"},
+	{ 0x14, "BTN_TR2"},
+	{ 0x15, "ABS_Y_UP"},
+	{ 0x16, "ABS_Y_DOWN"},
+	{ 0x17, "ABS_X_LEFT"},
+	{ 0x18, "ABS_X_RIGHT"},
+	{ 0x19, "ABS_RY_UP"},
+	{ 0x1a, "ABS_RY_DOWN"},
+	{ 0x1b, "ABS_RX_LEFT"},
+	{ 0x1c, "ABS_RX_RIGHT"},
+	/* Keyboard keys */
+	{ 0x32, "KEY_ESC" },
+	{ 0x33, "KEY_F1" },
+	{ 0x34, "KEY_F2" },
+	{ 0x35, "KEY_F3" },
+	{ 0x36, "KEY_F4" },
+	{ 0x37, "KEY_F5" },
+	{ 0x38, "KEY_F6" },
+	{ 0x39, "KEY_F7" },
+	{ 0x3a, "KEY_F8" },
+	{ 0x3b, "KEY_F9" },
+	{ 0x3c, "KEY_F10" },
+	{ 0x3d, "KEY_F11" },
+	{ 0x3e, "KEY_F12" },
+	{ 0x3f, "KEY_GRAVE" },
+	{ 0x40, "KEY_1" },
+	{ 0x41, "KEY_2" },
+	{ 0x42, "KEY_3" },
+	{ 0x43, "KEY_4" },
+	{ 0x44, "KEY_5" },
+	{ 0x45, "KEY_6" },
+	{ 0x46, "KEY_7" },
+	{ 0x47, "KEY_8" },
+	{ 0x48, "KEY_9" },
+	{ 0x49, "KEY_0" },
+	{ 0x4a, "KEY_MINUS" },
+	{ 0x4b, "KEY_EQUAL" },
+	{ 0x4c, "KEY_BACKSPACE" },
+	{ 0x4d, "KEY_TAB" },
+	{ 0x4e, "KEY_Q" },
+	{ 0x4f, "KEY_W" },
+	{ 0x50, "KEY_E" },
+	{ 0x51, "KEY_R" },
+	{ 0x52, "KEY_T" },
+	{ 0x53, "KEY_Y" },
+	{ 0x54, "KEY_U" },
+	{ 0x55, "KEY_I" },
+	{ 0x56, "KEY_O" },
+	{ 0x57, "KEY_P" },
+	{ 0x58, "KEY_LEFTBRACE" },
+	{ 0x59, "KEY_RIGHTBRACE" },
+	{ 0x5a, "KEY_BACKSLASH" },
+	{ 0x5b, "KEY_CAPSLOCK" },
+	{ 0x5c, "KEY_A" },
+	{ 0x5d, "KEY_S" },
+	{ 0x5e, "KEY_D" },
+	{ 0x5f, "KEY_F" },
+	{ 0x60, "KEY_G" },
+	{ 0x61, "KEY_H" },
+	{ 0x62, "KEY_J" },
+	{ 0x63, "KEY_K" },
+	{ 0x64, "KEY_L" },
+	{ 0x65, "KEY_SEMICOLON" },
+	{ 0x66, "KEY_APOSTROPHE" },
+	{ 0x67, "KEY_ENTER" },
+	{ 0x68, "KEY_LEFTSHIFT" },
+	{ 0x69, "KEY_Z" },
+	{ 0x6a, "KEY_X" },
+	{ 0x6b, "KEY_C" },
+	{ 0x6c, "KEY_V" },
+	{ 0x6d, "KEY_B" },
+	{ 0x6e, "KEY_N" },
+	{ 0x6f, "KEY_M" },
+	{ 0x70, "KEY_COMMA" },
+	{ 0x71, "KEY_DOT" },
+	{ 0x72, "KEY_SLASH" },
+	{ 0x73, "KEY_RIGHTSHIFT" },
+	{ 0x74, "KEY_LEFTCTRL" },
+	{ 0x75, "KEY_LEFTMETA" },
+	{ 0x76, "KEY_LEFTALT" },
+	{ 0x77, "KEY_SPACE" },
+	{ 0x78, "KEY_RIGHTALT" },
+	{ 0x79, "KEY_RIGHTCTRL" },
+	{ 0x7a, "KEY_INSERT" },
+	{ 0x7b, "KEY_HOME" },
+	{ 0x7c, "KEY_PAGEUP" },
+	{ 0x7d, "KEY_DELETE" },
+	{ 0x7e, "KEY_END" },
+	{ 0x7f, "KEY_PAGEDOWN" },
+	{ 0x8a, "KEY_KPENTER" },
+	{ 0x8b, "KEY_KP0" },
+	{ 0x8c, "KEY_KP1" },
+	{ 0x8d, "KEY_KP2" },
+	{ 0x8e, "KEY_KP3" },
+	{ 0x8f, "KEY_KP4" },
+	{ 0x90, "KEY_KP5" },
+	{ 0x91, "KEY_KP6" },
+	{ 0x92, "KEY_KP7" },
+	{ 0x93, "KEY_KP8" },
+	{ 0x94, "KEY_KP9" },
+	{ 0x95, "MD_PLAY" },
+	{ 0x96, "MD_STOP" },
+	{ 0x97, "MD_NEXT" },
+	{ 0x98, "MD_PREV" },
+	{ 0x99, "MD_VOL_UP" },
+	{ 0x9a, "MD_VOL_DOWN" },
+	{ 0x9b, "MD_VOL_MUTE" },
+	{ 0x9c, "KEY_F23" },
+	/* Mouse events */
+	{ 0xc8, "BTN_LEFT" },
+	{ 0xc9, "BTN_MIDDLE" },
+	{ 0xca, "BTN_RIGHT" },
+	{ 0xcb, "BTN_SIDE" },
+	{ 0xcc, "BTN_EXTRA" },
+	{ 0xcd, "REL_WHEEL_UP" },
+	{ 0xce, "REL_WHEEL_DOWN" },
+	{ 0xff, "DISABLED" },
+};
+
+static const u16 button_mapping_addr_old[] = {
+	0x007a,  /* M1 */
+	0x011f,  /* M2 */
+};
+
+static const u16 button_mapping_addr_new[] = {
+	0x00bb,  /* M1 */
+	0x0164,  /* M2 */
+};
+
 struct claw_command_report {
 	u8 report_id;
 	u8 padding[2];
@@ -88,22 +250,30 @@ struct claw_command_report {
 
 struct claw_drvdata {
 	/* MCU General Variables */
+	enum claw_profile_ack_pending profile_pending;
 	struct completion send_cmd_complete;
 	struct delayed_work cfg_resume;
 	struct delayed_work cfg_setup;
+	struct mutex profile_mutex; /* mutex for profile_pending calls */
 	struct hid_device *hdev;
 	struct mutex cfg_mutex; /* mutex for synchronous data */
+	struct mutex rom_mutex; /* mutex for SYNC_TO_ROM calls */
 	bool waiting_for_ack;
 	spinlock_t cmd_lock; /* Lock for cmd data read/write */
 	u8 waiting_cmd;
 	int cmd_status;
+	u16 bcd_device;
 	u8 ep;
 
 	/* Gamepad Variables */
 	enum claw_mkeys_function_index mkeys_function;
 	enum claw_gamepad_mode_index gamepad_mode;
+	u8 m1_codes[CLAW_KEYS_MAX];
+	u8 m2_codes[CLAW_KEYS_MAX];
 	bool gamepad_registered;
 	spinlock_t mode_lock; /* Lock for mode data read/write */
+	const u16 *bmap_addr;
+	bool bmap_support;
 };
 
 static int get_endpoint_address(struct hid_device *hdev)
@@ -135,6 +305,30 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
 	return 0;
 }
 
+static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep)
+{
+	u8 *codes;
+	int i;
+
+	switch (drvdata->profile_pending) {
+	case CLAW_M1_PENDING:
+	case CLAW_M2_PENDING:
+		codes = (drvdata->profile_pending == CLAW_M1_PENDING) ?
+			drvdata->m1_codes : drvdata->m2_codes;
+		for (i = 0; i < CLAW_KEYS_MAX; i++)
+			codes[i] = (cmd_rep->data[6 + i]);
+		break;
+	default:
+		dev_dbg(&drvdata->hdev->dev,
+			"Got profile event without changes pending from command: %x\n",
+			cmd_rep->cmd);
+		return -EINVAL;
+	}
+	drvdata->profile_pending = CLAW_NO_PENDING;
+
+	return 0;
+}
+
 static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report,
 			  u8 *data, int size)
 {
@@ -165,6 +359,19 @@ static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *repor
 			}
 		}
 
+		break;
+	case CLAW_COMMAND_TYPE_READ_PROFILE_ACK:
+		ret = claw_profile_event(drvdata, cmd_rep);
+
+		scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
+			if (drvdata->waiting_for_ack &&
+			    drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_PROFILE) {
+				drvdata->cmd_status = ret;
+				drvdata->waiting_for_ack = false;
+				complete(&drvdata->send_cmd_complete);
+			}
+		}
+
 		break;
 	case CLAW_COMMAND_TYPE_ACK:
 		scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
@@ -422,6 +629,168 @@ static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
 }
 static DEVICE_ATTR_WO(reset);
 
+static int button_mapping_name_to_code(const char *name)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) {
+		if (!strcmp(name, claw_button_mapping_key_map[i].name))
+			return claw_button_mapping_key_map[i].code;
+	}
+
+	return -EINVAL;
+}
+
+static const char *button_mapping_code_to_name(u8 code)
+{
+	int i;
+
+	if (code == 0xff)
+		return NULL;
+
+	for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) {
+		if (claw_button_mapping_key_map[i].code == code)
+			return claw_button_mapping_key_map[i].name;
+	}
+
+	return NULL;
+}
+
+static int claw_buttons_store(struct device *dev, const char *buf, u8 mkey_idx)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	u8 data[] = { 0x01, (drvdata->bmap_addr[mkey_idx] >> 8) & 0xff,
+		      drvdata->bmap_addr[mkey_idx] & 0xff, 0x07,
+		      0x04, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff };
+	char **raw_keys __free(argv_free) = NULL;
+	size_t len = ARRAY_SIZE(data);
+	int ret, key_count, i;
+
+	if (!drvdata->gamepad_registered)
+		return -ENODEV;
+
+	raw_keys = argv_split(GFP_KERNEL, buf, &key_count);
+	if (!raw_keys)
+		return -ENOMEM;
+
+	if (key_count > CLAW_KEYS_MAX)
+		return -EINVAL;
+
+	if (key_count == 0)
+		goto set_buttons;
+
+	for (i = 0; i < key_count; i++) {
+		ret = button_mapping_name_to_code(raw_keys[i]);
+		if (ret < 0)
+			return ret;
+
+		data[6 + i] = ret;
+	}
+
+set_buttons:
+	scoped_guard(mutex, &drvdata->rom_mutex) {
+		ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+					    data, len, 25);
+		if (ret)
+			return ret;
+
+		ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 25);
+	}
+
+	return ret;
+}
+
+static int claw_buttons_show(struct device *dev, char *buf, enum claw_key_index m_key)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	u8 data[] = { 0x01, (drvdata->bmap_addr[m_key] >> 8) & 0xff,
+		      drvdata->bmap_addr[m_key] & 0xff, 0x07 };
+	size_t len = ARRAY_SIZE(data);
+	int i, ret, count = 0;
+	const char *name;
+	u8 *codes;
+
+	if (!drvdata->gamepad_registered)
+		return -ENODEV;
+
+	codes = (m_key == CLAW_KEY_M1) ? drvdata->m1_codes : drvdata->m2_codes;
+
+	guard(mutex)(&drvdata->profile_mutex);
+	drvdata->profile_pending = (m_key == CLAW_KEY_M1) ? CLAW_M1_PENDING : CLAW_M2_PENDING;
+
+	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 25);
+	if (ret) {
+		drvdata->profile_pending = CLAW_NO_PENDING;
+		return ret;
+	}
+	for (i = 0; i < CLAW_KEYS_MAX; i++) {
+		name = button_mapping_code_to_name(codes[i]);
+		if (name)
+			count += sysfs_emit_at(buf, count, "%s ", name);
+	}
+
+	if (!count)
+		return sysfs_emit(buf, "(not set)\n");
+
+	buf[count - 1] = '\n';
+
+	return count;
+}
+
+static ssize_t button_m1_store(struct device *dev, struct device_attribute *attr,
+			       const char *buf, size_t count)
+{
+	int ret;
+
+	ret = claw_buttons_store(dev, buf, CLAW_KEY_M1);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t button_m1_show(struct device *dev, struct device_attribute *attr,
+			      char *buf)
+{
+	return claw_buttons_show(dev, buf, CLAW_KEY_M1);
+}
+static DEVICE_ATTR_RW(button_m1);
+
+static ssize_t button_m2_store(struct device *dev, struct device_attribute *attr,
+			       const char *buf, size_t count)
+{
+	int ret;
+
+	ret = claw_buttons_store(dev, buf, CLAW_KEY_M2);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t button_m2_show(struct device *dev, struct device_attribute *attr,
+			      char *buf)
+{
+	return claw_buttons_show(dev, buf, CLAW_KEY_M2);
+}
+static DEVICE_ATTR_RW(button_m2);
+
+static ssize_t button_mapping_options_show(struct device *dev,
+					   struct device_attribute *attr, char *buf)
+{
+	int i, count = 0;
+
+	for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++)
+		count += sysfs_emit_at(buf, count, "%s ", claw_button_mapping_key_map[i].name);
+
+	buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(button_mapping_options);
+
 static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
 					    int n)
 {
@@ -434,10 +803,22 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu
 		return 0;
 	}
 
-	return attr->mode;
+	/* Always show attrs available on all firmware */
+	if (attr == &dev_attr_gamepad_mode.attr ||
+	    attr == &dev_attr_gamepad_mode_index.attr ||
+	    attr == &dev_attr_mkeys_function.attr ||
+	    attr == &dev_attr_mkeys_function_index.attr ||
+	    attr == &dev_attr_reset.attr)
+		return attr->mode;
+
+	/* Hide button mapping attrs if it isn't supported */
+	return drvdata->bmap_support ? attr->mode : 0;
 }
 
 static struct attribute *claw_gamepad_attrs[] = {
+	&dev_attr_button_m1.attr,
+	&dev_attr_button_m2.attr,
+	&dev_attr_button_mapping_options.attr,
 	&dev_attr_gamepad_mode.attr,
 	&dev_attr_gamepad_mode_index.attr,
 	&dev_attr_mkeys_function.attr,
@@ -486,8 +867,31 @@ static void cfg_resume_fn(struct work_struct *work)
 		schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
 }
 
+static void claw_features_supported(struct claw_drvdata *drvdata)
+{
+	u8 major = (drvdata->bcd_device >> 8) & 0xff;
+	u8 minor = drvdata->bcd_device & 0xff;
+
+	if (major == 0x01) {
+		drvdata->bmap_support = true;
+		if (minor >= 0x66)
+			drvdata->bmap_addr = button_mapping_addr_new;
+		else
+			drvdata->bmap_addr = button_mapping_addr_old;
+		return;
+	}
+
+	if ((major == 0x02 && minor >= 0x17) || major >= 0x03) {
+		drvdata->bmap_support = true;
+		drvdata->bmap_addr = button_mapping_addr_new;
+		return;
+	}
+}
+
 static int claw_probe(struct hid_device *hdev, u8 ep)
 {
+	struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+	struct usb_device *udev = interface_to_usbdev(intf);
 	struct claw_drvdata *drvdata;
 	int ret;
 
@@ -499,7 +903,16 @@ static int claw_probe(struct hid_device *hdev, u8 ep)
 	drvdata->hdev = hdev;
 	drvdata->ep = ep;
 
+	/* Determine feature level from firmware version */
+	drvdata->bcd_device = le16_to_cpu(udev->descriptor.bcdDevice);
+	claw_features_supported(drvdata);
+
+	if (!drvdata->bmap_support)
+		dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n");
+
 	mutex_init(&drvdata->cfg_mutex);
+	mutex_init(&drvdata->profile_mutex);
+	mutex_init(&drvdata->rom_mutex);
 	spin_lock_init(&drvdata->cmd_lock);
 	spin_lock_init(&drvdata->mode_lock);
 	init_completion(&drvdata->send_cmd_complete);
-- 
2.53.0


^ permalink raw reply related

* [PATCH v6 1/4] HID: hid-msi: Add MSI Claw configuration driver
From: Derek J. Clark @ 2026-05-18 22:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
	Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260518222935.1802071-1-derekjohn.clark@gmail.com>

Adds configuration HID driver for the MSI Claw series of handheld PC's.
In this initial patch add the initial driver outline and attributes for
changing the gamepad mode, M-key behavior, and add a WO reset function.

Sending the SWITCH_MODE and RESET commands causes a USB disconnect in
the device. The completion will therefore never get hit and would trigger
an -EIO. To avoid showing the user an error for every write to these
attrs a bypass for the completion handling is introduced when timeout ==
0.

The initial version of this patch was written by Denis Benato, which
contained the initial reverse-engineering and implementation for the
gamepad mode switching. This work was later expanded by Zhouwang Huang
to include more gamepad modes. Finally, I refactored the drivers data
in/out flow and overall format to conform to kernel driver best
practices and style guides. Claude was used as an initial reviewer of
this patch.

Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Add send/ack pattern to ensure synchronous acks.
  - Use spinlock_irqsave instead of mutex for read/write MODE event
    data.
  - add select NEW_LEDS to kconfig.
  - Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz
    config.
  - Gate all attribute show/store functions with gamepad_registered,
    enabling use of devm_device_add_group.
  - Re-arm cfg_setup in resume if it was canceled in an early suspend.
  - Don't set gamepad_mode on resume, MCU preserves state.
  - Ensure all count variables are checked for > 0 characters before
    setting buf - 1 to \n.
v5:
  - Swap disabled & combination mkeys_function enum values.
  - Ensure mode_mutex is properly init.
  - Ensure claw_remove is calling hid_hw_close and not hid_hw_stop for
    all paths.
v4:
  - Add msi_suspend/claw_suspend.
  - Reorder claw_remove to cancel all work before removing sysfs.
  - Add mutex lock for removing sysfs attributes.
  - Add mutex lock for MODE command data read/write.
v3:
  - Ensure claw_hw_output_report is properly guarded.
  - Reoder claw_probe to ensure all mutex, completion, and variable
    assignments are in place prior to setting drvdata.
  - Ensure gamepad_mode is set to a valid enum value in claw_probe.
v2:
  - Rename driver to hid-msi from hid-msi-claw.
  - Rename reusable/generic functions to msi_* from claw_*, retaining
    claw specific functions.
  - Add generic entrypoints for probe, remove, and raw event that route
    to claw specific functions.
---
 MAINTAINERS           |   6 +
 drivers/hid/Kconfig   |  13 +
 drivers/hid/Makefile  |   1 +
 drivers/hid/hid-ids.h |   5 +
 drivers/hid/hid-msi.c | 675 ++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 700 insertions(+)
 create mode 100644 drivers/hid/hid-msi.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 6f6517bf4f970..8e2de98b768f7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17965,6 +17965,12 @@ S:	Odd Fixes
 F:	Documentation/devicetree/bindings/net/ieee802154/mrf24j40.txt
 F:	drivers/net/ieee802154/mrf24j40.c
 
+MSI HID DRIVER
+M:	Derek J. Clark <derekjohn.clark@gmail.com>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	drivers/hid/hid-msi.c
+
 MSI EC DRIVER
 M:	Nikita Kravets <teackot@gmail.com>
 L:	platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 10c12d8e65579..7766676051a52 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -492,6 +492,19 @@ config HID_GT683R
 	Currently the following devices are know to be supported:
 	  - MSI GT683R
 
+config HID_MSI
+	tristate "MSI Claw Gamepad Support"
+	depends on USB_HID
+	select NEW_LEDS
+	select LEDS_CLASS
+	select LEDS_CLASS_MULTICOLOR
+	help
+	Support for the MSI Claw RGB and controller configuration
+
+	Say Y here to include configuration interface support for the MSI Claw Line
+	of Handheld Console Controllers. Say M here to compile this driver as a
+	module. The module will be called hid-msi.
+
 config HID_KEYTOUCH
 	tristate "Keytouch HID devices"
 	help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 07dfdb6a49c59..80925a17b059c 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -92,6 +92,7 @@ obj-$(CONFIG_HID_MAYFLASH)	+= hid-mf.o
 obj-$(CONFIG_HID_MEGAWORLD_FF)	+= hid-megaworld.o
 obj-$(CONFIG_HID_MICROSOFT)	+= hid-microsoft.o
 obj-$(CONFIG_HID_MONTEREY)	+= hid-monterey.o
+obj-$(CONFIG_HID_MSI)		+= hid-msi.o
 obj-$(CONFIG_HID_MULTITOUCH)	+= hid-multitouch.o
 obj-$(CONFIG_HID_NINTENDO)	+= hid-nintendo.o
 obj-$(CONFIG_HID_NTI)			+= hid-nti.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 933b7943bdb50..94a9b89dc240a 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1047,7 +1047,12 @@
 #define USB_DEVICE_ID_MOZA_R16_R21_2	0x0010
 
 #define USB_VENDOR_ID_MSI		0x1770
+#define USB_VENDOR_ID_MSI_2		0x0db0
 #define USB_DEVICE_ID_MSI_GT683R_LED_PANEL 0xff00
+#define USB_DEVICE_ID_MSI_CLAW_XINPUT	0x1901
+#define USB_DEVICE_ID_MSI_CLAW_DINPUT	0x1902
+#define USB_DEVICE_ID_MSI_CLAW_DESKTOP	0x1903
+#define USB_DEVICE_ID_MSI_CLAW_BIOS	0x1904
 
 #define USB_VENDOR_ID_NATIONAL_SEMICONDUCTOR 0x0400
 #define USB_DEVICE_ID_N_S_HARMONY	0xc359
diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
new file mode 100644
index 0000000000000..c79eb0bfeb776
--- /dev/null
+++ b/drivers/hid/hid-msi.c
@@ -0,0 +1,675 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *  HID driver for MSI Claw Handheld PC gamepads.
+ *
+ *  Provides configuration support for the MSI Claw series of handheld PC
+ *  gamepads. Multiple iterations of the device firmware has led to some
+ *  quirks for how certain attributes are handled. The original firmware
+ *  did not support remapping of the M1 (right) and M2 (left) rear paddles.
+ *  Additionally, the MCU RAM address for writing configuration data has
+ *  changed twice. Checks are done during probe to enumerate these variances.
+ *
+ *  Copyright (c) 2026 Zhouwang Huang <honjow311@gmail.com>
+ *  Copyright (c) 2026 Denis Benato <denis.benato@linux.dev>
+ *  Copyright (c) 2026 Valve Corporation
+ */
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/container_of.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/kobject.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pm.h>
+#include <linux/spinlock.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define CLAW_OUTPUT_REPORT_ID	0x0f
+#define CLAW_INPUT_REPORT_ID	0x10
+
+#define CLAW_PACKET_SIZE	64
+
+#define CLAW_DINPUT_CFG_INTF_IN	0x82
+#define CLAW_XINPUT_CFG_INTF_IN	0x83
+
+enum claw_command_index {
+	CLAW_COMMAND_TYPE_READ_PROFILE =		0x04,
+	CLAW_COMMAND_TYPE_READ_PROFILE_ACK =		0x05,
+	CLAW_COMMAND_TYPE_ACK =				0x06,
+	CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA =		0x21,
+	CLAW_COMMAND_TYPE_SYNC_TO_ROM =			0x22,
+	CLAW_COMMAND_TYPE_SWITCH_MODE =			0x24,
+	CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE =		0x26,
+	CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK =		0x27,
+	CLAW_COMMAND_TYPE_RESET_DEVICE =		0x28,
+};
+
+enum claw_gamepad_mode_index {
+	CLAW_GAMEPAD_MODE_XINPUT =	0x01,
+	CLAW_GAMEPAD_MODE_DINPUT =	0x02,
+	CLAW_GAMEPAD_MODE_DESKTOP =	0x04,
+};
+
+static const char * const claw_gamepad_mode_text[] = {
+	[CLAW_GAMEPAD_MODE_XINPUT] =	"xinput",
+	[CLAW_GAMEPAD_MODE_DINPUT] =	"dinput",
+	[CLAW_GAMEPAD_MODE_DESKTOP] =	"desktop",
+};
+
+enum claw_mkeys_function_index {
+	CLAW_MKEY_FUNCTION_MACRO,
+	CLAW_MKEY_FUNCTION_DISABLED,
+	CLAW_MKEY_FUNCTION_COMBO,
+};
+
+static const char * const claw_mkeys_function_text[] = {
+	[CLAW_MKEY_FUNCTION_MACRO] =	"macro",
+	[CLAW_MKEY_FUNCTION_DISABLED] =	"disabled",
+	[CLAW_MKEY_FUNCTION_COMBO] =	"combination",
+};
+
+struct claw_command_report {
+	u8 report_id;
+	u8 padding[2];
+	u8 header_tail;
+	u8 cmd;
+	u8 data[59];
+} __packed;
+
+struct claw_drvdata {
+	/* MCU General Variables */
+	struct completion send_cmd_complete;
+	struct delayed_work cfg_resume;
+	struct delayed_work cfg_setup;
+	struct hid_device *hdev;
+	struct mutex cfg_mutex; /* mutex for synchronous data */
+	bool waiting_for_ack;
+	spinlock_t cmd_lock; /* Lock for cmd data read/write */
+	u8 waiting_cmd;
+	int cmd_status;
+	u8 ep;
+
+	/* Gamepad Variables */
+	enum claw_mkeys_function_index mkeys_function;
+	enum claw_gamepad_mode_index gamepad_mode;
+	bool gamepad_registered;
+	spinlock_t mode_lock; /* Lock for mode data read/write */
+};
+
+static int get_endpoint_address(struct hid_device *hdev)
+{
+	struct usb_host_endpoint *ep;
+	struct usb_interface *intf;
+
+	intf = to_usb_interface(hdev->dev.parent);
+	ep = intf->cur_altsetting->endpoint;
+	if (ep)
+		return ep->desc.bEndpointAddress;
+
+	return -ENODEV;
+}
+
+static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
+				   struct claw_command_report *cmd_rep)
+{
+	if (cmd_rep->data[0] >= ARRAY_SIZE(claw_gamepad_mode_text) ||
+	    !claw_gamepad_mode_text[cmd_rep->data[0]] ||
+	    cmd_rep->data[1] >= ARRAY_SIZE(claw_mkeys_function_text))
+		return -EINVAL;
+
+	scoped_guard(spinlock_irqsave, &drvdata->mode_lock) {
+		drvdata->gamepad_mode = cmd_rep->data[0];
+		drvdata->mkeys_function = cmd_rep->data[1];
+	}
+
+	return 0;
+}
+
+static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report,
+			  u8 *data, int size)
+{
+	struct claw_command_report *cmd_rep;
+	int ret = 0;
+
+	if (size != CLAW_PACKET_SIZE)
+		return 0;
+
+	cmd_rep = (struct claw_command_report *)data;
+
+	if (cmd_rep->report_id != CLAW_INPUT_REPORT_ID || cmd_rep->header_tail != 0x3c)
+		return 0;
+
+	dev_dbg(&drvdata->hdev->dev, "Rx data as raw input report: [%*ph]\n",
+		CLAW_PACKET_SIZE, data);
+
+	switch (cmd_rep->cmd) {
+	case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK:
+		ret = claw_gamepad_mode_event(drvdata, cmd_rep);
+
+		scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
+			if (drvdata->waiting_for_ack &&
+			    drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE) {
+				drvdata->cmd_status = ret;
+				drvdata->waiting_for_ack = false;
+				complete(&drvdata->send_cmd_complete);
+			}
+		}
+
+		break;
+	case CLAW_COMMAND_TYPE_ACK:
+		scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
+			if (drvdata->waiting_for_ack) {
+				drvdata->cmd_status = 0;
+				drvdata->waiting_for_ack = false;
+				complete(&drvdata->send_cmd_complete);
+			}
+			dev_dbg(&drvdata->hdev->dev, "Waiting CMD: %x\n", drvdata->waiting_cmd);
+		}
+
+		break;
+	default:
+		dev_dbg(&drvdata->hdev->dev, "Unknown command: %x\n", cmd_rep->cmd);
+		return 0;
+	}
+
+	return ret;
+}
+
+static int msi_raw_event(struct hid_device *hdev, struct hid_report *report,
+			 u8 *data, int size)
+{
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+	if (!drvdata || (drvdata->ep != CLAW_XINPUT_CFG_INTF_IN &&
+			 drvdata->ep != CLAW_DINPUT_CFG_INTF_IN))
+		return 0;
+
+	return claw_raw_event(drvdata, report, data, size);
+}
+
+static int claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data,
+				 size_t len, unsigned int timeout)
+{
+	unsigned char *dmabuf __free(kfree) = NULL;
+	u8 header[] = { CLAW_OUTPUT_REPORT_ID, 0, 0, 0x3c, index };
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	size_t header_size = ARRAY_SIZE(header);
+	int ret;
+
+	if (header_size + len > CLAW_PACKET_SIZE)
+		return -EINVAL;
+
+	/* We can't use a devm_alloc reusable buffer without side effects during suspend */
+	dmabuf = kzalloc(CLAW_PACKET_SIZE, GFP_KERNEL);
+	if (!dmabuf)
+		return -ENOMEM;
+
+	memcpy(dmabuf, header, header_size);
+	if (data && len)
+		memcpy(dmabuf + header_size, data, len);
+
+	guard(mutex)(&drvdata->cfg_mutex);
+	if (timeout) {
+		scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
+			drvdata->waiting_cmd = index;
+			drvdata->waiting_for_ack = true;
+			drvdata->cmd_status = -ETIMEDOUT;
+		}
+		reinit_completion(&drvdata->send_cmd_complete);
+	}
+
+	dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
+		CLAW_PACKET_SIZE, dmabuf);
+
+	ret = hid_hw_output_report(hdev, dmabuf, CLAW_PACKET_SIZE);
+	if (ret < 0)
+		return ret;
+
+	ret = ret == CLAW_PACKET_SIZE ? 0 : -EIO;
+	if (ret)
+		return ret;
+
+	if (timeout) {
+		ret = wait_for_completion_interruptible_timeout(&drvdata->send_cmd_complete,
+								msecs_to_jiffies(timeout));
+
+		dev_dbg(&hdev->dev, "Remaining timeout: %u\n", ret);
+		ret = ret > 0 ? drvdata->cmd_status : ret ?: -EBUSY;
+		scoped_guard(spinlock_irqsave, &drvdata->cmd_lock)
+			drvdata->waiting_for_ack = false;
+	}
+
+	return ret;
+}
+
+static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr,
+				  const char *buf, size_t count)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	int i, ret = -EINVAL;
+	u8 data[2];
+
+	if (!drvdata->gamepad_registered)
+		return -ENODEV;
+
+	for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) {
+		if (claw_gamepad_mode_text[i] && sysfs_streq(buf, claw_gamepad_mode_text[i])) {
+			ret = i;
+			break;
+		}
+	}
+	if (ret < 0)
+		return ret;
+
+	data[0] = ret;
+	scoped_guard(spinlock_irqsave, &drvdata->mode_lock)
+		data[1] = drvdata->mkeys_function;
+
+	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t gamepad_mode_show(struct device *dev,
+				 struct device_attribute *attr, char *buf)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	int ret, i;
+
+	if (drvdata->gamepad_registered)
+		return -ENODEV;
+
+	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 25);
+	if (ret)
+		return ret;
+
+	scoped_guard(spinlock_irqsave, &drvdata->mode_lock)
+		i = drvdata->gamepad_mode;
+
+	if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0')
+		return sysfs_emit(buf, "unsupported\n");
+
+	return sysfs_emit(buf, "%s\n", claw_gamepad_mode_text[i]);
+}
+static DEVICE_ATTR_RW(gamepad_mode);
+
+static ssize_t gamepad_mode_index_show(struct device *dev,
+				       struct device_attribute *attr, char *buf)
+{
+	ssize_t count = 0;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) {
+		if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0')
+			continue;
+		count += sysfs_emit_at(buf, count, "%s ", claw_gamepad_mode_text[i]);
+	}
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(gamepad_mode_index);
+
+static ssize_t mkeys_function_store(struct device *dev, struct device_attribute *attr,
+				    const char *buf, size_t count)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	int i, ret = -EINVAL;
+	u8 data[2];
+
+	if (!drvdata->gamepad_registered)
+		return -ENODEV;
+
+	for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) {
+		if (claw_mkeys_function_text[i] && sysfs_streq(buf, claw_mkeys_function_text[i])) {
+			ret = i;
+			break;
+		}
+	}
+	if (ret < 0)
+		return ret;
+
+	scoped_guard(spinlock_irqsave, &drvdata->mode_lock)
+		data[0] = drvdata->gamepad_mode;
+	data[1] = ret;
+
+	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t mkeys_function_show(struct device *dev, struct device_attribute *attr,
+				   char *buf)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	int ret, i;
+
+	if (!drvdata->gamepad_registered)
+		return -ENODEV;
+
+	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 25);
+	if (ret)
+		return ret;
+
+	scoped_guard(spinlock_irqsave, &drvdata->mode_lock)
+		i = drvdata->mkeys_function;
+
+	if (i >= ARRAY_SIZE(claw_mkeys_function_text))
+		return sysfs_emit(buf, "unsupported\n");
+
+	return sysfs_emit(buf, "%s\n", claw_mkeys_function_text[i]);
+}
+static DEVICE_ATTR_RW(mkeys_function);
+
+static ssize_t mkeys_function_index_show(struct device *dev,
+					 struct device_attribute *attr, char *buf)
+{
+	int i, count = 0;
+
+	for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++)
+		count += sysfs_emit_at(buf, count, "%s ", claw_mkeys_function_text[i]);
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(mkeys_function_index);
+
+static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
+			   const char *buf, size_t count)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+	bool val;
+	int ret;
+
+	if (!drvdata->gamepad_registered)
+		return -ENODEV;
+
+	ret = kstrtobool(buf, &val);
+	if (ret)
+		return ret;
+
+	if (!val)
+		return -EINVAL;
+
+	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_RESET_DEVICE, NULL, 0, 0);
+	if (ret)
+		return ret;
+
+	return count;
+}
+static DEVICE_ATTR_WO(reset);
+
+static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
+					    int n)
+{
+	struct hid_device *hdev = to_hid_device(kobj_to_dev(kobj));
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+	if (!drvdata) {
+		dev_warn(&hdev->dev,
+			 "Failed to get drvdata from kobj. Gamepad attributes are not available.\n");
+		return 0;
+	}
+
+	return attr->mode;
+}
+
+static struct attribute *claw_gamepad_attrs[] = {
+	&dev_attr_gamepad_mode.attr,
+	&dev_attr_gamepad_mode_index.attr,
+	&dev_attr_mkeys_function.attr,
+	&dev_attr_mkeys_function_index.attr,
+	&dev_attr_reset.attr,
+	NULL,
+};
+
+static const struct attribute_group claw_gamepad_attr_group = {
+	.attrs = claw_gamepad_attrs,
+	.is_visible = claw_gamepad_attr_is_visible,
+};
+
+static void cfg_setup_fn(struct work_struct *work)
+{
+	struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+	struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_setup);
+	int ret;
+
+	ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE,
+				    NULL, 0, 25);
+	if (ret) {
+		dev_err(&drvdata->hdev->dev,
+			"Failed to setup device, can't read gamepad mode: %d\n", ret);
+		return;
+	}
+
+	/* Add sysfs attributes after we get the device state */
+	ret = devm_device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
+	if (ret) {
+		dev_err(&drvdata->hdev->dev,
+			"Failed to setup device, can't create gamepad attrs: %d\n", ret);
+		return;
+	}
+	drvdata->gamepad_registered = true;
+
+	kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE);
+}
+
+static void cfg_resume_fn(struct work_struct *work)
+{
+	struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+	struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_resume);
+
+	if (!drvdata->gamepad_registered)
+		schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
+}
+
+static int claw_probe(struct hid_device *hdev, u8 ep)
+{
+	struct claw_drvdata *drvdata;
+	int ret;
+
+	drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
+	if (!drvdata)
+		return -ENOMEM;
+
+	drvdata->gamepad_mode = CLAW_GAMEPAD_MODE_XINPUT;
+	drvdata->hdev = hdev;
+	drvdata->ep = ep;
+
+	mutex_init(&drvdata->cfg_mutex);
+	spin_lock_init(&drvdata->cmd_lock);
+	spin_lock_init(&drvdata->mode_lock);
+	init_completion(&drvdata->send_cmd_complete);
+	INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
+	INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
+
+	/* For control interface: open the HID transport for sending commands. */
+	ret = hid_hw_open(hdev);
+	if (ret)
+		return ret;
+
+	hid_set_drvdata(hdev, drvdata);
+	schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
+
+	return 0;
+}
+
+static int msi_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+	int ret;
+	u8 ep;
+
+	if (!hid_is_usb(hdev)) {
+		ret = -ENODEV;
+		goto err_probe;
+	}
+
+	ret = hid_parse(hdev);
+	if (ret)
+		goto err_probe;
+
+	/* Set quirk to create separate input devices per HID application */
+	hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT;
+	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+	if (ret)
+		goto err_probe;
+
+	/* For non-control interfaces (keyboard/mouse), allow userspace to grab the devices. */
+	ret = get_endpoint_address(hdev);
+	if (ret < 0)
+		goto err_stop_hw;
+
+	ep = ret;
+	if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) {
+		ret = claw_probe(hdev, ep);
+		if (ret)
+			goto err_stop_hw;
+	}
+
+	return 0;
+
+err_stop_hw:
+	hid_hw_stop(hdev);
+err_probe:
+	return dev_err_probe(&hdev->dev, ret, "Failed to init device\n");
+}
+
+static void claw_remove(struct hid_device *hdev)
+{
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+	if (!drvdata) {
+		hid_hw_close(hdev);
+		return;
+	}
+
+	cancel_delayed_work_sync(&drvdata->cfg_setup);
+	cancel_delayed_work_sync(&drvdata->cfg_resume);
+
+	drvdata->gamepad_registered = false;
+
+	hid_hw_close(hdev);
+}
+
+static void msi_remove(struct hid_device *hdev)
+{
+	int ret;
+	u8 ep;
+
+	ret = get_endpoint_address(hdev);
+	if (ret <= 0)
+		goto hw_stop;
+
+	ep = ret;
+	if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+		claw_remove(hdev);
+
+hw_stop:
+	hid_hw_stop(hdev);
+}
+
+static int claw_resume(struct hid_device *hdev)
+{
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+	if (!drvdata)
+		return -ENODEV;
+
+	/* MCU can take up to 500ms to be ready after resume */
+	schedule_delayed_work(&drvdata->cfg_resume, msecs_to_jiffies(500));
+	return 0;
+}
+
+static int msi_resume(struct hid_device *hdev)
+{
+	int ret;
+	u8 ep;
+
+	ret = get_endpoint_address(hdev);
+	if (ret <= 0)
+		return 0;
+
+	ep = ret;
+	if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+		return claw_resume(hdev);
+
+	return 0;
+}
+
+static int claw_suspend(struct hid_device *hdev)
+{
+	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+	if (!drvdata)
+		return -ENODEV;
+
+	cancel_delayed_work_sync(&drvdata->cfg_setup);
+	cancel_delayed_work_sync(&drvdata->cfg_resume);
+
+	return 0;
+}
+
+static int msi_suspend(struct hid_device *hdev, pm_message_t msg)
+{
+	int ret;
+	u8 ep;
+
+	ret = get_endpoint_address(hdev);
+	if (ret <= 0)
+		return 0;
+
+	ep = ret;
+	if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+		return claw_suspend(hdev);
+
+	return 0;
+}
+
+static const struct hid_device_id msi_devices[] = {
+	{ HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_XINPUT) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DINPUT) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DESKTOP) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_BIOS) },
+	{ }
+};
+MODULE_DEVICE_TABLE(hid, msi_devices);
+
+static struct hid_driver msi_driver = {
+	.name		= "hid-msi",
+	.id_table	= msi_devices,
+	.raw_event	= msi_raw_event,
+	.probe		= msi_probe,
+	.remove		= msi_remove,
+	.resume		= msi_resume,
+	.suspend	= pm_ptr(msi_suspend),
+};
+module_hid_driver(msi_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Denis Benato <denis.benato@linux.dev>");
+MODULE_AUTHOR("Zhouwang Huang <honjow311@gmail.com>");
+MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
+MODULE_DESCRIPTION("HID driver for MSI Claw Handheld PC gamepads");
-- 
2.53.0


^ permalink raw reply related

* [PATCH v6 0/4] Add MSI Claw HID Configuration Driver
From: Derek J. Clark @ 2026-05-18 22:29 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
	Derek J . Clark, linux-input, linux-doc, linux-kernel

This series adds an HID Configuration driver for the MSI Claw line of
Handheld Gaming PC's. The MSI Claw HID interface provides multiple
features, such as the ability to switch between xinput, dinput, and a
desktop mode, RGB control, rumble intensity, and mapping of the rear "M"
keys. There are additional gamepad modes that are not included in this
driver as they appear to be used in assembly line testing or are
incomplete in the firmware. During my testing I found them to be unstable.

The initial version of this driver was written by Denis Benato, which
contained the initial reverse-engineering and implementation for the
gamepad mode switching. This work was later expanded by Zhouwang Huang
to include more gamepad modes and additional features. Finally, I
refactored the entire driver, fixed multiple bugs, and refined the overall
format to conform to kernel driver best practices and style guide.

Claude was used initially by Zhouwang Huang to quickly parse HID captures
during the reverse-engineering of some of the features. Since Claude had
already been used, as a test of its capabilities I had it implement the
rumble intensity attribute after I had already rewritten most of the
driver, which I then manually edited to fix some mistakes. I also used
Claude to review the driver and these patches for any mistakes and bugs.

Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v6:
  - Add send/ack pattern to ensure synchronous acks.
  - Use spinlock_irqsave instead of mutex for read/write MODE event
    data.
  - add select NEW_LEDS to kconfig.
  - Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz
    config.
  - Gate all attribute show/store functions with gamepad_registered or
    rgb_registered, enabling use of devm_device_add_group and ending
    the need to hold a mutex during remove.
  - Don't set gamepad_mode on resume, MCU preserves state.
  - Ensure all count variables are checked for > 0 characters before
    setting buf - 1 to \n.
  - Re-arm cfg_setup in resume if it was canceled in an early suspend.
  - Remove duplicated argv_free macro.
  - Add spinlock_irqsave vice mutex for read/write access on attribute
    variables.
v5: https://lore.kernel.org/linux-input/20260517013925.3120314-1-derekjohn.clark@gmail.com/
  - Swap disabled & combination mkeys_function enum values.
  - Fix bug introduced in v5 where claw_buttons_store would return
    -EINVAL on all valid key entries.
  - Ensure mode_mutex is properly init.
  - Ensure claw_remove is calling hid_hw_close and not hid_hw_stop for
    all paths.
  - Ensure adding "DISABLED" key to valid entries is done in the correct
    patch.
  - Re-enable sending an empty string to clear button mappings in
    addition to setting DISABLED.
  - Move adding the RGB device into cfg_setup to prevent led core
    attributes from being written to prior to setup completing.
  - Ensure frame_lock is properly init.
  - Change variable names in RGB functions from frame and zone to f and
    z respectively to fit all scoped_guard actions in 100 columns.
v4: https://lore.kernel.org/linux-input/20260516042841.500299-1-derekjohn.clark@gmail.com/
  - Add msi_suspend/claw_suspend.
  - Reorder claw_remove to cancel all work before removing sysfs.
  - Add mutex lock for removing sysfs attributes.
  - Add mutex lock for MODE command data read/write.
  - Change dev_warn to dev_dbg in claw_profile_event.
  - use __free with DEFINE_FREE macro for argv instead of manually
    running argv_free, cleaining up scoped_guard goto.
  - Fix frame_calc validity check to use >=.
  - Use spinlock instead of mutex in raw_event and related attribute
    _store function.
  - Ensure delayed work is canceled in suspend & canceled before sysfs
    attribute removal.
v3: https://lore.kernel.org/linux-input/20260515033622.2095277-1-derekjohn.clark@gmail.com/
  - Add mutex for read/write if rgb frame data.
  - Ensure claw_hw_output_report is properly guarded.
  - Remove setting rgb_frame_count when reading rgb profiles as it always
    returns garbage data.
  - Ensure rgb_speed is getting drvdata from a valid lookup (not hdev).
  - Use scoped_guard where necessary.
  - Reoder claw_probe to ensure all mutex, completion, and variable
    assignments are in place prior to setting drvdata.
  - Ensure gamepad_mode is set to a valid enum value in claw_probe.
v2: https://lore.kernel.org/linux-input/20260513231445.3213501-1-derekjohn.clark@gmail.com/
  - Use mutexes to guard SYNC_TO_ROM calls and pending_profile calls.
  - Rename driver to hid-msi and add generic entrypoints for
    probe/resume/remove that call claw specific functions in order to
    future proof the driver for other MSI HID interfaces.
  - Fix various bugs and formatting issues.
v1: https://lore.kernel.org/linux-input/20260510043510.442807-1-derekjohn.clark@gmail.com/
Derek J. Clark (4):
  HID: hid-msi: Add MSI Claw configuration driver
  HID: hid-msi: Add M-key mapping attributes
  HID: hid-msi: Add RGB control interface
  HID: hid-msi: Add Rumble Intensity Attributes

 MAINTAINERS           |    6 +
 drivers/hid/Kconfig   |   13 +
 drivers/hid/Makefile  |    1 +
 drivers/hid/hid-ids.h |    5 +
 drivers/hid/hid-msi.c | 1766 +++++++++++++++++++++++++++++++++++++++++
 5 files changed, 1791 insertions(+)
 create mode 100644 drivers/hid/hid-msi.c

-- 
2.53.0


^ 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