更新:我创造了更多M,但仍然是CVE再现崩溃。摘要:删除Bool* bools_
类中Base
字段的所有使用(但仍必须定义或不发生崩溃)。还从Base::Initialize()
及其后代中删除了Rule
和虚拟方法Base
。附上了新的MCVE。
我已成功为此代码创建了一个MCVE并将其发布在下面。
一些描述性细节:代码使用虚拟基类和派生类,并且实例化的某些派生类具有构造函数,这些构造函数调用从“基础”类继承的非虚方法(实际上是派生类,但在继承层次结构比我所谓的“派生”类要初始化“基类”类数据。该方法调用在派生类中重写的虚方法。我意识到这是一件危险的事情,但是从我(可能是有限的)对C ++的理解来看,它似乎应该有效,因为派生类构造函数的主体在设置“基”类虚拟表之前不会执行。在任何情况下,在调用“base”类的初始化方法时都不会发生段错误。
段错误发生在“base”类构造函数中,并且仅在构造函数的主体为空时发生。如果我向构造函数添加一个调试行,以便在到达该点时打印出来,则打印出调试行并且代码正常运行。我的猜测是,由于某种原因,编译器正在优化在“base”类的构造函数的主体执行之前应该发生的初始化,包括设置vtable。
正如主题所说,这个代码在没有使用Apple的g ++或g ++ 7.2.0进行优化编译时运行正常,并且在使用g ++ 7.2.0编译甚至-O3时运行正常。使用Apple的LLVM实现g ++编译-O2
或-O3
时,它只会出现段错误。该编译器的g++ --version
输出为:
% /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
MCVE如下。
#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;
}
链接到错误报告:https://bugreport.apple.com/web/
参考编号36382481
答案 0 :(得分:14)
这看起来像是由于无效生成未对齐的SSE存储而导致的Clang中的错误。以下是基于您的代码的最小示例:
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; }
这是由Clang生成的布局(GCC使用相同的布局):
*** 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]
我们可以看到Base3
与Base1
合并,因此他们共享地址。 Base2
由Base3
实例化,之后放置 8字节偏移量,将Base2
实例与 8字节对齐,即使{ {1}}是16.这是仍然正确的行为,因为这是alignof(Base2)
中所有成员字段之间的最大对齐。从虚拟基类Base2
继承的对齐不需要保留,因为Base1
由派生类Base1
实例化,派生类Base3
负责正确对齐Base1
。
问题在于Clang生成的代码:
mov rbx,rdi ; rdi contains this pointer
...
xorps xmm0,xmm0
movaps XMMWORD PTR [rbx+0x10],xmm0
Clang决定使用单data1_
条指令初始化data2_
和movaps
,这需要16字节对齐,但Base2
实例仅对齐8字节,导致段错误。
看起来Clang认为它可以使用16字节对齐的存储,因为alignof(Base2)
是16,但对于具有虚拟基础的类,这种假设是错误的。
如果您需要临时解决方案,可以使用-mno-sse
标志禁用SSE指令。请注意,这可能会对性能产生影响。
Itanium ABI文档可在此处找到:https://refspecs.linuxfoundation.org/cxxabi-1.75.html
明确提到 nvalign :
nvalign(O):对象的非虚拟对齐,即 没有虚拟基础的O的对齐。
然后有关于如何完成分配的解释:
分配虚拟基地以外的成员
如果D不是空基类或D是数据成员:从偏移量开始 dsize(C),如果需要增加以对齐到nvalign(D)for 基类或对齐(D)数据成员。将D放在此偏移处 除非这样做会导致两个组成部分(直接或间接) 具有相同偏移量的相同类型。如果是这样的组件类型 发生冲突时,将候选偏移量增加nvalign(D)表示基数 类或通过对齐(D)数据成员并再试一次,重复直到 成功发生(不会晚于sizeof(C)四舍五入到 所需的对齐方式)。
看起来Clang和GCC都尊重Itanium ABI,使用非虚拟对齐方式正确对齐Base2
。我们还可以在上面的记录布局转储中看到。
您可以使用-fsanitize=undefined
(GCC和Clang)编译您的程序,以便在运行时获得此误报警告消息:
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
目前有三个漏洞。我已经报告了所有这些: