[RFC PATCH] xhci: fix reporting of 0-sized URBs in control endpoints

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



When a control transfer has a short data stage, the xHCI controller generates
two transfer events: a COMP_SHORT_TX event that specifies the untransferred
amount, and a COMP_SUCCESS event. But when the data stage is not short, only
the COMP_SUCCESS event occurs. Therefore, xhci-hcd sets urb->actual_length
to urb->transfer_buffer_length while processing the COMP_SUCCESS event,
unless urb->actual_length was set already by a previous COMP_SHORT_TX event.

The driver checks this by seeing whether urb->actual_length == 0, but this
alone is the wrong test, as it is entirely possible for a short transfer to
have an urb->actual_length = 0.

This patch changes the xhci driver to set the urb->actual_length in advance
to the expected value of a successful control transfer.
The urb->actual_length is then only adjusted in case of short transfers or
other special events, but not on COMP_SUCCESS events.

This fixes a bug which affected the HSO plugin, which relies on URBs with
urb->actual_length == 0 to halt re-submitting the RX URB in the control
endpoint.

Signed-off-by: Mathias Nyman <mathias.nyman@xxxxxxxxxxxxxxx>
---
 drivers/usb/host/xhci-ring.c | 73 ++++++++++++++++++++++----------------------
 1 file changed, 37 insertions(+), 36 deletions(-)

diff --git a/drivers/usb/host/xhci-ring.c b/drivers/usb/host/xhci-ring.c
index b46b5b9..0e02e79 100644
--- a/drivers/usb/host/xhci-ring.c
+++ b/drivers/usb/host/xhci-ring.c
@@ -732,7 +732,11 @@ remove_finished_td:
 		/* Clean up the cancelled URB */
 		/* Doesn't matter what we pass for status, since the core will
 		 * just overwrite it (because the URB has been unlinked).
+		 * Control urbs have the urb->actual_length pre-set, clear it
+		 * as well
 		 */
+		if (usb_endpoint_xfer_control(&cur_td->urb->ep->desc))
+			cur_td->urb->actual_length = 0;
 		xhci_giveback_urb_in_irq(xhci, cur_td, 0);
 
 		/* Stop processing the cancelled list if the watchdog timer is
@@ -755,6 +759,7 @@ static void xhci_kill_ring_urbs(struct xhci_hcd *xhci, struct xhci_ring *ring)
 		list_del_init(&cur_td->td_list);
 		if (!list_empty(&cur_td->cancelled_td_list))
 			list_del_init(&cur_td->cancelled_td_list);
+		cur_td->urb->actual_length = 0;
 		xhci_giveback_urb_in_irq(xhci, cur_td, -ESHUTDOWN);
 	}
 }
@@ -792,6 +797,7 @@ static void xhci_kill_endpoint_urbs(struct xhci_hcd *xhci,
 		cur_td = list_first_entry(&ep->cancelled_td_list,
 				struct xhci_td, cancelled_td_list);
 		list_del_init(&cur_td->cancelled_td_list);
+		cur_td->urb->actual_length = 0;
 		xhci_giveback_urb_in_irq(xhci, cur_td, -ESHUTDOWN);
 	}
 }
@@ -1888,6 +1894,7 @@ static int process_ctrl_td(struct xhci_hcd *xhci, struct xhci_td *td,
 	int ep_index;
 	struct xhci_ep_ctx *ep_ctx;
 	u32 trb_comp_code;
+	bool force_finish_td = false;
 
 	slot_id = TRB_TO_SLOT_ID(le32_to_cpu(event->flags));
 	xdev = xhci->devs[slot_id];
@@ -1906,7 +1913,8 @@ static int process_ctrl_td(struct xhci_hcd *xhci, struct xhci_td *td,
 			xhci_warn(xhci, "WARN: Success on ctrl data TRB "
 					"without IOC set??\n");
 			*status = -ESHUTDOWN;
-		} else {
+		} else if (*status == -EINPROGRESS) {
+			/* only set to 0 if no previous event set it earlier */
 			*status = 0;
 		}
 		break;
@@ -1918,6 +1926,8 @@ static int process_ctrl_td(struct xhci_hcd *xhci, struct xhci_td *td,
 		break;
 	case COMP_STOP_INVAL:
 	case COMP_STOP:
+		/* we don't continue stopped TDs, so length can be set to 0 */
+		td->urb->actual_length = 0;
 		return finish_td(xhci, td, event_trb, event, ep, status, false);
 	default:
 		if (!xhci_requires_manual_halt_cleanup(xhci,
@@ -1928,44 +1938,26 @@ static int process_ctrl_td(struct xhci_hcd *xhci, struct xhci_td *td,
 				trb_comp_code, ep_index);
 		/* else fall through */
 	case COMP_STALL:
-		/* Did we transfer part of the data (middle) phase? */
-		if (event_trb != ep_ring->dequeue &&
-				event_trb != td->last_trb)
-			td->urb->actual_length =
-				td->urb->transfer_buffer_length -
-				EVENT_TRB_LEN(le32_to_cpu(event->transfer_len));
-		else
-			td->urb->actual_length = 0;
-
-		return finish_td(xhci, td, event_trb, event, ep, status, false);
+		/* length will be set later below if we stall on data stage */
+		td->urb->actual_length = 0;
+		force_finish_td = true;
+		break;
 	}
+
+	/* If in setup stage, nothing transferred */
+	if (event_trb == ep_ring->dequeue)
+		td->urb->actual_length = 0;
+
 	/*
-	 * Did we transfer any data, despite the errors that might have
-	 * happened?  I.e. did we get past the setup stage?
+	 * In data stage, check if we transferred any data despite the possible
+	 * errors that might have happened. Give back the urb if stalled,
+	 * otherwise wait for the status stage event.
 	 */
-	if (event_trb != ep_ring->dequeue) {
-		/* The event was for the status stage */
-		if (event_trb == td->last_trb) {
-			if (td->urb->actual_length != 0) {
-				/* Don't overwrite a previously set error code
-				 */
-				if ((*status == -EINPROGRESS || *status == 0) &&
-						(td->urb->transfer_flags
-						 & URB_SHORT_NOT_OK))
-					/* Did we already see a short data
-					 * stage? */
-					*status = -EREMOTEIO;
-			} else {
-				td->urb->actual_length =
-					td->urb->transfer_buffer_length;
-			}
-		} else {
-		/* Maybe the event was for the data stage? */
-			td->urb->actual_length =
-				td->urb->transfer_buffer_length -
-				EVENT_TRB_LEN(le32_to_cpu(event->transfer_len));
-			xhci_dbg(xhci, "Waiting for status "
-					"stage event\n");
+	if (event_trb != td->last_trb) {
+		td->urb->actual_length = td->urb->transfer_buffer_length -
+			EVENT_TRB_LEN(le32_to_cpu(event->transfer_len));
+		if (!force_finish_td) {
+			xhci_dbg(xhci, "Waiting for status stage event\n");
 			return 0;
 		}
 	}
@@ -3346,6 +3338,15 @@ int xhci_queue_ctrl_tx(struct xhci_hcd *xhci, gfp_t mem_flags,
 	 */
 	if (urb->transfer_buffer_length > 0)
 		num_trbs++;
+
+	/*
+	 * We want to set the urb->actual_length in advance and change it in
+	 * case of error or short transfer. A SHORT_TX event may be followed
+	 * by a SUCCESS event for that same TD, messing up the transfer length.
+	 * So we can't touch urb->actual_length later on successful transfers
+	 */
+	urb->actual_length = urb->transfer_buffer_length;
+
 	ret = prepare_transfer(xhci, xhci->devs[slot_id],
 			ep_index, urb->stream_id,
 			num_trbs, urb, 0, mem_flags);
-- 
1.8.3.2

--
To unsubscribe from this list: send the line "unsubscribe linux-usb" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html




[Index of Archives]     [Linux Media]     [Linux Input]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]     [Old Linux USB Devel Archive]

  Powered by Linux