I have a simple program written in C++ that build in the following configurations:
- Using/linked with
libstdc++
- Using/linked with
libc++
I run both builds using valgrind like so:
valgrind –leak-check=full –show-reachable=yes –track-origins=yes –log-file=test_program.log -v ./test_program
The libstdc++
version runs and result in no memory leaks:
==
== HEAP SUMMARY:
== in use at exit: 0 bytes in 0 blocks
== total heap usage: 24,813,106 allocs, 24,813,106 frees, 51,325,970,073 bytes allocated
==
== All heap blocks were freed -- no leaks are possible
==
== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
However when libc++
is run it shows a memory leak:
==434036== HEAP SUMMARY:
==434036== in use at exit: 16 bytes in 1 blocks
==434036== total heap usage: 317,709,577 allocs, 317,709,576 frees, 645,827,127,171 bytes allocated
==434036==
==434036== Searching for pointers to 1 not-freed blocks
==434036== Checked 401,408 bytes
==434036==
==434036== 16 bytes in 1 blocks are still reachable in loss record 1 of 1
==434036== at 0x484DA83: calloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==434036== by 0x49A365F: ??? (in /usr/lib/llvm-14/lib/libc++abi.so.1.0)
==434036== by 0x49A24E9: __cxa_get_globals (in /usr/lib/llvm-14/lib/libc++abi.so.1.0)
==434036== by 0x49A53F6: __cxa_throw (in /usr/lib/llvm-14/lib/libc++abi.so.1.0)
==434036== by 0x2EE7B3: goal::details::special_node<double>::value() const (in workspace/goal/goal_test)
==434036== by 0x2DC349: goal::details::caller_node<double>::value() const (in workspace/goal/goal_test)
==434036== by 0x50AC40: double goal::details::arg_node<double>::process<main_node<double> > const&) (in workspace/goal/goal_test)
==434036== by 0x45A79C: bool execute_test_base<double>() (in workspace/goal/goal_test)
==434036== by 0x2322A0: main (in workspace/goal/goal_test)
==434036==
==434036== LEAK SUMMARY:
==434036== definitely lost: 0 bytes in 0 blocks
==434036== indirectly lost: 0 bytes in 0 blocks
==434036== possibly lost: 0 bytes in 0 blocks
==434036== still reachable: 16 bytes in 1 blocks
==434036== suppressed: 0 bytes in 0 blocks
==434036==
==434036== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
I have two further builds of the program:
- Using/linked with
libstdc++
withASAN/LSAN/UBSAN/TSAN
- Using/linked with
libc++
withASAN/LSAN/UBSAN/TSAN
When running them, neither of them trigger sanitizer errors or warnings.
Compilers used:
- g++-13 (Ubuntu 13.1.0-8ubuntu1~22.04) 13.1.0
- clang version 20.0.0git
The valgrind leak is observed on both compilers only when linking with libc++.
Questions: Could the leak from valgrind be a false positive and what else can be done to verify it’s legitimate?
2
Answers
Simple rule. Do not delude yourself into thinking that there may be false positives. It’s almost always just wishful thinking and confirmation bias.
Valgrind memcheck does generate some false positives but they are few and far between. Leak detection is an “easy” job and so the rate of false positives is essentially zero.
Memcheck can do a better job of leak detection than sanitizers because it has a complete view of the execution of the guest exe, from the very first instruction to the last. I’m not a sanitizer expert, but I imagine that the sanitizer code can only start when the first global constructor executes.
As said in the comments, if your exe is throwing an exception that ends with std::terminate then things like atexit cleanup don’t get performed. Leak detection is only meaningful if the exe performs a clean exit.
The short of it: The situation you have described above is NOT a memory leak, in the sense that a piece of memory was allocated (eg: via malloc) assigned to a pointer, and later on the pointer was over-written or otherwise lost (scope), resulting in an explicit free of the allocated memory not being carried out.
So what is this leak report that valgrind is giving?
The Standard library implementations have a lot of leeway in regards to how they implement specified C++ standard features.
In your case, what you are seeing is that
libc++
, unlikelibstdc++
, creates a Thread Local Storage (TLS) instance of memory (via__calloc_with_fallback
) the first time the executable(aka process)
attempts to throw an exception within a given thread (main is also considered as being a thread).The code can be found here:
https://github.com/llvm/llvm-project/blob/main/libcxxabi/src/cxa_exception_storage.cpp#L80
The
libc++
code that instantiates the memory immediately registers the associated pointer with a shutdown memory manager via the call to__libcpp_tls_set
.The idea here is that as part of the process shutdown procedure: Once all the state of the process has been free/destroyed/cleaned-up the
TLS
associated allocations (as there could be more than one thread in the process) registered with__libcpp_tls_set
are then finally released (or freed).Valgrind raises an issue here as it’s not able to track the the true location of allocation and line it up with its associated destruction, though all the memory allocated since the start of the process running is eventually freed explicity – and not implicitly via OS clean-up.
The leak you are seeing can easily be replicated with the following code:
Build:
c++ -pedantic-errors -Wall -Wextra -Werror -O2 -o exceptiontest exceptiontest.cpp -L/usr/lib -lc++
Run valgrind:
valgrind –leak-check=full –show-reachable=yes –track-origins=yes –log-file=exceptiontest.log -v ./exceptiontest
Valgrind output:
As you can see the written program is very simple and completely standards conforming and does not either explicitly or implicitly leak memory, but yet depending on the c++ standard library implementation used, valgrind will raise an issue related to non-freed blocks.
When building the program with
libstdc++
, as expected no leaks or reachable blocks are generated: