[PATCH 6/6] gpiolib: let the character device free its IRQs first on chip removal

From: Adrian Ambrozewicz

Date: Mon Oct 12 2026 - 10:34:20 EST


gpiochip_remove() force-frees every IRQ still requested on the chip's
lines with gpiochip_free_remaining_irqs() before anything else, then
clears the chip pointer, removes the irqchip, disposing of the IRQ
mappings, and only at the very end, from gpiolib_cdev_unregister(),
calls the device notifier on which line requests and v1 line events
stop edge detection and free their own IRQs. That leaves two windows in
which the character device frees an IRQ it no longer owns:

- The notifier runs after the mappings are gone. If an IRQ number freed
by gpiochip_irqchip_remove() is allocated to another device in the
meantime, irq_has_action() finds that device's action and free_irq()
warns "Trying to free already-free IRQ". IRQs requested through the
character device after gpiochip_free_remaining_irqs() but before the
chip pointer is cleared are still installed when their mappings are
disposed of.

- Releasing a request concurrently with gpiochip_remove() races
gpiod_free_irqs(), which can free the action between the release
path's irq_has_action() check and its free_irq(), and then
gpiochip_irqchip_remove(), which can dispose of the descriptor that
free_irq() is still using.

Call the device notifier from gpiochip_remove() right after the chip
pointer is cleared and the SRCU grace period has elapsed, when no ioctl
can request new IRQs any more, and only free the remaining IRQs after
it. Have the release paths free their IRQs before unregistering from the
notifier, under the lock that the notifier callback also takes, so that
the notifier does not return while a release is still freeing an IRQ.
For v1 line events that needs a new mutex; line requests use their
config_mutex, which now has to be initialized before the first error
path. As the chip pointer is NULL by the time gpiod_free_irqs() runs,
have it call gc->to_irq() directly rather than gpiod_to_irq().

Every IRQ requested through the character device is thus freed exactly
once, by the notifier or by the release path, and before
gpiod_free_irqs() and the removal of the irqchip.

Moving the character device's part of the removal earlier was proposed
by Herve Codina before gpiochip_free_remaining_irqs() existed; with that
in place, only the order of the two is left to fix.

Reproduced with a test-only GPIO chip that uses the gpiolib hierarchical
irqchip, unbound from its driver with lines requested through the
character device. For the first window, the chip hands the IRQ number of
line 0 to a dummy handler while gpiochip_irqchip_remove() disposes of
the mappings, and the requests are closed after the unbind. For the
second, it sleeps in its irq_bus_lock() callback once removal has
started, and the request is closed while gpiochip_remove() is in
free_irq(). Each of 30 cycles warns "Trying to free already-free IRQ" in
both cases, for v2 line requests and v1 line events alike; with this
change none does.

Link: https://lore.kernel.org/all/20240227113426.253232-1-herve.codina@xxxxxxxxxxx/
Fixes: ec8b6f55b981 ("gpio: free irqs that are still requested when the chip is being removed")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Adrian Ambrozewicz <aambrozewicz@xxxxxxxxxx>
---
drivers/gpio/gpiolib-cdev.c | 39 +++++++++++++++++++++++++++----------
drivers/gpio/gpiolib.c | 29 ++++++++++++++++++++++-----
2 files changed, 53 insertions(+), 15 deletions(-)

diff --git a/drivers/gpio/gpiolib-cdev.c b/drivers/gpio/gpiolib-cdev.c
index ae19b4c3e9d8..7565c9f68060 100644
--- a/drivers/gpio/gpiolib-cdev.c
+++ b/drivers/gpio/gpiolib-cdev.c
@@ -1580,15 +1580,24 @@ static void linereq_free(struct linereq *lr)
{
unsigned int i;

+ /*
+ * Stop edge detection while still registered for the notification, so
+ * that gpiochip_remove() does not get past it until the IRQs are freed.
+ */
+ scoped_guard(mutex, &lr->config_mutex) {
+ for (i = 0; i < lr->num_lines; i++) {
+ if (lr->lines[i].desc)
+ edge_detector_stop(&lr->lines[i]);
+ }
+ }
+
if (lr->device_unregistered_nb.notifier_call)
blocking_notifier_chain_unregister(&lr->gdev->device_notifier,
&lr->device_unregistered_nb);

for (i = 0; i < lr->num_lines; i++) {
- if (lr->lines[i].desc) {
- edge_detector_stop(&lr->lines[i]);
+ if (lr->lines[i].desc)
gpiod_free(lr->lines[i].desc);
- }
}
kfifo_free(&lr->events);
kfree(lr->label);
@@ -1663,6 +1672,7 @@ static int linereq_create(struct gpio_device *gdev, void __user *ip)
if (!lr)
return -ENOMEM;
lr->num_lines = ulr.num_lines;
+ mutex_init(&lr->config_mutex);

lr->gdev = gpio_device_get(gdev);

@@ -1680,7 +1690,6 @@ static int linereq_create(struct gpio_device *gdev, void __user *ip)
return -ENOMEM;
}

- mutex_init(&lr->config_mutex);
init_waitqueue_head(&lr->wait);
INIT_KFIFO(lr->events);
lr->event_buffer_size = ulr.event_buffer_size;
@@ -1780,6 +1789,7 @@ static int linereq_create(struct gpio_device *gdev, void __user *ip)
* @eflags: the event flags this line was requested with
* @irq: the interrupt that trigger in response to events on this GPIO
* @irq_label: the name @irq was requested with
+ * @irq_lock: serializes freeing @irq on release and on chip removal
* @wait: wait queue that handles blocking reads of events
* @device_unregistered_nb: notifier block for receiving gdev unregister events
* @events: KFIFO for the GPIO events
@@ -1794,6 +1804,8 @@ struct lineevent_state {
u32 eflags;
int irq;
char *irq_label;
+ /* release vs. chip removal notifier */
+ struct mutex irq_lock;
wait_queue_head_t wait;
struct notifier_block device_unregistered_nb;
DECLARE_KFIFO(events, struct gpioevent_data, 16);
@@ -1829,9 +1841,11 @@ static int lineevent_unregistered_notify(struct notifier_block *nb,
struct lineevent_state *le = container_of(nb, struct lineevent_state,
device_unregistered_nb);

- if (le->irq) {
- free_line_irq(le->irq, le, &le->irq_label);
- le->irq = 0;
+ scoped_guard(mutex, &le->irq_lock) {
+ if (le->irq) {
+ free_line_irq(le->irq, le, &le->irq_label);
+ le->irq = 0;
+ }
}

wake_up_poll(&le->wait, EPOLLIN | EPOLLERR);
@@ -1913,11 +1927,16 @@ static ssize_t lineevent_read(struct file *file, char __user *buf,

static void lineevent_free(struct lineevent_state *le)
{
+ /* As in linereq_free(), free the IRQ before unregistering. */
+ scoped_guard(mutex, &le->irq_lock) {
+ if (le->irq) {
+ free_line_irq(le->irq, le, &le->irq_label);
+ le->irq = 0;
+ }
+ }
if (le->device_unregistered_nb.notifier_call)
blocking_notifier_chain_unregister(&le->gdev->device_notifier,
&le->device_unregistered_nb);
- if (le->irq)
- free_line_irq(le->irq, le, &le->irq_label);
if (le->desc)
gpiod_free(le->desc);
kfree(le->label);
@@ -2093,6 +2112,7 @@ static int lineevent_create(struct gpio_device *gdev, void __user *ip)
le = kzalloc_obj(*le);
if (!le)
return -ENOMEM;
+ mutex_init(&le->irq_lock);
le->gdev = gpio_device_get(gdev);

if (eventreq.consumer_label[0] != '\0') {
@@ -2829,5 +2849,4 @@ void gpiolib_cdev_unregister(struct gpio_device *gdev)
{
destroy_workqueue(gdev->line_state_wq);
cdev_device_del(&gdev->chrdev, &gdev->dev);
- blocking_notifier_call_chain(&gdev->device_notifier, 0, NULL);
}
diff --git a/drivers/gpio/gpiolib.c b/drivers/gpio/gpiolib.c
index 750378f3907a..f12f6eeb578d 100644
--- a/drivers/gpio/gpiolib.c
+++ b/drivers/gpio/gpiolib.c
@@ -851,11 +851,23 @@ bool gpiochip_line_is_valid(const struct gpio_chip *gc,
}
EXPORT_SYMBOL_GPL(gpiochip_line_is_valid);

-static void gpiod_free_irqs(struct gpio_desc *desc)
+static void gpiod_free_irqs(struct gpio_chip *gc, struct gpio_desc *desc)
{
- int irq = gpiod_to_irq(desc);
- struct irq_desc *irqd = irq_to_desc(irq);
+ struct irq_desc *irqd;
void *cookie;
+ int irq;
+
+ /* Called with gdev->chip already NULL, so gpiod_to_irq() can't be used. */
+ if (!gc->to_irq)
+ return;
+
+ irq = gc->to_irq(gc, gpiod_hwgpio(desc));
+ if (irq <= 0)
+ return;
+
+ irqd = irq_to_desc(irq);
+ if (!irqd)
+ return;

for (;;) {
/*
@@ -887,7 +899,7 @@ static void gpiochip_free_remaining_irqs(struct gpio_chip *gc)
struct gpio_desc *desc;

for_each_gpio_desc_with_flag(gc, desc, GPIOD_FLAG_USED_AS_IRQ)
- gpiod_free_irqs(desc);
+ gpiod_free_irqs(gc, desc);
}

static void gpiodev_release(struct device *dev)
@@ -1422,7 +1434,6 @@ void gpiochip_remove(struct gpio_chip *gc)
/* FIXME: should the legacy sysfs handling be moved to gpio_device? */
gpiochip_sysfs_unregister(gc);
gpiochip_free_hogs(gc);
- gpiochip_free_remaining_irqs(gc);

scoped_guard(mutex, &gpio_devices_lock)
list_del_rcu(&gdev->list);
@@ -1431,6 +1442,14 @@ void gpiochip_remove(struct gpio_chip *gc)
/* Numb the device, cancelling all outstanding operations */
rcu_assign_pointer(gdev->chip, NULL);
synchronize_srcu(&gdev->srcu);
+
+ /*
+ * No new interrupts can be requested through the character device now.
+ * Have it free the ones it holds before forcibly freeing the rest, and
+ * both before the irqchip and its mappings go away.
+ */
+ blocking_notifier_call_chain(&gdev->device_notifier, 0, NULL);
+ gpiochip_free_remaining_irqs(gc);
gpio_device_teardown_shared(gdev);
gpiochip_irqchip_remove(gc);
acpi_gpiochip_remove(gc);
--
2.43.0