Skip to content

CDC-ECM host class: transmission callback arms transfers from unchecked/unallocated buffers when UX_HOST_CLASS_CDC_ECM_PACKET_CHAIN_SUPPORT is enabled (NULL-pointer transfer submitted from ISR context) #272

Description

@halytech-dm

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:

  1. calls nx_packet_data_extract_offset() with a NULL destination, and
  2. 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_SubmitRequestUSB_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

  1. 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.)
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions