usbh: EP0 opened at an illegal max packet size for USB 3.x devices that fall back to full/high speed
Duplicate check performed (2026-09-16): searched issues and PRs for
bMaxPacketSize0, ep0_size, SuperSpeed/USB 3.x enumeration, "Invalid Device
descriptor", and reviewed the four open enumeration issues (#3898, #3876,
#3149, #3000) and the open USB3 host bring-up PR #3922. None covers this.
PR #3541 is adjacent — it added the descriptor validation quoted below — but
does not decode the USB 3.x encoding.
Summary
bMaxPacketSize0 is not the same field in USB 2.x and USB 3.x. USB 2.x gives
a literal byte count; USB 3.x gives a power-of-two exponent. The host
treats it as a byte count unconditionally, so a device reporting 09h
(meaning 2^9 = 512) has EP0 opened with a maximum packet size of 9 bytes,
which is not a legal control endpoint size at any speed. Enumeration then
fails on the first data stage.
This is not confined to links running at SuperSpeed. A SuperSpeed-capable device that falls back to full or high speed keeps reporting its USB 3.x device descriptor, so the exponent encoding arrives on links TinyUSB is otherwise perfectly happy with.
Where it happens (master)
The 8-byte descriptor is validated in ENUM_SET_ADDR:
// src/host/usbh.c
if (!(desc_device->bDescriptorType == TUSB_DESC_DEVICE && desc_device->bMaxPacketSize0 >= 8)) {
TU_LOG_USBH("Invalid Device descriptor\r\n");
is_enum_failed = true;
break;
}
...
new_dev->desc_device.bMaxPacketSize0 = desc_device->bMaxPacketSize0;and the stored value is later handed straight to the HCD:
case ENUM_AFTER_SET_ADDRESS_RECOVERY_DELAY: {
...
if (!usbh_edpt_control_open(new_addr, new_dev->desc_device.bMaxPacketSize0)) {The >= 8 check added in #3541 does not catch this: a USB 3.x device reports
9, which passes, and 9 is then programmed into the controller.
Worth noting separately that >= 8 is permissive in general — it also accepts
10, 17, 63 and so on, none of which are legal. Only 8, 16, 32 and 64 are valid
for USB 2.x.
Spec references
- USB 2.0 §9.6.1 —
bMaxPacketSize0is the maximum packet size in bytes; "only 8, 16, 32, or 64 are valid". - USB 3.2 §9.6.1 — for a SuperSpeed device the field "shall be set to 09H, indicating a 512 byte maximum packet", i.e. an exponent, not a count.
So 9 is simultaneously a valid USB 3.x encoding and an invalid USB 2.x
value. Either way it should never reach hcd_edpt_open().
Observed behaviour
Captured with CFG_TUSB_DEBUG=3 on a chipidea-HS host (NXP MCX N947), Yamaha
Montage attached behind a USB 2.0 hub. First 8 bytes of the device descriptor:
[1:0] Control data:
0000: 12 01 10 03 EF 02 01 09 |........|bcdUSB=0x0310(USB 3.1)bDeviceClass/SubClass/Protocol=EF 02 01(composite, IAD)bMaxPacketSize0=0x09→ 512 bytes
The next line takes it literally:
Set Address = 1
[1:1] Open EP0 with Size = 9For contrast, the hub enumerated moments earlier reports bcdUSB 0x0200 with
bMaxPacketSize0 = 0x40 and correctly gets Open EP0 with Size = 64.
Everything after that follows mechanically. The 18-byte
GET_DESCRIPTOR(DEVICE) overruns a 9-byte endpoint, the queue head's error
counter saturates, all three attempts fail identically, and the device is torn
down:
Get Device Descriptor
[1:1] Get Descriptor: 80 06 00 01 00 00 12 00
on EP 00 with 8 bytes: OK
QHD xfer err count: 3
on EP 80 with 0 bytes: FAILED
[1:1] Control FAILED, xferred_bytes = 0
Enumeration attempt 1
... (identical, x3)
[1:0:0] USBH DEVICE REMOVEDThe SETUP stage succeeds because SETUP is always 8 bytes, so the wrong size does not bite until the data stage.
Suggested fix
Decode the exponent for USB 3.x descriptors, and clamp to what the link can actually use — EP0 is 8/16/32/64 below SuperSpeed:
static uint8_t enum_ep0_size(tusb_desc_device_t const *desc_device) {
uint8_t ep0_size = desc_device->bMaxPacketSize0;
if (tu_le16toh(desc_device->bcdUSB) >= 0x0300 && ep0_size <= 9) {
uint16_t const decoded = (uint16_t) (1u << ep0_size);
// A device asking for 512 on an FS/HS link can only have 64.
ep0_size = (uint8_t) (decoded > 64 ? 64 : decoded);
}
return ep0_size;
}Scoped deliberately to bcdUSB >= 0x0300, so a USB 2.x device's reported
value passes through untouched and nothing that enumerates correctly today can
change behaviour. Linux does the equivalent in hub.c, which is why these
devices enumerate on a PC but not here.
Tightening the validation to accept only 8/16/32/64 (after decoding) would also be reasonable, but is a separate change with more potential to reject devices that currently work, so I have kept it out of the suggestion above.
Happy to open a PR.
Status of testing
Being upfront: this fix is not yet confirmed against the device that prompted it. We do not have the Montage on site — the capture above came from a colleague, and a build with the fix is with them now.
What has been verified locally is the regression risk: ordinary full-speed USB
2.0 MIDI devices are unaffected, still reporting Open EP0 with Size = 64 and
enumerating and streaming normally.
I did not want to send a PR claiming a fix I have not seen work end to end, so I am filing the defect first. The analysis stands regardless of how that test turns out: opening EP0 at 9 bytes is wrong even if something else is also wrong with that device.
Environment
- Reproduced on TinyUSB 0.18.0; code inspected on
master(the enumeration state machine has been restructured since, but the defect is unchanged) - Host controller: chipidea-HS / EHCI core (
src/portable/ehci/ehci.c) - MCU: NXP MCX N947 (FRDM-MCXN947)
- Device: Yamaha Montage, via a USB 2.0 hub
One incidental observation from the same capture, which I am not raising as part of this issue: the hub itself enumerated at full speed on a high-speed-capable controller. That may be unrelated and I have not investigated it.
Source: hathach/tinyusb