使用-fsanitize=address定位bug

-- TOC --

打开-fsanitize=address编译选项后,代码在编译时会被instrumentation,在发生内存读写地址错误的时候,会打印出一个report,给出很多可以协助定位bug的信息!(需安装libasan库)

AddressSanitinzer论文:https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/37752.pdf

heap-buffer-overflow

比如下面这段代码:

#include <stdlib.h>

int main(){
    char *p = malloc(18);
    p[21] = '1';  // heap-buffer-overflow
    free(p);
    return 0;
}

编译和运行:

$ gcc -Wall -Wextra test2.c -o test2  # no error and warning
$ ./test2                             # nothing happened
$ gcc -Wall -Wextra test2.c -fsanitize=address -o test2  # -fsanitize=address
$ ./test2
=================================================================
==280728==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000000055 at pc 0x0000004011b8 bp 0x7fff9aab5dc0 sp 0x7fff9aab5db8
WRITE of size 1 at 0x603000000055 thread T0
    #0 0x4011b7 in main (/home/xinlin/test2/test2+0x4011b7)
    #1 0x7f706a740eaf in __libc_start_call_main (/lib64/libc.so.6+0x3feaf)
    #2 0x7f706a740f5f in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x3ff5f)
    #3 0x4010a4 in _start (/home/xinlin/test2/test2+0x4010a4)

0x603000000055 is located 3 bytes to the right of 18-byte region [0x603000000040,0x603000000052)
allocated by thread T0 here:
    #0 0x7f706a9b891f in __interceptor_malloc (/lib64/libasan.so.6+0xae91f)
    #1 0x401177 in main (/home/xinlin/test2/test2+0x401177)
    #2 0x7f706a740eaf in __libc_start_call_main (/lib64/libc.so.6+0x3feaf)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/xinlin/test2/test2+0x4011b7) in main
Shadow bytes around the buggy address:
  0x0c067fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c067fff8000: fa fa 00 00 00 fa fa fa 00 00[02]fa fa fa fa fa
  0x0c067fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==280728==ABORTING

编译时加上-fsanitize=address,运行时就能跑出这个刻意为之的heap-buffer-overflow错误。

如何阅读这个错误report?

首先是错误类型,heap-buffer-overflow,并且给出了出错的地址,PC指针地址和调用栈,对出错地址的操作是read还是write,以及size,这些信息足够我们定位触发错误的代码,以及做一些分析判断。

=================================================================
==280728==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000000055 at pc 0x0000004011b8 bp 0x7fff9aab5dc0 sp 0x7fff9aab5db8
WRITE of size 1 at 0x603000000055 thread T0
    #0 0x4011b7 in main (/home/xinlin/test2/test2+0x4011b7)
    #1 0x7f706a740eaf in __libc_start_call_main (/lib64/libc.so.6+0x3feaf)
    #2 0x7f706a740f5f in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x3ff5f)
    #3 0x4010a4 in _start (/home/xinlin/test2/test2+0x4010a4)

紧接着,report给出了这块内存申请时的size,用左闭右开的区间表示,然后计算了出错的地址与这个区间的相对距离,还有申请这块内存的调用栈。(可以看到malloc接口来自libasan库)

0x603000000055 is located 3 bytes to the right of 18-byte region [0x603000000040,0x603000000052)
allocated by thread T0 here:
    #0 0x7f706a9b891f in __interceptor_malloc (/lib64/libasan.so.6+0xae91f)
    #1 0x401177 in main (/home/xinlin/test2/test2+0x401177)
    #2 0x7f706a740eaf in __libc_start_call_main (/lib64/libc.so.6+0x3feaf)

ASAN的report最后这段summary,给出了shadow-memory的信息。下面这部分信息,包含shadow-memory的内容,以及如何解读这部分内容的说明。

SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/xinlin/test2/test2+0x4011b7) in main
Shadow bytes around the buggy address:
  0x0c067fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c067fff8000: fa fa 00 00 00 fa fa fa 00 00[02]fa fa fa fa fa
  0x0c067fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==280728==ABORTING

默认情况下,ASAN的shadow-memory使用1个字节来表达8个字节的状态,其地址对应关系如下:

shadow-memory = (address>>3) + 0x7FFF8000

对地址(正数)左移3位,相当于/8,然后加上个固定的偏移,就是shadow-memory的区域,这里的每个字节,对应了程序的8个字节。因为,malloc返回的起始地址,一定是8字节对齐的!

Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07

00表示可以正常读写,01-07表示可部分读写。比如本例malloc(18),最后的字节在shadow-memory 中,就是02,表示只有前2个字节可读写,即:

=>0x0c067fff8000: fa fa 00 00 00 fa fa fa 00 00[02]fa fa fa fa fa

以上这些信息,都对定位内存bug很有帮助。建议测试时,打开-fsanitize=address编译选项。

代码在编译时被instrument

看看汇编:

$ gcc -S -masm=intel -fsanitize=address test2.c
main:
.LASANPC6:
.LFB6:
        .cfi_startproc
        push    rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        mov     rbp, rsp
        .cfi_def_cfa_register 6
        sub     rsp, 16
        mov     edi, 18
        call    malloc
        mov     QWORD PTR [rbp-8], rax
        mov     rax, QWORD PTR [rbp-8]
        lea     rcx, [rax+21]
        mov     rax, rcx
        mov     rdx, rax
        shr     rdx, 3
        add     rdx, 2147450880
        movzx   edx, BYTE PTR [rdx]
        test    dl, dl
        setne   sil
        mov     rdi, rax
        and     edi, 7
        cmp     dil, dl
        setge   dl
        and     edx, esi
        test    dl, dl
        je      .L2
        mov     rdi, rax
        call    __asan_report_store1
.L2:
        mov     BYTE PTR [rcx], 49
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    free
        mov     eax, 0
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc

代码已被修改,整个过程是自动的。

stack-buffer-overflow

测试代码:

int main(){
    char a[10] = {};
    a[10] = 1;
    return 0;
}

编译:

$ gcc -fsanitize=address test3.c -o test3 

运行:

=================================================================
==283010==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffa3c72c8a at pc 0x000000401286 bp 0x7fffa3c72c50 sp 0x7fffa3c72c48
WRITE of size 1 at 0x7fffa3c72c8a thread T0
    #0 0x401285 in main (/home/xinlin/test2/a.out+0x401285)
    #1 0x7f3b30f21eaf in __libc_start_call_main (/lib64/libc.so.6+0x3feaf)
    #2 0x7f3b30f21f5f in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x3ff5f)
    #3 0x4010a4 in _start (/home/xinlin/test2/a.out+0x4010a4)

Address 0x7fffa3c72c8a is located in stack of thread T0 at offset 42 in frame
    #0 0x401175 in main (/home/xinlin/test2/a.out+0x401175)

  This frame has 1 object(s):
    [32, 42) 'a' (line 2) <== Memory access at offset 42 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (/home/xinlin/test2/a.out+0x401285) in main
Shadow bytes around the buggy address:
  0x100074786540: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100074786550: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100074786560: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100074786570: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100074786580: 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1
=>0x100074786590: 00[02]f3 f3 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000747865a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000747865b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000747865c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000747865d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000747865e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==283010==ABORTING

形式一样,内容稍有不同。

underflow

从前面的case中可以理解,overflow是高地址越界。可以很容易的测试,underflow就是低地址越界,ASAN也支持underflow的检测。从编程的角度看,overflow更容易发生,要触发underflow,array要使用负的index,一般很难出现用负index的情况。(这不是Python)

测试代码:

int main(){
    char a[10] = {};
    a[-1] = 1;
    return 0;
}

后面只给测试代码,不再贴出ASAN的report!

heap-use-after-free

#include <stdlib.h>

int main(){
    char *p = malloc(32);
    free(p);
    p[0] = 2;
    return 0;
}

double-free

#include <stdlib.h>

int main(){
    char *p = malloc(32);
    free(p);
    free(p);
    return 0;
}

C++也支持

C++版double free:

int main(){
    char *p = new char[24];
    delete[] p;
    delete[] p;
    return 0;
}

stack-use-after-scope

int main(){
    char *p;
    {
        char a[10] = {};
        p = a;
    }
    p[0] = 2;
    return 0;
}

global-buffer-overflow

#include <stdlib.h>

char ga[8];

int main(){
    ga[8] = 1;
    return 0;
}

new-delete-type-mismatch

#include <string>
#include <cstring>
using namespace std;

int main() {
    string *p = new string[4];
    for(size_t i{}; i<4; ++i)
        p[i] = "hello";

    // array cookie's type is size_t,
    // clean array cookie!
    memset((char*)p-8, 0, 8);

    delete[] p;
    return 0;
}

在 cxxabi array-cookies 要求中,对于 non-trivial 类型的 operator new[] 实际需要在内存前端存储实际分配的元素个数 (使用 std::size_t 类型来记录)。如果不小心内存越界将 array-cookies 给写没了,那么在 delete[] non-trivial 类型时它们的 dtor 就没法正确被调用,因而无法正确析构。

对部分函数关闭ASAN功能

#include <stdlib.h>

char ga[8];

void __attribute__((no_sanitize("address"))) func(){
    ga[8] = 1;
}

int main(){
    func();
    return 0;
}

给需要关闭ASAN功能的函数增加属性:__attribute__((no_sanitize("address")))

本文链接:https://cs.pynote.net/sf/c/cdm/202310261/

-- EOF --

-- MORE --