* [PATCH v2 0/5] HID: add battery driver for the UNCTRL RAGE Wireless Mouse
@ 2026-10-12 21:13 ` Bishwa Bhusan Saha
0 siblings, 0 replies; 14+ messages in thread
From: Bishwa Bhusan Saha @ 2026-10-12 21:13 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bishwa Bhusan Saha
This patchset adds battery reporting for the UNCTRL RAGE Wireless Mouse.
Parts of the driver are inspired by the Kysona HID driver
(drivers/hid/hid-kysona.c).
It was tested on an UNCTRL RAGE Wireless Mouse.
Signed-off-by: Bishwa Bhusan Saha <contact@bishwasaha.com>
---
Changes in v2:
- restructure the battery handover to fix the races found in review
- register the replacement supply before the previous owner is torn down,
so a failed registration cannot leave the mouse batteryless
- deliver the probe-time query reply with hid_device_io_start()
- schedule the polling on the freezable workqueue, no CPU wake during
suspend
- Link to v1: https://patch.msgid.link/20261013-b4-unctrl-m1-battery-v1-0-4a50203b7177@bishwasaha.com
To: Jiri Kosina <jikos@kernel.org>
To: Benjamin Tissoires <bentiss@kernel.org>
Cc: linux-input@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
---
Bishwa Bhusan Saha (5):
HID: add IDs for UNCTRL RAGE Mouse
HID: UNCTRL: add basic battery reporting for UNCTRL RAGE Mouse
HID: UNCTRL: add periodic battery reporting using a workqueue
HID: UNCTRL: keep a single battery when a mouse has multiple connections
HID: UNCTRL: only report to userspace on battery changes
drivers/hid/Kconfig | 8 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 2 +
drivers/hid/hid-unctrl-m1.c | 436 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 447 insertions(+)
---
base-commit: 145c2b2e9a5c0f794fb4009bcb072ab19f8ccfcd
change-id: 20261013-b4-unctrl-m1-battery-4afbaf17c0ea
Best regards,
--
Bishwa Bhusan Saha <contact@bishwasaha.com>
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH v2 1/5] HID: add IDs for UNCTRL RAGE Mouse
2026-10-12 21:13 ` Bishwa Bhusan Saha
@ 2026-10-12 21:13 ` Bishwa Bhusan Saha
-1 siblings, 0 replies; 14+ messages in thread
From: Bishwa Bhusan Saha via B4 Relay @ 2026-10-12 21:13 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bishwa Bhusan Saha
From: Bishwa Bhusan Saha <contact@bishwasaha.com>
Add the product IDs for both wired and wireless (dongle) mode of
UNCTRL RAGE mouse (also known as UNCTRL M1).
Signed-off-by: Bishwa Bhusan Saha <contact@bishwasaha.com>
---
drivers/hid/hid-ids.h | 2 ++
1 file changed, 2 insertions(+)
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index a9537b7bb..fb4c25c71 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1008,6 +1008,8 @@
#define USB_DEVICE_ID_MADCATZ_RAT5 0x1705
#define USB_DEVICE_ID_MADCATZ_RAT9 0x1709
#define USB_DEVICE_ID_MADCATZ_MMO7 0x1713
+#define USB_DEVICE_ID_UNCTRL_M1_WIRED 0xA09F
+#define USB_DEVICE_ID_UNCTRL_M1_DONGLE 0xA0AF
#define USB_VENDOR_ID_MCC 0x09db
#define USB_DEVICE_ID_MCC_PMD1024LS 0x0076
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH v2 1/5] HID: add IDs for UNCTRL RAGE Mouse
@ 2026-10-12 21:13 ` Bishwa Bhusan Saha
0 siblings, 0 replies; 14+ messages in thread
From: Bishwa Bhusan Saha @ 2026-10-12 21:13 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bishwa Bhusan Saha
Add the product IDs for both wired and wireless (dongle) mode of
UNCTRL RAGE mouse (also known as UNCTRL M1).
Signed-off-by: Bishwa Bhusan Saha <contact@bishwasaha.com>
---
drivers/hid/hid-ids.h | 2 ++
1 file changed, 2 insertions(+)
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index a9537b7bb..fb4c25c71 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1008,6 +1008,8 @@
#define USB_DEVICE_ID_MADCATZ_RAT5 0x1705
#define USB_DEVICE_ID_MADCATZ_RAT9 0x1709
#define USB_DEVICE_ID_MADCATZ_MMO7 0x1713
+#define USB_DEVICE_ID_UNCTRL_M1_WIRED 0xA09F
+#define USB_DEVICE_ID_UNCTRL_M1_DONGLE 0xA0AF
#define USB_VENDOR_ID_MCC 0x09db
#define USB_DEVICE_ID_MCC_PMD1024LS 0x0076
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH v2 2/5] HID: UNCTRL: add basic battery reporting for UNCTRL RAGE Mouse
2026-10-12 21:13 ` Bishwa Bhusan Saha
@ 2026-10-12 21:13 ` Bishwa Bhusan Saha
-1 siblings, 0 replies; 14+ messages in thread
From: Bishwa Bhusan Saha via B4 Relay @ 2026-10-12 21:13 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bishwa Bhusan Saha
From: Bishwa Bhusan Saha <contact@bishwasaha.com>
This initial patch will probe the battery only once. A later patch
will add periodic checking.
Signed-off-by: Bishwa Bhusan Saha <contact@bishwasaha.com>
Assisted-by: GLM:glm-5.3
---
drivers/hid/Kconfig | 8 ++
drivers/hid/Makefile | 1 +
drivers/hid/hid-unctrl-m1.c | 320 ++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 329 insertions(+)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 0e3a0ccd6..f3730c94f 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1362,6 +1362,14 @@ config HID_PULSAR
Additional supported devices: Kysona M600, ATK VXE R1 SE+
+config HID_UNCTRL_M1
+ tristate "UNCTRL RAGE Wireless Mouse battery support"
+ depends on USB_HID
+ select POWER_SUPPLY
+ help
+ Report the battery status of the UNCTRL RAGE Wireless Mouse
+ over its 2.4 GHz receiver and while wired.
+
config HID_WACOM
tristate "Wacom Intuos/Graphire tablet support (USB)"
depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 79384d905..8a9d00a98 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -150,6 +150,7 @@ hid-uclogic-objs := hid-uclogic-core.o \
hid-uclogic-params.o
obj-$(CONFIG_HID_UCLOGIC) += hid-uclogic.o
obj-$(CONFIG_HID_UDRAW_PS3) += hid-udraw-ps3.o
+obj-$(CONFIG_HID_UNCTRL_M1) += hid-unctrl-m1.o
obj-$(CONFIG_HID_UNIVERSAL_PIDFF) += hid-universal-pidff.o
obj-$(CONFIG_HID_LED) += hid-led.o
obj-$(CONFIG_HID_XIAOMI) += hid-xiaomi.o
diff --git a/drivers/hid/hid-unctrl-m1.c b/drivers/hid/hid-unctrl-m1.c
new file mode 100644
index 000000000..3896468ad
--- /dev/null
+++ b/drivers/hid/hid-unctrl-m1.c
@@ -0,0 +1,320 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID battery driver for the UNCTRL RAGE Wireless Mouse (model MSPTM101,
+ * known as "UNCTRL M1" over Bluetooth. Mad Catz, receiver 0xA0AF,
+ * wired 0xA09F). The mouse also connects over Bluetooth, on which no
+ * battery queries are made.
+ *
+ * The same mouse appears as two USB devices at once when it is both
+ * docked on the cable and linked to the receiver. Without care that would
+ * expose two batteries for one mouse, and only the cable connection
+ * actually reaches it, so the driver keeps a single power supply and
+ * hands it to whichever connection can answer queries.
+ *
+ * The device echoes every query before answering. The mode byte in the
+ * reply tells the answer from the echoes. The wired connection only
+ * answers over the interrupt out endpoint, where the queries go.
+ */
+
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/power_supply.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+
+#include "hid-ids.h"
+
+#define UNCTRL_VENDOR_IFNUM 2
+
+#define UNCTRL_MODE_RECEIVER 0x01
+#define UNCTRL_MODE_WIRED 0x44
+
+/* the battery query: report number, 4-byte header, padding, terminator */
+static const u8 unctrl_battery_query[33] = {
+ 0x00,
+ 0x0A, 0xAA, 0xFF, 0xDD,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00,
+ 0x6F,
+};
+
+struct unctrl_dev {
+ struct hid_device *hdev;
+
+ struct power_supply_desc battery_desc;
+ struct power_supply *battery;
+
+ u8 battery_capacity;
+ u8 mode;
+ bool online;
+ bool wired;
+};
+
+static enum power_supply_property unctrl_battery_props[] = {
+ POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_PRESENT,
+ POWER_SUPPLY_PROP_CAPACITY, POWER_SUPPLY_PROP_SCOPE,
+ POWER_SUPPLY_PROP_MODEL_NAME, POWER_SUPPLY_PROP_SERIAL_NUMBER,
+ POWER_SUPPLY_PROP_ONLINE,
+};
+
+static int unctrl_battery_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct unctrl_dev *dev = power_supply_get_drvdata(psy);
+ int ret = 0;
+ u8 capacity, mode;
+ bool online;
+
+ capacity = dev->battery_capacity;
+ mode = dev->mode;
+ online = dev->online;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_PRESENT:
+ val->intval = 1;
+ break;
+ case POWER_SUPPLY_PROP_ONLINE:
+ val->intval = online;
+ break;
+ case POWER_SUPPLY_PROP_STATUS:
+ if (!online)
+ val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+ else if (mode == UNCTRL_MODE_WIRED && capacity < 100)
+ val->intval = POWER_SUPPLY_STATUS_CHARGING;
+ else if (capacity >= 100)
+ val->intval = POWER_SUPPLY_STATUS_FULL;
+ else
+ val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
+ break;
+ case POWER_SUPPLY_PROP_SCOPE:
+ val->intval = POWER_SUPPLY_SCOPE_DEVICE;
+ break;
+ case POWER_SUPPLY_PROP_CAPACITY:
+ val->intval = capacity;
+ break;
+ case POWER_SUPPLY_PROP_MODEL_NAME:
+ val->strval = dev->hdev->name;
+ break;
+ case POWER_SUPPLY_PROP_SERIAL_NUMBER:
+ val->strval = "unctrl-m1";
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+ return ret;
+}
+
+static void unctrl_fetch_battery(struct hid_device *hdev)
+{
+ u8 *write_buf;
+ int ret;
+
+ /* buf[0] is the report number, and hid_hw_output_report() sends the
+ * buffer verbatim.
+ */
+ write_buf = kmemdup(unctrl_battery_query, sizeof(unctrl_battery_query),
+ GFP_KERNEL);
+ if (!write_buf)
+ return;
+
+ ret = hid_hw_output_report(hdev, write_buf,
+ sizeof(unctrl_battery_query));
+ if (ret < 0)
+ hid_dbg(hdev, "%s:battery query failed with error %d\n",
+ __func__, ret);
+
+ kfree(write_buf);
+}
+
+/* Register the power supply of this connection and start reporting. */
+static int unctrl_battery_register(struct unctrl_dev *dev)
+{
+ struct power_supply_config config = { .drv_data = dev };
+ struct power_supply *battery;
+ int ret;
+
+ battery = power_supply_register(&dev->hdev->dev, &dev->battery_desc,
+ &config);
+ if (IS_ERR(battery)) {
+ ret = PTR_ERR(battery);
+ hid_err(dev->hdev,
+ "%s:power_supply_register failed with error %d\n",
+ __func__, ret);
+ return ret;
+ }
+
+ power_supply_powers(battery, &dev->hdev->dev);
+ dev->battery = battery;
+ hid_info(dev->hdev, "%s:battery registered as %s\n", __func__,
+ dev->battery_desc.name);
+
+ unctrl_fetch_battery(dev->hdev);
+
+ return 0;
+}
+
+static int unctrl_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ struct unctrl_dev *dev;
+ struct usb_interface *usbif;
+ unsigned int connect_mask;
+ bool vendor;
+ int ret;
+
+ if (!hid_is_usb(hdev))
+ return -EINVAL;
+
+ usbif = to_usb_interface(hdev->dev.parent);
+ vendor = usbif->cur_altsetting->desc.bInterfaceNumber ==
+ UNCTRL_VENDOR_IFNUM;
+
+ /* the pointer, buttons and hidraw have to keep working */
+ connect_mask = vendor ? HID_CONNECT_HIDRAW : HID_CONNECT_DEFAULT;
+
+ ret = hid_parse(hdev);
+ if (ret) {
+ hid_err(hdev, "%s:parse failed with error %d\n", __func__, ret);
+ return ret;
+ }
+
+ ret = hid_hw_start(hdev, connect_mask);
+ if (ret) {
+ hid_err(hdev, "%s:hw start failed with error %d\n", __func__,
+ ret);
+ return ret;
+ }
+
+ if (!vendor)
+ return 0;
+
+ dev = kzalloc_obj(*dev);
+ if (!dev) {
+ hid_hw_stop(hdev);
+ return -ENOMEM;
+ }
+
+ dev->hdev = hdev;
+ dev->battery_capacity = 100;
+ dev->mode = UNCTRL_MODE_RECEIVER;
+ dev->wired = hdev->product == USB_DEVICE_ID_UNCTRL_M1_WIRED;
+
+ dev->battery_desc.name = devm_kasprintf(&hdev->dev, GFP_KERNEL,
+ "unctrl-m1-%04x-battery",
+ hdev->product);
+ if (!dev->battery_desc.name) {
+ hid_hw_stop(hdev);
+ kfree(dev);
+ return -ENOMEM;
+ }
+ dev->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
+ dev->battery_desc.properties = unctrl_battery_props;
+ dev->battery_desc.num_properties = ARRAY_SIZE(unctrl_battery_props);
+ dev->battery_desc.get_property = unctrl_battery_get_property;
+ dev->battery_desc.use_for_apm = 0;
+
+ /* usbhid polls the interrupt in endpoint only while it is open */
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_err(hdev, "%s:open failed with error %d\n", __func__, ret);
+ hid_hw_stop(hdev);
+ kfree(dev);
+ return ret;
+ }
+
+ /* the reply to the probe-time query may arrive before probe
+ * returns, when the core still gates the input path
+ */
+ hid_device_io_start(hdev);
+
+ hid_set_drvdata(hdev, dev);
+
+ ret = unctrl_battery_register(dev);
+ if (ret)
+ goto err_del;
+
+ return 0;
+
+err_del:
+ hid_device_io_stop(hdev);
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+ hid_set_drvdata(hdev, NULL);
+ kfree(dev);
+ return ret;
+}
+
+static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct unctrl_dev *dev = hid_get_drvdata(hdev);
+ struct power_supply *battery = NULL;
+
+ if (!dev || size < 8)
+ return 0;
+
+ if (data[0] != 0x0A || data[1] != 0xAA || data[2] != 0xFF ||
+ data[3] != 0xDD)
+ return 0;
+
+ /* real answers carry the mode here, echoes of the query do not */
+ if (data[5] != UNCTRL_MODE_RECEIVER && data[5] != UNCTRL_MODE_WIRED)
+ return 0;
+
+ /* the receiver reports 0% while the mouse is switched off */
+ dev->battery_capacity = min_t(u8, data[4], 100);
+ dev->mode = data[5];
+ dev->online = dev->battery_capacity != 0;
+ battery = dev->battery;
+
+ /* only schedules a work, so this is safe from the completion path */
+ if (battery)
+ power_supply_changed(battery);
+
+ return 0;
+}
+
+static void unctrl_remove(struct hid_device *hdev)
+{
+ struct unctrl_dev *dev = hid_get_drvdata(hdev);
+
+ hid_hw_close(hdev);
+
+ hid_hw_stop(hdev);
+
+ if (!dev)
+ return;
+
+ if (dev->battery)
+ power_supply_unregister(dev->battery);
+
+ hid_set_drvdata(hdev, NULL);
+ kfree(dev);
+}
+
+static const struct hid_device_id unctrl_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_MADCATZ,
+ USB_DEVICE_ID_UNCTRL_M1_DONGLE) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MADCATZ,
+ USB_DEVICE_ID_UNCTRL_M1_WIRED) },
+ {}
+};
+MODULE_DEVICE_TABLE(hid, unctrl_devices);
+
+static struct hid_driver unctrl_driver = {
+ .name = "unctrl-m1",
+ .id_table = unctrl_devices,
+ .probe = unctrl_probe,
+ .raw_event = unctrl_raw_event,
+ .remove = unctrl_remove,
+};
+module_hid_driver(unctrl_driver);
+
+MODULE_AUTHOR("Bishwa Bhusan Saha <contact@bishwasaha.com>");
+MODULE_DESCRIPTION("HID battery driver for the UNCTRL M1 Mouse");
+MODULE_LICENSE("GPL");
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH v2 2/5] HID: UNCTRL: add basic battery reporting for UNCTRL RAGE Mouse
@ 2026-10-12 21:13 ` Bishwa Bhusan Saha
0 siblings, 0 replies; 14+ messages in thread
From: Bishwa Bhusan Saha @ 2026-10-12 21:13 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bishwa Bhusan Saha
This initial patch will probe the battery only once. A later patch
will add periodic checking.
Signed-off-by: Bishwa Bhusan Saha <contact@bishwasaha.com>
Assisted-by: GLM:glm-5.3
---
drivers/hid/Kconfig | 8 ++
drivers/hid/Makefile | 1 +
drivers/hid/hid-unctrl-m1.c | 320 ++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 329 insertions(+)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 0e3a0ccd6..f3730c94f 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1362,6 +1362,14 @@ config HID_PULSAR
Additional supported devices: Kysona M600, ATK VXE R1 SE+
+config HID_UNCTRL_M1
+ tristate "UNCTRL RAGE Wireless Mouse battery support"
+ depends on USB_HID
+ select POWER_SUPPLY
+ help
+ Report the battery status of the UNCTRL RAGE Wireless Mouse
+ over its 2.4 GHz receiver and while wired.
+
config HID_WACOM
tristate "Wacom Intuos/Graphire tablet support (USB)"
depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 79384d905..8a9d00a98 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -150,6 +150,7 @@ hid-uclogic-objs := hid-uclogic-core.o \
hid-uclogic-params.o
obj-$(CONFIG_HID_UCLOGIC) += hid-uclogic.o
obj-$(CONFIG_HID_UDRAW_PS3) += hid-udraw-ps3.o
+obj-$(CONFIG_HID_UNCTRL_M1) += hid-unctrl-m1.o
obj-$(CONFIG_HID_UNIVERSAL_PIDFF) += hid-universal-pidff.o
obj-$(CONFIG_HID_LED) += hid-led.o
obj-$(CONFIG_HID_XIAOMI) += hid-xiaomi.o
diff --git a/drivers/hid/hid-unctrl-m1.c b/drivers/hid/hid-unctrl-m1.c
new file mode 100644
index 000000000..3896468ad
--- /dev/null
+++ b/drivers/hid/hid-unctrl-m1.c
@@ -0,0 +1,320 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID battery driver for the UNCTRL RAGE Wireless Mouse (model MSPTM101,
+ * known as "UNCTRL M1" over Bluetooth. Mad Catz, receiver 0xA0AF,
+ * wired 0xA09F). The mouse also connects over Bluetooth, on which no
+ * battery queries are made.
+ *
+ * The same mouse appears as two USB devices at once when it is both
+ * docked on the cable and linked to the receiver. Without care that would
+ * expose two batteries for one mouse, and only the cable connection
+ * actually reaches it, so the driver keeps a single power supply and
+ * hands it to whichever connection can answer queries.
+ *
+ * The device echoes every query before answering. The mode byte in the
+ * reply tells the answer from the echoes. The wired connection only
+ * answers over the interrupt out endpoint, where the queries go.
+ */
+
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/power_supply.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+
+#include "hid-ids.h"
+
+#define UNCTRL_VENDOR_IFNUM 2
+
+#define UNCTRL_MODE_RECEIVER 0x01
+#define UNCTRL_MODE_WIRED 0x44
+
+/* the battery query: report number, 4-byte header, padding, terminator */
+static const u8 unctrl_battery_query[33] = {
+ 0x00,
+ 0x0A, 0xAA, 0xFF, 0xDD,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00,
+ 0x6F,
+};
+
+struct unctrl_dev {
+ struct hid_device *hdev;
+
+ struct power_supply_desc battery_desc;
+ struct power_supply *battery;
+
+ u8 battery_capacity;
+ u8 mode;
+ bool online;
+ bool wired;
+};
+
+static enum power_supply_property unctrl_battery_props[] = {
+ POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_PRESENT,
+ POWER_SUPPLY_PROP_CAPACITY, POWER_SUPPLY_PROP_SCOPE,
+ POWER_SUPPLY_PROP_MODEL_NAME, POWER_SUPPLY_PROP_SERIAL_NUMBER,
+ POWER_SUPPLY_PROP_ONLINE,
+};
+
+static int unctrl_battery_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct unctrl_dev *dev = power_supply_get_drvdata(psy);
+ int ret = 0;
+ u8 capacity, mode;
+ bool online;
+
+ capacity = dev->battery_capacity;
+ mode = dev->mode;
+ online = dev->online;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_PRESENT:
+ val->intval = 1;
+ break;
+ case POWER_SUPPLY_PROP_ONLINE:
+ val->intval = online;
+ break;
+ case POWER_SUPPLY_PROP_STATUS:
+ if (!online)
+ val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+ else if (mode == UNCTRL_MODE_WIRED && capacity < 100)
+ val->intval = POWER_SUPPLY_STATUS_CHARGING;
+ else if (capacity >= 100)
+ val->intval = POWER_SUPPLY_STATUS_FULL;
+ else
+ val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
+ break;
+ case POWER_SUPPLY_PROP_SCOPE:
+ val->intval = POWER_SUPPLY_SCOPE_DEVICE;
+ break;
+ case POWER_SUPPLY_PROP_CAPACITY:
+ val->intval = capacity;
+ break;
+ case POWER_SUPPLY_PROP_MODEL_NAME:
+ val->strval = dev->hdev->name;
+ break;
+ case POWER_SUPPLY_PROP_SERIAL_NUMBER:
+ val->strval = "unctrl-m1";
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+ return ret;
+}
+
+static void unctrl_fetch_battery(struct hid_device *hdev)
+{
+ u8 *write_buf;
+ int ret;
+
+ /* buf[0] is the report number, and hid_hw_output_report() sends the
+ * buffer verbatim.
+ */
+ write_buf = kmemdup(unctrl_battery_query, sizeof(unctrl_battery_query),
+ GFP_KERNEL);
+ if (!write_buf)
+ return;
+
+ ret = hid_hw_output_report(hdev, write_buf,
+ sizeof(unctrl_battery_query));
+ if (ret < 0)
+ hid_dbg(hdev, "%s:battery query failed with error %d\n",
+ __func__, ret);
+
+ kfree(write_buf);
+}
+
+/* Register the power supply of this connection and start reporting. */
+static int unctrl_battery_register(struct unctrl_dev *dev)
+{
+ struct power_supply_config config = { .drv_data = dev };
+ struct power_supply *battery;
+ int ret;
+
+ battery = power_supply_register(&dev->hdev->dev, &dev->battery_desc,
+ &config);
+ if (IS_ERR(battery)) {
+ ret = PTR_ERR(battery);
+ hid_err(dev->hdev,
+ "%s:power_supply_register failed with error %d\n",
+ __func__, ret);
+ return ret;
+ }
+
+ power_supply_powers(battery, &dev->hdev->dev);
+ dev->battery = battery;
+ hid_info(dev->hdev, "%s:battery registered as %s\n", __func__,
+ dev->battery_desc.name);
+
+ unctrl_fetch_battery(dev->hdev);
+
+ return 0;
+}
+
+static int unctrl_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ struct unctrl_dev *dev;
+ struct usb_interface *usbif;
+ unsigned int connect_mask;
+ bool vendor;
+ int ret;
+
+ if (!hid_is_usb(hdev))
+ return -EINVAL;
+
+ usbif = to_usb_interface(hdev->dev.parent);
+ vendor = usbif->cur_altsetting->desc.bInterfaceNumber ==
+ UNCTRL_VENDOR_IFNUM;
+
+ /* the pointer, buttons and hidraw have to keep working */
+ connect_mask = vendor ? HID_CONNECT_HIDRAW : HID_CONNECT_DEFAULT;
+
+ ret = hid_parse(hdev);
+ if (ret) {
+ hid_err(hdev, "%s:parse failed with error %d\n", __func__, ret);
+ return ret;
+ }
+
+ ret = hid_hw_start(hdev, connect_mask);
+ if (ret) {
+ hid_err(hdev, "%s:hw start failed with error %d\n", __func__,
+ ret);
+ return ret;
+ }
+
+ if (!vendor)
+ return 0;
+
+ dev = kzalloc_obj(*dev);
+ if (!dev) {
+ hid_hw_stop(hdev);
+ return -ENOMEM;
+ }
+
+ dev->hdev = hdev;
+ dev->battery_capacity = 100;
+ dev->mode = UNCTRL_MODE_RECEIVER;
+ dev->wired = hdev->product == USB_DEVICE_ID_UNCTRL_M1_WIRED;
+
+ dev->battery_desc.name = devm_kasprintf(&hdev->dev, GFP_KERNEL,
+ "unctrl-m1-%04x-battery",
+ hdev->product);
+ if (!dev->battery_desc.name) {
+ hid_hw_stop(hdev);
+ kfree(dev);
+ return -ENOMEM;
+ }
+ dev->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
+ dev->battery_desc.properties = unctrl_battery_props;
+ dev->battery_desc.num_properties = ARRAY_SIZE(unctrl_battery_props);
+ dev->battery_desc.get_property = unctrl_battery_get_property;
+ dev->battery_desc.use_for_apm = 0;
+
+ /* usbhid polls the interrupt in endpoint only while it is open */
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_err(hdev, "%s:open failed with error %d\n", __func__, ret);
+ hid_hw_stop(hdev);
+ kfree(dev);
+ return ret;
+ }
+
+ /* the reply to the probe-time query may arrive before probe
+ * returns, when the core still gates the input path
+ */
+ hid_device_io_start(hdev);
+
+ hid_set_drvdata(hdev, dev);
+
+ ret = unctrl_battery_register(dev);
+ if (ret)
+ goto err_del;
+
+ return 0;
+
+err_del:
+ hid_device_io_stop(hdev);
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+ hid_set_drvdata(hdev, NULL);
+ kfree(dev);
+ return ret;
+}
+
+static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct unctrl_dev *dev = hid_get_drvdata(hdev);
+ struct power_supply *battery = NULL;
+
+ if (!dev || size < 8)
+ return 0;
+
+ if (data[0] != 0x0A || data[1] != 0xAA || data[2] != 0xFF ||
+ data[3] != 0xDD)
+ return 0;
+
+ /* real answers carry the mode here, echoes of the query do not */
+ if (data[5] != UNCTRL_MODE_RECEIVER && data[5] != UNCTRL_MODE_WIRED)
+ return 0;
+
+ /* the receiver reports 0% while the mouse is switched off */
+ dev->battery_capacity = min_t(u8, data[4], 100);
+ dev->mode = data[5];
+ dev->online = dev->battery_capacity != 0;
+ battery = dev->battery;
+
+ /* only schedules a work, so this is safe from the completion path */
+ if (battery)
+ power_supply_changed(battery);
+
+ return 0;
+}
+
+static void unctrl_remove(struct hid_device *hdev)
+{
+ struct unctrl_dev *dev = hid_get_drvdata(hdev);
+
+ hid_hw_close(hdev);
+
+ hid_hw_stop(hdev);
+
+ if (!dev)
+ return;
+
+ if (dev->battery)
+ power_supply_unregister(dev->battery);
+
+ hid_set_drvdata(hdev, NULL);
+ kfree(dev);
+}
+
+static const struct hid_device_id unctrl_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_MADCATZ,
+ USB_DEVICE_ID_UNCTRL_M1_DONGLE) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MADCATZ,
+ USB_DEVICE_ID_UNCTRL_M1_WIRED) },
+ {}
+};
+MODULE_DEVICE_TABLE(hid, unctrl_devices);
+
+static struct hid_driver unctrl_driver = {
+ .name = "unctrl-m1",
+ .id_table = unctrl_devices,
+ .probe = unctrl_probe,
+ .raw_event = unctrl_raw_event,
+ .remove = unctrl_remove,
+};
+module_hid_driver(unctrl_driver);
+
+MODULE_AUTHOR("Bishwa Bhusan Saha <contact@bishwasaha.com>");
+MODULE_DESCRIPTION("HID battery driver for the UNCTRL M1 Mouse");
+MODULE_LICENSE("GPL");
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* Re: [PATCH v2 2/5] HID: UNCTRL: add basic battery reporting for UNCTRL RAGE Mouse
2026-10-12 21:13 ` Bishwa Bhusan Saha
(?)
@ 2026-10-12 21:20 ` sashiko-bot
-1 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-12 21:20 UTC (permalink / raw)
To: Bishwa Bhusan Saha; +Cc: dmitry.torokhov, linux-input
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] hid: unbalanced hid_hw_close() in unctrl_remove() for non-vendor interfaces
--
commit 34f6450fbc06f0259d8e3dc8b22b5c1567b8c302
Author: Bishwa Bhusan Saha <contact@bishwasaha.com>
HID: UNCTRL: add basic battery reporting for UNCTRL RAGE Mouse
This initial patch will probe the battery only once. A later patch
will add periodic checking.
> diff --git a/drivers/hid/hid-unctrl-m1.c b/drivers/hid/hid-unctrl-m1.c
> new file mode 100644
> index 0000000000000..3896468adf77f
> --- /dev/null
> +++ b/drivers/hid/hid-unctrl-m1.c
[ ... ]
> +static int unctrl_probe(struct hid_device *hdev, const struct hid_device_id *id)
> +{
[ ... ]
> + ret = hid_hw_start(hdev, connect_mask);
> + if (ret) {
> + hid_err(hdev, "%s:hw start failed with error %d\n", __func__,
> + ret);
> + return ret;
> + }
> +
> + if (!vendor)
> + return 0;
> +
> + dev = kzalloc_obj(*dev);
[ ... ]
> + /* usbhid polls the interrupt in endpoint only while it is open */
> + ret = hid_hw_open(hdev);
[ ... ]
> +static void unctrl_remove(struct hid_device *hdev)
> +{
> + struct unctrl_dev *dev = hid_get_drvdata(hdev);
> +
> + hid_hw_close(hdev);
[Severity: High]
Does this code cause a hardware reference count underflow for non-vendor
interfaces?
When the driver binds to a non-vendor interface, unctrl_probe() correctly
returns 0 early without calling hid_hw_open() or allocating the private
dev struct. However, upon driver unbind, unctrl_remove() unconditionally
calls hid_hw_close() before checking if dev is NULL.
Any hot-unplug, module unload, or manual driver unbind of a non-vendor
interface will trigger this unbalanced close, underflowing the
hdev->ll_open_count. This corrupts the transport open state and prevents
the device from being properly opened subsequently (e.g., by hidraw or
hidinput).
> +
> + hid_hw_stop(hdev);
> +
> + if (!dev)
> + return;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261013-b4-unctrl-m1-battery-v2-0-bd6752021753@bishwasaha.com?part=2
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v2 3/5] HID: UNCTRL: add periodic battery reporting using a workqueue
2026-10-12 21:13 ` Bishwa Bhusan Saha
@ 2026-10-12 21:13 ` Bishwa Bhusan Saha
-1 siblings, 0 replies; 14+ messages in thread
From: Bishwa Bhusan Saha via B4 Relay @ 2026-10-12 21:13 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bishwa Bhusan Saha
From: Bishwa Bhusan Saha <contact@bishwasaha.com>
This patch uses a workqueue to check battery status every 5 seconds.
Signed-off-by: Bishwa Bhusan Saha <contact@bishwasaha.com>
Assisted-by: GLM:glm-5.3
---
drivers/hid/hid-unctrl-m1.c | 26 +++++++++++++++++++++++++-
1 file changed, 25 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-unctrl-m1.c b/drivers/hid/hid-unctrl-m1.c
index 3896468ad..d7e2f207e 100644
--- a/drivers/hid/hid-unctrl-m1.c
+++ b/drivers/hid/hid-unctrl-m1.c
@@ -22,11 +22,14 @@
#include <linux/power_supply.h>
#include <linux/slab.h>
#include <linux/usb.h>
+#include <linux/workqueue.h>
#include "hid-ids.h"
#define UNCTRL_VENDOR_IFNUM 2
+#define UNCTRL_BATTERY_TIMEOUT_MS 5000
+
#define UNCTRL_MODE_RECEIVER 0x01
#define UNCTRL_MODE_WIRED 0x44
@@ -47,6 +50,8 @@ struct unctrl_dev {
struct power_supply_desc battery_desc;
struct power_supply *battery;
+ struct delayed_work battery_work;
+
u8 battery_capacity;
u8 mode;
bool online;
@@ -132,6 +137,17 @@ static void unctrl_fetch_battery(struct hid_device *hdev)
kfree(write_buf);
}
+static void unctrl_battery_timer_tick(struct work_struct *work)
+{
+ struct unctrl_dev *dev =
+ container_of(work, struct unctrl_dev, battery_work.work);
+
+ unctrl_fetch_battery(dev->hdev);
+
+ queue_delayed_work(system_freezable_wq, &dev->battery_work,
+ msecs_to_jiffies(UNCTRL_BATTERY_TIMEOUT_MS));
+}
+
/* Register the power supply of this connection and start reporting. */
static int unctrl_battery_register(struct unctrl_dev *dev)
{
@@ -155,6 +171,8 @@ static int unctrl_battery_register(struct unctrl_dev *dev)
dev->battery_desc.name);
unctrl_fetch_battery(dev->hdev);
+ queue_delayed_work(system_freezable_wq, &dev->battery_work,
+ msecs_to_jiffies(UNCTRL_BATTERY_TIMEOUT_MS));
return 0;
}
@@ -200,6 +218,8 @@ static int unctrl_probe(struct hid_device *hdev, const struct hid_device_id *id)
}
dev->hdev = hdev;
+ INIT_DELAYED_WORK(&dev->battery_work, unctrl_battery_timer_tick);
+
dev->battery_capacity = 100;
dev->mode = UNCTRL_MODE_RECEIVER;
dev->wired = hdev->product == USB_DEVICE_ID_UNCTRL_M1_WIRED;
@@ -283,7 +303,11 @@ static void unctrl_remove(struct hid_device *hdev)
{
struct unctrl_dev *dev = hid_get_drvdata(hdev);
- hid_hw_close(hdev);
+ /* Stop polling before the transport goes down. */
+ if (dev) {
+ cancel_delayed_work_sync(&dev->battery_work);
+ hid_hw_close(hdev);
+ }
hid_hw_stop(hdev);
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH v2 3/5] HID: UNCTRL: add periodic battery reporting using a workqueue
@ 2026-10-12 21:13 ` Bishwa Bhusan Saha
0 siblings, 0 replies; 14+ messages in thread
From: Bishwa Bhusan Saha @ 2026-10-12 21:13 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bishwa Bhusan Saha
This patch uses a workqueue to check battery status every 5 seconds.
Signed-off-by: Bishwa Bhusan Saha <contact@bishwasaha.com>
Assisted-by: GLM:glm-5.3
---
drivers/hid/hid-unctrl-m1.c | 26 +++++++++++++++++++++++++-
1 file changed, 25 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-unctrl-m1.c b/drivers/hid/hid-unctrl-m1.c
index 3896468ad..d7e2f207e 100644
--- a/drivers/hid/hid-unctrl-m1.c
+++ b/drivers/hid/hid-unctrl-m1.c
@@ -22,11 +22,14 @@
#include <linux/power_supply.h>
#include <linux/slab.h>
#include <linux/usb.h>
+#include <linux/workqueue.h>
#include "hid-ids.h"
#define UNCTRL_VENDOR_IFNUM 2
+#define UNCTRL_BATTERY_TIMEOUT_MS 5000
+
#define UNCTRL_MODE_RECEIVER 0x01
#define UNCTRL_MODE_WIRED 0x44
@@ -47,6 +50,8 @@ struct unctrl_dev {
struct power_supply_desc battery_desc;
struct power_supply *battery;
+ struct delayed_work battery_work;
+
u8 battery_capacity;
u8 mode;
bool online;
@@ -132,6 +137,17 @@ static void unctrl_fetch_battery(struct hid_device *hdev)
kfree(write_buf);
}
+static void unctrl_battery_timer_tick(struct work_struct *work)
+{
+ struct unctrl_dev *dev =
+ container_of(work, struct unctrl_dev, battery_work.work);
+
+ unctrl_fetch_battery(dev->hdev);
+
+ queue_delayed_work(system_freezable_wq, &dev->battery_work,
+ msecs_to_jiffies(UNCTRL_BATTERY_TIMEOUT_MS));
+}
+
/* Register the power supply of this connection and start reporting. */
static int unctrl_battery_register(struct unctrl_dev *dev)
{
@@ -155,6 +171,8 @@ static int unctrl_battery_register(struct unctrl_dev *dev)
dev->battery_desc.name);
unctrl_fetch_battery(dev->hdev);
+ queue_delayed_work(system_freezable_wq, &dev->battery_work,
+ msecs_to_jiffies(UNCTRL_BATTERY_TIMEOUT_MS));
return 0;
}
@@ -200,6 +218,8 @@ static int unctrl_probe(struct hid_device *hdev, const struct hid_device_id *id)
}
dev->hdev = hdev;
+ INIT_DELAYED_WORK(&dev->battery_work, unctrl_battery_timer_tick);
+
dev->battery_capacity = 100;
dev->mode = UNCTRL_MODE_RECEIVER;
dev->wired = hdev->product == USB_DEVICE_ID_UNCTRL_M1_WIRED;
@@ -283,7 +303,11 @@ static void unctrl_remove(struct hid_device *hdev)
{
struct unctrl_dev *dev = hid_get_drvdata(hdev);
- hid_hw_close(hdev);
+ /* Stop polling before the transport goes down. */
+ if (dev) {
+ cancel_delayed_work_sync(&dev->battery_work);
+ hid_hw_close(hdev);
+ }
hid_hw_stop(hdev);
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH v2 4/5] HID: UNCTRL: keep a single battery when a mouse has multiple connections
2026-10-12 21:13 ` Bishwa Bhusan Saha
@ 2026-10-12 21:13 ` Bishwa Bhusan Saha
-1 siblings, 0 replies; 14+ messages in thread
From: Bishwa Bhusan Saha via B4 Relay @ 2026-10-12 21:13 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bishwa Bhusan Saha
From: Bishwa Bhusan Saha <contact@bishwasaha.com>
The UNCTRL RAGE mouse can be connected over its 2.4 GHz dongle and
its wired connection at the same time. Both are visible as separate
devices on lsusb. Without this patch two separate batteries would be
detected in case of a single mouse connected over 2 interfaces. This
patch solves it by registering the power supply only on the connection
that can answer queries and taking it away when that connection goes
away.
Signed-off-by: Bishwa Bhusan Saha <contact@bishwasaha.com>
Assisted-by: GLM:glm-5.3
---
drivers/hid/hid-unctrl-m1.c | 139 +++++++++++++++++++++++++++++++++++---------
1 file changed, 111 insertions(+), 28 deletions(-)
diff --git a/drivers/hid/hid-unctrl-m1.c b/drivers/hid/hid-unctrl-m1.c
index d7e2f207e..624ac1365 100644
--- a/drivers/hid/hid-unctrl-m1.c
+++ b/drivers/hid/hid-unctrl-m1.c
@@ -16,9 +16,12 @@
* answers over the interrupt out endpoint, where the queries go.
*/
+#include <linux/cleanup.h>
#include <linux/device.h>
#include <linux/hid.h>
+#include <linux/list.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/power_supply.h>
#include <linux/slab.h>
#include <linux/usb.h>
@@ -45,19 +48,27 @@ static const u8 unctrl_battery_query[33] = {
};
struct unctrl_dev {
+ struct list_head list;
struct hid_device *hdev;
struct power_supply_desc battery_desc;
struct power_supply *battery;
+ /* raw_event runs in the completion path, so this is a spinlock. */
+ spinlock_t lock;
+
struct delayed_work battery_work;
u8 battery_capacity;
u8 mode;
bool online;
bool wired;
+ bool vendor;
};
+static LIST_HEAD(unctrl_connections);
+static DEFINE_MUTEX(unctrl_lock);
+
static enum power_supply_property unctrl_battery_props[] = {
POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_PRESENT,
POWER_SUPPLY_PROP_CAPACITY, POWER_SUPPLY_PROP_SCOPE,
@@ -74,9 +85,11 @@ static int unctrl_battery_get_property(struct power_supply *psy,
u8 capacity, mode;
bool online;
- capacity = dev->battery_capacity;
- mode = dev->mode;
- online = dev->online;
+ scoped_guard(spinlock_irqsave, &dev->lock) {
+ capacity = dev->battery_capacity;
+ mode = dev->mode;
+ online = dev->online;
+ }
switch (psp) {
case POWER_SUPPLY_PROP_PRESENT:
@@ -148,13 +161,34 @@ static void unctrl_battery_timer_tick(struct work_struct *work)
msecs_to_jiffies(UNCTRL_BATTERY_TIMEOUT_MS));
}
-/* Register the power supply of this connection and start reporting. */
+static struct unctrl_dev *unctrl_battery_owner(void)
+{
+ struct unctrl_dev *dev;
+
+ list_for_each_entry(dev, &unctrl_connections, list) {
+ if (dev->battery)
+ return dev;
+ }
+
+ return NULL;
+}
+
+/*
+ * Register the power supply on the connection the mouse talks over,
+ * a wired connection takes it away from a receiver. Called with
+ * unctrl_lock held.
+ */
static int unctrl_battery_register(struct unctrl_dev *dev)
{
struct power_supply_config config = { .drv_data = dev };
+ struct unctrl_dev *owner;
struct power_supply *battery;
int ret;
+ owner = unctrl_battery_owner();
+ if (owner == dev || (owner && (!dev->wired || owner->wired)))
+ return 0;
+
battery = power_supply_register(&dev->hdev->dev, &dev->battery_desc,
&config);
if (IS_ERR(battery)) {
@@ -166,10 +200,24 @@ static int unctrl_battery_register(struct unctrl_dev *dev)
}
power_supply_powers(battery, &dev->hdev->dev);
- dev->battery = battery;
+ scoped_guard(spinlock_irqsave, &dev->lock)
+ dev->battery = battery;
hid_info(dev->hdev, "%s:battery registered as %s\n", __func__,
dev->battery_desc.name);
+ /* the takeover cannot fail now, our supply already exists */
+ if (owner) {
+ struct power_supply *owner_battery;
+
+ cancel_delayed_work_sync(&owner->battery_work);
+ /* so a raw_event cannot touch the supply being unregistered */
+ scoped_guard(spinlock_irqsave, &owner->lock) {
+ owner_battery = owner->battery;
+ owner->battery = NULL;
+ }
+ power_supply_unregister(owner_battery);
+ }
+
unctrl_fetch_battery(dev->hdev);
queue_delayed_work(system_freezable_wq, &dev->battery_work,
msecs_to_jiffies(UNCTRL_BATTERY_TIMEOUT_MS));
@@ -223,6 +271,8 @@ static int unctrl_probe(struct hid_device *hdev, const struct hid_device_id *id)
dev->battery_capacity = 100;
dev->mode = UNCTRL_MODE_RECEIVER;
dev->wired = hdev->product == USB_DEVICE_ID_UNCTRL_M1_WIRED;
+ dev->vendor = vendor;
+ spin_lock_init(&dev->lock);
dev->battery_desc.name = devm_kasprintf(&hdev->dev, GFP_KERNEL,
"unctrl-m1-%04x-battery",
@@ -254,19 +304,26 @@ static int unctrl_probe(struct hid_device *hdev, const struct hid_device_id *id)
hid_set_drvdata(hdev, dev);
+ /* the list membership, the registration and any failure cleanup are
+ * one step under unctrl_lock, so a concurrent handover cannot hand
+ * this probe a battery while it is failing out
+ */
+ mutex_lock(&unctrl_lock);
+ list_add_tail(&dev->list, &unctrl_connections);
ret = unctrl_battery_register(dev);
if (ret)
- goto err_del;
+ list_del(&dev->list);
+ mutex_unlock(&unctrl_lock);
- return 0;
+ if (ret) {
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+ hid_set_drvdata(hdev, NULL);
+ kfree(dev);
+ return ret;
+ }
-err_del:
- hid_device_io_stop(hdev);
- hid_hw_close(hdev);
- hid_hw_stop(hdev);
- hid_set_drvdata(hdev, NULL);
- kfree(dev);
- return ret;
+ return 0;
}
static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
@@ -286,15 +343,20 @@ static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
if (data[5] != UNCTRL_MODE_RECEIVER && data[5] != UNCTRL_MODE_WIRED)
return 0;
- /* the receiver reports 0% while the mouse is switched off */
- dev->battery_capacity = min_t(u8, data[4], 100);
- dev->mode = data[5];
- dev->online = dev->battery_capacity != 0;
- battery = dev->battery;
-
- /* only schedules a work, so this is safe from the completion path */
- if (battery)
- power_supply_changed(battery);
+ scoped_guard(spinlock_irqsave, &dev->lock) {
+ /* the receiver reports 0% while the mouse is switched off */
+ dev->battery_capacity = min_t(u8, data[4], 100);
+ dev->mode = data[5];
+ dev->online = dev->battery_capacity != 0;
+ battery = dev->battery;
+ /*
+ * Only schedules a work, so safe from the completion path that
+ * raw_event runs in. Under the lock so that the unregistering side
+ * is either done or still blocked when we touch the pointer.
+ */
+ if (battery)
+ power_supply_changed(battery);
+ }
return 0;
}
@@ -302,20 +364,41 @@ static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
static void unctrl_remove(struct hid_device *hdev)
{
struct unctrl_dev *dev = hid_get_drvdata(hdev);
+ struct unctrl_dev *other;
/* Stop polling before the transport goes down. */
- if (dev) {
- cancel_delayed_work_sync(&dev->battery_work);
+ if (dev)
hid_hw_close(hdev);
- }
hid_hw_stop(hdev);
if (!dev)
return;
- if (dev->battery)
- power_supply_unregister(dev->battery);
+ /*
+ * Pass the battery to a capable connection without one, so that it
+ * keeps being reported when the owner disappears.
+ */
+ mutex_lock(&unctrl_lock);
+ list_del(&dev->list);
+ /* out of the list, nothing can reach this device anymore */
+ cancel_delayed_work_sync(&dev->battery_work);
+ if (dev->battery) {
+ struct power_supply *battery;
+
+ scoped_guard(spinlock_irqsave, &dev->lock) {
+ battery = dev->battery;
+ dev->battery = NULL;
+ }
+ power_supply_unregister(battery);
+ list_for_each_entry(other, &unctrl_connections, list) {
+ if (!other->vendor || other->battery)
+ continue;
+ unctrl_battery_register(other);
+ break;
+ }
+ }
+ mutex_unlock(&unctrl_lock);
hid_set_drvdata(hdev, NULL);
kfree(dev);
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH v2 4/5] HID: UNCTRL: keep a single battery when a mouse has multiple connections
@ 2026-10-12 21:13 ` Bishwa Bhusan Saha
0 siblings, 0 replies; 14+ messages in thread
From: Bishwa Bhusan Saha @ 2026-10-12 21:13 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bishwa Bhusan Saha
The UNCTRL RAGE mouse can be connected over its 2.4 GHz dongle and
its wired connection at the same time. Both are visible as separate
devices on lsusb. Without this patch two separate batteries would be
detected in case of a single mouse connected over 2 interfaces. This
patch solves it by registering the power supply only on the connection
that can answer queries and taking it away when that connection goes
away.
Signed-off-by: Bishwa Bhusan Saha <contact@bishwasaha.com>
Assisted-by: GLM:glm-5.3
---
drivers/hid/hid-unctrl-m1.c | 139 +++++++++++++++++++++++++++++++++++---------
1 file changed, 111 insertions(+), 28 deletions(-)
diff --git a/drivers/hid/hid-unctrl-m1.c b/drivers/hid/hid-unctrl-m1.c
index d7e2f207e..624ac1365 100644
--- a/drivers/hid/hid-unctrl-m1.c
+++ b/drivers/hid/hid-unctrl-m1.c
@@ -16,9 +16,12 @@
* answers over the interrupt out endpoint, where the queries go.
*/
+#include <linux/cleanup.h>
#include <linux/device.h>
#include <linux/hid.h>
+#include <linux/list.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/power_supply.h>
#include <linux/slab.h>
#include <linux/usb.h>
@@ -45,19 +48,27 @@ static const u8 unctrl_battery_query[33] = {
};
struct unctrl_dev {
+ struct list_head list;
struct hid_device *hdev;
struct power_supply_desc battery_desc;
struct power_supply *battery;
+ /* raw_event runs in the completion path, so this is a spinlock. */
+ spinlock_t lock;
+
struct delayed_work battery_work;
u8 battery_capacity;
u8 mode;
bool online;
bool wired;
+ bool vendor;
};
+static LIST_HEAD(unctrl_connections);
+static DEFINE_MUTEX(unctrl_lock);
+
static enum power_supply_property unctrl_battery_props[] = {
POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_PRESENT,
POWER_SUPPLY_PROP_CAPACITY, POWER_SUPPLY_PROP_SCOPE,
@@ -74,9 +85,11 @@ static int unctrl_battery_get_property(struct power_supply *psy,
u8 capacity, mode;
bool online;
- capacity = dev->battery_capacity;
- mode = dev->mode;
- online = dev->online;
+ scoped_guard(spinlock_irqsave, &dev->lock) {
+ capacity = dev->battery_capacity;
+ mode = dev->mode;
+ online = dev->online;
+ }
switch (psp) {
case POWER_SUPPLY_PROP_PRESENT:
@@ -148,13 +161,34 @@ static void unctrl_battery_timer_tick(struct work_struct *work)
msecs_to_jiffies(UNCTRL_BATTERY_TIMEOUT_MS));
}
-/* Register the power supply of this connection and start reporting. */
+static struct unctrl_dev *unctrl_battery_owner(void)
+{
+ struct unctrl_dev *dev;
+
+ list_for_each_entry(dev, &unctrl_connections, list) {
+ if (dev->battery)
+ return dev;
+ }
+
+ return NULL;
+}
+
+/*
+ * Register the power supply on the connection the mouse talks over,
+ * a wired connection takes it away from a receiver. Called with
+ * unctrl_lock held.
+ */
static int unctrl_battery_register(struct unctrl_dev *dev)
{
struct power_supply_config config = { .drv_data = dev };
+ struct unctrl_dev *owner;
struct power_supply *battery;
int ret;
+ owner = unctrl_battery_owner();
+ if (owner == dev || (owner && (!dev->wired || owner->wired)))
+ return 0;
+
battery = power_supply_register(&dev->hdev->dev, &dev->battery_desc,
&config);
if (IS_ERR(battery)) {
@@ -166,10 +200,24 @@ static int unctrl_battery_register(struct unctrl_dev *dev)
}
power_supply_powers(battery, &dev->hdev->dev);
- dev->battery = battery;
+ scoped_guard(spinlock_irqsave, &dev->lock)
+ dev->battery = battery;
hid_info(dev->hdev, "%s:battery registered as %s\n", __func__,
dev->battery_desc.name);
+ /* the takeover cannot fail now, our supply already exists */
+ if (owner) {
+ struct power_supply *owner_battery;
+
+ cancel_delayed_work_sync(&owner->battery_work);
+ /* so a raw_event cannot touch the supply being unregistered */
+ scoped_guard(spinlock_irqsave, &owner->lock) {
+ owner_battery = owner->battery;
+ owner->battery = NULL;
+ }
+ power_supply_unregister(owner_battery);
+ }
+
unctrl_fetch_battery(dev->hdev);
queue_delayed_work(system_freezable_wq, &dev->battery_work,
msecs_to_jiffies(UNCTRL_BATTERY_TIMEOUT_MS));
@@ -223,6 +271,8 @@ static int unctrl_probe(struct hid_device *hdev, const struct hid_device_id *id)
dev->battery_capacity = 100;
dev->mode = UNCTRL_MODE_RECEIVER;
dev->wired = hdev->product == USB_DEVICE_ID_UNCTRL_M1_WIRED;
+ dev->vendor = vendor;
+ spin_lock_init(&dev->lock);
dev->battery_desc.name = devm_kasprintf(&hdev->dev, GFP_KERNEL,
"unctrl-m1-%04x-battery",
@@ -254,19 +304,26 @@ static int unctrl_probe(struct hid_device *hdev, const struct hid_device_id *id)
hid_set_drvdata(hdev, dev);
+ /* the list membership, the registration and any failure cleanup are
+ * one step under unctrl_lock, so a concurrent handover cannot hand
+ * this probe a battery while it is failing out
+ */
+ mutex_lock(&unctrl_lock);
+ list_add_tail(&dev->list, &unctrl_connections);
ret = unctrl_battery_register(dev);
if (ret)
- goto err_del;
+ list_del(&dev->list);
+ mutex_unlock(&unctrl_lock);
- return 0;
+ if (ret) {
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+ hid_set_drvdata(hdev, NULL);
+ kfree(dev);
+ return ret;
+ }
-err_del:
- hid_device_io_stop(hdev);
- hid_hw_close(hdev);
- hid_hw_stop(hdev);
- hid_set_drvdata(hdev, NULL);
- kfree(dev);
- return ret;
+ return 0;
}
static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
@@ -286,15 +343,20 @@ static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
if (data[5] != UNCTRL_MODE_RECEIVER && data[5] != UNCTRL_MODE_WIRED)
return 0;
- /* the receiver reports 0% while the mouse is switched off */
- dev->battery_capacity = min_t(u8, data[4], 100);
- dev->mode = data[5];
- dev->online = dev->battery_capacity != 0;
- battery = dev->battery;
-
- /* only schedules a work, so this is safe from the completion path */
- if (battery)
- power_supply_changed(battery);
+ scoped_guard(spinlock_irqsave, &dev->lock) {
+ /* the receiver reports 0% while the mouse is switched off */
+ dev->battery_capacity = min_t(u8, data[4], 100);
+ dev->mode = data[5];
+ dev->online = dev->battery_capacity != 0;
+ battery = dev->battery;
+ /*
+ * Only schedules a work, so safe from the completion path that
+ * raw_event runs in. Under the lock so that the unregistering side
+ * is either done or still blocked when we touch the pointer.
+ */
+ if (battery)
+ power_supply_changed(battery);
+ }
return 0;
}
@@ -302,20 +364,41 @@ static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
static void unctrl_remove(struct hid_device *hdev)
{
struct unctrl_dev *dev = hid_get_drvdata(hdev);
+ struct unctrl_dev *other;
/* Stop polling before the transport goes down. */
- if (dev) {
- cancel_delayed_work_sync(&dev->battery_work);
+ if (dev)
hid_hw_close(hdev);
- }
hid_hw_stop(hdev);
if (!dev)
return;
- if (dev->battery)
- power_supply_unregister(dev->battery);
+ /*
+ * Pass the battery to a capable connection without one, so that it
+ * keeps being reported when the owner disappears.
+ */
+ mutex_lock(&unctrl_lock);
+ list_del(&dev->list);
+ /* out of the list, nothing can reach this device anymore */
+ cancel_delayed_work_sync(&dev->battery_work);
+ if (dev->battery) {
+ struct power_supply *battery;
+
+ scoped_guard(spinlock_irqsave, &dev->lock) {
+ battery = dev->battery;
+ dev->battery = NULL;
+ }
+ power_supply_unregister(battery);
+ list_for_each_entry(other, &unctrl_connections, list) {
+ if (!other->vendor || other->battery)
+ continue;
+ unctrl_battery_register(other);
+ break;
+ }
+ }
+ mutex_unlock(&unctrl_lock);
hid_set_drvdata(hdev, NULL);
kfree(dev);
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* Re: [PATCH v2 4/5] HID: UNCTRL: keep a single battery when a mouse has multiple connections
2026-10-12 21:13 ` Bishwa Bhusan Saha
(?)
@ 2026-10-12 21:27 ` sashiko-bot
-1 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-12 21:27 UTC (permalink / raw)
To: Bishwa Bhusan Saha; +Cc: dmitry.torokhov, linux-input
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] hid: unctrl: use-after-free via concurrent battery handover to a stopped device
- [High] hid: unctrl: UAF in usbhid_output_report due to delayed worker cancel order
--
commit e4a86ecd29d9dc9de2cb5ac805fe6b509cef727d
Author: Bishwa Bhusan Saha <contact@bishwasaha.com>
HID: UNCTRL: keep a single battery when a mouse has multiple connections
The UNCTRL RAGE mouse can be connected over its 2.4 GHz dongle and its
wired connection at the same time. This patch avoids reporting two
separate batteries by registering the power supply only on the active
connection, handing it over dynamically when that connection goes away.
> diff --git a/drivers/hid/hid-unctrl-m1.c b/drivers/hid/hid-unctrl-m1.c
> --- a/drivers/hid/hid-unctrl-m1.c
> +++ b/drivers/hid/hid-unctrl-m1.c
[ ... ]
> @@ -302,20 +364,41 @@ static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
> static void unctrl_remove(struct hid_device *hdev)
> {
> struct unctrl_dev *dev = hid_get_drvdata(hdev);
> + struct unctrl_dev *other;
>
> /* Stop polling before the transport goes down. */
> - if (dev) {
> - cancel_delayed_work_sync(&dev->battery_work);
> + if (dev)
> hid_hw_close(hdev);
> - }
>
> hid_hw_stop(hdev);
>
> if (!dev)
> return;
>
> - if (dev->battery)
> - power_supply_unregister(dev->battery);
> + /*
> + * Pass the battery to a capable connection without one, so that it
> + * keeps being reported when the owner disappears.
> + */
> + mutex_lock(&unctrl_lock);
> + list_del(&dev->list);
> + /* out of the list, nothing can reach this device anymore */
> + cancel_delayed_work_sync(&dev->battery_work);
[Severity: High]
Can this cause a use-after-free in the transport driver? By moving the
cancellation of the delayed worker battery_work to after hid_hw_stop(),
the self-rescheduling worker might run concurrently with or after the
transport is torn down.
For example, hid_hw_stop() invokes the transport driver's stop routine
like usbhid_stop(), which frees resources such as usbhid->urbout. If the
un-cancelled worker executes unctrl_battery_timer_tick() and calls
hid_hw_output_report(), the transport driver might evaluate its urbout
pointer without locking, creating a time-of-check to time-of-use race
that dereferences the freed pointer.
> + if (dev->battery) {
> + struct power_supply *battery;
> +
> + scoped_guard(spinlock_irqsave, &dev->lock) {
> + battery = dev->battery;
> + dev->battery = NULL;
> + }
> + power_supply_unregister(battery);
> + list_for_each_entry(other, &unctrl_connections, list) {
> + if (!other->vendor || other->battery)
> + continue;
> + unctrl_battery_register(other);
[Severity: High]
Can this trigger a use-after-free if two devices are concurrently removed?
hid_hw_stop() is called before the device is removed from the
unctrl_connections list under unctrl_lock.
If Thread A is executing unctrl_remove() for Device A, it will call
hid_hw_stop(A->hdev).
If Thread B is concurrently executing unctrl_remove() for Device B, it might
acquire unctrl_lock before Thread A. Thread B will iterate the list, find
Device A, and hand the battery to it by calling unctrl_battery_register().
This triggers unctrl_fetch_battery(), which issues a hardware query via
hid_hw_output_report(). Since Device A's transport has already been stopped
by Thread A, accessing the hardware can trigger a use-after-free.
> + break;
> + }
> + }
> + mutex_unlock(&unctrl_lock);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261013-b4-unctrl-m1-battery-v2-0-bd6752021753@bishwasaha.com?part=4
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v2 5/5] HID: UNCTRL: only report to userspace on battery changes
2026-10-12 21:13 ` Bishwa Bhusan Saha
@ 2026-10-12 21:14 ` Bishwa Bhusan Saha
-1 siblings, 0 replies; 14+ messages in thread
From: Bishwa Bhusan Saha via B4 Relay @ 2026-10-12 21:14 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bishwa Bhusan Saha
From: Bishwa Bhusan Saha <contact@bishwasaha.com>
This patch calls power_supply_changed() only when the reported
capacity or mode actually changes.
Signed-off-by: Bishwa Bhusan Saha <contact@bishwasaha.com>
Assisted-by: GLM:glm-5.3
---
drivers/hid/hid-unctrl-m1.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-unctrl-m1.c b/drivers/hid/hid-unctrl-m1.c
index 624ac1365..c44d1cd5b 100644
--- a/drivers/hid/hid-unctrl-m1.c
+++ b/drivers/hid/hid-unctrl-m1.c
@@ -331,6 +331,8 @@ static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
{
struct unctrl_dev *dev = hid_get_drvdata(hdev);
struct power_supply *battery = NULL;
+ bool changed;
+ u8 old_capacity, old_mode;
if (!dev || size < 8)
return 0;
@@ -345,16 +347,23 @@ static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
scoped_guard(spinlock_irqsave, &dev->lock) {
/* the receiver reports 0% while the mouse is switched off */
+ old_capacity = dev->battery_capacity;
+ old_mode = dev->mode;
+
dev->battery_capacity = min_t(u8, data[4], 100);
dev->mode = data[5];
dev->online = dev->battery_capacity != 0;
+
+ changed = dev->battery_capacity != old_capacity ||
+ dev->mode != old_mode;
+
battery = dev->battery;
/*
* Only schedules a work, so safe from the completion path that
* raw_event runs in. Under the lock so that the unregistering side
* is either done or still blocked when we touch the pointer.
*/
- if (battery)
+ if (battery && changed)
power_supply_changed(battery);
}
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH v2 5/5] HID: UNCTRL: only report to userspace on battery changes
@ 2026-10-12 21:14 ` Bishwa Bhusan Saha
0 siblings, 0 replies; 14+ messages in thread
From: Bishwa Bhusan Saha @ 2026-10-12 21:14 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Bishwa Bhusan Saha
This patch calls power_supply_changed() only when the reported
capacity or mode actually changes.
Signed-off-by: Bishwa Bhusan Saha <contact@bishwasaha.com>
Assisted-by: GLM:glm-5.3
---
drivers/hid/hid-unctrl-m1.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-unctrl-m1.c b/drivers/hid/hid-unctrl-m1.c
index 624ac1365..c44d1cd5b 100644
--- a/drivers/hid/hid-unctrl-m1.c
+++ b/drivers/hid/hid-unctrl-m1.c
@@ -331,6 +331,8 @@ static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
{
struct unctrl_dev *dev = hid_get_drvdata(hdev);
struct power_supply *battery = NULL;
+ bool changed;
+ u8 old_capacity, old_mode;
if (!dev || size < 8)
return 0;
@@ -345,16 +347,23 @@ static int unctrl_raw_event(struct hid_device *hdev, struct hid_report *report,
scoped_guard(spinlock_irqsave, &dev->lock) {
/* the receiver reports 0% while the mouse is switched off */
+ old_capacity = dev->battery_capacity;
+ old_mode = dev->mode;
+
dev->battery_capacity = min_t(u8, data[4], 100);
dev->mode = data[5];
dev->online = dev->battery_capacity != 0;
+
+ changed = dev->battery_capacity != old_capacity ||
+ dev->mode != old_mode;
+
battery = dev->battery;
/*
* Only schedules a work, so safe from the completion path that
* raw_event runs in. Under the lock so that the unregistering side
* is either done or still blocked when we touch the pointer.
*/
- if (battery)
+ if (battery && changed)
power_supply_changed(battery);
}
--
2.56.0
^ permalink raw reply related [flat|nested] 14+ messages in thread