gpio: sysfs: fix chip removal with GPIOs exported over sysfs

Currently if we export a GPIO over sysfs and unbind the parent GPIO
controller, the exported attribute will remain under /sys/class/gpio
because once we remove the parent device, we can no longer associate the
descriptor with it in gpiod_unexport() and never drop the final
reference.

Rework the teardown code: provide an unlocked variant of
gpiod_unexport() and remove all exported GPIOs with the sysfs_lock taken
before unregistering the parent device itself. This is done to prevent
any new exports happening before we unregister the device completely.

Cc: stable@vger.kernel.org
Fixes: 1cd53df733 ("gpio: sysfs: don't look up exported lines as class devices")
Link: https://patch.msgid.link/20260212133505.81516-1-bartosz.golaszewski@oss.qualcomm.com
Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
This commit is contained in:
Bartosz Golaszewski
2026-02-18 09:42:56 +01:00
parent ff91965ad8
commit 6766f59012
+58 -54
View File
@@ -919,6 +919,51 @@ int gpiod_export_link(struct device *dev, const char *name,
}
EXPORT_SYMBOL_GPL(gpiod_export_link);
static void gpiod_unexport_unlocked(struct gpio_desc *desc)
{
struct gpiod_data *tmp, *desc_data = NULL;
struct gpiodev_data *gdev_data;
struct gpio_device *gdev;
if (!test_bit(GPIOD_FLAG_EXPORT, &desc->flags))
return;
gdev = gpiod_to_gpio_device(desc);
gdev_data = gdev_get_data(gdev);
if (!gdev_data)
return;
list_for_each_entry(tmp, &gdev_data->exported_lines, list) {
if (gpiod_is_equal(desc, tmp->desc)) {
desc_data = tmp;
break;
}
}
if (!desc_data)
return;
list_del(&desc_data->list);
clear_bit(GPIOD_FLAG_EXPORT, &desc->flags);
#if IS_ENABLED(CONFIG_GPIO_SYSFS_LEGACY)
sysfs_put(desc_data->value_kn);
device_unregister(desc_data->dev);
/*
* Release irq after deregistration to prevent race with
* edge_store.
*/
if (desc_data->irq_flags)
gpio_sysfs_free_irq(desc_data);
#endif /* CONFIG_GPIO_SYSFS_LEGACY */
sysfs_remove_groups(desc_data->parent,
desc_data->chip_attr_groups);
mutex_destroy(&desc_data->mutex);
kfree(desc_data);
}
/**
* gpiod_unexport - reverse effect of gpiod_export()
* @desc: GPIO to make unavailable
@@ -927,54 +972,14 @@ EXPORT_SYMBOL_GPL(gpiod_export_link);
*/
void gpiod_unexport(struct gpio_desc *desc)
{
struct gpiod_data *tmp, *desc_data = NULL;
struct gpiodev_data *gdev_data;
struct gpio_device *gdev;
if (!desc) {
pr_warn("%s: invalid GPIO\n", __func__);
return;
}
scoped_guard(mutex, &sysfs_lock) {
if (!test_bit(GPIOD_FLAG_EXPORT, &desc->flags))
return;
guard(mutex)(&sysfs_lock);
gdev = gpiod_to_gpio_device(desc);
gdev_data = gdev_get_data(gdev);
if (!gdev_data)
return;
list_for_each_entry(tmp, &gdev_data->exported_lines, list) {
if (gpiod_is_equal(desc, tmp->desc)) {
desc_data = tmp;
break;
}
}
if (!desc_data)
return;
list_del(&desc_data->list);
clear_bit(GPIOD_FLAG_EXPORT, &desc->flags);
#if IS_ENABLED(CONFIG_GPIO_SYSFS_LEGACY)
sysfs_put(desc_data->value_kn);
device_unregister(desc_data->dev);
/*
* Release irq after deregistration to prevent race with
* edge_store.
*/
if (desc_data->irq_flags)
gpio_sysfs_free_irq(desc_data);
#endif /* CONFIG_GPIO_SYSFS_LEGACY */
sysfs_remove_groups(desc_data->parent,
desc_data->chip_attr_groups);
}
mutex_destroy(&desc_data->mutex);
kfree(desc_data);
gpiod_unexport_unlocked(desc);
}
EXPORT_SYMBOL_GPL(gpiod_unexport);
@@ -1054,29 +1059,28 @@ void gpiochip_sysfs_unregister(struct gpio_device *gdev)
struct gpio_desc *desc;
struct gpio_chip *chip;
scoped_guard(mutex, &sysfs_lock) {
data = gdev_get_data(gdev);
if (!data)
return;
guard(mutex)(&sysfs_lock);
#if IS_ENABLED(CONFIG_GPIO_SYSFS_LEGACY)
device_unregister(data->cdev_base);
#endif /* CONFIG_GPIO_SYSFS_LEGACY */
device_unregister(data->cdev_id);
kfree(data);
}
data = gdev_get_data(gdev);
if (!data)
return;
guard(srcu)(&gdev->srcu);
chip = srcu_dereference(gdev->chip, &gdev->srcu);
if (!chip)
return;
/* unregister gpiod class devices owned by sysfs */
for_each_gpio_desc_with_flag(chip, desc, GPIOD_FLAG_SYSFS) {
gpiod_unexport(desc);
gpiod_unexport_unlocked(desc);
gpiod_free(desc);
}
#if IS_ENABLED(CONFIG_GPIO_SYSFS_LEGACY)
device_unregister(data->cdev_base);
#endif /* CONFIG_GPIO_SYSFS_LEGACY */
device_unregister(data->cdev_id);
kfree(data);
}
/*