Linux USB
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Jhon Wilker Rodrigues Aquino" <jhonwilker0112@gmail.com>
Cc: linux-usb@vger.kernel.org
Subject: Re: [PATCH] usb: musb: host: clear stale TT buffers after aborted transfers
Date: Tue, 03 Nov 2026 19:27:57 +0000	[thread overview]
Message-ID: <sashiko-outbox-195915@kernel.org> (raw)
In-Reply-To: <20261103190333.87207-1-jhonwilker0112@gmail.com>

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

      reply	other threads:[~2026-11-03 19:27 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-195915@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=jhonwilker0112@gmail.com \
    --cc=linux-usb@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox