summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDenis Benato <denis.benato@linux.dev>2026-10-09 11:49:19 +0000
committerJiri Kosina <jkosina@suse.com>2026-10-16 11:39:57 +0200
commit6f2204e35a164cb593929918112bb3f217f7ae84 (patch)
tree5b8e1990a11cefe454b5aca1456d288915a2b5dc
parent8099f9da59f1c338bf88e25b7aa2a7fdf840879c (diff)
HID: asus: document and harden the worker teardown
asus_work() executes actions against the device: they send feature reports with hid_hw_raw_request() and, for the Fn+F5 fan key fallback, re-inject the raw report into the HID core with hid_report_raw_event(). For this reason the worker must be quiesced before hid_hw_stop(): after the low level driver has stopped, the transport is gone (usbhid_stop() frees the URBs and the I/O buffers) and re-injected reports would race against the input devices being unregistered by hid_disconnect(). Make so that the teardown cannot race to a use-after-free: every site queueing an action holds worker->lock across the .removed check, the list insertion and schedule_work(), and asus_worker_stop() sets .removed and drains the queue under that same lock before calling cancel_work_sync(). An action that passed the check is caught by the latter while any later attempt is discarded by asus_worker_schedule(). Closes: https://lore.kernel.org/all/20260908180032.34C2D1F00A3A@smtp.kernel.org/ Fixes: 47669bec44fe ("HID: asus: refactor the two workqueues and init sequence") Assisted-by: zcode:glm-5.3-flash Signed-off-by: Denis Benato <denis.benato@linux.dev> Signed-off-by: Jiri Kosina <jkosina@suse.com>
-rw-r--r--drivers/hid/hid-asus.c28
1 files changed, 26 insertions, 2 deletions
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index bd46aba6622a..3a8b8b7e90f7 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -765,9 +765,14 @@ static void asus_work(struct work_struct *work)
struct asus_work_action *action = NULL;
unsigned long flags;
- /* Save the action to be performed and clear the flag */
+ /*
+ * Dequeue the next action, if any. Once teardown has begun .removed
+ * is set and asus_worker_stop() drains the queue: leave the queued
+ * actions alone, they are dropped instead of being executed against
+ * a device that is being removed.
+ */
spin_lock_irqsave(&worker->lock, flags);
- if (!list_empty(&worker->actions)) {
+ if (!worker->removed && !list_empty(&worker->actions)) {
action = list_first_entry(&worker->actions,
struct asus_work_action, node);
list_del(&action->node);
@@ -817,6 +822,25 @@ static int asus_worker_create(struct hid_device *hdev, struct asus_drvdata *drvd
return 0;
}
+/**
+ * asus_worker_stop - quiesce the worker
+ * @worker: the worker to quiesce
+ *
+ * Once this function returns no more actions can be queued and no instance
+ * of asus_work() is running or pending.
+ *
+ * Callers must do this before hid_hw_stop(): actions are executed while the
+ * device is fully operational, since they send raw requests to it and, in
+ * the fan-key fallback path, re-inject raw reports into the HID core. After
+ * hid_hw_stop() the transport is gone (usbhid_stop() frees the URBs and the
+ * I/O buffers) and the input devices have been unregistered.
+ *
+ * The quiescing is race free because every site that queues an action holds
+ * worker->lock across the .removed check, the list insertion and
+ * schedule_work(): anything scheduled before .removed is set here is caught
+ * by the cancel_work_sync() below, anything after it is discarded by
+ * asus_worker_schedule().
+ */
static void asus_worker_stop(struct asus_worker *worker)
{
struct asus_work_action *action, *tmp;