Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH 0/2] HID: fix battery queries with nonzero field offsets
@ 2026-07-12  4:47 Mason Camara
  2026-07-12  4:47 ` [PATCH 1/2] HID: input: honor report field offsets in battery queries Mason Camara
  2026-07-12  4:47 ` [PATCH 2/2] selftests/hid: test battery queries with nonzero field offsets Mason Camara
  0 siblings, 2 replies; 3+ messages in thread
From: Mason Camara @ 2026-07-12  4:47 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Shuah Khan, John Chen, linux-input, linux-kselftest, linux-kernel

Magic Mouse 2 input report 0x90 places a status byte before the capacity
field. The synchronous battery query treats that byte as capacity and
reports 4% in sysfs when the device reports 95%.

Use the field offset and width from the report descriptor when processing
GET_REPORT responses, and add a UHID regression test.

Tool use:
  Codex (GPT-5) reviewed the completed series for submission-process,
  formatting, commit-message issues, and suggested edits to the commit
  messages. The code and test were written without AI assistance. I
  reviewed all resulting changes and take responsibility for the series.

Testing:
  - W=1 builds of drivers/hid/hid-input.o with
    CONFIG_HID_BATTERY_STRENGTH=y and =n
  - v7.2-rc2 plus patch 2 only: FAIL (capacity 4, expected 95)
  - v7.2-rc2 plus the complete series: PASS

Mason Camara (2):
  HID: input: honor report field offsets in battery queries
  selftests/hid: test battery queries with nonzero field offsets

 drivers/hid/hid-input.c                       | 39 ++++++++++----
 include/linux/hid.h                           |  6 +++
 tools/testing/selftests/hid/config            |  1 +
 .../testing/selftests/hid/tests/test_mouse.py | 54 +++++++++++++++++++
 4 files changed, 90 insertions(+), 10 deletions(-)


base-commit: 8cdeaa50eae8dad34885515f62559ee83e7e8dda
-- 
2.55.0


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH 1/2] HID: input: honor report field offsets in battery queries
  2026-07-12  4:47 [PATCH 0/2] HID: fix battery queries with nonzero field offsets Mason Camara
@ 2026-07-12  4:47 ` Mason Camara
  2026-07-12  4:47 ` [PATCH 2/2] selftests/hid: test battery queries with nonzero field offsets Mason Camara
  1 sibling, 0 replies; 3+ messages in thread
From: Mason Camara @ 2026-07-12  4:47 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Shuah Khan, John Chen, linux-input, linux-kselftest, linux-kernel

hidinput_query_battery_capacity() always reads buf[1] from a GET_REPORT
reply. This assumes capacity usage starts at bit 0 of the report payload
and that capacity occupies the first byte.

The Magic Mouse 2 puts a status byte before AbsoluteStateOfCharge in
input report 0x90. Its GET_REPORT response is:

  90 04 5f

Here 0x04 is the status byte and 0x5f is the capacity. Reading buf[1]
reports 4% in sysfs.

This query runs when userspace reads capacity before an input report
updates the battery cache. The event path already extracts the usage
from the descriptor, so moving the mouse before the sysfs read can hide
the bug.

Record the report containing the capacity field, bit offset, and size when
setting up the battery. Allocate a buffer large enough for the complete
report, skip the report ID byte, and extract the value at the recorded bit
offset. Reject replies that do not contain the complete field.

Fixes: 9de07a4e8d4c ("HID: input: map battery capacity (00850065)")
Closes: https://bugzilla.kernel.org/show_bug.cgi?id=221263
Assisted-by: Codex:GPT-5
Signed-off-by: Mason Camara <ping@masoncamara.com>
---
 drivers/hid/hid-input.c | 39 +++++++++++++++++++++++++++++----------
 include/linux/hid.h     |  6 ++++++
 2 files changed, 35 insertions(+), 10 deletions(-)

