[PATCH v3] gpio: mpsse: fix race when arming the IRQ poll worker

From: Fan Wu

Date: Tue Sep 29 2026 - 05:57:43 EST


gpio_mpsse_irq_enable() arms the poll worker before publishing it:
schedule_work() runs before the worker is added to priv->workers. If
gpio_mpsse_disconnect() walks the list in that window it misses the
worker, and once disconnect returns, the USB core frees mpsse_priv
while the orphaned gpio_mpsse_poll() work keeps accessing it, causing
a use-after-free.

Fix this by publishing and arming the worker in one irq_spin
critical section. Teardown walks the same list under irq_spin, so a
worker found on the list is guaranteed to be armed, and
cancel_work_sync() handles it whether it is queued or running.

A worker armed after the disconnect walk would still be missed, so
also set a new priv->dying flag under irq_spin before the teardown
walk, and check it in the same critical section, freeing it via
kfree_rcu() instead when the device is going away, since kfree()
would be called with the IRQ core's raw spinlock held and may sleep
on PREEMPT_RT. schedule_work() is safe to call with irq_spin held,
and the next probe gets a fresh mpsse_priv, so the flag never needs
to be cleared.

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>
---

Changes in v2, responding to feedback from Bartosz Golaszewski:

- Rework the worker disposal to be PREEMPT_RT-safe: v1 called
kfree() under priv->irq_spin with the IRQ descriptor's raw
spinlock also held (found by Sashiko, relayed by Bartosz); free
the never-published worker with kfree_rcu() instead.
- Recheck the pre-existing GFP_NOWAIT allocation in this irqchip
callback: no change needed.

Changes in v3: expand the comment above the deferred free, reword the
commit message and add this changelog; no functional change since v2.

v1: https://lore.kernel.org/linux-gpio/20260923030855.410109-1-fanwu01@xxxxxxxxxx/
v2: https://lore.kernel.org/linux-gpio/20260928112546.631440-1-fanwu01@xxxxxxxxxx/
drivers/gpio/gpio-mpsse.c | 24 ++++++++++++++++++++++--
1 file changed, 22 insertions(+), 2 deletions(-)

diff --git a/drivers/gpio/gpio-mpsse.c b/drivers/gpio/gpio-mpsse.c
index a859deab2..5ce822c0d 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,24 @@ 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)) {
+ /*
+ * Cannot kfree() here: this callback
+ * runs with the IRQ descriptor's raw
+ * spinlock held and kfree() may sleep
+ * on PREEMPT_RT. The worker is not
+ * yet published, so the deferred free
+ * is unobservable.
+ */
+ kfree_rcu(worker, rcu);
+ return;
+ }
+
list_add(&worker->list, &priv->workers);
+ schedule_work(&worker->work);
+ }
}
}

@@ -704,6 +721,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

--
2.34.1