user3819404
user3819404

Reputation: 621

Allocating and Deallocating memory across shared lib boundaries

When working with windows dll's, it is established that we should limit the memory allocation/deallocation within the dll boundary since the dll could be using its own heap. So we have export allocators and free functions to from dll.

IsomeInterface* getObject();
void freeObject(IsomeInterface *object);

This way the object creation and deletion will reside within the dll.

Does this problem also exist on shared libraries on linux? When dealing with shared libs (.so) do we also need to take care of keeping allocation/deallocation within the shared lib. I did some quick tryout below and it works on linux. The same example would not work with windows dll (it will work with windows dll if both exe and dll compiled with /MD to make use of the same heap).

std::vector<int> vec;
vec.push_back(42);
PassAVector(vec);

where PassAVector resides in a shared lib

void PassAVector(std::vector<int> &vec)
{
     vec.push_back(1);  // These would cause reallocation
     vec.push_back(2);
     vec.push_back(3);
     vec.push_back(4);
     vec.push_back(5);
}

Does this mean shared libs on unix share the heap with the executables (equivalent to the /MD switch on windows)?

Is it possible to compile (some compiler flags) the shared lib (.so) or the executable on linux in such a way that they start using different heaps (/MT switch on windows) and this problem surfaces?

EDIT: Found this which seem to suggest passing STL across boundaries is okay as long long as the compiler is gcc.

Upvotes: 7

Views: 1520

Answers (3)

eungenue
eungenue

Reputation: 81

Polymorphic objects created by a library should never outlive the library because they will get broken when the library is unloaded from memory.

It may sound obvious, but shared libraries can be unloaded. Such a design may be used by plugins for example.

The following scenario looks simple and harmless but triggers segmentation fault:

  1. A shared library is loaded.
  2. Some method of the library creates and returns a polymorphic object.
  3. The library gets uloaded from memory.
  4. Calling any virtual method on the object triggers segmentation fault.

The problem is in the vtable of the polymorphic object. It is generated by the compiler in the address space of the library and when the library is gone the vtable is gone as well. But the real problem is that the segmentation fault is dormant and most of the time everything will pretend to work corectly until one day it will suddenly crash.

There are many implementation and operating system details that affect the behavior.

Below is the (incomplete) list of the things that affect the problem:

  • This will not happen when shared libraries are linked statically
  • dlclose may delay library unloading hiding the problem
  • vtable symbol resolution depends on environment settings and dlopen flags
  • Different versions and different compilers may generate vtables differently
  • Usage scope of the polymorphic class may change and vtable will move from one library to another
  • Shared libraries loading and unloading order matters and it may change over time in a complicated program triggering the problem

This affects all polymorphic classes. Virtual destructors are welcome. STL classes may easily use virtual calls under the hood - std::shared_ptr definitely does.

The example code that reproduces the problem. https://godbolt.org/z/scKEz47sY

I'm assuming C++ as the author mentioned STL containers in his question.

object.h

struct object
{
    // object vtable resides in .so and is gone when .so is dlclosed
    virtual void method() {}
};

plugin.cpp

#include "object.h"

extern "C" {
    object* create_object() {
        return new object();
    }
}

main.cpp

#include <dlfcn.h>
#include "object.h"

int main(int, char*[])
{
    void* so = dlopen("build/libplugin.so", RTLD_LAZY);
    typedef object* (*create_object_method)(void);
    create_object_method create_object = reinterpret_cast<create_object_method>(dlsym(so, "create_object"));
    object* obj = create_object();
    dlclose(so);
    obj->method();
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(shared_mem)

add_library(plugin SHARED plugin.cpp)

add_executable(the_executable main.cpp)
target_link_libraries(the_executable dl)

Create the files in one directory and run:

mkdir build && cd build && cmake .. && make && ./the_executable

Upvotes: 1

yugr
yugr

Reputation: 21955

As long as you stick with Glibc or other "normal" allocators (jemalloc, tcmalloc, etc.) the heap state will be shared by all libraries and thus you'll be able to free memory allocated somewhere with malloc anywhere you want.

In theory it may be possible to circumvent this. For example some library may be linked with custom implementation of malloc/free (via symbol scripts trickery of -Bsymbolic) which has it's own private heap and thus will not interact well with other parts of your program. But I've never seen anything like this in real life.

STL containers are based on malloc/free so it is possible to pass/modify them across library boundaries as well. Of course different libraries may be compiled with different compilers and different incompatible versions of STL (e.g. libstdc++, libcxx, etc.) but their C++ container types would be different and compiler simply would not allow you to pass them across incompatible modules.

Upvotes: 4

Hans Olsson
Hans Olsson

Reputation: 12507

In C++ the normal way to change the heap is to overload the global operator new/delete.

In Windows that overload is restricted to the specific dll and thus you get the problems.

In Linux the overloaded global operator is normally truly global - so the first one wins, but if you really want specific heaps in them you can achieve that (and then you need the exported allocate and free-functions):

C++ custom global new/delete overriding system libraries

Upvotes: 1

Related Questions