asan内存检测

Last updated on 8 months ago

asan 是什么

ASAN(Address Sanitizer)是针对 C/C++ 的快速内存错误检测工具,用于检测原生代码中的内存错误。相对于我们比较熟悉的 valgrind ,Asan 开销更小、检测到的错误范围更大;

ASAN 早先是 LLVM 中的特性,后被集成到 GCC 4.8 中,可通过在编译时设置 CFLAGS,指定相关的编译参数来开启ASAN 特性

ASan 可以检测以下问题:

  • 堆栈和堆缓冲区上溢/下溢
  • 释放之后的堆使用情况
  • 超出范围的堆栈使用情况
  • 重复释放/错误释放

相关参数配置

在官方的wiki 有更多的介绍, 这里介绍常用的 https://github.com/google/sanitizers/wiki

编译flag

  • -fsanitize=address:启用快速内存错误检测器 ASAN,内存访问指令用于检测out-of-bounds 和 use-after-free 错误
  • -fsanitize-recover=addressr : 不能遇到错误就简单退出,而是继续运行,采用该选项支持内存出错之后程序继续运行(需要叠加设置 ASAN_OPTIONS=halt_on_error=0 才会生效;若未设置此选项,则内存出错即报错退出)
    …..

运行时flag

ASAN_OPTIONS设置
  • ASAN_OPTIONS是Address-Sanitizier的运行选项环境变量。
    • halt_on_error=0:检测内存错误后继续运行 (编译时需要添加参数 -fsanitize-recover=addressr )
    • detect_leaks=1:使能内存泄露检测 (默认是开启的)
    • malloc_context_size=15:内存错误发生时,显示的调用栈层数为15
    • log_path=/home/asan.log:内存检查问题日志存放文件路径
    • suppressions=$SUPP_FILE: 屏蔽打印某些内存错误
    • detect_stack_use_after_return=1:检查访问指向已被释放的栈空间
    • handle_segv=1:处理段错误;也可以添加handle_sigill=1处理SIGILL信号
    • quarantine_size=4194304:内存cache可缓存free内存大小4M

使用方法: export ASAN_OPTIONS=halt_on_error=0:use_unaligned=4

LSAN_OPTIONS设置

  • LSAN_OPTIONS是LeakSanitizier运行选项的环境变量,而LeakSanitizier是ASAN的内存泄漏检测模块,常用运行选项有:

  • exitcode=0:设置内存泄露退出码为0,默认情况内存泄露退出码0x16 (在程序中 如果有检测 退出码的, 需要注意)

  • use_unaligned=4:4字节对齐

  • export LSAN_OPTIONS=exitcode=0:use_unaligned=4

  • LeakSanitizer: detected memory leaks

  • handle_segv=1:处理段错误;也可以添加handle_sigill=1处理SIGILL信号

    使用方法: export LSAN_OPTIONS=exitcode=0:use_unaligned=4

在ceph 中怎么使用?

ceph 编译宏中已经加入了相关配置,用以控制是否开启 asan

  • -DWITH_ASAN=ON
    再执行do_cmake.sh 的时候,添加该参数
    1
    do_cmake.sh -DWITH_ASAN=ON

编译时踩过的坑

asan 版本 要和 gcc 版本相匹配,目前测试了gcc 8 和 gcc 9的,两者都能编译,但是 gcc8 会遇到很多问题,用gcc9 编译则不会,gcc9 对应的 asan6 ,gcc 匹配对应的glibc 版本
**
asan 和gcc 版本对应关系
在 GCC 4.8后 就集成asan ,每个gcc 版本都有相对的 asan 库
What is the difference between these packages: The different packages are for different versions of gcc libasan0: gcc-4.8
libasan2: gcc-5
libasan3: gcc-6
libasan4: gcc-7
libasan5: gcc-8 ( libasan5 相关的bug ,遇到rwlock 会core掉 gnu bug
libasan6: gcc-9

当前编译成功且跑得通的配置是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@node86 build]# uname -a
Linux node86 4.18.0-305.3.1.el8.x86_64 #1 SMP Tue Jun 1 16:14:33 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

[root@node86 build]# gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/opt/rh/gcc-toolset-9/root/usr/libexec/gcc/x86_64-redhat-linux/9/lto-
gcc version 9.2.1 20191120 (Red Hat 9.2.1-2) (GCC)

[root@node86 build]# ll /lib64/libasan.so.6
lrwxrwxrwx. 1 root root 16 Nov 13 2021 /lib64/libasan.so.6 -> libasan.so.6.0.0
[root@node86 build]#


[root@node86 build]# ldd --version
ldd (GNU libc) 2.28
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
[root@node86 build]#


gcc 和 glibc 版本都不能太低

运行时遇到的问题

我目前就时搭建一个简单的单点存储,用 vstart 搭建

  • 搭建之前的准备工作

    1
    2
    3
    4
    # 开启内存泄漏检测、遇到检测异常不退出、日志重定向
    export ASAN_OPTIONS=detect_leaks=1:halt_on_error=0:log_path=/home/asan.log
    # 设置内存泄露退出码为0
    export LSAN_OPTIONS=exitcode=1
  • 编译asan的时候,需要 用 LD_PRELOAD 来指定 asan的库,优先加载 asan的库,不建议用export 导入作为全局变量,因为导入后,使用一些系统命令时会卡死,直接在运行程序前添加即可,如果不加,默认不开

    1
    2
    3
    4
    5
     LD_PRELOAD=/usr/lib64/libasan.so.6   <your daemon>

    # eg
    LD_PRELOAD=/usr/lib64/libasan.so.6 ./bin/ceph -s

  • 为什么要用 LD_PRELOAD?

    Linux操作系统的动态链接库在加载过程中,动态链接器会优先读取 LD_PRELOAD环境变量和默认配置文件/etc/ld.so.preload, 并将读取到的动态链接库文件进行预加载。即使程序不依赖这些动态链接库,LD_PRELOAD环境变量和/etc/ld.so.preload配置文件中指定的动态链接库依然会被加载,因为它们的优先级比LD_LIBRARY_PATH环境变量所定义的链接库查找路径的文件优先级要高,所以能够提前于用户调用的动态库载入

    LD_PRELOAD > LD_LIBRARY_PATH > /etc/ld.so.cache > /lib > /usr/lib
    asan编译时候是 重入了关于内存的函数,需要替换成glibc提供的接口 ,如mallco,memcpy等等(所以才能监控内存的使用),使用LD_PRELOAD 优先加载asan 库,这意味着就程序代码优先使用asan的函数

具体的案例分析

  • use after free(free之后继续使用)

Pasted image 20240129182101

  • Out of memory

Pasted image 20240129200307

  • detecte memory(堆溢出)

Pasted image 20240130145834

内存泄漏误报场景

  • 结构体非 4 字节对齐:报错提示结构体 A 内存泄漏,A 内存的指针存放在结构体 B 中,A 内存指针在结构体 B 中的偏移量非 4 的整数倍,由于 ASan 扫描内存时是按照 4 字节偏移进行,从而扫描不到 A 内存指针导致误报。解决方法:对非4字节对齐的结构体进行整改。
  • 信号栈内存:该内存是在信号处理函数执行时做栈内存用的,其指针会保存在内核中,所以在用户态的 ASan 扫描不到,产生误报;
  • 内存指针偏移后保存;
  • 存在ASan未监控的内存接口;
  • 越界太离谱,越界访问的地址不在 buffer 的 redzone 内:
  • 对于memcpy的dest和src是在同一个malloc的内存块中时,内存重叠的情况无法检测到。
  • ASan对于overflow的检测依赖于安全区,而安全区总归是有大小的。它可能是64bytes,128bytes或者其他什么值,但不管怎么样终归是有限的。如果某次踩踏跨过了安全区,踩踏到另一片可寻址的内存区域,ASan同样不会报错。这是ASan的另一种漏检。
  • ASan对于UseAfterFree的检测依赖于隔离区,而隔离时间是非永久的。也就意味着已经free的区域过一段时间后又会重新被分配给其他人。当它被重新分配给其他人后,原先的持有者再次访问此块区域将不会报错。因为这一块区域的shadow memory不再是0xfd。所以这算是ASan漏检的一种情况。

在项目中的应用注意事项

  • 项目的构建方案应当有编译选项,能随时启用/关闭ASan。
  • 项目送测阶段可以打开ASan,以帮助暴露更多的低概率诡异问题。
  • 请勿在生产版本中启用ASan,其会降低程序运行速度大概2-5倍,并会出现内存持续增长现象(占用的RedZone并不会自动释放,所以会出现内存溢出的假象,关闭ASan现象即会消失)。
  • 实际开发测试过程中通过ASan扫出的常见问题有:多线程下临界资源未加保护导致同时出现读写访问,解决方案一般是对该资源恰当地加锁即可;内存越界,如申请了N字节的内存却向其内存地址拷贝大于N字节的数据,这种情况在没有开启ASan的情况下一般都很难发现。
  • 一些显而易见的访问无效内存操作可能会被编译器优化而会漏报。