usbip: stability changes
Subsystems (2)
cover usbip: stability changes
While using usbip on kirkwood embedded machine, I came accross some stability issues. This series is addressing the reference counting on and usage of the event handler itself.
---
Michael Grzeschik (2):
usbip: stub: reference count struct stub_device
usbip: identify the event handler with current_work()
drivers/usb/usbip/stub.h | 3 ++ drivers/usb/usbip/stub_dev.c | 69 ++++++++++++++++++++++++++++++++-------- drivers/usb/usbip/stub_rx.c | 3 ++ drivers/usb/usbip/stub_tx.c | 3 ++ drivers/usb/usbip/usbip_common.h | 6 ++++ drivers/usb/usbip/usbip_event.c | 17 +++++----- 6 files changed, 79 insertions(+), 22 deletions(-)
---
base-commit: 067c5c24cef23d913b67eaaf71ae67e7169a6620
change-id: 20261111-usbip-fcb6d02254a7
Best regards,
Signature · 1 line
Michael Grzeschik <[email protected]>
PATCH 1/2 usbip: stub: reference count struct stub_device
A stub_device is freed by stub_disconnect() right after usbip_stop_eh() reported that the event handler is done with it. The rx and tx threads and the event handler can still use it at that point:
- The event handler clears an event bit after it ran the handler for it. A SHUTDOWN requested while a previous shutdown is running is lost, so threads started after that shutdown are not stopped before the stub_device is freed.
- An event queued for the device while another one is handled leaves an entry in the event list. Once the event bits are clear, usbip_stop_eh() returns, the stub_device is freed, and the handler then locks the sysfs_lock of the freed device.
- The stub_device no longer holds a reference to the usb_device, so a thread outliving the disconnect touches a freed usb_device too.
This was seen exporting the serial port of an ST-Link from an STM32MP1-DK2 on a Kirkwood (ARMv5) host. After repeated "endpoint 5 is stalled" / usb_clear_halt cycles while a client opened and closed the port, stub_rx crashed in dev_err() with the device pointer computed from a NULL sdev->udev of freed memory:
Unable to handle kernel NULL pointer dereference at virtual address 000000b8 when read PC is at dev_driver_string+0x0/0x38 LR is at __dev_printk+0x20/0x6c Call trace: dev_driver_string from __dev_printk+0x20/0x6c __dev_printk from _dev_err+0x40/0x74 _dev_err from stub_rx_loop+0x5d8/0xd68 [usbip_host] stub_rx_loop [usbip_host] from kthread+0x100/0x120
Reference count the stub_device instead of relying on the event handler for its lifetime:
- the binding (probe until disconnect) holds the initial reference, stub_disconnect() drops it instead of freeing the device
- the rx and tx thread hold one each while they run, taken before they are woken and dropped when they end. If kthread_stop() reports that a thread never ran, stub_shutdown_connection() drops it for the thread.
- the event handler holds one per queued event until it is completely done with the device, through new optional get/put event handler operations, which vhci and vudc do not set
- the stub_device holds a reference to its usb_device again, dropped with the last reference
Threads that miss their shutdown now end when the connection goes away instead of using freed memory.
--- drivers/usb/usbip/stub.h | 3 ++ drivers/usb/usbip/stub_dev.c | 69 ++++++++++++++++++++++++++++++++-------- drivers/usb/usbip/stub_rx.c | 3 ++ drivers/usb/usbip/stub_tx.c | 3 ++ drivers/usb/usbip/usbip_common.h | 6 ++++ drivers/usb/usbip/usbip_event.c | 6 ++++ 6 files changed, 77 insertions(+), 13 deletions(-)
drivers/usb/usbip/stub.h +3 -0
@@ -6,6 +6,7 @@#ifndef __USBIP_STUB_H#define __USBIP_STUB_H +#include <linux/kref.h> #include <linux/list.h> #include <linux/slab.h> #include <linux/spinlock.h>
@@ -20,6 +21,7 @@struct stub_device {struct usb_device *udev; +struct kref kref;struct usbip_device ud;__u32 devid;
@@ -85,6 +87,7 @@ extern struct kmem_cache *stub_priv_cache;/* stub_dev.c */ extern struct usb_device_driver stub_driver; +void stub_device_put (struct stub_device *sdev);/* stub_main.c */ struct bus_id_priv *get_busid_priv (const char *busid);
drivers/usb/usbip/stub_dev.c +56 -13
127 lines not on this page · raw message
drivers/usb/usbip/stub_rx.c +3 -0
@@ -707,5 +707,8 @@ int stub_rx_loop(void *data)stub_rx_pdu (ud); } +/* drop the reference taken in usbip_sockfd_store() */ +stub_device_put (container_of(ud ,struct stub_device ,ud )); +return 0 ; }
drivers/usb/usbip/stub_tx.c +3 -0
@@ -446,5 +446,8 @@ int stub_tx_loop(void *data) kthread_should_stop())); } +/* drop the reference taken in usbip_sockfd_store() */ +stub_device_put (sdev); +return 0 ; }
drivers/usb/usbip/usbip_common.h +6 -0
@@ -280,6 +280,12 @@ struct usbip_device {void (*shutdown)(struct usbip_device *);void (*reset)(struct usbip_device *);void (*unusable)(struct usbip_device *); +/* +* Optional: hold a reference while an event for this device is +* queued for or being handled by the event handler. +*/ +void (*get)(struct usbip_device *ud); +void (*put)(struct usbip_device *ud); } eh_ops;struct kcov_common_handle_id kcov_handle;
drivers/usb/usbip/usbip_event.c +6 -0
@@ -94,6 +94,10 @@ static void event_handler(struct work_struct *work)mutex_unlock (&ud->sysfs_lock);wake_up (&ud->eh_waitq); + +/* drop the reference taken by usbip_event_add() */ +if (ud->eh_ops.put) + ud->eh_ops.put (ud); } }
@@ -163,6 +167,8 @@ void usbip_event_add(struct usbip_device *ud, unsigned long event) goto out; ue->ud = ud; +if (ud->eh_ops.get) + ud->eh_ops.get (ud);list_add_tail (&ue->node, &event_list);queue_work (usbip_queue, &usbip_work);
Signature · 1 line
2.53.0
[email protected]
AI reviewRe: [PATCH 1/2] usbip: stub: reference count struct stub_device
Thank you for your contribution! Sashiko AI review found 2 potential issues to consider: - [High] usbip: stub: hung stub_tx, leaks, and URB UAF when shutdown is missed - [Medium] usbip…
Thank you for your contribution! Sashiko AI review found 2 potential issues to consider:
- [High] usbip: stub: hung stub_tx, leaks, and URB UAF when shutdown is missed
- [Medium] usbip: use-after-free in event_handler() reading eh_ops.put after wake_up()
--
commit cd9a80911da15b88ceb306a5e8d623b311108647
Author: Michael Grzeschik <[email protected]>
usbip: stub: reference count struct stub_device
This commit adds kref reference counting to struct stub_device and holds references across device binding, the rx/tx kthreads, and queued events so that stub_device and its usb_device are not freed while still in use.
Quoted 13 lines
drivers/usb/usbip/stub_dev.c +5
-0@@ -11,6 +11,42 @@#include "usbip_common.h" #include "stub.h" +/* +* A stub_device is referenced by its binding to the USB device (from probe +* until disconnect), by the rx and tx threads while they run, and by the +* event handler while an event for it is queued or being handled. It must +* not be freed before all of them are done with it: stopping the threads
drivers/usb/usbip/stub_dev.c line 28
Quoted 10 lines
drivers/usb/usbip/stub_dev.c +10
-0+* relies on the event handler, which can miss a shutdown request or still +* have an event queued when disconnect wants to free the device. +*/ +static void stub_device_release (struct kref *kref) +{ + struct stub_device *sdev = container_of(kref, struct stub_device, kref); + +usb_put_dev (sdev->udev); +kfree (sdev); +}
[Severity: High]
If the rx and tx threads outlive stub_disconnect() because stub_shutdown_connection() was missed, does stub_tx_loop() hang forever on sdev->tx_waitq, or does stub_device_release() free sdev while in-flight URBs and connection resources are still active?
Once shutdown_busid() queues SDEV_EVENT_REMOVED during stub_disconnect(), USBIP_EH_BYE is set in ud->event, which causes any later usbip_event_add() call to return early without scheduling stub_shutdown_connection():
drivers/usb/usbip/usbip_event.c:usbip_event_add() {
if (ud->event & USBIP_EH_BYE)
return;
}Without stub_shutdown_connection() calling kthread_stop_put(ud->tcp_tx), stub_tx_loop() blocks indefinitely in wait_event_interruptible() on sdev->tx_waitq because neither socket closure nor usbip_event_add() wakes that queue.
Even if both threads exit on a socket error and drop their references, stub_device_release() only drops sdev->udev and frees sdev without running the cleanup from stub_shutdown_connection(). That leaks the ud->tcp_rx and ud->tcp_tx task structs, ud->tcp_socket, and the sdev->unlink_tx and sdev->unlink_free lists, and leaves in-flight URBs on sdev->priv_init to dereference the freed sdev in stub_complete(). In addition, letting stub_disconnect() return while threads or events are still running allows usbip_host_exit() to destroy stub_priv_cache and finish unloading the module while sdev is still in use.
[ ... ]
Quoted 5 lines
drivers/usb/usbip/usbip_event.c +0
-0
drivers/usb/usbip/usbip_event.c static void event_handler(struct work_struct *work) · line 102
Quoted 9 lines
drivers/usb/usbip/usbip_event.c +4
-0@@ -94,6 +94,10 @@ static void event_handler(struct work_struct *work)mutex_unlock (&ud->sysfs_lock);wake_up (&ud->eh_waitq); + +/* drop the reference taken by usbip_event_add() */ +if (ud->eh_ops.put) + ud->eh_ops.put (ud); } }
[Severity: Medium]
Can waking ud->eh_waitq before reading ud->eh_ops.put cause a use-after-free on ud?
vhci and vudc do not set eh_ops.get or eh_ops.put, and their removal paths free the enclosing device as soon as usbip_stop_eh() returns from waiting on ud->eh_waitq:
CPU0 (event_handler) CPU1 (vudc_remove)unset_event(ud, ...)wake_up(&ud->eh_waitq)
usb_del_gadget_udc()
vgadget_pullup()
usbip_stop_eh(&udc->ud) /* returns */
kfree(udc)if (ud->eh_ops.put) <-- use-after-free
For stub_device, waking usbip_stop_eh() before ud->eh_ops.put(ud) runs (or while a second event queued for the same ud is still on event_list) also lets stub_disconnect() and usbip_host_exit() finish unloading usbip-host.ko while event_handler() in usbip-core.ko is still about to call stub_eh_put().
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
PATCH 2/2 usbip: identify the event handler with current_work()
In reply to Michael Grzeschikusbip_in_eh() compares the task with worker_context, the first worker that ever ran the event handler. Workers of the unbound usbip_event workqueue are reaped when idle and new ones run the handler later, so the check fails inside the event handler, and the remembered task_struct may be reused by any other task, which then wrongly counts as the event handler. stub_disconnect() relies on it to skip the shutdown of a device reset from the event handler.
Ask the workqueue which work item the current task executes instead.
--- drivers/usb/usbip/usbip_event.c | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-)
drivers/usb/usbip/usbip_event.c +2 -9
@@ -57,16 +57,11 @@ static struct usbip_device *get_event(void)return ud; } -static struct task_struct *worker_context;static void event_handler (struct work_struct *work) {struct usbip_device *ud; -if (worker_context ==NULL ) { - worker_context = current; - } -while ((ud =get_event ()) !=NULL ) {usbip_dbg_eh ("pending event %lx\n" , ud->event);
@@ -194,9 +189,7 @@ EXPORT_SYMBOL_GPL(usbip_event_happened);int usbip_in_eh (struct task_struct *task) { -if (task == worker_context) -return 1 ; - -return 0 ; +/* only the worker running usbip_work is the event handler */ +return task == current &¤t_work () == &usbip_work; }EXPORT_SYMBOL_GPL (usbip_in_eh);
Signature · 1 line
2.53.0
[email protected]
AI reviewRe: [PATCH 2/2] usbip: identify the event handler with current_work()
Thank you for your contribution! Sashiko AI review found 1 potential issue to consider: - [Low] usbip: missing Fixes: trailer for commit bb7871ad99ea --
Thank you for your contribution! Sashiko AI review found 1 potential issue to consider:
- [Low] usbip: missing Fixes: trailer for commit bb7871ad99ea --
commit 823e2d0c8c9cae9400078a44aab5ee7919c0b1e1
Author: Michael Grzeschik <[email protected]>
Quoted 10 lines
usbip: identify the event handler with
current_work()
usbip_in_eh()compares the task withworker_context, the first worker that ever ran the event handler. Workers of the unboundusbip_eventworkqueue are reaped when idle and new ones run the handler later, so the check fails inside the event handler, and the rememberedtask_structmay be reused by any other task, which then wrongly counts as the event handler.stub_disconnect()relies on it to skip the shutdown of a device reset from the event handler.
Quoted 3 lines
Ask the workqueue which work item the current task executes instead.
Signed-off-by Michael Grzeschik <[email protected]>
[Severity: Low]
This fixes the worker_context check introduced when the event handler was converted to a workqueue. Should this include:
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2