From fb69c15168cdf5572cb02046bc9567e4f3d30b52 Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 23 Sep 2026 18:42:50 +0700 Subject: [PATCH 1/4] Clamp endpoint stream requests before narrowing to the fifo length tu_edpt_stream_write/read take a uint32_t bufsize but passed (uint16_t)bufsize to the fifo, so a 65536-byte request became 0 and 65537 became 1 and the transfer stalled. #2219 added this clamp in CDC; f11adb0 moved CDC onto the shared stream and dropped it. Clamp at the stream boundary so every stream user is covered, and let tud_vendor_n_write pass the full request through. --- src/class/vendor/vendor_device.c | 2 +- src/tusb.c | 4 +- test/unit-test/test/device/usbd/test_usbd.c | 49 +++++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/class/vendor/vendor_device.c b/src/class/vendor/vendor_device.c index 595513cf63..3293bb6c82 100644 --- a/src/class/vendor/vendor_device.c +++ b/src/class/vendor/vendor_device.c @@ -253,7 +253,7 @@ uint32_t tud_vendor_n_write(uint8_t idx, const void *buffer, uint32_t bufsize) { vendord_interface_t *p_itf = &_vendord_itf[idx]; #if CFG_TUD_VENDOR_TXRX_BUFFERED - return tu_edpt_stream_write(&p_itf->tx_stream, buffer, (uint16_t)bufsize); + return tu_edpt_stream_write(&p_itf->tx_stream, buffer, bufsize); #else // non-fifo mode: direct transfer (ep_in is 0 while an altsetting without a bulk IN ep is active) diff --git a/src/tusb.c b/src/tusb.c index ca298cdf75..94b2b34c20 100644 --- a/src/tusb.c +++ b/src/tusb.c @@ -406,7 +406,7 @@ uint32_t tu_edpt_stream_write_xfer(tu_edpt_stream_t *s) { uint32_t tu_edpt_stream_write(tu_edpt_stream_t *s, const void *buffer, uint32_t bufsize) { TU_VERIFY(bufsize > 0); - const uint16_t ret = tu_fifo_write_n(&s->ff, buffer, (uint16_t) bufsize); + const uint16_t ret = tu_fifo_write_n(&s->ff, buffer, (uint16_t) tu_min32(bufsize, UINT16_MAX)); // flush if fifo has more than packet size or // in rare case: fifo depth is configured too small (which never reach packet size) @@ -448,7 +448,7 @@ uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) { } uint32_t tu_edpt_stream_read(tu_edpt_stream_t *s, void *buffer, uint32_t bufsize) { - const uint32_t num_read = tu_fifo_read_n(&s->ff, buffer, (uint16_t)bufsize); + const uint32_t num_read = tu_fifo_read_n(&s->ff, buffer, (uint16_t) tu_min32(bufsize, UINT16_MAX)); tu_edpt_stream_read_xfer(s); return num_read; } diff --git a/test/unit-test/test/device/usbd/test_usbd.c b/test/unit-test/test/device/usbd/test_usbd.c index 466d785fbe..df1b9f6165 100644 --- a/test/unit-test/test/device/usbd/test_usbd.c +++ b/test/unit-test/test/device/usbd/test_usbd.c @@ -563,3 +563,52 @@ void test_usbd_stream_write_zlp_after_full_packet(void) dcd_edpt_xfer_ExpectAndReturn(rhport, 0x82, NULL, 0, false, true); TEST_ASSERT_TRUE(tu_edpt_stream_write_zlp_if_needed(&stream, 64)); } + +//--------------------------------------------------------------------+ +// Endpoint stream wide requests +//--------------------------------------------------------------------+ + +// The stream API takes a uint32_t request but the fifo API is uint16_t: narrowed, 65536 becomes 0 +// and 65537 becomes 1, so the request must be clamped to UINT16_MAX first. A closed stream +// (ep_addr 0) never claims an endpoint, so no DCD expectation is needed. +enum { WIDE_REQ_MAX = 65537 }; +static uint8_t wide_src[WIDE_REQ_MAX]; +static uint8_t wide_dst[WIDE_REQ_MAX]; + +static void wide_stream_setup(tu_edpt_stream_t *stream, bool overwritable, uint8_t *ff_buf) { + memset(stream, 0, sizeof(*stream)); + TEST_ASSERT_TRUE(tu_edpt_stream_init(stream, false, true, overwritable, ff_buf, 64, NULL)); +} + +void test_usbd_stream_wide_request(void) +{ + static const uint32_t sizes[] = { 65535, 65536, WIDE_REQ_MAX }; + uint8_t ff_buf[64]; + tu_edpt_stream_t stream; + + for (uint32_t i = 0; i < WIDE_REQ_MAX; i++) { + wide_src[i] = (uint8_t) (i * 7u + 3u); + } + + for (size_t i = 0; i < TU_ARRAY_SIZE(sizes); i++) { + // write: the fifo takes the first 64 bytes of the request + wide_stream_setup(&stream, false, ff_buf); + TEST_ASSERT_EQUAL_UINT32(64, tu_edpt_stream_write(&stream, wide_src, sizes[i])); + TEST_ASSERT_EQUAL_UINT16(64, tu_fifo_count(&stream.ff)); + TEST_ASSERT_EQUAL_MEMORY(wide_src, ff_buf, 64); + + // read: the 64 queued bytes come out and nothing past them is written + memset(wide_dst, 0xA5, sizeof(wide_dst)); + TEST_ASSERT_EQUAL_UINT32(64, tu_edpt_stream_read(&stream, wide_dst, sizes[i])); + TEST_ASSERT_EQUAL_UINT16(0, tu_fifo_count(&stream.ff)); + TEST_ASSERT_EQUAL_MEMORY(wide_src, wide_dst, 64); + TEST_ASSERT_EQUAL_UINT8(0xA5, wide_dst[64]); + } + + // overwritable: the fifo keeps the tail of the clamped 65535-byte request, not of the whole one + wide_stream_setup(&stream, true, ff_buf); + TEST_ASSERT_EQUAL_UINT32(64, tu_edpt_stream_write(&stream, wide_src, 65536)); + TEST_ASSERT_EQUAL_UINT16(64, tu_fifo_count(&stream.ff)); + TEST_ASSERT_EQUAL_UINT32(64, tu_edpt_stream_read(&stream, wide_dst, 64)); + TEST_ASSERT_EQUAL_MEMORY(wide_src + 65535 - 64, wide_dst, 64); +} From 3e017f3dd2cfae718e236469107caa9e0d5e1695 Mon Sep 17 00:00:00 2001 From: hathach Date: Fri, 25 Sep 2026 17:54:10 +0700 Subject: [PATCH 2/4] msc host: split data stages larger than a 16-bit transfer tuh_msc_read10/write10 take a uint16_t block count, but the data stage was submitted as one usbh_edpt_xfer() of (uint16_t) total_bytes, so 128 sectors of 512 bytes became a 0-byte transfer and the command wedged. Submit the data stage in 0xFC00-byte pieces, a multiple of every bulk packet size, ending on a short or failed piece, and reject block counts whose byte total overflows the CBW field. --- src/class/msc/msc_host.c | 43 ++++-- test/unit-test/project.yml | 4 + test/unit-test/test/host/msc/test_msc_host.c | 140 +++++++++++++++++++ 3 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 test/unit-test/test/host/msc/test_msc_host.c diff --git a/src/class/msc/msc_host.c b/src/class/msc/msc_host.c index 8afd707058..a197a8dec6 100644 --- a/src/class/msc/msc_host.c +++ b/src/class/msc/msc_host.c @@ -43,6 +43,7 @@ typedef struct { // SCSI command data uint8_t stage; void* buffer; + uint32_t data_xferred; tuh_msc_complete_cb_t complete_cb; uintptr_t complete_arg; @@ -68,6 +69,21 @@ TU_ATTR_ALWAYS_INLINE static inline msch_epbuf_t* get_epbuf(uint8_t daddr) { return &_msch_epbuf[daddr - 1]; } +// usbh_edpt_xfer() length is 16-bit: a larger data stage is split into transfers of this size, +// a multiple of every bulk max packet size so that only the last one can end short +#define MSCH_DATA_XFER_MAX 0xFC00u + +static bool data_stage_xfer(uint8_t daddr, msch_interface_t* p_msc, const msc_cbw_t* cbw) { + const uint8_t ep_data = (cbw->dir & TUSB_DIR_IN_MASK) ? p_msc->ep_in : p_msc->ep_out; + const uint16_t len = (uint16_t) tu_min32(cbw->total_bytes - p_msc->data_xferred, MSCH_DATA_XFER_MAX); + return usbh_edpt_xfer(daddr, ep_data, (uint8_t*) p_msc->buffer + p_msc->data_xferred, len); +} + +static bool status_stage_xfer(uint8_t daddr, msch_interface_t* p_msc, msc_csw_t* csw) { + p_msc->stage = MSC_STAGE_STATUS; + return usbh_edpt_xfer(daddr, p_msc->ep_in, (uint8_t*) csw, (uint16_t) sizeof(msc_csw_t)); +} + //--------------------------------------------------------------------+ // Weak stubs: invoked if no strong implementation is available //--------------------------------------------------------------------+ @@ -222,7 +238,9 @@ bool tuh_msc_read10(uint8_t dev_addr, uint8_t lun, void* buffer, uint32_t lba, u msc_cbw_t cbw; cbw_init(&cbw, lun); - cbw.total_bytes = block_count * p_msc->capacity[lun].block_size; + const uint32_t block_size = p_msc->capacity[lun].block_size; + TU_VERIFY(block_count <= UINT32_MAX / tu_max32(block_size, 1)); // CBW data length is 32-bit + cbw.total_bytes = block_count * block_size; cbw.dir = TUSB_DIR_IN_MASK; cbw.cmd_len = sizeof(scsi_read10_t); @@ -244,7 +262,9 @@ bool tuh_msc_write10(uint8_t dev_addr, uint8_t lun, void const* buffer, uint32_t msc_cbw_t cbw; cbw_init(&cbw, lun); - cbw.total_bytes = block_count * p_msc->capacity[lun].block_size; + const uint32_t block_size = p_msc->capacity[lun].block_size; + TU_VERIFY(block_count <= UINT32_MAX / tu_max32(block_size, 1)); // CBW data length is 32-bit + cbw.total_bytes = block_count * block_size; cbw.dir = TUSB_DIR_OUT; cbw.cmd_len = sizeof(scsi_write10_t); @@ -318,16 +338,21 @@ bool msch_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t event, uint32 if (cbw->total_bytes && p_msc->buffer) { // Data stage if any p_msc->stage = MSC_STAGE_DATA; - uint8_t const ep_data = (cbw->dir & TUSB_DIR_IN_MASK) ? p_msc->ep_in : p_msc->ep_out; - TU_ASSERT(usbh_edpt_xfer(dev_addr, ep_data, p_msc->buffer, (uint16_t) cbw->total_bytes)); - break; + p_msc->data_xferred = 0; + TU_ASSERT(data_stage_xfer(dev_addr, p_msc, cbw)); + } else { + TU_ASSERT(status_stage_xfer(dev_addr, p_msc, csw)); } - TU_ATTR_FALLTHROUGH; // fallthrough to data stage + break; case MSC_STAGE_DATA: - // Status stage - p_msc->stage = MSC_STAGE_STATUS; - TU_ASSERT(usbh_edpt_xfer(dev_addr, p_msc->ep_in, (uint8_t*) csw, (uint16_t) sizeof(msc_csw_t))); + p_msc->data_xferred += xferred_bytes; + if (event == XFER_RESULT_SUCCESS && xferred_bytes == MSCH_DATA_XFER_MAX && + p_msc->data_xferred < cbw->total_bytes) { + TU_ASSERT(data_stage_xfer(dev_addr, p_msc, cbw)); + } else { + TU_ASSERT(status_stage_xfer(dev_addr, p_msc, csw)); + } break; case MSC_STAGE_STATUS: diff --git a/test/unit-test/project.yml b/test/unit-test/project.yml index 599dabc212..18f96ee4c8 100644 --- a/test/unit-test/project.yml +++ b/test/unit-test/project.yml @@ -139,6 +139,10 @@ - CFG_TUSB_DEBUG=0 - CFG_TUH_AUDIO_EPIN_BUFSIZE=1024 - CFG_TUH_AUDIO_EPOUT_BUFSIZE=1024 + :test_msc_host: + - CFG_TUH_ENABLED=1 + - CFG_TUH_MSC=1 + - CFG_TUSB_DEBUG=0 :release: [] # Enable to inject name of a test as a unique compilation symbol into its respective executable build. diff --git a/test/unit-test/test/host/msc/test_msc_host.c b/test/unit-test/test/host/msc/test_msc_host.c new file mode 100644 index 0000000000..21f31b8dfd --- /dev/null +++ b/test/unit-test/test/host/msc/test_msc_host.c @@ -0,0 +1,140 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 TinyUSB contributors + * SPDX-License-Identifier: MIT + */ + +#include "unity.h" +#include "tusb_option.h" +#include "host/usbh.h" +#include "host/usbh_pvt.h" +#include "class/msc/msc_host.h" + +TEST_SOURCE_FILE("msc_host.c") + +enum { + DADDR = 1, + EP_OUT = 0x01, + EP_IN = 0x81, + MAX_XFERS = 8, +}; + +static uint8_t xfer_ep[MAX_XFERS]; +static uint8_t *xfer_buf[MAX_XFERS]; +static uint16_t xfer_len[MAX_XFERS]; +static uint8_t xfer_count; +static uint8_t enum_buf[64]; +static uint8_t data[98304]; + +bool tuh_edpt_open(uint8_t daddr, const tusb_desc_endpoint_t *desc_ep) { + (void) daddr; + (void) desc_ep; + return true; +} + +bool tuh_control_xfer(tuh_xfer_t *xfer) { + (void) xfer; + return true; +} + +uint8_t *usbh_get_enum_buf(void) { + return enum_buf; +} + +void usbh_driver_set_config_complete(uint8_t dev_addr, uint8_t itf_num) { + (void) dev_addr; + (void) itf_num; +} + +bool usbh_edpt_claim(uint8_t dev_addr, uint8_t ep_addr) { + (void) dev_addr; + (void) ep_addr; + return true; +} + +bool usbh_edpt_release(uint8_t dev_addr, uint8_t ep_addr) { + (void) dev_addr; + (void) ep_addr; + return true; +} + +bool usbh_edpt_busy(uint8_t dev_addr, uint8_t ep_addr) { + (void) dev_addr; + (void) ep_addr; + return false; +} + +bool usbh_edpt_xfer_with_callback(uint8_t dev_addr, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + (void) dev_addr; + (void) complete_cb; + (void) user_data; + TEST_ASSERT_LESS_THAN(MAX_XFERS, xfer_count); + xfer_ep[xfer_count] = ep_addr; + xfer_buf[xfer_count] = buffer; + xfer_len[xfer_count] = total_bytes; + xfer_count++; + return true; +} + +static void mount_bot_interface(void) { + struct TU_ATTR_PACKED { + tusb_desc_interface_t itf; + tusb_desc_endpoint_t ep_out; + tusb_desc_endpoint_t ep_in; + } const desc = { + .itf = {sizeof(tusb_desc_interface_t), TUSB_DESC_INTERFACE, 0, 0, 2, TUSB_CLASS_MSC, MSC_SUBCLASS_SCSI, + MSC_PROTOCOL_BOT, 0}, + .ep_out = {sizeof(tusb_desc_endpoint_t), TUSB_DESC_ENDPOINT, EP_OUT, {.xfer = TUSB_XFER_BULK}, 512, 0}, + .ep_in = {sizeof(tusb_desc_endpoint_t), TUSB_DESC_ENDPOINT, EP_IN, {.xfer = TUSB_XFER_BULK}, 512, 0}, + }; + + TEST_ASSERT_EQUAL(sizeof(desc), msch_open(0, DADDR, &desc.itf, sizeof(desc))); + TEST_ASSERT_TRUE(msch_set_config(DADDR, 0)); +} + +void setUp(void) { + xfer_count = 0; + msch_init(); + mount_bot_interface(); +} + +void tearDown(void) {} + +static void start_data_in(void) { + const msc_cbw_t cbw = {.signature = MSC_CBW_SIGNATURE, .total_bytes = sizeof(data), .dir = TUSB_DIR_IN_MASK}; + TEST_ASSERT_TRUE(tuh_msc_scsi_command(DADDR, &cbw, data, NULL, 0)); + TEST_ASSERT_TRUE(msch_xfer_cb(DADDR, EP_OUT, XFER_RESULT_SUCCESS, sizeof(msc_cbw_t))); +} + +// usbh_edpt_xfer() takes a 16-bit length, so a data stage of 64 KiB or more must be split into +// packet-aligned transfers rather than narrowed (98304 would otherwise become 32768). +void test_msc_host_data_stage_over_64k(void) { + start_data_in(); + TEST_ASSERT_EQUAL(2, xfer_count); + TEST_ASSERT_EQUAL_HEX8(EP_IN, xfer_ep[1]); + TEST_ASSERT_EQUAL_PTR(data, xfer_buf[1]); + const uint16_t first = xfer_len[1]; + TEST_ASSERT_TRUE(first > sizeof(data) - UINT16_MAX); + TEST_ASSERT_EQUAL(0, first % 512); + + TEST_ASSERT_TRUE(msch_xfer_cb(DADDR, EP_IN, XFER_RESULT_SUCCESS, first)); + TEST_ASSERT_EQUAL(3, xfer_count); + TEST_ASSERT_EQUAL_HEX8(EP_IN, xfer_ep[2]); + TEST_ASSERT_EQUAL_PTR(data + first, xfer_buf[2]); + TEST_ASSERT_EQUAL(sizeof(data) - first, xfer_len[2]); + + // last data transfer done: status stage reads the CSW + TEST_ASSERT_TRUE(msch_xfer_cb(DADDR, EP_IN, XFER_RESULT_SUCCESS, xfer_len[2])); + TEST_ASSERT_EQUAL(4, xfer_count); + TEST_ASSERT_EQUAL_HEX8(EP_IN, xfer_ep[3]); + TEST_ASSERT_EQUAL(sizeof(msc_csw_t), xfer_len[3]); +} + +// A short transfer ends the data stage early: go straight to the CSW +void test_msc_host_data_stage_short_ends_early(void) { + start_data_in(); + TEST_ASSERT_TRUE(msch_xfer_cb(DADDR, EP_IN, XFER_RESULT_SUCCESS, 512)); + + TEST_ASSERT_EQUAL(3, xfer_count); + TEST_ASSERT_EQUAL(sizeof(msc_csw_t), xfer_len[2]); +} From 8d735dd8bc643d3dd72c5d671ad13867132b3402 Mon Sep 17 00:00:00 2001 From: hathach Date: Fri, 25 Sep 2026 17:54:22 +0700 Subject: [PATCH 3/4] Reject transfer lengths that do not fit a 16-bit endpoint transfer tuh_edpt_xfer() narrowed a uint32_t buflen, so 65536 queued a 0-byte transfer and returned success. tud_mtp_response_send() took the container length unchecked, wrapping above 64 KiB and reading past the endpoint buffer below that. --- src/class/mtp/mtp_device.c | 3 +++ src/host/usbh.c | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/class/mtp/mtp_device.c b/src/class/mtp/mtp_device.c index f867bb31be..1e0b5ff375 100644 --- a/src/class/mtp/mtp_device.c +++ b/src/class/mtp/mtp_device.c @@ -90,6 +90,8 @@ typedef struct { TU_ATTR_ALIGNED(4) uint8_t control_buf[CFG_TUD_MTP_EP_CONTROL_BUFSIZE]; } mtpd_interface_t; +TU_VERIFY_STATIC(CFG_TUD_MTP_EP_BUFSIZE <= UINT16_MAX, "usbd_edpt_xfer() length is 16-bit"); + typedef struct { TUD_EPBUF_DEF(buf, CFG_TUD_MTP_EP_BUFSIZE); TUD_EPBUF_TYPE_DEF(mtp_event_t, buf_event); @@ -223,6 +225,7 @@ bool tud_mtp_data_receive(mtp_container_info_t *p_container) { bool tud_mtp_response_send(mtp_container_info_t* p_container) { mtpd_interface_t* p_mtp = &_mtpd_itf; + TU_VERIFY(p_container->header->len <= CFG_TUD_MTP_EP_BUFSIZE); p_mtp->phase = MTP_PHASE_RESPONSE; p_container->header->type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK; p_container->header->transaction_id = p_mtp->command.header.transaction_id; diff --git a/src/host/usbh.c b/src/host/usbh.c index fd0c9b7b59..5aa73df6b7 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -1170,7 +1170,7 @@ bool tuh_edpt_xfer(tuh_xfer_t* xfer) { uint8_t const daddr = xfer->daddr; uint8_t const ep_addr = xfer->ep_addr; - TU_VERIFY(daddr && ep_addr); + TU_VERIFY(daddr && ep_addr && xfer->buflen <= UINT16_MAX); TU_VERIFY(usbh_edpt_claim(daddr, ep_addr)); if (!usbh_edpt_xfer_with_callback(daddr, ep_addr, xfer->buffer, (uint16_t) xfer->buflen, From df5d5e00125c42250049be542ddb9f3de78b6505 Mon Sep 17 00:00:00 2001 From: hathach Date: Fri, 25 Sep 2026 23:07:21 +0700 Subject: [PATCH 4/4] msc device: fail READ10/WRITE10 whose block size does not fit 16 bits The host-computed block size was narrowed to uint16_t, so 66048 bytes for one block read as 512 and walked past the requested LBAs. --- src/class/msc/msc_device.c | 2 +- .../test/device/msc/test_msc_device.c | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/class/msc/msc_device.c b/src/class/msc/msc_device.c index 8485105e41..c98dca4f11 100644 --- a/src/class/msc/msc_device.c +++ b/src/class/msc/msc_device.c @@ -167,7 +167,7 @@ static uint8_t rdwr10_validate_cmd(msc_cbw_t const* cbw) { } else if (SCSI_CMD_WRITE_10 == cbw->command[0] && is_data_in(cbw->dir)) { TU_LOG_DRV(" SCSI case 8 (Hi <> Do)\r\n"); status = MSC_CSW_STATUS_PHASE_ERROR; - } else if (0 == block_count) { + } else if (0 == block_count || (cbw->total_bytes >> 16) >= block_count) { // or block size > 16-bit, without a divide TU_LOG_DRV(" SCSI case 4 Hi > Dn (READ10) or case 9 Ho > Dn (WRITE10) \r\n"); status = MSC_CSW_STATUS_FAILED; } else if (cbw->total_bytes / block_count == 0) { diff --git a/test/unit-test/test/device/msc/test_msc_device.c b/test/unit-test/test/device/msc/test_msc_device.c index ea02b7050a..7c1e4598ad 100644 --- a/test/unit-test/test/device/msc/test_msc_device.c +++ b/test/unit-test/test/device/msc/test_msc_device.c @@ -282,3 +282,73 @@ void test_msc(void) tud_task(); } + +// A block size computed from the CBW that does not fit 16 bits must fail, not wrap (66048 / 1 would read as 512) +void test_msc_read10_block_size_over_16bit(void) +{ + uint32_t const total_bytes = 65536 + 512; + + msc_cbw_t cbw_read10 = + { + .signature = MSC_CBW_SIGNATURE, + .tag = 0xCAFECAFE, + .total_bytes = total_bytes, + .lun = 0, + .dir = TUSB_DIR_IN_MASK, + .cmd_len = sizeof(scsi_read10_t) + }; + + scsi_read10_t cmd_read10 = + { + .cmd_code = SCSI_CMD_READ_10, + .lba = tu_htonl(0), + .block_count = tu_htons(1) + }; + + memcpy(cbw_read10.command, &cmd_read10, cbw_read10.cmd_len); + + desc_configuration = data_desc_configuration; + uint8_t const* desc_ep = tu_desc_next(tu_desc_next(desc_configuration)); + + dcd_event_setup_received(rhport, (uint8_t*) &request_set_configuration, false); + + dcd_edpt_open_ExpectAndReturn(rhport, (tusb_desc_endpoint_t const *) desc_ep, true); + dcd_edpt_open_ExpectAndReturn(rhport, (tusb_desc_endpoint_t const *) tu_desc_next(desc_ep), true); + + dcd_edpt_xfer_ExpectAndReturn(rhport, EDPT_MSC_OUT, NULL, sizeof(msc_cbw_t), false, true); + dcd_edpt_xfer_IgnoreArg_buffer(); + dcd_edpt_xfer_ReturnMemThruPtr_buffer( (uint8_t*) &cbw_read10, sizeof(msc_cbw_t)); + + dcd_event_xfer_complete(rhport, EDPT_MSC_OUT, sizeof(msc_cbw_t), 0, true); + + dcd_edpt_xfer_ExpectAndReturn(rhport, EDPT_CTRL_IN, NULL, 0, false, true); + + // case 4 (Hi > Dn): no data, STALL Bulk-In, then a failed CSW with full residue after the host clears it + dcd_edpt_stall_Expect(rhport, EDPT_MSC_IN); + + tud_task(); + + tusb_control_request_t const request_clear_halt = + { + .bmRequestType = 0x02, + .bRequest = TUSB_REQ_CLEAR_FEATURE, + .wValue = TUSB_REQ_FEATURE_EDPT_HALT, + .wIndex = EDPT_MSC_IN, + .wLength = 0 + }; + + msc_csw_t const csw = + { + .signature = MSC_CSW_SIGNATURE, + .tag = 0xCAFECAFE, + .data_residue = total_bytes, + .status = MSC_CSW_STATUS_FAILED + }; + + dcd_event_setup_received(rhport, (uint8_t const*) &request_clear_halt, false); + dcd_edpt_clear_stall_Expect(rhport, EDPT_MSC_IN); + dcd_edpt_xfer_ExpectWithArrayAndReturn(rhport, EDPT_MSC_IN, (uint8_t*) &csw, sizeof(msc_csw_t), sizeof(msc_csw_t), false, true); + dcd_edpt_xfer_ExpectAndReturn(rhport, EDPT_CTRL_IN, NULL, 0, false, true); + + tud_task(); +}