GWP-ASan¶
简介¶
GWP-ASan 是一个抽样分配器框架,旨在帮助在生产环境中查找释放后使用和堆缓冲区溢出错误。它非正式地是一个递归首字母缩略词,“GWP-ASan Will Provide Allocation SANity”(GWP-ASan 将提供分配健全性)。
GWP-ASan 基于经典的 Electric Fence Malloc Debugger(Electric Fence Malloc 调试器),并进行了一项关键的调整。值得注意的是,我们仅选择非常小比例的分配进行抽样,并仅对这些抽样分配应用保护页。抽样比例足够小,从而使我们能够保持非常低的性能开销。
存在一个小的、可调整的内存开销,该开销在进程的生命周期内是固定的。使用默认设置时,每个进程大约为 ~40KiB,具体取决于您的平均分配大小。
GWP-ASan vs. ASan¶
与 AddressSanitizer 不同,GWP-ASan 不会引起显著的性能开销。ASan 通常需要使用专用的 Canary 值才能在生产环境中可行,因此通常是不切实际的。此外,ASan 的运行时开发并未考虑安全性,这使得编译后的二进制文件更容易受到漏洞利用。
然而,GWP-ASan 仅能够查找 ASan 检测到的一部分内存问题。此外,GWP-ASan 的错误检测能力仅是概率性的。因此,我们建议在测试以及任何保证错误检测比 2 倍执行速度减慢/二进制文件大小膨胀更有价值的任何地方使用 ASan 而不是 GWP-ASan。对于大多数生产环境而言,这种影响太大了,而安全性是不可或缺的,因此 GWP-ASan 被证明非常有用。
设计¶
请注意: GWP-ASan 的实现很大程度上处于变动之中,这些细节可能会发生变化。目前还有其他 GWP-ASan 的实现,例如 Chromium 中使用的实现。长期支持目标是确保在合理的情况下实现功能对等,并支持 compiler-rt 作为参考实现。
分配器支持¶
GWP-ASan 不是传统分配器的替代品。相反,它的工作原理是在支持分配器中插入存根,以便在选择分配进行抽样时,将分配重定向到 GWP-ASan。这些存根通常在 malloc()
、free()
和 realloc()
的实现中实现。这些存根非常小,这使得在大多数分配器中使用 GWP-ASan 相当简单。这些存根遵循相同的通用模式(例如,下面的 malloc()
伪代码)
#ifdef INSTALL_GWP_ASAN_STUBS
gwp_asan::GuardedPoolAllocator GWPASanAllocator;
#endif
void* YourAllocator::malloc(..) {
#ifdef INSTALL_GWP_ASAN_STUBS
if (GWPASanAllocator.shouldSample(..))
return GWPASanAllocator.allocate(..);
#endif
// ... the rest of your allocator code here.
}
然后,所有支持分配器需要做的就是使用 -DINSTALL_GWP_ASAN_STUBS
进行编译,并链接 GWP-ASan 库!出于性能原因,我们强烈建议静态链接 GWP-ASan 库。
保护分配池¶
GWP-ASan 的核心是保护分配池。每个抽样分配都使用其自己的保护槽进行支持,该槽可能由一个或多个可访问页面组成。每个保护槽都由两个保护页包围,这两个保护页被映射为不可访问。所有保护槽的集合构成了保护分配池。
缓冲区下溢/溢出检测¶
我们通过这些保护页获得缓冲区溢出和缓冲区下溢检测。当内存访问超出已分配的缓冲区时,它将触及不可访问的保护页,从而导致内存异常。此异常会被内部崩溃处理程序捕获和处理。由于每个分配都记录了有关其分配和释放位置(以及由哪个线程)的元数据,因此我们可以提供有助于识别错误根本原因的信息。
随机选择分配是左对齐还是右对齐,以便平等地检测下溢和溢出。
释放后使用检测¶
保护分配池还提供释放后使用检测。每当释放抽样分配时,我们都会将其保护槽映射为不可访问。因此,释放后的任何内存访问都将触发崩溃处理程序,并且我们可以提供有关错误来源的有用信息。
请注意,抽样分配的释放后使用检测是短暂的。为了在保持内存开销固定的同时仍然检测错误,已释放的槽会被随机重用以保护未来的分配。
用法¶
GWP-ASan 已经默认包含在 Scudo Hardened Allocator 中,因此使用 -fsanitize=scudo
进行构建是尝试 GWP-ASan 最快速和最简单的方法。
选项¶
GWP-ASan 的配置由支持分配器管理。我们提供了一个通用的配置管理库,Scudo 正在使用它。它允许通过以下方法配置 GWP-ASan 的多个方面
编译 GWP-ASan 库时,通过将
-DGWP_ASAN_DEFAULT_OPTIONS
设置为您想要默认设置的选项字符串。如果您正在构建作为 compiler-rt/LLVM 构建一部分的 GWP-ASan,请在 cmake 配置时添加它(例如cmake ... -DGWP_ASAN_DEFAULT_OPTIONS="..."
)。如果您在 compiler-rt 之外构建 GWP-ASan,只需确保在构建optional/options_parser.cpp
时指定-DGWP_ASAN_DEFAULT_OPTIONS="..."
即可。通过在程序中定义一个
__gwp_asan_default_options
函数,该函数返回要解析的选项字符串。所述函数必须具有以下原型:extern "C" const char* __gwp_asan_default_options(void)
,具有默认可见性。这将覆盖编译时定义;取决于分配器支持(Scudo 支持此机制):通过环境变量,包含要解析的选项字符串。在 Scudo 中,这是通过 SCUDO_OPTIONS=GWP_ASAN_${OPTION_NAME}=${VALUE}(例如 SCUDO_OPTIONS=GWP_ASAN_SampleRate=100)。以此方式定义的选项将覆盖通过
__gwp_asan_default_options
所做的任何定义。
选项字符串遵循类似于 ASan 的语法,其中可以在同一字符串中分配不同的选项,并用冒号分隔。
例如,使用环境变量
GWP_ASAN_OPTIONS="MaxSimultaneousAllocations=16:SampleRate=5000" ./a.out
或使用函数
extern "C" const char *__gwp_asan_default_options() {
return "MaxSimultaneousAllocations=16:SampleRate=5000";
}
以下选项可用
选项 |
默认值 |
描述 |
Enabled |
true |
是否启用 GWP-ASan? |
PerfectlyRightAlign |
false |
当分配右对齐时,我们是否应将其完美对齐到页面边界?默认情况下(false),出于性能原因,我们将分配大小向上舍入到最接近的 2 的幂(2、4、8、16),最大对齐到 16 字节。将此设置为 true 可以找到单字节缓冲区溢出,但会牺牲性能,并且可能与某些架构不兼容。 |
MaxSimultaneousAllocations |
16 |
池中可用的同时保护分配的数量。 |
SampleRate |
5000 |
页面被选中进行 GWP-ASan 抽样的概率 (1 / SampleRate)。支持高达 (2^31 - 1) 的采样率。 |
InstallSignalHandlers |
true |
在动态加载期间为 SIGSEGV 安装 GWP-ASan 信号处理程序。这可以通过在报告内存错误时提供分配和释放的堆栈跟踪来改进错误报告。GWP-ASan 的信号处理程序会将信号转发到任何先前安装的处理程序,并且安装其他信号处理程序的用户程序应确保它们也这样做。请注意,如果先前安装的 SIGSEGV 处理程序是 SIG_IGN,我们会在转储错误报告后终止进程。 |
示例¶
以下代码存在释放后使用错误,其中 string_view
被创建为对 string+
运算符的临时结果的引用。当在第 8 行取消引用 sv
时,会发生释放后使用错误。
1: #include <iostream>
2: #include <string>
3: #include <string_view>
4:
5: int main() {
6: std::string s = "Hellooooooooooooooo ";
7: std::string_view sv = s + "World\n";
8: std::cout << sv;
9: }
使用 Scudo+GWP-ASan 编译此代码将有可能捕获此错误,并为我们提供详细的错误报告
$ clang++ -fsanitize=scudo -g buggy_code.cpp
$ for i in `seq 1 500`; do
SCUDO_OPTIONS="GWP_ASAN_SampleRate=100" ./a.out > /dev/null;
done
|
| *** GWP-ASan detected a memory error ***
| Use after free at 0x7feccab26000 (0 bytes into a 41-byte allocation at 0x7feccab26000) by thread 31027 here:
| ...
| #9 ./a.out(_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St17basic_string_viewIS3_S4_E+0x45) [0x55585c0afa55]
| #10 ./a.out(main+0x9f) [0x55585c0af7cf]
| #11 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
| #12 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| 0x7feccab26000 was deallocated by thread 31027 here:
| ...
| #7 ./a.out(main+0x83) [0x55585c0af7b3]
| #8 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
| #9 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| 0x7feccab26000 was allocated by thread 31027 here:
| ...
| #12 ./a.out(main+0x57) [0x55585c0af787]
| #13 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
| #14 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| *** End GWP-ASan report ***
| Segmentation fault
要符号化这些堆栈跟踪,需要采取一些措施。Scudo 当前使用 GNU 的 backtrace_symbols()
从 <execinfo.h>
进行展开。展开器以 function+offset
形式而不是正常的 binary+offset
形式提供人类可读的堆栈跟踪。为了使用 addr2line 或类似的工具来恢复确切的行号,我们必须将 function+offset
转换为 binary+offset
。辅助脚本位于 compiler-rt/lib/gwp_asan/scripts/symbolize.sh
。使用此脚本将尝试符号化每一行可能的行,如果任何操作失败,则回退到先前的输出。这会产生以下输出
$ cat my_gwp_asan_error.txt | symbolize.sh
|
| *** GWP-ASan detected a memory error ***
| Use after free at 0x7feccab26000 (0 bytes into a 41-byte allocation at 0x7feccab26000) by thread 31027 here:
| ...
| #9 /usr/lib/gcc/x86_64-linux-gnu/8.0.1/../../../../include/c++/8.0.1/string_view:547
| #10 /tmp/buggy_code.cpp:8
|
| 0x7feccab26000 was deallocated by thread 31027 here:
| ...
| #7 /tmp/buggy_code.cpp:8
| #8 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
| #9 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| 0x7feccab26000 was allocated by thread 31027 here:
| ...
| #12 /tmp/buggy_code.cpp:7
| #13 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
| #14 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| *** End GWP-ASan report ***
| Segmentation fault