All of lore.kernel.org
 help / color / mirror / Atom feed
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

  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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.