usbd: don't leak the queued-setup counter when the event queue is full

A SETUP arriving while the event queue is full is silently dropped by
queue_event(), but _usbd_queued_setup has already been incremented. The
leaked count makes the event handler skip every subsequent SETUP
("Skipped since there is other SETUP in queue") forever: EP0 stays deaf
until tud_init() while the device otherwise looks alive - enumerated,
endpoints armed. Undo the increment when the enqueue fails.

Unit test: fill the queue so a SETUP is dropped, then verify the next
SETUP still completes a GET_DESCRIPTOR control transfer.
This commit is contained in:
hathach
2026-08-12 22:36:49 +07:00
parent 32530d8f4b
commit a0249ada90
2 changed files with 42 additions and 2 deletions

View File

@ -1473,8 +1473,10 @@ TU_ATTR_FAST_FUNC void dcd_event_handler(dcd_event_t const* event, bool in_isr)
break;
}
if (send) {
queue_event(event, in_isr);
if (send && !queue_event(event, in_isr) && event->event_id == DCD_EVENT_SETUP_RECEIVED) {
// dropped by a full queue: undo the increment, else every later SETUP is skipped as
// "other SETUP in queue" and EP0 is deaf until re-init
_usbd_queued_setup--;
}
}

View File

@ -270,6 +270,44 @@ void test_usbd_control_in_zlp(void)
tud_task();
}
//--------------------------------------------------------------------+
// SETUP dropped by full event queue
//--------------------------------------------------------------------+
// When the event queue is full, queue_event() drops the SETUP event. The queued-setup
// counter must not keep the dropped SETUP's increment: a leaked count makes the handler
// skip every later SETUP ("other SETUP in queue") forever, leaving EP0 permanently deaf.
void test_usbd_setup_dropped_by_full_queue_recovers(void)
{
// fillers drain through usbd_reset -> class reset
mscd_reset_Ignore();
// fill the queue to the brim, then post one more SETUP: queue_event() drops it
for (unsigned i = 0; i < CFG_TUD_TASK_QUEUE_SZ; i++) {
dcd_event_bus_signal(rhport, DCD_EVENT_UNPLUGGED, false);
}
dcd_event_setup_received(rhport, (uint8_t*) &req_get_desc_device, false);
// drain all fillers (each tud_task pass handles at most CFG_TUD_TASK_EVENTS_PER_RUN
// events); the dropped SETUP never arrives
for (unsigned i = 0; i < (CFG_TUD_TASK_QUEUE_SZ / CFG_TUD_TASK_EVENTS_PER_RUN) + 1; i++) {
tud_task();
}
// the next SETUP must still be answered
desc_device = (uint8_t const*) &data_desc_device;
dcd_event_setup_received(rhport, (uint8_t*) &req_get_desc_device, false);
dcd_edpt_xfer_ExpectWithArrayAndReturn(rhport, 0x80, (uint8_t*) &data_desc_device, sizeof(tusb_desc_device_t), sizeof(tusb_desc_device_t), false, true);
dcd_event_xfer_complete(rhport, EDPT_CTRL_IN, sizeof(tusb_desc_device_t), 0, false);
dcd_edpt_xfer_ExpectAndReturn(rhport, EDPT_CTRL_OUT, NULL, 0, false, true);
dcd_event_xfer_complete(rhport, EDPT_CTRL_OUT, 0, 0, false);
dcd_edpt0_status_complete_ExpectWithArray(rhport, &req_get_desc_device, 1);
tud_task();
}
//--------------------------------------------------------------------+
// Control OUT data stage host overrun
//--------------------------------------------------------------------+