Google sanitizers和其他内存工具的对比
AddressSanitizer | Valgrind/Memcheck | Dr. Memory | Mudflap | Guard Page | gperftools | |
---|---|---|---|---|---|---|
technology | CTI | DBI | DBI | CTI | Library | Library |
ARCH | x86, ARM, PPC | x86, ARM, PPC, MIPS, S390X, TILEGX | x86 | all(?) | all(?) | all(?) |
OS | Linux, OS X, Windows, FreeBSD, Android, iOS Simulator | Linux, OS X, Solaris, Android | Windows, Linux | Linux, Mac(?) | All (1) | Linux, Windows |
Slowdown | 2x | 20x | 10x | 2x-40x | ? | ? |
Heap OOB | yes | yes | yes | yes | some | some |
Stack OOB | yes | no | no | some | no | no |
Global OOB | yes | no | no | ? | no | no |
UAF | yes | yes | yes | yes | yes | yes |
UAR | yes | no | no | no | no | no |
UMR | no(MemorySanitizer) | yes | yes | ? | no | no |
Leaks | yes | yes | yes | ? | no | no |
DBI: dynamic binary instrumentation
CTI: compile-time instrumentation
UMR: uninitialized memory reads
UAF: use-after-free (aka dangling pointer)
UAR: use-after-return
OOB: out-of-bounds
x86: includes 32- and 64-bit.
mudflap was removed in GCC 4.9, as it has been superseded by AddressSanitizer.
Guard Page: a family of memory error detectors (Electric fence or DUMA on Linux, Page Heap on Windows, libgmalloc on OS X)
gperftools: various performance tools/error detectors bundled with TCMalloc. Heap checker (leak detector) is only available on Linux. Debug allocator provides both guard pages and canary values for more precise detection of OOB writes, so it's better than guard page-only detectors.
AddressSanitizer(ASAN)可以检查的错误类型:
ASAN是一个执行速度非常快的工具,典型的程序在加上ASAN后,执行时间只会增加1倍。
ASAN工具由一个编译器插桩模块(当前实现为LLVM的一个pass)和一个运行库(替换malloc函数等)组成。
// RUN: clang -O -g -fsanitize=address %t && ./a.out
int main(int argc, char **argv) {
int *array = new int[100];
delete [] array;
return array[argc]; // BOOM
}
// RUN: clang -O -g -fsanitize=address %t && ./a.out
int main(int argc, char **argv) {
int *array = new int[100];
array[0] = 0;
int res = array[argc + 100]; // BOOM
delete [] array;
return res;
}
// RUN: clang -O -g -fsanitize=address %t && ./a.out
int main(int argc, char **argv) {
int stack_array[100];
stack_array[1] = 0;
return stack_array[argc + 100]; // BOOM
}
// RUN: clang -O -g -fsanitize=address %t && ./a.out
int global_array[100] = {-1};
int main(int argc, char **argv) {
return global_array[argc + 100]; // BOOM
}
// RUN: clang -O -g -fsanitize=address %t && ./a.out
// By default, AddressSanitizer does not try to detect
// stack-use-after-return bugs.
// It may still find such bugs occasionally
// and report them as a hard-to-explain stack-buffer-overflow.
// You need to run the test with ASAN_OPTIONS=detect_stack_use_after_return=1
int *ptr;
__attribute__((noinline))
void FunctionThatEscapesLocalObject() {
int local[100];
ptr = &local[0];
}
int main(int argc, char **argv) {
FunctionThatEscapesLocalObject();
return ptr[argc];
}
// RUN: clang -O -g -fsanitize=address -fsanitize-address-use-after-scope \
// use-after-scope.cpp -o /tmp/use-after-scope
// RUN: /tmp/use-after-scope
// Check can be disabled in run-time:
// RUN: ASAN_OPTIONS=detect_stack_use_after_scope=0 /tmp/use-after-scope
volatile int *p = 0;
int main() {
{
int x = 0;
p = &x;
}
*p = 5;
return 0;
}
$ cat tmp/init-order/example/a.cc
#include
extern int extern_global;
int __attribute__((noinline)) read_extern_global() {
return extern_global;
}
int x = read_extern_global() + 1;
int main() {
printf("%d\n", x);
return 0;
}
$ cat tmp/init-order/example/b.cc
int foo() { return 42; }
int extern_global = foo();
由于x和x依赖的extern_global处于不同的编译单元,所以x的初值依赖编译单元的初始化执行顺序。
$ cat memory-leak.c
#include
void *p;
int main() {
p = malloc(7);
p = 0; // The memory is leaked here.
return 0;
}
$ clang -fsanitize=address -g memory-leak.c
$ ./a.out
=================================================================
==7829==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 7 byte(s) in 1 object(s) allocated from:
#0 0x42c0c5 in __interceptor_malloc /usr/home/hacker/llvm/projects/compiler-rt/lib/asan/asan_malloc_linux.cc:74
#1 0x43ef81 in main /usr/home/hacker/memory-leak.c:6
#2 0x7fef044b876c in __libc_start_main /build/buildd/eglibc-2.15/csu/libc-start.c:226
SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s).
ASAN技术本质上既通过编译器插桩,实现访问地址前对地址进行检查。
原始代码:
*address = ...; // or: ... = *address;
插桩后代码:
if (IsPoisoned(address)) {
ReportError(address, kAccessSize, kIsWrite);
}
*address = ...; // or: ... = *address;
ASAN技术的核心就是实现一个非常快的IsPoisoned和用非常少的指令实现ReportError。
进程的虚拟内存空间被分割成了两个部分:
MemToShadow具体实现为8byte的主内存对应1byte的影子内存,1byte的影子内存总共有9种不同的值,分别代表不同的含义:
基于影子内存的值,再通过编译器插桩,可以在内存访问的指令前后,插入检查代码:
// Check the cases where we access first k bytes of the qword
// and these k bytes are unpoisoned.
bool SlowPathCheck(shadow_value, address, kAccessSize) {
last_accessed_byte = (address & 7) + kAccessSize - 1;
return (last_accessed_byte >= shadow_value);
}
byte *shadow_address = MemToShadow(address);
byte shadow_value = *shadow_address;
if (shadow_value) {
if (SlowPathCheck(shadow_value, address, kAccessSize)) {
ReportError(address, kAccessSize, kIsWrite);
}
}
如果对MemToShadow传入影子内存的地址ShadowAddr,会得到ShadowGap region的地址,是不可寻址的,程序读ShadowGap region的地址的值时,会直接挂死。
64-bit的程序,ASAN的内存布局:
[0x10007fff8000, 0x7fffffffffff] HighMem
[0x02008fff7000, 0x10007fff7fff] HighShadow
[0x00008fff7000, 0x02008fff6fff] ShadowGap
[0x00007fff8000, 0x00008fff6fff] LowShadow
[0x000000000000, 0x00007fff7fff] LowMem
Shadow = (Mem >> 3) + 0x7fff8000;
32-bit的程序,ASAN的内存布局:
[0x40000000, 0xffffffff] HighMem
[0x28000000, 0x3fffffff] HighShadow
[0x24000000, 0x27ffffff] ShadowGap
[0x20000000, 0x23ffffff] LowShadow
[0x00000000, 0x1fffffff] LowMem
Shadow = (Mem >> 3) + 0x20000000;
原始代码:
void foo() {
char a[8];
...
return;
}
编译器插桩后的代码:
void foo() {
char redzone1[32]; // 32-byte aligned
char a[8]; // 32-byte aligned
char redzone2[24];
char redzone3[32]; // 32-byte aligned
int *shadow_base = MemToShadow(redzone1);
shadow_base[0] = 0xffffffff; // poison redzone1
shadow_base[1] = 0xffffff00; // poison redzone2, unpoison 'a'
shadow_base[2] = 0xffffffff; // poison redzone3
...
shadow_base[0] = shadow_base[1] = shadow_base[2] = 0; // unpoison all
return;
}
由于主内存和影子内存是8:1压缩的,所以对非对齐的内存访问越界,会存在漏报,示例如下:
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);
*u = 1; // Access to range [6-9]
malloc:申请内存时,在内存前后加上redzone,redzone对应的影子内存值设置为不可寻址,这样堆内存访问越界时,会上报错误。
free:内存释放后,将对应区域的影子内存值设置为不可寻址,并将内存放入到隔离区一段时间,这样在隔离时间内,如果对该内存进行读写,会上报错误。
ReportError当前实现为一个函数调用,通常实现为3条指令:
函数的具体实现由ASAN运行库提供,如__asan_report_load8。
MemorySanitizer(MSAN)用于检查C/C++程序中读取未初始化的栈内存或堆内存的问题。
MSAN只实现了Valgrind类似功能的一个子集,但是运行效率远高于Valgrind。
在编译和链接程序的时候,加上-fsanitize=memory -fPIE -pie选项,示例如下:
% cat umr.cc
#include
int main(int argc, char** argv) {
int* a = new int[10];
a[5] = 0;
if (a[argc])
printf("xx\n");
return 0;
}
%clang -fsanitize=memory -fPIE -pie -fno-omit-frame-pointer -g -O2 umr.cc
% ./a.out
==6726== WARNING: MemorySanitizer: UMR (uninitialized-memory-read)
#0 0x7fd1c2944171 in main umr.cc:6
#1 0x7fd1c1d4676c in __libc_start_main /build/buildd/eglibc-2.15/csu/libc-start.c:226
如果加上-fsanitize-memory-track-origins选项,MSAN可以追踪到内存申请的位置,对上述例子,加上选项后,错误报告如下:
%clang -fsanitize=memory -fsanitize-memory-track-origins -fPIE -pie -fno-omit-frame-pointer -g -O2 umr.cc
% ./a.out
==6726== WARNING: MemorySanitizer: UMR (uninitialized-memory-read)
#0 0x7fd1c2944171 in main umr.cc:6
#1 0x7fd1c1d4676c in __libc_start_main /build/buildd/eglibc-2.15/csu/libc-start.c:226
ORIGIN: heap allocation:
#0 0x7f5872b6a31b in operator new[](unsigned long) msan_new_delete.cc:39
#1 0x7f5872b62151 in main umr.cc:4
#2 0x7f5871f6476c in __libc_start_main /build/buildd/eglibc-2.15/csu/libc-start.c:226
Valgrind Memcheck:使用VBits和ABits的影子内存,映射主内存的每一个Byte的状态:
Valgrind只在未初始化的内存可能影响程序的行为时上报错误,几乎可以做到零误报。
缺点是程序执行时间会增加20倍,在被分析程序是多线程时会恶化得更严重。
Dr. Memory是一个基于DynamoRIO二进制翻译系统的工具,技术原理和Valgrind Memcheck类似。
由于DynamoRIO是一个多线程的二进制翻译系统,所以Dr. Memory会比Valgrind运行快2倍左右。
但是同样由于多线程的影响,可能会存在并行更新相邻的影子内存的bit的情况,导致误报。
Memcheck和Dr. Memory由于同时检查读取未初始化内存和内存寻址类问题,导致较多的额外开销(执行效率和内存)。
MemorySanitizer只检测读取未初始化内存类问题,可以大幅降低额外开销。(内存寻址类问题通过ASAN检查)
并且,相比于其他的工具都基于动态二进制翻译,MemorySanitizer是第一个基于静态编译器插桩的工具,执行效率更高。
MemorySanitizer采用1:1的影子内存,既主内存中的1byte对应影子内存中的1byte。
影子内存的地址可以用非常简单的计算得到:ShadowAddr = Addr & ShadowMask
ShadowMask是一个常量,具体的值是平台相关的,以x86_64 Linux为例,ShadowMask = ~0x400000000000。
内存映射如下图所示:
Shadow Region中的每1bit对应主内存中的每1bit,该bit为0表示已初始化,为1表示未初始化。
为了做Origin Tracking(既追踪内存申请的位置),申请了1个相同大小的Origin Region,紧跟在Shadow Region后面。
Origin Region和Shadow Region都采用MAP NORESERVE标志进行mmap。
所有新申请的内存都是有毒的(poisoned),关联的影子内存被填充为0xFF,表示所有的bit都未初始化。
C++标准规定对未初始化的对象做左值到右值转换(lvalue-to-rvalue conversion)属于未定义行为(Undefined Behavior)。
但在实际情况下:
考虑到这些情况,MemorySanitizer允许未初始化内存的拷贝操作和其他类似的安全的操作,不上报错误。
为了处理这些例外情况,MemorySanitizer实现了影子传播算法:既给编译器的临时变量赋一个影子值,并写入对应的主内存,同时把这个值写入到对应的影子内存。
一部分操作符要求操作数必须是已初始化的内存:条件分支、系统调用、指针解引用等。如果操作数未初始化,则会上报错误。
MemorySanitizer插桩通过LLVM的一个优化Pass实现。
不同于AddressSanitizer/ThreadSanitizer只关心内存访问,MemorySanitizer需要处理所有的LLVM IR指令:检查操作数的影子值,计算结果的影子值。
MemorySanitizer为每个IR中的临时变量创建了一个临时变量,存放它的影子值。影子值的类型由如下规则确定:
示例如下:
Shadow(iN) = iN
Shadow(float) = i32
Shadow(double) = i64
Shadow(i8*) = i64
Shadow(< 4 x float >) =< 4 x i32 >
Shadow({double, {float, i1}}) = {i64, {i32, i1}}
对指令A = op B,C,会生成一个额外的指令,A' = op' B,C,B',C'。A',B',C'为A,B,C对应的影子内存的值。
典型的基础指令的影子传播规则:
A = load P check P′, A′ = load (P & ShadowMask)
store P, A check P′,store (P & ShadowMask), A′
A = const A′ = 0
A = undef A′ = 0xff
A = B & C A′ = (B′ & C′)|(B & C′)|(B′ & C)
A = B | C A′ = (B′ & C′)|(~ B & C′)|(B′ & ~ C)
A = B xor C A′ = B′ | C′
A = B << C A′ = (sign-extend(C′ != 0))|(B′ << C)
近似传播
有些op对应的op'实现代价高,考虑到效率,会采用近似传播的方式,这样会导致少量的漏报。
近似传播需要满足如下2个基本原则:
一个近似传播的例子:A = B + C => A' = B' | C'
整数乘法
整数乘法的处理有一定的技巧性。
如果一个操作数的低位有1个或多个0,那么可以将乘法分解为一个左移加乘法:
A = B * C => A = B * (C * 2^D) => A = (B << D) * C
这样就可以将影子传播实现为:
A = (B << D) * C => A' = B' << D
不过通常情况下,影子传播实现为:
A = B * C => A' = B' | C'
关系比较
关系比较影子传播简单实现为:A = B > C => A' = B' | C'。
如果B或者C不是完全初始化的,会导致误报,示例如下:
struct S { int a : 3; int b : 5; };
bool f(S *s ) { return s -> b; }
表达式s->b的结果可能会被优化为:*( unsigned char *) s > 7。
这样如果使用上面的简单影子传播就会导致误报。
为了解决这个问题,MemorySanitizer设计了一个更复杂的影子传播。
给定一个任意的无符号整数X和对应的影子值X'(未定义bits的掩码),则X的取值范围为:[VMin(X, X′), VMax(X, X′)]。
其中VMin(X, X′) = X &(~ X′), VMax(X, X′) = X | X′。
对有符号数,VMin和VMax的计算更复杂,但原理类似。
所以A = B < C => A′ =((VMin(B, B′) < VMax(C, C′)) xor(VMax(B, B′) < VMin(C, C′)),既两个数的范围的交集为空。
实际在MemorySanitizer中,如果操作符的至少有1个操作数时常数时,会使用复杂的影子传播,其余场景均使用简单的影子传播。
相等比较
和关系比较类似,相等比较也存在一些场景操作数是局部初始化的。
相等比较的结果是确定的场景有2种:
A = (B == C)可以转换为:D = B xor C,A = (D == 0)
这样,A = (B == C)的影子传播,就可以实现为:
D' = B' | C'
A' = (!(D & ~D'))&&(D'!=0)
三元操作符
A = B ? C : D => A′ = B′ ? [(C xor D)| C′ | D′] :[B ? C′ : D′]
[(C xor D)| C′ | D′]的含义是,如果存在一个bit位i,Ci'和Di'都是确定的,且Ci和Di相等,则Ai'也是确定的。
矢量指令
矢量指令和单个指令的影子传播规则相同,既对矢量中的操作符,逐个应用单个指令的操作符的影子传播。
线程安全性
在多线程环境下,影子值的更新和主内存的更新相同,都使用并行更新的方式。
由于LLVM IR也遵从了C++的内存模型,避免了数据竞争。如果有一个store在load前,那么对应的影子值的store和load也会遵从相同的顺序。
需要特殊处理的是原子操作,每一个原子写按理来说也需要对相应的影子值进行原子写操作,但是这种实现会严重拖慢程序的原子操作的执行速度。
MemorySanitizer实际采用了一种更快的实现方式:
这样如果有一个原子store在原子load前面,影子值的store一定会在load前面。
CAS(compare and swap)和RMW(read-modify-write)操作使用和原子操作相同的方式,既将影子值store为0插桩到指令前。
函数调用
MemorySanitizer采用一个特殊的TLS的数组在函数调用方和函数之间传递参数的影子值。
对变长参数的函数,MemorySanitizer对va_start进行了插桩,这个实现是平台特定的。
MemorySanitizer的运行库复用了很多AddressSanitizer和ThreadSanitizer的代码。
同时为了实现libc库相关的内存操作的影子内存的更新,打桩了接近300个标准C库函数。
众所周知,使用未初始化的内存类问题是非常难定位的。因为问题的根源在于某些应该做的操作没有做(初始化内存)。
要知道在哪些插入应该做的操作,超出了工具的能力范围。
为了解决这个问题,MemorySanitizer实现了内存溯源的功能。
在内存溯源的模式下,MemorySanitizer使用一个32bit的源值(Origin Value)关联主内存中的值。
这个值用于标识内存申请,既哪里产生了这些未初始化的值。(堆或者栈等)
为了维护源值,需要插入额外的指令,比如对三元表达式,源值的传播方式:
A'' = (B') ? (B'') : (B ? C'' : D'')
AddressSanitizer使用1byte的影子内存对应主内存中的8byte,并且使用redzone去检测内存溢出,使用隔离区防止内存释放后使用。所有这些措施都带来了额外的内存开销。
AArch64架构硬件上有一个Address Tagging的功能(or top-byte-ignore, TBI), 允许软件将一个64位指针的最高8bit当做一个tag来使用。
HWASAN使用了这个tag机制实现了类似AddressSanitizer的功能。
Intel的Linear Address Masking (LAM)技术也为x86_64架构提供了类似的功能,不过当前的硬件上支持较少。目前HWASAN主要使用了页别名的技术实现了部分功能。
UndefinedBehaviorSanitizer是一个运行快速的UB(程序未定义行为)检测器。在程序编译期修改程序的行为,捕获多种UB:
https://github.com/google/sanitizers/wiki/AddressSanitizer
https://github.com/google/sanitizers/wiki/MemorySanitizer
https://llvm.org/devmtg/2011-11/Serebryany_FindingRacesMemoryErrors.pdf