diff --git a/drivers/hid/hid-input.c b/drivers/hid/hid-input.c
index 3487600cadb4..01ce9e176299 100644
--- a/drivers/hid/hid-input.c
+++ b/drivers/hid/hid-input.c
@@ -431,18 +431,28 @@ static int hidinput_scale_battery_capacity(struct hid_battery *bat,
 
 static int hidinput_query_battery_capacity(struct hid_battery *bat)
 {
+	u8 *buf __free(kfree) = NULL;
+	unsigned int field_end;
+	unsigned int report_len;
+	u32 value;
 	int ret;
 
-	u8 *buf __free(kfree) = kmalloc(4, GFP_KERNEL);
+	report_len = hid_report_len(bat->report) + !bat->report->id;
+	buf = hid_alloc_report_buf(bat->report, GFP_KERNEL);
 	if (!buf)
 		return -ENOMEM;
 
-	ret = hid_hw_raw_request(bat->dev, bat->report_id, buf, 4,
+	ret = hid_hw_raw_request(bat->dev, bat->report_id, buf, report_len,
 				 bat->report_type, HID_REQ_GET_REPORT);
-	if (ret < 2)
+	field_end = DIV_ROUND_UP(bat->report_offset + bat->report_size, 8) +
+		    1;
+	if (ret < 0 || (unsigned int)ret < field_end)
 		return -ENODATA;
 
-	return hidinput_scale_battery_capacity(bat, buf[1]);
+	value = hid_field_extract(bat->dev, buf + 1, bat->report_offset,
+				  bat->report_size);
+
+	return hidinput_scale_battery_capacity(bat, value);
 }
 
 static int hidinput_get_battery_property(struct power_supply *psy,
@@ -529,7 +539,8 @@ static void hidinput_cleanup_battery(void *res)
 }
 
 static int hidinput_setup_battery(struct hid_device *dev, unsigned report_type,
-				  struct hid_field *field, bool is_percentage)
+				  struct hid_field *field, unsigned int usage_index,
+				  bool is_percentage)
 {
 	struct hid_battery *bat;
 	struct power_supply_desc *psy_desc;
@@ -593,6 +604,10 @@ static int hidinput_setup_battery(struct hid_device *dev, unsigned report_type,
 	bat->max = max;
 	bat->report_type = report_type;
 	bat->report_id = field->report->id;
+	bat->report = field->report;
+	bat->report_offset = field->report_offset +
+			     usage_index * field->report_size;
+	bat->report_size = field->report_size;
 	bat->charge_status = POWER_SUPPLY_STATUS_DISCHARGING;
 	bat->status = HID_BATTERY_UNKNOWN;
 
@@ -687,7 +702,8 @@ static void hidinput_update_battery(struct hid_device *dev, int report_id,
 }
 #else  /* !CONFIG_HID_BATTERY_STRENGTH */
 static int hidinput_setup_battery(struct hid_device *dev, unsigned report_type,
-				  struct hid_field *field, bool is_percentage)
+				  struct hid_field *field, unsigned int usage_index,
+				  bool is_percentage)
 {
 	return 0;
 }
@@ -1035,7 +1051,8 @@ static void hidinput_configure_usage(struct hid_input *hidinput, struct hid_fiel
 			break;
 
 		case 0x3b: /* Battery Strength */
-			hidinput_setup_battery(device, HID_INPUT_REPORT, field, false);
+			hidinput_setup_battery(device, HID_INPUT_REPORT, field,
+					       usage_index, false);
 			usage->type = EV_PWR;
 			return;
 
@@ -1313,7 +1330,8 @@ static void hidinput_configure_usage(struct hid_input *hidinput, struct hid_fiel
 	case HID_UP_GENDEVCTRLS:
 		switch (usage->hid) {
 		case HID_DC_BATTERYSTRENGTH:
-			hidinput_setup_battery(device, HID_INPUT_REPORT, field, false);
+			hidinput_setup_battery(device, HID_INPUT_REPORT, field,
+					       usage_index, false);
 			usage->type = EV_PWR;
 			return;
 		}
@@ -1322,7 +1340,8 @@ static void hidinput_configure_usage(struct hid_input *hidinput, struct hid_fiel
 	case HID_UP_BATTERY:
 		switch (usage->hid) {
 		case HID_BAT_ABSOLUTESTATEOFCHARGE:
-			hidinput_setup_battery(device, HID_INPUT_REPORT, field, true);
+			hidinput_setup_battery(device, HID_INPUT_REPORT, field,
+					       usage_index, true);
 			usage->type = EV_PWR;
 			return;
 		case HID_BAT_CHARGING:
@@ -2040,7 +2059,7 @@ static void report_features(struct hid_device *hid)
 				/* Verify if Battery Strength feature is available */
 				if (usage->hid == HID_DC_BATTERYSTRENGTH)
 					hidinput_setup_battery(hid, HID_FEATURE_REPORT,
-							       rep->field[i], false);
+							       rep->field[i], j, false);
 
 				if (drv->feature_mapping)
 					drv->feature_mapping(hid, rep->field[i], usage);
diff --git a/include/linux/hid.h b/include/linux/hid.h
index b240baa95ab5..396de1080fdc 100644
--- a/include/linux/hid.h
+++ b/include/linux/hid.h
@@ -642,6 +642,9 @@ enum hid_battery_status {
  * @max: maximum battery value from HID descriptor
  * @report_type: HID report type (input/feature)
  * @report_id: HID report ID for this battery
+ * @report: HID report containing the capacity field
+ * @report_offset: bit offset of the capacity field in the report
+ * @report_size: size of the capacity field in bits
  * @charge_status: current charging status
  * @status: battery reporting status
  * @capacity: current battery capacity (0-100)
@@ -657,6 +660,9 @@ struct hid_battery {
 	__s32 max;
 	__s32 report_type;
 	__s32 report_id;
+	struct hid_report *report;
+	__u32 report_offset;
+	__u32 report_size;
 	__s32 charge_status;
 	enum hid_battery_status status;
 	__s32 capacity;
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* [PATCH 2/2] selftests/hid: test battery queries with nonzero field offsets
  2026-07-12  4:47 [PATCH 0/2] HID: fix battery queries with nonzero field offsets Mason Camara
  2026-07-12  4:47 ` [PATCH 1/2] HID: input: honor report field offsets in battery queries Mason Camara
@ 2026-07-12  4:47 ` Mason Camara
  1 sibling, 0 replies; 3+ messages in thread
From: Mason Camara @ 2026-07-12  4:47 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Shuah Khan, John Chen, linux-input, linux-kselftest, linux-kernel

Add a UHID mouse whose input report 0x90 contains a status byte before
the capacity field. Answer GET_REPORT with 90 04 5f and read the power
supply capacity before sending any input report.

On an unpatched kernel, the capacity read returns the status byte (4)
instead of the capacity value (95), so the test fails. Enable
CONFIG_HID_BATTERY_STRENGTH in the HID selftest configuration.

Assisted-by: Codex:GPT-5
Signed-off-by: Mason Camara <ping@masoncamara.com>
---
 tools/testing/selftests/hid/config            |  1 +
 .../testing/selftests/hid/tests/test_mouse.py | 54 +++++++++++++++++++
 2 files changed, 55 insertions(+)

diff --git a/tools/testing/selftests/hid/config b/tools/testing/selftests/hid/config
index 1758b055f295..da52335b865a 100644
--- a/tools/testing/selftests/hid/config
+++ b/tools/testing/selftests/hid/config
@@ -16,6 +16,7 @@ CONFIG_FTRACE_SYSCALLS=y
 CONFIG_FUNCTION_TRACER=y
 CONFIG_HIDRAW=y
 CONFIG_HID=y
+CONFIG_HID_BATTERY_STRENGTH=y
 CONFIG_HID_BPF=y
 CONFIG_INPUT_EVDEV=y
 CONFIG_UHID=y
diff --git a/tools/testing/selftests/hid/tests/test_mouse.py b/tools/testing/selftests/hid/tests/test_mouse.py
index eb4e15a0e53b..141c1f06905b 100644
--- a/tools/testing/selftests/hid/tests/test_mouse.py
+++ b/tools/testing/selftests/hid/tests/test_mouse.py
@@ -11,6 +11,7 @@ import hidtools.hid
 from hidtools.util import BusType
 import libevdev
 import logging
+import threading
 import pytest
 
 logger = logging.getLogger("hidtools.test.mouse")
@@ -598,6 +599,35 @@ class ResolutionMultiplierHWheelMouse(TwoWheelMouse):
         return 0
 
 
+class BatteryOffsetMouse(BaseMouse):
+    report_descriptor = [
+        # Mouse report
+        0x05, 0x01, 0x09, 0x02, 0xa1, 0x01, 0x85, 0x12,
+        0x05, 0x09, 0x19, 0x01, 0x29, 0x02, 0x15, 0x00,
+        0x25, 0x01, 0x95, 0x02, 0x75, 0x01, 0x81, 0x02,
+        0x95, 0x01, 0x75, 0x06, 0x81, 0x01, 0x05, 0x01,
+        0x09, 0x01, 0xa1, 0x00, 0x09, 0x30, 0x09, 0x31,
+        0x15, 0x81, 0x25, 0x7f, 0x75, 0x08, 0x95, 0x02,
+        0x81, 0x06, 0xc0, 0xc0,
+        # Battery report: one status byte followed by capacity
+        0x06, 0x00, 0xff, 0x09, 0x14, 0xa1, 0x01, 0x85,
+        0x90, 0x05, 0x84, 0x75, 0x01, 0x95, 0x03, 0x15,
+        0x00, 0x25, 0x01, 0x09, 0x61, 0x05, 0x85, 0x09,
+        0x44, 0x09, 0x46, 0x81, 0x02, 0x95, 0x05, 0x81,
+        0x01, 0x75, 0x08, 0x95, 0x01, 0x15, 0x00, 0x26,
+        0xff, 0x00, 0x09, 0x65, 0x81, 0x02, 0xc0,
+    ]
+
+    def __init__(self, rdesc=report_descriptor, name=None, input_info=None):
+        super().__init__(rdesc, name, input_info)
+
+    def get_report(self, req, rnum, rtype):
+        if rtype != self.UHID_INPUT_REPORT or rnum != 0x90:
+            return (1, [])
+
+        return (0, [0x90, 0x04, 0x5F])
+
+
 class BaseTest:
     class TestMouse(base.BaseTestCase.TestUhid):
         def test_buttons(self):
@@ -1045,3 +1075,27 @@ class TestBadReportDescriptorMouse(base.BaseTestCase.TestUhid):
 
     def assertName(self, uhdev):
         pass
+
+
+class TestBatteryOffsetMouse(base.BaseTestCase.TestUhid):
+    def create_device(self):
+        return BatteryOffsetMouse()
+
+    def test_queried_battery_field_offset(self):
+        uhdev = self.uhdev
+        power_supply = uhdev.power_supply_class
+        assert power_supply is not None
+
+        done = False
+
+        def dispatch():
+            while not done:
+                uhdev.dispatch(1)
+
+        thread = threading.Thread(target=dispatch)
+        thread.start()
+        try:
+            assert power_supply.capacity == 95
+        finally:
+            done = True
+            thread.join()
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-07-12  4:47 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-12  4:47 [PATCH 0/2] HID: fix battery queries with nonzero field offsets Mason Camara
2026-07-12  4:47 ` [PATCH 1/2] HID: input: honor report field offsets in battery queries Mason Camara
2026-07-12  4:47 ` [PATCH 2/2] selftests/hid: test battery queries with nonzero field offsets Mason Camara

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