Using AddressSanitizer (ASan)
Learn to find memory bugs at runtime by integrating AddressSanitizer (ASan) into a CMake project
The previous lessons in this chapter have focused on functional correctness. Our test suite, powered by GoogleTest and managed by CTest, answers the question: "Does my code do what it's supposed to do?"
But we generally want our build systems to automatically detect other problems too. For example:
- Are there any obvious memory errors and undefined behaviors?
- Does the code run efficiently?
- Is the coding style clean, consistent, and maintainable?
This chapter expands our utility belt with tools that help us implement such checks. We'll survey three categories of automated analysis and learn how to integrate them into our CMake projects:
- Dynamic Analysis with AddressSanitizer to find memory bugs at runtime.
- Performance Analysis with Google Benchmark to measure our code's speed.
- Static Analysis with Clang-Tidy to enforce good practices and find bugs before we even compile.
Finding Memory Errors with AddressSanitizer (ASan)
Dynamic analysis is the process of checking code for errors while it is running. The most powerful tools in this category are the sanitizers, which are special modes built into modern compilers like Clang and GCC.
The most famous of these is the AddressSanitizer (ASan). It instruments our code with extra checks to detect a wide range of memory errors the moment they happen, such as:
- Buffer overflows (reading or writing past the end of an array).
- Use-after-free (using a pointer to memory that has already been deallocated).
- Use-after-return (using a pointer to a local variable after its function has returned).
These bugs are notoriously difficult to debug because they often cause crashes or unintended behaviors much later in the program's execution, far from the original error. ASan makes them trivial to find by stopping the program as soon as they happen, and providing a detailed stack trace.
Integrating ASan with CMake
Enabling ASan involves simply setting a compiler and linker flag: -fsanitize=address
on GCC/Clang, or /fsanitize=address
in MSVC.
To improve stack traces when an error is detected, we generally also want to provide the -fno-omit-frame-pointer
and -g
arguments for GCC/Clang, or /Zi
on MSVC.
The best way to manage this is with a dedicated build type and preset. Let's create a module file to handle this:
cmake/Sanitize.cmake
cmake_minimum_required(VERSION 3.23)
set(SANITIZE_CONDITION "$<CONFIG:Sanitize>")
if (MSVC)
set(
SANITIZE_FLAGS
"$<${SANITIZE_CONDITION}:/fsanitize=address>"
)
add_compile_options("$<${SANITIZE_CONDITION}:/Zi>")
else()
set(SANITIZE_FLAGS
"$<${SANITIZE_CONDITION}:-fsanitize=address>"
"$<${SANITIZE_CONDITION}:-fno-omit-frame-pointer>"
"$<${SANITIZE_CONDITION}:-g>"
)
endif()
add_compile_options(${SANITIZE_FLAGS})
add_link_options(${SANITIZE_FLAGS})
And we'll include()
it in our root CMakeLists.txt
file:
CMakeLists.txt
cmake_minimum_required(VERSION 3.23)
project(Greeter)
include(cmake/Coverage.cmake)
include(cmake/Sanitize.cmake)
add_subdirectory(app)
add_subdirectory(greeter)
enable_testing()
add_subdirectory(tests)
Finally, let's add the corresponding presets to CMakePresets.json
.
CMakePresets.json
{
"version": 3,
"configurePresets": [
// ... other presets
{
"name": "sanitize",
"displayName": "Address Sanitizer (ASan)",
"inherits": "default",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Sanitize"
}
}
],
"buildPresets": [
// ... other presets
{
"name": "sanitize",
"configurePreset": "sanitize"
}
],
"testPresets": [
// ... other presets
{
"name": "sanitize",
"inherits": "default",
"configurePreset": "sanitize"
}
]
}
Seeing ASan in Action
Now, let's introduce a classic memory bug to the start of our greet()
function in GreeterLib
. We'll create a buffer of 5
bytes, then try to read index 5
of that buffer - ie, the byte at the 6th position:
greeter/src/Greeter.cpp
#include <greeter/Greeter.h>
#include <iostream>
// ...
std::string Greeter::greet() const {
char buffer[5];
std::cout << buffer[5];
// ...
}
ASan can only detect a memory issue if we actually cause that issue to occur when running our ASan-enabled build. For this reason, we get the most benefit out ASan when we combine it with a test suite that covers as much of our code as possible.
The test suite executes as many functions and branches of our code as possible, whilst ASan is keeping watch to detect if any of those actions cause memory issues.
First though, let's run our tests against our standard build, without ASan enabled. As long as our memory issue doesn't cause the program to crash, our test suite will likely pass:
cmake --preset default
cmake --build --preset default
ctest --preset default
...
100% tests passed, 0 tests failed out of 4
...
Let's try again, but this time with ASan enabled.
Note if you're using GCC in a MinGW environment, such as MSYS2 UCRT64, these steps may not work. We explain why this is and how to address it at the end of this section.
On other environments, we can delete our existing build directory, and then configure, build, and test our project from scratch, but this time using our new sanitize
preset:
cmake --preset sanitize
cmake --build --preset sanitize
ctest --preset sanitize
This time, the test should fail with a detailed report from ASan. This will be a huge dump of information containing everything we might need to debug a complex issue.
For our simple program, some of the key output is shown below, where it tells us the type of error that was detected (stack-buffer-overflow
) and points us to exactly where it happened:
ERROR: AddressSanitizer: stack-buffer-overflow on address 0x008d920fe905
...
Address 0x008d920fe905 is located in stack of thread T0 at offset 101 in frame
#0 0x7ff6a39dc73f in Greeter::greet()
This frame has 2 object(s):
[32, 56) 'day_str' (line 18)
[96, 101) 'buffer' (line 25) <== Memory access at offset 101 overflows this variable
...
0% tests passed, 4 tests failed out of 4
Configuring ASan Runtime Behavior
AddressSanitizer's default settings find many common memory errors, but some more advanced checks are disabled by default because they can have a performance impact or are specific to certain bug classes. You can enable these checks at runtime to get even more detailed analysis. This is done by setting the ASAN_OPTIONS
environment variable before running an instrumented executable.
A classic and dangerous C++ bug is a use-after-return. This happens when a function returns a pointer to one of its local variables. Once the function returns, its stack frame is destroyed, and the memory that the variable occupied is now free to be reused. The returned pointer is now "dangling," and any attempt to use it results in undefined behavior.
The ASan check for this, detect_stack_use_after_return
, is disabled by default in many configurations. Let's write a test to demonstrate this bug and see how we can configure ASan to detect it.
Creating a "Use-After-Return" Bug in a Test
We'll add a new test case to tests/greeter/test_greeter.cpp
. This test will contain its own helper function that intentionally creates this bug.
tests/greeter/test_greeter.cpp
#include <gtest/gtest.h>
#include <greeter/Greeter.h>
// ... existing tests
const char* getDanglingPointer() {
char local_buffer[] = "This is temporary";
// DANGER: returning pointer to local variable
return local_buffer;
}
TEST(
ASanRuntimeOptionTests,
FailsToDetectUseAfterReturnByDefault
) {
const char* dangling_ptr = getDanglingPointer();
// The memory dangling_ptr points to is no longer valid.
// Accessing it is undefined behavior.
char first_char = dangling_ptr[0]; // <w>
// This assertion might pass by chance if
// the memory hasn't been overwritten.
EXPECT_NE(first_char, '\0');
}
First, let's run this test with our sanitize
preset and the default ASan options.
cmake --preset sanitize
cmake --build --preset sanitize
ctest --preset sanitize
In most environments, this test will pass. The undefined behavior doesn't cause an immediate crash, and since the specific check is disabled, ASan remains silent.
...
100% tests passed, 0 tests failed out of 5
...
Enabling the Check in CMake
Now, let's enable the detect_stack_use_after_return
check. We'll set the ASAN_OPTIONS
environment variable. We can do this in several ways. One option is to set it system-wide or for our terminal session, using techniques we covered earlier.
We can also apply it when a CMake preset is being used via the environment
key:
CMakePresets.json
{
"version": 3,
"configurePresets": [/*...*/],
"buildPresets": [/*...*/],
"testPresets": [{
"name": "sanitize",
"inherits": "default",
"configurePreset": "sanitize",
"environment": {
"ASAN_OPTIONS": "detect_stack_use_after_return=1"
}
}]
}
Alternatively, we can apply it to a set of tests found by a gtest_discover_tests()
invocation:
tests/greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.23)
find_package(GTest REQUIRED)
add_executable(GreeterLibTests test_greeter.cpp)
target_link_libraries(GreeterLibTests PRIVATE
GreeterLib
GTest::gtest
GTest::gmock
GTest::gmock_main
)
gtest_discover_tests(GreeterLibTests PROPERTIES
LABELS "library"
ENVIRONMENT
"ASAN_OPTIONS=detect_stack_use_after_return=1"
)
After setting this environment variable using any of these approaches, we can rebuild and run the tests again:
cmake --preset sanitize
cmake --build --preset sanitize
ctest --preset sanitize
This time, the test should fail. ASan, with the check now enabled, intercepts the invalid memory access and provides a detailed report:
ERROR: AddressSanitizer: stack-use-after-return on address 0x020481232e20
...
Address 0x020481232e20 is located in ... getDanglingPointer()
...
80% tests passed, 1 tests failed out of 5
This is just one example of the many ways you can customize ASan's behavior. For a full list of options and detailed explanations of the errors ASan can find, the official AddressSanitizer documentation is an excellent resource.
Summary
In this lesson, we've seen how to integrate dynamic analysis into our build process to catch a whole class of memory bugs that are often missed by standard testing.
- Dynamic Analysis: Sanitizers like ASan find errors by instrumenting your code to perform checks as it runs.
- AddressSanitizer (ASan): The most common sanitizer, it excels at finding memory errors like buffer overflows and use-after-free bugs.
- CMake Integration: We enabled ASan by adding the
-fsanitize=address
flag and created a dedicatedSanitize
build type and preset to manage it. - Clear Error Reports: When ASan detects an error, it immediately terminates the program and provides a detailed stack trace, making bugs easy to locate and fix.
Using Google Benchmark
Adding benchmarks to our build to measure the performance of our code, and track how it changes over time