diff mbox series

gpiolib: cdev: Fix use after free in lineinfo_changed_notify

Message ID 20240501022612.1787143-1-quic_zhonhan@quicinc.com
State New
Headers show
Series gpiolib: cdev: Fix use after free in lineinfo_changed_notify | expand

Commit Message

Zhongqiu Han May 1, 2024, 2:26 a.m. UTC
The use-after-free issue occurs when userspace closes the GPIO chip device
file (e.g., "/dev/gpiochip0") by invoking gpio_chrdev_release(), while one
of its GPIO lines is simultaneously being released. In a stress test
scenario, stress-ng tool starts multi stress-ng-dev threads to continuously
open and close device file in the /dev, the use-after-free issue will occur
with a low reproducibility.

Here is the typical stack when issue happened:

BUG: KASAN: slab-use-after-free in lineinfo_changed_notify+0x84/0x1bc
Read of size 8 at addr ffffff803c06e840 by task stress-ng-dev/5437
Call trace:
 dump_backtrace
 show_stack
 dump_stack_lvl
 print_report
 kasan_report
 __asan_load8
 lineinfo_changed_notify
 notifier_call_chain
 blocking_notifier_call_chain
 gpiod_free_commit
 gpiod_free
 gpio_free
 st54spi_gpio_dev_release
 __fput
 __fput_sync
 __arm64_sys_close
 invoke_syscall
 el0_svc_common
 do_el0_svc
 el0_svc
 el0t_64_sync_handler
 el0t_64_sync
Allocated by task 5425:
 kasan_set_track
 kasan_save_alloc_info
 __kasan_kmalloc
 __kmalloc
 bitmap_zalloc
 gpio_chrdev_open
 chrdev_open
 do_dentry_open
 vfs_open
 path_openat
 do_filp_open
 do_sys_openat2
 __arm64_sys_openat
 invoke_syscall
 el0_svc_common
 do_el0_svc
 el0_svc
 el0t_64_sync_handler
 el0t_64_sync
Freed by task 5425:
 kasan_set_track
 kasan_save_free_info
 ____kasan_slab_free
 __kasan_slab_free
 slab_free_freelist_hook
 __kmem_cache_free
 kfree
 bitmap_free
 gpio_chrdev_release
 __fput
 __fput_sync
 __arm64_sys_close
 invoke_syscall
 el0_svc_common
 do_el0_svc
 el0_svc
 el0t_64_sync_handler
 el0t_64_sync

The use-after-free issue occurs as follows: watched_lines is freed by
bitmap_free(), but the lineinfo_changed_nb notifier chain cannot be
successfully unregistered due to waiting write rwsem when closing the
GPIO chip device file. Additionally, one of the GPIO chip's lines is
also in the release process and holds the notifier chain's read rwsem.
Consequently, a race condition leads to the use-after-free of
watched_lines.

[free]
gpio_chrdev_release()
  --> bitmap_free(cdev->watched_lines)                  <-- freed
  --> blocking_notifier_chain_unregister()
    --> down_write(&nh->rwsem)                          <-- waiting rwsem
          --> __down_write_common()
            --> rwsem_down_write_slowpath()
                  --> schedule_preempt_disabled()
                    --> schedule()

[use]
st54spi_gpio_dev_release()
  --> gpio_free()
    --> gpiod_free()
      --> gpiod_free_commit()
        --> gpiod_line_state_notify()
          --> blocking_notifier_call_chain()
            --> down_read(&nh->rwsem);                  <-- held rwsem
            --> notifier_call_chain()
              --> lineinfo_changed_notify()
                --> test_bit(xxxx, cdev->watched_lines) <-- use after free

To fix this issue, call the bitmap_free() function after successfully
unregistering the notifier chain. This prevents lineinfo_changed_notify()
from being called, thus avoiding the use-after-free race condition.

Fixes: 51c1064e82e7 ("gpiolib: add new ioctl() for monitoring changes in line info")
Signed-off-by: Zhongqiu Han <quic_zhonhan@quicinc.com>
---
 drivers/gpio/gpiolib-cdev.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Comments

Kent Gibson May 2, 2024, 1:51 a.m. UTC | #1
On Wed, May 01, 2024 at 10:26:12AM +0800, Zhongqiu Han wrote:
> The use-after-free issue occurs when userspace closes the GPIO chip device
> file (e.g., "/dev/gpiochip0") by invoking gpio_chrdev_release(), while one
> of its GPIO lines is simultaneously being released. In a stress test
> scenario, stress-ng tool starts multi stress-ng-dev threads to continuously
> open and close device file in the /dev, the use-after-free issue will occur
> with a low reproducibility.

