Hi,

xhci's calculation of 'xfer->actlen' is wrong if the xfer was split into
multiple TRBs. That's because the code just looks at the remainder
reported by the status TRB. However, this remainder only refers to the
total size of this single TRB; not to the total size of the xfer.

Example: assume a 16 kb xfer is split into two TRBs for 8 kb each.
        Then we get a short read of 6 kb. The status TRB will refer
        to the first TRB with remainder set to 2 kb. Currently xhci
        would assume that actlen is 'xfer->length' (16 kb) minus the
        remainder (2 kb). That yields a wrong actlen of 14 kb instead
        of 6 kb.

The patch below fixes this by adding up all the remainders of all the
transfer TRBs up to the current one.

Gerhard


Index: sys/dev/usb/xhci.c
===================================================================
RCS file: /cvs/src/sys/dev/usb/xhci.c,v
retrieving revision 1.106
diff -u -p -u -p -r1.106 xhci.c
--- sys/dev/usb/xhci.c  6 Oct 2019 17:30:00 -0000       1.106
+++ sys/dev/usb/xhci.c  12 Nov 2019 09:29:47 -0000
@@ -810,6 +810,28 @@ xhci_event_xfer(struct xhci_softc *sc, u
        xhci_xfer_done(xfer);
 }
 
+uint32_t
+xhci_xfer_length_generic(struct xhci_xfer *xx, struct xhci_pipe *xp,
+    int trb_idx)
+{
+       int      trb0_idx;
+       uint32_t len = 0;
+
+       KASSERT(xx->index >= 0);
+       trb0_idx =
+           ((xx->index + xp->ring.ntrb) - xx->ntrb) % (xp->ring.ntrb - 1);
+
+       while (1) {
+               len +=
+                   le32toh(XHCI_TRB_LEN(xp->ring.trbs[trb0_idx].trb_status));
+               if (trb0_idx == trb_idx)
+                       break;
+               if (++trb0_idx == xp->ring.ntrb)
+                       trb0_idx = 0;
+       }
+       return len;
+}
+
 int
 xhci_event_xfer_generic(struct xhci_softc *sc, struct usbd_xfer *xfer,
     struct xhci_pipe *xp, uint32_t remain, int trb_idx,
@@ -823,12 +845,22 @@ xhci_event_xfer_generic(struct xhci_soft
                 * This might be the last TRB of a TD that ended up
                 * with a Short Transfer condition, see below.
                 */
-               if (xfer->actlen == 0)
-                       xfer->actlen = xfer->length - remain;
+               if (xfer->actlen == 0) {
+                       if (remain)
+                               xfer->actlen =
+                                   xhci_xfer_length_generic(xx, xp, trb_idx) -
+                                   remain;
+                       else
+                               xfer->actlen = xfer->length;
+               }
                xfer->status = USBD_NORMAL_COMPLETION;
                break;
        case XHCI_CODE_SHORT_XFER:
-               xfer->actlen = xfer->length - remain;
+               /*
+                * Use values from the transfer TRB instead of the status TRB.
+                */
+               xfer->actlen = xhci_xfer_length_generic(xx, xp, trb_idx) -
+                   remain;
                /*
                 * If this is not the last TRB of a transfer, we should
                 * theoretically clear the IOC at the end of the chain

Reply via email to