Re: [PATCH v3] usb: dwc3: gadget: Prevent EP resource conflicts during StartTransfer
From: Selvarasu Ganesan
Date: Thu Sep 24 2026 - 08:32:57 EST
On 9/24/2026 5:35 PM, Selvarasu Ganesan wrote:
>
> On 3/7/2026 3:11 AM, Thinh Nguyen wrote:
>> On Fri, Mar 06, 2026, Selvarasu Ganesan wrote:
>>> On 3/3/2026 6:09 AM, Thinh Nguyen wrote:
>>>> On Sat, Feb 28, 2026, Thinh Nguyen wrote:
>>>>> On Fri, Feb 27, 2026, Selvarasu Ganesan wrote:
>>>>>> The below “No resource for ep” warning appears when a StartTransfer
>>>>>> command is issued for bulk or interrupt endpoints in
>>>>>> `dwc3_gadget_ep_enable` while a previous StartTransfer on the same
>>>>>> endpoint is still in progress. The gadget functions drivers can
>>>>>> invoke
>>>>>> `usb_ep_enable` (which triggers a new StartTransfer command)
>>>>>> before the
>>>>>> earlier transfer has completed. Because the previous
>>>>>> StartTransfer is
>>>>>> still active, `dwc3_gadget_ep_disable` can skip the required
>>>>>> `EndTransfer` due to `DWC3_EP_DELAY_STOP`, leading to the endpoint
>>>>>> resources are busy for previous StartTransfer and warning ("No
>>>>>> resource
>>>>>> for ep") from dwc3 driver.
>>>>>>
>>>>>> Additionally, a race condition exists between
>>>>>> dwc3_gadget_ep_disable()
>>>>>> and dwc3_gadget_ep_queue() when manipulating dep->flags. When
>>>>>> dwc3_gadget_ep_disable() calls dwc3_gadget_giveback(), the
>>>>>> dwc->lock is
>>>>>> temporarily released. If dwc3_gadget_ep_queue() runs in that
>>>>>> window, it
>>>>>> may set the DWC3_EP_TRANSFER_STARTED flag as part of
>>>>>> dwc3_send_gadget_ep_cmd(). When ep_disable resumes, it
>>>>>> unconditionally
>>>>>> clears all flags except those explicitly masked, potentially
>>>>>> clearing
>>>>>> DWC3_EP_TRANSFER_STARTED even though a new transfer has started.
>>>>>> This
>>>>>> leads to "No resource for ep" warnings on subsequent StartTransfer
>>>>>> attempts.
>>>>>>
>>>>>> The underlying framework issue is that usb_ep_disable() is
>>>>>> expected to
>>>>>> complete pending requests before returning, but is allowed to be
>>>>>> called
>>>>>> from interrupt context where sleeping to wait for completion is not
>>>>>> possible.
>>>>>>
>>>>>> As temporary workarounds for this framework limitation:
>>>>>>
>>>>>> 1. In __dwc3_gadget_ep_enable(), add a check for the
>>>>>> DWC3_EP_TRANSFER_STARTED flag before issuing a new
>>>>>> StartTransfer.
>>>>>> This prevents a second StartTransfer on an already busy
>>>>>> endpoint,
>>>>>> eliminating the resource conflict.
>>>>>>
>>>>>> 2. In __dwc3_gadget_ep_disable(), preserve the
>>>>>> DWC3_EP_TRANSFER_STARTED
>>>>>> flag when masking dep->flags if it is actually set,
>>>>>> preventing the
>>>>>> race with dwc3_gadget_ep_queue() from corrupting the flag
>>>>>> state.
>>>>>>
>>>>>> These changes eliminate the "No resource for ep" warnings and
>>>>>> potential
>>>>>> kernel panics caused by panic_on_warn.
>>>>>>
>>>>>> dwc3 13200000.dwc3: No resource for ep1out
>>>>>> WARNING: CPU: 0 PID: 700 at drivers/usb/dwc3/gadget.c:398
>>>>>> dwc3_send_gadget_ep_cmd+0x2f8/0x76c
>>>>>> Call trace:
>>>>>> dwc3_send_gadget_ep_cmd+0x2f8/0x76c
>>>>>> __dwc3_gadget_ep_enable+0x490/0x7c0
>>>>>> dwc3_gadget_ep_enable+0x6c/0xe4
>>>>>> usb_ep_enable+0x5c/0x15c
>>>>>> mp_eth_stop+0xd4/0x11c
>>>>>> __dev_close_many+0x160/0x1c8
>>>>>> __dev_change_flags+0xfc/0x220
>>>>>> dev_change_flags+0x24/0x70
>>>>>> devinet_ioctl+0x434/0x524
>>>>>> inet_ioctl+0xa8/0x224
>>>>>> sock_do_ioctl+0x74/0x128
>>>>>> sock_ioctl+0x3bc/0x468
>>>>>> __arm64_sys_ioctl+0xa8/0xe4
>>>>>> invoke_syscall+0x58/0x10c
>>>>>> el0_svc_common+0xa8/0xdc
>>>>>> do_el0_svc+0x1c/0x28
>>>>>> el0_svc+0x38/0x88
>>>>>> el0t_64_sync_handler+0x70/0xbc
>>>>>> el0t_64_sync+0x1a8/0x1ac
>>>>>>
>>>>>> Cc: stable@xxxxxxxxxxxxxxx
>>>>>> Signed-off-by: Selvarasu Ganesan <selvarasu.g@xxxxxxxxxxx>
>>>>>> ---
>>>>>>
>>>>>> Note: No Fixes tag is added because this is a workaround for the
>>>>>> gadget framework issue where the gadget framework calls
>>>>>> usb_ep_disable()
>>>>>> in interrupt context without ensuring endpoint flushing completes.
>>>>>> A proper fix requires refactoring the framework to make sure
>>>>>> usb_ep_disable is invoked in process context.
>>>>>>
>>>>>> Changes in v3:
>>>>>> - Revised the commit message to detail the real gadget
>>>>>> framework issue
>>>>>> pointed out by the reviewer.
>>>>>> - Merged the two fixes for the same ep wringing into one patch.
>>>>>> Link to v2:
>>>>>> https://protect2.fireeye.com/v1/url?k=5535d0f4-344e7a7d-55345bbb-74fe48600034-c617d7b77912682d&q=1&e=2a6508e6-6363-4d6e-b6ab-ce6c77832c8d&u=https%3A%2F%2Flore.kernel.org%2Flinux-usb%2F20251117155920.643-1-selvarasu.g%40samsung.com%2F
>>>>>>
>>>>>> Changes in v2:
>>>>>> - Removed change-id.
>>>>>> - Updated commit message.
>>>>>> Link to v1:
>>>>>> https://protect2.fireeye.com/v1/url?k=c8daed0d-a9a14784-c8db6642-74fe48600034-8488506d5854e40d&q=1&e=2a6508e6-6363-4d6e-b6ab-ce6c77832c8d&u=https%3A%2F%2Flore.kernel.org%2Flinux-usb%2F20251117152812.622-1-selvarasu.g%40samsung.com%2F
>>>>>> ---
>>>>>> drivers/usb/dwc3/gadget.c | 22 ++++++++++++++++++++--
>>>>>> 1 file changed, 20 insertions(+), 2 deletions(-)
>>>>>>
>>>>>> diff --git a/drivers/usb/dwc3/gadget.c b/drivers/usb/dwc3/gadget.c
>>>>>> index 0a688904ce8c5..3af1bbfe3d92b 100644
>>>>>> --- a/drivers/usb/dwc3/gadget.c
>>>>>> +++ b/drivers/usb/dwc3/gadget.c
>>>>>> @@ -971,8 +971,9 @@ static int __dwc3_gadget_ep_enable(struct
>>>>>> dwc3_ep *dep, unsigned int action)
>>>>>> * Issue StartTransfer here with no-op TRB so we can
>>>>>> always rely on No
>>>>>> * Response Update Transfer command.
>>>>>> */
>>>>>> - if (usb_endpoint_xfer_bulk(desc) ||
>>>>>> - usb_endpoint_xfer_int(desc)) {
>>>>>> + if ((usb_endpoint_xfer_bulk(desc) ||
>>>>>> + usb_endpoint_xfer_int(desc)) &&
>>>>>> + !(dep->flags & DWC3_EP_TRANSFER_STARTED)) {
>>>>>> struct dwc3_gadget_ep_cmd_params params;
>>>>>> struct dwc3_trb *trb;
>>>>>> dma_addr_t trb_dma;
>>>>>> @@ -1096,6 +1097,23 @@ static int __dwc3_gadget_ep_disable(struct
>>>>>> dwc3_ep *dep)
>>>>>> */
>>>>>> if (dep->flags & DWC3_EP_DELAY_STOP)
>>>>>> mask |= (DWC3_EP_DELAY_STOP | DWC3_EP_TRANSFER_STARTED);
>>>>>> +
>>>>>> + /*
>>>>>> + * When dwc3_gadget_ep_disable() calls dwc3_gadget_giveback(),
>>>>>> + * the dwc->lock is temporarily released. If
>>>>>> dwc3_gadget_ep_queue()
>>>>>> + * runs in that window it may set the
>>>>>> DWC3_EP_TRANSFER_STARTED flag as
>>>>>> + * part of dwc3_send_gadget_ep_cmd. The original code
>>>>>> cleared the flag
>>>>>> + * unconditionally in the mask operation, which could
>>>>>> overwrite the
>>>>>> + * concurrent modification.
>>>>>> + *
>>>>>> + * As a workaround for the interrupt context constraint
>>>>>> where we cannot
>>>>>> + * wait for endpoint flushing, preserve the
>>>>>> DWC3_EP_TRANSFER_STARTED
>>>>>> + * flag if it is set, avoiding resource conflicts until the
>>>>>> framework
>>>>>> + * is fixed to properly synchronize endpoint lifecycle
>>>>>> management.
>>>>>> + */
>>>>>> + if (dep->flags & DWC3_EP_TRANSFER_STARTED)
>>>>>> + mask |= DWC3_EP_TRANSFER_STARTED;
>>>>>> +
>>>>>> dep->flags &= mask;
>>>>>> /* Clear out the ep descriptors for non-ep0 */
>>>>>> --
>>>>>> 2.34.1
>>>>>>
>>>>> Acked-by: Thinh Nguyen <Thinh.Nguyen@xxxxxxxxxxxx>
>>>>>
>>>> Oh wait, don't pick this patch up yet.
>>>>
>>>> This will cause a regression for UAS device. When switching
>>>> alt-setting
>>>> interface for BOT to UASP, the device needs to issue a Start Transfer
>>>> command.
>>>>
>>>> This workaround won't work. Can we fix the usb_ep_disable() interface
>>>> and rework this instead?
>>>>
>>>> BR,
>>>> Thinh
>>> Hi Thinh,
>>>
>>> We’re trying to see how this change could cause a regression for UAS
>>> devices.
>>> Could you explain why the workaround might be a problem for UAS? Are
>>> you
>>> concerned that it could miss a valid StartTransfer when a previous
>>> transfer finishes later than expected as part of ep_disable?
>> In UAS, the device controller uses the first PRIME to synchronize with
>> the host to determine whether the Start Transfer command can initiate
>> the stream. After configuring an endpoint, if we issue the StartTransfer
>> command too late, then the device controller may not initiate the
>> transfer (sending ERDY), and host will not know when to start the
>> transfer.
>
>
> HI Thinh,
>
> Sorry for the delayed response. We're seeing this issue a lot in
> Exynos platform, so we want to get it fixed.
>
> Thanks for the explanation about UASP in last comment.
>
> We agree that for UASP, the new StartTransfer sent in ep_enable() is
> what makes the controller send ERDY for the host's first PRIME. The
> old patch skipped that StartTransfer that was wrong. If it is skipped
> and no request is queued right away, the controller never sends ERDY
> for the first PRIME and the UAS device possible hangs as you mentioned.
>
> The new patch is simpler, we won't skip it and just trigger it once
> the inflight end transfer is done.
>
> Please see the proposed sequence,
>
> 1. usb_ep_disable() --> EndTransfer deferred due to pending control
> transfer data/status stages (DWC3_EP_DELAY_STOP), old transfer still
> active in HW.
> 2. usb_ep_enable() on the same endpoint --> instead of issuing the
> new StartTransfer, set DWC3_EP_PENDING_START_TRANSFER (New flag) as
> below,
>
> /* __dwc3_gadget_ep_enable() */
> if (dep->flags & (DWC3_EP_DELAY_STOP |
> DWC3_EP_END_TRANSFER_PENDING |
> DWC3_EP_TRANSFER_STARTED))
> dep->flags |= DWC3_EP_PENDING_START_TRANSFER;
> else
> ret = dwc3_gadget_ep_start_noop_transfer(dep); /* unchanged
> path */
>
> 3. Control request finishes --> next SETUP --> existing delayed stop
> retry in dwc3_ep0_out_start() sends the EndTransfer for all delayed
> stop endpoints.
> 4. EndTransfer completion and trigger a previously skipped
> StartTransfer in ep_enable by checking DWC3_EP_PENDING_START_TRANSFER.
>
> /* dwc3_gadget_endpoint_command_complete() */
> if (dep->flags & DWC3_EP_PENDING_START_TRANSFER) {
> dep->flags &= ~DWC3_EP_PENDING_START_TRANSFER;
> dwc3_gadget_ep_start_noop_transfer(dep);
> }
>
> So the endpoint gets the same start/stop sequence you described, only
> sent later, it goes out as soon as the EndTransfer finishes. So this
> is high possible of happens before the host's first PRIME arrives.
> ERDY is still sent, no UAS regression.
>
> The below testing log is for the reference. You can see the timestamp
> where pending start transfer triggered.
>
> [ 3273.597476] Entry __dwc3_gadget_ep_disable
> [ 3273.597481] dwc3_remove_requests ep1out skip stop transfer due to
> DWC3_EP_DELAY_STOP
> [ 3273.597510] Entry __dwc3_gadget_ep_enable 1045 dep->name =ep1out
> dep->flags =e009
> [ 3273.597590] dwc3_gadget_endpoint_command_complete 3857 dep->name
> =ep1out dep->flags =4009 --> Triggered skipped Start transfer when
> endpoint command completion is done.
>
>
> Thanks,
> Selva
>
The below patch is for this proposed sequence,
diff --git a/drivers/usb/dwc3/gadget.c b/drivers/usb/dwc3/gadget.c
index fa0f16ffafef..40fe930a6c2c 100644
--- a/drivers/usb/dwc3/gadget.c
+++ b/drivers/usb/dwc3/gadget.c
@@ -906,6 +906,72 @@ static int dwc3_gadget_resize_tx_fifos(struct
dwc3_ep *dep)
return 0;
}
+/**
+ * dwc3_gadget_ep_start_noop_transfer - start a no-op transfer on the
endpoint
+ * @dep: bulk or interrupt endpoint
+ *
+ * Issue StartTransfer here with no-op TRB so we can always rely on No
+ * Response Update Transfer command.
+ *
+ * Caller should take care of locking. The endpoint must be enabled and
must
+ * not have an active transfer.
+ */
+static int dwc3_gadget_ep_start_noop_transfer(struct dwc3_ep *dep)
+{
+ struct dwc3_gadget_ep_cmd_params params;
+ struct dwc3_trb *trb;
+ struct dwc3 *dwc = dep->dwc;
+ dma_addr_t trb_dma;
+ u32 cmd;
+ int ret;
+
+ memset(¶ms, 0, sizeof(params));
+ trb = &dep->trb_pool[0];
+ trb_dma = dwc3_trb_dma_offset(dep, trb);
+
+ params.param0 = upper_32_bits(trb_dma);
+ params.param1 = lower_32_bits(trb_dma);
+
+ cmd = DWC3_DEPCMD_STARTTRANSFER;
+
+ ret = dwc3_send_gadget_ep_cmd(dep, cmd, ¶ms);
+ if (ret < 0)
+ return ret;
+
+ if (dep->stream_capable) {
+ /*
+ * For streams, at start, there maybe a race where the
+ * host primes the endpoint before the function driver
+ * queues a request to initiate a stream. In that case,
+ * the controller will not see the prime to generate the
+ * ERDY and start stream. To workaround this, issue a
+ * no-op TRB as normal, but end it immediately. As a
+ * result, when the function driver queues the request,
+ * the next START_TRANSFER command will cause the
+ * controller to generate an ERDY to initiate the
+ * stream.
+ */
+ dwc3_stop_active_transfer(dep, true, true);
+
+ /*
+ * All stream eps will reinitiate stream on NoStream
+ * rejection until we can determine that the host can
+ * prime after the first transfer.
+ *
+ * However, if the controller is capable of
+ * TXF_FLUSH_BYPASS, then IN direction endpoints will
+ * automatically restart the stream without the driver
+ * initiation.
+ */
+ if (!dep->direction ||
+ !(dwc->hwparams.hwparams9 &
+ DWC3_GHWPARAMS9_DEV_TXF_FLUSH_BYPASS))
+ dep->flags |= DWC3_EP_FORCE_RESTART_STREAM;
+ }
+
+ return 0;
+}
+
/**
* __dwc3_gadget_ep_enable - initializes a hw endpoint
* @dep: endpoint to be initialized
@@ -973,52 +1039,26 @@ static int __dwc3_gadget_ep_enable(struct dwc3_ep
*dep, unsigned int action)
*/
if (usb_endpoint_xfer_bulk(desc) ||
usb_endpoint_xfer_int(desc)) {
- struct dwc3_gadget_ep_cmd_params params;
- struct dwc3_trb *trb;
- dma_addr_t trb_dma;
- u32 cmd;
-
- memset(¶ms, 0, sizeof(params));
- trb = &dep->trb_pool[0];
- trb_dma = dwc3_trb_dma_offset(dep, trb);
-
- params.param0 = upper_32_bits(trb_dma);
- params.param1 = lower_32_bits(trb_dma);
-
- cmd = DWC3_DEPCMD_STARTTRANSFER;
-
- ret = dwc3_send_gadget_ep_cmd(dep, cmd, ¶ms);
- if (ret < 0)
- return ret;
-
- if (dep->stream_capable) {
+ if (dep->flags & (DWC3_EP_DELAY_STOP |
+ DWC3_EP_END_TRANSFER_PENDING |
+ DWC3_EP_TRANSFER_STARTED)) {
/*
- * For streams, at start, there maybe a race
where the
- * host primes the endpoint before the function
driver
- * queues a request to initiate a stream. In
that case,
- * the controller will not see the prime to
generate the
- * ERDY and start stream. To workaround this,
issue a
- * no-op TRB as normal, but end it immediately. As a
- * result, when the function driver queues the
request,
- * the next START_TRANSFER command will cause the
- * controller to generate an ERDY to initiate the
- * stream.
- */
- dwc3_stop_active_transfer(dep, true, true);
-
- /*
- * All stream eps will reinitiate stream on NoStream
- * rejection.
- *
- * However, if the controller is capable of
- * TXF_FLUSH_BYPASS, then IN direction endpoints
will
- * automatically restart the stream without the
driver
- * initiation.
+ * The previous transfer has not been ended in
+ * hardware yet: the prior usb_ep_disable() left a
+ * deferred (DWC3_EP_DELAY_STOP) or in-flight
+ * (DWC3_EP_END_TRANSFER_PENDING) EndTransfer
+ * command. Issuing StartTransfer now would be
+ * rejected with "No resource" because the endpoint
+ * still has an active transfer. Defer the no-op
+ * StartTransfer; it will be replayed from
+ * dwc3_gadget_endpoint_command_complete() once the
+ * EndTransfer has completed.
*/
- if (!dep->direction ||
- !(dwc->hwparams.hwparams9 &
- DWC3_GHWPARAMS9_DEV_TXF_FLUSH_BYPASS))
- dep->flags |= DWC3_EP_FORCE_RESTART_STREAM;
+ dep->flags |= DWC3_EP_PENDING_START_TRANSFER;
+ } else {
+ ret = dwc3_gadget_ep_start_noop_transfer(dep);
+ if (ret < 0)
+ return ret;
}
}
@@ -1096,6 +1136,23 @@ static int __dwc3_gadget_ep_disable(struct
dwc3_ep *dep)
*/
if (dep->flags & DWC3_EP_DELAY_STOP)
mask |= (DWC3_EP_DELAY_STOP | DWC3_EP_TRANSFER_STARTED);
+
+ /*
+ * When dwc3_gadget_ep_disable() calls dwc3_gadget_giveback(),
+ * the dwc->lock is temporarily released. If dwc3_gadget_ep_queue()
+ * runs in that window it may set the DWC3_EP_TRANSFER_STARTED
flag as
+ * part of dwc3_send_gadget_ep_cmd. The original code cleared
the flag
+ * unconditionally in the mask operation, which could overwrite the
+ * concurrent modification.
+ *
+ * As a workaround for the interrupt context constraint where we
cannot
+ * wait for endpoint flushing, preserve the DWC3_EP_TRANSFER_STARTED
+ * flag if it is set, avoiding resource conflicts until the
framework
+ * is fixed to properly synchronize endpoint lifecycle management.
+ */
+ if (dep->flags & DWC3_EP_TRANSFER_STARTED)
+ mask |= DWC3_EP_TRANSFER_STARTED;
+
dep->flags &= mask;
/* Clear out the ep descriptors for non-ep0 */
@@ -3861,6 +3918,22 @@ static void
dwc3_gadget_endpoint_command_complete(struct dwc3_ep *dep,
dwc3_ep0_send_delayed_status(dwc);
}
+ /*
+ * __dwc3_gadget_ep_enable() deferred the no-op StartTransfer while
+ * this EndTransfer was pending (the endpoint was re-enabled before
+ * the deferred EndTransfer of a prior usb_ep_disable() completed).
+ * Now that the previous transfer has been ended in hardware, issue
+ * the no-op StartTransfer to (re)arm the endpoint, mirroring the
+ * original usb_ep_enable() path. If requests were queued in the
+ * meantime, the delayed-start kick below issues an UpdateTransfer
+ * with the first request's TRB, exactly like the normal
+ * enable-then-queue sequence.
+ */
+ if (dep->flags & DWC3_EP_PENDING_START_TRANSFER) {
+ dep->flags &= ~DWC3_EP_PENDING_START_TRANSFER;
+ dwc3_gadget_ep_start_noop_transfer(dep);
+ }
+
if ((dep->flags & DWC3_EP_DELAY_START) &&
!usb_endpoint_xfer_isoc(dep->endpoint.desc))
__dwc3_gadget_kick_transfer(dep);
Thanks,
Selva
>> So we have this workaround that we would have the device Start and Stop
>> the endpoint immediately just to arm the endpoint for UASP transfers. In
>> the newer IPs, this workaround may not be needed.
>>
>>> If we don’t use this temporary fix, the driver can still report “EP
>>> resource busy” when an earlier StartTransfer hasn’t finished
>>> before ep_disable returns. That can happen when a UAS device needs to
>>> start a new transfer during ep_enable while the prior transfer is still
>>> pending.
>>>
>>> The patch simply blocks a second StartTransfer when the same endpoint
>>> already has a transfer in progress to prevent a “EP resource busy”
>>> issue.
>>>
>>> And it can cause a new StartTransfer to be issued later from ep_queue
>>> while the starttransfer that should have been started during ep_enable
>>> is skipped.
>>>
>> Also, if the gadget driver just uses usb_ep_disable() to handle the
>> teardown instead of proactively dequeuing all the active requests, then
>> the DWC3_EP_TRANSFER_STARTED flag will still be cleared immediately on
>> usb_ep_disable() and we will still run into this issue again.
>>
>> Another workaround is to have the dwc3 driver retry the command after a
>> small delay if there's no resource error report. Only do dev_WARN after
>> a few times of the same failure.
>>
>> Ideally, we should fix the usb_ep_disable() and have the composite
>> framework properly handle the "wait" for completion.
>>
>> BR,
>> Thinh