[PATCH v2] gpio: mpsse: fix race when arming the IRQ poll worker
From: Fan Wu
Date: Mon Sep 28 2026 - 07:36:56 EST
gpio_mpsse_irq_enable() schedules a poll worker before adding it to
priv->workers. gpio_mpsse_disconnect() can miss the worker while
tearing down the list, after which the worker can access freed priv.
Publish and schedule the worker while holding irq_spin. Set dying under
the same lock before disconnect tears down the list, so a worker created
after teardown starts is neither published nor scheduled. Use
kfree_rcu() for such a worker because irq_enable() runs with the IRQ
descriptor raw lock held.
This issue was found by an in-house static analysis tool.
Fixes: 179ef1127d7a ("gpio: mpsse: ensure worker is torn down")
Cc: stable@xxxxxxxxxxxxxxx
Co-developed-by: Song Li <songl@xxxxxxxxxx>
Signed-off-by: Song Li <songl@xxxxxxxxxx>
Signed-off-by: Fan Wu <fanwu01@xxxxxxxxxx>
---
drivers/gpio/gpio-mpsse.c | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/drivers/gpio/gpio-mpsse.c b/drivers/gpio/gpio-mpsse.c
index a859deab2..958d74427 100644
--- a/drivers/gpio/gpio-mpsse.c
+++ b/drivers/gpio/gpio-mpsse.c
@@ -10,6 +10,7 @@
#include <linux/cleanup.h>
#include <linux/gpio/driver.h>
#include <linux/mutex.h>
+#include <linux/rcupdate.h>
#include <linux/spinlock.h>
#include <linux/usb.h>
@@ -24,6 +25,7 @@ struct mpsse_priv {
raw_spinlock_t irq_spin; /* protects worker list */
atomic_t irq_type[16]; /* pin -> edge detection type */
atomic_t irq_enabled;
+ atomic_t dying; /* no new workers after disconnect */
int id;
u8 gpio_outputs[2]; /* Output states for GPIOs [L, H] */
@@ -46,6 +48,7 @@ struct mpsse_worker {
atomic_t cancelled;
struct list_head list; /* linked list */
struct list_head destroy; /* teardown linked list */
+ struct rcu_head rcu; /* deferred free for dying path */
};
struct bulk_desc {
@@ -525,10 +528,17 @@ static void gpio_mpsse_irq_enable(struct irq_data *irqd)
worker->priv = priv;
INIT_LIST_HEAD(&worker->list);
INIT_WORK(&worker->work, gpio_mpsse_poll);
- schedule_work(&worker->work);
- scoped_guard(raw_spinlock_irqsave, &priv->irq_spin)
+ scoped_guard(raw_spinlock_irqsave, &priv->irq_spin) {
+ if (atomic_read(&priv->dying)) {
+ /* irq_enable() runs under the IRQ descriptor raw lock. */
+ kfree_rcu(worker, rcu);
+ return;
+ }
+
list_add(&worker->list, &priv->workers);
+ schedule_work(&worker->work);
+ }
}
}
@@ -704,6 +714,9 @@ static void gpio_mpsse_disconnect(struct usb_interface *intf)
{
struct mpsse_priv *priv = usb_get_intfdata(intf);
+ scoped_guard(raw_spinlock_irqsave, &priv->irq_spin)
+ atomic_set(&priv->dying, 1);
+
/*
* Lock prevents double-free of worker from here and the teardown
* step at the beginning of gpio_mpsse_poll