Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ code segfaults when compiled -O with Apple's LLVM compiler, but not with g++ -7.2.0

Update: I've created an even more M, but still CVE that reproduces the crash. Summary: removed all use of the Bool* bools_ field in Base class (but it still must be defined or the crash does not happen). Also removed Base::Initialize() and the virtual method Rule from Base and its descendants. New MCVE is attached.

I've managed to create an MCVE for this code and posted it below.

Some descriptive details: the code uses virtual base and derived classes, and certain derived classes that are instantiated have constructors that call a non-virtual method inherited from a "base" class (actually a derived class, but higher up in the inheritance hierarchy than what I am calling the "derived" classes) to initialize "base" class data. That method calls a virtual method that is overridden in the derived classes. I realize that that is a dangerous thing to do, but from my (possibly limited) understanding of C++, it seems like it should work because the body of the derived class constructor does not execute until the "base" class virtual tables are set up. In any case, the segfault does NOT occur during the call to the "base" class's initialization method.

The segfault occurs in the "base" class constructor, and ONLY when the body of the constructor is empty. If I add a debugging line to the constructor to print out when that point is reached, the debugging line is printed out and the code runs normally. My guess is that for some reason the compiler is optimizing away the initializations that should happen before the body of the "base" class's constructor is executed, including the setting up of the vtable.

As the subject line says, this code runs fine when compiled without optimization using either Apple's g++ or g++ 7.2.0, and it runs fine when compiled even -O3 using g++ 7.2.0. It ONLY segfault when compiled -O2 or -O3 with Apple's LLVM implementation of g++. The output of g++ --version for that compiler is:

% /usr/bin/g++ --version

Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 9.0.0 (clang-900.0.39.2)
Target: x86_64-apple-darwin17.3.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Content

The MCVE follows.

#include <iostream>

using namespace std;

class OriginalBaseClass {
public:
  OriginalBaseClass(long double data1 = 1, long int data2 = 1) : data1_(data1), data2_(data2) { cout << "In OriginalBaseClass constructor\n"; }
private:
  long double data1_;
  long int data2_;
};

class Base : public virtual OriginalBaseClass {
public:
  Base(long int data1 = 0, long int data2 = 0) : data1_(data1), data2_(data2) { cout << "In Base constructor\n"; }
  virtual ~Base();
private:
  bool* bools_;
  long int data1_;
  long int data2_;
};

Base::~Base()
{
  cout << "In Base destructor\n";
}

class Derived_A : public virtual Base {
public:
  Derived_A() { cout << "In Derived_A constructor\n"; }
};

class Derived_B : public Derived_A {
public:
  Derived_B() : OriginalBaseClass(), Base(4, 1), Derived_A() { cout << "In Derived_B constructor\n"; }
};

int main()
{
  Derived_B Derb;
}

Link to bug report: https://bugreport.apple.com/web/

Reference number 36382481

like image 242
fizzixgal Avatar asked Jan 08 '18 21:01

fizzixgal


1 Answers

This looks like a bug in Clang caused by invalid generation of unaligned SSE stores. Below is a minimal example based on your code:

struct alignas(16) Base1 { };

struct Base2 : virtual Base1 {
    __attribute__((noinline)) Base2() : data1_(0), data2_(0) { }

    long dummy_, data1_, data2_;
};

struct Base3 : virtual Base2 { };

int main() { Base3 obj; }

This is layout generated by Clang (GCC uses the same layout):

*** Dumping AST Record Layout
         0 | struct Base1 (empty)
           | [sizeof=16, dsize=16, align=16,
           |  nvsize=16, nvalign=16]

*** Dumping AST Record Layout
         0 | struct Base2
         0 |   (Base2 vtable pointer)
         8 |   long dummy_
        16 |   long data1_
        24 |   long data2_
         0 |   struct Base1 (virtual base) (empty)
           | [sizeof=32, dsize=32, align=16,
           |  nvsize=32, nvalign=8]

*** Dumping AST Record Layout
         0 | struct Base3
         0 |   (Base3 vtable pointer)
         0 |   struct Base1 (virtual base) (empty)
         8 |   struct Base2 (virtual base)
         8 |     (Base2 vtable pointer)
        16 |     long dummy_
        24 |     long data1_
        32 |     long data2_
           | [sizeof=48, dsize=40, align=16,
           |  nvsize=8, nvalign=8]

We can see that Base3 is merged with Base1, so they share address. Base2 is instantiated by Base3 and is placed afterwards with 8 byte offset, aligning Base2 instance at 8 bytes, even though alignof(Base2) is 16. This is still correct behavior, since this is the maximum alignment among all member fields in Base2. Alignment inherited from virtual base class Base1 does not need to be preserved, as Base1 is instantiated by derived class Base3 which is responsible for aligning Base1 properly.

The problem is with the code Clang generates:

mov    rbx,rdi ; rdi contains this pointer
...
xorps  xmm0,xmm0
movaps XMMWORD PTR [rbx+0x10],xmm0

Clang decides to initialize both data1_ and data2_ with a single movaps instruction which requires 16 byte alignment, but Base2 instance is only 8-bytes aligned, leading to segfault.

Looks like Clang assumes it can use 16-byte aligned stores because alignof(Base2) is 16, but such assumption is wrong for classes with virtual bases.

If you need a temporary solution, you can disable usage of SSE instructions with -mno-sse flag. Note that this can have performance impact.


Itanium ABI document can be found here: https://refspecs.linuxfoundation.org/cxxabi-1.75.html

It explicitly mentions nvalign:

nvalign(O): the non-virtual alignment of an object, which is the alignment of O without virtual bases.

Then there is an explanation on how allocation is done:

Allocation of Members Other Than Virtual Bases

If D is not an empty base class or D is a data member: Start at offset dsize(C), incremented if necessary for alignment to nvalign(D) for base classes or to align(D) for data members. Place D at this offset unless doing so would result in two components (direct or indirect) of the same type having the same offset. If such a component type conflict occurs, increment the candidate offset by nvalign(D) for base classes or by align(D) for data members and try again, repeating until success occurs (which will occur no later than sizeof(C) rounded up to the required alignment).

Looks like both Clang and GCC honor Itanium ABI, properly aligning Base2 using non-virtual alignment. We can also see that in the record layout dump above.


You can compile your program with -fsanitize=undefined (both GCC and Clang) to get this false-positive warning message at runtime:

main.cpp:29:5: runtime error: constructor call on misaligned address 0x7ffd3b895dd8 for type 'Base2', which requires 16 byte alignment
0x7ffd3b895dd8: note: pointer points here
 e9 55 00 00  ea c6 2e 02 9b 7f 00 00  01 00 00 00 00 00 00 00  02 00 00 00 00 00 00 00  f8 97 95 34

So there are three bugs at the moment. I have reported all of them:

  • Clang's unaligned movaps bug: https://bugs.llvm.org/show_bug.cgi?id=35901
  • Clang's sanitizer bug: https://bugs.llvm.org/show_bug.cgi?id=35902
  • GCC's sanitizer bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=83780
like image 66
StaceyGirl Avatar answered Oct 16 '22 23:10

StaceyGirl