Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH 0/5] HID: steam: Add 2026 Steam Controller support
@ 2026-08-05  1:51 Vicki Pfau
  2026-08-05  1:51 ` [PATCH 1/5] HID: steam: Initial " Vicki Pfau
                   ` (4 more replies)
  0 siblings, 5 replies; 10+ messages in thread
From: Vicki Pfau @ 2026-08-05  1:51 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau

This is the second half of a series that modernizes the hid-steam
controller. It adds support for the new 2026 Steam Controller, with feature
parity compared to the 2015 model and the Steam Deck drivers, as well as
some smaller cleanup. The biggest cleanup is cleaning up the somewhat lax
locking practices, but a few more minor things exposed to userspace include
logging improvements and zeroing inputs when switching from gamepad mode to
lizard mode.

Vicki Pfau (5):
  HID: steam: Initial 2026 Steam Controller support
  HID: steam: Fix wording of connect/disconnect logs
  HID: steam: Don't set feature reports when disconnecting
  HID: steam: Clean up locking
  HID: steam: Zero out inputs when disabling gamepad mode

 drivers/hid/hid-ids.h   |    4 +
 drivers/hid/hid-steam.c | 1005 ++++++++++++++++++++++++++++++++-------
 2 files changed, 827 insertions(+), 182 deletions(-)

-- 
2.54.0


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

* [PATCH 1/5] HID: steam: Initial 2026 Steam Controller support
  2026-08-05  1:51 [PATCH 0/5] HID: steam: Add 2026 Steam Controller support Vicki Pfau
@ 2026-08-05  1:51 ` Vicki Pfau
  2026-08-05  2:13   ` sashiko-bot
  2026-08-05  1:51 ` [PATCH 2/5] HID: steam: Fix wording of connect/disconnect logs Vicki Pfau
                   ` (3 subsequent siblings)
  4 siblings, 1 reply; 10+ messages in thread
From: Vicki Pfau @ 2026-08-05  1:51 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau

This brings support for the 2026 Steam Controller, comparably featureful to
the existing support for the Steam Deck.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/hid/hid-ids.h   |   4 +
 drivers/hid/hid-steam.c | 847 +++++++++++++++++++++++++++++++++-------
 2 files changed, 712 insertions(+), 139 deletions(-)

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 1059922baaac..b3559e70eec8 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1374,6 +1374,10 @@
 #define USB_DEVICE_ID_STEAM_CONTROLLER		0x1102
 #define USB_DEVICE_ID_STEAM_CONTROLLER_WIRELESS	0x1142
 #define USB_DEVICE_ID_STEAM_DECK	0x1205
+#define USB_DEVICE_ID_STEAM_CONTROLLER_IBEX	0x1302
+#define USB_DEVICE_ID_STEAM_CONTROLLER_IBEX_BLE	0x1303
+#define USB_DEVICE_ID_STEAM_CONTROLLER_PROTEUS	0x1304
+#define USB_DEVICE_ID_STEAM_CONTROLLER_NEREID	0x1305
 
 #define USB_VENDOR_ID_STEELSERIES	0x1038
 #define USB_DEVICE_ID_STEELSERIES_SRWS1	0x1410
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 6199f67f3c4c..111e57926d04 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -44,6 +44,7 @@
 #include <linux/delay.h>
 #include <linux/power_supply.h>
 #include <linux/unaligned.h>
+#include <linux/usb.h>
 #include "hid-ids.h"
 
 MODULE_DESCRIPTION("HID driver for Valve Steam Controller");
@@ -58,6 +59,8 @@ static LIST_HEAD(steam_devices);
 
 #define STEAM_QUIRK_WIRELESS		BIT(0)
 #define STEAM_QUIRK_DECK		BIT(1)
+#define STEAM_QUIRK_IBEX		BIT(2)
+#define STEAM_QUIRK_BLE			BIT(3)
 
 /* Touch pads are 40 mm in diameter and 65535 units */
 #define STEAM_PAD_RESOLUTION 1638
@@ -310,11 +313,83 @@ enum {
 	TRACKPAD_GESTURE_KEYBOARD,
 };
 
+/* Report identifiers (Ibex only) */
+enum {
+	/* Feature */
+	REPORT_ID_FEATURES_CONTROLLER	= 1,
+	REPORT_ID_FEATURES_DONGLE	= 2,
+
+	/* Input */
+	REPORT_ID_INPUT			= 0x42,
+	REPORT_ID_BATTERY		= 0x43,
+	REPORT_ID_INPUT2		= 0x45,
+	REPORT_ID_WIRELESS_EVENT	= 0x79,
+
+	/* Output */
+	REPORT_ID_HAPTIC_RUMBLE		= 0x80,
+	REPORT_ID_HAPTIC_PULSE		= 0x81,
+	REPORT_ID_HAPTIC_COMMAND	= 0x82,
+	REPORT_ID_HAPTIC_LFO_TONE	= 0x83,
+	REPORT_ID_HAPTIC_LOG_SWEEP	= 0x84,
+	REPORT_ID_HAPTIC_SCRIPT		= 0x85,
+};
+
+/* Ibex charge state */
+enum {
+	CHARGE_STATE_DISCHARGING	= 1,
+	CHARGE_STATE_CHARGING		= 2,
+	CHARGE_STATE_CHARGING_DONE	= 4,
+};
+
+/* Ibex wireless events */
+enum {
+	WIRELESS_EVENT_DISCONNECT	= 1,
+	WIRELESS_EVENT_CONNECT		= 2,
+	WIRELESS_EVENT_PAIR		= 3,
+};
+
 struct steam_controller_attribute {
 	unsigned char tag;
 	__le32 value;
 } __packed;
 
+struct steam_ibex_battery_status {
+	u8 charge_state;
+	u8 battery_level;
+	__le16 battery_voltage;
+	__le16 system_voltage;
+	__le16 input_voltage;
+	__le16 battery_current;
+	__le16 input_current;
+	__le16 temperature;
+};
+
+struct steam_ibex_haptic_rumble {
+	u8 type;
+	__le16 intensity;
+	struct {
+		__le16 speed;
+		u8 gain;
+	} __packed left, right;
+} __packed;
+static_assert(sizeof(struct steam_ibex_haptic_rumble) == 9);
+
+struct steam_ibex_haptic_pulse {
+	u8 side;
+	__le16 on_us;
+	__le16 off_us;
+	__le16 repeat_count;
+} __packed;
+static_assert(sizeof(struct steam_ibex_haptic_pulse) == 7);
+
+struct steam_ibex_output_report {
+	u8 id;
+	union {
+		struct steam_ibex_haptic_rumble rumble;
+		struct steam_ibex_haptic_pulse pulse;
+	};
+} __packed;
+
 /* Pad identifiers for the deck */
 #define STEAM_PAD_LEFT 0
 #define STEAM_PAD_RIGHT 1
@@ -338,7 +413,10 @@ struct steam_device {
 	struct power_supply_desc battery_desc;
 	struct power_supply __rcu *battery;
 	u8 battery_charge;
-	u16 voltage;
+	u8 battery_status;
+	u16 battery_temp;
+	u16 battery_voltage;
+	u16 battery_current;
 	struct delayed_work mode_switch;
 	bool did_mode_switch;
 	bool gamepad_mode;
@@ -350,12 +428,14 @@ struct steam_device {
 	unsigned int sensor_update_rate_us;
 };
 
-static int steam_recv_report(struct steam_device *steam,
-		u8 *data, int size)
+static int steam_recv_report_id(struct steam_device *steam,
+		u8 *data, int size, u8 report_id)
 {
 	struct hid_report *r;
 	u8 *buf;
+	unsigned int retries = 50;
 	int ret;
+	u32 len;
 
 	/*
 	 * All reports start with a two byte header.
@@ -364,13 +444,14 @@ static int steam_recv_report(struct steam_device *steam,
 	if (size < 2)
 		return -EINVAL;
 
-	r = steam->hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[0];
+	r = steam->hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[report_id];
 	if (!r) {
-		hid_err(steam->hdev, "No HID_FEATURE_REPORT submitted -  nothing to read\n");
+		hid_err(steam->hdev, "No HID_FEATURE_REPORT present for ID %u\n", report_id);
 		return -EINVAL;
 	}
 
-	if (hid_report_len(r) < 64)
+	len = hid_report_len(r);
+	if (len < 64)
 		return -EINVAL;
 
 	buf = hid_alloc_report_buf(r, GFP_KERNEL);
@@ -378,21 +459,38 @@ static int steam_recv_report(struct steam_device *steam,
 		return -ENOMEM;
 
 	/*
-	 * The report ID is always 0, so strip the first byte from the output.
+	 * The report ID is consistent, so strip the first byte from the output.
 	 * hid_report_len() is not counting the report ID, so +1 to the length
 	 * or else we get a EOVERFLOW. We are safe from a buffer overflow
 	 * because hid_alloc_report_buf() allocates +7 bytes.
 	 */
-	ret = hid_hw_raw_request(steam->hdev, 0x00,
-			buf, hid_report_len(r) + 1,
-			HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+	if (!(steam->quirks & STEAM_QUIRK_IBEX))
+		len += 1;
+
+	/*
+	 * Sometimes the wireless controller fails with EPIPE
+	 * when sending a feature report.
+	 * Doing a HID_REQ_GET_REPORT and waiting for a while
+	 * seems to fix that.
+	 */
+	do {
+		ret = hid_hw_raw_request(steam->hdev, report_id,
+				buf, len,
+				HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+		if (ret != -EPIPE)
+			break;
+		msleep(20);
+	} while (--retries);
 	if (ret > 0) {
 		/* Remove the report ID from the return buffer */
 		ret--;
 		size = min(size, ret);
 		memcpy(data, buf + 1, size);
 	}
+
 	kfree(buf);
+	if (ret < 0)
+		hid_err(steam->hdev, "%s: error %d\n", __func__, ret);
 
 	if (ret < 0)
 		hid_err(steam->hdev, "%s: error %d\n", __func__, ret);
@@ -413,41 +511,60 @@ static int steam_recv_report(struct steam_device *steam,
 	return size;
 }
 
-static int steam_send_report(struct steam_device *steam,
-		u8 *cmd, int size)
+static int steam_recv_report(struct steam_device *steam,
+		u8 *data, int size)
+{
+	u8 report_id;
+
+	if (steam->quirks & STEAM_QUIRK_IBEX)
+		report_id = REPORT_ID_FEATURES_CONTROLLER;
+	else
+		report_id = 0;
+
+	return steam_recv_report_id(steam, data, size, report_id);
+}
+
+static int steam_send_report_id(struct steam_device *steam,
+		u8 *cmd, int size, u8 report_id)
 {
 	struct hid_report *r;
 	u8 *buf;
 	unsigned int retries = 50;
 	int ret;
+	u32 len;
 
-	r = steam->hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[0];
+	r = steam->hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[report_id];
 	if (!r) {
-		hid_err(steam->hdev, "No HID_FEATURE_REPORT submitted -  nothing to read\n");
+		hid_err(steam->hdev, "No HID_FEATURE_REPORT present for ID %u\n", report_id);
 		return -EINVAL;
 	}
 
-	if (hid_report_len(r) < 64)
+	len = hid_report_len(r);
+	if (len < 64)
 		return -EINVAL;
 
 	buf = hid_alloc_report_buf(r, GFP_KERNEL);
 	if (!buf)
 		return -ENOMEM;
 
-	/* The report ID is always 0 */
+	/* The report ID is always consistent */
+	buf[0] = report_id;
 	memcpy(buf + 1, cmd, size);
 
 	hid_dbg(steam->hdev, "Sending report %*ph\n", size, cmd);
 
+	if (!(steam->quirks & STEAM_QUIRK_IBEX))
+		len += 1;
+
 	/*
 	 * Sometimes the wireless controller fails with EPIPE
 	 * when sending a feature report.
-	 * Doing a HID_REQ_GET_REPORT and waiting for a while
+	 * Doing a HID_REQ_SET_REPORT and waiting for a while
 	 * seems to fix that.
 	 */
 	do {
-		ret = hid_hw_raw_request(steam->hdev, 0,
-				buf, max(size, 64) + 1,
+		ret = hid_hw_raw_request(steam->hdev, report_id,
+				buf, max(size + 1, len),
 				HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
 		if (ret != -EPIPE)
 			break;
@@ -461,6 +578,19 @@ static int steam_send_report(struct steam_device *steam,
 	return ret;
 }
 
+static int steam_send_report(struct steam_device *steam,
+		u8 *cmd, int size)
+{
+	u8 report_id;
+
+	if (steam->quirks & STEAM_QUIRK_IBEX)
+		report_id = REPORT_ID_FEATURES_CONTROLLER;
+	else
+		report_id = 0;
+
+	return steam_send_report_id(steam, cmd, size, report_id);
+}
+
 static inline int steam_send_report_byte(struct steam_device *steam, u8 cmd)
 {
 	return steam_send_report(steam, &cmd, 1);
@@ -473,7 +603,6 @@ static int steam_write_settings(struct steam_device *steam,
 	u8 reg;
 	u16 val;
 	u8 cmd[64] = {ID_SET_SETTINGS_VALUES, 0x00};
-	int ret;
 	va_list args;
 
 	va_start(args, steam);
@@ -489,16 +618,7 @@ static int steam_write_settings(struct steam_device *steam,
 	}
 	va_end(args);
 
-	ret = steam_send_report(steam, cmd, 2 + cmd[1]);
-	if (ret < 0)
-		return ret;
-
-	/*
-	 * Sometimes a lingering report for this command can
-	 * get read back instead of the last set report if
-	 * this isn't explicitly queried
-	 */
-	return steam_recv_report(steam, cmd, 2 + cmd[1]);
+	return steam_send_report(steam, cmd, 2 + cmd[1]);
 }
 
 static int steam_exchange_report(struct steam_device *steam, u8 *cmd, int csize,
@@ -591,15 +711,32 @@ static int steam_get_attributes(struct steam_device *steam)
 	return 0;
 }
 
-/*
- * This command requests the wireless adaptor to post an event
- * with the connection status. Useful if this driver is loaded when
- * the controller is already connected.
- */
-static inline int steam_request_conn_status(struct steam_device *steam)
+static int steam_get_conn_status(struct steam_device *steam)
 {
+	int ret = 0;
+	u8 cmd[] = {ID_DONGLE_GET_WIRELESS_STATE};
+	u8 reply[3] = {};
+	u8 report_id;
+
+	if (steam->quirks & STEAM_QUIRK_IBEX)
+		report_id = REPORT_ID_FEATURES_DONGLE;
+	else
+		report_id = 0;
+
 	guard(mutex)(&steam->report_mutex);
-	return steam_send_report_byte(steam, ID_DONGLE_GET_WIRELESS_STATE);
+	ret = steam_send_report_id(steam, cmd, sizeof(cmd), report_id);
+	if (ret < 0)
+		return ret;
+	ret = steam_recv_report_id(steam, reply, sizeof(reply), report_id);
+	if (ret < 0)
+		return ret;
+	if (reply[0] != ID_DONGLE_GET_WIRELESS_STATE || reply[1] < 1) {
+		hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
+				(int)sizeof(reply), reply);
+		return -EIO;
+	}
+
+	return reply[2];
 }
 
 /*
@@ -612,24 +749,42 @@ static inline int steam_haptic_pulse(struct steam_device *steam, u8 pad,
 				u16 duration, u16 interval, u16 count, u8 gain)
 {
 	int ret;
-	u8 report[10] = {ID_TRIGGER_HAPTIC_PULSE, 8};
 
 	/* Left and right are swapped on this report for legacy reasons */
 	if (pad < STEAM_PAD_BOTH)
 		pad ^= 1;
 
-	report[2] = pad;
-	report[3] = duration & 0xFF;
-	report[4] = duration >> 8;
-	report[5] = interval & 0xFF;
-	report[6] = interval >> 8;
-	report[7] = count & 0xFF;
-	report[8] = count >> 8;
-	report[9] = gain;
+	if (steam->quirks & STEAM_QUIRK_IBEX) {
+		struct steam_ibex_output_report *report =
+			kzalloc(sizeof(struct steam_ibex_output_report), GFP_KERNEL);
+
+		if (!report)
+			return -ENOMEM;
+
+		report->id = REPORT_ID_HAPTIC_PULSE;
+		report->pulse.side = pad;
+		put_unaligned_le16(duration, &report->pulse.on_us);
+		put_unaligned_le16(interval, &report->pulse.off_us);
+		put_unaligned_le16(count, &report->pulse.repeat_count);
+
+		ret = hid_hw_output_report(steam->hdev, (u8 *)report, 8);
+
+		kfree(report);
+	} else {
+		u8 report[10] = {ID_TRIGGER_HAPTIC_PULSE, 8, pad};
+
+		report[3] = duration & 0xFF;
+		report[4] = duration >> 8;
+		report[5] = interval & 0xFF;
+		report[6] = interval >> 8;
+		report[7] = count & 0xFF;
+		report[8] = count >> 8;
+		report[9] = gain;
+
+		guard(mutex)(&steam->report_mutex);
+		ret = steam_send_report(steam, report, 10);
+	}
 
-	mutex_lock(&steam->report_mutex);
-	ret = steam_send_report(steam, report, sizeof(report));
-	mutex_unlock(&steam->report_mutex);
 	return ret;
 }
 
@@ -638,20 +793,39 @@ static inline int steam_haptic_rumble(struct steam_device *steam,
 				u8 left_gain, u8 right_gain)
 {
 	int ret;
-	u8 report[11] = {ID_TRIGGER_RUMBLE_CMD, 9};
 
-	report[3] = intensity & 0xFF;
-	report[4] = intensity >> 8;
-	report[5] = left_speed & 0xFF;
-	report[6] = left_speed >> 8;
-	report[7] = right_speed & 0xFF;
-	report[8] = right_speed >> 8;
-	report[9] = left_gain;
-	report[10] = right_gain;
+	if (steam->quirks & STEAM_QUIRK_IBEX) {
+		struct steam_ibex_output_report *report =
+			kzalloc(sizeof(struct steam_ibex_output_report), GFP_KERNEL);
 
-	mutex_lock(&steam->report_mutex);
-	ret = steam_send_report(steam, report, sizeof(report));
-	mutex_unlock(&steam->report_mutex);
+		if (!report)
+			return -ENOMEM;
+
+		report->id = REPORT_ID_HAPTIC_RUMBLE;
+		put_unaligned_le16(intensity, &report->rumble.intensity);
+		put_unaligned_le16(left_speed, &report->rumble.left.speed);
+		report->rumble.left.gain = left_gain;
+		put_unaligned_le16(right_speed, &report->rumble.right.speed);
+		report->rumble.right.gain = right_gain;
+
+		ret = hid_hw_output_report(steam->hdev, (u8 *)report, 10);
+
+		kfree(report);
+	} else {
+		u8 report[11] = {ID_TRIGGER_RUMBLE_CMD, 9};
+
+		report[3] = intensity & 0xFF;
+		report[4] = intensity >> 8;
+		report[5] = left_speed & 0xFF;
+		report[6] = left_speed >> 8;
+		report[7] = right_speed & 0xFF;
+		report[8] = right_speed >> 8;
+		report[9] = left_gain;
+		report[10] = right_gain;
+
+		guard(mutex)(&steam->report_mutex);
+		ret = steam_send_report(steam, report, sizeof(report));
+	}
 	return ret;
 }
 
@@ -713,20 +887,18 @@ static void steam_set_lizard_mode(struct steam_device *steam, bool enable)
 		/* disable esc, enter, cursor */
 		steam_send_report_byte(steam, ID_CLEAR_DIGITAL_MAPPINGS);
 
-		if (steam->quirks & STEAM_QUIRK_DECK) {
+		if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))
 			steam_write_settings(steam,
-				SETTING_LEFT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
-				SETTING_RIGHT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
-				SETTING_LEFT_TRACKPAD_CLICK_PRESSURE, 0xFFFF, /* disable haptic click */
-				SETTING_RIGHT_TRACKPAD_CLICK_PRESSURE, 0xFFFF, /* disable haptic click */
-				SETTING_STEAM_WATCHDOG_ENABLE, 0, /* disable watchdog that tests if Steam is active */
+				/* disable lizard mode */
+				SETTING_LIZARD_MODE, 0,
+				/* disable watchdog that tests if Steam is active */
+				SETTING_STEAM_WATCHDOG_ENABLE, 0,
 				0);
-		} else {
+		else
 			steam_write_settings(steam,
 				SETTING_LEFT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
 				SETTING_RIGHT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
 				0);
-		}
 	}
 	mutex_unlock(&steam->report_mutex);
 }
@@ -742,7 +914,7 @@ static int steam_input_open(struct input_dev *dev)
 	 * Controller. On the Steam Deck, this is toggled manually by holding
 	 * the options button instead, handled by steam_mode_switch_cb.
 	 */
-	if (!(steam->quirks & STEAM_QUIRK_DECK)) {
+	if (!(steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))) {
 		spin_lock_irqsave(&steam->lock, flags);
 		set_lizard_mode = !steam->client_opened && lizard_mode;
 		spin_unlock_irqrestore(&steam->lock, flags);
@@ -759,7 +931,7 @@ static void steam_input_close(struct input_dev *dev)
 	unsigned long flags;
 	bool set_lizard_mode;
 
-	if (!(steam->quirks & STEAM_QUIRK_DECK)) {
+	if (!(steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))) {
 		spin_lock_irqsave(&steam->lock, flags);
 		set_lizard_mode = !steam->client_opened && lizard_mode;
 		spin_unlock_irqrestore(&steam->lock, flags);
@@ -817,25 +989,40 @@ static int steam_battery_get_property(struct power_supply *psy,
 {
 	struct steam_device *steam = power_supply_get_drvdata(psy);
 	unsigned long flags;
-	s16 volts;
+	u16 volts;
+	u16 curr;
+	u16 temp;
 	u8 batt;
+	u8 status;
 	int ret = 0;
 
 	spin_lock_irqsave(&steam->lock, flags);
-	volts = steam->voltage;
+	volts = steam->battery_voltage;
+	curr = steam->battery_current;
 	batt = steam->battery_charge;
+	temp = steam->battery_temp;
+	status = steam->battery_status;
 	spin_unlock_irqrestore(&steam->lock, flags);
 
 	switch (psp) {
 	case POWER_SUPPLY_PROP_PRESENT:
 		val->intval = 1;
 		break;
+	case POWER_SUPPLY_PROP_STATUS:
+		val->intval = status;
+		break;
 	case POWER_SUPPLY_PROP_SCOPE:
 		val->intval = POWER_SUPPLY_SCOPE_DEVICE;
 		break;
 	case POWER_SUPPLY_PROP_VOLTAGE_NOW:
 		val->intval = volts * 1000; /* mV -> uV */
 		break;
+	case POWER_SUPPLY_PROP_CURRENT_NOW:
+		val->intval = curr * 1000; /* mA -> uA */
+		break;
+	case POWER_SUPPLY_PROP_TEMP:
+		val->intval = temp / 100; /* thousandths °C -> tenths °C */
+		break;
 	case POWER_SUPPLY_PROP_CAPACITY:
 		val->intval = batt;
 		break;
@@ -846,6 +1033,16 @@ static int steam_battery_get_property(struct power_supply *psy,
 	return ret;
 }
 
+static enum power_supply_property steam_ibex_battery_props[] = {
+	POWER_SUPPLY_PROP_PRESENT,
+	POWER_SUPPLY_PROP_STATUS,
+	POWER_SUPPLY_PROP_SCOPE,
+	POWER_SUPPLY_PROP_VOLTAGE_NOW,
+	POWER_SUPPLY_PROP_CURRENT_NOW,
+	POWER_SUPPLY_PROP_CAPACITY,
+	POWER_SUPPLY_PROP_TEMP,
+};
+
 static int steam_battery_register(struct steam_device *steam)
 {
 	struct power_supply *battery;
@@ -854,19 +1051,35 @@ static int steam_battery_register(struct steam_device *steam)
 	int ret;
 
 	steam->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
-	steam->battery_desc.properties = steam_battery_props;
-	steam->battery_desc.num_properties = ARRAY_SIZE(steam_battery_props);
+	if (steam->quirks & STEAM_QUIRK_IBEX) {
+		steam->battery_desc.properties = steam_ibex_battery_props;
+		steam->battery_desc.num_properties = ARRAY_SIZE(steam_ibex_battery_props);
+		/*
+		 * Ibex needs a shorter name as it has a temperature and the
+		 * thermal zone name length limit is 20 characters. It's more
+		 * ambiguous sounding, so let's only use it when needed.
+		 */
+		steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
+				GFP_KERNEL, "steam-%s",
+				steam->serial_no);
+	} else {
+		steam->battery_desc.properties = steam_battery_props;
+		steam->battery_desc.num_properties = ARRAY_SIZE(steam_battery_props);
+		steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
+				GFP_KERNEL, "steam-controller-%s-battery",
+				steam->serial_no);
+	}
 	steam->battery_desc.get_property = steam_battery_get_property;
-	steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
-			GFP_KERNEL, "steam-controller-%s-battery",
-			steam->serial_no);
 	if (!steam->battery_desc.name)
 		return -ENOMEM;
 
 	/* avoid the warning of 0% battery while waiting for the first info */
 	spin_lock_irqsave(&steam->lock, flags);
-	steam->voltage = 3000;
+	steam->battery_voltage = 3000;
 	steam->battery_charge = 100;
+	steam->battery_current = 0;
+	steam->battery_temp = 200;
+	steam->battery_status = POWER_SUPPLY_STATUS_UNKNOWN;
 	spin_unlock_irqrestore(&steam->lock, flags);
 
 	battery = power_supply_register(&steam->hdev->dev,
@@ -938,7 +1151,7 @@ static int steam_input_register(struct steam_device *steam)
 	input_set_capability(input, EV_KEY, BTN_THUMB2);
 	input_set_capability(input, EV_KEY, BTN_GRIPL);
 	input_set_capability(input, EV_KEY, BTN_GRIPR);
-	if (steam->quirks & STEAM_QUIRK_DECK) {
+	if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX)) {
 		input_set_capability(input, EV_KEY, BTN_BASE);
 		input_set_capability(input, EV_KEY, BTN_GRIPL2);
 		input_set_capability(input, EV_KEY, BTN_GRIPR2);
@@ -952,7 +1165,7 @@ static int steam_input_register(struct steam_device *steam)
 	input_set_abs_params(input, ABS_HAT0Y, -32767, 32767,
 			STEAM_PAD_FUZZ, 0);
 
-	if (steam->quirks & STEAM_QUIRK_DECK) {
+	if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX)) {
 		input_set_abs_params(input, ABS_HAT2Y, 0, 32767, 0, 0);
 		input_set_abs_params(input, ABS_HAT2X, 0, 32767, 0, 0);
 
@@ -992,7 +1205,7 @@ static int steam_input_register(struct steam_device *steam)
 	input_abs_set_res(input, ABS_HAT0Y, STEAM_PAD_RESOLUTION);
 
 #ifdef CONFIG_STEAM_FF
-	if (steam->quirks & STEAM_QUIRK_DECK) {
+	if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX)) {
 		input_set_capability(input, EV_FF, FF_RUMBLE);
 		ret = input_ff_create_memless(input, NULL, steam_play_effect);
 		if (ret)
@@ -1017,6 +1230,7 @@ static int steam_sensors_register(struct steam_device *steam)
 	struct hid_device *hdev = steam->hdev;
 	struct input_dev *sensors;
 	int ret;
+	bool needs_open_close;
 
 	rcu_read_lock();
 	sensors = rcu_dereference(steam->sensors);
@@ -1032,7 +1246,27 @@ static int steam_sensors_register(struct steam_device *steam)
 
 	input_set_drvdata(sensors, steam);
 	sensors->dev.parent = &hdev->dev;
-	if (!(steam->quirks & STEAM_QUIRK_DECK)) {
+
+	/*
+	 * The open/close calls are needed in these specific cases:
+	 *
+	 * - Steam Controller (2015): Always
+	 * - Steam Deck: Never
+	 * - Steam Controller (2026): Only when wireless/BLE
+	 */
+	if (steam->quirks & STEAM_QUIRK_DECK)
+		needs_open_close = false;
+	else if (steam->quirks & (STEAM_QUIRK_WIRELESS | STEAM_QUIRK_BLE))
+		/* Both wireless Steam Controller setups */
+		needs_open_close = true;
+	else if (steam->quirks & STEAM_QUIRK_IBEX)
+		/* Wired Steam Controller (2026) */
+		needs_open_close = false;
+	else
+		/* Wired Steam Controller (2015) */
+		needs_open_close = true;
+
+	if (needs_open_close) {
 		sensors->open = steam_sensor_open;
 		sensors->close = steam_sensor_close;
 	}
@@ -1139,9 +1373,6 @@ static void steam_battery_unregister(struct steam_device *steam)
 static int steam_register(struct steam_device *steam)
 {
 	int ret;
-	unsigned long client_opened;
-	unsigned long flags;
-	bool do_add;
 
 	/*
 	 * This function can be called several times in a row with the
@@ -1149,51 +1380,42 @@ static int steam_register(struct steam_device *steam)
 	 * another client send a get_connection_status command, for example.
 	 * The battery and serial number are set just once per device.
 	 */
-	if (!steam->serial_no[0]) {
-		/*
-		 * Unlikely, but getting the serial could fail, and it is not so
-		 * important, so make up a serial number and go on.
-		 */
-		if (steam_get_serial(steam) < 0)
-			strscpy(steam->serial_no, "XXXXXXXXXX",
-					sizeof(steam->serial_no));
-
-		ret = steam_get_attributes(steam);
-		if (ret < 0)
-			hid_err(steam->hdev,
-				"%s:steam_get_attributes failed with error %d\n",
-				__func__, ret);
+	if (steam->serial_no[0])
+		return 0;
 
-		hid_info(steam->hdev, "Steam Controller '%s' connected",
-				steam->serial_no);
+	/*
+	 * Unlikely, but getting the serial could fail, and it is not so
+	 * important, so make up a serial number and go on.
+	 */
+	if (steam_get_serial(steam) < 0)
+		strscpy(steam->serial_no, "XXXXXXXXXX",
+				sizeof(steam->serial_no));
 
-		/* ignore battery errors, we can live without it */
-		if (steam->quirks & STEAM_QUIRK_WIRELESS)
-			steam_battery_register(steam);
+	ret = steam_get_attributes(steam);
+	if (ret < 0)
+		hid_err(steam->hdev,
+			"%s:steam_get_attributes failed with error %d\n",
+			__func__, ret);
 
-		do_add = true;
-	}
+	hid_info(steam->hdev, "Steam Controller '%s' connected",
+			steam->serial_no);
 
-	spin_lock_irqsave(&steam->lock, flags);
-	client_opened = steam->client_opened;
-	spin_unlock_irqrestore(&steam->lock, flags);
+	/* ignore battery errors, we can live without it */
+	if (steam->quirks & (STEAM_QUIRK_WIRELESS | STEAM_QUIRK_IBEX))
+		steam_battery_register(steam);
 
-	if (!client_opened) {
-		steam_set_lizard_mode(steam, lizard_mode);
-		ret = steam_input_register(steam);
-		if (ret != 0)
-			goto steam_register_input_fail;
-		ret = steam_sensors_register(steam);
-		if (ret != 0)
-			goto steam_register_sensors_fail;
-	}
+	steam_set_lizard_mode(steam, lizard_mode);
+	ret = steam_input_register(steam);
+	if (ret != 0)
+		goto steam_register_input_fail;
+	ret = steam_sensors_register(steam);
+	if (ret != 0)
+		goto steam_register_sensors_fail;
 
-	if (do_add) {
-		mutex_lock(&steam_devices_lock);
-		if (list_empty(&steam->list))
-			list_add(&steam->list, &steam_devices);
-		mutex_unlock(&steam_devices_lock);
-	}
+	mutex_lock(&steam_devices_lock);
+	if (list_empty(&steam->list))
+		list_add(&steam->list, &steam_devices);
+	mutex_unlock(&steam_devices_lock);
 	return 0;
 
 steam_register_sensors_fail:
@@ -1276,21 +1498,46 @@ static void steam_mode_switch_cb(struct work_struct *work)
 	}
 }
 
-static bool steam_is_valve_interface(struct hid_device *hdev)
+static bool steam_is_valve_interface(struct hid_device *hdev, int quirks)
 {
 	struct hid_report_enum *rep_enum;
 
 	/*
-	 * The wired device creates 3 interfaces:
+	 * The 2015 wired controller creates 3 interfaces:
 	 *  0: emulated mouse.
 	 *  1: emulated keyboard.
 	 *  2: the real game pad.
-	 * The wireless device creates 5 interfaces:
+	 * The 2015 wireless adapter creates 5 interfaces:
 	 *  0: emulated keyboard.
-	 *  1-4: slots where up to 4 real game pads will be connected to.
-	 * We know which one is the real gamepad interface because they are the
-	 * only ones with a feature report.
+	 *  1-4: slots where up to 4 real controllers will be connected to.
+	 * The Steam Deck creates 5 interfaces:
+	 *  0: emulated mouse.
+	 *  1: emulated keyboard.
+	 *  2: the real game pad.
+	 *  3-4: internal device comms (not HID).
+	 * The 2026 wired controller creates 1 unified interface.
+	 * The 2026 wireless puck creates 7 interfaces:
+	 *  0-1: internal device comms (not HID).
+	 *  2-5: slots where up to 4 real controllers will be connected to.
+	 *  6: basic pogo pin interface.
+	 *
+	 * We know which one is the real controller interface for the pre-2026
+	 * controllers because they are the only ones with a feature report.
+	 *
+	 * The puck's pogo pin interface should be ignored as it's stripped
+	 * down. It has one collection with usage page FF00 with usage ID 2.
 	 */
+	if (quirks & STEAM_QUIRK_IBEX) {
+		/* There is only one BLE HID interface */
+		if (quirks & STEAM_QUIRK_BLE)
+			return true;
+
+		if (hdev->maxcollection < 1)
+			return true;
+
+		return hdev->collection[0].usage != 0xFF000002;
+	}
+
 	rep_enum = &hdev->report_enum[HID_FEATURE_REPORT];
 	return !list_empty(&rep_enum->report_list);
 }
@@ -1350,6 +1597,14 @@ static int steam_client_ll_raw_request(struct hid_device *hdev,
 			report_type, reqtype);
 }
 
+static int steam_client_ll_output_report(struct hid_device *hdev,
+				u8 *buf, size_t count)
+{
+	struct steam_device *steam = hdev->driver_data;
+
+	return hid_hw_output_report(steam->hdev, buf, count);
+}
+
 static const struct hid_ll_driver steam_client_ll_driver = {
 	.parse = steam_client_ll_parse,
 	.start = steam_client_ll_start,
@@ -1357,6 +1612,7 @@ static const struct hid_ll_driver steam_client_ll_driver = {
 	.open = steam_client_ll_open,
 	.close = steam_client_ll_close,
 	.raw_request = steam_client_ll_raw_request,
+	.output_report = steam_client_ll_output_report,
 };
 
 static struct hid_device *steam_create_client_hid(struct hid_device *hdev)
@@ -1411,7 +1667,7 @@ static int steam_probe(struct hid_device *hdev,
 	 * The non-valve interfaces (mouse and keyboard emulation) are
 	 * connected without changes.
 	 */
-	if (!steam_is_valve_interface(hdev))
+	if (!steam_is_valve_interface(hdev, id->driver_data))
 		return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
 
 	steam = devm_kzalloc(&hdev->dev, sizeof(*steam), GFP_KERNEL);
@@ -1450,14 +1706,21 @@ static int steam_probe(struct hid_device *hdev,
 		goto err_hw_stop;
 	}
 
+	steam->connected = true;
+
 	if (steam->quirks & STEAM_QUIRK_WIRELESS) {
 		hid_info(hdev, "Steam wireless receiver connected");
 		/* If using a wireless adaptor ask for connection status */
 		steam->connected = false;
-		steam_request_conn_status(steam);
-	} else {
-		/* A wired connection is always present */
-		steam->connected = true;
+		ret = steam_get_conn_status(steam);
+		if (ret < 0)
+			hid_err(hdev,
+				"%s:steam_get_conn_status failed with error %d\n",
+				__func__, ret);
+		else if (ret == WIRELESS_EVENT_CONNECT)
+			steam->connected = true;
+	}
+	if (steam->connected) {
 		ret = steam_register(steam);
 		if (ret) {
 			hid_err(hdev,
@@ -1968,7 +2231,7 @@ static void steam_do_battery_event(struct steam_device *steam,
 	battery = rcu_dereference(steam->battery);
 	if (likely(battery)) {
 		spin_lock_irqsave(&steam->lock, flags);
-		steam->voltage = volts;
+		steam->battery_voltage = volts;
 		steam->battery_charge = batt;
 		spin_unlock_irqrestore(&steam->lock, flags);
 		power_supply_changed(battery);
@@ -1976,6 +2239,216 @@ static void steam_do_battery_event(struct steam_device *steam,
 	rcu_read_unlock();
 }
 
+/*
+ * The size for this message payload is 53 in REPORT_ID_INPUT and 45 in REPORT_ID_INPUT2.
+ * The values are:
+ *  (* values only in REPORT_ID_INPUT)
+ *  Offset| Type  | Mapped to |Meaning
+ * -------+-------+-----------+--------------------------
+ *  1     | u8    | --        | sequence number
+ *  2-5   | u32   | see below | buttons
+ *  6-7   | s16   | ABS_HAT2Y | left trigger (uncalibrated)
+ *  8-9   | s16   | ABS_HAT2X | right trigger (uncalibrated)
+ *  10-11 | s16   | ABS_X     | left joystick X
+ *  12-13 | s16   | ABS_Y     | left joystick Y
+ *  14-15 | s16   | ABS_RX    | right joystick X
+ *  16-17 | s16   | ABS_RY    | right joystick Y
+ *  18-19 | s16   | ABS_HAT0X | left-pad X value
+ *  20-21 | s16   | ABS_HAT0Y | left-pad Y value
+ *  22-23 | u16   | --        | left pad pressure
+ *  24-25 | s16   | ABS_HAT1X | right-pad X value
+ *  26-27 | s16   | ABS_HAT1Y | right-pad Y value
+ *  28-29 | u16   | --        | right pad pressure
+ *  30-33 | u32   | IMU MSC_TIMESTAMP | IMU timestamp
+ *  34-35 | s16   | IMU ABS_X | accelerometer X value
+ *  36-37 | s16   | IMU ABS_Z | accelerometer Y value
+ *  38-39 | s16   | IMU ABS_Y | accelerometer Z value
+ *  40-41 | s16   | IMU ABS_RX | gyro X value
+ *  42-43 | s16   | IMU ABS_RZ | gyro Y value
+ *  44-45 | s16   | IMU ABS_RY | gyro Z value
+ *  46-47 | s16   | --        | * quaternion W value
+ *  48-49 | s16   | --        | * quaternion X value
+ *  50-51 | s16   | --        | * quaternion Y value
+ *  52-53 | s16   | --        | * quaternion Z value
+ *
+ * The buttons are:
+ *  Bit  | Mapped to  | Description
+ * ------+------------+--------------------------------
+ *  2.0  | BTN_A      | button A
+ *  2.1  | BTN_B      | button B
+ *  2.2  | BTN_X      | button X
+ *  2.3  | BTN_Y      | button Y
+ *  2.4  | BTN_BASE   | quick access button
+ *  2.5  | BTN_THUMBR | right joystick clicked
+ *  2.6  | BTN_START  | menu
+ *  2.7  | BTN_GRIPR  | right top grip button
+ *  3.0  | BTN_GRIPR2 | right bottom grip button
+ *  3.1  | BTN_TR     | right shoulder
+ *  3.2  | BTN_DPAD_DOWN  | left-pad down
+ *  3.3  | BTN_DPAD_RIGHT | left-pad right
+ *  3.4  | BTN_DPAD_LEFT  | left-pad left
+ *  3.5  | BTN_DPAD_UP    | left-pad up
+ *  3.6  | BTN_SELECT | view
+ *  3.7  | BTN_THUMBL | left joystick clicked
+ *  4.0  | BTN_MODE   | steam logo
+ *  4.1  | BTN_GRIPL  | left top grip button
+ *  4.2  | BTN_GRIPL2 | left bottom grip button
+ *  4.3  | BTN_TL     | left shoulder
+ *  4.4  | --         | right joystick touched
+ *  4.5  | --         | right pad touched
+ *  4.6  | BTN_THUMB2 | right pad pressed
+ *  4.7  | BTN_TR2    | right trigger fully pressed
+ *  5.0  | --         | left joystick touched
+ *  5.1  | --         | left pad touched
+ *  5.2  | BTN_THUMB  | left pad pressed
+ *  5.3  | BTN_TL2    | left trigger fully pressed
+ *  5.4  | --         | right grip touch
+ *  5.5  | --         | left grip touch
+ *  5.6  | --         | unmapped
+ *  5.7  | --         | unmapped
+ */
+
+static const struct steam_button_mapping steam_ibex_button_mappings[] = {
+	{ BTN_A,		2, 0 },
+	{ BTN_B,		2, 1 },
+	{ BTN_X,		2, 2 },
+	{ BTN_Y,		2, 3 },
+	{ BTN_BASE,		2, 4 },
+	{ BTN_THUMBR,		2, 5 },
+	{ BTN_START,		2, 6 },
+	{ BTN_GRIPR,		2, 7 },
+	{ BTN_GRIPR2,		3, 0 },
+	{ BTN_TR,		3, 1 },
+	{ BTN_DPAD_DOWN,	3, 2 },
+	{ BTN_DPAD_RIGHT,	3, 3 },
+	{ BTN_DPAD_LEFT,	3, 4 },
+	{ BTN_DPAD_UP,		3, 5 },
+	{ BTN_SELECT,		3, 6 },
+	{ BTN_THUMBL,		3, 7 },
+	{ BTN_MODE,		4, 0 },
+	{ BTN_GRIPL,		4, 1 },
+	{ BTN_GRIPL2,		4, 2 },
+	{ BTN_TL,		4, 3 },
+	{ BTN_THUMB2,		4, 6 },
+	{ BTN_TR2,		4, 7 },
+	{ BTN_THUMB,		5, 2 },
+	{ BTN_TL2,		5, 3 },
+	{ /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_ibex_axis_mappings[] = {
+	{ ABS_X,	 1, 10 },
+	{ ABS_Y,	-1, 12 },
+	{ ABS_RX,	 1, 14 },
+	{ ABS_RY,	-1, 16 },
+	{ ABS_HAT2Y,	 1, 6 },
+	{ ABS_HAT2X,	 1, 8 },
+	{ /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_ibex_imu_mappings[] = {
+	{ ABS_X,   1, 34 },
+	{ ABS_Z,  -1, 36 },
+	{ ABS_Y,   1, 38 },
+	{ ABS_RX,  1, 40 },
+	{ ABS_RZ, -1, 42 },
+	{ ABS_RY,  1, 44 },
+	{ /* sentinel */ },
+};
+
+static void steam_do_ibex_input_event(struct steam_device *steam,
+		struct input_dev *input, const u8 *data)
+{
+	bool start_pressed;
+	bool lpad_touched, rpad_touched;
+
+	start_pressed = data[2] & BIT(6);
+
+	if (!start_pressed && steam->did_mode_switch) {
+		steam->did_mode_switch = false;
+		cancel_delayed_work(&steam->mode_switch);
+	} else if (!steam->client_opened && start_pressed && !steam->did_mode_switch) {
+		steam->did_mode_switch = true;
+		schedule_delayed_work(&steam->mode_switch, 45 * HZ / 100);
+	}
+
+	if (!steam->gamepad_mode && lizard_mode)
+		return;
+
+	lpad_touched = data[5] & BIT(1);
+	rpad_touched = data[4] & BIT(5);
+
+	if (lpad_touched) {
+		input_report_abs(input, ABS_HAT0X, steam_le16(data + 18));
+		input_report_abs(input, ABS_HAT0Y, steam_le16(data + 20));
+	} else {
+		input_report_abs(input, ABS_HAT0X, 0);
+		input_report_abs(input, ABS_HAT0Y, 0);
+	}
+
+	if (rpad_touched) {
+		input_report_abs(input, ABS_HAT1X, steam_le16(data + 24));
+		input_report_abs(input, ABS_HAT1Y, steam_le16(data + 26));
+	} else {
+		input_report_abs(input, ABS_HAT1X, 0);
+		input_report_abs(input, ABS_HAT1Y, 0);
+	}
+	steam_map_buttons(input, steam_ibex_button_mappings, data);
+	steam_map_axes(input, steam_ibex_axis_mappings, data);
+
+	input_sync(input);
+}
+
+static void steam_do_ibex_sensors_event(struct steam_device *steam,
+		struct input_dev *sensors, const u8 *data)
+{
+	u32 timestamp;
+
+	if (!steam->gamepad_mode && lizard_mode)
+		return;
+
+	timestamp = (u32) get_unaligned_le32((__le32 *)&data[30]);
+	input_event(sensors, EV_MSC, MSC_TIMESTAMP, timestamp);
+	steam_map_axes(sensors, steam_ibex_imu_mappings, data);
+
+	input_sync(sensors);
+}
+
+static void steam_do_ibex_battery_event(struct steam_device *steam,
+		struct power_supply *battery,
+		const struct steam_ibex_battery_status *data)
+{
+	unsigned long flags;
+
+	/* Creating the battery may have failed */
+	guard(rcu)();
+	battery = rcu_dereference(steam->battery);
+	if (!likely(battery))
+		return;
+
+	spin_lock_irqsave(&steam->lock, flags);
+	steam->battery_voltage = get_unaligned_le16(&data->battery_voltage);
+	steam->battery_current = get_unaligned_le16(&data->battery_current);
+	steam->battery_temp = get_unaligned_le16(&data->temperature);
+	steam->battery_charge = data->battery_level;
+	switch (data->charge_state) {
+	case CHARGE_STATE_CHARGING:
+		steam->battery_status = POWER_SUPPLY_STATUS_CHARGING;
+		break;
+	case CHARGE_STATE_DISCHARGING:
+		steam->battery_status = POWER_SUPPLY_STATUS_DISCHARGING;
+		break;
+	case CHARGE_STATE_CHARGING_DONE:
+		steam->battery_status = POWER_SUPPLY_STATUS_FULL;
+		break;
+	default:
+		steam->battery_status = POWER_SUPPLY_STATUS_UNKNOWN;
+		break;
+	}
+	spin_unlock_irqrestore(&steam->lock, flags);
+	power_supply_changed(battery);
+}
+
 static int steam_raw_event(struct hid_device *hdev,
 			struct hid_report *report, u8 *data,
 			int size)
@@ -1989,8 +2462,84 @@ static int steam_raw_event(struct hid_device *hdev,
 		return 0;
 
 	if (steam->client_opened)
-		hid_input_report(steam->client_hdev, HID_FEATURE_REPORT,
-				data, size, 0);
+		hid_input_report(steam->client_hdev, report->type, data, size, 0);
+
+	/* Ibex uses a different report format */
+	if (steam->quirks & STEAM_QUIRK_IBEX) {
+		if (report->type != HID_INPUT_REPORT)
+			return 0;
+
+		switch (report->id) {
+		case REPORT_ID_INPUT:
+			if (size != 54)
+				return 0;
+			if (steam->client_opened)
+				return 0;
+			rcu_read_lock();
+			input = rcu_dereference(steam->input);
+			if (likely(input)) {
+				steam_do_ibex_input_event(steam, input, data);
+			} else {
+				dbg_hid("%s: input data without connect event\n",
+					__func__);
+				steam_do_connect_event(steam, true);
+			}
+			sensors = rcu_dereference(steam->sensors);
+			if (likely(sensors))
+				steam_do_ibex_sensors_event(steam, sensors, data);
+			rcu_read_unlock();
+			break;
+		case REPORT_ID_INPUT2:
+			if (size != 46)
+				return 0;
+			if (steam->client_opened)
+				return 0;
+			rcu_read_lock();
+			input = rcu_dereference(steam->input);
+			if (likely(input)) {
+				steam_do_ibex_input_event(steam, input, data);
+			} else {
+				dbg_hid("%s: input data without connect event\n",
+					__func__);
+				steam_do_connect_event(steam, true);
+			}
+			sensors = rcu_dereference(steam->sensors);
+			if (likely(sensors))
+				steam_do_ibex_sensors_event(steam, sensors, data);
+			rcu_read_unlock();
+			break;
+		case REPORT_ID_BATTERY:
+			if (size != 15)
+				return 0;
+			rcu_read_lock();
+			battery = rcu_dereference(steam->battery);
+			if (likely(battery)) {
+				steam_do_ibex_battery_event(steam, battery,
+					(const struct steam_ibex_battery_status *)&data[1]);
+			} else {
+				dbg_hid("%s: battery data without connect event\n",
+					__func__);
+				steam_do_connect_event(steam, true);
+			}
+			rcu_read_unlock();
+			break;
+		case REPORT_ID_WIRELESS_EVENT:
+			if (size != 2)
+				return 0;
+			switch (data[1]) {
+			case WIRELESS_EVENT_DISCONNECT:
+				steam_do_connect_event(steam, false);
+				break;
+			case WIRELESS_EVENT_CONNECT:
+				steam_do_connect_event(steam, true);
+				break;
+			}
+			break;
+		}
+
+		return 0;
+	}
+
 	/*
 	 * All messages are size=64, all values little-endian.
 	 * The format is:
@@ -2113,6 +2662,26 @@ static const struct hid_device_id steam_controllers[] = {
 		USB_DEVICE_ID_STEAM_DECK),
 	  .driver_data = STEAM_QUIRK_DECK
 	},
+	{ /* Steam Controller (2026) wired */
+	  HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
+		USB_DEVICE_ID_STEAM_CONTROLLER_IBEX),
+	  .driver_data = STEAM_QUIRK_IBEX
+	},
+	{ /* Steam Controller (2026) BLE */
+	  HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_VALVE,
+		USB_DEVICE_ID_STEAM_CONTROLLER_IBEX_BLE),
+	  .driver_data = STEAM_QUIRK_IBEX | STEAM_QUIRK_BLE
+	},
+	{ /* Steam Controller (2026) Puck */
+	  HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
+		USB_DEVICE_ID_STEAM_CONTROLLER_PROTEUS),
+	  .driver_data = STEAM_QUIRK_IBEX | STEAM_QUIRK_WIRELESS
+	},
+	{ /* Steam Controller (2026) Steam Machine internal receiver */
+	  HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
+		USB_DEVICE_ID_STEAM_CONTROLLER_NEREID),
+	  .driver_data = STEAM_QUIRK_IBEX | STEAM_QUIRK_WIRELESS
+	},
 	{}
 };
 
-- 
2.54.0


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

* [PATCH 2/5] HID: steam: Fix wording of connect/disconnect logs
  2026-08-05  1:51 [PATCH 0/5] HID: steam: Add 2026 Steam Controller support Vicki Pfau
  2026-08-05  1:51 ` [PATCH 1/5] HID: steam: Initial " Vicki Pfau
@ 2026-08-05  1:51 ` Vicki Pfau
  2026-08-05  2:02   ` sashiko-bot
  2026-08-05  1:51 ` [PATCH 3/5] HID: steam: Don't set feature reports when disconnecting Vicki Pfau
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 10+ messages in thread
From: Vicki Pfau @ 2026-08-05  1:51 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau

It always said Controller, even on Deck. Since we special-case other
instances of Controller vs. Deck in strings, let's be consistent here too.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/hid/hid-steam.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 111e57926d04..fdf32722c8b3 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -1397,7 +1397,8 @@ static int steam_register(struct steam_device *steam)
 			"%s:steam_get_attributes failed with error %d\n",
 			__func__, ret);
 
-	hid_info(steam->hdev, "Steam Controller '%s' connected",
+	hid_info(steam->hdev, "Steam %s '%s' connected",
+			steam->quirks & STEAM_QUIRK_DECK ? "Deck" : "Controller",
 			steam->serial_no);
 
 	/* ignore battery errors, we can live without it */
@@ -1429,7 +1430,8 @@ static void steam_unregister(struct steam_device *steam)
 	if (!steam->serial_no[0])
 		return;
 
-	hid_info(steam->hdev, "Steam Controller '%s' disconnected",
+	hid_info(steam->hdev, "Steam %s '%s' disconnected",
+			steam->quirks & STEAM_QUIRK_DECK ? "Deck" : "Controller",
 			steam->serial_no);
 	steam_battery_unregister(steam);
 	steam_sensors_unregister(steam);
-- 
2.54.0


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

* [PATCH 3/5] HID: steam: Don't set feature reports when disconnecting
  2026-08-05  1:51 [PATCH 0/5] HID: steam: Add 2026 Steam Controller support Vicki Pfau
  2026-08-05  1:51 ` [PATCH 1/5] HID: steam: Initial " Vicki Pfau
  2026-08-05  1:51 ` [PATCH 2/5] HID: steam: Fix wording of connect/disconnect logs Vicki Pfau
@ 2026-08-05  1:51 ` Vicki Pfau
  2026-08-05  1:51 ` [PATCH 4/5] HID: steam: Clean up locking Vicki Pfau
  2026-08-05  1:51 ` [PATCH 5/5] HID: steam: Zero out inputs when disabling gamepad mode Vicki Pfau
  4 siblings, 0 replies; 10+ messages in thread
From: Vicki Pfau @ 2026-08-05  1:51 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau

When an input device is closed, we set a feature report to reset lizard
mode and IMU mode. However, if the input device is closed because it was
removed, then we will necessarily error out when sending this, resulting in
logged errors. Since an error here is expected, we should just fail
silently.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/hid/hid-steam.c | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index fdf32722c8b3..2d60b79441b6 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -489,12 +489,13 @@ static int steam_recv_report_id(struct steam_device *steam,
 	}
 
 	kfree(buf);
-	if (ret < 0)
-		hid_err(steam->hdev, "%s: error %d\n", __func__, ret);
-
-	if (ret < 0)
+	/*
+	 * Don't log if the failure is -ENODEV, as this
+	 * can happen normally on disconnect.
+	 */
+	if (ret < 0 && ret != -ENODEV)
 		hid_err(steam->hdev, "%s: error %d\n", __func__, ret);
-	else
+	else if (ret > 0)
 		hid_dbg(steam->hdev, "Received report %*ph\n", size, data);
 	if (ret < 0)
 		return ret;
@@ -572,7 +573,11 @@ static int steam_send_report_id(struct steam_device *steam,
 	} while (--retries);
 
 	kfree(buf);
-	if (ret < 0)
+	/*
+	 * Don't log if the failure is -ENODEV, as this
+	 * can happen normally on disconnect.
+	 */
+	if (ret < 0 && ret != -ENODEV)
 		hid_err(steam->hdev, "%s: error %d (%*ph)\n", __func__,
 				ret, size, cmd);
 	return ret;
-- 
2.54.0


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

* [PATCH 4/5] HID: steam: Clean up locking
  2026-08-05  1:51 [PATCH 0/5] HID: steam: Add 2026 Steam Controller support Vicki Pfau
                   ` (2 preceding siblings ...)
  2026-08-05  1:51 ` [PATCH 3/5] HID: steam: Don't set feature reports when disconnecting Vicki Pfau
@ 2026-08-05  1:51 ` Vicki Pfau
  2026-08-05  2:16   ` sashiko-bot
  2026-08-05  1:51 ` [PATCH 5/5] HID: steam: Zero out inputs when disabling gamepad mode Vicki Pfau
  4 siblings, 1 reply; 10+ messages in thread
From: Vicki Pfau @ 2026-08-05  1:51 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau

This cleans up several issues with locking behavior, including RCU accesses
not being guarded behind a lock.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/hid/hid-steam.c | 86 ++++++++++++++++++++---------------------
 1 file changed, 43 insertions(+), 43 deletions(-)

diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 2d60b79441b6..87af4bee9f5f 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -632,7 +632,6 @@ static int steam_exchange_report(struct steam_device *steam, u8 *cmd, int csize,
 	unsigned int retries = 5;
 	int ret;
 
-	guard(mutex)(&steam->report_mutex);
 	do {
 		ret = steam_send_report(steam, cmd, csize);
 		if (ret < 0)
@@ -728,7 +727,6 @@ static int steam_get_conn_status(struct steam_device *steam)
 	else
 		report_id = 0;
 
-	guard(mutex)(&steam->report_mutex);
 	ret = steam_send_report_id(steam, cmd, sizeof(cmd), report_id);
 	if (ret < 0)
 		return ret;
@@ -786,7 +784,6 @@ static inline int steam_haptic_pulse(struct steam_device *steam, u8 pad,
 		report[8] = count >> 8;
 		report[9] = gain;
 
-		guard(mutex)(&steam->report_mutex);
 		ret = steam_send_report(steam, report, 10);
 	}
 
@@ -828,7 +825,6 @@ static inline int steam_haptic_rumble(struct steam_device *steam,
 		report[9] = left_gain;
 		report[10] = right_gain;
 
-		guard(mutex)(&steam->report_mutex);
 		ret = steam_send_report(steam, report, sizeof(report));
 	}
 	return ret;
@@ -839,8 +835,10 @@ static void steam_haptic_rumble_cb(struct work_struct *work)
 	struct steam_device *steam = container_of(work, struct steam_device,
 							rumble_work);
 
+	mutex_lock(&steam->report_mutex);
 	steam_haptic_rumble(steam, 0, steam->rumble_left,
 		steam->rumble_right, 2, 0);
+	mutex_unlock(&steam->report_mutex);
 }
 
 static void steam_coalesce_rumble_cb(struct work_struct *work)
@@ -849,8 +847,10 @@ static void steam_coalesce_rumble_cb(struct work_struct *work)
 							struct steam_device,
 							coalesce_rumble_work);
 
+	mutex_lock(&steam->report_mutex);
 	steam_haptic_rumble(steam, 0, steam->rumble_left,
 		steam->rumble_right, 2, 0);
+	mutex_unlock(&steam->report_mutex);
 
 	if (steam->rumble_left || steam->rumble_right)
 		schedule_delayed_work(&steam->coalesce_rumble_work, HZ / 20);
@@ -882,7 +882,6 @@ static void steam_set_lizard_mode(struct steam_device *steam, bool enable)
 	if (steam->gamepad_mode)
 		enable = false;
 
-	mutex_lock(&steam->report_mutex);
 	if (enable) {
 		/* enable esc, enter, cursors */
 		steam_send_report_byte(steam, ID_SET_DEFAULT_DIGITAL_MAPPINGS);
@@ -905,14 +904,13 @@ static void steam_set_lizard_mode(struct steam_device *steam, bool enable)
 				SETTING_RIGHT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
 				0);
 	}
-	mutex_unlock(&steam->report_mutex);
 }
 
 static int steam_input_open(struct input_dev *dev)
 {
 	struct steam_device *steam = input_get_drvdata(dev);
 	unsigned long flags;
-	bool set_lizard_mode;
+	bool client_opened;
 
 	/*
 	 * Disabling lizard mode automatically is only done on the Steam
@@ -921,9 +919,10 @@ static int steam_input_open(struct input_dev *dev)
 	 */
 	if (!(steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))) {
 		spin_lock_irqsave(&steam->lock, flags);
-		set_lizard_mode = !steam->client_opened && lizard_mode;
+		client_opened = steam->client_opened;
 		spin_unlock_irqrestore(&steam->lock, flags);
-		if (set_lizard_mode)
+		guard(mutex)(&steam->report_mutex);
+		if (!client_opened && lizard_mode)
 			steam_set_lizard_mode(steam, false);
 	}
 
@@ -934,13 +933,14 @@ static void steam_input_close(struct input_dev *dev)
 {
 	struct steam_device *steam = input_get_drvdata(dev);
 	unsigned long flags;
-	bool set_lizard_mode;
+	bool client_opened;
 
 	if (!(steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))) {
 		spin_lock_irqsave(&steam->lock, flags);
-		set_lizard_mode = !steam->client_opened && lizard_mode;
+		client_opened = steam->client_opened;
 		spin_unlock_irqrestore(&steam->lock, flags);
-		if (set_lizard_mode)
+		guard(mutex)(&steam->report_mutex);
+		if (!client_opened && lizard_mode)
 			steam_set_lizard_mode(steam, true);
 	}
 }
@@ -948,14 +948,11 @@ static void steam_input_close(struct input_dev *dev)
 static int steam_sensor_open(struct input_dev *dev)
 {
 	struct steam_device *steam = input_get_drvdata(dev);
-	unsigned long flags;
-	bool client_opened;
 
-	spin_lock_irqsave(&steam->lock, flags);
-	client_opened = steam->client_opened;
-	spin_unlock_irqrestore(&steam->lock, flags);
-	if (client_opened)
-		return 0;
+	scoped_guard(spinlock_irqsave, &steam->lock) {
+		if (steam->client_opened)
+			return 0;
+	}
 
 	guard(mutex)(&steam->report_mutex);
 	steam_write_settings(steam, SETTING_IMU_MODE,
@@ -968,14 +965,11 @@ static int steam_sensor_open(struct input_dev *dev)
 static void steam_sensor_close(struct input_dev *dev)
 {
 	struct steam_device *steam = input_get_drvdata(dev);
-	unsigned long flags;
-	bool client_opened;
 
-	spin_lock_irqsave(&steam->lock, flags);
-	client_opened = steam->client_opened;
-	spin_unlock_irqrestore(&steam->lock, flags);
-	if (client_opened)
-		return;
+	scoped_guard(spinlock_irqsave, &steam->lock) {
+		if (steam->client_opened)
+			return;
+	}
 
 	guard(mutex)(&steam->report_mutex);
 	steam_write_settings(steam, SETTING_IMU_MODE, 0, 0);
@@ -1460,6 +1454,7 @@ static void steam_work_connect_cb(struct work_struct *work)
 	bool opened;
 	int ret;
 
+	guard(mutex)(&steam->report_mutex);
 	spin_lock_irqsave(&steam->lock, flags);
 	opened = steam->client_opened;
 	connected = steam->connected;
@@ -1482,23 +1477,28 @@ static void steam_mode_switch_cb(struct work_struct *work)
 							struct steam_device, mode_switch);
 	unsigned long flags;
 	bool client_opened;
+	bool gamepad_mode;
+
 	if (!lizard_mode)
 		return;
 
+	spin_lock_irqsave(&steam->lock, flags);
 	steam->gamepad_mode = !steam->gamepad_mode;
+	gamepad_mode = steam->gamepad_mode;
+	client_opened = steam->client_opened;
+	spin_unlock_irqrestore(&steam->lock, flags);
+
+	guard(mutex)(&steam->report_mutex);
 	hid_dbg(steam->hdev, "%s: switching gamepad mode to %i\n", __func__, steam->gamepad_mode);
-	if (steam->gamepad_mode)
+	if (gamepad_mode)
 		steam_set_lizard_mode(steam, false);
 	else {
-		spin_lock_irqsave(&steam->lock, flags);
-		client_opened = steam->client_opened;
-		spin_unlock_irqrestore(&steam->lock, flags);
 		if (!client_opened)
 			steam_set_lizard_mode(steam, lizard_mode);
 	}
 
 	steam_haptic_pulse(steam, STEAM_PAD_RIGHT, 0x190, 0, 1, 0);
-	if (steam->gamepad_mode) {
+	if (gamepad_mode) {
 		steam_haptic_pulse(steam, STEAM_PAD_LEFT, 0x14D, 0x14D, 0x2D, 0);
 	} else {
 		steam_haptic_pulse(steam, STEAM_PAD_LEFT, 0x1F4, 0x1F4, 0x1E, 0);
@@ -1728,7 +1728,9 @@ static int steam_probe(struct hid_device *hdev,
 			steam->connected = true;
 	}
 	if (steam->connected) {
+		mutex_lock(&steam->report_mutex);
 		ret = steam_register(steam);
+		mutex_unlock(&steam->report_mutex);
 		if (ret) {
 			hid_err(hdev,
 				"%s:steam_register failed with error %d\n",
@@ -1787,19 +1789,17 @@ static void steam_remove(struct hid_device *hdev)
 	if (steam->quirks & STEAM_QUIRK_WIRELESS) {
 		hid_info(hdev, "Steam wireless receiver disconnected");
 	}
+	guard(mutex)(&steam->report_mutex);
 	steam_unregister(steam);
 	hid_hw_stop(hdev);
 }
 
 static void steam_do_connect_event(struct steam_device *steam, bool connected)
 {
-	unsigned long flags;
 	bool changed;
 
-	spin_lock_irqsave(&steam->lock, flags);
 	changed = steam->connected != connected;
 	steam->connected = connected;
-	spin_unlock_irqrestore(&steam->lock, flags);
 
 	if (changed && schedule_work(&steam->work_connect) == 0)
 		dbg_hid("%s: connected=%d event already queued\n",
@@ -2228,8 +2228,6 @@ static void steam_do_deck_sensors_event(struct steam_device *steam,
 static void steam_do_battery_event(struct steam_device *steam,
 		struct power_supply *battery, u8 *data)
 {
-	unsigned long flags;
-
 	s16 volts = steam_le16(data + 12);
 	u8 batt = data[14];
 
@@ -2237,10 +2235,8 @@ static void steam_do_battery_event(struct steam_device *steam,
 	rcu_read_lock();
 	battery = rcu_dereference(steam->battery);
 	if (likely(battery)) {
-		spin_lock_irqsave(&steam->lock, flags);
 		steam->battery_voltage = volts;
 		steam->battery_charge = batt;
-		spin_unlock_irqrestore(&steam->lock, flags);
 		power_supply_changed(battery);
 	}
 	rcu_read_unlock();
@@ -2425,15 +2421,12 @@ static void steam_do_ibex_battery_event(struct steam_device *steam,
 		struct power_supply *battery,
 		const struct steam_ibex_battery_status *data)
 {
-	unsigned long flags;
-
 	/* Creating the battery may have failed */
 	guard(rcu)();
 	battery = rcu_dereference(steam->battery);
 	if (!likely(battery))
 		return;
 
-	spin_lock_irqsave(&steam->lock, flags);
 	steam->battery_voltage = get_unaligned_le16(&data->battery_voltage);
 	steam->battery_current = get_unaligned_le16(&data->battery_current);
 	steam->battery_temp = get_unaligned_le16(&data->temperature);
@@ -2452,7 +2445,6 @@ static void steam_do_ibex_battery_event(struct steam_device *steam,
 		steam->battery_status = POWER_SUPPLY_STATUS_UNKNOWN;
 		break;
 	}
-	spin_unlock_irqrestore(&steam->lock, flags);
 	power_supply_changed(battery);
 }
 
@@ -2468,6 +2460,7 @@ static int steam_raw_event(struct hid_device *hdev,
 	if (!steam)
 		return 0;
 
+	guard(spinlock_irqsave)(&steam->lock);
 	if (steam->client_opened)
 		hid_input_report(steam->client_hdev, report->type, data, size, 0);
 
@@ -2631,6 +2624,8 @@ static int steam_param_set_lizard_mode(const char *val,
 {
 	struct steam_device *steam;
 	int ret;
+	bool client_opened;
+	unsigned long flags;
 
 	ret = param_set_bool(val, kp);
 	if (ret)
@@ -2638,8 +2633,13 @@ static int steam_param_set_lizard_mode(const char *val,
 
 	mutex_lock(&steam_devices_lock);
 	list_for_each_entry(steam, &steam_devices, list) {
-		if (!steam->client_opened)
+		spin_lock_irqsave(&steam->lock, flags);
+		client_opened = steam->client_opened;
+		spin_unlock_irqrestore(&steam->lock, flags);
+		if (client_opened) {
+			guard(mutex)(&steam->report_mutex);
 			steam_set_lizard_mode(steam, lizard_mode);
+		}
 	}
 	mutex_unlock(&steam_devices_lock);
 	return 0;
-- 
2.54.0


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

* [PATCH 5/5] HID: steam: Zero out inputs when disabling gamepad mode
  2026-08-05  1:51 [PATCH 0/5] HID: steam: Add 2026 Steam Controller support Vicki Pfau
                   ` (3 preceding siblings ...)
  2026-08-05  1:51 ` [PATCH 4/5] HID: steam: Clean up locking Vicki Pfau
@ 2026-08-05  1:51 ` Vicki Pfau
  2026-08-05  2:09   ` sashiko-bot
  4 siblings, 1 reply; 10+ messages in thread
From: Vicki Pfau @ 2026-08-05  1:51 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau

When gamepad mode is disabled the gamepad input devices will stop receiving
updates. However, in the case where there are buttons still pressed this
will appear as an indefinitely-held button. Instead we should zero out the
inputs to make it look like things are all released. We do the same thing
for gyroscope inputs to make sure it doesn't look like it's endlessly
rotating, but we freeze the accelerometer input since zero isn't a neutral
input on the surface of the Earth.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 drivers/hid/hid-steam.c | 69 +++++++++++++++++++++++++++++++++++++++--
 1 file changed, 67 insertions(+), 2 deletions(-)

diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 87af4bee9f5f..93a6f4e17fb7 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -1490,11 +1490,76 @@ static void steam_mode_switch_cb(struct work_struct *work)
 
 	guard(mutex)(&steam->report_mutex);
 	hid_dbg(steam->hdev, "%s: switching gamepad mode to %i\n", __func__, steam->gamepad_mode);
-	if (gamepad_mode)
+	if (gamepad_mode) {
 		steam_set_lizard_mode(steam, false);
-	else {
+	} else {
+		struct input_dev *input;
+		struct input_dev *sensors;
+
 		if (!client_opened)
 			steam_set_lizard_mode(steam, lizard_mode);
+
+		/*
+		 * Zero out inputs so it doesn't look like we're holding
+		 * anything indefinitely.
+		 */
+		guard(spinlock_irqsave)(&steam->lock);
+		rcu_read_lock();
+		input = rcu_dereference(steam->input);
+		if (likely(input)) {
+			input_report_key(input, BTN_TR2, 0);
+			input_report_key(input, BTN_TL2, 0);
+			input_report_key(input, BTN_TR, 0);
+			input_report_key(input, BTN_TL, 0);
+			input_report_key(input, BTN_Y, 0);
+			input_report_key(input, BTN_B, 0);
+			input_report_key(input, BTN_X, 0);
+			input_report_key(input, BTN_A, 0);
+			input_report_key(input, BTN_DPAD_UP, 0);
+			input_report_key(input, BTN_DPAD_RIGHT, 0);
+			input_report_key(input, BTN_DPAD_LEFT, 0);
+			input_report_key(input, BTN_DPAD_DOWN, 0);
+			input_report_key(input, BTN_SELECT, 0);
+			input_report_key(input, BTN_MODE, 0);
+			input_report_key(input, BTN_START, 0);
+			input_report_key(input, BTN_THUMBR, 0);
+			input_report_key(input, BTN_THUMBL, 0);
+			input_report_key(input, BTN_THUMB, 0);
+			input_report_key(input, BTN_THUMB2, 0);
+			input_report_key(input, BTN_GRIPL, 0);
+			input_report_key(input, BTN_GRIPR, 0);
+
+			input_report_abs(input, ABS_X, 0);
+			input_report_abs(input, ABS_Y, 0);
+			input_report_abs(input, ABS_RX, 0);
+			input_report_abs(input, ABS_RY, 0);
+			input_report_abs(input, ABS_HAT0X, 0);
+			input_report_abs(input, ABS_HAT0Y, 0);
+			input_report_abs(input, ABS_HAT2Y, 0);
+			input_report_abs(input, ABS_HAT2X, 0);
+
+			if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX)) {
+				input_report_key(input, BTN_BASE, 0);
+				input_report_key(input, BTN_GRIPL2, 0);
+				input_report_key(input, BTN_GRIPR2, 0);
+
+				input_report_abs(input, ABS_HAT1X, 0);
+				input_report_abs(input, ABS_HAT1Y, 0);
+			}
+
+			input_sync(input);
+		}
+		sensors = rcu_dereference(steam->sensors);
+		if (likely(sensors)) {
+			/* Skip accelerometers since 0 isn't a neutral input */
+
+			input_report_abs(sensors, ABS_RX, 0);
+			input_report_abs(sensors, ABS_RY, 0);
+			input_report_abs(sensors, ABS_RZ, 0);
+
+			input_sync(sensors);
+		}
+		rcu_read_unlock();
 	}
 
 	steam_haptic_pulse(steam, STEAM_PAD_RIGHT, 0x190, 0, 1, 0);
-- 
2.54.0


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

* Re: [PATCH 2/5] HID: steam: Fix wording of connect/disconnect logs
  2026-08-05  1:51 ` [PATCH 2/5] HID: steam: Fix wording of connect/disconnect logs Vicki Pfau
@ 2026-08-05  2:02   ` sashiko-bot
  0 siblings, 0 replies; 10+ messages in thread
From: sashiko-bot @ 2026-08-05  2:02 UTC (permalink / raw)
  To: Vicki Pfau; +Cc: dmitry.torokhov, linux-input

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

Pre-existing issues:
- [High] The error path in steam_register() fails to unregister the battery, leading to a leaked power supply and subsequent use-after-free if probe fails.
--

--- Patch [2]: [PATCH 2/5] HID: steam: Fix wording of connect/disconnect logs ---
commit 86b8e9ab8ffc7b90075c933cefda13a663141100
Author: Vicki Pfau <vi@endrift.com>

HID: steam: Fix wording of connect/disconnect logs
    
It always said Controller, even on Deck. Since we special-case other
instances of Controller vs. Deck in strings, let's be consistent here too.

> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index 111e57926d049..fdf32722c8b31 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
> @@ -1397,7 +1397,8 @@ static int steam_register(struct steam_device *steam)
>  			"%s:steam_get_attributes failed with error %d\n",
>  			__func__, ret);
>  
> -	hid_info(steam->hdev, "Steam Controller '%s' connected",
> +	hid_info(steam->hdev, "Steam %s '%s' connected",
> +			steam->quirks & STEAM_QUIRK_DECK ? "Deck" : "Controller",
>  			steam->serial_no);
>  
>  	/* ignore battery errors, we can live without it */

[Severity: High]
This is a pre-existing issue, but I noticed a potential use-after-free
issue regarding the battery registration near this log message.

If steam_battery_register() is called, but a subsequent registration fails:

drivers/hid/hid-steam.c:steam_register() {
    ...
	if (steam->quirks & (STEAM_QUIRK_WIRELESS | STEAM_QUIRK_IBEX))
		steam_battery_register(steam);
    ...
	ret = steam_input_register(steam);
	if (ret != 0)
		goto steam_register_input_fail;
	ret = steam_sensors_register(steam);
	if (ret != 0)
		goto steam_register_sensors_fail;
    ...
steam_register_sensors_fail:
	steam_input_unregister(steam);
steam_register_input_fail:
	return ret;
}

Could this error path leave the power supply registered when steam_register()
returns an error?

If probe fails, the driver core will release devres allocations, freeing the
steam_device struct. A user querying sysfs for battery properties could then
trigger a use-after-free since the power supply subsystem would still have
the freed pointer.

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

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

* Re: [PATCH 5/5] HID: steam: Zero out inputs when disabling gamepad mode
  2026-08-05  1:51 ` [PATCH 5/5] HID: steam: Zero out inputs when disabling gamepad mode Vicki Pfau
@ 2026-08-05  2:09   ` sashiko-bot
  0 siblings, 0 replies; 10+ messages in thread
From: sashiko-bot @ 2026-08-05  2:09 UTC (permalink / raw)
  To: Vicki Pfau; +Cc: dmitry.torokhov, linux-input

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

New issues:
- [High] AB-BA deadlock between `report_mutex` and workqueues during device unregistration.

Pre-existing issues:
- [High] Concurrent unregistration in `steam_probe` error path leads to a double free of input devices.
--

--- Patch [5]: [PATCH 5/5] HID: steam: Zero out inputs when disabling gamepad mode ---
commit 7a64f604e0a34949b1434a03544f0ca71ef9305f
Author: Vicki Pfau <vi@endrift.com>

HID: steam: Zero out inputs when disabling gamepad mode

When gamepad mode is disabled the gamepad input devices will stop receiving
updates. However, in the case where there are buttons still pressed this
will appear as an indefinitely-held button. Instead we should zero out the
inputs to make it look like things are all released.

> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index 87af4bee9f5ff..93a6f4e17fb75 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
> @@ -1490,11 +1490,76 @@ static void steam_mode_switch_cb(struct work_struct *work)
>  
>  	guard(mutex)(&steam->report_mutex);

[Severity: High]
Is there a risk of an AB-BA deadlock here during device unregistration?

If steam_remove() is called and acquires steam->report_mutex, it then calls
steam_unregister(). If steam_unregister() then calls
cancel_delayed_work_sync(&steam->mode_switch) while the mutex is held:

steam_remove()
  guard(mutex)(&steam->report_mutex);
  steam_unregister(steam);
    cancel_delayed_work_sync(&steam->mode_switch);

This workqueue callback would block here attempting to acquire the same
steam->report_mutex, while steam_remove() blocks waiting for this work
to finish. Since the wait condition is permanent, this could hang the USB/HID
hotplug subsystem.

[ ... ]

[Severity: High]
This is a pre-existing issue, but could the error path in steam_probe() race
with disconnect events and cause a double-free of the input devices?

If steam_probe() fails in hid_add_device(), it jumps to err_steam_unregister
and calls steam_unregister() if steam->connected is true (checked locklessly):

steam_probe():
err_steam_unregister:
	if (steam->connected)
		steam_unregister(steam);

Because hid_device_io_start() was already called, a concurrent disconnect
event can queue steam_work_connect_cb(). If that worker runs, sees
connected == false, and also calls steam_unregister(steam), both threads can
concurrently enter steam_input_unregister() since the error path does not
hold report_mutex.

Both threads would then read steam->input, find it valid, set it to NULL
locklessly, and call input_unregister_device() on the same pointer:

steam_input_unregister():
	input = rcu_dereference(steam->input);
	...
	RCU_INIT_POINTER(steam->input, NULL);
	...
	input_unregister_device(input);

Can this lead to a double-free of the input_dev structure?

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

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

* Re: [PATCH 1/5] HID: steam: Initial 2026 Steam Controller support
  2026-08-05  1:51 ` [PATCH 1/5] HID: steam: Initial " Vicki Pfau
@ 2026-08-05  2:13   ` sashiko-bot
  0 siblings, 0 replies; 10+ messages in thread
From: sashiko-bot @ 2026-08-05  2:13 UTC (permalink / raw)
  To: Vicki Pfau; +Cc: linux-input, dmitry.torokhov

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

New issues:
- [High] Battery registration fails due to thermal zone name length limits if the device serial number is long.
- [Medium] Incorrect initialization of battery temperature results in reporting 0.2 °C.
- [Low] Duplicate error logging block introduced in steam_recv_report_id.

Pre-existing issues:
- [High] Probe failure leaves a registered power supply with dangling devm pointers.
- [High] Unbinding the driver while the device is active can lead to a Use-After-Free in steam_work_connect_cb.
--

--- Patch [1]: [PATCH 1/5] HID: steam: Initial 2026 Steam Controller support ---
commit 10f5a4093529f8973ab3200723fc61d0c419c6bb
Author: Vicki Pfau <vi@endrift.com>

HID: steam: Initial 2026 Steam Controller support

This brings support for the 2026 Steam Controller, comparably featureful to
the existing support for the Steam Deck.

> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index 6199f67f3c4cd..111e57926d049 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c

[ ... ]

> @@ -378,21 +459,38 @@ static int steam_recv_report(struct steam_device *steam,
>  		return -ENOMEM;
>  
>  	/*
> -	 * The report ID is always 0, so strip the first byte from the output.
> +	 * The report ID is consistent, so strip the first byte from the output.
>  	 * hid_report_len() is not counting the report ID, so +1 to the length
>  	 * or else we get a EOVERFLOW. We are safe from a buffer overflow
>  	 * because hid_alloc_report_buf() allocates +7 bytes.
>  	 */
> -	ret = hid_hw_raw_request(steam->hdev, 0x00,
> -			buf, hid_report_len(r) + 1,
> -			HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
> +	if (!(steam->quirks & STEAM_QUIRK_IBEX))
> +		len += 1;
> +
> +	/*
> +	 * Sometimes the wireless controller fails with EPIPE
> +	 * when sending a feature report.
> +	 * Doing a HID_REQ_GET_REPORT and waiting for a while
> +	 * seems to fix that.
> +	 */
> +	do {
> +		ret = hid_hw_raw_request(steam->hdev, report_id,
> +				buf, len,
> +				HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
> +		if (ret != -EPIPE)
> +			break;
> +		msleep(20);
> +	} while (--retries);
>  	if (ret > 0) {
>  		/* Remove the report ID from the return buffer */
>  		ret--;
>  		size = min(size, ret);
>  		memcpy(data, buf + 1, size);
>  	}
> +
>  	kfree(buf);
> +	if (ret < 0)
> +		hid_err(steam->hdev, "%s: error %d\n", __func__, ret);
>  
>  	if (ret < 0)
>  		hid_err(steam->hdev, "%s: error %d\n", __func__, ret);

[Severity: Low]
Is this error logging block accidentally duplicated? There are two identical
checks and hid_err() calls sequentially in steam_recv_report_id(), which will
cause the exact same error message to be printed twice upon a single
hid_hw_raw_request() failure.

[ ... ]

> @@ -854,19 +1051,35 @@ static int steam_battery_register(struct steam_device *steam)
>  	int ret;
>  
>  	steam->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
> -	steam->battery_desc.properties = steam_battery_props;
> -	steam->battery_desc.num_properties = ARRAY_SIZE(steam_battery_props);
> +	if (steam->quirks & STEAM_QUIRK_IBEX) {
> +		steam->battery_desc.properties = steam_ibex_battery_props;
> +		steam->battery_desc.num_properties = ARRAY_SIZE(steam_ibex_battery_props);
> +		/*
> +		 * Ibex needs a shorter name as it has a temperature and the
> +		 * thermal zone name length limit is 20 characters. It's more
> +		 * ambiguous sounding, so let's only use it when needed.
> +		 */
> +		steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
> +				GFP_KERNEL, "steam-%s",
> +				steam->serial_no);

[Severity: High]
Will this name exceed the 20-character maximum permitted for a thermal zone
device type if the serial number is long?

Because STEAM_SERIAL_LEN is 21, a serial number of 14 characters or
longer combined with the "steam-" prefix will exceed 20 characters. This
causes thermal_zone_device_register_with_trips() to return -EINVAL,
resulting in power_supply_register() and steam_battery_register() failing
entirely.

> +	} else {
> +		steam->battery_desc.properties = steam_battery_props;
> +		steam->battery_desc.num_properties = ARRAY_SIZE(steam_battery_props);
> +		steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
> +				GFP_KERNEL, "steam-controller-%s-battery",
> +				steam->serial_no);
> +	}
>  	steam->battery_desc.get_property = steam_battery_get_property;
> -	steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
> -			GFP_KERNEL, "steam-controller-%s-battery",
> -			steam->serial_no);
>  	if (!steam->battery_desc.name)
>  		return -ENOMEM;
>  
>  	/* avoid the warning of 0% battery while waiting for the first info */
>  	spin_lock_irqsave(&steam->lock, flags);
> -	steam->voltage = 3000;
> +	steam->battery_voltage = 3000;
>  	steam->battery_charge = 100;
> +	steam->battery_current = 0;
> +	steam->battery_temp = 200;

[Severity: Medium]
Does this initialize the default temperature to 0.2 degrees Celsius?

In steam_battery_get_property(), this value is divided by 100:
val->intval = temp / 100;

Since the power supply subsystem expects POWER_SUPPLY_PROP_TEMP in tenths
of a degree Celsius, returning 2 represents 0.2 C. If the intended default
temperature was 20.0 C, should this be initialized to 20000?

> +	steam->battery_status = POWER_SUPPLY_STATUS_UNKNOWN;
>  	spin_unlock_irqrestore(&steam->lock, flags);
>  
>  	battery = power_supply_register(&steam->hdev->dev,

[ ... ]

> @@ -1149,51 +1380,42 @@ static int steam_register(struct steam_device *steam)
> -	spin_lock_irqsave(&steam->lock, flags);
> -	client_opened = steam->client_opened;
> -	spin_unlock_irqrestore(&steam->lock, flags);
> +	/* ignore battery errors, we can live without it */
> +	if (steam->quirks & (STEAM_QUIRK_WIRELESS | STEAM_QUIRK_IBEX))
> +		steam_battery_register(steam);
>  
> -	if (!client_opened) {
> -		steam_set_lizard_mode(steam, lizard_mode);
> -		ret = steam_input_register(steam);
> -		if (ret != 0)
> -			goto steam_register_input_fail;
> -		ret = steam_sensors_register(steam);
> -		if (ret != 0)
> -			goto steam_register_sensors_fail;
> -	}
> +	steam_set_lizard_mode(steam, lizard_mode);
> +	ret = steam_input_register(steam);
> +	if (ret != 0)
> +		goto steam_register_input_fail;
> +	ret = steam_sensors_register(steam);
> +	if (ret != 0)
> +		goto steam_register_sensors_fail;

[Severity: High]
This is a pre-existing issue, but does this error path leave the battery
registered if steam_input_register() or steam_sensors_register() fails?

If steam_register() fails, steam_probe() jumps to the err_hw_close label,
which bypasses steam_unregister(). Because steam_probe() returns an error,
devres will free the steam_device structure.

Since the battery was not unregistered, it remains active in sysfs, and any
subsequent userspace read of the battery's properties will dereference the
freed steam_device pointer, causing a kernel use-after-free.

[ ... ]

> @@ -1989,8 +2462,84 @@ static int steam_raw_event(struct hid_device *hdev,
>  		return 0;
>  
>  	if (steam->client_opened)
> -		hid_input_report(steam->client_hdev, HID_FEATURE_REPORT,
> -				data, size, 0);
> +		hid_input_report(steam->client_hdev, report->type, data, size, 0);
> +
> +	/* Ibex uses a different report format */
> +	if (steam->quirks & STEAM_QUIRK_IBEX) {
> +		if (report->type != HID_INPUT_REPORT)
> +			return 0;
> +
> +		switch (report->id) {
> +		case REPORT_ID_INPUT:
> +			if (size != 54)

[ ... ]

> +		case REPORT_ID_WIRELESS_EVENT:
> +			if (size != 2)
> +				return 0;
> +			switch (data[1]) {
> +			case WIRELESS_EVENT_DISCONNECT:
> +				steam_do_connect_event(steam, false);
> +				break;
> +			case WIRELESS_EVENT_CONNECT:
> +				steam_do_connect_event(steam, true);
> +				break;
> +			}
> +			break;

[Severity: High]
This is a pre-existing issue, but can this lead to a use-after-free during
driver unbind?

When the driver is unbound, steam_remove() cancels the steam->work_connect
work item and calls steam_unregister(), but hid_hw_stop() is not called
until the very end.

In this window, the device can still stream data and trigger
steam_raw_event(), which can call steam_do_connect_event(). This
reschedules steam->work_connect back onto the workqueue. Once
steam_remove() returns, devres frees the steam_device structure, causing
the scheduled work item to execute steam_work_connect_cb() on freed memory.

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

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

* Re: [PATCH 4/5] HID: steam: Clean up locking
  2026-08-05  1:51 ` [PATCH 4/5] HID: steam: Clean up locking Vicki Pfau
@ 2026-08-05  2:16   ` sashiko-bot
  0 siblings, 0 replies; 10+ messages in thread
From: sashiko-bot @ 2026-08-05  2:16 UTC (permalink / raw)
  To: Vicki Pfau; +Cc: linux-input, dmitry.torokhov

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

New issues:
- [High] Self-deadlock in `steam_unregister` due to `report_mutex` acquisition.
- [High] AB-BA lock inversion between `steam_devices_lock` and `steam->report_mutex`.
- [Medium] Inverted lizard mode logic in `steam_param_set_lizard_mode` breaks the parameter.
- [Medium] Missing `report_mutex` in `steam_get_conn_status` creates a race condition with concurrent hardware access.

Pre-existing issues:
- [High] Use-after-free caused by cancelling workqueues before stopping the hardware.
- [High] Resource leak and use-after-free of `power_supply` object on probe failure.
--

--- Patch [4]: [PATCH 4/5] HID: steam: Clean up locking ---
commit 4a13ea9d8650f6b804ba3ebe1f98c65bb697f0c4
Author: Vicki Pfau <vi@endrift.com>

HID: steam: Clean up locking

This cleans up several issues with locking behavior, including RCU accesses
not being guarded behind a lock.

> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index 2d60b79441b69..87af4bee9f5ff 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c

[ ... ]

> @@ -728,7 +727,6 @@ static int steam_get_conn_status(struct steam_device *steam)
>  	else
>  		report_id = 0;
>  
> -	guard(mutex)(&steam->report_mutex);
>  	ret = steam_send_report_id(steam, cmd, sizeof(cmd), report_id);

[Severity: Medium]
Will this removal create a race condition with concurrent hardware access?
During steam_probe() in hid-steam.c, the hardware is opened and interrupts
are enabled before steam_get_conn_status() is called. If the device
immediately sends a connection event, the interrupt handler schedules
steam_work_connect_cb(), which acquires report_mutex and sends feature
reports. Without the mutex here, can the control transfers from
steam_get_conn_status() interleave with the worker's transfers and
confuse the controller firmware?

[ ... ]

> @@ -1728,7 +1728,9 @@ static int steam_probe(struct hid_device *hdev,
>  			steam->connected = true;
>  	}
>  	if (steam->connected) {
> +		mutex_lock(&steam->report_mutex);
>  		ret = steam_register(steam);
> +		mutex_unlock(&steam->report_mutex);
>  		if (ret) {
>  			hid_err(hdev,
>  				"%s:steam_register failed with error %d\n",
>  				__func__, ret);
>  			goto err_hw_close;

[Severity: High]
This isn't a bug introduced by this patch, but does steam_probe() leak
the power_supply object if steam_register() fails? If steam_input_register()
or steam_sensors_register() fails inside steam_register(), it jumps to an
error label and returns an error without unregistering the battery created
via steam_battery_register(). The error propagates here, skipping
steam_unregister() and jumping to err_hw_close. Doesn't this leave the
power_supply device registered while devm frees the steam_device, leading to
a use-after-free when userspace interacts with the stale power_supply?

[ ... ]

> @@ -1787,19 +1789,17 @@ static void steam_remove(struct hid_device *hdev)
>  	if (steam->quirks & STEAM_QUIRK_WIRELESS) {
>  		hid_info(hdev, "Steam wireless receiver disconnected");
>  	}
> +	guard(mutex)(&steam->report_mutex);
>  	steam_unregister(steam);
>  	hid_hw_stop(hdev);
>  }

[Severity: High]
Can holding report_mutex across steam_unregister() cause a self-deadlock?
When a device is removed, steam_remove() acquires report_mutex here and calls
steam_unregister(). steam_unregister() calls cancel_work_sync() on
rumble_work, and steam_input_unregister(). If the rumble work is running in
steam_haptic_rumble_cb(), it will attempt to acquire the same report_mutex
and block forever. Additionally, unregistering the input device triggers
the steam_input_close() callback, which also attempts to acquire this
non-recursive report_mutex.

[Severity: High]
This is a pre-existing issue, but is there a use-after-free window here
in steam_remove() due to the order of teardown? cancel_work_sync() for
work_connect is called just before the quoted code, but hid_hw_stop() is
called at the end. If an interrupt arrives between the cancellation and
hid_hw_stop(), steam_raw_event() can process it and re-queue the work.
When steam_remove() returns, devm frees the steam_device, but the queued
work could later execute steam_work_connect_cb() on the freed memory.

[ ... ]

> @@ -2631,6 +2624,8 @@ static int steam_param_set_lizard_mode(const char *val,
>  {
>  	struct steam_device *steam;
>  	int ret;
> +	bool client_opened;
> +	unsigned long flags;
>  
>  	ret = param_set_bool(val, kp);
>  	if (ret)
> @@ -2638,8 +2633,13 @@ static int steam_param_set_lizard_mode(const char *val,
>  
>  	mutex_lock(&steam_devices_lock);
>  	list_for_each_entry(steam, &steam_devices, list) {
> -		if (!steam->client_opened)
> +		spin_lock_irqsave(&steam->lock, flags);
> +		client_opened = steam->client_opened;
> +		spin_unlock_irqrestore(&steam->lock, flags);
> +		if (client_opened) {
> +			guard(mutex)(&steam->report_mutex);
>  			steam_set_lizard_mode(steam, lizard_mode);
> +		}
>  	}
>  	mutex_unlock(&steam_devices_lock);
>  	return 0;

[Severity: High]
Does acquiring report_mutex while holding steam_devices_lock introduce an
lock inversion? Here in steam_param_set_lizard_mode(),
steam_devices_lock is acquired first, followed by report_mutex. However,
concurrently in steam_probe() or steam_work_connect_cb(), report_mutex is
acquired first, and then steam_register() is called, which attempts to
acquire steam_devices_lock to add the device to the list. Could this lead
to an indefinite deadlock?

[Severity: Medium]
Was the condition intentionally changed from !steam->client_opened to
if (client_opened)? It seems like this applies the lizard mode settings
to devices actively controlled by the Steam client, and ignores devices that
actually need the fallback mappings.

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

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

end of thread, other threads:[~2026-08-05  2:16 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-05  1:51 [PATCH 0/5] HID: steam: Add 2026 Steam Controller support Vicki Pfau
2026-08-05  1:51 ` [PATCH 1/5] HID: steam: Initial " Vicki Pfau
2026-08-05  2:13   ` sashiko-bot
2026-08-05  1:51 ` [PATCH 2/5] HID: steam: Fix wording of connect/disconnect logs Vicki Pfau
2026-08-05  2:02   ` sashiko-bot
2026-08-05  1:51 ` [PATCH 3/5] HID: steam: Don't set feature reports when disconnecting Vicki Pfau
2026-08-05  1:51 ` [PATCH 4/5] HID: steam: Clean up locking Vicki Pfau
2026-08-05  2:16   ` sashiko-bot
2026-08-05  1:51 ` [PATCH 5/5] HID: steam: Zero out inputs when disabling gamepad mode Vicki Pfau
2026-08-05  2:09   ` sashiko-bot

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