diff mbox series

video: fbdev: smscufx: Fix use-after-free in ufx_ops_open()

Message ID 20220925110329.GA380036@ubuntu
State New
Headers show
Series video: fbdev: smscufx: Fix use-after-free in ufx_ops_open() | expand

Commit Message

V4bel Sept. 25, 2022, 11:03 a.m. UTC
A race condition may occur if the user physically removes the
USB device while calling open() for this device node.

This is a race condition between the ufx_ops_open() function and
the ufx_usb_disconnect() function, which may eventually result in UAF.

So, add a mutex to the ufx_ops_open() and ufx_usb_disconnect() functions
to avoid race contidion of krefs.

Signed-off-by: Hyunwoo Kim <imv4bel@gmail.com>
---
 drivers/video/fbdev/smscufx.c | 10 ++++++++++
 1 file changed, 10 insertions(+)



test code:
```
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <pthread.h>
#include <errno.h>
#include <sched.h>
#include <malloc.h>
#include <poll.h>
#include <pty.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/ipc.h>
#include <linux/userfaultfd.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <linux/netlink.h>
#include <stddef.h>
#include <sys/param.h>
#include <sys/resource.h>
#include <linux/bpf.h>
#include <linux/ioctl.h>
#include <linux/types.h>


#define die() do { \
    fprintf(stderr, "died in %s: %u\n", __func__, __LINE__); \
    exit(EXIT_FAILURE); \
} while (0)


void *fb_open(void) {
	int fd;

	fd = open("/dev/fb1", O_RDONLY);
	if (fd > 0) {
		printf("[step 1] fb1 open() : %d  pid : %ld\n", fd, syscall(SYS_gettid));
	} else {
		perror("/dev/fb1 open() failed");
		die();
	}

	sleep(10);
}

void *usb_disconnect_for_debug(void) {
	int ret;
	char input[2];

	sleep(3);

	printf("Disconnect now (After disconnecting, type enter)\n");
	read(0, input, 1);
	printf("[step 2] disconnect fbdev usb\n");

	sleep(5);
}

int main() {
	int p1, p2;
	int status1, status2;
	pthread_t hdr1, hdr2;
	int ret;

	p1 = pthread_create(&hdr1, NULL, fb_open, (void *)NULL);
	if (p1 != 0) {
		perror("pthread_create 1");
		die();
	}

	p2 = pthread_create(&hdr2, NULL, usb_disconnect_for_debug, (void *)NULL);
	if (p2 != 0) {
		perror("pthread_create 2");
		die();
	}

	pthread_join(hdr1, (void **)&status1);
	pthread_join(hdr2, (void **)&status2);

	return 0;
}
```


KASAN log:
```
[  138.037981] BUG: KASAN: use-after-free in ufx_ops_open+0x3e2/0x4d0 [smscufx]
[  138.038010] Read of size 4 at addr ffff88812d6a305c by task smscufx/2763

[  138.038035] CPU: 12 PID: 2763 Comm: smscufx Tainted: G        W          6.0.0-rc6+ #1
[  138.038053] Hardware name: Gigabyte Technology Co., Ltd. B460MDS3H/B460M DS3H, BIOS F3 05/27/2020
[  138.038064] Call Trace:
[  138.038073]  <TASK>
[  138.038085]  dump_stack_lvl+0x49/0x63
[  138.038108]  print_report.cold+0x5e/0x5d9
[  138.038123]  ? __kasan_check_write+0x14/0x20
[  138.038146]  ? ufx_ops_open+0x3e2/0x4d0 [smscufx]
[  138.038165]  kasan_report+0xa0/0x120
[  138.038184]  ? ufx_ops_open+0x3e2/0x4d0 [smscufx]
[  138.038203]  __asan_report_load4_noabort+0x14/0x20
[  138.038221]  ufx_ops_open+0x3e2/0x4d0 [smscufx]
[  138.038238]  ? mutex_unlock+0x81/0xd0
[  138.038258]  ? ufx_ops_ioctl+0x3c0/0x3c0 [smscufx]
[  138.038277]  ? destroy_sched_domains_rcu+0x70/0x70
[  138.038300]  ? try_module_get.part.0+0xb8/0x1a0
[  138.038317]  fb_open+0x187/0x3b0
[  138.038335]  chrdev_open+0x230/0x6d0
[  138.038353]  ? cdev_device_add+0x1f0/0x1f0
[  138.038368]  ? fsnotify_perm.part.0+0x1d9/0x4e0
[  138.038390]  do_dentry_open+0x404/0xf80
[  138.038408]  ? inode_permission+0x125/0x560
[  138.038422]  ? cdev_device_add+0x1f0/0x1f0
[  138.038440]  vfs_open+0x9f/0xd0
[  138.038457]  path_openat+0xd58/0x3f60
[  138.038474]  ? kasan_save_stack+0x3a/0x50
[  138.038489]  ? kasan_save_stack+0x26/0x50
[  138.038503]  ? __kasan_slab_alloc+0x94/0xd0
[  138.038519]  ? getname_flags.part.0+0x52/0x490
[  138.038534]  ? getname+0x7a/0xb0
[  138.038550]  ? path_lookupat+0x660/0x660
[  138.038565]  ? get_partial_node.part.0+0xd2/0x330
[  138.038586]  do_filp_open+0x1b1/0x3e0
[  138.038600]  ? ___slab_alloc+0x52c/0xa50
[  138.038615]  ? may_open_dev+0xd0/0xd0
[  138.038635]  ? _raw_spin_lock_bh+0xe0/0xe0
[  138.038648]  ? __check_object_size+0x174/0x650
[  138.038671]  do_sys_openat2+0x132/0x450
[  138.038688]  ? _raw_spin_unlock_irq+0x1f/0x3b
[  138.038702]  ? build_open_flags+0x450/0x450
[  138.038719]  ? __ia32_sys_ssetmask+0x1d0/0x1d0
[  138.038735]  ? __kasan_check_write+0x14/0x20
[  138.038756]  __x64_sys_openat+0x128/0x210
[  138.038774]  ? __ia32_compat_sys_open+0x1b0/0x1b0
[  138.038792]  ? fpregs_assert_state_consistent+0x52/0xc0
[  138.038813]  ? exit_to_user_mode_prepare+0x49/0x1a0
[  138.038834]  do_syscall_64+0x59/0x90
[  138.038849]  ? fpregs_assert_state_consistent+0x52/0xc0
[  138.038866]  ? exit_to_user_mode_prepare+0x49/0x1a0
[  138.038885]  ? syscall_exit_to_user_mode+0x26/0x50
[  138.038902]  ? do_syscall_64+0x69/0x90
[  138.038915]  ? switch_fpu_return+0xe/0x20
[  138.038932]  ? exit_to_user_mode_prepare+0x16a/0x1a0
[  138.038951]  ? syscall_exit_to_user_mode+0x26/0x50
[  138.038967]  entry_SYSCALL_64_after_hwframe+0x63/0xcd
[  138.038981] RIP: 0033:0x4534a4
[  138.038998] Code: 24 20 eb 8f 66 90 44 89 54 24 0c e8 d6 ab 02 00 44 8b 54 24 0c 44 89 e2 48 89 ee 41 89 c0 bf 9c ff ff ff b8 01 01 00 00 0f 05 <48> 3d 00 f0 ff ff 77 34 44 89 c7 89 44 24 0c e8 18 ac 02 00 8b 44
[  138.039014] RSP: 002b:00007f3618d20140 EFLAGS: 00000293 ORIG_RAX: 0000000000000101
[  138.039034] RAX: ffffffffffffffda RBX: 00007f3618d20640 RCX: 00000000004534a4
[  138.039048] RDX: 0000000000000000 RSI: 00000000004b1008 RDI: 00000000ffffff9c
[  138.039060] RBP: 00000000004b1008 R08: 0000000000000000 R09: 00007ffdf3d4a94f
[  138.039071] R10: 0000000000000000 R11: 0000000000000293 R12: 0000000000000000
[  138.039082] R13: 0000000000000000 R14: 000000000041b260 R15: 00007f3618520000
[  138.039099]  </TASK>
```


Best Regards,
Hyunwoo Kim.

Comments

