I have a simple program written in C++ that build in the following configurations:
libstdc++
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:
libstdc++
with ASAN/LSAN/UBSAN/TSAN
libc++
with ASAN/LSAN/UBSAN/TSAN
When running them, neither of them trigger sanitizer errors or warnings.
Compilers used:
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?
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++
, unlike libstdc++
, 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).
__cxa_eh_globals * __cxa_get_globals () {
// Try to get the globals for this thread
__cxa_eh_globals* retVal = __cxa_get_globals_fast ();
// If this is the first time we've been asked for these globals, create them
if ( NULL == retVal ) {
retVal = static_cast<__cxa_eh_globals*>
(__calloc_with_fallback (1, sizeof (__cxa_eh_globals)));
if ( NULL == retVal )
abort_message("cannot allocate __cxa_eh_globals");
if ( 0 != std::__libcpp_tls_set ( key_, retVal ) )
abort_message("std::__libcpp_tls_set failure in __cxa_get_globals()");
}
return retVal;
}
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:
int main()
{
try
{
throw 1;
}
catch (int)
{}
return 0;
}
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:
==551373== HEAP SUMMARY:
==551373== in use at exit: 16 bytes in 1 blocks
==551373== total heap usage: 2 allocs, 1 frees, 160 bytes allocated
==551373==
==551373== Searching for pointers to 1 not-freed blocks
==551373== Checked 135,200 bytes
==551373==
==551373== 16 bytes in 1 blocks are still reachable in loss record 1 of 1
==551373== at 0x484DA83: calloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==551373== by 0x48AE65F: ??? (in /usr/lib/llvm-14/lib/libc++abi.so.1.0)
==551373== by 0x48AD4E9: __cxa_get_globals (in /usr/lib/llvm-14/lib/libc++abi.so.1.0)
==551373== by 0x48B03F6: __cxa_throw (in /usr/lib/llvm-14/lib/libc++abi.so.1.0)
==551373== by 0x109105: main (in temp/exception_test/exceptiontest)
==551373==
==551373== LEAK SUMMARY:
==551373== definitely lost: 0 bytes in 0 blocks
==551373== indirectly lost: 0 bytes in 0 blocks
==551373== possibly lost: 0 bytes in 0 blocks
==551373== still reachable: 16 bytes in 1 blocks
==551373== suppressed: 0 bytes in 0 blocks
==551373==
==551373== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
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:
==551500== HEAP SUMMARY:
==551500== in use at exit: 0 bytes in 0 blocks
==551500== total heap usage: 2 allocs, 2 frees, 73,860 bytes allocated
==551500==
==551500== All heap blocks were freed -- no leaks are possible
==551500==
==551500== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)