欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

GoogleSanitizers

时间:2023-06-26

Google的sanitizers一共有5种:

AddressSanitizer (检查寻址问题) :包含LeakSanitizer (检查内存泄漏问题)ThreadSanitizer:检查数据竞争和死锁问题(支持C++和Go)MemorySanitizer:检查使用未初始化的内存问题HWASAN(Hardware-assisted AddressSanitizer):AddressSanitizer的变种,相比AddressSanitizer消耗的内存更少。UBSan:检查使用语言的UndefinedBehavior问题。

Google sanitizers和其他内存工具的对比

AddressSanitizerValgrind/MemcheckDr、MemoryMudflapGuard PagegperftoolstechnologyCTIDBIDBICTILibraryLibraryARCHx86, ARM, PPCx86, ARM, PPC, MIPS, S390X, TILEGXx86all(?)all(?)all(?)OSLinux, OS X, Windows, FreeBSD, Android, iOS SimulatorLinux, OS X, Solaris, AndroidWindows, LinuxLinux, Mac(?)All (1)Linux, WindowsSlowdown2x20x10x2x-40x??Heap OOByesyesyesyessomesomeStack OOByesnonosomenonoGlobal OOByesnono?nonoUAFyesyesyesyesyesyesUARyesnononononoUMRno(MemorySanitizer)yesyes?nonoLeaksyesyesyes?nono

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

AddressSanitizer(ASAN)可以检查的错误类型:

Use after free(dangling pointer dereference):内存释放后继续使用,悬挂指针问题。Heap buffer overflow:堆内存溢出Stack buffer overflow:栈内存溢出Global buffer overflow:全局内存溢出(如全局变量)Use after return:局部变量在函数返回后使用Use after scope:局部变量在作用范围外使用Initialization order bugs:初始化顺序问题Memory leaks:内存泄漏

ASAN是一个执行速度非常快的工具,典型的程序在加上ASAN后,执行时间只会增加1倍。
ASAN工具由一个编译器插桩模块(当前实现为LLVM的一个pass)和一个运行库(替换malloc函数等)组成。

错误类型典型案例

Use after free

// RUN: clang -O -g -fsanitize=address %t && ./a.outint main(int argc, char **argv) { int *array = new int[100]; delete [] array; return array[argc]; // BOOM}

Heap buffer overflow

// RUN: clang -O -g -fsanitize=address %t && ./a.outint main(int argc, char **argv) { int *array = new int[100]; array[0] = 0; int res = array[argc + 100]; // BOOM delete [] array; return res;}

Stack buffer overflow

// RUN: clang -O -g -fsanitize=address %t && ./a.outint main(int argc, char **argv) { int stack_array[100]; stack_array[1] = 0; return stack_array[argc + 100]; // BOOM}

Global buffer overflow

// RUN: clang -O -g -fsanitize=address %t && ./a.outint global_array[100] = {-1};int main(int argc, char **argv) { return global_array[argc + 100]; // BOOM}

Use after return

// 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=1int *ptr;__attribute__((noinline))void FunctionThatEscapesLocalObject() { int local[100]; ptr = &local[0];}int main(int argc, char **argv) { FunctionThatEscapesLocalObject(); return ptr[argc];}

Use after scope

// 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-scopevolatile int *p = 0;int main() { { int x = 0; p = &x; } *p = 5; return 0;}

Initialization order bugs

$ 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("%dn", x); return 0;}$ cat tmp/init-order/example/b.ccint foo() { return 42; }int extern_global = foo();

由于x和x依赖的extern_global处于不同的编译单元,所以x的初值依赖编译单元的初始化执行顺序。

Memory leaks

$ 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 leaksDirect 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:226SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s).

ASAN技术原理

ASAN技术本质上既通过编译器插桩,实现访问地址前对地址进行检查。

原始代码:

*address = ...; // or: ..、= *address;

插桩后代码:

if (IsPoisoned(address)) { ReportError(address, kAccessSize, kIsWrite);}*address = ...; // or: ..、= *address;

ASAN技术的核心就是实现一个非常快的IsPoisoned和用非常少的指令实现ReportError。

IsPoisoned的基础:内存映射和插桩

