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: 7+ 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
2026-10-30 6:37 ` Martin Oberleiter via B4 Relay
2026-10-30 6:37 ` [PATCH 1/2] HID: steelseries: Select POWER_SUPPLY Martin Oberleiter
2026-10-30 6:37 ` Martin Oberleiter via B4 Relay
2026-10-30 6:37 ` [PATCH 2/2] HID: steelseries: Add Rival 3 Wireless battery reporting Martin Oberleiter
2026-10-30 6:37 ` 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.