Helge Deller Sept. 25, 2022, 11:57 a.m. UTC | #1
On 9/25/22 13:03, Hyunwoo Kim wrote:
> A race condition may occur if the user physically removes the
> USB device while calling open() for this device node.
>
> This is a race condition between the ufx_ops_open() function and
> the ufx_usb_disconnect() function, which may eventually result in UAF.
>
> So, add a mutex to the ufx_ops_open() and ufx_usb_disconnect() functions
> to avoid race contidion of krefs.
>
> Signed-off-by: Hyunwoo Kim <imv4bel@gmail.com>
> ---
>   drivers/video/fbdev/smscufx.c | 10 ++++++++++
>   1 file changed, 10 insertions(+)
>
> diff --git a/drivers/video/fbdev/smscufx.c b/drivers/video/fbdev/smscufx.c
> index d7aa5511c361..a4378a7241f7 100644
> --- a/drivers/video/fbdev/smscufx.c
> +++ b/drivers/video/fbdev/smscufx.c
> @@ -1065,6 +1067,8 @@ static int ufx_ops_open(struct fb_info *info, int user)
>   {
>   	struct ufx_data *dev = info->par;
>
> +	mutex_lock(&disconnect_mutex);
> +

The next few lines show:
         if (user == 0 && !console)
                 return -EBUSY;

in this case this function exits with the mutex held.

Please check all possible exit paths and unlock the mutex
where necessary.

Helge
V4bel Sept. 25, 2022, 12:34 p.m. UTC | #2
On Sun, Sep 25, 2022 at 01:57:46PM +0200, Helge Deller wrote:
> On 9/25/22 13:03, Hyunwoo Kim wrote:
> > A race condition may occur if the user physically removes the
> > USB device while calling open() for this device node.
> > 
> > This is a race condition between the ufx_ops_open() function and
> > the ufx_usb_disconnect() function, which may eventually result in UAF.
> > 
> > So, add a mutex to the ufx_ops_open() and ufx_usb_disconnect() functions
> > to avoid race contidion of krefs.
> > 
> > Signed-off-by: Hyunwoo Kim <imv4bel@gmail.com>
> > ---
> >   drivers/video/fbdev/smscufx.c | 10 ++++++++++
> >   1 file changed, 10 insertions(+)
> > 
> > diff --git a/drivers/video/fbdev/smscufx.c b/drivers/video/fbdev/smscufx.c
> > index d7aa5511c361..a4378a7241f7 100644
> > --- a/drivers/video/fbdev/smscufx.c
> > +++ b/drivers/video/fbdev/smscufx.c
> > @@ -1065,6 +1067,8 @@ static int ufx_ops_open(struct fb_info *info, int user)
> >   {
> >   	struct ufx_data *dev = info->par;
> > 
> > +	mutex_lock(&disconnect_mutex);
> > +
> 
> The next few lines show:
>         if (user == 0 && !console)
>                 return -EBUSY;
> 
> in this case this function exits with the mutex held.
> 
> Please check all possible exit paths and unlock the mutex
> where necessary.

yes. I made a mistake.
I submitted a fixed v2 patch.


Best Regards,
Hyunwoo Kim.
diff mbox series

Patch

diff --git a/drivers/video/fbdev/smscufx.c b/drivers/video/fbdev/smscufx.c
index d7aa5511c361..a4378a7241f7 100644
--- a/drivers/video/fbdev/smscufx.c
+++ b/drivers/video/fbdev/smscufx.c
@@ -137,6 +137,8 @@  static int ufx_submit_urb(struct ufx_data *dev, struct urb * urb, size_t len);
 static int ufx_alloc_urb_list(struct ufx_data *dev, int count, size_t size);
 static void ufx_free_urb_list(struct ufx_data *dev);
 
+static DEFINE_MUTEX(disconnect_mutex);
+
 /* reads a control register */
 static int ufx_reg_read(struct ufx_data *dev, u32 index, u32 *data)
 {
@@ -1065,6 +1067,8 @@  static int ufx_ops_open(struct fb_info *info, int user)
 {
 	struct ufx_data *dev = info->par;
 
+	mutex_lock(&disconnect_mutex);
+
 	/* fbcon aggressively connects to first framebuffer it finds,
 	 * preventing other clients (X) from working properly. Usually
 	 * not what the user wants. Fail by default with option to enable. */
@@ -1097,6 +1101,8 @@  static int ufx_ops_open(struct fb_info *info, int user)
 	pr_debug("open /dev/fb%d user=%d fb_info=%p count=%d",
 		info->node, user, info, dev->fb_count);
 
+	mutex_unlock(&disconnect_mutex);
+
 	return 0;
 }
 
@@ -1741,6 +1747,8 @@  static void ufx_usb_disconnect(struct usb_interface *interface)
 {
 	struct ufx_data *dev;
 
+	mutex_lock(&disconnect_mutex);
+
 	dev = usb_get_intfdata(interface);
 
 	pr_debug("USB disconnect starting\n");
@@ -1761,6 +1769,8 @@  static void ufx_usb_disconnect(struct usb_interface *interface)
 	kref_put(&dev->kref, ufx_free);
 
 	/* consider ufx_data freed */
+
+	mutex_unlock(&disconnect_mutex);
 }
 
 static struct usb_driver ufx_driver = {
-- 
2.25.1


Dear all,


I found a race-condition-to-UAF vulnerability in "drivers/video/fbdev/smscufx.c".


# Introduction

This vulnerability is a race condition between the ufx_ops_open() and ufx_usb_disconnect() functions.

Because this driver uses kref to manage the "dev" structure, it looks like a race condition will not occur, 
but in reality, a race condition occurs because there is no lock between the above two functions:
```
                cpu0                                                cpu1
       1. open()
          ufx_ops_open()
          if (dev->virtualized)
                                                             2. ufx_usb_disconnect()
                                                                dev->virtualized = true;
                                                                atomic_set()
                                                                usb_set_intfdata()

                                                             3. if (dev->fb_count == 0)
                                                                schedule_delayed_work(&dev->free_framebuffer_work, 0)
                                                                kref_put()   <- kref count : 1
                                                                kref_put()   <- kref count : 0
                                                                ufx_free()
                                                                kfree(dev);

       4. dev->fb_count++;   <- UAF start
          kref_get()   <- refcount_t: addition on 0; use-after-free.
```
The detailed exploit flow is as follows:

1. open() the device node.
dev->virtualized set to "true" only in ufx_usb_disconnect(),
It passes "if (dev->virtualized)" because it hasn't been set yet.

2. ufx_usb_disconnect() is called by physically removing the usb device.
"dev->virtualized = true;" is executed. 
But checking this flag doesn't make any sense because step 1 has already passed.
1 ~ 2 are the first race conditions.

3. After "if (dev->fb_count == 0)" is passed, call kref_put() twice with delayed queue. 
Eventually "kfree(dev);" is executed.

4. "dev->fb_count++" is called. 
However, at this point "dev" is kfree()d, so it becomes a UAF.
3 ~ 4 are the second race conditions.
After this, many UAF scenarios can be configured.


However, if you actually check the code, the workload of 1 ~ 4 of open() is much shorter 
than the workload of 2 ~ 3 of disconnect().
As a result, it is almost impossible to trigger this race condition with a brute force.

Here, an attacker can successfully achieve a race condition using the technique introduced in this paper:
https://www.usenix.org/system/files/sec21-lee-yoochan.pdf

To briefly summarize the paper, 
the processing time between 1 and 4 of open() can be extended longer by using Reschedule IPI.
In other words, it can dramatically increase the success probability of the above race condition scenario.


# for debugging

To debug this race condition, it is convenient to modify the code as follows:
```
diff --git a/drivers/video/fbdev/smscufx.c b/drivers/video/fbdev/smscufx.c
index d7aa5511c361..bbb42e9e768d 100644
--- a/drivers/video/fbdev/smscufx.c
+++ b/drivers/video/fbdev/smscufx.c
@@ -137,6 +137,10 @@  static int ufx_submit_urb(struct ufx_data *dev, struct urb * urb, size_t len);
 static int ufx_alloc_urb_list(struct ufx_data *dev, int count, size_t size);
 static void ufx_free_urb_list(struct ufx_data *dev);

+DECLARE_WAIT_QUEUE_HEAD(race_condition_debugq);
+static int race_check = 0;
+static struct ufx_data *open_dev;
+
 /* reads a control register */
 static int ufx_reg_read(struct ufx_data *dev, u32 index, u32 *data)
 {
@@ -1075,6 +1079,9 @@  static int ufx_ops_open(struct fb_info *info, int user)
        if (dev->virtualized)
                return -ENODEV;

+       open_dev = dev;
+       wait_event(race_condition_debugq, (race_check == 1));
+
        dev->fb_count++;

        kref_get(&dev->kref);
@@ -1760,6 +1767,12 @@  static void ufx_usb_disconnect(struct usb_interface *interface)
        /* release reference taken by kref_init in probe() */
        kref_put(&dev->kref, ufx_free);

+       if (open_dev == dev) {
+                usleep_range(1000000, 1000001);
+                race_check = 1;
+                wake_up(&race_condition_debugq);
+        }
+
        /* consider ufx_data freed */
 }
```