This is an automated email from the ASF dual-hosted git repository.
xiaoxiang781216 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx.git
The following commit(s) were added to refs/heads/master by this push:
new 47568187442 usbmsc: fix composite-mode class requests and Hi>Di stall
behavior
47568187442 is described below
commit 475681874424979149c4441f78f33ef02df6caff
Author: Ricard Rosson <[email protected]>
AuthorDate: Wed Jul 15 21:01:19 2026 +0100
usbmsc: fix composite-mode class requests and Hi>Di stall behavior
Three defects that together prevented macOS from ever mounting a
composite USBMSC function (Linux was mostly unaffected because its
probe sequence and recovery timing never exercised these paths):
1. usbmsc_setup() compared the class-request wIndex against the
compile-time constant USBMSC_INTERFACEID (= CONFIG_USBMSC_IFNOBASE,
i.e. 0) instead of the composite-assigned priv->devinfo.ifnobase.
In composite mode the MSC interface number is nonzero, so
GET MAX LUN, Bulk-Only Mass Storage Reset, and GET/SET INTERFACE
all failed the index check and stalled EP0. Standalone MSC is
unaffected (ifnobase == 0), which is why this went unnoticed.
2. usbmsc_deferredresponse() has its entire body inside
#ifndef CONFIG_USBMSC_COMPOSITE, so the deferred EP0 status stage
for MSRESET/SETINTERFACE was never sent in composite mode and the
host's Bulk-Only reset timed out. (Unreachable before fix 1 --
MSRESET used to stall at the wrong-interface check.) Compile the
body in composite mode too, but suppress the worker's deferred
response for SETCONFIGURATION there: the composite driver answers
that request itself, and a duplicate zero-length packet corrupts
the EP0 state.
3. usbmsc_cmdfinishstate() stalled the bulk IN endpoint whenever a
device-to-host command left a residue, even when the response had
already been sent and terminated by a short packet (or ZLP). The
stall is BOT-legal (USB MSC BOT 6.7.2) but gratuitous: the short
packet already ended the data phase and the residue is reported in
dCSWDataResidue. Hosts such as macOS answer any bulk-IN halt during
device probing with a full Bulk-Only reset sequence, which costs
seconds per command or aborts the probe entirely (macOS probes
MODE SENSE(6) with allocation lengths that exceed the response;
Linux's probe does not). Only halt the endpoint when nothing
terminated the data phase.
Root-cause analysis and host traces in apache/nuttx#19435.
Validated on RP2350 silicon (Raspberry Pi Pico 2 W, composite
CDC-ACM + CDC-NCM + USBMSC): GET MAX LUN answers 1 LUN (previously
EP0 stall and a garbage LUN count on macOS), MSRESET completes 10/10
(previously ETIMEDOUT), MODE SENSE(6) alloc=0xC0 returns short data
plus a CSW with dCSWDataResidue and zero bulk-IN stalls across the
exact-length suite, and macOS now mounts the volume (together with the
companion DCD fixes).
Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Ricard Rosson <[email protected]>
---
drivers/usbdev/usbmsc.c | 10 ++++------
drivers/usbdev/usbmsc_scsi.c | 42 ++++++++++++++++++++++++++++++++++++++++--
2 files changed, 44 insertions(+), 8 deletions(-)
diff --git a/drivers/usbdev/usbmsc.c b/drivers/usbdev/usbmsc.c
index bfac6ff03f9..f668f702da6 100644
--- a/drivers/usbdev/usbmsc.c
+++ b/drivers/usbdev/usbmsc.c
@@ -741,7 +741,7 @@ static int usbmsc_setup(FAR struct usbdevclass_driver_s
*driver,
if (ctrl->type == USB_REQ_RECIPIENT_INTERFACE)
{
if (priv->config == USBMSC_CONFIGID &&
- index == USBMSC_INTERFACEID &&
+ index == priv->devinfo.ifnobase &&
value == USBMSC_ALTINTERFACEID)
{
/* Signal to instantiate the interface change */
@@ -764,7 +764,7 @@ static int usbmsc_setup(FAR struct usbdevclass_driver_s
*driver,
if (ctrl->type == (USB_DIR_IN | USB_REQ_RECIPIENT_INTERFACE) &&
priv->config == USBMSC_CONFIGIDNONE)
{
- if (index != USBMSC_INTERFACEID)
+ if (index != priv->devinfo.ifnobase)
{
ret = -EDOM;
}
@@ -805,7 +805,7 @@ static int usbmsc_setup(FAR struct usbdevclass_driver_s
*driver,
{
/* Only one interface is supported */
- if (index != USBMSC_INTERFACEID)
+ if (index != priv->devinfo.ifnobase)
{
usbtrace(TRACE_CLSERROR(USBMSC_TRACEERR_MSRESETNDX),
index);
@@ -836,7 +836,7 @@ static int usbmsc_setup(FAR struct usbdevclass_driver_s
*driver,
{
/* Only one interface is supported */
- if (index != USBMSC_INTERFACEID)
+ if (index != priv->devinfo.ifnobase)
{
usbtrace(TRACE_CLSERROR(USBMSC_TRACEERR_GETMAXLUNNDX),
index);
@@ -1280,7 +1280,6 @@ void usbmsc_rdcomplete(FAR struct usbdev_ep_s *ep,
void usbmsc_deferredresponse(FAR struct usbmsc_dev_s *priv, bool failed)
{
-#ifndef CONFIG_USBMSC_COMPOSITE
FAR struct usbdev_s *dev;
FAR struct usbdev_req_s *ctrlreq;
int ret;
@@ -1322,7 +1321,6 @@ void usbmsc_deferredresponse(FAR struct usbmsc_dev_s
*priv, bool failed)
usbtrace(TRACE_CLSERROR(USBMSC_TRACEERR_DEFERREDRESPSTALLED), 0);
EP_STALL(dev->ep0);
}
-#endif
}
/****************************************************************************
diff --git a/drivers/usbdev/usbmsc_scsi.c b/drivers/usbdev/usbmsc_scsi.c
index d63478be224..0b2f217d77b 100644
--- a/drivers/usbdev/usbmsc_scsi.c
+++ b/drivers/usbdev/usbmsc_scsi.c
@@ -2614,6 +2614,8 @@ static int usbmsc_cmdfinishstate(FAR struct usbmsc_dev_s
*priv)
case USBMSC_FLAGS_DIRDEVICE2HOST:
if (priv->cbwlen > 0)
{
+ bool terminated = false;
+
/* On most commands (the exception is outgoing, write commands),
* the data has not yet been sent.
*/
@@ -2645,11 +2647,35 @@ static int usbmsc_cmdfinishstate(FAR struct
usbmsc_dev_s *priv)
usbtrace(TRACE_CLSERROR(USBMSC_TRACEERR_CMDFINISHSUBMIT),
(uint16_t)-ret);
}
+ else
+ {
+ /* The request was submitted with USBDEV_REQFLAGS_NULLPKT,
+ * so the transfer ends with a short packet (or a ZLP if
+ * the response length is an exact multiple of the packet
+ * size). Either way the host's Data-In phase terminates
+ * cleanly when this request completes.
+ */
+
+ terminated = true;
+ }
}
- /* Stall the BULK In endpoint if there is a residue */
+ /* If there is a residue, the host expected more data than we
+ * sent. If the data phase was already terminated by a short
+ * packet (or ZLP), nothing more is needed: the host's transfer
+ * has completed and the residue is reported in the CSW
+ * (dCSWDataResidue). Stalling here as well is permitted by BOT
+ * (USB MSC BOT 6.7.2), but it is gratuitous and some hosts
+ * (macOS) respond with a full Bulk-Only reset sequence to any
+ * bulk-IN halt during device probing, which costs seconds per
+ * command or aborts the probe entirely.
+ *
+ * Only halt the endpoint when nothing terminated the data
+ * phase - otherwise the host would mistake the following CSW
+ * for transfer data.
+ */
- if (priv->residue > 0)
+ if (priv->residue > 0 && !terminated)
{
#ifndef CONFIG_USBMSC_NOT_STALL_BULKEP
usbtrace(TRACE_CLSERROR(USBMSC_TRACEERR_CMDFINISHRESIDUE),
@@ -2975,8 +3001,20 @@ int usbmsc_scsi_main(int argc, FAR char *argv[])
* response
*/
+#ifdef CONFIG_USBMSC_COMPOSITE
+ /* In composite mode the composite driver itself responds to
+ * SETCONFIGURATION (see composite_ep0submit); only the deferred
+ * responses to MSRESET and SETINTERFACE are owed by this class.
+ * Submitting a second response for SETCONFIGURATION would corrupt
+ * the EP0 state.
+ */
+
+ if ((eventset & (USBMSC_EVENT_RESET |
+ USBMSC_EVENT_IFCHANGE)) != 0)
+#else
if ((eventset & (USBMSC_EVENT_RESET | USBMSC_EVENT_CFGCHANGE |
USBMSC_EVENT_IFCHANGE)) != 0)
+#endif
{
usbmsc_deferredresponse(priv, false);
}