When configuring interrupt handlers for GPIOs requested by virtual users, we never verify if an interrupt has been previously requested and just leak it if it was. We also never free the interrupts at device unbind. This is both a leak and a potential UAF bug. We use atomic_t for the irq field for now reason, it can't be modifying or read from atomic context. Let's repurpose the existing consumer_lock mutex and serialize access to the irq field. Let's add sanitization of input and schedule a tear-down action. Cc: stable@vger.kernel.org Fixes: 91581c4b3f29 ("gpio: virtuser: new virtual testing driver for the GPIO API") Closes: https://sashiko.dev/#/bug/linux-d8935133-0e80-487a-9e00-e748056f595e Signed-off-by: Bartosz Golaszewski --- drivers/gpio/gpio-virtuser.c | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/drivers/gpio/gpio-virtuser.c b/drivers/gpio/gpio-virtuser.c index 185c1be39b27760b468011acee1c21e649e87f8f..21b4bc64d4d346c51811491ab1d92b4af36ec3c6 100644 --- a/drivers/gpio/gpio-virtuser.c +++ b/drivers/gpio/gpio-virtuser.c @@ -55,9 +55,9 @@ struct gpio_virtuser_line_array_data { struct gpio_virtuser_line_data { struct gpio_virtuser_attr_data ad; char consumer[GPIO_VIRTUSER_NAME_BUF_LEN]; - struct mutex consumer_lock; + struct mutex lock; unsigned int debounce; - atomic_t irq; + int irq; atomic_t irq_count; }; @@ -607,7 +607,7 @@ static ssize_t gpio_virtuser_consumer_read(struct file *file, memset(buf, 0x0, sizeof(buf)); - scoped_guard(mutex, &data->consumer_lock) + scoped_guard(mutex, &data->lock) ret = snprintf(buf, sizeof(buf), "%s\n", data->consumer); return simple_read_from_buffer(user_buf, size, ppos, buf, ret); @@ -635,7 +635,7 @@ static ssize_t gpio_virtuser_consumer_write(struct file *file, if (ret) return ret; - scoped_guard(mutex, &data->consumer_lock) + scoped_guard(mutex, &data->lock) strscpy(data->consumer, buf, sizeof(data->consumer)); return count; @@ -675,7 +675,12 @@ static int gpio_virtuser_interrupts_set(void *data, u64 val) if (val > 1) return -EINVAL; + guard(mutex)(&ld->lock); + if (val) { + if (ld->irq >= 0) + return -EBUSY; + irq = gpiod_to_irq(ld->ad.desc); if (irq < 0) return irq; @@ -689,11 +694,13 @@ static int gpio_virtuser_interrupts_set(void *data, u64 val) if (ret) return ret; - atomic_set(&ld->irq, irq); + ld->irq = irq; } else { - irq = atomic_xchg(&ld->irq, 0); - if (irq) - free_irq(irq, ld); + if (ld->irq < 0) + return -EBUSY; + + free_irq(ld->irq, ld); + ld->irq = -1; } return 0; @@ -793,6 +800,14 @@ static int gpio_virtuser_dbgfs_init_line_array_attrs(struct device *dev, data->ad.dbgfs_dir, data); } +static void gpio_virtuser_free_irq(void *data) +{ + struct gpio_virtuser_line_data *ld = data; + + if (ld->irq >= 0) + free_irq(ld->irq, ld); +} + static int gpio_virtuser_dbgfs_init_line_attrs(struct device *dev, struct gpio_desc *desc, const char *id, @@ -809,14 +824,18 @@ static int gpio_virtuser_dbgfs_init_line_attrs(struct device *dev, data->ad.desc = desc; strscpy(data->consumer, id); - atomic_set(&data->irq, 0); + data->irq = -1; atomic_set(&data->irq_count, 0); + ret = devm_add_action_or_reset(dev, gpio_virtuser_free_irq, data); + if (ret) + return ret; + name = devm_kasprintf(dev, GFP_KERNEL, "gpiod:%s:%u", id, index); if (!name) return -ENOMEM; - ret = devm_mutex_init(dev, &data->consumer_lock); + ret = devm_mutex_init(dev, &data->lock); if (ret) return ret; --- base-commit: ddfe51b0a0f5dc9e415cc2c8ad8db17532dd426b change-id: 20261106-gpio-virtuser-irq-uaf-leak-392b42bf2833 Best regards, -- Bartosz Golaszewski