5

I am using AddressSanitizer for all my projects in order to detect memory leaks, heap corruptions etc. However, when loading a dynamic library at runtime via dlopen, the output of AddressSanitizer leaves a lot to be desired. I wrote a simple test program to illustrate the problem. The code itself is not interesting, simply two libraries, one linked at compile time via -l, the other loaded at runtime with dlopen. For completeness, here the code I used for testing:

// ----------------------------------------------------------------------------
// dllHelper.hpp
#pragma once

#include <string>
#include <sstream>
#include <iostream>

#include <errno.h>
#include <dlfcn.h>

// Generic helper definitions for shared library support
#if defined WIN32
#define MY_DLL_EXPORT __declspec(dllexport)
#define MY_DLL_IMPORT __declspec(dllimport)
#define MY_DLL_LOCAL
#define MY_DLL_INTERNAL
#else
#if __GNUC__ >= 4
#define MY_DLL_EXPORT __attribute__ ((visibility ("default")))
#define MY_DLL_IMPORT __attribute__ ((visibility ("default")))
#define MY_DLL_LOCAL  __attribute__ ((visibility ("hidden")))
#define MY_DLL_INTERNAL __attribute__ ((visibility ("internal")))
#else
#define MY_DLL_IMPORT
#define MY_DLL_EXPORT
#define MY_DLL_LOCAL
#define MY_DLL_INTERNAL
#endif
#endif

void* loadLibrary(const std::string& filename) {
    void* module = dlopen(filename.c_str(), RTLD_NOW | RTLD_GLOBAL);

    if(module == nullptr) {
        char* error = dlerror();
        std::stringstream stream;
        stream << "Error trying to load the library. Filename: " << filename << " Error: " << error;
        std::cout << stream.str() << std::endl;
    }

    return module;
}

void unloadLibrary(void* module) {
    dlerror(); //clear all errors
    int result = dlclose(module);
    if(result != 0) {
        char* error = dlerror();
        std::stringstream stream;
        stream << "Error trying to free the library. Error code: " << error;
        std::cout << stream.str() << std::endl;
    }
}

void* loadFunction(void* module, const std::string& functionName) {
    if(!module) {
        std::cerr << "Invalid module" << std::endl;
        return nullptr;
    }

    dlerror(); //clear all errors
    #ifdef __GNUC__
    __extension__
    #endif
    void* result = dlsym(module, functionName.c_str());
    char* error;
    if((error = dlerror()) != nullptr) {
        std::stringstream stream;
        stream << "Error trying to get address of function \"" << functionName << "\" from the library. Error code: " << error;
        std::cout << stream.str() << std::endl;
    }

    return result;
}


// ----------------------------------------------------------------------------
// testLib.hpp
#pragma once

#include "dllHelper.hpp"

#ifdef TESTLIB
#define TESTLIB_EXPORT MY_DLL_EXPORT
#else
#define TESTLIB_EXPORT MY_DLL_IMPORT
#endif

namespace TestLib {

// will be linked at compile time
class TESTLIB_EXPORT LeakerTestLib {
    public:
        void leak();
};

}


// ----------------------------------------------------------------------------
// testLib.cpp
#include "testLib.hpp"

namespace TestLib {

void LeakerTestLib::leak() {
    volatile char* myLeak = new char[10];
    (void)myLeak;
}

}


// ----------------------------------------------------------------------------
// testLibRuntime.hpp
#pragma once

#include "dllHelper.hpp"

#ifdef TESTLIBRUNTIME
#define TESTLIBRUNTIME_EXPORT MY_DLL_EXPORT
#else
#define TESTLIBRUNTIME_EXPORT MY_DLL_IMPORT
#endif

namespace TestLibRuntime {

// will be loaded via dlopen at runtime
class TESTLIBRUNTIME_EXPORT LeakerTestLib {
    public:
        void leak();
};

}

extern "C" {
    TestLibRuntime::LeakerTestLib* TESTLIBRUNTIME_EXPORT createInstance();
    void TESTLIBRUNTIME_EXPORT freeInstance(TestLibRuntime::LeakerTestLib* instance);
    void TESTLIBRUNTIME_EXPORT performLeak(TestLibRuntime::LeakerTestLib* instance);
}

// ----------------------------------------------------------------------------
// testLibRuntime.cpp
#include "testLibRuntime.hpp"

namespace TestLibRuntime {

void LeakerTestLib::leak() {
    volatile char* myLeak = new char[10];
    (void)myLeak;
}

extern "C" {

    LeakerTestLib* createInstance() {
        return new LeakerTestLib();
    }

    void freeInstance(LeakerTestLib* instance) {
        delete instance;
    }

    void performLeak(LeakerTestLib* instance) {
        if(instance) {
            instance->leak();
        }
    }

}

}


// ----------------------------------------------------------------------------
// main.cpp
#include "testLib.hpp"
#include "testLibRuntime.hpp"

#define LEAK_TESTLIB
#define LEAK_TESTLIBRUNTIME

