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
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: