HomeAbout UsContact Us

Fixing ThreadX Queue Stack Corruption on STM32

By Jithin Tom
Published in Embedded OS
November 13, 2026
7 min read
Fixing ThreadX Queue Stack Corruption on STM32

Table Of Contents

01
Why This Bug Evades Standard Testing
02
Problem Statement
03
Root Cause Analysis
04
Solution Approaches
05
Complete Working Example
06
Verification and Testing
07
Summary
08
Related Reading
09
References
10
Frequently Asked Questions

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.

Why This Bug Evades Standard Testing

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.

Problem Statement

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:

  • “ThreadX queue stack corruption hard fault STM32”
  • “tx_queue_receive return address corrupted”
  • “ThreadX message_size parameter correct value”
  • “Azure RTOS queue hard fault debug build only”

Why This Bug Is Particularly Dangerous

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.

Root Cause Analysis

The API Contract

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 Bug Pattern

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.

How the Copy Macro Works Internally

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:

  • Iteration 1 (z=4): *(d)++ = *(s)++; copies Word 0 (bytes 0-3) into fx_media.
  • Iteration 2 (z=3): *(d)++ = *(s)++; writes Word 1 (bytes 4-7) into destination + 4.
  • Iteration 3 (z=2): *(d)++ = *(s)++; writes Word 2 (bytes 8-11) into destination + 8.
  • Iteration 4 (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.

Memory Layout During Corruption

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).

Why Debug Builds Fail, Release Builds Survive

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:

  • Inline tx_queue_receive into the thread entry function
  • Reorder local variables to minimize stack usage
  • Eliminate the frame pointer entirely (using r7 as a general-purpose register)
  • Pack fx_media into a register instead of stack memory

When 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.

Solution Approaches

/* 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.

Fix 2: Compact Event Structure by Value (When Passing Data)

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.

Fix 3: Wrapper Macro for Type Sizing

#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.

Fix 4: Compile-Time Assertion Macro (Strongest Defense)

Add a static assertion macro that validates three critical invariants at compile time:

  1. The message type’s size is a strict multiple of sizeof(ULONG) (4 bytes), preventing truncation.
  2. The word count is within ThreadX’s architectural limit of 1 to 16 ULONGs.
  3. The backing memory buffer is large enough for the requested depth.
#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.

Complete Working Example

Queue Creation

/* 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);
}

Sender (ISR Context)

/* 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");
}
}

Receiver (Thread Context)

/* 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);
}
}
}

Stack Frame Layout and Overwrite Flow

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] |
+---------------------------------------------------------------------------------------+ <-- SP
LOW 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) |
+----------------------+----------------------+----------------------+----------------------+

Verification and Testing

1. HardFault Forensics

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>).

2. Map File and Disassembly Analysis

# Locate the source code line for the stacked return address or faulting PC
arm-none-eabi-addr2line -e build/app.elf -a 0x0801A3BB
# Check the disassembly around the call site to inspect stack frame layout
arm-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.

3. Stack Watermark Monitoring

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);
}
}
}

4. Static Analysis Rule

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));

5. Runtime Queue Copy Verification

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

6. Trace-Based Verification (Percepio Tracealyzer)

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:

  • Message size field matches MSG_SIZE_WORDS
  • Copy duration is consistent (no partial copies)
  • No queue overrun events (TX_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.

Summary

SymptomLikely CauseFix
HardFault in _tx_thread_system_resume or function returnmessage_size in bytes, not ULONGsUse TX_1_ULONG or sizeof(ptr)/sizeof(ULONG)
Debug builds crash, release builds surviveStack frame layout HeisenbugFix API contract; enforce _Static_assert guards
Return address / LR corrupted on stackQueue copies 4x declared word countCorrect message_size parameter to 32-bit words
Queue prematurely full / messages dropped in ISRMemory sized for 1-word messages but consumed in 4-word unitsAllocate 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.

Why This Matters Beyond ThreadX

This class of bug — API parameter unit mismatch — appears across embedded systems:

  • FreeRTOS vs ThreadX: FreeRTOS 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.
  • DMA transfer counts: Some HALs expect byte counts, others 16-bit halfwords or 32-bit words. A factor-of-4 error here causes memory corruption or incomplete transfers.
  • Flash programming: STM32 flash programming functions may expect 32-bit words or bytes depending on the driver layer.
  • Network stack MTU: Some stacks configure buffer pools in bytes, others in 32-bit words or cache lines.

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.

Prevention Checklist

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)
  • Queue memory size = message_size * sizeof(ULONG) * depth (verified by TX_QUEUE_CREATE_SAFE)
  • Sender passes &pointer not pointer for pointer queues
  • Receiver declares TYPE *var = NULL and passes &var to tx_queue_receive
  • ISR context uses TX_NO_WAIT; thread context uses TX_WAIT_FOREVER
  • Stack watermark monitoring and tx_thread_stack_error_notify enabled in test builds
  • HardFault handler logs stacked PC/LR and CFSR for forensics

With 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.

References

  1. Microsoft, “Azure RTOS ThreadX User Guide”, Chapter 4: Message Queues, tx_queue_create parameter specification. https://learn.microsoft.com/en-us/azure/rtos/threadx/chapter4#tx_queue_create
  2. Eclipse ThreadX, “ThreadX API Reference - tx_queue_create”, Message size unit definition (1-16 ULONGs). https://github.com/eclipse-threadx/rtos-docs/blob/main/rtos-docs/threadx/chapter4.md
  3. STMicroelectronics, “X-CUBE-AZRTOS-H7 USBX Host MSC Example”, ux_host_msc.c queue usage pattern. https://github.com/STMicroelectronics/x-cube-azrtos-h7/tree/main/Projects/STM32H735G-DK/Applications/USBX/Ux_Host_MSC
  4. ARM, “Cortex-M Hard Fault Debugging Guide”, HardFault handler implementation and stack frame analysis. https://developer.arm.com/documentation/100067/0618/
  5. FreeRTOS, “Queue API Reference - xQueueCreate”, Item size parameter in bytes (contrast with ThreadX). https://www.freertos.org/Documentation/02-Kernel/04-API-references/06-Queues/00-QueueManagement
  6. Percepio, “Tracealyzer for ThreadX - Queue Analysis”, Visualizing queue copy operations and stack usage. https://percepio.com/tracealyzer/threadx/

Frequently Asked Questions

Why does ThreadX queue cause stack corruption on STM32?

ThreadX queues expect message_size in 32-bit words (ULONGs), not bytes. Passing sizeof(pointer) instead of sizeof(pointer)/sizeof(ULONG) (or TX_1_ULONG) makes the queue copy 4x more data than allocated, overwriting adjacent stack variables, stack canaries, or the thread's return address.

What is the correct message_size for a pointer queue in ThreadX?

For a queue holding pointers, use message_size = TX_1_ULONG (or sizeof(void*) / sizeof(ULONG)), which equals 1 on 32-bit ARM Cortex-M. The total queue buffer size in bytes is message_size * sizeof(ULONG) * queue_depth.

How do you debug a ThreadX hard fault during queue operations?

Inspect the stacked PC and LR in the HardFault handler, correlate with the application .map and .elf files using arm-none-eabi-addr2line, then audit tx_queue_create call sites to verify that message_size is specified in ULONGs (1-16) rather than bytes.

Why does the corruption only appear with TX_WAIT_FOREVER?

When a thread waits with TX_WAIT_FOREVER on an empty queue, it suspends and stores its stack destination pointer in its thread control block. When tx_queue_send runs, it copies message_size words directly into that suspended stack frame. An oversized copy corrupts the frame while the thread is asleep, crashing as soon as the thread resumes.

Can this issue affect other RTOS queue implementations?

Yes. Any RTOS where queue message size is specified in words rather than bytes (or vice versa) is vulnerable to unit mismatches. FreeRTOS xQueueCreate expects item size in bytes, whereas ThreadX tx_queue_create expects 32-bit words (1-16 ULONGs).

Tags

threadxazure-rtosstm32queuestack-corruptionhard-faultdebugging

Share


Previous Article
Fixing Embedded C Code That Breaks With -O2 Optimization
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Zephyr Dynamic Memory Allocation Failures: Debugging
Zephyr Dynamic Memory Allocation Failures: Debugging
September 29, 2026
5 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media