From: sashiko-bot@kernel.org
To: "Petar Stepanovic" <pstepanovic@axiado.com>
Cc: linux-usb@vger.kernel.org, devicetree@vger.kernel.org,
robh@kernel.org, conor+dt@kernel.org
Subject: Re: [PATCH v3 5/6] usb: gadget: udc: crg: Add gadget and endpoint operations
Date: Tue, 20 Oct 2026 10:01:27 +0000 [thread overview]
Message-ID: <sashiko-outbox-178786@kernel.org> (raw)
In-Reply-To: <20261020-axiado-ax3000-usb-device-controller-v3-5-3012ecfc0a93@axiado.com>
Thank you for your contribution! Sashiko AI review found 12 potential issue(s) to consider:
- [Critical] usb: gadget: crg: TRB interleaving, premature ZLP, and stall on multi-TD rings
- [Critical] usb: gadget: crg: dangling ring pointers and unlocked free in crg_udc_ep_enable()
- [Critical] usb: gadget: crg: race and missing async_callbacks check in crg_udc_reset_work()
- [High] usb: gadget: crg: broken last_trb calculation in crg_udc_queue_trbs()
- [High] usb: gadget: crg: broken CMPL_CODE_SHORT_PKT handling in crg_udc_handle_xfer_event()
- [High] usb: gadget: crg: wrong dequeue cycle state and stalled ring on dequeue/unhalt
- [High] usb: gadget: crg: missing hardware disable wait and lock-drop race in crg_udc_ep_disable()
- [High] usb: gadget: crg: stale state updates after lock drop in crg_udc_req_done()
- [High] usb: gadget: crg: High-Speed maxburst always 0 in crg_udc_epcx_setup()
- [High] usb: gadget: crg: broken PLC resume, state, and disconnect in crg_udc_handle_port_status()
- [High] usb: gadget: crg: missing state revalidation after lock drop in crg_udc_ep_queue()
- [Medium] usb: gadget: crg: lockless state updates in crg_gadget_stop() and start()
--
commit 61353c2b5c4b6d93cbb68f036c35a32b502efad8
Author: Petar Stepanovic <pstepanovic@axiado.com>
usb: gadget: udc: crg: Add gadget and endpoint operations
This commit implements the USB gadget and endpoint operations, TRB ring
queueing, and event handling for the Axiado AX3000/AX3005 UDC driver.
> diff --git a/drivers/usb/gadget/udc/crg/crg-udc-gadget.c b/drivers/usb/gadget/udc/crg/crg-udc-gadget.c
> new file mode 100644
> index 0000000000000..a330e857a9509
> --- /dev/null
> +++ b/drivers/usb/gadget/udc/crg/crg-udc-gadget.c
> @@ -0,0 +1,2299 @@
[ ... ]
> +static int crg_gadget_start(struct usb_gadget *g,
> + struct usb_gadget_driver *driver)
> +{
> + struct crg_gadget_dev *crg_udc = gadget_to_udc(g);
> + int ret;
> +
> + /* The UDC core calls udc_set_speed() before udc_start(). */
> + ret = crg_udc_program_speed(crg_udc);
> + if (ret)
> + return ret;
> +
> + crg_udc->gadget_driver = driver;
> + crg_udc_set_vbus_detect(crg_udc, true);
> + return 0;
> +}
> +
> +static int crg_gadget_stop(struct usb_gadget *g)
> +{
> + struct crg_gadget_dev *crg_udc;
> + unsigned long flags = 0;
> +
> + crg_udc = gadget_to_udc(g);
> +
> + /*
> + * Clear gadget_driver first so crg_udc_handle_port_status() can no
> + * longer schedule udc_reset_work (it only does so while
> + * gadget_driver is set), then cancel_work_sync() is guaranteed to
> + * be the last word: no new reset notification can be queued after
> + * it returns.
> + */
> + spin_lock_irqsave(&crg_udc->udc_lock, flags);
> + crg_udc->gadget_driver = NULL;
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
> +
> + cancel_work_sync(&crg_udc->udc_reset_work);
> +
> + crg_udc->device_state = USB_STATE_ATTACHED;
> + crg_udc_set_vbus_detect(crg_udc, false);
> +
> + spin_lock_irqsave(&crg_udc->udc_lock, flags);
[Severity: Medium]
Can updating crg_udc->device_state and calling crg_udc_set_vbus_detect()
without holding crg_udc->udc_lock race with the threaded interrupt handler?
Before crg_gadget_stop() runs crg_udc_reset() and
crg_udc_reset_data_struct(), hardware interrupts remain enabled in
uccr->control (crg_gadget_pullup() only clears CRG_U3DC_CTRL_RUN, and sysfs
soft_connect_store() can invoke udc_stop() without synchronize_irq()).
If crg_gadget_handle_interrupt() is concurrently running under udc_lock in
crg_udc_handle_port_status() or crg_udc_handle_event(), setting
crg_udc->device_state = USB_STATE_ATTACHED outside udc_lock can race with the
state checks (such as crg_udc->device_state < USB_STATE_RECONNECTING),
causing crg_udc_handle_port_status() to prematurely call
crg_udc_enable_setup() or crg_udc_handle_event() to drop events. Similarly,
crg_gadget_start() updates config0, crg_udc->gadget_driver, and
CRG_UDC_VENDOR_REG without acquiring crg_udc->udc_lock.
Should these state and register updates in crg_gadget_stop() and
crg_gadget_start() be performed under crg_udc->udc_lock?
[ ... ]
> +static void crg_gadget_async_callbacks(struct usb_gadget *g, bool enable)
> +{
> + struct crg_gadget_dev *crg_udc = gadget_to_udc(g);
> + unsigned long flags;
> +
> + spin_lock_irqsave(&crg_udc->udc_lock, flags);
> + crg_udc->async_callbacks = enable;
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
> +}
[ ... ]
> +static int crg_udc_ep_dequeue(struct usb_ep *_ep, struct usb_request *_req)
> +{
[ ... ]
> + if (crg_udc_is_pointer_less_than(pause_pt, udc_req->first_trb,
> + udc_ep_ptr)) {
> + crg_udc_squeeze_xfer_ring(udc_ep_ptr, udc_req);
> + crg_udc_req_done(udc_ep_ptr, udc_req, -ECONNRESET);
> + } else if (udc_req->last_trb &&
> + crg_udc_is_pointer_less_than(udc_req->last_trb, pause_pt,
> + udc_ep_ptr)) {
> + crg_udc_req_done(udc_ep_ptr, udc_req, -ECONNRESET);
> + } else {
> + struct crg_udc_request *next_req = NULL;
> +
> + udc_req->usb_req.actual =
> + crg_udc_actual_before(udc_ep_ptr, udc_req, pause_pt);
> +
> + if (!list_is_last(&udc_req->queue, &udc_ep_ptr->queue))
> + next_req = list_entry(udc_req->queue.next,
> + struct crg_udc_request, queue);
> +
> + if (next_req && next_req->first_trb)
> + udc_ep_ptr->deq_pt = next_req->first_trb;
> + else
> + udc_ep_ptr->deq_pt = udc_ep_ptr->enq_pt;
> +
> + if (crg_udc_epcx_update_dqptr(udc_ep_ptr)) {
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
> + return -EIO;
> + }
> + crg_udc_req_done(udc_ep_ptr, udc_req, -ECONNRESET);
> + }
> +
> + if (old_ep_state == EP_STATE_RUNNING) {
> + udc_ep_ptr->ep_state = EP_STATE_RUNNING;
> + crg_udc_knock_doorbell(crg_udc, dci);
> + }
> +
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
> + return 0;
> +}
[Severity: High]
When the active request is dequeued in the else branch above, advancing
udc_ep_ptr->deq_pt frees space on the transfer ring, but tran_ring_full is
never cleared and crg_udc_queue_pending_trbs() is not called. If
tran_ring_full was true and next_req has first_trb == NULL, won't setting
deq_pt = enq_pt while leaving tran_ring_full true permanently stall the
endpoint queue?
In addition, crg_udc_req_done() drops crg_udc->udc_lock while
udc_ep_ptr->ep_state is EP_STATE_STOPPED. When it returns, checking the saved
old_ep_state == EP_STATE_RUNNING unconditionally sets udc_ep_ptr->ep_state =
EP_STATE_RUNNING and rings the doorbell. If the endpoint was disabled or
halted while udc_lock was dropped during the completion callback, won't this
clobber the new endpoint state?
> +
> +/* endpoint operations */
> +
> +static int crg_udc_ep_disable(struct usb_ep *ep)
> +{
[ ... ]
> + /*
> + * ep_enable is a write-1-to-disable register: endpoints are enabled
> + * via the CONFIG_EP command, but writing a set bit here disables the
> + * corresponding endpoint.
> + */
> + writel(BIT(udc_ep->dci), &uccr->ep_enable);
> +
> + udc_ep->desc = NULL;
> + udc_ep->comp_desc = NULL;
> +
> + /* clean up the request queue */
> + crg_udc_ep_nuke(udc_ep, -ESHUTDOWN);
> +
> + /* decrement ep counters */
> + crg_udc->num_enabled_eps--;
> + /* clean up the endpoint context */
> + memset(p_ep_cx, 0, sizeof(struct crg_ep_cx));
> +
> + /* If device state was changed to default by port
> + * reset, should not overwrite it again
> + */
> + if (crg_udc->num_enabled_eps == 0 &&
> + crg_udc->device_state == USB_STATE_CONFIGURED) {
> + crg_udc->device_state = USB_STATE_ADDRESS;
> + }
> +
> + udc_ep->ep_state = EP_STATE_DISABLED;
> +
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
> +
> + return 0;
> +}
[Severity: High]
Does crg_udc_ep_disable() need to wait for the hardware to clear
BIT(udc_ep->dci) in uccr->ep_enable (similar to the readl_poll_timeout_atomic
in crg_udc_reinit()) before unmapping DMA buffers in crg_udc_ep_nuke() and
zeroing p_ep_cx?
Also, crg_udc_ep_nuke() calls crg_udc_req_done(), which drops and re-acquires
crg_udc->udc_lock around each request's completion callback. Because
udc_ep->ep_state, crg_udc->num_enabled_eps--, and memset(p_ep_cx, 0, ...) are
only updated after crg_udc_ep_nuke() returns:
- If a bus reset runs crg_udc_reinit() while udc_lock is dropped,
crg_udc->num_enabled_eps is reset to 0, and crg_udc_ep_disable() will then
underflow num_enabled_eps to 0xffffffff.
- Because udc_ep->ep_state is still EP_STATE_RUNNING during the callback, a
concurrent re-enable or disable can race with the post-nuke memset(p_ep_cx)
and udc_ep->ep_state = EP_STATE_DISABLED assignment.
Should the hardware disable poll, endpoint context cleanup, num_enabled_eps
decrement, and EP_STATE_DISABLED transition happen before calling
crg_udc_ep_nuke()?
> +
> +static int crg_udc_ep_enable(struct usb_ep *ep,
> + const struct usb_endpoint_descriptor *desc)
> +{
[ ... ]
> + if (udc_ep->dci) {
> + u32 ring_size = 0;
> + size_t len;
> +
> + if (usb_endpoint_xfer_bulk(desc))
> + ring_size = CRG_UDC_BULK_EP_TD_RING_SIZE;
> + else if (usb_endpoint_xfer_isoc(desc))
> + ring_size = CRG_UDC_ISOC_EP_TD_RING_SIZE;
> + else if (usb_endpoint_xfer_int(desc))
> + ring_size = CRG_UDC_INT_EP_TD_RING_SIZE;
> +
> + if (!ring_size) {
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
> + return -EINVAL;
> + }
> + len = ring_size * sizeof(struct crg_transfer_trb);
> +
> + if (udc_ep->tran_ring_info.cpu_addr &&
> + udc_ep->tran_ring_info.len != len) {
> + void *old_vaddr = udc_ep->tran_ring_info.cpu_addr;
> + dma_addr_t old_dma = udc_ep->tran_ring_info.dma_addr;
> + size_t old_len = udc_ep->tran_ring_info.len;
> +
> + udc_ep->tran_ring_info.cpu_addr = NULL;
> + udc_ep->enq_pt = NULL;
> + udc_ep->deq_pt = NULL;
> +
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
> + dma_free_coherent(crg_udc->dev, old_len, old_vaddr,
> + old_dma);
> + spin_lock_irqsave(&crg_udc->udc_lock, flags);
> +
> + if (udc_ep->desc != desc ||
> + udc_ep->ep_state != EP_STATE_DISABLED) {
> + spin_unlock_irqrestore(&crg_udc->udc_lock,
> + flags);
> + return -EAGAIN;
> + }
> + }
> +
> + /* setup transfer ring */
> + if (!udc_ep->tran_ring_info.cpu_addr) {
> + dma_addr_t dma;
> + void *vaddr;
> +
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
> + vaddr = dma_alloc_coherent(crg_udc->dev, len, &dma,
> + GFP_KERNEL);
> + if (!vaddr)
> + return -ENOMEM;
> + spin_lock_irqsave(&crg_udc->udc_lock, flags);
> + if (udc_ep->desc != desc ||
> + udc_ep->ep_state != EP_STATE_DISABLED) {
> + spin_unlock_irqrestore(&crg_udc->udc_lock,
> + flags);
> + dma_free_coherent(crg_udc->dev, len, vaddr,
> + dma);
> + return -EAGAIN;
> + }
> + udc_ep->tran_ring_info.cpu_addr = vaddr;
> + udc_ep->tran_ring_info.dma_addr = dma;
> + udc_ep->tran_ring_info.len = len;
> + udc_ep->first_trb = vaddr;
> + udc_ep->last_trb = udc_ep->first_trb + ring_size - 1;
> + }
> + memset(udc_ep->first_trb, 0, udc_ep->tran_ring_info.len);
> + /* Ensure that transfer ring is updated */
> + dma_wmb();
> + crg_udc_setup_link_trb(udc_ep->last_trb, true,
> + udc_ep->tran_ring_info.dma_addr);
> + udc_ep->enq_pt = udc_ep->first_trb;
> + udc_ep->deq_pt = udc_ep->first_trb;
> + udc_ep->pcs = 1;
> + udc_ep->tran_ring_full = false;
> + crg_udc->num_enabled_eps++;
> + status = crg_udc_epcx_setup(udc_ep);
> + if (status)
> + goto err_ep_enable;
> + }
> + param0 = BIT(udc_ep->dci);
> + status = crg_udc_issue_command(crg_udc, CRG_CMD_CONFIG_EP, param0, 0);
> + if (status)
> + goto err_ep_enable;
> + if (crg_udc->device_state == USB_STATE_ADDRESS)
> + crg_udc->device_state = USB_STATE_CONFIGURED;
> + udc_ep->wedge = false;
> + udc_ep->usb_ep.enabled = 1;
> + udc_ep->ep_state = EP_STATE_RUNNING;
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
> + return 0;
> +
> +err_ep_enable:
> + if (udc_ep->dci && crg_udc->num_enabled_eps)
> + crg_udc->num_enabled_eps--;
> + udc_ep->desc = NULL;
> + udc_ep->comp_desc = NULL;
> + udc_ep->ep_state = EP_STATE_DISABLED;
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
> +
> + if (udc_ep->tran_ring_info.cpu_addr) {
> + dma_free_coherent(crg_udc->dev, udc_ep->tran_ring_info.len,
> + udc_ep->tran_ring_info.cpu_addr,
> + udc_ep->tran_ring_info.dma_addr);
> + udc_ep->tran_ring_info.cpu_addr = NULL;
> + udc_ep->tran_ring_info.dma_addr = DMA_MAPPING_ERROR;
> + udc_ep->tran_ring_info.len = 0;
> + udc_ep->first_trb = NULL;
> + udc_ep->last_trb = NULL;
> + }
> + return status;
> +}
[Severity: Critical]
Can the transfer ring resize and error paths in crg_udc_ep_enable() leave
dangling ring pointers or race on udc_ep->tran_ring_info?
1. When resizing an existing ring (udc_ep->tran_ring_info.len != len),
udc_ep->tran_ring_info.cpu_addr, udc_ep->enq_pt, and udc_ep->deq_pt are
set to NULL and old_vaddr is freed, but udc_ep->first_trb and
udc_ep->last_trb still point to old_vaddr. If the subsequent
dma_alloc_coherent() returns -ENOMEM (or the state re-check returns
-EAGAIN, or !ring_size returns -EINVAL), crg_udc_ep_enable() returns
without clearing udc_ep->desc, udc_ep->comp_desc, udc_ep->first_trb, or
udc_ep->last_trb. A subsequent crg_udc_ep_queue() will see non-NULL
udc_ep_ptr->first_trb and udc_ep_ptr->desc and dereference NULL
udc_ep_ptr->enq_pt (or freed first_trb).
2. Under err_ep_enable, spin_unlock_irqrestore(&crg_udc->udc_lock, flags) is
called before checking, freeing, and clearing udc_ep->tran_ring_info and
first_trb/last_trb (and enq_pt/deq_pt are left non-NULL). Once udc_lock is
dropped with udc_ep->ep_state == EP_STATE_DISABLED, can a concurrent
crg_udc_ep_enable() or teardown path race on udc_ep->tran_ring_info.cpu_addr
and double-free or use-after-free the coherent ring buffer?
[ ... ]
> +static int crg_udc_ep_queue(struct usb_ep *_ep, struct usb_request *_req,
> + gfp_t gfp_flags)
> +{
[ ... ]
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
> +
> + crg_udc_clear_req_container(udc_req_ptr);
> + udc_req_ptr->mapped = false;
> +
> + if (_req->length) {
> + udc_req_ptr->map_dir = dma_data_dir;
> + status = usb_gadget_map_request_by_dev(
> + crg_udc->dev, _req, dma_data_dir == DMA_TO_DEVICE);
> + if (status)
> + goto err_unmap;
> +
> + if (is_sg) {
> + if (!_req->num_mapped_sgs ||
> + crg_udc_sg_init(udc_req_ptr)) {
> + status = -EIO;
> + goto err_unmap;
> + }
> + } else if (_req->dma_mapped) {
> + udc_req_ptr->mapped = true;
> + }
> + }
> +
> + spin_lock_irqsave(&crg_udc->udc_lock, flags);
> + if (udc_ep_ptr->desc != desc || !udc_req_ptr->usb_req.complete ||
> + !list_empty(&udc_req_ptr->queue) || crg_udc->resetting) {
> + status = -ESHUTDOWN;
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
> + crg_udc_unmap_request(udc_ep_ptr, udc_req_ptr);
> + return status;
> + }
> +
> + _req->status = -EINPROGRESS;
> + _req->actual = 0;
> +
> + if (udc_ep_ptr->tran_ring_full)
> + status = 0;
> + else
> + status = crg_udc_build_td(udc_ep_ptr, udc_req_ptr);
> +
> + if (!status)
> + list_add_tail(&udc_req_ptr->queue, &udc_ep_ptr->queue);
> + spin_unlock_irqrestore(&crg_udc->udc_lock, flags);
[Severity: High]
When crg_udc_ep_queue() drops crg_udc->udc_lock to map the DMA buffer and then
re-acquires it, is checking udc_ep_ptr->desc != desc sufficient?
For EP0 (dci == 0), udc_ep_ptr->desc always points to &crg_udc->ep0_desc. If
a new SETUP packet arrives while udc_lock is dropped during DMA mapping,
crg_udc_abort_ep0_for_setup() and crg_udc_handle_setup_pkt() can complete and
start a new control transfer, yet udc_ep_ptr->desc != desc will still be
false. Without revalidating ep0_state, setup_status, and setup_tag after
re-acquiring udc_lock, can a stale EP0 request (potentially mapped with the
wrong DMA direction) be queued onto the new control transfer?
Similarly, for non-EP0 endpoints, crg_udc_ep_queue() never checks that
udc_ep_ptr->ep_state == EP_STATE_RUNNING (either before or after dropping
udc_lock), which allows requests to be queued while an endpoint is still
mid-enable in crg_udc_ep_enable() or after it was disabled and re-enabled with
the same descriptor pointer.
[ ... ]
> +static int crg_udc_epcx_setup(struct crg_udc_ep *udc_ep)
> +{
[ ... ]
> + } else if ((crg_udc->gadget.speed == USB_SPEED_HIGH ||
> + crg_udc->gadget.speed == USB_SPEED_FULL) &&
> + (usb_endpoint_xfer_int(udc_ep->desc) ||
> + usb_endpoint_xfer_isoc(udc_ep->desc))) {
> + if (crg_udc->gadget.speed == USB_SPEED_HIGH)
> + maxburst = (usb_endpoint_maxp(desc) >> 11) & 0x3;
> + if (maxburst == 0x3) {
> + dev_err(crg_udc->dev, "invalid maxburst\n");
> + maxburst = 0x2;
> + }
> + }
[Severity: High]
In crg_udc_epcx_setup(), usb_endpoint_maxp(desc) masks wMaxPacketSize with
USB_ENDPOINT_MAXP_MASK (0x07ff), stripping bits [15:11]. Doesn't shifting the
result right by 11 bits always evaluate to 0, leaving High-Speed
high-bandwidth isochronous and interrupt endpoints configured with
maxburst = 0 instead of usb_endpoint_maxp_mult(desc) - 1?
[ ... ]
> +static int crg_udc_epcx_update_dqptr(struct crg_udc_ep *udc_ep)
> +{
> + struct crg_gadget_dev *crg_udc = udc_ep->crg_udc;
> + u8 dci = udc_ep->dci;
> + struct crg_ep_cx *epcx;
> + u32 dw;
> + dma_addr_t dqptaddr;
> + u32 cmd_param0;
> +
> + if (dci < 2 || dci >= CRG_NUM_EP_CX) {
> + dev_err(crg_udc->dev, "invalid DCI %u for dequeue pointer\n", dci);
> + return -EINVAL;
> + }
> +
> + epcx = &crg_udc->p_epcx[dci - 2];
> + dqptaddr = crg_udc_tran_trb_virt_to_dma(udc_ep, udc_ep->deq_pt);
> +
> + if (dqptaddr == DMA_MAPPING_ERROR)
> + return -EINVAL;
> +
> + /* fill ep_dw2 */
> + dw = lower_32_bits(dqptaddr) & EP_CX_TR_DQPT_LO_MASK;
> + FIELD_MODIFY(EP_CX_DEQ_CYC_STATE_MASK, &dw, udc_ep->pcs);
> + epcx->dw2 = cpu_to_le32(dw);
[Severity: High]
Does crg_udc_epcx_update_dqptr() program the wrong Dequeue Cycle State (DCS)
when udc_ep->enq_pt has wrapped past the Link TRB?
udc_ep->pcs tracks the producer cycle state at udc_ep->enq_pt and is toggled
whenever enq_pt wraps to udc_ep->first_trb in crg_udc_queue_trbs(). When
udc_ep->deq_pt != udc_ep->enq_pt and enq_pt has wrapped while deq_pt has not
yet wrapped (deq_pt > enq_pt), the TRBs at udc_ep->deq_pt have cycle bit
udc_ep->pcs ^ 1, not udc_ep->pcs. Programming EP_CX_DEQ_CYC_STATE_MASK with
udc_ep->pcs in crg_udc_ep_dequeue() or crg_udc_ep_halt() will cause the
controller to see a cycle bit mismatch at deq_pt and stall the endpoint ring.
[ ... ]
> +static int crg_udc_queue_trbs(struct crg_udc_ep *udc_ep_ptr,
> + struct crg_udc_request *udc_req_ptr, bool b_isoc,
> + u32 xfer_ring_size, u32 num_trbs_needed,
> + u64 buffer_length)
> +{
> + struct crg_gadget_dev *crg_udc = udc_ep_ptr->crg_udc;
> + struct crg_transfer_trb *p_xfer_ring = udc_ep_ptr->first_trb;
> + struct crg_transfer_trb *enq_pt = udc_ep_ptr->enq_pt;
> + struct usb_request *usb_req = &udc_req_ptr->usb_req;
> + u32 num_trbs_ava, count, i;
> + u32 td_trbs;
> + u32 td_size;
> + u32 j = 1;
> + u32 intr_rate = b_isoc ? CRG_UDC_ISOC_EP_INTERRUPT_RATE :
> + CRG_UDC_BULK_EP_INTERRUPT_RATE;
> + bool full_td = true;
> + bool need_zlp = false;
> + bool is_sg = usb_req->num_mapped_sgs;
> +
> + if (!b_isoc && usb_req->zero && usb_req->length &&
> + !(usb_req->length % udc_ep_ptr->usb_ep.maxpacket))
> + need_zlp = true;
> +
> + if (!num_trbs_needed)
> + return -EINVAL;
> +
> + /* TD_SIZE is five bits and encodes the number of TRBs remaining
> + * in this TD minus one. A request may contain multiple TDs.
> + */
> + td_trbs = min_t(u32, num_trbs_needed, 32);
> +
> + num_trbs_ava = crg_udc_room_on_ring(crg_udc, xfer_ring_size,
> + p_xfer_ring, udc_ep_ptr->enq_pt,
> + udc_ep_ptr->deq_pt);
> +
> + if (num_trbs_ava < td_trbs) {
> + if (b_isoc)
> + return -EBUSY;
> + trace_crg_udc_no_room_on_ring(
> + "deferring TRBs to next doorbell");
> + count = num_trbs_ava;
> + full_td = false;
> + udc_ep_ptr->tran_ring_full = true;
> + need_zlp = false;
> + } else {
> + count = td_trbs;
> + }
[ ... ]
> + if (b_isoc) {
> + crg_udc_setup_trb(
> + enq_pt, trb_len, trb_buf_addr, td_size,
> + i == 0 ? (original_pcs ^ 1) : udc_ep_ptr->pcs,
> + TRB_TYPE_XFER_DATA_ISOCH, short_pkt, chain_bit,
> + ioc, false, 0, true, 0, 1, 0);
> + } else {
> + bool azp = ioc && !chain_bit && need_zlp;
> +
> + crg_udc_setup_trb(
> + enq_pt, trb_len, trb_buf_addr, td_size,
> + i == 0 ? (original_pcs ^ 1) : udc_ep_ptr->pcs,
> + TRB_TYPE_XFER_NORMAL, short_pkt, chain_bit, ioc,
> + false, 0, false, 0, 0, azp);
> + }
[Severity: Critical]
How are requests requiring more than 32 TRBs (or queued behind a full ring)
handled across crg_udc_queue_trbs(), crg_udc_ep_queue(), and
crg_udc_handle_xfer_event()?
1. When num_trbs_needed > 32 and num_trbs_ava >= 32, td_trbs is capped at 32,
so buffer_length is still non-zero at the 32nd TRB (i == count - 1), but
need_zlp is only cleared when num_trbs_ava < td_trbs. Won't
azp = ioc && !chain_bit && need_zlp set TRB_APPEND_ZLP_MASK on the 32nd TRB
in the middle of the transfer, sending a premature zero-length packet
before the remaining TRBs of the request are queued?
2. In that same case (num_trbs_needed > 32 and num_trbs_ava > 32),
crg_udc_queue_trbs() queues the first 32 TRBs and leaves
udc_req_ptr->all_trbs_queued = false and udc_ep_ptr->tran_ring_full =
false. If a second request is then submitted via crg_udc_ep_queue(), it
checks only if (udc_ep_ptr->tran_ring_full) and immediately calls
crg_udc_build_td() for the second request, interleaving the second
request's TRBs onto the hardware ring ahead of the remaining TRBs of the
first request.
3. In crg_udc_handle_xfer_event() at queue_more_trbs, udc_ep_ptr->tran_ring_full
is cleared to false before calling crg_udc_queue_pending_trbs(). Because
crg_udc_queue_pending_trbs() breaks out of its loop and returns 0 after
building a single TD for the first pending request, if that TD does not
fill the ring, tran_ring_full stays false even when later requests in
udc_ep_ptr->queue still have !all_trbs_queued (first_trb == NULL), leaving
them unscheduled on subsequent completions and allowing newly queued
requests in crg_udc_ep_queue() to bypass them.
> +
> + trace_crg_udc_queue_trb(udc_ep_ptr, enq_pt);
> +
> + if (is_sg && !crg_udc_sg_advance(udc_req_ptr, trb_len))
> + return -EINVAL;
> +
> + td_size--;
> + enq_pt++;
> + j++;
> +
> + if (enq_pt == udc_ep_ptr->last_trb) {
> + dma_wmb();
> + le32p_replace_bits(&enq_pt->dw3, udc_ep_ptr->pcs,
> + TRB_CYCLE_BIT_MASK);
> + udc_ep_ptr->pcs ^= 1;
> + enq_pt = udc_ep_ptr->first_trb;
> + }
> + }
> +
> + dma_wmb();
> + le32p_replace_bits(&udc_ep_ptr->enq_pt->dw3, original_pcs,
> + TRB_CYCLE_BIT_MASK);
> +
> + if (!udc_req_ptr->trbs_needed)
> + udc_req_ptr->first_trb = udc_ep_ptr->enq_pt;
> +
> + udc_ep_ptr->enq_pt = enq_pt;
> + if (count == num_trbs_ava)
> + udc_ep_ptr->tran_ring_full = true;
> + udc_req_ptr->buff_len_left = buffer_length;
> + if (buffer_length)
> + udc_req_ptr->trbs_needed = num_trbs_needed - count;
> + else
> + udc_req_ptr->trbs_needed = 0;
> +
> + if (!udc_req_ptr->buff_len_left) {
> + if (udc_ep_ptr->enq_pt == udc_req_ptr->first_trb)
> + udc_req_ptr->last_trb = udc_ep_ptr->last_trb - 1;
> + else
> + udc_req_ptr->last_trb = udc_ep_ptr->enq_pt - 1;
> + }
> +
> + return 0;
> +}
[Severity: High]
There appear to be two issues with how udc_req_ptr->last_trb is computed here:
1. When the final TRB of a request is placed at udc_ep_ptr->last_trb - 1, the
loop increments enq_pt to udc_ep_ptr->last_trb and wraps it to
udc_ep_ptr->first_trb. However, the check at the end compares:
if (udc_ep_ptr->enq_pt == udc_req_ptr->first_trb)
instead of udc_ep_ptr->first_trb (unlike crg_udc_queue_ctrl() in
crg-udc-ep0.c). For any request that did not start at udc_ep_ptr->first_trb,
this condition is false on wrap and udc_req_ptr->last_trb is set to
udc_ep_ptr->first_trb - 1 (pointing out-of-bounds before the start of the
ring buffer).
2. udc_req_ptr->last_trb is only assigned when !udc_req_ptr->buff_len_left.
When a request is split across multiple TDs (because num_trbs_needed > 32
or num_trbs_ava < td_trbs), buff_len_left is non-zero and
udc_req_ptr->last_trb remains NULL while the first TD is in flight. When
the completion event for the last TRB of that TD (at count - 1 > 0, so
trb_pt != udc_req->first_trb) arrives in crg_udc_handle_xfer_event(),
is_request_dequeued() sees udc_req->last_trb == NULL,
crg_udc_is_pointer_less_than(trb_pt, NULL, udc_ep) returns false, and
is_request_dequeued() returns true. crg_udc_handle_xfer_event() then jumps
to queue_more_trbs and skips the !udc_req_ptr->all_trbs_queued continuation
path, leaving the transfer hung.
[ ... ]
> +void crg_udc_reset_work(struct work_struct *work)
> +{
> + struct crg_gadget_dev *crg_udc =
> + container_of(work, struct crg_gadget_dev, udc_reset_work);
> +
> + if (crg_udc->gadget_driver)
> + usb_gadget_udc_reset(&crg_udc->gadget, crg_udc->gadget_driver);
> +}
[Severity: Critical]
Can crg_udc_reset_work() dereference a NULL crg_udc->gadget_driver or call
driver->reset() after usb_gadget_disable_async_callbacks()?
1. crg_udc_reset_work() reads crg_udc->gadget_driver twice without holding
crg_udc->udc_lock. If crg_gadget_stop() sets crg_udc->gadget_driver = NULL
on another CPU right after the if (crg_udc->gadget_driver) check,
usb_gadget_udc_reset() can be passed NULL and dereference driver->reset().
2. During unbind, the UDC core calls usb_gadget_disable_async_callbacks()
(which calls crg_gadget_async_callbacks(g, false)), synchronize_irq(), and
driver->unbind() before calling usb_gadget_udc_stop_locked()
(crg_gadget_stop()). Because crg_udc_handle_port_status() and
crg_udc_reset_work() do not check crg_udc->async_callbacks and
crg_gadget_async_callbacks() does not cancel udc_reset_work, can
udc_reset_work run concurrently with or after driver->unbind()?
> +
> +int crg_udc_handle_port_status(struct crg_gadget_dev *crg_udc,
> + unsigned long *flags)
> +{
[ ... ]
> + /* handle Port Connection Change */
> + tmp = readl(&uccr->portsc);
> + if ((tmp & (CRG_U3DC_PORTSC_CCS | CRG_U3DC_PORTSC_PP)) ==
> + (CRG_U3DC_PORTSC_CCS | CRG_U3DC_PORTSC_PP)) {
> + if (FIELD_GET(CRG_U3DC_PORTSC_PLS_MASK, tmp) != 0 ||
> + (!(tmp & CRG_U3DC_PORTSC_PED)))
> + return 0;
[ ... ]
> + } else if (!(tmp & CRG_U3DC_PORTSC_CCS)) {
> + bool cable_connected;
> + int ccs_drop_ignore = 0;
> +
> + if ((FIELD_GET(CRG_U3DC_PORTSC_PLS_MASK, tmp) == 0x0) &&
> + (FIELD_GET(CRG_U3DC_PORTSC_SPEED_MASK, tmp) <
> + CRG_U3DC_PORTSC_SPEED_SS)) {
> + ccs_drop_ignore = 1;
> + dev_err(crg_udc->dev, "ccs glitch detect on HS/FS!\n");
> + }
> +
> + if (!ccs_drop_ignore)
> + crg_udc->gadget.speed = USB_SPEED_UNKNOWN;
> +
> + cable_connected = crg_udc_vbus_connected(crg_udc);
> + if (cable_connected && !ccs_drop_ignore) {
> + crg_udc->device_state = USB_STATE_POWERED;
> + crg_udc_reinit(crg_udc);
> + crg_udc->connected = 0;
> + } else if (!cable_connected) {
> + dev_dbg(crg_udc->dev,
> + "cable disconnected, rst controller\n");
> + if (was_connected)
> + crg_udc_notify_disconnect(crg_udc, flags);
> + if (crg_udc_reset(crg_udc, flags))
> + dev_err(crg_udc->dev,
> + "controller reset failed on disconnect\n");
> + crg_udc_reset_data_struct(crg_udc);
> + crg_udc->connected = 0;
> + crg_udc_init_ep0(crg_udc);
> +
> + crg_udc->device_state = USB_STATE_ATTACHED;
> + return -ECONNRESET;
> + }
> + }
> + /* handle Port Connection Change end*/
> +
> + if (portsc_val & CRG_U3DC_PORTSC_PLC) {
> + tmp = readl(&uccr->portsc);
> + if (FIELD_GET(CRG_U3DC_PORTSC_PLS_MASK, tmp) == 0xf) {
> + FIELD_MODIFY(CRG_U3DC_PORTSC_PLS_MASK, &tmp, 0x0);
> + writel(tmp, &uccr->portsc);
> + }
> + }
> +
> + return 0;
> +}
[Severity: High]
There seem to be several issues in crg_udc_handle_port_status():
1. The Port Connection Change block runs unconditionally even when
CRG_U3DC_PORTSC_CSC is not set in portsc_val, and returns 0 whenever
(tmp & (CRG_U3DC_PORTSC_CCS | CRG_U3DC_PORTSC_PP)) ==
(CRG_U3DC_PORTSC_CCS | CRG_U3DC_PORTSC_PP) and PLS != 0. When a Port Link
State Change (CRG_U3DC_PORTSC_PLC) event arrives with PLS == 0xf (Resume)
on a connected port, PLS != 0 is true, so the function returns 0 early and
never reaches the if (portsc_val & CRG_U3DC_PORTSC_PLC) block below.
2. In the CRG_U3DC_PORTSC_PLC block, writing PLS = 0x0 to &uccr->portsc does
not set CRG_U3DC_PORTSC_LWS (Link Write Strobe) and does not mask out
CRG_U3DC_PORTSC_W1C_MASK (unlike crg_gadget_wakeup()). Without LWS, will
the hardware ignore the PLS write while clearing any newly asserted W1C
bits in portsc?
3. In the !(tmp & CRG_U3DC_PORTSC_CCS) branch when cable_connected &&
!ccs_drop_ignore is true:
- crg_udc->device_state is set to USB_STATE_POWERED, but the immediate call
to crg_udc_reinit(crg_udc) overwrites crg_udc->device_state with
USB_STATE_RECONNECTING.
- crg_udc->connected is set to 0 without calling
crg_udc_notify_disconnect(). If a cable unplug drops CCS before VBUS
(CRG_U3DC_PORTSC_PP) decays, crg_udc->connected becomes 0 on the first
event, so when VBUS subsequently drops and enters else if
(!cable_connected), was_connected is false and
crg_udc_notify_disconnect() is never called.
> +
> +int crg_udc_handle_xfer_event(struct crg_gadget_dev *crg_udc,
> + struct crg_event_trb *event, unsigned long *flags)
> +{
[ ... ]
> + if (udc_req_ptr->all_trbs_queued) {
> + udc_req_ptr->usb_req.actual =
> + udc_req_ptr->usb_req.length -
> + udc_req_ptr->buff_len_left;
> + residual = FIELD_GET(EVE_TRB_TRAN_LEN_MASK,
> + le32_to_cpu(event->dw2));
> + if (residual > udc_req_ptr->usb_req.actual)
> + residual = 0;
> + udc_req_ptr->usb_req.actual -= residual;
> + crg_udc_req_done(udc_ep_ptr, udc_req_ptr, 0);
> +
> + if (dci == 0 && udc_ep_ptr->desc &&
> + usb_endpoint_xfer_control(udc_ep_ptr->desc))
> + crg_udc_ep0_req_complete(udc_ep_ptr,
> + flags);
[Severity: High]
For a standard USB_REQ_GET_STATUS request on EP0, getstatusrequest() in
crg-udc-ep0.c sets udc_req_ptr->usb_req.complete = get_status_cmpl and
crg_udc->setup_status = DATA_STAGE_XFER.
When the data stage completes here, crg_udc_req_done() runs get_status_cmpl(),
which sets crg_udc->setup_status = STATUS_STAGE_RECV and calls
crg_udc_build_ep0_status() to queue the status stage TRB. Immediately after
crg_udc_req_done() returns, crg_udc_ep0_req_complete() is called; because
setup_status is already STATUS_STAGE_RECV instead of DATA_STAGE_XFER,
crg_udc_ep0_req_complete() hits its default case and resets
crg_udc->setup_status = WAIT_FOR_SETUP while the status stage TRB is still in
flight.
> + } else {
> + int ret;
> +
> + ret = crg_udc_build_td(udc_ep_ptr, udc_req_ptr);
> + if (ret) {
> + udc_req_ptr->usb_req.actual =
> + udc_req_ptr->usb_req.length -
> + udc_req_ptr->buff_len_left;
> + crg_udc_req_done(udc_ep_ptr, udc_req_ptr,
> + -EIO);
> + } else {
> + crg_udc_knock_doorbell(crg_udc, dci);
> + }
> + }
> + }
> + break;
> +
> + case CMPL_CODE_SHORT_PKT:
> + if (list_empty(&udc_ep_ptr->queue))
> + break;
> +
> + udc_req_ptr = list_first_entry(&udc_ep_ptr->queue,
> + struct crg_udc_request, queue);
> + udc_req_ptr->usb_req.actual = udc_req_ptr->usb_req.length -
> + udc_req_ptr->buff_len_left;
> + residual = FIELD_GET(EVE_TRB_TRAN_LEN_MASK,
> + le32_to_cpu(event->dw2));
> + if (residual > udc_req_ptr->usb_req.actual)
> + residual = 0;
> + udc_req_ptr->usb_req.actual -= residual;
> +
> + if (udc_req_ptr->usb_req.short_not_ok &&
> + usb_endpoint_dir_out(udc_ep_ptr->desc))
> + crg_udc_req_done(udc_ep_ptr, udc_req_ptr, -EREMOTEIO);
> + else
> + crg_udc_req_done(udc_ep_ptr, udc_req_ptr, 0);
> +
> + trbs_dequeued = true;
> + crg_udc_advance_dequeue_pt(udc_ep_ptr);
> + break;
[Severity: High]
Can CMPL_CODE_SHORT_PKT cause EP0 control OUT transfers to hang and miscalculate
usb_req.actual on multi-TRB transfers?
1. crg_udc_setup_datastage_trb() sets TRB_INTR_ON_SHORT_PKT_MASK on EP0 data
stage TRBs. If a host sends a short packet during a control OUT data stage
(DATA_STAGE_RECV), completion arrives with CMPL_CODE_SHORT_PKT, which
completes the request via crg_udc_req_done() but never calls
crg_udc_ep0_req_complete(udc_ep_ptr, flags) when dci == 0, so the status
stage TRB is never queued.
2. EVE_TRB_TRAN_LEN_MASK in event->dw2 is the residual byte count of the
single TRB that short-terminated, whereas
udc_req_ptr->usb_req.length - udc_req_ptr->buff_len_left is the total byte
count of all TRBs queued in the TD. If an OUT request spans multiple TRBs
(for example, a scatter-gather list or a buffer crossing a 64 KB boundary)
and short-terminates on an earlier TRB, subtracting only that TRB's
residual counts all subsequent unexecuted TRBs in the TD as received.
3. Calling crg_udc_req_done() before crg_udc_advance_dequeue_pt(udc_ep_ptr)
means udc_lock is dropped for the completion callback while udc_ep_ptr->deq_pt
is still stale; if the callback queues a new request, advancing deq_pt
afterwards can corrupt the dequeue pointer. (A similar ordering issue
exists in crg_udc_ep_halt() and set_ep_halt(), where crg_udc_ep_nuke()
drops udc_lock before udc_ep_ptr->deq_pt = udc_ep_ptr->enq_pt and
udc_ep_ptr->ep_state = EP_STATE_HALTED are set.)
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261020-axiado-ax3000-usb-device-controller-v3-0-3012ecfc0a93@axiado.com?part=5
next prev parent reply other threads:[~2026-10-20 10:01 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-20 7:18 [PATCH v3 0/6] Axiado AX3000 and AX3005 USB Device Controller (UDC) Petar Stepanovic
2026-10-20 7:18 ` [PATCH v3 1/6] dt-bindings: usb: gadget: udc: add Axiado AX3000 and AX3005 UDC Petar Stepanovic
2026-10-20 8:36 ` sashiko-bot
2026-10-20 7:18 ` [PATCH v3 2/6] usb: gadget: udc: crg: Add hardware definitions and structs Petar Stepanovic
2026-10-20 8:37 ` sashiko-bot
2026-10-20 7:18 ` [PATCH v3 3/6] usb: gadget: udc: crg: Add ring and transfer management Petar Stepanovic
2026-10-20 8:58 ` sashiko-bot
2026-10-20 7:18 ` [PATCH v3 4/6] usb: gadget: udc: crg: Add endpoint 0 handling Petar Stepanovic
2026-10-20 10:01 ` sashiko-bot
2026-10-20 7:18 ` [PATCH v3 5/6] usb: gadget: udc: crg: Add gadget and endpoint operations Petar Stepanovic
2026-10-20 10:01 ` sashiko-bot [this message]
2026-10-20 7:19 ` [PATCH v3 6/6] usb: gadget: udc: crg: Add core driver and build integration Petar Stepanovic
2026-10-20 9:52 ` sashiko-bot
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-178786@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=linux-usb@vger.kernel.org \
--cc=pstepanovic@axiado.com \
--cc=robh@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