| From 282a4b71816b6076029017a7bab3a9dcee12a920 Mon Sep 17 00:00:00 2001 |
| From: Eli Billauer <eli.billauer@gmail.com> |
| Date: Sun, 30 Oct 2022 11:42:09 +0200 |
| Subject: char: xillybus: Prevent use-after-free due to race condition |
| |
| From: Eli Billauer <eli.billauer@gmail.com> |
| |
| commit 282a4b71816b6076029017a7bab3a9dcee12a920 upstream. |
| |
| The driver for XillyUSB devices maintains a kref reference count on each |
| xillyusb_dev structure, which represents a physical device. This reference |
| count reaches zero when the device has been disconnected and there are no |
| open file descriptors that are related to the device. When this occurs, |
| kref_put() calls cleanup_dev(), which clears up the device's data, |
| including the structure itself. |
| |
| However, when xillyusb_open() is called, this reference count becomes |
| tricky: This function needs to obtain the xillyusb_dev structure that |
| relates to the inode's major and minor (as there can be several such). |
| xillybus_find_inode() (which is defined in xillybus_class.c) is called |
| for this purpose. xillybus_find_inode() holds a mutex that is global in |
| xillybus_class.c to protect the list of devices, and releases this |
| mutex before returning. As a result, nothing protects the xillyusb_dev's |
| reference counter from being decremented to zero before xillyusb_open() |
| increments it on its own behalf. Hence the structure can be freed |
| due to a rare race condition. |
| |
| To solve this, a mutex is added. It is locked by xillyusb_open() before |
| the call to xillybus_find_inode() and is released only after the kref |
| counter has been incremented on behalf of the newly opened inode. This |
| protects the kref reference counters of all xillyusb_dev structs from |
| being decremented by xillyusb_disconnect() during this time segment, as |
| the call to kref_put() in this function is done with the same lock held. |
| |
| There is no need to hold the lock on other calls to kref_put(), because |
| if xillybus_find_inode() finds a struct, xillyusb_disconnect() has not |
| made the call to remove it, and hence not made its call to kref_put(), |
| which takes place afterwards. Hence preventing xillyusb_disconnect's |
| call to kref_put() is enough to ensure that the reference doesn't reach |
| zero before it's incremented by xillyusb_open(). |
| |
| It would have been more natural to increment the reference count in |
| xillybus_find_inode() of course, however this function is also called by |
| Xillybus' driver for PCIe / OF, which registers a completely different |
| structure. Therefore, xillybus_find_inode() treats these structures as |
| void pointers, and accordingly can't make any changes. |
| |
| Reported-by: Hyunwoo Kim <imv4bel@gmail.com> |
| Suggested-by: Alan Stern <stern@rowland.harvard.edu> |
| Signed-off-by: Eli Billauer <eli.billauer@gmail.com> |
| Link: https://lore.kernel.org/r/20221030094209.65916-1-eli.billauer@gmail.com |
| Signed-off-by: Bin Lan <bin.lan.cn@windriver.com> |
| Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org> |
| --- |
| drivers/char/xillybus/xillyusb.c | 22 +++++++++++++++++++--- |
| 1 file changed, 19 insertions(+), 3 deletions(-) |
| |
| --- a/drivers/char/xillybus/xillyusb.c |
| +++ b/drivers/char/xillybus/xillyusb.c |
| @@ -185,6 +185,14 @@ struct xillyusb_dev { |
| struct mutex process_in_mutex; /* synchronize wakeup_all() */ |
| }; |
| |
| +/* |
| + * kref_mutex is used in xillyusb_open() to prevent the xillyusb_dev |
| + * struct from being freed during the gap between being found by |
| + * xillybus_find_inode() and having its reference count incremented. |
| + */ |
| + |
| +static DEFINE_MUTEX(kref_mutex); |
| + |
| /* FPGA to host opcodes */ |
| enum { |
| OPCODE_DATA = 0, |
| @@ -1234,9 +1242,16 @@ static int xillyusb_open(struct inode *i |
| int rc; |
| int index; |
| |
| + mutex_lock(&kref_mutex); |
| + |
| rc = xillybus_find_inode(inode, (void **)&xdev, &index); |
| - if (rc) |
| + if (rc) { |
| + mutex_unlock(&kref_mutex); |
| return rc; |
| + } |
| + |
| + kref_get(&xdev->kref); |
| + mutex_unlock(&kref_mutex); |
| |
| chan = &xdev->channels[index]; |
| filp->private_data = chan; |
| @@ -1272,8 +1287,6 @@ static int xillyusb_open(struct inode *i |
| ((filp->f_mode & FMODE_WRITE) && chan->open_for_write)) |
| goto unmutex_fail; |
| |
| - kref_get(&xdev->kref); |
| - |
| if (filp->f_mode & FMODE_READ) |
| chan->open_for_read = 1; |
| |
| @@ -1410,6 +1423,7 @@ unopen: |
| return rc; |
| |
| unmutex_fail: |
| + kref_put(&xdev->kref, cleanup_dev); |
| mutex_unlock(&chan->lock); |
| return rc; |
| } |
| @@ -2244,7 +2258,9 @@ static void xillyusb_disconnect(struct u |
| |
| xdev->dev = NULL; |
| |
| + mutex_lock(&kref_mutex); |
| kref_put(&xdev->kref, cleanup_dev); |
| + mutex_unlock(&kref_mutex); |
| } |
| |
| static struct usb_driver xillyusb_driver = { |