skip to Main Content

I have a simple program written in C++ that build in the following configurations:

  1. Using/linked with libstdc++
  2. 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:

  1. Using/linked with libstdc++ with ASAN/LSAN/UBSAN/TSAN
  2. Using/linked with libc++ with ASAN/LSAN/UBSAN/TSAN

When running them, neither of them trigger sanitizer errors or warnings.

Compilers used:

  1. g++-13 (Ubuntu 13.1.0-8ubuntu1~22.04) 13.1.0
  2. 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


  1. 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.

    Login or Signup to reply.
  2. 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)
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search