Linux USB
 help / color / mirror / Atom feed
From: "Elvira Khabirova" <lineprinter0@gmail.com>
To: <sashiko-reviews@lists.linux.dev>,
	"Elvira Khabirova" <lineprinter0@gmail.com>
Cc: <linux-usb@vger.kernel.org>
Subject: Re: [PATCH v2] usb: musb: gadget: don't flush a waiting RX packet when queueing a request
Date: Sat, 07 Nov 2026 13:12:45 +0100	[thread overview]
Message-ID: <DMOYWU5HB1UM.BQG0BZT7RFCP@gmail.com> (raw)
In-Reply-To: <sashiko-outbox-202856@kernel.org>

On Sat Nov 7, 2026 at 12:30 PM CET, sashiko-bot wrote:
> Thank you for your contribution! Sashiko AI review found 3 potential
> issues to consider:
> - [High] usb: musb: gadget: overlapping musb_g_giveback() calls wedge
>   ep->busy
> - [High] usb: musb: gadget: race and ordering bug in
>   musb_gadget_disable() for rx_kick
> - [Medium] usb: musb: gadget: lost rx_kick across system suspend and
>   resume
> --
>
> commit cef9b9e2278bf856fd53af421c899c0ae67866b7
> Author: Elvira Khabirova <lineprinter0@gmail.com>
>
> usb: musb: gadget: don't flush a waiting RX packet when queueing a request
>
> This commit replaces the RX FIFO flush in musb_ep_restart() with a
> system_bh_wq work item that invokes musb_g_rx(), and defers RX processing
> via ep->rx_kick_deferred while ep->busy is set.
>
>> diff --git a/drivers/usb/musb/musb_gadget.c b/drivers/usb/musb/musb_gadget.c
>> index 016d3f3fc1e05..c71c60efe5106 100644
>> --- a/drivers/usb/musb/musb_gadget.c
>> +++ b/drivers/usb/musb/musb_gadget.c
>> @@ -148,6 +148,21 @@ __acquires(ep->musb->lock)
>>  	usb_gadget_giveback_request(&req->ep->end_point, &req->request);
>>  	spin_lock(&musb->lock);
>>  	ep->busy = busy;
>> +
>> +	if (!ep->busy && ep->rx_kick_deferred) {
>> +		ep->rx_kick_deferred = false;
>> +		if (!queue_work(system_bh_wq, &ep->rx_kick))
>> +			pm_runtime_put_noidle(musb->controller);
>> +	}
>> +}
>
> [Severity: High]
> Can overlapping musb_g_giveback() calls finish out of LIFO order and leave
> ep->busy permanently stuck at 1?
>
> musb_g_giveback() saves ep->busy in a stack variable, sets ep->busy = 1,
> and drops musb->lock around usb_gadget_giveback_request().  With RX
> givebacks running in system_bh_wq, a second giveback on the same endpoint
> (for example via musb_gadget_dequeue(), which enters musb_g_giveback()
> when musb_ep->busy is set) can overlap on another CPU:
>
> CPU0                              CPU1
> musb_g_rx_kick()
>   musb_g_rx()
>     musb_g_giveback()
>       busy = ep->busy; // 0
>       ep->busy = 1;
>       spin_unlock(&musb->lock)
>       usb_gadget_giveback_request()
>                                   musb_gadget_dequeue()
>                                     spin_lock_irqsave(&musb->lock, ...)
>                                     musb_g_giveback()
>                                       busy = ep->busy; // 1
>                                       ep->busy = 1;
>                                       spin_unlock(&musb->lock)
>                                       usb_gadget_giveback_request()
>       spin_lock(&musb->lock)
>       ep->busy = 0;
>       queue_work(..., &ep->rx_kick)
>       spin_unlock(&musb->lock)
>                                       spin_lock(&musb->lock)
>                                       ep->busy = 1;  <-- stuck at 1
>
> First, CPU0 clears ep->busy to 0 and schedules ep->rx_kick while CPU1 is
> still in usb_gadget_giveback_request().  Second, when CPU1 finishes, it
> writes its saved busy = 1 back to ep->busy.  After that, every subsequent
> musb_g_rx(), musb_g_rx_kick(), and musb_gadget_queue() call sees
> ep->busy == 1 and defers work forever, wedging the endpoint and leaking
> the runtime PM reference taken by musb_g_defer_rx_kick().
>
> Making ep->busy an active giveback counter (incremented before dropping
> musb->lock and decremented after re-acquiring it) would ensure ep->busy
> only reaches 0 when the last giveback completes.

Yes, that is possible, but this is a pre-existing bug. I even have
a separate patch prepared to make `busy` a counter instead of a flag.
This patch does not aggravate this problem.
The same race is possible between a dequeue and a HW IRQ.

>> @@ -1111,6 +1137,12 @@ static int musb_gadget_disable(struct usb_ep *ep)
>>  
>>  	/* abort all pending DMA and requests */
>>  	nuke(musb_ep, -ESHUTDOWN);
>> +	if (cancel_work(&musb_ep->rx_kick))
>> +		pm_runtime_put_autosuspend(musb->controller);
>> +	if (musb_ep->rx_kick_deferred) {
>> +		musb_ep->rx_kick_deferred = false;
>> +		pm_runtime_put_autosuspend(musb->controller);
>> +	}
>>  
>>  	musb_ep->desc = NULL;
>>  	musb_ep->end_point.desc = NULL;
>
> [Severity: High]
> Can musb_g_rx_kick() run while nuke() drops musb->lock or after the
> endpoint is re-enabled?
>
> Because musb_ep->desc is cleared after nuke(), musb->lock is dropped
> inside nuke() -> musb_g_giveback() while musb_ep->desc is still non-NULL.
> If an earlier giveback on another CPU finishes during that window, it
> restores ep->busy = 0 and queues ep->rx_kick, allowing musb_g_rx_kick() ->
> musb_g_rx() -> rxstate() to run after nuke() has already freed
> musb_ep->dma.

Good catch, and actually I think this can be resolved with making
ep->busy a counter instead.
I will send them together as a series for v3.

> In addition, cancel_work() is non-synchronous:
>
> CPU0                              CPU1
>                                   <softirq>
>                                   musb_g_rx_kick()
>                                     // waiting for musb->lock
> musb_gadget_disable()
>   spin_lock_irqsave(&musb->lock, ...)
>   cancel_work(&musb_ep->rx_kick) // false
>   musb_ep->desc = NULL;
>   spin_unlock_irqrestore(&musb->lock, ...)
> musb_gadget_enable()
>   spin_lock_irqsave(&musb->lock, ...)
>   musb_ep->desc = desc;
>   musb_ep->busy = 0;
>   spin_unlock_irqrestore(&musb->lock, ...)
>                                     spin_lock_irqsave(&musb->lock, ...)
>                                     if (!musb_ep->desc || ...) // false
>                                     musb_g_rx()  <-- stale kick runs
>
> Clearing musb_ep->desc and musb_ep->end_point.desc before calling nuke()
> prevents musb_g_rx_kick() and musb_gadget_queue() from acting on the
> endpoint while nuke() drops musb->lock, and prevented stale kicks will see
> the updated state if musb_gadget_enable() also cancels any pending
> rx_kick.

Is this any different from before the patch, with a HW IRQ?
I think a stale kick handles everything well, if there is no work,
musb_g_rx() just returns.

>> @@ -1810,6 +1881,36 @@ void musb_gadget_cleanup(struct musb *musb)
>>  
>>  	cancel_delayed_work_sync(&musb->gadget_work);
>>  	usb_del_gadget_udc(&musb->g);
>> +
>> +	musb_gadget_disable_rx_kicks(musb);
>> +}
>> +
>> +static void musb_g_disable_rx_kick(struct musb *musb, struct musb_ep *musb_ep)
>> +{
>> +	if (musb_ep->rx_kick.func && disable_work_sync(&musb_ep->rx_kick))
>> +		pm_runtime_put_noidle(musb->controller);
>> +}
>> +
>> +void musb_gadget_disable_rx_kicks(struct musb *musb)
>> +{
>> +	int i;
>> +
>> +	for (i = 0; i < musb->nr_endpoints; i++) {
>> +		musb_g_disable_rx_kick(musb, &musb->endpoints[i].ep_in);
>> +		musb_g_disable_rx_kick(musb, &musb->endpoints[i].ep_out);
>> +	}
>> +}
>> +
>> +void musb_gadget_enable_rx_kicks(struct musb *musb)
>> +{
>> +	int i;
>> +
>> +	for (i = 0; i < musb->nr_endpoints; i++) {
>> +		if (musb->endpoints[i].ep_in.rx_kick.func)
>> +			enable_work(&musb->endpoints[i].ep_in.rx_kick);
>> +		if (musb->endpoints[i].ep_out.rx_kick.func)
>> +			enable_work(&musb->endpoints[i].ep_out.rx_kick);
>> +	}
>>  }
>
> [Severity: Medium]
> Does musb_gadget_disable_rx_kicks() lose a pending or deferred rx_kick
> across system suspend and resume?
>
> If ep->rx_kick is pending when musb_suspend() calls disable_work_sync(),
> the work item is cancelled and its runtime PM reference is dropped, or if
> musb_g_rx_kick() is currently in musb_g_giveback() with
> ep->rx_kick_deferred set, disable_work_sync() marks ep->rx_kick disabled
> before waiting for completion so queue_work() in musb_g_giveback() fails.
> On resume, musb_gadget_enable_rx_kicks() only calls enable_work(), which
> does not re-queue the cancelled work.  When the USB session is preserved
> across suspend/resume (such as with MUSB_PRESERVE_SESSION or standby),
> MUSB_RXCSR_RXPKTRDY remains set in hardware with no interrupt to
> re-trigger it, stalling RX on the endpoint.

I see, I wish I had more hardware to test on.
This seems to be possible on da8xx.
I will find a way to re-queue the cancelled work items for v3.

      reply	other threads:[~2026-11-07 12:12 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-07 10:55 [PATCH v2] usb: musb: gadget: don't flush a waiting RX packet when queueing a request Elvira Khabirova
2026-11-07 11:30 ` sashiko-bot
2026-11-07 12:12   ` Elvira Khabirova [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=DMOYWU5HB1UM.BQG0BZT7RFCP@gmail.com \
    --to=lineprinter0@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