How and Why You Should Be Using ASan
If you’ve ever touched C++ in your life, you most likely know you have to be super careful with memory management. Allocating memory on the heap with a simple new
and subsequently forgetting to delete
that memory can be catastrophic — causing bugs that are hard to track down and near impossible to reproduce.
You may argue that smart pointers such as shared_ptr
and unique_ptr
solve most of our memory problems, and that’s sometimes true, but not always. Complicated class relationships can still cause memory issues even when we’re using smart pointers, and because of that, we need to look to tools to provide a safety net for our poor little programming brains.
One of those tools is AddressSanitizer (ASan), which is what we’ll look at today.
What Is AddressSanitizer (ASan)?
ASan is a tool used to detect memory issues such as use after free (UAF) errors, buffer overflow issues, and more. It’s implemented in Clang (including Apple Clang), GCC, and MSVC, so you’ll most likely be able to use the tool no matter what environment you’re working in.
💡 Tip: If you’re working in Xcode, you’ll probably want to read our ASan in Xcode blog post.
One of the big advantages of ASan over other memory tools — such as Valgrind — is its speed. As you can imagine, doing any type of memory checking takes a lot of time, and anyone who has been a user of Valgrind in the past knows how slow (and memory intensive) situations can get. Lucky for us, ASan is implemented a little differently than Valgrind. In turn, performance is only slightly affected with its use — although for all the advantages you gain with ASan, you can still expect a slow down of roughly 2×, larger memory usage, and a larger binary.
Knowing that you can still expect a performance hit with ASan, you should be aware of when to use the tool — and that’s not in your production environment. Instead, it’s wise to use it at development time as a quick reassurance. And if you can afford the CI costs, running ASan before any merge is wise.
So now that you know when and where to use ASan, let’s look at how you can use it.
How to Use ASan with a C++ Project
To use ASan, just add a few compilation and link flags, and you’re off!
To demonstrate how easy it is, we’ll use Clang on the command line. But first, let’s create a file that has an obvious memory leak in it:
int main() { int *useAfterFree = new int[10]; delete[] useAfterFree; return useAfterFree[0]; // Accessing memory that has been freed. }
Hopefully, you can see the obvious UAF issue in the code above. But for some reason, let’s pretend you didn’t see the read of useAfterFree[0]
after its memory has already been deleted. Don’t worry; ASan can catch it!
Compile the file with -fsanitize=address
, and to make the stack trace easier to read, use -fno-omit-frame-pointer
. Then, during link time, add the -fsanitize=address
flag again and output to a binary:
clang++ -g -fsanitize=address -fno-omit-frame-pointer -c main.cpp clang++ -g -fsanitize=address main.o -o asan.out
Now, you can run the asan.out
binary and watch ASan in all its glory:
./asan.out ================================================================= ==62475==ERROR: AddressSanitizer: heap-use-after-free on address 0x6040000002d0 at pc 0x00010fdcff42 bp 0x7ffedfe33780 sp 0x7ffedfe33778 READ of size 4 at 0x6040000002d0 thread T0 #0 0x10fdcff41 in main main.cpp:6 #1 0x7fff20504f3c in start+0x0 (libdyld.dylib:x86_64+0x15f3c) 0x6040000002d0 is located 0 bytes inside of 40-byte region [0x6040000002d0,0x6040000002f8) freed by thread T0 here: #0 0x10fe3315d in wrap__ZdaPv+0x7d (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x5515d) #1 0x10fdcff0a in main main.cpp:5 #2 0x7fff20504f3c in start+0x0 (libdyld.dylib:x86_64+0x15f3c) previously allocated by thread T0 here: #0 0x10fe32d4d in wrap__Znam+0x7d (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x54d4d) #1 0x10fdcfeff in main main.cpp:4 #2 0x7fff20504f3c in start+0x0 (libdyld.dylib:x86_64+0x15f3c) SUMMARY: AddressSanitizer: heap-use-after-free main.cpp:6 in main ...
It caught the deliberate bug!
So that shows how ASan can work in a very simple case. But let’s look at a slightly more convoluted example — one where ASan can really save you big time:
#include <memory> struct Client; struct Server; struct Client { std::shared_ptr<Server> server; }; struct Server { std::shared_ptr<Client> client; }; int main() { std::shared_ptr<Client> client = std::make_shared<Client>(); std::shared_ptr<Server> server = std::make_shared<Server>(); client->server = server; server->client = client; return 0; }
That example looks a bit silly, and it is, but you’d be surprised at how easily a situation like this can arise.
Because client
has a reference to server
and server
has a reference to client
, there’s a cyclic dependency, and neither shared pointer can be freed upon exiting main. server
can’t be freed, because it still holds a reference to client
, which still holds a reference to server
. Round and round we go.
From the ASan output, you’d see:
./asan.out ================================================================= ==11==ERROR: LeakSanitizer: detected memory leaks Indirect leak of 32 byte(s) in 1 object(s) allocated from: ...
ℹ️ Note: The above error is a LeakSanitizer and it isn’t implemented in Apple Clang, so you won’t see this ASan error in Xcode or with the Apple Clang compiler.
From the above example, it’s hard to understand who would ever program such a thing. Well, it happens, and it’s not always oh so obvious. For example, take the following:
#include <memory> struct Client; struct Server; struct Client { void sendRequest() { // Send a request here. } }; struct Server { std::shared_ptr<Client> client; void processAllRequests() { // Ensure all requests are processed. } }; int main() { std::shared_ptr<Server> server = std::make_shared<Server>(); std::shared_ptr<Client> client = std::shared_ptr<Client>(new Client(), [server](Client* client){ server->processAllRequests(); delete client; }); server->client = client; return 0; }
The server
holds a reference to client
, but not vice versa. Or does it?
It’s tricky. The Client
struct doesn’t hold a reference to Server
, but the shared_ptr
deleter for client
does! And the deleter is the lambda passed upon the creation of client
. So again, we’d receive indirect leaks because there’s a cyclic dependency.
Now imagine how convoluted and distributed this example could become, and consider numerous developers working in the area of code.
Let’s just say, why not use ASan?
What Target Should You Run ASan Against?
When working on a simple single-path executable like one of the above examples, you’d want to run ASan against the main application. But what about a large application with many operating paths? How about a library? Well, in these cases, you’d want to cover as much of the code and in as many variations as possible. For this reason, here at PSPDFKit, we run ASan on our C++ tests. Hopefully you already have tests in your project and they already provide a decent amount of coverage.
PSPDFKit C++ tests cover many flavors and variations of operation, and they touch as many paths as possible, so that’s the most logical place to test. And we don’t just run ASan locally: We also run ASan on our CI to ensure all code being merged is free from memory issues like what’s detailed in the examples above.
Conclusion
In reading this post, you hopefully learned how you can use ASan in your C++ projects, and even if you think your code is safe, you understand why you should be using ASan to keep it that way!
I have mixed feelings about developers still having to worry about these kinds of memory issues, especially as other languages have succeeded in providing safe memory management without developer input (although these have drawbacks too). Those feelings aside, it’s great to know there are tools to lend us a helping hand when we need it!
When Nick started tinkering with guitar effects pedals, he didn’t realize it’d take him all the way to a career in software. He has worked on products that communicate with space, blast Metallica to packed stadiums, and enable millions to use documents through Nutrient, but in his personal life, he enjoys the simplicity of running in the mountains.