int main(int argc, char** argv) {
    #ifdef LEAK_TESTLIBRUNTIME
    void* testLibRuntimeModule = loadLibrary("libtestLibRuntime.so");

    if(!testLibRuntimeModule) {
        return -1;
    }

    TestLibRuntime::LeakerTestLib* testLibRuntime = nullptr;

    auto createInstance = (TestLibRuntime::LeakerTestLib * (*)())loadFunction(testLibRuntimeModule, "createInstance");
    if(!createInstance) {
        return -1;
    }
    auto freeInstance = (void(*)(TestLibRuntime::LeakerTestLib*))loadFunction(testLibRuntimeModule, "freeInstance");
    if(!freeInstance) {
        return -1;
    }
    auto performLeak = (void(*)(TestLibRuntime::LeakerTestLib*))loadFunction(testLibRuntimeModule, "performLeak");
    if(!performLeak) {
        return -1;
    }

    testLibRuntime = createInstance();
    performLeak(testLibRuntime);
    freeInstance(testLibRuntime);
    #endif

    #ifdef LEAK_TESTLIB
    TestLib::LeakerTestLib testLib;
    testLib.leak();
    #endif

    #ifdef LEAK_TESTLIBRUNTIME
    unloadLibrary(testLibRuntimeModule);
    #endif

    return 0;
}

I compiled the code above with the following commands:

clang++ -std=c++11 -O0 -g -ggdb -Wl,-undefined -Wl,dynamic_lookup -fsanitize=address -fsanitize-recover=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope -DTESTLIB -shared -fPIC -o libtestLib.so testLib.cpp -ldl -shared-libasan
clang++ -std=c++11 -O0 -g -ggdb -Wl,-undefined -Wl,dynamic_lookup -fsanitize=address -fsanitize-recover=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope -DTESTLIBRUNTIME -shared -fPIC -o libtestLibRuntime.so testLibRuntime.cpp -ldl -shared-libasan
clang++ -std=c++11 -O0 -g -ggdb -Wl,-undefined -Wl,dynamic_lookup -fsanitize=address -fsanitize-recover=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope -o leak main.cpp -ldl -L./ -ltestLib -shared-libasan

When I run the program, I get the following output (I have to export LD_LIBRARY_PATH beforehand in order to find libasan):

$ export LD_LIBRARY_PATH=/usr/lib/clang/4.0.0/lib/linux/:./
$ ./leak

=================================================================
==4210==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 10 byte(s) in 1 object(s) allocated from:
    #0 0x7fb665a210f0 in operator new[](unsigned long) (/usr/lib/clang/4.0.0/lib/linux/libclang_rt.asan-x86_64.so+0x10e0f0)
    #1 0x7fb66550d58a in TestLib::LeakerTestLib::leak() /home/jae/projects/clang_memcheck/testLib.cpp:6:29
    #2 0x402978 in main /home/jae/projects/clang_memcheck/main.cpp:37:13
    #3 0x7fb6648d4439 in __libc_start_main (/usr/lib/libc.so.6+0x20439)

Direct leak of 10 byte(s) in 1 object(s) allocated from:
    #0 0x7fb665a210f0 in operator new[](unsigned long) (/usr/lib/clang/4.0.0/lib/linux/libclang_rt.asan-x86_64.so+0x10e0f0)
    #1 0x7fb6617fd6da  (<unknown module>)
    #2 0x7fb6617fd75f  (<unknown module>)
    #3 0x402954 in main /home/jae/projects/clang_memcheck/main.cpp:31:5
    #4 0x7fb6648d4439 in __libc_start_main (/usr/lib/libc.so.6+0x20439)

SUMMARY: AddressSanitizer: 20 byte(s) leaked in 2 allocation(s).

While the leaks are detected, AddressSanitizer seems to be unable to resolve module name, function names and line numbers of the library which gets loaded via dlopen (printing ( < unknown module > ) instead), while libraries linked at compile time work flawlessly. My question is:

Is it possible to fix this using some compiler switches, or is there no way to get more information using AddressSanitizer when it comes to libraries loaded using dlopen? Obviously llvm-symbolizer can be found, or there wouldn't be line numbers for the other library. Running the program with

ASAN_OPTIONS=symbolize=1 ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer ./leak

does not result in a different output. I compiled the program with g++ instead, but the output remained the same. I also piped the output through asan_symbolize.py, but nothing changed. I don't know where to look next. Is there a fundamental mistake in my thinking? I'm not a specialist when it comes to dynamic loading of libraries.

kamshi
  • 605
  • 6
  • 19

2 Answers2

6

I've been really cutting corners when it comes to tracking such problems in dynamically loaded libraries, but I just omit library unload code for testing purposes so symbols will be still available for sanitizer (and valgrind) when program terminates. Though doing so may lead to some false leak detection, as staff allocated by dlopen won't be freed.

And it seems that there is no proper solution for this problem because technically after library is unloaded nothing prevents another library to be loaded at the same address.

user7860670
  • 35,849
  • 4
  • 58
  • 84
  • Thank you, after I disabled unloading of the library, I got the correct output. It's a shame that it doesn't work when you unload the library like you should. Guess I will take your approach and simply disable library unloading when searching for memory leaks. – kamshi Jun 19 '17 at 10:09
3

That's a known bug in ASan (see Issue 89). It has been around for a while but seems noone is motivated to fix it.

yugr
  • 19,769
  • 3
  • 51
  • 96