This could be clearer.  Use-after-free of what?  We don't find out it is
the watched_lines bitmap until much later...

>
> Here is the typical stack when issue happened:
>

This stack trace is excessive [1].

> BUG: KASAN: slab-use-after-free in lineinfo_changed_notify+0x84/0x1bc
> Read of size 8 at addr ffffff803c06e840 by task stress-ng-dev/5437
> Call trace:
>  dump_backtrace
>  show_stack
>  dump_stack_lvl
>  print_report
>  kasan_report
>  __asan_load8
>  lineinfo_changed_notify
>  notifier_call_chain
>  blocking_notifier_call_chain
>  gpiod_free_commit
>  gpiod_free
>  gpio_free
>  st54spi_gpio_dev_release
>  __fput
>  __fput_sync
>  __arm64_sys_close
>  invoke_syscall
>  el0_svc_common
>  do_el0_svc
>  el0_svc
>  el0t_64_sync_handler
>  el0t_64_sync
> Allocated by task 5425:
>  kasan_set_track
>  kasan_save_alloc_info
>  __kasan_kmalloc
>  __kmalloc
>  bitmap_zalloc
>  gpio_chrdev_open
>  chrdev_open
>  do_dentry_open
>  vfs_open
>  path_openat
>  do_filp_open
>  do_sys_openat2
>  __arm64_sys_openat
>  invoke_syscall
>  el0_svc_common
>  do_el0_svc
>  el0_svc
>  el0t_64_sync_handler
>  el0t_64_sync
> Freed by task 5425:
>  kasan_set_track
>  kasan_save_free_info
>  ____kasan_slab_free
>  __kasan_slab_free
>  slab_free_freelist_hook
>  __kmem_cache_free
>  kfree
>  bitmap_free
>  gpio_chrdev_release
>  __fput
>  __fput_sync
>  __arm64_sys_close
>  invoke_syscall
>  el0_svc_common
>  do_el0_svc
>  el0_svc
>  el0t_64_sync_handler
>  el0t_64_sync
>
> The use-after-free issue occurs as follows: watched_lines is freed by
> bitmap_free(), but the lineinfo_changed_nb notifier chain cannot be
> successfully unregistered due to waiting write rwsem when closing the
> GPIO chip device file. Additionally, one of the GPIO chip's lines is
> also in the release process and holds the notifier chain's read rwsem.
> Consequently, a race condition leads to the use-after-free of
> watched_lines.
>

Drop the stack trace above and rework this paragraph into the opening
paragraph.

Might be good to note the side effects of the use-after-free.
AFAICT it may only result in an event being generated for userspace where
it shouldn't.  But, as the chrdev is being closed, userspace wont have the
chance to read that event anyway, so no appreciable difference?

> [free]
> gpio_chrdev_release()
>   --> bitmap_free(cdev->watched_lines)                  <-- freed
>   --> blocking_notifier_chain_unregister()
>     --> down_write(&nh->rwsem)                          <-- waiting rwsem
>           --> __down_write_common()
>             --> rwsem_down_write_slowpath()
>                   --> schedule_preempt_disabled()
>                     --> schedule()
>
> [use]
> st54spi_gpio_dev_release()
>   --> gpio_free()
>     --> gpiod_free()
>       --> gpiod_free_commit()
>         --> gpiod_line_state_notify()
>           --> blocking_notifier_call_chain()
>             --> down_read(&nh->rwsem);                  <-- held rwsem
>             --> notifier_call_chain()
>               --> lineinfo_changed_notify()
>                 --> test_bit(xxxx, cdev->watched_lines) <-- use after free
>
> To fix this issue, call the bitmap_free() function after successfully

"successfully" is confusing here as there is no unsuccessfully.

> unregistering the notifier chain. This prevents lineinfo_changed_notify()
> from being called, thus avoiding the use-after-free race condition.

The final sentence doesn't add anything - the reorder fixing the problem
is clear enough.

>
> Fixes: 51c1064e82e7 ("gpiolib: add new ioctl() for monitoring changes in line info")
> Signed-off-by: Zhongqiu Han <quic_zhonhan@quicinc.com>
> ---
>  drivers/gpio/gpiolib-cdev.c | 2 +-
>  1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/drivers/gpio/gpiolib-cdev.c b/drivers/gpio/gpiolib-cdev.c
> index d09c7d728365..6b3a43e3ba47 100644
> --- a/drivers/gpio/gpiolib-cdev.c
> +++ b/drivers/gpio/gpiolib-cdev.c
> @@ -2799,11 +2799,11 @@ static int gpio_chrdev_release(struct inode *inode, struct file *file)
>  	struct gpio_chardev_data *cdev = file->private_data;
>  	struct gpio_device *gdev = cdev->gdev;
>
> -	bitmap_free(cdev->watched_lines);
>  	blocking_notifier_chain_unregister(&gdev->device_notifier,
>  					   &cdev->device_unregistered_nb);
>  	blocking_notifier_chain_unregister(&gdev->line_state_notifier,
>  					   &cdev->lineinfo_changed_nb);
> +	bitmap_free(cdev->watched_lines);
>  	gpio_device_put(gdev);
>  	kfree(cdev);
>

