Sanitizers,一类基于编译时插桩(instrumentation)的动态测试工具,用来检测程序运行时的各种错误。
int global_array[100] = {-1};
int main(int argc, char **argv) {
return global_array[argc + 100]; // 越界访问,导致错误
}
clang++ -O1 -fsanitize=address a.cc
./a.out
10538 ERROR: AddressSanitizer global-buffer-overflow
READ of size 4 at 0x000000415354 thread T0
#0 0x402481 in main a.cc:3
#1 0x7f0a1c295c4d in __libc_start_main ??:0
#2 0x402379 in _start ??:0
0x000000415354 is located 4 bytes to the right of global
variable ‘global_array’ (0x4151c0) of size 400
main
函数,源代码第3行global_array
末尾多了4字节,越界访问了int *array = new int[100]; // 分配了 100 个 int 的堆内存
delete [] array; // 正确地释放了这块内存
return array[argc]; // 错误!使用了已经释放的内存:Use-After-Free
==30226== ERROR: AddressSanitizer heap-use-after-free
READ of size 4 at 0x7faa07fce084 thread T0
int
),地址是 0x7faa07fce084
#0 0x40433c in main a.cc:4
main()
函数的第 4 行,也就是 return array[argc];
freed by thread T0 here:
#0 0x4058fd in operator delete[](void*) _asan_rtl_
#1 0x404303 in main a.cc:3
delete [] array;
previously allocated by thread T0 here:
#0 0x405579 in operator new[](unsigned long) _asan_rtl_
#1 0x4042f3 in main a.cc:2
new int[100]
-fsanitize=address
)很好地捕捉了这个问题delete[]
之后再使用该指针int main(int argc, char **argv) {
int *array = new int[100];
int result = array[argc]; // 在 delete 之前使用
delete [] array;
return result;
}
int *g;
void LeakLocal() {
int local;
g = &local; // 把局部变量的地址赋值给全局指针
}
int main() {
LeakLocal();
return *g; // 函数返回后访问已经无效的栈内存
}
local
是 LeakLocal()
的局部变量,分配在栈上LeakLocal()
返回时,栈帧被销毁g
仍然指向已销毁的栈空间 → 未定义行为==19177==ERROR: AddressSanitizer: stack-use-after-return
READ of size 4 at 0x7f473d0000a0 thread T0
#0 0x461ccf in main a.cc:8
int
)Address is located in stack of thread T0 at offset 32 in frame
#0 0x461a5f in LeakLocal() a.cc:2
LeakLocal()
中的局部变量 local
This frame has 1 object(s):
[32, 36) 'local' <== Memory access at offset 32
local
占用的是偏移 [32, 36)
的栈空间,ASan 检测到了你访问了这块空间// 错误
int *g;
void LeakLocal() {
int local;
g = &local; // local 是局部变量,生命周期短
}
int *g;
void LeakLocal() {
g = new int(42); // 分配在堆上
}
int main() {
LeakLocal();
int result = *g;
delete g; // 别忘了释放
return result;
}
detect_stack_use_after_return=1
重要?默认 ASan 并不会检测这种错误行为,因为栈帧地址可能被重用。加上这个选项后,ASan 会将局部变量放入“后备堆”,从而保留它的元数据以检测使用。
ASAN_OPTIONS=detect_stack_use_after_return=1 ./a.out
TSan(ThreadSanitizer) 是一种 动态检测工具,用于捕捉 并发相关的错误,如:
数据竞争定义:
两个线程并发访问同一个内存地址,至少一个是写操作,并且它们之间没有同步机制(如锁)。
int counter = 0;
void *increment(void *) {
counter++; // 竞态:多个线程同时修改
return nullptr;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, nullptr, increment, nullptr);
pthread_create(&t2, nullptr, increment, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
return 0;
}
这个代码在逻辑上没错,但多个线程访问 counter
没有加锁,会导致 不可预测结果。
使用支持 TSan 的编译器(如 Clang):
clang++ -fsanitize=thread -g -O1 race.cc -o race
./race
WARNING: ThreadSanitizer: data race (pid=1234)
Write of size 4 at 0x0000004005d0 by thread T1:
#0 increment race.cc:4
Previous write of size 4 at 0x0000004005d0 by thread T2:
#0 increment race.cc:4
Location is global 'counter' of size 4 at 0x0000004005d0
SUMMARY: ThreadSanitizer: data race in counter
使用 std::mutex
保护共享数据:
#include
std::mutex mtx;
int counter = 0;
void *increment(void *) {
std::lock_guard<std::mutex> lock(mtx);
counter++; // 安全访问
return nullptr;
}
工具 | 用于检测 |
---|---|
ASan | 内存越界、use-after-free、栈错误等 |
TSan | 并发错误:数据竞争、死锁等 |
UBSan | 未定义行为(例如整数溢出、类型转换) |
MSan | 未初始化内存读取(MemorySanitizer) |
std::thread
:int X;
std::thread t([&]{ X = 42; }); // 线程 T1 写入 X
X = 43; // 主线程同时写入 X
t.join(); // 等待 T1 完成
X
。X
是共享变量,多个写入操作之间未加锁,形成数据竞争。-fsanitize=thread
捕捉到这一点,并报告了数据竞争。WARNING: ThreadSanitizer: data race
Write of size 4 at ... by thread T1:
#0 main::$_0::operator()() const race.cc:4
Previous write ... by main thread:
#0 main race.cc:5
X
进行了修改,但彼此没有任何同步。用 std::mutex
保护共享变量:
#include
int X;
std::mutex m;
int main() {
std::thread t([&] {
std::lock_guard<std::mutex> lock(m);
X = 42;
});
{
std::lock_guard<std::mutex> lock(m);
X = 43;
}
t.join();
}
#include
std::atomic<int> X;
int main() {
std::thread t([&]{ X.store(42); });
X.store(43);
t.join();
}
MSan 是一种检测 未初始化内存读取(use of uninitialized memory) 的工具。它不会像 ASan/TSan 那样关注内存越界或数据竞争,而是专注于:
变量或内存内容还未被初始化,就被读取或使用了。
int main() {
int x;
int y = x + 1; // x 没有初始化
return y;
}
运行时会被 MSan 报告为 使用未初始化值。
struct Point {
int x, y;
};
int main() {
Point p;
int z = p.x + p.y; // p.x 和 p.y 未初始化
return z;
}
clang++ -fsanitize=memory -fPIE -pie -g test.cc -o test
注意:MSan 只在 Clang 上支持,并且不支持一些系统库(如 glibc),常用于 LLVM instrumented 环境或自定义 libc(如 musl)。
Sanitizer | 检查内容 |
---|---|
ASan | 内存越界、use-after-free 等 |
TSan | 多线程数据竞争 |
MSan | 读取未初始化内存 |
UBSan | 未定义行为(除零、溢出等) |
int main(int argc, char **argv) {
int x[10]; // 数组 x 没有被完全初始化
x[0] = 1; // x[0] 初始化了
return x[argc]; // x[argc] 可能未初始化
}
argc == 0
,x[0]
会被访问,是安全的;argc == 1
,会访问 x[1]
—— 没有被初始化,MSan 报告错误。WARNING: Use of uninitialized value
#0 0x7f1c31f16d10 in main a.cc:4
Uninitialized value was created by an
allocation of 'x' in the stack frame of
function 'main'
意思是:
x[...]
,但那个位置没有被赋过值;x[10]
数组中,除 x[0]
外,其余元素未初始化;int main(int argc, char **argv) {
int x[10] = {0}; // 初始化所有元素为 0
x[0] = 1;
return x[argc];
}
或者更安全地,使用 std::array
或 std::vector
并进行显式初始化。
UBSan 是用于检测 C/C++ 程序中未定义行为(UB, Undefined Behavior) 的工具。
它不像 ASan(内存)或 TSan(线程)那样专注于特定问题,而是涵盖一大类语言层级的错误。
问题类型 | 示例代码 | 描述 |
---|---|---|
整数溢出 | int x = INT_MAX + 1; |
有符号整数溢出 |
除以零 | int x = 1 / 0; |
数学未定义 |
不对齐访问 | 强制访问未对齐的结构体字段 | 会在某些硬件上崩溃 |
类型混淆(type punning) | 错误地使用 union 来存不同类型 | 会破坏内存语义 |
无效的虚函数调用 | 基类未构造完成就调用虚函数 | 未定义行为 |
空指针解引用 | int *p = nullptr; *p = 5; |
经典崩溃场景 |
访问已析构的对象(use-after-lifetime) | 用完对象后还访问其成员 | 会产生悬空指针 |
enum 值超范围 | 设置了未定义的枚举值 | 违反类型安全 |
clang++ -fsanitize=undefined -g main.cpp -o main
./main
你还可以加上 -fno-sanitize-recover=undefined
让程序遇到 UB 直接崩溃。
int main() {
int a = 10;
int b = 0;
int c = a / b; // 除以 0
return c;
}
UBSan 输出:
runtime error: division by zero
int main(int argc, char **argv) {
int t = argc << 16;
return t * t;
}
假设 argc = 1
,那么:
t = 1 << 16 = 65536
t * t = 65536 * 65536 = 4,294,967,296
int
所能表示的范围(通常为 −2,147,483,648 到 2,147,483,647),属于有符号整数溢出,根据 C++ 标准,这是 未定义行为(UB)。runtime error: signed integer overflow: 65536 * 65536 cannot be represented in type 'int'
UBSan 成功捕捉到了这个未定义行为并发出警告。
-fsanitize=undefined
能在运行时捕捉很多细节错误,像整数溢出、除零、虚函数错误等。Sanitizers(如 ASan、TSan、MSan、UBSan)是 Clang/LLVM 提供的一组运行时工具,能在开发和测试阶段捕捉各种常见但难以发现的错误。
Sanitizer | 检测内容 | 示例错误类型 |
---|---|---|
ASan (AddressSanitizer) | 内存地址相关 | Use-after-free、越界访问 |
TSan (ThreadSanitizer) | 多线程数据竞争 | Data race、竞争条件 |
MSan (MemorySanitizer) | 未初始化内存读取 | 使用未初始化变量 |
UBSan (UndefinedBehaviorSanitizer) | 未定义行为 | 整数溢出、无效类型转换 |
clang++ -fsanitize=address,undefined -g your_file.cc
来在开发阶段抓住更多潜在问题。
虽然 ASan、TSan、MSan、UBSan 非常强大,但它们 不是银弹(silver bullet):
libFuzzer + ASan
是黄金搭档-fstack-protector-strong
, -fsanitize=cfi
, 等技术 | 用途 |
---|---|
Sanitizers | 找 bug(依赖测试) |
Fuzzing | 提升测试质量、触发隐藏 bug |
Hardening | 让 bug 更难被攻击者利用 |
Formal Methods (进阶) | 用数学方法“证明”程序正确 |
你有个函数处理网络数据包:
-fsanitize-coverage=
选项做代码覆盖率的插桩,支持以下模式:extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size);
-fsanitize-coverage=edge[,indirect-calls][,8bit-counters]
实现代码覆盖率插桩。CORPUS
),可以为空目录,libFuzzer 也会自动生成变异测试用例。./my-fuzzer CORPUS
-jobs=N
:指定并行任务数,多个工作线程共同作用于同一个语料库。-max_len=N
:限制单个测试输入的最大长度(默认64字节)。-help
:查看更多可调节参数。CORPUS
目录中,方便持续扩大和改进语料库。void TestOneInput(const uint8_t *data, size_t size) {
FT_Face face;
if (size < 1) return;
if (!FT_New_Memory_Face(library, data, size, 0, &face)) {
FT_Done_Face(face);
}
}
Results with FreeType (ASan+UBsan)
#45999 left shift of negative value -4592
#45989 leak in t42_parse_charstrings
#45987 512 byte input consumes 1.7Gb / 2 sec to process
#45986 leak in ps_parser_load_field
#45985 signed integer overflow: -35475362522895417 * -8256 cannot be represented in t
#45984 signed integer overflow: 2 * 1279919630 cannot be represented in type 'int
#45983 runtime error: left shift of negative value -9616
#45966 leaks in parse_encoding, parse_blend_design_map, t42_parse_encoding
#45965 left shift of 184 by 24 places cannot be represented in type ‘int’
#45964 signed integer overflow: 6764195537992704 * 7200 cannot be represented in type
#45961 FT_New_Memory_Face consumes 6Gb+
#45955 buffer overflow in T1_Get_Private_Dict/strncmp
#45938 shift exponent 2816 is too large for 64-bit type ‘FT_ULong’
#45937 memory leak in FT_New_Memory_Face/FT_Stream_OpenGzip
#45923 buffer overflow in T1_Get_Private_Dict while doing FT_New_Memory_Face
#45922 buffer overflow in skip_comment while doing FT_New_Memory_Face
#45920 FT_New_Memory_Face takes infinite time (in PS_Conv_Strtol)
#45919 FT_New_Memory_Face consumes 17Gb on a small input
SSL_CTX *sctx;
int Init() { ... }
extern "C" void LLVMFuzzerTestOneInput(unsigned char * Data, size_t Size) {
static int unused = Init();
SSL *server = SSL_new(sctx);
BIO *sinbio = BIO_new(BIO_s_mem());
BIO *soutbio = BIO_new(BIO_s_mem());
SSL_set_bio(server, sinbio, soutbio);
SSL_set_accept_state(server);
BIO_write(sinbio, Data, Size);
SSL_do_handshake(server);
SSL_free(server);
}
这是一个用 libFuzzer 测试 OpenSSL 的例子,关键点:
Init()
函数初始化 SSL 环境,只执行一次(用 static int unused = Init();
保证)SSL_new(sctx)
BIO_s_mem()
),分别用于模拟输入输出SSL_set_bio
),设定为服务端接受状态(SSL_set_accept_state
)BIO_write(sinbio, Data, Size);
)SSL_do_handshake(server);
),这里可能暴露握手相关漏洞SSL_free
)Concolic execution(结合具体执行与符号执行)的核心思路是:
clang++
加上 -fsanitize=cfi-vcall
和 -flto
编译链接,即可启用CFI对虚函数调用的保护。-flto
(Link Time Optimization)能让编译器在链接阶段对整个程序做优化,确保CFI检查能够覆盖跨模块的调用。bt
指令检查偏移位是否在允许的 bitset 中。testb
测试某个位是否允许CRASH
位置通过 ud2
指令(非法指令)终止程序,防止利用漏洞继续执行。-fsanitize=cfi-nvcall
做检查,防止非法非虚函数调用。-fsanitize=cfi-icall
保障间接调用安全。-fsanitize=cfi-derived-cast
void*
转换为类指针:-fsanitize=cfi-unrelated-cast
/d2guard4
和 /Guard:cf
实现,支持跨模块保护。-fstack-protector
系列clang++ -fsanitize=safe-stack your_code.cpp -o your_program
int main() {
int local_var = 0x123456;
bar(&local_var);
}
push %r14
push %rbx
push %rax
mov 0x207d0d(%rip),%r14
mov %fs:(%r14),%rbx # Get unsafe_stack_ptr
lea -0x10(%rbx),%rax # Update unsafe_stack_ptr
mov %rax,%fs:(%r14) # Store unsafe_stack_ptr
lea -0x4(%rbx),%rdi
movl $0x123456,-0x4(%rbx)
callq 40f2c0 <_Z3barPi>
mov %rbx,%fs:(%r14) # Restore unsafe_stack_ptr
xor %eax,%eax
add $0x8,%rsp
pop %rbx
pop %r14
retq
mov %fs:(%r14),%rbx
:从线程局部存储(TLS)读取当前 unsafe_stack_ptr
lea -0x10(%rbx),%rax
:在 unsafe stack 上为局部变量预留空间mov %rax,%fs:(%r14)
:更新 unsafe stack 指针,表示空间已分配movl $0x123456,-0x4(%rbx)
:把局部变量 local_var = 0x123456
写入 unsafe stackbar
,传入局部变量地址(指向 unsafe stack)%rsp
) 仅存放返回地址、保存寄存器等,局部变量移至单独的 unsafe stack 区域%fs:(%r14)
是一个线程局部存储段寄存器的偏移,存储 unsafe stack 指针(实现线程安全)