进程的虚拟内存空间被分割成了两个部分:

主应用内存(main memory):应用程序代码使用的内存。影子内存(shadow memory):存放影子值(元数据)的内存。影子内存和主内存存在关联:将一些特殊的值写入影子内存,既对主内存中对应的内存投毒。

MemToShadow具体实现为8byte的主内存对应1byte的影子内存,1byte的影子内存总共有9种不同的值,分别代表不同的含义:

0:8 byte无毒,可以寻址。负数:8 byte全有毒。正数k:前k byte无毒,后8-k byte有毒。

基于影子内存的值,再通过编译器插桩,可以在内存访问的指令前后,插入检查代码:

// 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;

IsPoisoned实现1:栈越界检查

原始代码:

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]

IsPoisoned实现2:堆内存检查

malloc:申请内存时,在内存前后加上redzone,redzone对应的影子内存值设置为不可寻址,这样堆内存访问越界时,会上报错误。
free:内存释放后,将对应区域的影子内存值设置为不可寻址,并将内存放入到隔离区一段时间,这样在隔离时间内,如果对该内存进行读写,会上报错误。

ReportError:紧凑的错误上报

ReportError当前实现为一个函数调用,通常实现为3条指令:

将地址保存到%rax(%eax)执行ud2指令,产生SIGILL将访存类型和大小编码到一个byte中

函数的具体实现由ASAN运行库提供,如__asan_report_load8。

MemorySanitizer

MemorySanitizer(MSAN)用于检查C/C++程序中读取未初始化的栈内存或堆内存的问题。

MSAN只实现了Valgrind类似功能的一个子集,但是运行效率远高于Valgrind。

使用MemorySanitizer

在编译和链接程序的时候,加上-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("xxn"); 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

MSAN技术原理(MemorySanitizer: fast detector of uninitialized memory use in C++)

Valgrind Memcheck

Valgrind Memcheck:使用VBits和ABits的影子内存,映射主内存的每一个Byte的状态:

可寻址并初始化不可寻址可寻址但未初始化可寻址并部分初始化

Valgrind只在未初始化的内存可能影响程序的行为时上报错误,几乎可以做到零误报。

缺点是程序执行时间会增加20倍,在被分析程序是多线程时会恶化得更严重。

Dr、Memory

Dr、Memory是一个基于DynamoRIO二进制翻译系统的工具,技术原理和Valgrind Memcheck类似。

由于DynamoRIO是一个多线程的二进制翻译系统,所以Dr、Memory会比Valgrind运行快2倍左右。

但是同样由于多线程的影响,可能会存在并行更新相邻的影子内存的bit的情况,导致误报。

MemorySanitizer和其他工具的对比

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)。

但在实际情况下:

1、编译器通常允许读取integer或者float的未初始化的值,因为这样的操作在真实的代码中很常见。2、MemorySanitizer使用编译器的优化pass实现,可能存在源代码之外的读未初始化内存操作,比如拷贝构造函数拷贝的padding部分。

考虑到这些情况,MemorySanitizer允许未初始化内存的拷贝操作和其他类似的安全的操作,不上报错误。

为了处理这些例外情况,MemorySanitizer实现了影子传播算法:既给编译器的临时变量赋一个影子值,并写入对应的主内存,同时把这个值写入到对应的影子内存。

一部分操作符要求操作数必须是已初始化的内存:条件分支、系统调用、指针解引用等。如果操作数未初始化,则会上报错误。

插桩

MemorySanitizer插桩通过LLVM的一个优化Pass实现。

不同于AddressSanitizer/ThreadSanitizer只关心内存访问,MemorySanitizer需要处理所有的LLVM IR指令:检查操作数的影子值,计算结果的影子值。

MemorySanitizer为每个IR中的临时变量创建了一个临时变量,存放它的影子值。影子值的类型由如下规则确定:

标量类型(integer,float,pointer):一个相同位宽的integerSIMD矢量类型:一个相同长度的矢量。聚合类型:相同的聚合类型。

示例如下:
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个基本原则:

1、操作符的操作数影子值都为0,操作符的结果的影子值也是0。2、操作符中有操作数的影子值有一些bit为非0,且这些bit会影响操作符的结果,那么操作符的结果的影子值为非0。

一个近似传播的例子: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种:

两个操作数B和C都是确定的(B’ = C' = 0)存在1个bit位i,Bi’ = Ci' = 0且Bi != Ci

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实际采用了一种更快的实现方式:

在原子load的后面插桩影子内存的普通load在原子store的前面插桩影子内存的普通store,store的值为0

这样如果有一个原子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库函数。

内存溯源(Origin Tracking)

众所周知,使用未初始化的内存类问题是非常难定位的。因为问题的根源在于某些应该做的操作没有做(初始化内存)。

要知道在哪些插入应该做的操作,超出了工具的能力范围。

为了解决这个问题,MemorySanitizer实现了内存溯源的功能。

在内存溯源的模式下,MemorySanitizer使用一个32bit的源值(Origin Value)关联主内存中的值。

这个值用于标识内存申请,既哪里产生了这些未初始化的值。(堆或者栈等)

为了维护源值,需要插入额外的指令,比如对三元表达式,源值的传播方式:

A'' = (B') ? (B'') : (B ? C'' : D'')

Hardware-assisted AddressSanitizer

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

UndefinedBehaviorSanitizer是一个运行快速的UB(程序未定义行为)检测器。在程序编译期修改程序的行为,捕获多种UB:

-fsanitize=alignment: 使用非对齐的指针或者创建一个非对齐的引用。-fsanitize=bool: 读取一个bool值,它的值不为true,也不为false.-fsanitize=builtin: 传递非法值给编译器的builtin.-fsanitize=bounds: 数组索引越界,仅针对数组上界可以静态确定的场景。-fsanitize=enum: 读取一个枚举值,它的值不在枚举的表达范围内。-fsanitize=float-cast-overflow: 浮点数转换溢出。-fsanitize=float-divide-by-zero: 浮点数除0。-fsanitize=function: 使用函数指针调用函数,类型不匹配。(Darwin/Linux, C++ and x86/x86_64 only).-fsanitize=implicit-unsigned-integer-truncation, -fsanitize=implicit-signed-integer-truncation: 将一个大位宽的整型隐式转换为小位宽的整型。注:非UB-fsanitize=implicit-integer-sign-change: 隐式类型转换改变了符号。注:非UB-fsanitize=integer-divide-by-zero: 整数除以0.-fsanitize=nonnull-attribute: 传递空指针给声明为非空的函数参数。-fsanitize=null: 解引用空指针或者创建一个空引用。-fsanitize=nullability-arg: 传递空指针给使用_Nonnull注解的函数参数。which is annotated with _Nonnull.-fsanitize=nullability-assign: 传递空指针给使用_Nonnull注解的左值。-fsanitize=nullability-return: 返回空指针给使用_Nonnull注解返回值的函数。-fsanitize=objc-cast: 隐式转换一个ObjC的对象指针为一个不匹配的类型。目前只支持Darwin.-fsanitize=object-size: 尝试使用对象中可能被优化掉的部分内存。-fsanitize=pointer-overflow: 指针算术运算越界,或者参与指针运算的指针的值为空指针。-fsanitize=return: 有返回值的函数的结尾没有return语句。-fsanitize=returns-nonnull-attribute: 有nonnull属性的函数返回空指针。-fsanitize=shift: 移位的位宽超过或者等于左值的位宽或者小于0,或者移位的数本身是负数。同时,如果是有符号数左移,会检查符号位溢出。-fsanitize=unsigned-shift-base: 检查无符号数移位是否溢出.-fsanitize=signed-integer-overflow: 有符号整数溢出。-fsanitize=unreachable: 检查控制流达到不可达的程序点,如__builtin_unreachable。-fsanitize=unsigned-integer-overflow: 无符号整数溢出。-fsanitize=vla-bound: 变长数组的长度为非正数。-fsanitize=vptr: 对象的虚表表示这个对象的动态类型是错的参考材料

https://github.com/google/sanitizers/wiki/AddressSanitizer

https://github.com/google/sanitizers/wiki/MemorySanitizer

https://llvm.org/devmtg/2011-11/Serebryany_FindingRacesMemoryErrors.pdf

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。