From: sashiko-bot@kernel.org
To: "Martin Oberleiter" <martin.oberleiter@netixx.it>
Cc: linux-input@vger.kernel.org, dmitry.torokhov@gmail.com
Subject: Re: [PATCH 2/2] HID: steelseries: Add Rival 3 Wireless battery reporting
Date: Fri, 30 Oct 2026 06:56:42 +0000 [thread overview]
Message-ID: <sashiko-outbox-190621@kernel.org> (raw)
In-Reply-To: <20261030-rival3-battery-v1-2-6b1feeaf1f52@netixx.it>
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] HID: steelseries: missing suspend and runtime PM handling for battery_work
- [High] HID: steelseries: UAF on probe failure after steelseries_resume() arms work
- [High] HID: steelseries: race between steelseries_remove() and steelseries_resume()
- [Medium] HID: steelseries: deprecated system_wq mismatch in steelseries_resume()
--
commit bd1d30fd1ed7cb8e4a6c8b3c38fe5d24efafb14d
Author: Martin Oberleiter <martin.oberleiter@netixx.it>
HID: steelseries: Add Rival 3 Wireless battery reporting
This commit adds power_supply battery reporting for the SteelSeries Rival 3
Wireless mouse in 2.4 GHz receiver mode by polling vendor interface 3 via a
delayed work item and parsing the asynchronous input report replies.
> diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
> index 7292da0313e11..9fa70d21f49de 100644
> --- a/drivers/hid/hid-steelseries.c
> +++ b/drivers/hid/hid-steelseries.c
[ ... ]
> +static void steelseries_rival3_battery_work(struct work_struct *work)
> +{
> + struct steelseries_device *sd = container_of(work,
> + struct steelseries_device, battery_work.work);
> + static const u8 request[] = { 0x00, 0xaa, 0x01 }; /* report ID 0 */
> + bool was_online, was_charging, online, changed;
> + unsigned long flags, timeout;
> + u8 old_capacity;
> + u8 *buf;
> + int ret;
> +
> + spin_lock_irqsave(&sd->battery_lock, flags);
> + was_online = sd->battery_online;
> + was_charging = sd->battery_charging;
> + old_capacity = sd->battery_capacity;
> + spin_unlock_irqrestore(&sd->battery_lock, flags);
> +
> + buf = kmemdup(request, sizeof(request), GFP_KERNEL);
> + if (!buf) {
> + ret = -ENOMEM;
> + goto update;
> + }
> +
> + spin_lock_irqsave(&sd->battery_lock, flags);
> + reinit_completion(&sd->battery_reply);
> + sd->battery_awaiting_reply = true;
> + spin_unlock_irqrestore(&sd->battery_lock, flags);
> +
> + ret = hid_hw_raw_request(sd->hdev, 0x00, buf, sizeof(request),
> + HID_OUTPUT_REPORT, HID_REQ_SET_REPORT);
> + kfree(buf);
> + if (ret >= 0) {
> + timeout = msecs_to_jiffies(STEELSERIES_RIVAL3_WIRELESS_REPLY_TIMEOUT_MS);
> + ret = wait_for_completion_timeout(&sd->battery_reply, timeout) ? 0 : -ETIMEDOUT;
> + }
> +
> +update:
> + spin_lock_irqsave(&sd->battery_lock, flags);
> + /* A reply may still have arrived right after the timeout. */
> + if (ret && sd->battery_awaiting_reply) {
> + /* Keep the last level; the mouse is off or asleep. */
> + sd->battery_awaiting_reply = false;
> + sd->battery_online = false;
> + } else {
> + ret = 0;
> + }
> + online = sd->battery_online;
> + changed = online != was_online ||
> + sd->battery_charging != was_charging ||
> + sd->battery_capacity != old_capacity;
> + spin_unlock_irqrestore(&sd->battery_lock, flags);
> +
> + if (ret)
> + hid_dbg(sd->hdev, "No battery reply: %d\n", ret);
> + if (changed)
> + power_supply_changed(sd->battery);
[Severity: High]
What happens when steelseries_rival3_battery_work() in
drivers/hid/hid-steelseries.c runs during system suspend, suspend-to-idle
(s2idle), or USB runtime autosuspend?
Because steelseries_driver registers .resume and .reset_resume callbacks
without a matching .suspend callback and queues sd->battery_work on a
non-freezable workqueue, steelseries_rival3_battery_work() can fire after
usbhid's hid_suspend() has stopped I/O and usb_suspend() has cleared
udev->can_submit (or while the system is in s2idle).
When hid_hw_raw_request() fails with -EHOSTUNREACH (or
wait_for_completion_timeout() times out with -ETIMEDOUT),
steelseries_rival3_battery_work() sets sd->battery_online = false. If the
mouse was previously online (was_online == true), changed becomes true and
power_supply_changed(sd->battery) is called:
steelseries_rival3_battery_work()
power_supply_changed(sd->battery)
pm_stay_awake(&psy->dev)
Because battery_cfg.no_wakeup_source is left false in
steelseries_rival3_battery_register(), __power_supply_register() initializes
sd->battery->dev as a wakeup source, so pm_stay_awake() will abort system
suspend or wake the system from s2idle while also dropping the poll interval
to 1 minute.
In addition, usbhid enables USB runtime autosuspend and usbhid_open() drops
its usb_autopm reference before returning. Unlike
steelseries_msi_rgb_set_blocking() in the same file,
steelseries_rival3_battery_work() does not wrap the transfer and reply wait
with hid_hw_power(sd->hdev, PM_HINT_FULLON) and hid_hw_power(sd->hdev,
PM_HINT_NORMAL). Consequently, polls while the receiver is runtime-suspended
fail without waking the interface, and every runtime autoresume invokes
hid_resume() -> steelseries_resume(), resetting the 10-minute poll timer to
1 second.
Should steelseries_driver provide a .suspend callback that cancels
sd->battery_work, and should steelseries_rival3_battery_work() hold
hid_hw_power(sd->hdev, PM_HINT_FULLON) across the request and completion
wait?
> +
> + schedule_delayed_work(&sd->battery_work, msecs_to_jiffies(online ?
> + STEELSERIES_RIVAL3_WIRELESS_POLL_ONLINE_MS :
> + STEELSERIES_RIVAL3_WIRELESS_POLL_OFFLINE_MS));
> +}
> +
> +static int steelseries_rival3_battery_register(struct steelseries_device *sd)
> +{
[ ... ]
> + power_supply_powers(battery, &hdev->dev);
> + sd->battery = battery;
> + sd->battery_iface = true;
> +
> + return 0;
> +}
[ ... ]
> @@ -593,17 +811,38 @@ static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id
> }
> }
>
> + if (sd->quirks & STEELSERIES_RIVAL3_WIRELESS &&
> + !steelseries_rival3_is_battery_iface(hdev))
> + sd->quirks &= ~STEELSERIES_RIVAL3_WIRELESS;
> +
> ret = hid_parse(hdev);
> if (ret)
> return ret;
>
> + if (sd->quirks & STEELSERIES_RIVAL3_WIRELESS) {
> + ret = steelseries_rival3_battery_register(sd);
> + if (ret) {
> + hid_warn(hdev,
> + "Failed to register battery: %d, continuing without battery reporting\n",
> + ret);
> + sd->quirks &= ~STEELSERIES_RIVAL3_WIRELESS;
> + }
> + }
> +
> ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
> if (ret)
> return ret;
>
> - ret = hid_hw_open(hdev);
> - if (ret)
> - goto err_stop;
> + /* Only the Rival 3 Wireless battery interface needs to stay open. */
> + if (hdev->product != USB_DEVICE_ID_STEELSERIES_RIVAL_3_WIRELESS ||
> + sd->battery_iface) {
> + ret = hid_hw_open(hdev);
> + if (ret)
> + goto err_stop;
> + }
[Severity: High]
Can a failed probe in steelseries_probe() in drivers/hid/hid-steelseries.c
leave sd->battery_work armed after devres frees sd?
In __hid_device_probe(), hdev->driver is set to &steelseries_driver before
steelseries_probe() runs. In steelseries_probe(),
steelseries_rival3_battery_register() sets sd->battery_iface = true before
hid_hw_start() and hid_hw_open() are called.
If the parent usb_interface is runtime-suspended when hid_hw_open() is
called, usbhid_open() calls usb_autopm_get_interface(usbhid->intf), which
synchronously runs:
usbhid_open()
usb_autopm_get_interface()
hid_resume()
hid_driver_resume()
steelseries_resume()
mod_delayed_work(system_wq, &sd->battery_work, ...)
Because sd->battery_iface is already true, steelseries_resume() arms
sd->battery_work. If hid_hw_open() then fails (for example, when
hid_start_in() fails and usbhid_open() returns -EBUSY), steelseries_probe()
jumps to err_stop and returns without cancelling sd->battery_work, after
which __hid_device_probe() frees sd via devres_release_group() while the
delayed work timer is still pending.
Would it be safer to register the battery and set sd->battery_iface = true
only after hid_hw_start() and hid_hw_open() have succeeded (or cancel
sd->battery_work in the error path)?
[ ... ]
> @@ -632,10 +873,32 @@ static void steelseries_remove(struct hid_device *hdev)
> return;
> }
>
> - hid_hw_close(hdev);
> + sd = hid_get_drvdata(hdev);
> + if (sd->battery_iface)
> + cancel_delayed_work_sync(&sd->battery_work);
> +
> + if (hdev->product != USB_DEVICE_ID_STEELSERIES_RIVAL_3_WIRELESS ||
> + sd->battery_iface)
> + hid_hw_close(hdev);
> hid_hw_stop(hdev);
> }
[Severity: High]
Could steelseries_resume() race with steelseries_remove() in
drivers/hid/hid-steelseries.c and re-queue sd->battery_work during driver
unbind?
In hid_device_remove(), hdrv->remove(hdev) is called first, followed by
devres_release_group(&hdev->dev, hdev->devres_group_id), and hdev->driver is
only set to NULL afterwards. Meanwhile, usbhid's hid_resume() and
hid_reset_resume() invoke steelseries_resume() on the parent usb_interface
without holding device_lock(&hdev->dev) or hdev->driver_input_lock.
Because steelseries_remove() calls cancel_delayed_work_sync() rather than
disable_delayed_work_sync() and leaves sd->battery_iface true while
hid_hw_close(), hid_hw_stop(), and devres_release_group() execute, a
concurrent runtime resume or reset_resume on the USB interface can see
sd && sd->battery_iface and call mod_delayed_work() after
cancel_delayed_work_sync() has already returned, re-arming sd->battery_work
on memory that devres_release_group() is freeing.
Should steelseries_remove() use disable_delayed_work_sync(&sd->battery_work)
to prevent sd->battery_work from being re-queued once teardown begins?
>
> +static int steelseries_resume(struct hid_device *hdev)
> +{
> + struct steelseries_device *sd;
> +
> + if (hdev->product != USB_DEVICE_ID_STEELSERIES_RIVAL_3_WIRELESS)
> + return 0;
> +
> + /* Polls during suspend fail, so do not wait for the next one. */
> + sd = hid_get_drvdata(hdev);
> + if (sd && sd->battery_iface)
> + mod_delayed_work(system_wq, &sd->battery_work,
> + msecs_to_jiffies(1000));
[Severity: Medium]
In steelseries_resume() in drivers/hid/hid-steelseries.c, mod_delayed_work()
queues sd->battery_work onto system_wq, whereas steelseries_probe() and
steelseries_rival3_battery_work() use schedule_delayed_work(), which queues
onto system_percpu_wq.
In kernel/workqueue.c, system_wq and system_percpu_wq are separate
workqueue_struct instances, and system_wq is marked with __WQ_DEPRECATED,
causing __queue_work() to emit a runtime deprecation warning when the timer
expires:
workqueue: work func steelseries_rival3_battery_work enqueued on deprecated workqueue. Use system_{percpu|dfl}_wq instead.
Mixing two different workqueue_struct instances for the same delayed_work
also bypasses __queue_work()'s single-workqueue non-reentrancy check
(worker->current_pwq->wq == wq), allowing steelseries_rival3_battery_work()
to execute concurrently on two CPUs and race on
reinit_completion(&sd->battery_reply) and sd->battery_awaiting_reply.
Should steelseries_resume() pass system_percpu_wq to mod_delayed_work() so
all scheduling of sd->battery_work uses the same non-deprecated workqueue?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030-rival3-battery-v1-0-6b1feeaf1f52@netixx.it?part=2
prev parent reply other threads:[~2026-10-30 6:56 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-30 6:37 [PATCH 0/2] HID: steelseries: Add Rival 3 Wireless battery reporting Martin Oberleiter via B4 Relay
2026-10-30 6:37 ` [PATCH 1/2] HID: steelseries: Select POWER_SUPPLY Martin Oberleiter via B4 Relay
2026-10-30 6:37 ` [PATCH 2/2] HID: steelseries: Add Rival 3 Wireless battery reporting Martin Oberleiter via B4 Relay
2026-10-30 6:56 ` sashiko-bot [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-190621@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=dmitry.torokhov@gmail.com \
--cc=linux-input@vger.kernel.org \
--cc=martin.oberleiter@netixx.it \
--cc=sashiko-reviews@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox