
Thread stack overflow is a critical issue in real-time operating systems like Zephyr that can lead to unpredictable behavior, memory corruption, and system crashes. When a thread’s stack exceeds its allocated size, it overwrites adjacent memory regions, potentially corrupting other threads’ stacks, global variables, or kernel data structures. This article provides a comprehensive guide to detecting, diagnosing, and fixing stack overflow issues in Zephyr applications using runtime monitoring techniques.
In Zephyr, each thread is allocated a fixed-size stack area in memory. The stack is used for function call return addresses, local variables, and register saves during context switches. The kernel initializes the stack with a known pattern (0xaa) to enable stack usage monitoring.
A stack overflow occurs when:
Note: On architectures with a dedicated interrupt stack (e.g., ARM Cortex-M using MSP in Handler Mode), ISRs do not consume the thread’s stack. Zephyr configures a separate interrupt stack via
CONFIG_ISR_STACK_SIZE. However, on architectures without hardware-separated stacks, ISRs may share the current thread’s stack.
Zephyr provides several mechanisms to detect and prevent stack overflows, ranging from compile-time checks to runtime monitoring features.
Zephyr thread stacks grow downward from high memory addresses to low memory addresses. The stack base (highest address) is stored in the thread’s stack_info structure. The kernel tracks stack usage by checking the sentinel value at the stack end.
+------------------------------------------------------------------------+| ZEPHYR THREAD STACK MEMORY LAYOUT || Stack Grows Downward (High Addr -> Low Addr) |+------------------------------------------------------------------------+| HIGH ADDRESS || || Thread Stack Base || (Highest Address) || +------------------+ || | Thread Stack | || | Frame (RTOS) | || | k_thread struct | || +------------------+ || || +------------------+ || | Guard Region | || | (MPU / Sentinel) | || +------------------+ || || +------------------+ || | Thread Local | || | Variables | || | & Call Frames | || +------------------+ || || v STACK GROWS DOWN v || || +------------------+ || | (Stack Pointer) | || | Current SP --> | || +------------------+ || || Note: On ARM Cortex-M, ISRs use a separate || interrupt stack (MSP), not the thread stack. || || !!! !!! OVERFLOW ZONE !!! !!! || +------------------+ || | Overflow | || | corrupts | || | guard / adjacent | || | memory | || +------------------+ || || LOW ADDRESS || (Stack Limit) || || Detection: CONFIG_STACK_SENTINEL || checks guard pattern at stack end || Runtime: k_thread_stack_space_get() |+------------------------------------------------------------------------+
Insufficient Stack Size Allocation: The most common cause is allocating too little stack space for a thread’s actual needs. Zephyr’s K_THREAD_STACK_SIZEOF macro helps calculate required size, but underestimation is frequent.
Recursive Function Calls: Deep recursion, especially in algorithms like tree traversals or mathematical computations, can quickly consume stack space.
Large Local Variables: Declaring large arrays or structures as local variables (e.g., int buffer[1024];) allocates them on the stack, potentially causing overflow.
Interrupt Stack Sizing: On architectures without a dedicated interrupt stack, ISRs may share the thread’s stack. Even on architectures with a separate ISR stack (ARM Cortex-M), an undersized interrupt stack (CONFIG_ISR_STACK_SIZE) can cause a separate overflow in Handler Mode.
Function Pointer Indirection: Complex callback chains or event-driven architectures can lead to unpredictable call depths.
Zephyr’s stack sentinel feature places a known value (0xaaaaaaaa) at the end of the thread’s stack area. During context switches or stack inspections, the kernel verifies this sentinel value. If modified, it indicates a stack overflow.
To enable stack sentinel:
CONFIG_STACK_SENTINEL=y to your prj.confz_swap()), k_yield(), and when returning from non-nested interruptsWhen an overflow is detected, Zephyr triggers a fatal error with the reason K_ERR_STACK_CHK_FAIL, providing the thread name and fault context.
For more granular detection, Zephyr offers stack checking that verifies stack bounds on every function call. This uses either hardware features (like ARM’s MSPLIM/PSPLIM registers) or software instrumentation.
To enable stack checking:
CONFIG_STACK_CHECKING=y to your prj.confCONFIG_STACK_CHECKING uses software instrumentation (compiler support) and checks stack usage at runtime (e.g. during context switch). For zero-overhead hardware bounds checking on ARM Cortex-M, use CONFIG_HW_STACK_PROTECTION instead, which utilizes the MPU or MSPLIM/PSPLIM registers.Stack checking provides immediate detection at the point of overflow but incurs higher runtime overhead (typically 5-15% performance impact).
Zephyr provides runtime APIs to monitor actual stack usage without enabling full checking:
#include <zephyr/kernel.h>// Variable to store unused spacesize_t unused;// Get unused stack space (returns 0 on success)if (k_thread_stack_space_get(&thread_obj, &unused) == 0) {// Calculate actual usagesize_t usage = thread_obj.stack_info.size - unused;// Print stack usage informationprintk("Thread %s: %zu/%zu bytes used\n",k_thread_name_get(&thread_obj),usage,thread_obj.stack_info.size);}
These functions allow applications to log stack usage periodically or during critical operations to identify trends before overflow occurs.
The simplest solution is to increase the stack size when defining the thread:
#define MY_THREAD_STACK_SIZE 1024 // Increase from default 512 or 768K_THREAD_STACK_DEFINE(my_stack_area, MY_THREAD_STACK_SIZE);struct k_thread my_thread_data;k_thread_create(&my_thread_data, my_stack_area,K_THREAD_STACK_SIZEOF(my_stack_area),my_thread_func, NULL, NULL, NULL,MY_THREAD_PRIORITY, 0, K_NO_WAIT);
Trade-offs: Increases RAM consumption. For systems with limited memory, this may not be feasible.
Reduce stack consumption by:
Example: Moving a large buffer to global scope:
// Before: Local buffer on stackvoid process_data(void) {uint8_t buffer[512]; // Consumes 512 bytes of stack// ...}// After: Global bufferstatic uint8_t processing_buffer[512];void process_data(void) {// Use processing_buffer instead// No stack consumption for buffer}
Trade-offs: Increases global memory usage and may reduce reentrancy or thread safety if not carefully managed.
As mentioned, stack sentinel provides low-overhead detection. Combine it with application-level monitoring:
#include <zephyr/kernel.h>#include <zephyr/sys/printk.h>static void thread_stack_monitor_cb(const struct k_thread *thread, void *user_data){size_t unused;if (k_thread_stack_space_get(thread, &unused) == 0) {size_t total = thread->stack_info.size;size_t used = total - unused;if (used > total * 0.8) { // Warning at 80% usageprintk("WARNING: Thread %s stack usage: %zu/%zu (%.1f%%)\n",k_thread_name_get((struct k_thread *)thread), used, total,(float)used * 100.0f / total);}}}void stack_monitor_thread(void *arg1, void *arg2, void *arg3){while (1) {k_sleep(K_SECONDS(10)); // Check every 10 secondsk_thread_foreach(thread_stack_monitor_cb, NULL);}}
Trade-offs: Minimal performance impact (<1%) but only detects overflow at check intervals, not instantly.
On ARM Cortex-M7 and Cortex-M33 processors, Zephyr can use the Memory Protection Unit (MPU) to create a no-access guard region at the stack boundary. Any stack overflow triggers a MemManage fault.
To enable MPU-based stack protection:
CONFIG_HW_STACK_PROTECTION=y to prj.confTrade-offs: Requires MPU support and consumes one MPU region, but provides zero-overhead detection with immediate fault generation.
The following example demonstrates enabling stack sentinel, monitoring stack usage, and recovering from overflow conditions:
#include <zephyr/kernel.h>#include <zephyr/sys/printk.h>#include <zephyr/sys/reboot.h>#define STACK_SIZE 512#define MONITOR_INTERVAL K_SECONDS(5)K_THREAD_STACK_DEFINE(stack_area, STACK_SIZE);struct k_thread thread_data;/* Thread that intentionally causes stack overflow */void overflowing_thread(void *arg1, void *arg2, void *arg3){/* Large local array that will overflow the stack */uint8_t large_buffer[600]; /* Exceeds 512-byte stack *//* Use the buffer to prevent compiler optimization */for (int i = 0; i < 600; i++) {large_buffer[i] = (uint8_t)i;}/* This point may never be reached due to overflow */printk("Overflowing thread completed (should not happen)\n");}/* Monitor thread to check stack usage */void monitor_thread(void *arg1, void *arg2, void *arg3){while (1) {k_sleep(MONITOR_INTERVAL);size_t unused;if (k_thread_stack_space_get(&thread_data, &unused) == 0) {size_t total = STACK_SIZE;size_t used = total - unused;printk("Monitor: Target thread stack %zu/%zu bytes used\n",used, total);if (used > total * 0.75) {printk("WARNING: Approaching stack limit!\n");}}}}int main(void){/* Stack sentinel is enabled globally via CONFIG_STACK_SENTINEL=y in prj.conf *//* Create the monitoring thread */k_thread_create(&thread_data, stack_area,K_THREAD_STACK_SIZEOF(stack_area),monitor_thread, NULL, NULL, NULL,7, 0, K_NO_WAIT);/* Simulate work that might lead to overflow conditions */while (1) {k_sleep(K_SECONDS(1));/* Application logic here */}}
When stack sentinel detects an overflow, Zephyr triggers a fatal error (K_ERR_STACK_CHK_FAIL). To handle this gracefully, applications can:
k_sys_fatal_error_handler() to implement custom recovery logic (e.g., logging to persistent storage and performing a warm reboot via sys_reboot(SYS_REBOOT_WARM))CONFIG_DEBUG_COREDUMP) for post-mortem analysisTo verify your stack overflow detection mechanisms:
Intentional Overflow Test: Create a thread with a known small stack size and allocate a local buffer larger than the stack.
Check for Expected Fault: With stack sentinel enabled, the system should trigger a fatal error with stack check failure.
Validate Monitoring: Ensure your stack usage monitoring reports increasing usage before overflow.
Zephyr includes a built-in Thread Analyzer module that reports runtime thread stack usage. Enable it with CONFIG_THREAD_ANALYZER=y in your prj.conf. It provides APIs like thread_analyzer_run() and thread_analyzer_print() to output per-thread stack statistics.
To view stack usage at build time, use the compiler’s static stack usage analysis:
west build -t puncover
In production devices:
Thread stack overflow in Zephyr is a preventable issue through proper stack sizing, runtime monitoring, and Zephyr’s built-in detection features. Key takeaways:
CONFIG_STACK_SENTINEL for minimal-overhead runtime detection.k_thread_stack_space_get().By combining these techniques, developers can create robust Zephyr applications that gracefully handle stack constraints and maintain system reliability in embedded systems.
Quick Links
Legal Stuff





