HomeAbout UsContact Us

AddressSanitizer for Embedded C: Finding Memory Bugs in Firmware

By Jithin Tom
Published in Embedded C/C++
August 28, 2026
8 min read
AddressSanitizer for Embedded C: Finding Memory Bugs in Firmware

Table Of Contents

01
AddressSanitizer for Embedded C: Finding Memory Bugs in Firmware
02
Frequently Asked Questions

AddressSanitizer for Embedded C: Finding Memory Bugs in Firmware

Memory bugs are among the most insidious issues in embedded firmware. A single out-of-bounds write can corrupt control flow, trigger silent data corruption, or cause intermittent crashes that are nearly impossible to reproduce. Traditional debugging techniques like logging or breakpoint stepping often fail to catch these bugs because they happen sporadically and may not manifest until specific timing conditions align.

AddressSanitizer (ASan) changes the game by providing compile-time instrumentation that detects memory errors as they occur. Originally developed for user-space applications, ASan has proven surprisingly effective in embedded environments when used with host-based testing, emulation, or even on-target with sufficient resources. This article explains how to integrate ASan into your embedded C workflow, understand its trade-offs, and leverage it to catch bugs early—before they become costly field failures.

Problem Statement: Why Memory Bugs Are Especially Dangerous in Embedded Systems

Embedded firmware often runs without memory protection units (MPUs) or operates in bare-metal environments where a single stray write can overwrite critical data structures, interrupt vectors, or stack frames. Unlike desktop applications that benefit from virtual memory and process isolation, embedded systems frequently share a single address space between code, data, and peripherals. This makes memory bugs particularly devastating:

  • Silent Corruption: A buffer overflow might modify a sensor reading or control flag without causing an immediate crash, leading to incorrect system behavior that’s hard to trace.
  • Intermittent Faults: Stack overflows may only occur under deep call chains or specific interrupt nesting patterns, making them elusive during testing.
  • Security Vulnerabilities: Memory corruption can be exploited to execute arbitrary code or bypass safety mechanisms, especially in connected devices.

Consider a typical scenario: a firmware engineer implements a circular buffer for UART reception. Under high traffic, the buffer occasionally overflows, writing past its end and corrupting a nearby task control block. The system might crash hours later with a hard fault, but the root cause remains hidden in the noise.

Traditional approaches like manual code reviews, static analysis, or dynamic testing with limited instrumentation often miss these issues. Static analyzers can produce false positives and miss complex heap interactions, while debugging with breakpoints alters timing and may prevent the bug from occurring.

Solution Overview: How AddressSanitizer Works

AddressSanitizer detects memory errors by instrumenting loads and stores in the compiled code and adding shadow memory that tracks the state of each application byte. At compile time, the compiler (GCC or Clang) inserts calls to ASan runtime functions before every memory access. These checks validate whether the accessed address is valid based on shadow memory state.

When a violation occurs—such as writing to a use-after-free region or overflowing a buffer—ASan prints a detailed diagnostic report that includes:

  • Error type (heap-use-after-free, stack-buffer-overflow, etc.)
  • Faulting address and its relation to nearby memory regions
  • Stack trace of the erroneous access
  • Stack trace(s) of the allocation and deallocation (if applicable)
  • Shadow byte representation showing which bytes are valid, redzone, freed, etc.

The instrumentation adds red zones (poisoned memory) around allocations and uses a shadow mapping scheme where each shadow byte represents the state of 8 application bytes. This design keeps overhead reasonable while catching a wide range of bugs.

Setting Up AddressSanitizer for Embedded C

To use ASan with embedded C toolchains, you need a compiler that supports the -fsanitize=address flag (GCC 4.8+ or Clang 3.1+). The steps are:

  1. Compile with -fsanitize=address: This enables instrumentation and links against the ASan runtime.
  2. Link with the ASan runtime library: Usually -lasan (provided by the compiler suite).
  3. Ensure runtime support: The ASan runtime requires dynamic memory allocation (malloc/free) for its internal structures. For bare-metal targets without an OS, you may need to provide minimal stubs or run under an emulator like QEMU.
  4. Run the instrumented binary: Execute the firmware on a host, in an emulator, or on-target if resources permit.

Example: Building for STM32 with GCC

Assume you’re using the GNU Arm Embedded Toolchain. A simple Makefile snippet:

CFLAGS += -O1 -g -fsanitize=address
LDFLAGS += -lasan
# Ensure we link with libc that provides malloc/free
LIBS += -lc -lm
all: firmware.elf
firmware.elf: main.o
$(CC) $(LDFLAGS) $^ -o $@ $(LIBS)
clean:
rm -f *.o *.elf

Note: Optimization level -O1 is recommended; higher levels may reduce ASan’s effectiveness due to aggressive inlining or dead code elimination that removes instrumentation.

Running on Host with POSIX Emulation

For many embedded projects, the easiest approach is to abstract hardware dependencies and run the core logic on a POSIX host (Linux/macOS) using mocked hardware layers. This lets you leverage ASan’s full power without worrying about target resource constraints.

Example structure:

src/
core.c # Hardware-independent logic
hal_stm32.c # STM32-specific hardware abstraction
hal_mock.c # Mock implementation for host tests
tests/
test_uart.c # Unit tests using mocked HAL

Compile the test binary with ASan and run it under Valgrind or directly—ASan will catch bugs in the core logic.

On-Target Considerations

Running ASan directly on a microcontroller is possible but comes with constraints:

  • Memory Overhead: ASan typically triples code size and doubles RAM usage due to shadow memory and red zones. For a Cortex-M0 with 32KB flash, this may be prohibitive.
  • Execution Speed: Expect 2x-3x slowdown due to instrumentation checks.
  • Runtime Requirements: The ASan runtime needs a heap and thread support. You can port it to bare-metal using newlib’s malloc or a simple embedded heap.

If resources are tight, consider using ASan only for unit tests or integration tests on a more capable host or emulator (e.g., QEMU modeling an STM32).

Interpreting ASan Reports

When ASan detects an error, it aborts the program and prints a diagnostic to stderr. Understanding this output is key to fixing bugs quickly.

Example: Heap-Use-After-Free

=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x60200000fed0 at pc 0x0000004005ef bp 0x7ffd8a3c7a50 sp 0x7ffd8a3c7a48
READ of size 4 at 0x60200000fed0 thread T0
#0 0x4005ee in process_packet src/core.c:42
#1 0x4007a1 in main src/main.c:28
#2 0x7f8a3c7b2b96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
#3 0x400389 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x20890)
0x60200000fed0 is located 0 bytes inside of 4-byte region [0x60200000fed0,0x60200000fed4)
freed by thread T0 here:
#0 0x400a3c in free (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x9c3cf)
#1 0x4008f1 in release_buffer src/core.c:58
#2 0x4005d5 in process_packet src/core.c:35
#3 0x4007a1 in main src/main.c:28
#4 0x7f8a3c7b2b96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
previously allocated by thread T0 here:
#0 0x4009b2 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x12c92)
#1 0x400881 in allocate_buffer src/core.c:50
#2 0x4005d5 in process_packet src/core.c:35
#3 0x4007a1 in main src/main.c:28
#4 0x7f8a3c7b2b96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
SUMMARY: AddressSanitizer: heap-use-after-free src/core.c:42 in process_packet
=================================================================

Breaking this down:

  • Error Type: heap-use-after-free — the program read memory after it was freed.
  • Faulting Address: 0x60200000fed0 — the exact byte accessed.
  • Access Stack Trace: Shows the read occurred in process_packet at core.c:42.
  • Deallocation Stack Trace: Shows where the memory was freed (release_buffer at core.c:58).
  • Allocation Stack Trace: Shows where the memory was originally allocated (allocate_buffer at core.c:50).
  • Shadow Byte Explanation: (not shown in this snippet but typically follows) helps identify why the address was considered poisoned.

Example: Stack-Buffer-Overflow

=================================================================
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd8a3c7a60 at pc 0x000000400621 bp 0x7ffd8a3c7a50 sp 0x7ffd8a3c7a48
WRITE of size 8 at 0x7ffd8a3c7a60 thread T0
#0 0x400620 in copy_data src/util.c:19
#1 0x4007a1 in main src/main.c:28
#2 0x7f8a3c7b2b96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
#3 0x400389 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x20890)
Address 0x7ffd8a3c7a60 is located in stack of thread T0 at offset 32 in frame
#0 0x400620 in copy_data src/util.c:19
This frame has 1 object(s):
[32, 40) 'local_buf' <== Memory access at offset 32 overflows the variable
HINT: this may be due to an incorrect memset parameters
SUMMARY: AddressSanitizer: stack-buffer-overflow src/util.c:19 in copy_data
=================================================================

Here, ASan identifies a write past the end of a stack array local_buf (expected size 8 bytes, accessed at offset 32). The hint points to a possible memset length error.

Practical Example: Debugging a UART Buffer Overflow in STM32 Firmware

Let’s walk through a real-world scenario where ASan catches a subtle bug in STM32 HAL-based firmware.

The Bug

A firmware module implements a double-buffered UART receiver to avoid overruns. Under heavy load, the system occasionally crashes with a hard fault. Manual debugging shows the fault occurs in an interrupt handler, but the stack looks corrupted.

Code Snippet (Before ASan)

#define UART_BUF_SIZE 256
static uint8_t rx_buffer[UART_BUF_SIZE];
static uint16_t rx_head = 0;
static uint16_t rx_tail = 0;
void UART_IRQHandler(void)
{
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {
uint8_t data = (uint8_t)(huart1.Instance->DR & 0xFF);
rx_buffer[rx_head] = data; // Potential overflow if not checked
rx_head = (rx_head + 1) % UART_BUF_SIZE;
}
}

The bug: missing bounds check before writing to rx_buffer. If interrupts fire faster than the main loop consumes data, rx_head can wrap and overwrite unconsumed data, or worse, write past the buffer end if modulo operation is missed.

Enabling ASan

We compile the firmware for host execution with a mocked HAL:

// hal_mock.c
#include "hal.h"
void UART_IRQHandler(void)
{
// Simulate interrupt by calling the same logic
uint8_t data = mock_uart_getchar();
rx_buffer[rx_head] = data; // ASan will catch overflow here
rx_head = (rx_head + 1) % UART_BUF_SIZE;
}

Running the test suite with ASan enabled produces:

=================================================================
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd8a3c7a60 at pc 0x000000400621 bp 0x7ffd8a3c7a50 sp 0x7ffd8a3c7a48
WRITE of size 1 at 0x7ffd8a3c7a60 thread T0
#0 0x400620 in UART_IRQHandler hal_mock.c:12
#1 0x4007a1 in test_uart_rx_overload tests/test_uart.c:45
#2 0x7f8a3c7b2b96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
#3 0x400389 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x20890)
Address 0x7ffd8a3c7a60 is located in stack of thread T0 at offset 32 in frame
#0 0x400620 in UART_IRQHandler hal_mock.c:12
This frame has 1 object(s):
[0, 256) 'rx_buffer' <== Memory access at offset 32 is outside [0,256)
SUMMARY: AddressSanitizer: stack-buffer-overflow hal_mock.c:12 in UART_IRQHandler

ASan pinpoints the exact line and shows that the write to rx_buffer is out of bounds. The fix is to add a check for buffer full condition:

void UART_IRQHandler(void)
{
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {
uint8_t data = (uint8_t)(huart1.Instance->DR & 0xFF);
uint16_t next_head = (rx_head + 1) % UART_BUF_SIZE;
if (next_head != rx_tail) { // Buffer not full
rx_buffer[rx_head] = data;
rx_head = next_head;
} else {
// Handle overflow: set flag or drop data
uart_overflow = true;
}
}
}

After applying the fix, ASan reports no errors, confirming the buffer bounds are respected.

Limitations and Trade-Offs in Embedded Contexts

While ASan is powerful, it’s not a panacea for embedded systems. Understanding its limitations helps you apply it effectively.

Resource Overhead

ASan’s instrumentation increases code size significantly—often 2x to 3x—because each memory access is wrapped with checks. The shadow memory adds another 1/8th of the application’s memory usage (since 1 shadow byte tracks 8 application bytes), plus red zones around allocations. For a typical embedded binary of 64KB flash, expect 150KB-200KB with ASan.

RAM usage also grows due to:

  • Shadow memory (application RAM / 8)
  • Red zones (typically 64 bytes per allocation on heap, 128 bytes around stack)
  • ASan runtime metadata

On a Cortex-M0 with 8KB RAM, this overhead may leave little room for actual application data.

Performance Impact

Each load/store instruction gains extra instructions for shadow memory lookup and red zone checks. Expect 2x-3x slowdown, which can affect real-time performance. This makes ASan unsuitable for timing-sensitive code paths unless you can isolate and test them separately.

Incompatibility with Bare-Metal and Memory-Mapped I/O

ASan intercepts all memory accesses, including those to peripherals. Accessing a memory-mapped I/O register may trigger a false positive if the address falls within a red zone or shadow region. Workarounds include:

  • Explicitly excluding memory-mapped regions via compiler flags (-fsanitize-address-use-after-scope doesn’t help; need to avoid instrumenting those files).
  • Using separate compilation units for hardware access and compiling them without ASan.
  • Employing linker scripts to place peripherals in non-instrumented memory regions (though ASan still instruments the accesses).

Need for Host-Based Testing or Emulation

Given the overhead, ASan is most practical when used with host-based unit tests, integration tests on Linux/macOS, or full-system emulation (QEMU, Renode). For pure bare-metal targets without an OS, porting the ASan runtime is non-trivial and may not be worth the effort unless you have significant resources.

Best Practices for Using ASan in Embedded Development

To maximize benefits while minimizing drawbacks, follow these guidelines:

  1. Isolate Core Logic: Separate hardware-dependent code from platform-independent algorithms. Test the core logic with ASan on host.
  2. Use Moderate Optimization: Compile with -O1 or -O2; avoid -O3 or -Ofast as they may interfere with instrumentation.
  3. Prioritize Unit Tests: Focus ASan on unit tests that exercise memory-intensive modules (buffers, dynamic allocation, string handling).
  4. Leverage Emulation: For hardware-interaction tests, use emulators like QEMU that can run ASan-instrumented binaries.
  5. Combine with Other Tools: Use ASan alongside static analyzers (Cppcheck, clang-tidy), sanitizers like UndefinedBehaviorSanitizer (UBSan), and runtime bounds checking where possible.
  6. Address ASan Reports Promptly: Treat every ASan error as a critical bug—even if it seems benign in tests, it may indicate undefined behavior that could manifest differently in production.
  7. Monitor Code Size: Keep track of flash and RAM usage with ASan enabled; if it exceeds targets, consider sampling or conditional compilation.

Alternatives and Complements

ASan isn’t the only tool for memory safety. Consider these alternatives depending on your constraints:

  • Valgrind: Excellent for heap profiling and leak detection but slower and requires user-space OS; not ideal for bare-metal.
  • AddressSanitizer vs. HWASan: Hardware-assisted ASan (HWASan) uses lower memory overhead but needs ARMv8.2-A or newer with memory tagging support (not available on Cortex-M).
  • Static Analyzers: Tools like Polyspace or Coverity can detect certain classes of bugs without runtime overhead but may miss complex interleavings.
  • Runtime Bounds Checking: Compiler options like -fcheck-array-bounds (Fortran) or -fbounds-check (some C compilers) offer lighter-weight checks but less comprehensive coverage.
  • Custom Assertions and Canaries: For specific buffers, implement your own guard values or canary checks—lightweight but manual.

Conclusion

AddressSanitizer brings powerful memory error detection to embedded C development, helping engineers catch elusive bugs that traditional methods miss. By instrumenting memory accesses and tracking shadow state, ASan provides detailed reports that pinpoint the exact location and cause of violations—whether it’s a heap use-after-free, stack overflow, or global buffer overflow.

While the increased code size, RAM usage, and performance overhead make ASan less suitable for resource-constrained bare-metal targets, it shines when applied to host-based unit tests, integration tests on Linux/macOS, or emulation environments. The key is to isolate testable components, use moderate optimization, and treat every ASan report as a critical issue requiring immediate action.

For embedded teams striving for zero-defect firmware, integrating ASan into the continuous integration pipeline—especially for host-executable test suites—can significantly reduce escape rates of memory-related bugs. Combine it with good software architecture, rigorous code reviews, and other sanitizers to build a robust safety net that catches issues early in the development cycle.

The next time you encounter an intermittent crash or mysterious corruption in your firmware, consider compiling with -fsanitize=address. The few minutes of setup could save hours of debugging and prevent costly field failures.

ASCII Art Diagram: ASan Shadow Memory Mapping

The following diagram illustrates how AddressSanitizer uses shadow memory to track the state of application memory. Each shadow byte represents 8 application bytes, indicating whether they are valid, poisoned (red zone), freed, etc.

+---------------------+ +---------------------+
| Application Memory | | Shadow Memory |
| (Actual RAM) | | (1 byte per 8 app B)|
+---------------------+ +---------------------+
| [Valid Data] | | 00 00 00 00 00 00 |
| [Red Zone]~~~~~~~~~| | 00 00 00 00 00 f1 |
| [Valid Data] | | 00 00 00 00 00 00 |
| [Freed Heap]XXXXXXXX| | 00 00 00 00 00 fd |
| [Stack Buffer] | | 00 00 00 00 00 00 |
| [Red Zone]~~~~~~~~~| | 00 00 00 00 00 f1 |
+---------------------+ +---------------------+
^ ^
| |
| 8 app bytes -> 1 shadow byte
|
Legend:
00 = valid address
f1 = stack red zone
f2 = global red zone
f3 = stack left red zone
f4 = stack mid red zone
f5 = stack right red zone
f8 = invalid address
fd = freed heap
fz = stack use after return

References

  1. Google AddressSanitizer Documentation. https://github.com/google/sanitizers/wiki/AddressSanitizer
  2. GCC Instrumentation Options. https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
  3. LLVM Sanitizer Coverage. https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
  4. ARM Compiler Reference: Memory Sanitizers. https://developer.arm.com/documentation/100067/0618
  5. QEMU User Manual. https://www.qemu.org/documentation/
  6. Renode Framework Documentation. https://renode.readthedocs.io/
  7. “Embedded Systems Security: Practical Methods for Safe and Secure Software Development” by David Kleidermacher et al., 2022.
  8. “Effective C: An Introduction to Professional C Programming” by Robert C. Seacord, 2020.

Frequently Asked Questions

What is AddressSanitizer and how does it detect memory bugs?

AddressSanitizer (ASan) is a fast memory error detector that finds use-after-free, buffer overflow, stack buffer overflow, and global buffer overflow bugs. It works by instrumenting code to add red zones around memory allocations and tracking memory accesses through compile-time instrumentation.

Can AddressSanitizer be used with embedded C toolchains like GCC for ARM Cortex-M?

Yes, AddressSanitizer works with GCC and Clang toolchains commonly used for embedded development. You need to compile with -fsanitize=address and link with the ASan runtime library, which requires support for dynamic memory allocation and typically increases code size by 2x-3x.

What are the main limitations of using AddressSanitizer in embedded systems?

Key limitations include increased memory usage (2x-3x for code, 2x for runtime), reduced performance (2x-3x slowdown), need for host-based execution or emulation (not ideal for bare-metal), and incompatibility with some memory-mapped I/O accesses that trigger false positives.

How do you interpret an AddressSanitizer error report?

ASan reports show the error type (e.g., heap-use-after-free), memory address involved, stack trace of the bad access, and allocation/deallocation stack traces. The report includes shadow byte explanations that distinguish between valid, redzone, freed, and unaddressable memory.

Tags

addresssanitizerembedded-cmemory-bugsfirmwaredebugging

Share


Previous Article
Constexpr Metaprogramming for STM32 Real-Time Performance
Jithin Tom

Jithin Tom

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

Related Posts

Fixing Cortex-M Hard Fault Handler Stack Corruption
Fixing Cortex-M Hard Fault Handler Stack Corruption
August 28, 2026
4 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media