Description
With UX_HOST_CLASS_CDC_ECM_PACKET_CHAIN_SUPPORT enabled (it is defined
unconditionally in ux_host_class_cdc_ecm.h), the CDC-ECM host class
mishandles chained NX packets on the transmit queue path:
_ux_host_class_cdc_ecm_write() copies a chained packet into
ux_host_class_cdc_ecm_xmit_buffer, allocating that buffer lazily — but
only on its immediate-arm path (transmit queue empty):
/* ux_host_class_cdc_ecm_write.c */
if (packet -> nx_packet_next != UX_NULL)
{
if (cdc_ecm -> ux_host_class_cdc_ecm_xmit_buffer == UX_NULL)
{
cdc_ecm -> ux_host_class_cdc_ecm_xmit_buffer = _ux_utility_memory_allocate(...);
...
When a transfer is already in flight, write() just appends the packet to
ux_host_class_cdc_ecm_xmit_queue_* — no allocation, no copy. The queued
packet is later armed by _ux_host_class_cdc_ecm_transmission_callback(),
which runs in the transfer-completion context — on the STM32 host
controller (and any HCD that completes URBs from its interrupt handler),
that is inside the USB host ISR — and uses the buffer with no NULL check
and no ability to allocate:
/* ux_host_class_cdc_ecm_transmission_callback.c */
if (next_packet -> nx_packet_next != UX_NULL)
{
/* Put packet to continuous buffer to transfer. */
packet_header = cdc_ecm -> ux_host_class_cdc_ecm_xmit_buffer; /* may be UX_NULL */
nx_packet_data_extract_offset(next_packet, 0, packet_header, ...);
}
...
transfer_request -> ux_transfer_request_data_pointer = packet_header;
...
_ux_host_stack_transfer_request(transfer_request);
So if the first chained packet the endpoint ever sees is one that got
queued (routine inside a multi-segment TCP burst: segment 2 is written
while segment 1's transfer is still in flight), the callback:
- calls
nx_packet_data_extract_offset() with a NULL destination, and
- arms a bulk-OUT transfer whose
ux_transfer_request_data_pointer is NULL.
On the STM32 OTG_FS host controller driver (no DMA), that NULL propagates
through HAL_HCD_HC_SubmitRequest → USB_HC_StartXfer →
USB_WritePacket, which reads address 0 inside the OTG_FS interrupt
handler. On a TrustZone-enabled Cortex-M33 (STM32U575, address 0 is
SAU-secure) this is an immediate secure-escalated HardFault; on non-TZ parts
it silently transmits memory from address 0.
A related hazard in the same machinery: _ux_host_class_cdc_ecm_deactivate()
frees ux_host_class_cdc_ecm_xmit_buffer while transfers are being aborted;
a completion callback racing deactivation can extract into the freed buffer
(heap corruption). We observed both signatures on hardware: four
hard faults over two days (three clean NULL reads, one delayed memory-
corruption crash during a device-initiated disconnect).
Reproduction conditions
- USBX host, CDC-ECM class,
UX_HOST_CLASS_CDC_ECM_PACKET_CHAIN_SUPPORT
enabled (the default), ThreadX/NetX Duo integration.
- NX packet pool payload smaller than the Ethernet frame size (ours was
1020 bytes vs the 1514-byte frames NetX builds for the ECM interface's
advertised 1500 MTU), so full-MSS TCP segments are chained packets.
- Sustained transmit (an SMTP send whose TCP segments exceed one pool
packet) — the second and later segments of a burst queue behind the first,
and the completion callback arms them.
- Observed on: STM32U575 (OTG_FS host, no DMA), USBX 6.2.0-era sources
(ux_host_class_cdc_ecm_transmission_callback.c is functionally unchanged
on master as of March 2026), STM32 usbx_stm32_host_controllers glue,
Telit LE910C1 CDC-ECM device. Deterministically reproducible: with email
bodies above one MSS, every session hit it (141 occurrences in a weekend
soak once we replaced the crash with a guard + diagnostic).
Suggested fix
- In
_ux_host_class_cdc_ecm_activate(): pre-allocate
ux_host_class_cdc_ecm_xmit_buffer (size
UX_HOST_CLASS_CDC_ECM_NX_PAYLOAD_SIZE) when the instance goes live, so
the ISR-context callback never needs an allocation. (This is the fix we
are running.)
- In
_ux_host_class_cdc_ecm_transmission_callback(): check
packet_header != UX_NULL before extracting/arming; drop or defer the
packet otherwise rather than submitting a NULL-buffer transfer.
- Consider the deactivate ordering so the buffer cannot be freed while a
completion callback may still run (e.g. free only after the bulk-OUT
endpoint's transfer is aborted and the HCD confirms no completion is
pending).
We are happy to provide the full analysis trail (fault dumps, the
diagnostic-guard captures that isolated the path, and the two-line patches
we are running) if useful.
Description
With
UX_HOST_CLASS_CDC_ECM_PACKET_CHAIN_SUPPORTenabled (it is definedunconditionally in
ux_host_class_cdc_ecm.h), the CDC-ECM host classmishandles chained NX packets on the transmit queue path:
_ux_host_class_cdc_ecm_write()copies a chained packet intoux_host_class_cdc_ecm_xmit_buffer, allocating that buffer lazily — butonly on its immediate-arm path (transmit queue empty):
When a transfer is already in flight,
write()just appends the packet toux_host_class_cdc_ecm_xmit_queue_*— no allocation, no copy. The queuedpacket is later armed by
_ux_host_class_cdc_ecm_transmission_callback(),which runs in the transfer-completion context — on the STM32 host
controller (and any HCD that completes URBs from its interrupt handler),
that is inside the USB host ISR — and uses the buffer with no NULL check
and no ability to allocate:
So if the first chained packet the endpoint ever sees is one that got
queued (routine inside a multi-segment TCP burst: segment 2 is written
while segment 1's transfer is still in flight), the callback:
nx_packet_data_extract_offset()with a NULL destination, andux_transfer_request_data_pointeris NULL.On the STM32 OTG_FS host controller driver (no DMA), that NULL propagates
through
HAL_HCD_HC_SubmitRequest→USB_HC_StartXfer→USB_WritePacket, which reads address 0 inside the OTG_FS interrupthandler. On a TrustZone-enabled Cortex-M33 (STM32U575, address 0 is
SAU-secure) this is an immediate secure-escalated HardFault; on non-TZ parts
it silently transmits memory from address 0.
A related hazard in the same machinery:
_ux_host_class_cdc_ecm_deactivate()frees
ux_host_class_cdc_ecm_xmit_bufferwhile transfers are being aborted;a completion callback racing deactivation can extract into the freed buffer
(heap corruption). We observed both signatures on hardware: four
hard faults over two days (three clean NULL reads, one delayed memory-
corruption crash during a device-initiated disconnect).
Reproduction conditions
UX_HOST_CLASS_CDC_ECM_PACKET_CHAIN_SUPPORTenabled (the default), ThreadX/NetX Duo integration.
1020 bytes vs the 1514-byte frames NetX builds for the ECM interface's
advertised 1500 MTU), so full-MSS TCP segments are chained packets.
packet) — the second and later segments of a burst queue behind the first,
and the completion callback arms them.
(
ux_host_class_cdc_ecm_transmission_callback.cis functionally unchangedon master as of March 2026), STM32
usbx_stm32_host_controllersglue,Telit LE910C1 CDC-ECM device. Deterministically reproducible: with email
bodies above one MSS, every session hit it (141 occurrences in a weekend
soak once we replaced the crash with a guard + diagnostic).
Suggested fix
_ux_host_class_cdc_ecm_activate(): pre-allocateux_host_class_cdc_ecm_xmit_buffer(sizeUX_HOST_CLASS_CDC_ECM_NX_PAYLOAD_SIZE) when the instance goes live, sothe ISR-context callback never needs an allocation. (This is the fix we
are running.)
_ux_host_class_cdc_ecm_transmission_callback(): checkpacket_header != UX_NULLbefore extracting/arming; drop or defer thepacket otherwise rather than submitting a NULL-buffer transfer.
completion callback may still run (e.g. free only after the bulk-OUT
endpoint's transfer is aborted and the HCD confirms no completion is
pending).
We are happy to provide the full analysis trail (fault dumps, the
diagnostic-guard captures that isolated the path, and the two-line patches
we are running) if useful.