
ThreadX message queues are the primary inter-thread communication mechanism in Azure RTOS. A subtle API contract violation in tx_queue_create — specifying message_size in bytes instead of 32-bit words — causes silent stack corruption that manifests as a HardFault when the corrupted thread resumes. This article walks through the failure mode, root cause analysis via HardFault forensics, the one-line fix, and verification steps to prevent regression.
This bug is particularly dangerous because it evades standard testing methodologies:
Unit tests miss it — They typically mock tx_queue_create or test queue logic in isolation with correctly-sized buffers. The buffer overflow only occurs when the real ThreadX library copies into a real thread’s stack frame.
Integration tests miss it — They often run with debug builds (-Og) where the stack layout accidentally protects the return address. The bug only triggers in release builds with specific stack layouts.
Static analysis misses it — The parameter sizeof(FX_MEDIA*) is a valid compile-time constant. No type mismatch exists — the function expects ULONG and receives ULONG. The bug is semantic (wrong unit), not syntactic.
Code review misses it — The line sizeof(FX_MEDIA*) looks correct to reviewers unfamiliar with ThreadX’s word-based convention. It’s an expert-blindness trap: experienced FreeRTOS developers expect byte-sized parameters and never question the unit.
The only reliable detection is HardFault forensics combined with API contract review — which is exactly what this article provides.
An engineer integrates ThreadX on an STM32H7 using ST’s X-CUBE-AZRTOS-H7 package. A worker thread calls tx_queue_receive(&queue, &msg_ptr, TX_WAIT_FOREVER) to wait for USB MSC media insertion events. The sender calls tx_queue_send(&queue, &media, TX_NO_WAIT) from the USB interrupt context. The system runs for minutes, then crashes in HardFault_Handler with no clear trigger. Debug builds fail; release builds appear stable. Single-stepping shows the fault occurs at _tx_thread_system_resume inside tx_queue_send.
Search queries this article answers:
This bug is insidious for three reasons. First, it manifests as a Heisenbug — the act of debugging changes the optimization level and stack layout, causing the symptom to disappear. Second, the crash occurs far from the root cause: the fault triggers when the thread resumes, not when the queue is created with the wrong parameter. Third, the API surface looks correct at a glance: sizeof(FX_MEDIA*) is a perfectly reasonable size to pass, and the compiler accepts it without warning because the function signature takes ULONG message_size with no unit annotation.
Engineers unfamiliar with ThreadX’s word-based sizing convention will spend days chasing phantom hardware issues — checking power supplies, probing JTAG connections, verifying flash wait states — before realizing the bug is a single integer off by a factor of four.
ThreadX documentation states:
message_size — Specifies the size of each message in the queue. Message sizes range from 1 to 16 32-bit words. Valid values are 1 through 16 inclusive.
The unit is 32-bit words (ULONGs), not bytes. This is explicitly called out in the Microsoft Azure RTOS ThreadX User Guide (Chapter 4, tx_queue_create).
The ST example code (and many derivatives) contains:
/* INCORRECT: message_size in bytes */tx_queue_create(&ux_app_MsgQueue_msc, "MSC Queue",sizeof(FX_MEDIA*), /* <-- BUG: 4 on 32-bit ARM */queue_memory,sizeof(queue_memory));
On Cortex-M (32-bit), sizeof(FX_MEDIA*) = 4 bytes. ThreadX interprets this as 4 ULONGs = 16 bytes. The queue’s internal copy macro TX_QUEUE_MESSAGE_COPY copies 16 bytes per message into a destination buffer sized for 4 bytes.
ThreadX implements queue message copying via the TX_QUEUE_MESSAGE_COPY macro (defined in common/inc/tx_queue.h). In both _tx_queue_send.c and _tx_queue_receive.c, source and destination pointers are cast to ULONG * (32-bit word pointers):
/* Official ThreadX TX_QUEUE_MESSAGE_COPY macro (tx_queue.h) */#ifndef TX_QUEUE_MESSAGE_COPY#define TX_QUEUE_MESSAGE_COPY(s, d, z) \*(d)++ = *(s)++; \if ((z) > ((UINT) 1)) \{ \while (--(z)) \{ \*(d)++ = *(s)++; \} \}#endif
Here, s and d are pointers of type ULONG *, and z is the queue’s message_size in 32-bit words. When message_size = 4, z = 4. The loop performs four 32-bit assignments:
z=4): *(d)++ = *(s)++; copies Word 0 (bytes 0-3) into fx_media.z=3): *(d)++ = *(s)++; writes Word 1 (bytes 4-7) into destination + 4.z=2): *(d)++ = *(s)++; writes Word 2 (bytes 8-11) into destination + 8.z=1): *(d)++ = *(s)++; writes Word 3 (bytes 12-15) into destination + 12.Because the destination buffer (&fx_media in the receiver’s stack frame) is only a single 4-byte pointer, the trailing 12 bytes overflow into whatever sits immediately above fx_media in memory.
In ARM Cortex-M architecture, the stack is full descending (it grows downward from high memory addresses to low memory addresses). The stack pointer (SP) points to the lowest valid address of the active frame. The function’s saved Link Register (LR / return address) and callee-saved registers (R4-R11) sit at higher addresses than local variables:
HIGH MEMORY ADDRESS (Stack Base)+------------------------------------------+| Previous Stack Frame / Caller Variables |+------------------------------------------+| Saved Link Register (LR / Return Address)| <-- Overwritten by bytes 12-15 (Word 3)+------------------------------------------+| Saved Frame / Callee Registers (R4-R11) | <-- Overwritten by bytes 4-11 (Words 1-2)+------------------------------------------+| Local Variables / Compiler Stack Canary || fx_media (4 bytes) | <-- Destination buffer (Word 0 / Bytes 0-3)+------------------------------------------+ <-- Active Stack Pointer (SP)LOW MEMORY ADDRESS (Stack Limit)
Because C array indexing and pointer post-increments (*(d)++) advance toward higher memory addresses, writing 16 bytes starting at &fx_media climbs directly up into the saved registers and saved return address.
When tx_queue_send executes from the USB ISR, it copies 16 bytes from the ISR stack into the suspended thread’s stack frame. When ThreadX readies and schedules the thread, tx_queue_receive completes. As soon as the receiving function attempts to return (or when the thread unwinds its frame via POP {r4-r11, pc} or BX LR), the corrupted return address is loaded into PC, triggering a HardFault (typically INVSTATE or PRECISERR).
Debug builds (-Og -g) preserve frame pointers and stack layout. The return address sits at a predictable offset from fx_media. Release builds (-O2 -Os) may inline tx_queue_receive, eliminate the frame, or reorder locals — sometimes moving fx_media away from the return address, masking the bug. This Heisenbug behavior is a hallmark of stack corruption.
The Heisenbug effect is compounded by the compiler’s stack frame optimization. At -O2, the compiler may:
tx_queue_receive into the thread entry functionr7 as a general-purpose register)fx_media into a register instead of stack memoryWhen fx_media lives in a register, the queue copy overwrites unrelated stack memory (or nothing observable), and the bug becomes dormant. This is why “it works in release” is a dangerous assumption — the bug is still there, just not triggered by the current code layout.
/* CORRECT: message_size specified in 32-bit words (ULONGs) *//* Option A: Use ThreadX predefined constant for single-pointer queues */#define MSG_SIZE_WORDS TX_1_ULONG/* Option B: Explicit portable word calculation */#define MSG_SIZE_WORDS ((ULONG)(sizeof(FX_MEDIA*) / sizeof(ULONG))) /* 1 on 32-bit ARM */tx_queue_create(&ux_app_MsgQueue_msc, "MSC Queue",MSG_SIZE_WORDS,queue_memory,sizeof(queue_memory));
Trade-off: Requires understanding ThreadX’s word-based sizing. One-time fix, zero runtime cost.
ThreadX imposes a hard limit on queue message sizes: message_size must be between 1 and 16 32-bit words (TX_1_ULONG to TX_16_ULONG, or 4 to 64 bytes). This restriction prevents unbounded interrupt latency during the kernel’s critical-section message copy.
Attempting to queue a massive control block like FX_MEDIA (which spans hundreds of bytes) by value will trigger a TX_SIZE_ERROR (0x10) from tx_queue_create. However, you can queue compact composite event structures by value:
/* Queue a compact event descriptor by value (<= 16 ULONGs) */typedef struct {uint32_t event_id; /* Event type */uint32_t status; /* Event status / error code */FX_MEDIA *media_ptr; /* Pointer to media instance */} MSC_QUEUE_EVENT; /* 12 bytes = 3 ULONGs (valid: <= 16 ULONGs) */#define MSC_EVENT_WORDS ((ULONG)(sizeof(MSC_QUEUE_EVENT) / sizeof(ULONG)))tx_queue_create(&ux_app_MsgQueue_msc, "MSC Event Queue",MSC_EVENT_WORDS,queue_memory,sizeof(queue_memory));
Trade-off: Copies 12 bytes atomically per message without heap or pool allocation. For payloads exceeding 64 bytes, you must pass pointers to buffers allocated from a TX_BLOCK_POOL or static pool.
#define TX_QUEUE_MSG_WORDS(type) ((ULONG)(sizeof(type) / sizeof(ULONG)))tx_queue_create(&queue, "Name",TX_QUEUE_MSG_WORDS(FX_MEDIA*),mem, sizeof(mem));
Trade-off: Prevents developers from mistakenly passing raw sizeof(type) byte values. Recommended for shared team codebases.
Add a static assertion macro that validates three critical invariants at compile time:
sizeof(ULONG) (4 bytes), preventing truncation.ULONGs.#define TX_QUEUE_CREATE_SAFE(queue, name, type, depth, mem, mem_size) \do { \_Static_assert((sizeof(type) % sizeof(ULONG)) == 0, \"Queue message type size must be an exact multiple of sizeof(ULONG)"); \_Static_assert(((sizeof(type) / sizeof(ULONG)) >= 1) && \((sizeof(type) / sizeof(ULONG)) <= 16), \"ThreadX message_size must be between 1 and 16 ULONGs (TX_1_ULONG to TX_16_ULONG)"); \_Static_assert((mem_size) >= (sizeof(type) * (depth)), \"Backing queue memory buffer is smaller than requested message capacity"); \tx_queue_create((queue), (name), \TX_QUEUE_MSG_WORDS(type), \(mem), (mem_size)); \} while (0)
This prevents unit confusion, size truncation, and capacity miscalculations before the firmware ever compiles.
/* app_threadx_config.h */#ifndef APP_THREADX_CONFIG_H#define APP_THREADX_CONFIG_H#include "tx_api.h"#include "fx_api.h"/* Queue for USB MSC media pointers */#define MSC_QUEUE_MSG_WORDS ((ULONG)(sizeof(FX_MEDIA*) / sizeof(ULONG)))#define MSC_QUEUE_DEPTH 10#define MSC_QUEUE_MEM_SIZE (MSC_QUEUE_MSG_WORDS * sizeof(ULONG) * MSC_QUEUE_DEPTH)extern TX_QUEUE ux_app_MsgQueue_msc;extern UCHAR ux_app_MsgQueue_msc_memory[MSC_QUEUE_MEM_SIZE];UINT app_threadx_queue_init(void);#endif
/* app_threadx_config.c */#include "app_threadx_config.h"#include "fx_api.h"TX_QUEUE ux_app_MsgQueue_msc;UCHAR ux_app_MsgQueue_msc_memory[MSC_QUEUE_MEM_SIZE];UINT app_threadx_queue_init(void){return tx_queue_create(&ux_app_MsgQueue_msc,"MSC Media Queue",MSC_QUEUE_MSG_WORDS, /* 1 ULONG per message */ux_app_MsgQueue_msc_memory,MSC_QUEUE_MEM_SIZE);}
/* usb_host_msc.c - called from USB interrupt */VOID usb_host_msc_media_event(FX_MEDIA *media){UINT status = tx_queue_send(&ux_app_MsgQueue_msc,&media, /* Pointer to pointer */TX_NO_WAIT); /* ISR must use NO_WAIT */if (status != TX_SUCCESS) {/* Queue full -- log, drop, or handle backpressure */trace_printf("MSC queue full, media event dropped\n");}}
/* msc_process_thread.c */VOID msc_process_thread_entry(ULONG arg){(VOID)arg;FX_MEDIA *fx_media = TX_NULL;UINT status;while (1) {status = tx_queue_receive(&ux_app_MsgQueue_msc,&fx_media, /* Receives pointer */TX_WAIT_FOREVER);if (status == TX_SUCCESS && fx_media != TX_NULL) {process_media(fx_media);}}}
HIGH MEMORY ADDRESS (Stack Base / Caller Frame)+---------------------------------------------------------------------------------------+| Calling Function Stack Frame / Saved Context |+---------------------------------------------------------------------------------------+| Saved Link Register (LR / Return Address to Caller) || [CLOBBERED by Word 3 / Bytes 12-15 if message_size = 4 ULONGs] |+---------------------------------------------------------------------------------------+| Callee-Saved Registers (R4-R11 / Frame Pointer) || [CLOBBERED by Words 1-2 / Bytes 4-11 if message_size = 4 ULONGs] |+---------------------------------------------------------------------------------------+| Local Variables & Compiler Stack Protector Canary |+---------------------------------------------------------------------------------------+| Destination Buffer: FX_MEDIA *fx_media (4 bytes / 1 ULONG) || [Valid Target: receives Word 0 / Bytes 0-3] |+---------------------------------------------------------------------------------------+ <-- SPLOW MEMORY ADDRESS (Stack Limit)CORRUPTION BYTE STREAM (When message_size = 4 words = 16 bytes):tx_queue_send copies 16 bytes starting at &fx_media:+----------------------+----------------------+----------------------+----------------------+| Word 0 (Bytes 0-3) | Word 1 (Bytes 4-7) | Word 2 (Bytes 8-11) | Word 3 (Bytes 12-15) |+----------------------+----------------------+----------------------+----------------------+| Valid Media Pointer | Local Var / Canary | Saved Register | Saved LR / Ret Addr || Target: &fx_media | (OVERWRITTEN) | (OVERWRITTEN) | (CORRUPTED) |+----------------------+----------------------+----------------------+----------------------+
When the HardFault triggers upon thread resumption or function return:
/* HardFault_Handler.c - Cortex-M naked assembly fault dispatcher */__attribute__((naked)) void HardFault_Handler(void){__asm volatile("tst lr, #4\n" /* Check bit 2 of EXC_RETURN to select SP */"ite eq\n""mrseq r0, msp\n" /* r0 = Main Stack Pointer (MSP) */"mrsne r0, psp\n" /* r0 = Process Stack Pointer (PSP) */"b hard_fault_handler_c\n" /* Pass stacked frame pointer to C handler */);}void hard_fault_handler_c(uint32_t *stack){uint32_t r0 = stack[0];uint32_t r1 = stack[1];uint32_t r2 = stack[2];uint32_t r3 = stack[3];uint32_t r12 = stack[4];uint32_t lr = stack[5]; /* Link register at fault */uint32_t pc = stack[6]; /* Program counter / corrupted return address */uint32_t psr = stack[7]; /* Program status register */trace_printf("HARD FAULT: PC=0x%08lX LR=0x%08lX PSR=0x%08lX\n"" R0=0x%08lX R1=0x%08lX R2=0x%08lX R3=0x%08lX R12=0x%08lX\n",pc, lr, psr, r0, r1, r2, r3, r12);/* Break into debugger or halt */__disable_irq();while (1) { __WFI(); }}
Correlate PC and LR with build/app.map (or .elf via arm-none-eabi-addr2line -e build/app.elf -a <PC>).
# Locate the source code line for the stacked return address or faulting PCarm-none-eabi-addr2line -e build/app.elf -a 0x0801A3BB# Check the disassembly around the call site to inspect stack frame layoutarm-none-eabi-objdump -d -S build/app.elf > build/app.asm
When a thread crashes inside _tx_thread_system_resume during tx_queue_send, check whether the fault was caused by a corrupted thread control block pointer or an immediate context switch to a resumed thread whose stack pointer / return address was demolished by the oversized copy.
Enable ThreadX’s native stack checking and stack fill in your configuration:
/* In tx_user.h */#define TX_ENABLE_STACK_CHECKING/* In application initialization */void app_stack_error_handler(TX_THREAD *thread_ptr){trace_printf("FATAL: ThreadX stack overflow/corruption detected in thread '%s'!\n",thread_ptr->tx_thread_name);__disable_irq();while (1) { __WFI(); }}void app_threadx_init(void){/* Register ThreadX stack error callback */tx_thread_stack_error_notify(app_stack_error_handler);}/* Dedicated background stack watermark monitor thread */VOID stack_monitor_thread_entry(ULONG arg){(VOID)arg;TX_THREAD *thread;while (1) {tx_thread_sleep(100); /* Sample every 1 second at 100 Hz kernel tick */thread = _tx_thread_created_ptr;if (thread != TX_NULL) {do {/* Calculate minimum free stack headroom reached (highest watermark) */ULONG free_bytes = (ULONG)((uintptr_t)thread->tx_thread_stack_highest_ptr -(uintptr_t)thread->tx_thread_stack_start);if (free_bytes < 128u) {trace_printf("WARN: Thread '%s' stack watermark low: %lu bytes free\n",thread->tx_thread_name, free_bytes);}thread = thread->tx_thread_created_next;} while (thread != _tx_thread_created_ptr && thread != TX_NULL);}}}
Enforce safe queue initialization across the entire codebase with the TX_QUEUE_CREATE_SAFE macro:
/* In app_threadx_queue_util.h */#define TX_QUEUE_MSG_WORDS(type) ((ULONG)(sizeof(type) / sizeof(ULONG)))#define TX_QUEUE_CREATE_SAFE(queue, name, type, depth, mem, mem_size) \do { \_Static_assert((sizeof(type) % sizeof(ULONG)) == 0, \"Queue message type size must be an exact multiple of 4 bytes"); \_Static_assert(((sizeof(type) / sizeof(ULONG)) >= 1) && \((sizeof(type) / sizeof(ULONG)) <= 16), \"ThreadX message_size must be between 1 and 16 ULONGs"); \_Static_assert((mem_size) >= (sizeof(type) * (depth)), \"Queue memory buffer is smaller than requested message capacity"); \tx_queue_create((queue), (name), \TX_QUEUE_MSG_WORDS(type), \(mem), (mem_size)); \} while (0)/* Usage: Validates message size and buffer boundaries at compile-time */TX_QUEUE_CREATE_SAFE(&ux_app_MsgQueue_msc, "MSC Queue",FX_MEDIA*, 10,ux_app_MsgQueue_msc_memory,sizeof(ux_app_MsgQueue_msc_memory));
For safety-critical builds, define a receiving wrapper that checks that the queue’s configured message size exactly matches the expected word count of the destination variable:
#ifdef DEBUG#define TX_QUEUE_RECEIVE_VERIFIED(queue_ptr, dest_ptr, type, wait_option) \do { \_Static_assert((sizeof(type) % sizeof(ULONG)) == 0, \"Destination type size must be a multiple of 4 bytes"); \if ((queue_ptr)->tx_queue_message_size != TX_QUEUE_MSG_WORDS(type)) { \trace_printf("CRITICAL: Queue '%s' word size mismatch! Expected %lu words, queue has %lu words\n", \(queue_ptr)->tx_queue_name, \TX_QUEUE_MSG_WORDS(type), \(queue_ptr)->tx_queue_message_size); \__BKPT(0); \} \tx_queue_receive((queue_ptr), (dest_ptr), (wait_option)); \} while (0)#else#define TX_QUEUE_RECEIVE_VERIFIED(queue_ptr, dest_ptr, type, wait_option) \tx_queue_receive((queue_ptr), (dest_ptr), (wait_option))#endif
If using Percepio Tracealyzer for ThreadX, enable queue event tracing:
/* In tx_user.h */#define TX_TRACE_ENABLE#define TX_TRACE_OBJECT_QUEUE_ENABLE
In Tracealyzer, filter for tx_queue_send and tx_queue_receive events. Verify:
MSG_SIZE_WORDSTX_QUEUE_FULL)The trace viewer’s “Queue” tab shows each message as a colored block — oversized copies appear as blocks extending beyond the queue’s visual boundary.
| Symptom | Likely Cause | Fix |
|---|---|---|
HardFault in _tx_thread_system_resume or function return | message_size in bytes, not ULONGs | Use TX_1_ULONG or sizeof(ptr)/sizeof(ULONG) |
| Debug builds crash, release builds survive | Stack frame layout Heisenbug | Fix API contract; enforce _Static_assert guards |
| Return address / LR corrupted on stack | Queue copies 4x declared word count | Correct message_size parameter to 32-bit words |
| Queue prematurely full / messages dropped in ISR | Memory sized for 1-word messages but consumed in 4-word units | Allocate buffer as words * sizeof(ULONG) * depth |
The root cause is an API unit mismatch: ThreadX queues expect message_size in 32-bit words (ULONGs, bounded between 1 and 16), but developers accustomed to byte-oriented RTOS APIs pass sizeof(pointer) in bytes. On 32-bit ARM Cortex-M, this is off by a factor of 4 — the queue copies 16 bytes into a 4-byte destination, overwriting the suspended thread’s stack variables, frame registers, and saved return address.
The fix is straightforward: change sizeof(FX_MEDIA*) to TX_1_ULONG (or sizeof(FX_MEDIA*) / sizeof(ULONG)). This guarantees message_size = 1 ULONG per pointer message, matching the destination buffer size exactly.
This class of bug — API parameter unit mismatch — appears across embedded systems:
xQueueCreate takes item size in bytes; ThreadX takes it in 32-bit words. Porting code between them without adjusting this parameter causes immediate stack corruption.The defense is consistent: encode units in the macro name (MSG_SIZE_WORDS, DMA_COUNT_BYTES, FLASH_SIZE_WORDS) and use compile-time assertions to validate memory sizes against message sizes.
Before merging any ThreadX queue creation:
message_size uses TX_1_ULONG or sizeof(type) / sizeof(ULONG) (never raw sizeof(type))message_size is verified to be between 1 and 16 ULONGs (TX_1_ULONG to TX_16_ULONG)message_size * sizeof(ULONG) * depth (verified by TX_QUEUE_CREATE_SAFE)&pointer not pointer for pointer queuesTYPE *var = NULL and passes &var to tx_queue_receiveTX_NO_WAIT; thread context uses TX_WAIT_FOREVERtx_thread_stack_error_notify enabled in test buildsWith these guards in place, the ThreadX queue stack corruption bug becomes impossible to introduce — and if it somehow slips through, the forensics toolkit in this article makes it trivial to diagnose.
tx_queue_create parameter specification. https://learn.microsoft.com/en-us/azure/rtos/threadx/chapter4#tx_queue_createux_host_msc.c queue usage pattern. https://github.com/STMicroelectronics/x-cube-azrtos-h7/tree/main/Projects/STM32H735G-DK/Applications/USBX/Ux_Host_MSCQuick Links
Legal Stuff





