lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
Message-ID: <235531f556c5abfcae254a4e56441ba6@davidebeatrici.dev>
Date: Tue, 02 Dec 2025 06:36:46 +0100
From: Davide Beatrici <me@...idebeatrici.dev>
To: linux-kernel@...r.kernel.org
Cc: linux-input@...r.kernel.org, jikos@...nel.org,
 benjamin.tissoires@...hat.com
Subject: [PATCH] HID: validate report length and constants

 From 4956d80ba2bcdb0f5410dfe9365331bcece781cb Mon Sep 17 00:00:00 2001
 From: Davide Beatrici <git@...idebeatrici.dev>
Date: Tue, 2 Dec 2025 05:00:52 +0100
Subject: [PATCH] HID: validate report length and constants
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The HID specification defines Input(Constant) fields as padding or 
reserved
bits that must remain zero at runtime. Currently, Linux accepts non‑zero
values in these fields, allowing malformed reports to propagate to 
userspace.

The ATK X1 SE (373b:1107) is a popular USB mouse that triggers weird 
behavior.
A few seconds after connecting it sends a packet that is detected as 
malformed
by WireShark, but the kernel happily accepts and parses it.

Until the device is disconnected, modifier keys are forced into a fixed 
state:

LeftControl (0xe0): UP
LeftShift (0xe1): UP
LeftAlt (0xe2): DOWN
LeftGUI (0xe3): UP
RightControl (0xe4): UP
RightShift (0xe5): DOWN
RightAlt (0xe6): DOWN
RightGUI (0xe7): UP

And unknown keys are kept pressed:

kernel: usb 5-1: Unknown key (scancode 0xb2) pressed.
kernel: usb 5-1: Unknown key (scancode 0xff) pressed.
kernel: usb 5-1: Unknown key (scancode 0xff) pressed.

kernel: usb 5-1: Unknown key (scancode 0xc0) pressed.
kernel: usb 5-1: Unknown key (scancode 0xb6) pressed.
kernel: usb 5-1: Unknown key (scancode 0xff) pressed.
kernel: usb 5-1: Unknown key (scancode 0xff) pressed.

This patch extends hid-core to record Constant slices during descriptor
parsing and validate them at runtime. Reports with non‑zero Constant 
bits are
rejected, and a ratelimited warning is logged. Legitimate Data/Relative 
fields
(buttons, motion axes, wheel) continue to pass through unchanged.

Malformed reports are suppressed and no longer show up with 
usbmon/WireShark.
All other USB devices I own still work flawlessly after applying this 
patch,
but this is definitely something that requires extensive testing!

Signed-off-by: Davide Beatrici <git@...idebeatrici.dev>
---
  drivers/hid/hid-core.c | 134 ++++++++++++++++++++++++++++++++++++++++-
  include/linux/hid.h    |  15 +++++
  2 files changed, 146 insertions(+), 3 deletions(-)

diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index a5b3a8ca2fcb..bb45c5f6f4fb 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -280,6 +280,81 @@ static int hid_add_usage(struct hid_parser *parser, 
unsigned usage, u8 size)
  	return 0;
  }

+/*
+ * Append metadata for runtime payload validation.
+ */
+
+static int hid_validate_append_bits(struct hid_report *r, const __u16 
bit_off, const __u16 bit_len)
+{
+	struct hid_report_validate *v = &r->validate;
+	struct hid_const_slice *slices = krealloc(v->const_slices,
+		(v->const_count + 1) * sizeof(*slices), GFP_KERNEL);
+	if (!slices)
+		return -ENOMEM;
+
+	v->const_slices = slices;
+	v->const_slices[v->const_count].bit_off = bit_off;
+	v->const_slices[v->const_count].bit_len = bit_len;
+	v->const_count++;
+
+	return 0;
+}
+
+/*
+ * Validate runtime payload.
+ */
+
+static bool hid_validate_report(struct hid_device *hid, struct 
hid_report *r,
+		const __u8 *buf, size_t len)
+{
+	const __u8 *payload;
+	size_t payload_len;
+	__u16 i;
+
+	/* Report ID handling: if present, buf[0] is ID; payload follows */
+	payload = r->id ? buf + 1 : buf;
+	payload_len = r->id ? (len ? len - 1 : 0) : len;
+
+	if (r->validate.payload_len && payload_len != r->validate.payload_len) 
{
+		hid_warn_ratelimited(hid,
+			"Malformed report: length %zu != expected %u (ID %u)\n",
+			payload_len, r->validate.payload_len, r->id);
+		return false;
+	}
+
+	for (i = 0; i < r->validate.const_count; i++) {
+		const __u16 bit_off = r->validate.const_slices[i].bit_off;
+		const __u16 bit_len = r->validate.const_slices[i].bit_len;
+		const __u16 end_bit = bit_off + bit_len;
+
+		for (__u16 b = bit_off; b < end_bit;) {
+			size_t byte_off = b >> 3;
+			size_t bit_in_byte = b & 7;
+
+			__u16 rem_bits = end_bit - b;
+			__u8 span = (__u8)min_t(__u16, rem_bits, 8 - bit_in_byte);
+			__u8 mask = ((1u << span) - 1) << bit_in_byte;
+
+			if (byte_off >= payload_len) {
+				hid_warn_ratelimited(hid,
+					"Malformed report: const slice OOB (bit_off %u, len %u)\n",
+					bit_off, bit_len);
+				return false;
+			}
+			if (payload[byte_off] & mask) {
+				hid_warn_ratelimited(hid,
+					"Malformed report: non-zero constant at byte %zu mask 0x%02x val 
0x%02x\n",
+					byte_off, mask, payload[byte_off]);
+				return false;
+			}
+
+			b += span;
+		}
+	}
+
+	return true;
+}
+
  /*
   * Register a new field for this report.
   */
@@ -303,6 +378,8 @@ static int hid_add_field(struct hid_parser *parser, 
unsigned report_type, unsign
  		return -1;
  	}

+	parser->curr_report = report;
+
  	/* Handle both signed and unsigned cases properly */
  	if ((parser->global.logical_minimum < 0 &&
  		parser->global.logical_maximum <
@@ -638,11 +715,13 @@ static void hid_concatenate_last_usage_page(struct 
hid_parser *parser)
  static int hid_parser_main(struct hid_parser *parser, struct hid_item 
*item)
  {
  	__u32 data;
+	__u8 flags;
  	int ret;

  	hid_concatenate_last_usage_page(parser);

  	data = item_udata(item);
+	flags = (u8)data;

  	switch (item->tag) {
  	case HID_MAIN_ITEM_TAG_BEGIN_COLLECTION:
@@ -651,15 +730,61 @@ static int hid_parser_main(struct hid_parser 
*parser, struct hid_item *item)
  	case HID_MAIN_ITEM_TAG_END_COLLECTION:
  		ret = close_collection(parser);
  		break;
-	case HID_MAIN_ITEM_TAG_INPUT:
+	case HID_MAIN_ITEM_TAG_INPUT: {
+		__u16 offset_bits, size_bits;
+
+		if (flags & HID_MAIN_ITEM_RESERVED_MASK) {
+			hid_warn_ratelimited(parser->device,
+				"Malformed input descriptor: reserved bits set (0x%02x)\n",
+				flags);
+			return -EINVAL;
+		}
+
+		/* Compute field range in bits */
+		offset_bits = parser->curr_offset;
+		size_bits   = parser->global.report_size * 
parser->global.report_count;
+
+		/* Record Input(Constant) slices for runtime validation */
+		if ((flags & HID_MAIN_ITEM_CONSTANT) && parser->curr_report) {
+			/* Record bit-granular slice: store bit offset and size */
+			ret = hid_validate_append_bits(parser->curr_report, offset_bits, 
size_bits);
+			if (ret)
+				return ret;
+		}
+
+		/* Advance offset and add field */
+		parser->curr_offset += size_bits;
+
  		ret = hid_add_field(parser, HID_INPUT_REPORT, data);
+		if (!ret && parser->curr_report) {
+			/* Expected payload length (bytes) excluding the optional ID */
+			parser->curr_report->validate.payload_len = 
(parser->curr_report->size + 7) / 8;
+		}
+
  		break;
-	case HID_MAIN_ITEM_TAG_OUTPUT:
+	}
+	case HID_MAIN_ITEM_TAG_OUTPUT: {
+		if (flags & HID_MAIN_ITEM_RESERVED_MASK) {
+			hid_warn_ratelimited(parser->device,
+				"Malformed output descriptor: reserved bits set (0x%02x)\n",
+				flags);
+			return -EINVAL;
+		}
+
  		ret = hid_add_field(parser, HID_OUTPUT_REPORT, data);
  		break;
-	case HID_MAIN_ITEM_TAG_FEATURE:
+	}
+	case HID_MAIN_ITEM_TAG_FEATURE: {
+		if (flags & HID_MAIN_ITEM_RESERVED_MASK) {
+			hid_warn_ratelimited(parser->device,
+				"Malformed feature descriptor: reserved bits set (0x%02x)\n",
+				flags);
+			return -EINVAL;
+		}
+
  		ret = hid_add_field(parser, HID_FEATURE_REPORT, data);
  		break;
+	}
  	default:
  		if (item->tag >= HID_MAIN_ITEM_TAG_RESERVED_MIN &&
  			item->tag <= HID_MAIN_ITEM_TAG_RESERVED_MAX)
@@ -2062,6 +2187,9 @@ int hid_report_raw_event(struct hid_device *hid, 
enum hid_report_type type, u8 *
  		memset(cdata + csize, 0, rsize - csize);
  	}

+	if (!hid_validate_report(hid, report, data, size))
+		goto out;
+
  	if ((hid->claimed & HID_CLAIMED_HIDDEV) && hid->hiddev_report_event)
  		hid->hiddev_report_event(hid, report);
  	if (hid->claimed & HID_CLAIMED_HIDRAW) {
diff --git a/include/linux/hid.h b/include/linux/hid.h
index a4ddb94e3ee5..3c7b3a8faa48 100644
--- a/include/linux/hid.h
+++ b/include/linux/hid.h
@@ -88,6 +88,7 @@ struct hid_item {
   * HID report descriptor main item contents
   */

+#define HID_MAIN_ITEM_RESERVED_MASK	0x0c8 /* HID 1.11: flags reserved 
bits (3, 6, 7) must be zero */
  #define HID_MAIN_ITEM_CONSTANT		0x001
  #define HID_MAIN_ITEM_VARIABLE		0x002
  #define HID_MAIN_ITEM_RELATIVE		0x004
@@ -560,6 +561,17 @@ struct hid_field_entry {
  	__s32 priority;
  };

+struct hid_const_slice {
+	__u16 bit_off;
+	__u16 bit_len;
+};
+
+struct hid_report_validate {
+	struct hid_const_slice *const_slices;
+	__u16 const_count;
+	__u16 payload_len;
+};
+
  struct hid_report {
  	struct list_head list;
  	struct list_head hidinput_list;
@@ -576,6 +588,7 @@ struct hid_report {
  	/* tool related state */
  	bool tool_active;				/* whether the current tool is active */
  	unsigned int tool;				/* BTN_TOOL_* */
+	struct hid_report_validate validate;
  };

  #define HID_MAX_IDS 256
@@ -760,6 +773,8 @@ struct hid_parser {
  	unsigned int          collection_stack_size;
  	struct hid_device    *device;
  	unsigned int          scan_flags;
+	__u16                 curr_offset;
+	struct hid_report    *curr_report;
  };

  struct hid_class_descriptor {
-- 
2.51.2


Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