No problem with the code change - makes total sense.

Cheers,
Kent.

[1] https://www.kernel.org/doc/html/latest/process/submitting-patches.html#backtraces-in-commit-messages
Zhongqiu Han May 2, 2024, 4:55 p.m. UTC | #2
On 5/2/2024 9:51 AM, Kent Gibson wrote:
> On Wed, May 01, 2024 at 10:26:12AM +0800, Zhongqiu Han wrote:
>> The use-after-free issue occurs when userspace closes the GPIO chip device
>> file (e.g., "/dev/gpiochip0") by invoking gpio_chrdev_release(), while one
>> of its GPIO lines is simultaneously being released. In a stress test
>> scenario, stress-ng tool starts multi stress-ng-dev threads to continuously
>> open and close device file in the /dev, the use-after-free issue will occur
>> with a low reproducibility.
> 
> This could be clearer.  Use-after-free of what?  We don't find out it is
> the watched_lines bitmap until much later...
> 

Hi Kent,
Thanks a lot for the review, I will take care of this on v2 and plan to
verify it as follows:

The use-after-free issue occurs as follows: when the GPIO chip device
file is being closed by invoking gpio_chrdev_release(), watched_lines is
freed by bitmap_free(), but the unregistration of lineinfo_changed_nb
notifier chain failed due to waiting write rwsem. Additionally, one of
the GPIO chip's lines is also in the release process and holds the
notifier chain's read rwsem. Consequently, a race condition leads to the
use-after-free of watched_lines.

>>
>> Here is the typical stack when issue happened:
>>
> 
> This stack trace is excessive [1].
> 
Acknowledged. I will drop it.

>> BUG: KASAN: slab-use-after-free in lineinfo_changed_notify+0x84/0x1bc
>> Read of size 8 at addr ffffff803c06e840 by task stress-ng-dev/5437
>> Call trace:
>>   dump_backtrace
>>   show_stack
>>   dump_stack_lvl
>>   print_report
>>   kasan_report
>>   __asan_load8
>>   lineinfo_changed_notify
>>   notifier_call_chain
>>   blocking_notifier_call_chain
>>   gpiod_free_commit
>>   gpiod_free
>>   gpio_free
>>   st54spi_gpio_dev_release
>>   __fput
>>   __fput_sync
>>   __arm64_sys_close
>>   invoke_syscall
>>   el0_svc_common
>>   do_el0_svc
>>   el0_svc
>>   el0t_64_sync_handler
>>   el0t_64_sync
>> Allocated by task 5425:
>>   kasan_set_track
>>   kasan_save_alloc_info
>>   __kasan_kmalloc
>>   __kmalloc
>>   bitmap_zalloc
>>   gpio_chrdev_open
>>   chrdev_open
>>   do_dentry_open
>>   vfs_open
>>   path_openat
>>   do_filp_open
>>   do_sys_openat2
>>   __arm64_sys_openat
>>   invoke_syscall
>>   el0_svc_common
>>   do_el0_svc
>>   el0_svc
>>   el0t_64_sync_handler
>>   el0t_64_sync
>> Freed by task 5425:
>>   kasan_set_track
>>   kasan_save_free_info
>>   ____kasan_slab_free
>>   __kasan_slab_free
>>   slab_free_freelist_hook
>>   __kmem_cache_free
>>   kfree
>>   bitmap_free
>>   gpio_chrdev_release
>>   __fput
>>   __fput_sync
>>   __arm64_sys_close
>>   invoke_syscall
>>   el0_svc_common
>>   do_el0_svc
>>   el0_svc
>>   el0t_64_sync_handler
>>   el0t_64_sync
>>
>> The use-after-free issue occurs as follows: watched_lines is freed by
>> bitmap_free(), but the lineinfo_changed_nb notifier chain cannot be
>> successfully unregistered due to waiting write rwsem when closing the
>> GPIO chip device file. Additionally, one of the GPIO chip's lines is
>> also in the release process and holds the notifier chain's read rwsem.
>> Consequently, a race condition leads to the use-after-free of
>> watched_lines.
>>
> 
> Drop the stack trace above and rework this paragraph into the opening
> paragraph.
> 

Yes, I will drop the stack above and rework this paragraph into
opening paragraph. Like the first comments reply.


> Might be good to note the side effects of the use-after-free.
> AFAICT it may only result in an event being generated for userspace where
> it shouldn't.  But, as the chrdev is being closed, userspace wont have the
> chance to read that event anyway, so no appreciable difference?
> 

Yes, there is no NULL access issue because there doesn't set
watched_lines = NULL; after bitmap_free, I also think it can only cause
the unexpected event is being generated. I will take care of it on v2.


>> [free]
>> gpio_chrdev_release()
>>    --> bitmap_free(cdev->watched_lines)                  <-- freed
>>    --> blocking_notifier_chain_unregister()
>>      --> down_write(&nh->rwsem)                          <-- waiting rwsem
>>            --> __down_write_common()
>>              --> rwsem_down_write_slowpath()
>>                    --> schedule_preempt_disabled()
>>                      --> schedule()
>>
>> [use]
>> st54spi_gpio_dev_release()
>>    --> gpio_free()
>>      --> gpiod_free()
>>        --> gpiod_free_commit()
>>          --> gpiod_line_state_notify()
>>            --> blocking_notifier_call_chain()
>>              --> down_read(&nh->rwsem);                  <-- held rwsem
>>              --> notifier_call_chain()
>>                --> lineinfo_changed_notify()
>>                  --> test_bit(xxxx, cdev->watched_lines) <-- use after free
>>
>> To fix this issue, call the bitmap_free() function after successfully
>  > "successfully" is confusing here as there is no unsuccessfully.
> 

Acknowledged. I plan to verify it as follows:

To fix this issue, call the bitmap_free() function after
blocking_notifier_chain_unregister.

>> unregistering the notifier chain. This prevents lineinfo_changed_notify()
>> from being called, thus avoiding the use-after-free race condition.
> 
> The final sentence doesn't add anything - the reorder fixing the problem
> is clear enough.
> 

Acknowledged. I will remove it.

>>
>> Fixes: 51c1064e82e7 ("gpiolib: add new ioctl() for monitoring changes in line info")
>> Signed-off-by: Zhongqiu Han <quic_zhonhan@quicinc.com>
>> ---
>>   drivers/gpio/gpiolib-cdev.c | 2 +-
>>   1 file changed, 1 insertion(+), 1 deletion(-)
>>
>> diff --git a/drivers/gpio/gpiolib-cdev.c b/drivers/gpio/gpiolib-cdev.c
>> index d09c7d728365..6b3a43e3ba47 100644
>> --- a/drivers/gpio/gpiolib-cdev.c
>> +++ b/drivers/gpio/gpiolib-cdev.c
>> @@ -2799,11 +2799,11 @@ static int gpio_chrdev_release(struct inode *inode, struct file *file)
>>   	struct gpio_chardev_data *cdev = file->private_data;
>>   	struct gpio_device *gdev = cdev->gdev;
>>
>> -	bitmap_free(cdev->watched_lines);
>>   	blocking_notifier_chain_unregister(&gdev->device_notifier,
>>   					   &cdev->device_unregistered_nb);
>>   	blocking_notifier_chain_unregister(&gdev->line_state_notifier,
>>   					   &cdev->lineinfo_changed_nb);
>> +	bitmap_free(cdev->watched_lines);
>>   	gpio_device_put(gdev);
>>   	kfree(cdev);
>>
> 
> No problem with the code change - makes total sense.
> 
Thanks for the review.

> Cheers,
> Kent.
> 
> [1] https://www.kernel.org/doc/html/latest/process/submitting-patches.html#backtraces-in-commit-messages
diff mbox series

Patch

diff --git a/drivers/gpio/gpiolib-cdev.c b/drivers/gpio/gpiolib-cdev.c
index d09c7d728365..6b3a43e3ba47 100644
--- a/drivers/gpio/gpiolib-cdev.c
+++ b/drivers/gpio/gpiolib-cdev.c
@@ -2799,11 +2799,11 @@  static int gpio_chrdev_release(struct inode *inode, struct file *file)
 	struct gpio_chardev_data *cdev = file->private_data;
 	struct gpio_device *gdev = cdev->gdev;
 
-	bitmap_free(cdev->watched_lines);
 	blocking_notifier_chain_unregister(&gdev->device_notifier,
 					   &cdev->device_unregistered_nb);
 	blocking_notifier_chain_unregister(&gdev->line_state_notifier,
 					   &cdev->lineinfo_changed_nb);
+	bitmap_free(cdev->watched_lines);
 	gpio_device_put(gdev);
 	kfree(cdev);