Re: [PATCH] hwmon: (corsair-psu) serialize debugfs access against hwmon

Guenter Roeck <[email protected]> Tue, 4 Aug 2026 13:14:36 -0700
Newsgroups org.kernel.vger.linux-hwmon,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
On 8/4/26 11:37, Wilken Gottwalt wrote:
> On Tue, 4 Aug 2026 09:34:38 -0700
> Guenter Roeck <[email protected]> wrote:
> 
>> On Tue, Aug 04, 2026 at 04:11:11AM +0000, Wilken Gottwalt wrote:
>>>>
>>>> Gemini tells me that fixing the raw event problem will require a spinlock to
>>>> protect the completion and a separate receive buffer. No idea if it is correct,
>>>> but other drivers do the same, so it may have a point. Either case, this is a
>>>> bit too much to do without hardware to test, and I'd rather prefer to leave this
>>>> up to Wilken.
>>>
>>> I was working at that one, too, because I saw Claude Opus hinting on that one.
>>> But it drove me crazy, because every AI is saying something slighty different. I
>>> can not really pin down which one is actually the real solution. I tried to read
>>> through the subsystems code and other drivers, but, argh, I don't know. I was
>>> playing with the idea to (1) remove the raw HID mode completely or (2) make the
>>> driver switchable, raw HID or normal HID, but not both at the same time. On the
>>> other hand, in my Github repo where I develop the driver, I also have a tool
>>> which demonstrates how to access the PSU completely in userspace via libhidpi.
>>> There is actually no need to provide the raw HID access.
>>>
>>
>> Have a look at the patch below. It is part AI (Gemini) generated and part me.
>> Sashiko is happy with it, but of course that doesn't mean it is perfect or
>> even correct. It does look good to me, though.
>>
>> Making Sashiko happy required all core elements of the patch:
>> - the spinlock
>> - the separate receive buffer
>> - the rcv_pending boolean
>> - the size check in corsairpsu_raw_event()
>>
>> Sashiko reports race conditions if I drop just one of those elements.
>>
>> Guenter
>>
>> ---
>>  From 50fae138603a9a6b1929cbe9a644310495688eea Mon Sep 17 00:00:00 2001
>> From: Guenter Roeck <[email protected]>
>> Date: Mon, 3 Aug 2026 17:39:21 -0700
>> Subject: [PATCH] hwmon: (corsair-psu) Separate request/response buffers and
>>   validate reply echo
>>
>> In corsairpsu_usb_cmd(), a single shared buffer (priv->cmd_buffer) is used
>> both for transmitting command reports via hid_hw_output_report() and for
>> receiving device responses in corsairpsu_raw_event().
>>
>> If a command sent via corsairpsu_usb_cmd() times out, the caller stops
>> waiting, but the hardware may still process the command and send a delayed
>> response later. If a subsequent command is being prepared or transmitted
>> when this delayed response arrives, corsairpsu_raw_event() blindly copies
>> the incoming report into priv->cmd_buffer and completes wait_completion:
>>
>> corsairpsu_raw_event()
>>      if (completion_done(&priv->wait_completion))
>>          return 0;
>>
>>      memcpy(priv->cmd_buffer, data, min(CMD_BUFFER_SIZE, size));
>>      complete(&priv->wait_completion);
>>
>> This causes a data race where priv->cmd_buffer can be overwritten with
>> the old delayed response while hid_hw_output_report() is transmitting the
>> new command, potentially causing the PSU to receive invalid parameters or
>> shut down. In addition, the caller of the subsequent command will wake up
>> early and either consume stale data or fail unexpectedly.
>>
>> Fix the problem by:
>> - Allocating a separate response buffer (priv->res_buffer) so that
>>    corsairpsu_raw_event() never modifies priv->cmd_buffer during outgoing
>>    transfers.
>> - Validating incoming reports in corsairpsu_raw_event() to ensure that the
>>    echoed length and command opcode match the pending command in
>>    priv->cmd_buffer (or indicate an unsupported command opcode with 0).
>> - Protecting buffer initialization, reinit_completion(), and report
>>    validation/completion with a spinlock (wait_completion_lock).
>> - Adding a boolean flag indicating that the code is waiting for a response,
>>    and only copying the reply into the receive buffer if that is the case.
>>
>> Reported-by: Sashiko <[email protected]>
>> Signed-off-by: Guenter Roeck <[email protected]>
>> ---
>>   drivers/hwmon/corsair-psu.c | 33 +++++++++++++++++++++++++++------
>>   1 file changed, 27 insertions(+), 6 deletions(-)
>>
>> diff --git a/drivers/hwmon/corsair-psu.c b/drivers/hwmon/corsair-psu.c
>> index 5592b927e9d4..3eebf8494ed8 100644
>> --- a/drivers/hwmon/corsair-psu.c
>> +++ b/drivers/hwmon/corsair-psu.c
>> @@ -13,6 +13,7 @@
>>   #include <linux/kernel.h>
>>   #include <linux/module.h>
>>   #include <linux/slab.h>
>> +#include <linux/spinlock.h>
>>   #include <linux/types.h>
>>   
>>   /*
>> @@ -122,7 +123,10 @@ struct corsairpsu_data {
>>   	struct device *hwmon_dev;
>>   	struct dentry *debugfs;
>>   	struct completion wait_completion;
>> +	spinlock_t completion_lock;	/* locks wait_completion, cmd_buffer, and res_buffer
>> */ u8 *cmd_buffer;
>> +	u8 *res_buffer;
>> +	bool rcv_pending;
>>   	char vendor[REPLY_SIZE];
>>   	char product[REPLY_SIZE];
>>   	long temp_crit[TEMP_COUNT];
>> @@ -158,15 +162,19 @@ static int corsairpsu_dutycycle_to_pwm(const long dutycycle)
>>   
>>   static int corsairpsu_usb_cmd(struct corsairpsu_data *priv, u8 p0, u8 p1, u8 p2, void *data)
>>   {
>> +	unsigned long flags;
>>   	unsigned long time;
>>   	int ret;
>>   
>> +	spin_lock_irqsave(&priv->completion_lock, flags);
>>   	memset(priv->cmd_buffer, 0, CMD_BUFFER_SIZE);
>> +	memset(priv->res_buffer, 0, CMD_BUFFER_SIZE);
>>   	priv->cmd_buffer[0] = p0;
>>   	priv->cmd_buffer[1] = p1;
>>   	priv->cmd_buffer[2] = p2;
>> -
>>   	reinit_completion(&priv->wait_completion);
>> +	priv->rcv_pending = true;
>> +	spin_unlock_irqrestore(&priv->completion_lock, flags);
>>   
>>   	ret = hid_hw_output_report(priv->hdev, priv->cmd_buffer, CMD_BUFFER_SIZE);
>>   	if (ret < 0)
>> @@ -182,11 +190,11 @@ static int corsairpsu_usb_cmd(struct corsairpsu_data *priv, u8 p0, u8 p1,
>> u8 p2,
>>   	 * was send, not every command is supported on every device class, if a command is not
>>   	 * supported, the length value in the reply is okay, but the command value is set to 0
>>   	 */
>> -	if (p0 != priv->cmd_buffer[0] || p1 != priv->cmd_buffer[1])
>> +	if (p0 != priv->res_buffer[0] || p1 != priv->res_buffer[1])
>>   		return -EOPNOTSUPP;
>>   
>>   	if (data)
>> -		memcpy(data, priv->cmd_buffer + 2, REPLY_SIZE);
>> +		memcpy(data, priv->res_buffer + 2, REPLY_SIZE);
>>   
>>   	return 0;
>>   }
>> @@ -781,6 +789,10 @@ static int corsairpsu_probe(struct hid_device *hdev, const struct
>> hid_device_id if (!priv->cmd_buffer)
>>   		return -ENOMEM;
>>   
>> +	priv->res_buffer = devm_kmalloc(&hdev->dev, CMD_BUFFER_SIZE, GFP_KERNEL);
>> +	if (!priv->res_buffer)
>> +		return -ENOMEM;
>> +
>>   	ret = hid_parse(hdev);
>>   	if (ret)
>>   		return ret;
>> @@ -795,6 +807,7 @@ static int corsairpsu_probe(struct hid_device *hdev, const struct
>> hid_device_id
>>   	priv->hdev = hdev;
>>   	hid_set_drvdata(hdev, priv);
>> +	spin_lock_init(&priv->completion_lock);
>>   	init_completion(&priv->wait_completion);
>>   
>>   	hid_device_io_start(hdev);
>> @@ -848,12 +861,20 @@ static int corsairpsu_raw_event(struct hid_device *hdev, struct hid_report
>> *repo int size)
>>   {
>>   	struct corsairpsu_data *priv = hid_get_drvdata(hdev);
>> +	unsigned long flags;
>>   
>> -	if (completion_done(&priv->wait_completion))
>> +	if (size < 2)
>>   		return 0;
>>   
>> -	memcpy(priv->cmd_buffer, data, min(CMD_BUFFER_SIZE, size));
>> -	complete(&priv->wait_completion);
>> +	spin_lock_irqsave(&priv->completion_lock, flags);
>> +	if (priv->rcv_pending && !completion_done(&priv->wait_completion) &&
>> +	    data[0] == priv->cmd_buffer[0] &&
>> +	    (data[1] == priv->cmd_buffer[1] || data[1] == 0)) {
>> +		memcpy(priv->res_buffer, data, min(CMD_BUFFER_SIZE, size));
>> +		complete(&priv->wait_completion);
>> +		priv->rcv_pending = false;
>> +	}
>> +	spin_unlock_irqrestore(&priv->completion_lock, flags);
>>   
>>   	return 0;
>>   }
> 
> I don't know, something is odd about this. I'm not 100% sure, but the lock is
> released before hid_hw_output_report(), which is necessary because a spinlock
> must not be held by a potentially sleeping call. This leaves a window in which
> a delayed reply can satisfy a freshly reinitialized wait_completion() even
> before the actual send, if the new command has the same echo (p0/p1) as the old
> one. And that is precisely the typical scenario when polling the same sensor
> attributes (always 3, 0x8B for rail voltage). Or I just misinterpret this whole
> thing entirely, I'm getting tired...
> 
Unless I am missing something there is nothing you can do about this unless there
is a sequence number in the message. I think this is why the AI insists that there
is a separate receive buffer (to avoid overwriting the command buffer in this
scenario).

Guenter