* [PATCH] usb: musb: host: clear stale TT buffers after aborted transfers
@ 2026-11-03 19:03 Jhon Wilker Rodrigues Aquino
2026-11-03 19:27 ` sashiko-bot
0 siblings, 1 reply; 2+ messages in thread
From: Jhon Wilker Rodrigues Aquino @ 2026-11-03 19:03 UTC (permalink / raw)
To: Bin Liu, Greg Kroah-Hartman
Cc: linux-usb, linux-kernel, Jhon Wilker Rodrigues Aquino,
Jhon Wilker Rodrigues Aquino
From: Jhon Wilker Rodrigues Aquino <jhon.aquino@autotrac.com.br>
On AM335x with MUSB and a Microchip USB2514 hub, disconnecting a
full-speed CDC ACM device during bulk traffic can leave its TT unable
to enumerate full-speed devices. High-speed devices still work, and
Reset_TT restores operation.
Queue Clear-TT-Buffer for failed or unlinked control and bulk URBs
behind an external TT. Park the affected endpoint queue until the
asynchronous clear completes, so new URBs cannot use a buffer being
cleared. Keep the parked queue alive until the callback finishes,
including during endpoint disable.
Signed-off-by: Jhon Wilker Rodrigues Aquino <jhonwilker0112@gmail.com>
---
drivers/usb/musb/musb_host.c | 166 +++++++++++++++++++++++++++++++----
drivers/usb/musb/musb_host.h | 2 +
2 files changed, 152 insertions(+), 16 deletions(-)
diff --git a/drivers/usb/musb/musb_host.c b/drivers/usb/musb/musb_host.c
index 31b0612489911..6e8cf462676ec 100644
--- a/drivers/usb/musb/musb_host.c
+++ b/drivers/usb/musb/musb_host.c
@@ -22,6 +22,11 @@
#include "musb_host.h"
#include "musb_trace.h"
+static bool tt_clear = true;
+module_param(tt_clear, bool, 0644);
+MODULE_PARM_DESC(tt_clear,
+ "Clear hub TT buffers after failed or unlinked control/bulk transfers (default Y; set N only for A/B comparison or if a hub mishandles CLEAR_TT_BUFFER; affects new requests only)");
+
/* MUSB HOST status 22-mar-2006
*
* - There's still lots of partial code duplication for fault paths, so
@@ -287,6 +292,33 @@ __acquires(musb->lock)
spin_lock(&musb->lock);
}
+/*
+ * A failed or unlinked control/bulk split can leave the hub's transaction
+ * translator buffer busy; the TT then NAKs later splits using that buffer
+ * (USB 2.0 11.17.5). A STALL completes the split, so it needs no clear.
+ *
+ * Context: caller owns controller lock and an URB which was on hardware
+ */
+static void musb_clear_tt_buffer(struct musb *musb, struct musb_qh *qh,
+ struct urb *urb, int status)
+{
+ struct usb_tt *tt = urb->dev->tt;
+
+ if (!READ_ONCE(tt_clear) || qh->clearing_tt)
+ return;
+ if (!tt || !tt->hub || tt->hub == musb->hcd->self.root_hub)
+ return;
+ if (!usb_pipecontrol(urb->pipe) && !usb_pipebulk(urb->pipe))
+ return;
+ if (!urb->unlinked && (!status || status == -EPIPE))
+ return;
+
+ if (usb_hub_clear_tt_buffer(urb))
+ return;
+
+ qh->clearing_tt = true;
+}
+
/*
* Advance this hardware endpoint's queue, completing the specified URB and
* advancing to either the next URB queued to that qh, or else invalidating
@@ -329,11 +361,18 @@ static void musb_advance_schedule(struct musb *musb, struct urb *urb,
qh = musb_ep_get_qh(hw_ep, is_in);
/* reclaim resources (and bandwidth) ASAP; deschedule it, and
- * invalidate qh as soon as list_empty(&hep->urb_list)
+ * invalidate qh as soon as list_empty(&hep->urb_list).
+ *
+ * While Clear-TT-Buffer is pending the endpoint must not use the TT,
+ * so a qh with clearing_tt also releases hw_ep, letting other qhs
+ * (including the hub's own control requests) run. It stays in
+ * hep->hcpriv, so new URBs queue on it, until
+ * musb_clear_tt_buffer_complete() resumes or releases it.
*/
- if (qh && list_empty(&qh->hep->urb_list)) {
+ if (qh && (qh->clearing_tt || list_empty(&qh->hep->urb_list))) {
struct list_head *head;
struct dma_controller *dma = musb->dma_controller;
+ bool park = qh->clearing_tt;
if (is_in) {
ep->rx_reinit = 1;
@@ -351,7 +390,10 @@ static void musb_advance_schedule(struct musb *musb, struct urb *urb,
/* Clobber old pointers to this qh */
musb_ep_set_qh(ep, is_in, NULL);
- qh->hep->hcpriv = NULL;
+ if (park)
+ qh->tt_parked = true;
+ else
+ qh->hep->hcpriv = NULL;
switch (qh->type) {
@@ -362,8 +404,9 @@ static void musb_advance_schedule(struct musb *musb, struct urb *urb,
*/
if (qh->mux == 1) {
head = qh->ring.prev;
- list_del(&qh->ring);
- kfree(qh);
+ list_del_init(&qh->ring);
+ if (!park)
+ kfree(qh);
qh = first_qh(head);
break;
}
@@ -375,7 +418,8 @@ static void musb_advance_schedule(struct musb *musb, struct urb *urb,
* de-allocated if it's tracked and allocated;
* and where we'd update the schedule tree...
*/
- kfree(qh);
+ if (!park)
+ kfree(qh);
qh = NULL;
break;
}
@@ -1168,8 +1212,10 @@ irqreturn_t musb_h_ep0_irq(struct musb *musb)
musb->ep0_stage = MUSB_EP0_IDLE;
/* call completion handler if done */
- if (complete)
+ if (complete) {
+ musb_clear_tt_buffer(musb, qh, urb, status);
musb_advance_schedule(musb, urb, hw_ep, 1);
+ }
done:
return retval;
}
@@ -1406,6 +1452,7 @@ void musb_host_tx(struct musb *musb, u8 epnum)
/* set status */
urb->status = status;
urb->actual_length = qh->offset;
+ musb_clear_tt_buffer(musb, qh, urb, status);
musb_advance_schedule(musb, urb, hw_ep, USB_DIR_OUT);
return;
} else if ((usb_pipeisoc(pipe) || transfer_pending) && dma) {
@@ -1993,6 +2040,7 @@ void musb_host_rx(struct musb *musb, u8 epnum)
if (urb->status == -EINPROGRESS)
urb->status = status;
+ musb_clear_tt_buffer(musb, qh, urb, urb->status);
musb_advance_schedule(musb, urb, hw_ep, USB_DIR_IN);
}
}
@@ -2122,6 +2170,56 @@ static int musb_schedule(
return 0;
}
+/*
+ * Continue a qh parked by musb_advance_schedule() once its Clear-TT-Buffer
+ * finished: release it when no URB is queued, else schedule it again. While
+ * the endpoint is being disabled (!is_ready) or the host is inactive it stays
+ * parked; the dequeue and endpoint_disable paths finish it.
+ *
+ * Context: caller owns controller lock, irqs blocked
+ */
+static void musb_tt_resume(struct musb *musb, struct musb_qh *qh)
+{
+ struct usb_host_endpoint *hep = qh->hep;
+ int is_in = usb_endpoint_dir_in(&hep->desc);
+
+ if (!qh->tt_parked || qh->clearing_tt || !qh->is_ready)
+ return;
+
+ if (list_empty(&hep->urb_list)) {
+ hep->hcpriv = NULL;
+ kfree(qh);
+ return;
+ }
+
+ if (!is_host_active(musb) || !musb->is_active)
+ return;
+
+ qh->tt_parked = false;
+ /* as for a new control/bulk qh; the shared bulk_ep sets a NAK limit */
+ qh->intv_reg = 0;
+ if (musb_schedule(musb, qh, is_in))
+ qh->tt_parked = true;
+}
+
+/* Called by hub_tt_work() after the hub processed (or failed) the request */
+static void musb_clear_tt_buffer_complete(struct usb_hcd *hcd,
+ struct usb_host_endpoint *hep)
+{
+ struct musb *musb = hcd_to_musb(hcd);
+ struct musb_qh *qh;
+ unsigned long flags;
+
+ spin_lock_irqsave(&musb->lock, flags);
+ /* musb_h_disable() keeps hep->hcpriv valid while clearing_tt is set */
+ qh = hep->hcpriv;
+ if (qh && qh->clearing_tt) {
+ qh->clearing_tt = false;
+ musb_tt_resume(musb, qh);
+ }
+ spin_unlock_irqrestore(&musb->lock, flags);
+}
+
static int musb_urb_enqueue(
struct usb_hcd *hcd,
struct urb *urb,
@@ -2145,13 +2243,17 @@ static int musb_urb_enqueue(
spin_lock_irqsave(&musb->lock, flags);
ret = usb_hcd_link_urb_to_ep(hcd, urb);
qh = ret ? NULL : hep->hcpriv;
- if (qh)
+ if (qh) {
urb->hcpriv = qh;
+ /* restart a parked qh left idle while the host was inactive */
+ musb_tt_resume(musb, qh);
+ }
spin_unlock_irqrestore(&musb->lock, flags);
/* DMA mapping was already done, if needed, and this urb is on
* hep->urb_list now ... so we're done, unless hep wasn't yet
- * scheduled onto a live qh.
+ * scheduled onto a live qh. A qh parked for Clear-TT-Buffer
+ * also holds new URBs until musb_tt_resume() schedules it.
*
* REVISIT best to keep hep->hcpriv valid until the endpoint gets
* disabled, testing for empty qh->ring and avoiding qh setup costs
@@ -2367,8 +2469,11 @@ static int musb_cleanup_urb(struct urb *urb, struct musb_qh *qh)
} else {
musb_h_ep0_flush_fifo(ep);
}
- if (status == 0)
+ if (status == 0) {
+ /* hardware stopped; the TT may still hold this transaction */
+ musb_clear_tt_buffer(musb, qh, urb, urb->status);
musb_advance_schedule(ep->musb, urb, ep, is_in);
+ }
return status;
}
@@ -2412,10 +2517,13 @@ static int musb_urb_dequeue(struct usb_hcd *hcd, struct urb *urb, int status)
musb_giveback(musb, urb, 0);
qh->is_ready = ready;
- /* If nothing else (usually musb_giveback) is using it
- * and its URB list has emptied, recycle this qh.
- */
- if (ready && list_empty(&qh->hep->urb_list)) {
+ /* A parked qh is not on hw_ep; never clobber hw_ep for it */
+ if (qh->tt_parked) {
+ musb_tt_resume(musb, qh);
+ } else if (ready && list_empty(&qh->hep->urb_list)) {
+ /* If nothing else (usually musb_giveback) is using it
+ * and its URB list has emptied, recycle this qh.
+ */
musb_ep_set_qh(qh->hw_ep, is_in, NULL);
qh->hep->hcpriv = NULL;
list_del(&qh->ring);
@@ -2460,13 +2568,18 @@ musb_h_disable(struct usb_hcd *hcd, struct usb_host_endpoint *hep)
/* Then nuke all the others ... and advance the
* queue on hw_ep (e.g. bulk ring) when we're done.
+ * A qh parked by the cleanup no longer owns hw_ep.
*/
while (!list_empty(&hep->urb_list)) {
urb = next_urb(qh);
urb->status = -ESHUTDOWN;
- musb_advance_schedule(musb, urb, qh->hw_ep, is_in);
+ if (qh->tt_parked)
+ musb_giveback(musb, urb, -ESHUTDOWN);
+ else
+ musb_advance_schedule(musb, urb, qh->hw_ep,
+ is_in);
}
- } else {
+ } else if (!qh->tt_parked) {
/* Just empty the queue; the hardware is busy with
* other transfers, and since !qh->is_ready nothing
* will activate any of these as it advances.
@@ -2478,6 +2591,26 @@ musb_h_disable(struct usb_hcd *hcd, struct usb_host_endpoint *hep)
list_del(&qh->ring);
kfree(qh);
}
+
+ /*
+ * hub_tt_work() passes hep to musb_clear_tt_buffer_complete(), which
+ * dereferences hep->hcpriv: free a parked qh only after its clear
+ * finished (bounded by the hub control request timeout).
+ */
+ while ((qh = hep->hcpriv) && qh->tt_parked) {
+ while (!list_empty(&hep->urb_list))
+ musb_giveback(musb, next_urb(qh), -ESHUTDOWN);
+
+ if (!qh->clearing_tt) {
+ hep->hcpriv = NULL;
+ kfree(qh);
+ break;
+ }
+
+ spin_unlock_irqrestore(&musb->lock, flags);
+ schedule_timeout_uninterruptible(1);
+ spin_lock_irqsave(&musb->lock, flags);
+ }
exit:
spin_unlock_irqrestore(&musb->lock, flags);
}
@@ -2689,6 +2822,7 @@ static const struct hc_driver musb_hc_driver = {
.urb_enqueue = musb_urb_enqueue,
.urb_dequeue = musb_urb_dequeue,
.endpoint_disable = musb_h_disable,
+ .clear_tt_buffer_complete = musb_clear_tt_buffer_complete,
#ifndef CONFIG_MUSB_PIO_ONLY
.map_urb_for_dma = musb_map_urb_for_dma,
diff --git a/drivers/usb/musb/musb_host.h b/drivers/usb/musb/musb_host.h
index 63298bd233e02..8241fe2aaaa4c 100644
--- a/drivers/usb/musb/musb_host.h
+++ b/drivers/usb/musb/musb_host.h
@@ -40,6 +40,8 @@ struct musb_qh {
unsigned iso_idx; /* in urb->iso_frame_desc[] */
struct sg_mapping_iter sg_miter; /* for highmem in PIO mode */
bool use_sg; /* to track urb using sglist */
+ bool clearing_tt; /* Clear-TT-Buffer in progress */
+ bool tt_parked; /* off hw_ep until TT is cleared */
};
/* map from control or bulk queue head to the first qh on that ring */
--
2.43.0
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH] usb: musb: host: clear stale TT buffers after aborted transfers
2026-11-03 19:03 [PATCH] usb: musb: host: clear stale TT buffers after aborted transfers Jhon Wilker Rodrigues Aquino
@ 2026-11-03 19:27 ` sashiko-bot
0 siblings, 0 replies; 2+ messages in thread
From: sashiko-bot @ 2026-11-03 19:27 UTC (permalink / raw)
To: Jhon Wilker Rodrigues Aquino; +Cc: linux-usb
Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [Critical] usb: musb: qh UAF and is_ready corruption in musb_urb_dequeue()
- [High] usb: musb: unparked qh race across musb_giveback() in musb_advance_schedule()
- [High] usb: musb: stranded URBs on parked qh after inactive host in musb_tt_resume()
- [Medium] usb: musb: spurious TT clear on -EREMOTEIO in musb_clear_tt_buffer()
- [Medium] usb: musb: unreset qh->hb_mult before rescheduling in musb_tt_resume()
- [Low] usb: musb: undocumented tt_clear module parameter in musb_host.c
- [Low] usb: musb: open-coded spinlock polling loop in musb_h_disable()
--
commit 64201419e6f04cb16c53e5b8f07d1c18f4fb355b
Author: Jhon Wilker Rodrigues Aquino <jhon.aquino@autotrac.com.br>
usb: musb: host: clear stale TT buffers after aborted transfers
This commit queues Clear-TT-Buffer requests for failed or unlinked control
and bulk URBs behind an external hub transaction translator, and parks the
affected endpoint queue until the asynchronous clear finishes.
> diff --git a/drivers/usb/musb/musb_host.c b/drivers/usb/musb/musb_host.c
> index 31b0612489911..6e8cf462676ec 100644
> --- a/drivers/usb/musb/musb_host.c
> +++ b/drivers/usb/musb/musb_host.c
> @@ -22,6 +22,11 @@
> #include "musb_host.h"
> #include "musb_trace.h"
>
> +static bool tt_clear = true;
> +module_param(tt_clear, bool, 0644);
> +MODULE_PARM_DESC(tt_clear,
> + "Clear hub TT buffers after failed or unlinked control/bulk transfers (default Y; set N only for A/B comparison or if a hub mishandles CLEAR_TT_BUFFER; affects new requests only)");
[Severity: Low]
Should the new tt_clear module parameter (settable at boot as
musb_hdrc.tt_clear) also be documented in
Documentation/admin-guide/kernel-parameters.txt and mentioned in the commit
message?
> @@ -287,6 +292,33 @@ __acquires(musb->lock)
> spin_lock(&musb->lock);
> }
>
> +/*
> + * A failed or unlinked control/bulk split can leave the hub's transaction
> + * translator buffer busy; the TT then NAKs later splits using that buffer
> + * (USB 2.0 11.17.5). A STALL completes the split, so it needs no clear.
> + *
> + * Context: caller owns controller lock and an URB which was on hardware
> + */
> +static void musb_clear_tt_buffer(struct musb *musb, struct musb_qh *qh,
> + struct urb *urb, int status)
> +{
> + struct usb_tt *tt = urb->dev->tt;
> +
> + if (!READ_ONCE(tt_clear) || qh->clearing_tt)
> + return;
> + if (!tt || !tt->hub || tt->hub == musb->hcd->self.root_hub)
> + return;
> + if (!usb_pipecontrol(urb->pipe) && !usb_pipebulk(urb->pipe))
> + return;
> + if (!urb->unlinked && (!status || status == -EPIPE))
> + return;
> +
> + if (usb_hub_clear_tt_buffer(urb))
> + return;
> +
> + qh->clearing_tt = true;
> +}
[Severity: Medium]
Can this trigger a spurious Clear-TT-Buffer when a bulk or control IN URB
with URB_SHORT_NOT_OK completes normally with a short packet?
In musb_host_packet_rx(), receiving a short packet when URB_SHORT_NOT_OK is
set marks the URB as done with -EREMOTEIO without requesting another IN
token:
drivers/usb/musb/musb_host.c:musb_host_packet_rx() {
...
if (done
&& (urb->status == -EINPROGRESS)
&& (urb->transfer_flags & URB_SHORT_NOT_OK)
&& (urb->actual_length
< urb->transfer_buffer_length))
urb->status = -EREMOTEIO;
...
}
Then musb_host_rx() passes urb->status (-EREMOTEIO) into
musb_clear_tt_buffer(). Since a short packet completes the split transaction
on the bus and leaves the TT buffer free, should musb_clear_tt_buffer() also
exclude status == -EREMOTEIO (or should musb_host_rx() pass the hardware
status)?
> @@ -329,11 +361,18 @@ static void musb_advance_schedule(struct musb *musb, struct urb *urb,
> qh = musb_ep_get_qh(hw_ep, is_in);
>
> /* reclaim resources (and bandwidth) ASAP; deschedule it, and
> - * invalidate qh as soon as list_empty(&hep->urb_list)
> + * invalidate qh as soon as list_empty(&hep->urb_list).
> + *
> + * While Clear-TT-Buffer is pending the endpoint must not use the TT,
> + * so a qh with clearing_tt also releases hw_ep, letting other qhs
> + * (including the hub's own control requests) run. It stays in
> + * hep->hcpriv, so new URBs queue on it, until
> + * musb_clear_tt_buffer_complete() resumes or releases it.
> */
> - if (qh && list_empty(&qh->hep->urb_list)) {
> + if (qh && (qh->clearing_tt || list_empty(&qh->hep->urb_list))) {
> struct list_head *head;
> struct dma_controller *dma = musb->dma_controller;
> + bool park = qh->clearing_tt;
>
> if (is_in) {
> ep->rx_reinit = 1;
> @@ -351,7 +390,10 @@ static void musb_advance_schedule(struct musb *musb, struct urb *urb,
>
> /* Clobber old pointers to this qh */
> musb_ep_set_qh(ep, is_in, NULL);
> - qh->hep->hcpriv = NULL;
> + if (park)
> + qh->tt_parked = true;
> + else
> + qh->hep->hcpriv = NULL;
[Severity: High]
Because musb_clear_tt_buffer() sets qh->clearing_tt = true and schedules
tt->clear_work before musb_advance_schedule() calls musb_giveback(),
musb->lock is dropped inside musb_giveback() while qh is still attached to
hw_ep and qh->tt_parked is still false:
drivers/usb/musb/musb_host.c:musb_advance_schedule() {
...
qh->is_ready = 0;
musb_giveback(musb, urb, status);
qh->is_ready = ready;
qh = musb_ep_get_qh(hw_ep, is_in);
if (qh && (qh->clearing_tt || list_empty(&qh->hep->urb_list))) {
...
}
What happens if another CPU runs during that window while musb->lock is
dropped in musb_giveback()?
1. After usb_hcd_unlink_urb_from_ep() empties hep->urb_list (or after
__usb_hcd_giveback_urb() wakes usb_kill_urb()), a concurrent
musb_h_disable() can acquire musb->lock before musb_giveback() re-acquires
it. In musb_h_disable(), musb_ep_get_qh(qh->hw_ep, is_in) == qh is still
true, so it calls urb = next_urb(qh) on the empty list and dereferences a
NULL urb at if (!urb->unlinked).
2. If a concurrent musb_urb_dequeue() for another URB on the same qh started
before musb_advance_schedule() and finishes its own musb_giveback() while
hep->urb_list is now empty, it sees !qh->tt_parked && ready &&
list_empty(&qh->hep->urb_list) and frees qh while qh->clearing_tt is active.
3. If hub_tt_work() runs musb_clear_tt_buffer_complete() while musb->lock is
dropped in musb_giveback(), it clears qh->clearing_tt = false while
qh->tt_parked is still false. When musb_advance_schedule() resumes with
another URB queued on hep->urb_list, it sees !qh->clearing_tt and starts the
next URB without setting ep->rx_reinit or ep->tx_reinit.
[ ... ]
> @@ -2122,6 +2170,56 @@ static int musb_schedule(
> return 0;
> }
>
> +/*
> + * Continue a qh parked by musb_advance_schedule() once its Clear-TT-Buffer
> + * finished: release it when no URB is queued, else schedule it again. While
> + * the endpoint is being disabled (!is_ready) or the host is inactive it stays
> + * parked; the dequeue and endpoint_disable paths finish it.
> + *
> + * Context: caller owns controller lock, irqs blocked
> + */
> +static void musb_tt_resume(struct musb *musb, struct musb_qh *qh)
> +{
> + struct usb_host_endpoint *hep = qh->hep;
> + int is_in = usb_endpoint_dir_in(&hep->desc);
> +
> + if (!qh->tt_parked || qh->clearing_tt || !qh->is_ready)
> + return;
> +
> + if (list_empty(&hep->urb_list)) {
> + hep->hcpriv = NULL;
> + kfree(qh);
> + return;
> + }
> +
> + if (!is_host_active(musb) || !musb->is_active)
> + return;
[Severity: High]
If musb_clear_tt_buffer_complete() runs while !is_host_active(musb) ||
!musb->is_active (for example, during root port suspend/resume before
musb_host_finish_resume() sets musb->is_active = 1) and hep->urb_list is
non-empty, musb_clear_tt_buffer_complete() clears qh->clearing_tt = false and
musb_tt_resume() returns early here with qh->tt_parked still true and qh
detached from the hardware rings.
When the host becomes active again, nothing calls musb_tt_resume() unless a
new URB is submitted via musb_urb_enqueue(). If the driver is waiting for
the URBs already queued in hep->urb_list to complete before submitting more,
won't those URBs remain stranded on the parked qh?
> +
> + qh->tt_parked = false;
> + /* as for a new control/bulk qh; the shared bulk_ep sets a NAK limit */
> + qh->intv_reg = 0;
> + if (musb_schedule(musb, qh, is_in))
> + qh->tt_parked = true;
> +}
[Severity: Medium]
Does musb_tt_resume() also need to reset qh->hb_mult before calling
musb_schedule()?
When a bulk OUT transfer is programmed on a controller where
can_bulk_split(musb, qh->type) is true, musb_ep_program() overwrites
qh->hb_mult with hw_ep->max_packet_sz_tx / packet_sz:
drivers/usb/musb/musb_host.c:musb_ep_program() {
...
if (can_bulk_split(musb, qh->type)) {
qh->hb_mult = hw_ep->max_packet_sz_tx
/ packet_sz;
musb_writew(epio, MUSB_TXMAXP, packet_sz
| ((qh->hb_mult) - 1) << 11);
...
}
When musb_tt_resume() later calls musb_schedule(), musb_schedule() checks
candidate endpoints against diff -= (qh->maxpacket * qh->hb_mult). If
qh->hb_mult still holds the multiplier from the previous endpoint's TX FIFO
size instead of 1, won't musb_schedule() reject smaller free hardware
endpoints and fall back to the shared musb->bulk_ep?
[ ... ]
> @@ -2412,10 +2517,13 @@ static int musb_urb_dequeue(struct usb_hcd *hcd, struct urb *urb, int status)
> musb_giveback(musb, urb, 0);
> qh->is_ready = ready;
>
> - /* If nothing else (usually musb_giveback) is using it
> - * and its URB list has emptied, recycle this qh.
> - */
> - if (ready && list_empty(&qh->hep->urb_list)) {
> + /* A parked qh is not on hw_ep; never clobber hw_ep for it */
> + if (qh->tt_parked) {
> + musb_tt_resume(musb, qh);
> + } else if (ready && list_empty(&qh->hep->urb_list)) {
> + /* If nothing else (usually musb_giveback) is using it
> + * and its URB list has emptied, recycle this qh.
> + */
> musb_ep_set_qh(qh->hw_ep, is_in, NULL);
> qh->hep->hcpriv = NULL;
> list_del(&qh->ring);
[Severity: Critical]
Can this cause a use-after-free on qh or leave qh->is_ready stuck at 0 when
musb_urb_dequeue() drops musb->lock inside musb_giveback()?
In musb_urb_dequeue():
if (!qh->is_ready
|| urb->urb_list.prev != &qh->hep->urb_list
|| musb_ep_get_qh(qh->hw_ep, is_in) != qh) {
int ready = qh->is_ready;
qh->is_ready = 0;
musb_giveback(musb, urb, 0);
qh->is_ready = ready;
/* A parked qh is not on hw_ep; never clobber hw_ep for it */
if (qh->tt_parked) {
musb_tt_resume(musb, qh);
} else if (ready && list_empty(&qh->hep->urb_list)) {
There are two races across musb_giveback() here:
1. If urb1 is already inside musb_giveback() (either in
musb_advance_schedule() while parking qh, or in musb_urb_dequeue() on an
already-parked qh), it has saved ready = 1 and set qh->is_ready = 0. If a
second CPU calls musb_urb_dequeue() for urb2 on the same qh while musb->lock
is dropped, CPU 2 observes qh->is_ready == 0, saves ready = 0, unlinks urb2,
and drops musb->lock in musb_giveback().
If CPU 1 finishes musb_giveback() first, it restores qh->is_ready = 1, and
once qh->clearing_tt is false, musb_tt_resume() sees qh->is_ready == 1 and
list_empty(&hep->urb_list) and frees qh via kfree(qh). When CPU 2 returns
from musb_giveback(), it writes qh->is_ready = 0 and reads qh->tt_parked on
the freed qh.
Conversely, if qh->clearing_tt is still true when CPU 2 returns from
musb_giveback() after CPU 1 restored qh->is_ready = 1, CPU 2 overwrites
qh->is_ready = 0. When musb_clear_tt_buffer_complete() later calls
musb_tt_resume(), it bails out on !qh->is_ready, leaking qh if hep->urb_list
is empty or permanently wedging the endpoint queue if new URBs were queued.
2. Because musb_giveback() unlinks urb from hep->urb_list before dropping
musb->lock and __usb_hcd_giveback_urb() wakes usb_kill_urb() before
musb_giveback() re-acquires musb->lock, usb_disable_endpoint() ->
musb_h_disable() can run on another CPU while musb_urb_dequeue() is still in
musb_giveback(). In musb_h_disable(), if !qh->clearing_tt, it frees qh right
away; when musb_urb_dequeue() re-acquires musb->lock, it writes
qh->is_ready = 1 and calls musb_tt_resume() on the freed qh, leading to a
use-after-free and double-free.
> @@ -2460,13 +2568,18 @@ musb_h_disable(struct usb_hcd *hcd, struct usb_host_endpoint *hep)
>
> /* Then nuke all the others ... and advance the
> * queue on hw_ep (e.g. bulk ring) when we're done.
> + * A qh parked by the cleanup no longer owns hw_ep.
> */
> while (!list_empty(&hep->urb_list)) {
> urb = next_urb(qh);
> urb->status = -ESHUTDOWN;
> - musb_advance_schedule(musb, urb, qh->hw_ep, is_in);
> + if (qh->tt_parked)
> + musb_giveback(musb, urb, -ESHUTDOWN);
> + else
> + musb_advance_schedule(musb, urb, qh->hw_ep,
> + is_in);
> }
> - } else {
> + } else if (!qh->tt_parked) {
> /* Just empty the queue; the hardware is busy with
> * other transfers, and since !qh->is_ready nothing
> * will activate any of these as it advances.
> @@ -2478,6 +2591,26 @@ musb_h_disable(struct usb_hcd *hcd, struct usb_host_endpoint *hep)
> list_del(&qh->ring);
> kfree(qh);
> }
> +
> + /*
> + * hub_tt_work() passes hep to musb_clear_tt_buffer_complete(), which
> + * dereferences hep->hcpriv: free a parked qh only after its clear
> + * finished (bounded by the hub control request timeout).
> + */
> + while ((qh = hep->hcpriv) && qh->tt_parked) {
> + while (!list_empty(&hep->urb_list))
> + musb_giveback(musb, next_urb(qh), -ESHUTDOWN);
> +
> + if (!qh->clearing_tt) {
> + hep->hcpriv = NULL;
> + kfree(qh);
> + break;
> + }
> +
> + spin_unlock_irqrestore(&musb->lock, flags);
> + schedule_timeout_uninterruptible(1);
> + spin_lock_irqsave(&musb->lock, flags);
> + }
[Severity: Low]
Instead of polling !qh->clearing_tt in a loop with
schedule_timeout_uninterruptible(1) under musb->lock in musb_h_disable(),
could this use a completion variable signaled by
musb_clear_tt_buffer_complete()?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261103190333.87207-1-jhonwilker0112@gmail.com?part=1
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-11-03 19:27 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-03 19:03 [PATCH] usb: musb: host: clear stale TT buffers after aborted transfers Jhon Wilker Rodrigues Aquino
2026-11-03 19:27 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox