Search code examples
c++g++initializer-listctor-initializerstack-corruption

C++ class initializer list in library causes stack corruption


I've distilled my problem down to a (hopefully) very simple example. At a high level, I have a shared library which provides a class implementation, and a main executable which uses the library. In my example, the library is then extended with CPPFLAG=-DMORE so that the class initializer list now has one additional member. Since the ABI signature of the library does not changed, there should be no need to recompile the executable. Yet, in my case, I get a coredump. I do not understand why this is an issue. Can someone please point out where I am going wrong?

Environment

Linux, amd64, gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)

Setup

Using the code provided below, do the following:

  1. make clean
  2. make main (which also builds base-orig version of the library)
  3. ./main which runs just fine
  4. make base_more
  5. ./main which crashes with
    Base hello
    Base class constructor has non-null MORE
    Base goodbye
    Base class destructor has non-null MORE
    *** stack smashing detected ***: terminated
    Aborted (core dumped)
    

Code

library header (base.h)

#ifdef MORE
    #include <functional>
#endif

class base
{
  public:
    base();
    ~base();

  private:

#ifdef MORE
    std::function<void()> more_;
#endif
};

library source (base.cpp)

#include "base.h"
#include <iostream>

#ifdef MORE
void hi()
{
  std::cout << "Hello from MORE" << std::endl;
}
#endif

base::base()
#ifdef MORE
   : more_(std::bind(&hi))
#endif

{
  std::cout << "Base hello " << std::endl;

#ifdef MORE
  if (nullptr != more_)
  {
    std::cout << "Base class constructor has non-null MORE" << std::endl;
  }
#endif
}

base::~base()
{
  std::cout << "Base goodbye " << std::endl;

#ifdef MORE
  if (nullptr != more_)
  {
    std::cout << "Base class destructor has non-null MORE" << std::endl;
  }
#endif
}

Executable (main.cpp)

#include "base.h"

int main()
{
  base x;
}

Makefile

base_orig:
        g++ -O0 -g -fPIC -shared -olibbase.so base.cpp
        objdump -C -S -d libbase.so > orig.objdump

base_more:
        g++ -O0 -g -DMORE -fPIC -shared -olibbase.so base.cpp
        objdump -C -S -d libbase.so > more.objdump

main: base_orig
        g++ -O0 -g -Wextra -Werror main.cpp -o main -L. -Wl,-rpath=. -lbase
        objdump -C -S -d main > main.objdump

clean:
        rm -f main libbase.so

I tried to go through the objdump output to figure out why the stack is getting corrupted, but alas, my knowledge of amd64 assembly is rather weak.


Solution

  • You're trying to fit a probably 24 or 32 byte std::function member into a 1-byte empty class. There simply isn't enough space to hold it.

    When you say base x; in main, main does two things:

    1. Reserve enough memory to hold a base object
    2. Pass a pointer to that memory to base's constructor

    Since MORE wasn't defined when you compiled main, as far as it is concerned, base has no data members. Therefore it will only reserve 1 byte of memory (since every object needs a unique address, even if it's empty). It then passes a pointer to that 1 byte of memory to base's constructor, which is located in your dynamically-loaded library. Since MORE was defined when that library was compiled, it thinks a base object has one std::function member and will try to initialize that member in the memory that main passed it a pointer to. There isn't enough space there, and so it ends up initializing more_ in memory that was in used by something else.

    Remember, a pointer contains no information about how much memory is available where it points, so base's constructor must assume that it was passed a pointer to enough memory to hold a base object. That means that main and base's constructor need to agree on how big a base object is.


    The way to avoid this issue is to avoid passing actual objects across library boundaries and only ever pass pointers.

    That is, you can make base's constructor private and add a static function std::unique_ptr<base> make_base(). That way it becomes the sole responsibility of the library to allocate memory for base objects, and you can never encounter this situation where the main program and the library disagree on how much memory is needed to hold a base. This does, of course, come with some overhead, since it requires that all base objects be dynamically-allocated. It's also important to make sure the main program and library are compiled using the same compiler and C++ standard library so that you can make they agree on how big any standard library types that you do pass across the library boundary are (such as std::unique_ptr or std::string).