> 文章列表 > D用地址清理器找内存错误

D用地址清理器找内存错误

D用地址清理器找内存错误

使用地址清理器查找D代码中的内存错误

1.4.0版本以来,LDC改进了支持地址清理器.(ASan)地址清理器是运行时内存写入/读取检查器,可帮助发现和定位内存访问错误的工具.

ASanLDC官方发布二进制文件的一部分;要使用它,必须用-fsanitize=address构建.

地址清理器

地址清理器(或简称ASan)是快速内存访问错误检测器.它检测到程序试从无效内存地址读取/写入时,会中止该程序并输出包含有关错误细节的错误报告.

要使用ASan,必须在允许ASan时编译(-fsanitize=address),并且必须与ASan运行时库链接(与LDC链接时,再次要用-fsanitize=address).

ASan是为抓C++代码基中的错误而开发的.尽管D是更加内存安全的语言,但安全措施确实需要一些开发者的努力和纪律.在D中也可能有C++中的相同类型的内存错误.

CppCon2017上,Facebook工程总监LouisBrandy指出:"地址清理器可能在很长时间,是最重要的事情,至少工具集中是这样.",而地址清理器的意义很重要,它发现的漏洞很多,1,2.

ASan通过在编译时指示内存访问,来在运行时错误的内存访问,该指令代码根据(存储在"影子"内存区域中)可/无法访问的内存表,来检查访问的地址区间.
指令代码包括添加检查内存中每个读/写,并添加调用来,标记指定内存区域为访问的有效("未中毒")或无效("中毒").

有效内存区域(栈变量和堆)周围,检测和运行时库添加有毒的"红色区域",以便可检测到缓冲溢出/下溢.

ASan运行时库提供标记/检查内存和报告错误功能,但也覆盖mallocfree,这样,还可检查动态分配的内存区域访问有效性.这样,ASan可检测释放后使用错误堆缓冲溢出等.

简单示例

一个很容易被ASan抓错误的示例:

// File: asan_1.d
void foo(int* arr) {arr[10] = 1; // 爆炸 !!!
}void main() {int[10] tenIntegers;foo(&tenIntegers[0]);
}

看看asan_1.d小错误程序的运行时输出.使用-fsanitize=address -g -disable-fp-elim构建,然后运行程序:

> ldc2 -fsanitize=address -g -disable-fp-elim asan_1.d
> ./asan_1

输出:

==25860==ERROR: 地址清理器: stack-buffer-overflow on address 0x7ffee599f4c8 at pc 0x00010a261704 bp 0x7ffee599f440 sp 0x7ffee599f438
WRITE of size 4 at 0x7ffee599f4c8 thread T0#0 0x10a261703 in _D6asan_13fooFPiZv asan_1.d:3#1 0x10a2617fe in _Dmain asan_1.d:8#2 0x10a28b81e in _D2rt6dmain211_d_run_mainUiPPaPUAAaZiZ6runAllMFZ9__lambda1MFZv (asan_1:x86_64+0x10002b81e)#3 0x10a261964 in main __entrypoint.d:8#4 0x7fff79b49114 in start (libdyld.dylib:x86_64+0x1114)`0x7ffee599f4c8`地址,在帧中偏移为0的`T72`线程栈中.#0 0x10a26172f in _Dmain asan_1.d:61对象(s):[32, 72) '' <== Memory access at offset 72 overflows this variable

提示:如果程序使用某些自定义栈展开机制或交换环境,可能是误报.
(支持C++LongJMP和异常)

SUMMARY: 地址清理器: stack-buffer-overflow asan_1.d:3 in _D6asan_13fooFPiZv
Shadow bytes around the buggy address:0x1fffdcb33e40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffdcb33e50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffdcb33e60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffdcb33e70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffdcb33e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x1fffdcb33e90: f1 f1 f1 f1 00 00 00 00 00[f3]f3 f3 f3 f3 f3 f30x1fffdcb33ea0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffdcb33eb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffdcb33ec0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffdcb33ed0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffdcb33ee0: 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:           00Partially addressable: 01 02 03 04 05 06 07Heap left redzone:       faFreed heap region:       fdStack left redzone:      f1Stack mid redzone:       f2Stack right redzone:     f3
...
==25860==ABORTING

因为ASan拦截了错误的内存访问中止了程序:因为在_D4asan_0fooFPiZvasan_101.d:3中的96x6c13b1程序位置写入大小为4(字节)而导致栈缓冲溢出.

asan_1.d文件第3行是带有//爆炸!!!的行,因此,ASan不仅检测到错误,还会立即显示错误位置.ASan还报告在(_Dmainasan_1.d:6)哪个栈帧中的错误内存地址,并告诉哪个变量接近该地址:

 此帧有1个对象:[32, 72) '' <== Memory access at offset 72 overflows this variable

但未显示该变量名.
以下是[]之间指示的访问内存地址周围的影子内存视图.f3表示访问的位置在"栈右侧红区"中,即地址在栈分配变量之后.

注意,阴影区域大多是未毒化且可寻址的.表明ASan只会检测靠近变量的内存的错误访问.如果将第3行更改为arr[30]=1;,则ASan无法在系统上检测到该错误.

注意,在惯用D代码中,人们使用切片而不是指针,D边界检查会抓错误:

void foo(int[] arr) {arr[10] = 1;// D边界检查会抓到,不用Asan.
}void main() {int[10] tenIntegers;foo(tenIntegers);
}

ldc运行时中的错误

看看LDC运行时中的一个错误这里,在1.4.0版本之前,ASan很容易找到该错误.该错误是由EyalWeka.io报告的,我觉得他花了很多时间来弄清什么被破坏了:
在传递ulong(8字节)和int(4字节)时,core.atomic.atomicStore会过量读栈,导致atomicStoreulong一半写入垃圾.用ulongint调用atomicStore可能很容易,如下:

// File: asan_2.d
import core.atomic : atomicStore;
void main() {shared ulong x = 0x1234_5678_8765_4321;atomicStore(x, 0); // 0为`int`类型// assert(x == 0); // LDC 1.4.0之前失败
}

如果没有断定,代码会执行而不会崩溃或中止;不用说,破坏存储数据这样基本东西时,会导致严重错误.允许ASan后,它会立即发现错误,并报告栈缓冲溢出错误!

==22331==ERROR: 地址清理器: stack-buffer-overflow on address 0x7fff57a13360 at pc 0x0001081ece5c bp 0x7fff57a13330 sp 0x7fff57a13328
READ of size 8 at 0x7fff57a13360 thread T0#0 0x1081ece5b in _Dmain atomic.d:389

此时,ASan输出更难与源码相关联,因为即使用-O0,因为pragma(inline,true);也是内联调用core.atomic.atomicStore.

我在OSX上,所以需要先在asan_2二进制上运行dsymutil.然后在添加llvm-symbolizer到路径后,得到了更好的栈跟踪,它确实显示调用了core.atomic.atomicStore_Dmain:

==22051==ERROR: 地址清理器: stack-buffer-overflow on address 0x7fff562c0360 at pc 0x00010993fe5c bp 0x7fff562c0330 sp 0x7fff562c0328
READ of size 8 at 0x7fff562c0360 thread T0#0 0x10993fe5b in _D4core6atomic50__T11atomicStoreVE4core6atomic11MemoryOrderi3TmTiZ11atomicStoreFNaNbNiNeKOmiZv <..>/druntime/src/core/atomic.d:389:9#1 0x10993fe5b in _Dmain asan_2.d:5#2 0x109a7a3be in _D2rt6dmain211_d_run_mainUiPPaPUAAaZiZ6runAllMFZ9__lambda1MFZv (./asan_2:x86_64+0x10013b3be)#3 0x109940214 in main __entrypoint.d:8:15Address 0x7fff562c0360 is located in stack of thread T0 at offset 32 in frame#0 0x10993fcef in _Dmain /Users/johan/github.io/johanengelen.github.io/code/asan_2.d:3This frame has 2 object(s):[32, 36) '' <== Memory access at offset 32 partially overflows this variable[48, 56) ''提示:如果你的程序使用某些自定义栈展开机制或交换环境,是误报(支持`C++LongJMP`和异常)
SUMMARY: 地址清理器: stack-buffer-overflow /Users/johan/ldc/johan/runtime/druntime/src/core/atomic.d:389:9 in _D4core6atomic50__T11atomicStoreVE4core6atomic11MemoryOrderi3TmTiZ11atomicStoreFNaNbNiNeKOmiZv
Shadow bytes around the buggy address:0x1fffeac58010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffeac58020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffeac58030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffeac58040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffeac58050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x1fffeac58060: 00 00 00 00 00 00 00 00 f1 f1 f1 f1[04]f2 00 f30x1fffeac58070: f3 f3 f3 f3 00 00 00 00 00 00 00 00 00 00 00 000x1fffeac58080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffeac58090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffeac580a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1fffeac580b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

输出很好地显示,(查找[]括号)访问地址处,只有4个字节是可寻址的,而不是8字节的读取试.
教训很清楚:应该尽快开始运行允许ASanPhobosdruntime测试包!

黑名单

某些代码需要访问受ASan保护的内存区域.如,栈跟踪代码,必须读取超出用户变量的限制.如果没有特殊的预防措施,此类栈跟踪代码触发ASan栈缓冲溢出错误.
注意,标准库包含此类代码.但是,确实需要检测标准库,以查找误用(或标准库内部错误)的错误.为此,可指定一个黑名单,这样就不会检测与黑名单匹配的函数.

指定哪些函数不应由ASan检测,请使用-fsanitize-blacklist=<filename>编译器标志.此类黑名单文件的示例:

#文件:`asan_blacklist.txt`
#消毒剂黑名单.关闭指定检测
#函数或源.请小心使用.可设置黑名单的位置
#编译时使用`-fsanitize-blacklist=<path>`标志.
#用法示例:
#函数:`*bad_function_name*`
#src:file_with_tricky_code.cc
#黑名单`druntime`函数,其内联汇编不适合
#Asan,请见`https://github.com/ldc-developers/ldc/issues/2257`
函数:`_D*callWithStackShell*`
函数:`_D*getcacheinfoCPUID2*`
#来自保守`gcdruntime`的函数
函数:`*12`保守`2GC*`
函数:`_D4core5cpuid*`

asan_blacklist.txt文件中,我禁止了druntimePhobos中一些函数的检测,这样ASan就不会对有"无效"内存操作的代码触发.
注意:一些与此黑名单匹配的函数可能真有问题!可用ldc-build-runtime工具构建允许ASandruntimePhobos标准库,其中包含以下黑名单:

> ./ldc-build-runtime --dFlags='-fsanitize=address;-fsanitize-blacklist=asan_blacklist.txt' BUILD_SHARED_LIBS=OFF

现在为了运行Phobosdruntime测试包,也需要为链接器显式指定ASan库(我特意保留了系统的绝对路径):

> ./bin/ldc-build-runtime --dFlags='-fsanitize=address;-fsanitize-blacklist=/Users/johan/ldc/ldc2-1.7.0-beta1-osx-x86_64/asan_blacklist.txt' BUILD_SHARED_LIBS=OFF --testrunners --linkerFlags='/Users/johan/ldc/ldc2-1.7.0-beta1-osx-x86_64/lib/libldc_rt.asan_osx_dynamic.dylib'

未来工作:检测返回后的栈使用情况

D的@safe属性(结合-dip1000),可避免代码执行内存不安全操作.编译器拒绝从@safe函数返回局部栈变量引用,但在某些极端情况,仍可这样做,正如去年的论坛帖子中所讨论的那样.下面是演示该错误的示例:

class A {int i;
}void inc(A a) @safe {a.i += 1; // 第6行
}auto makeA() @safe {  // 第9行import std.algorithm : move;scope a = new A(); //栈上分配return move(a);
}void main() @safe {auto a = makeA();a.inc(); // 第17行
}

注意,编译器应该已(在编译时!)抓了此错误,但它目前没有.,ASan也会找到这些错误.

可设置ASAN_OPTIONS环境变量,传递额外运行时标志给ASan.有许多有趣选项,在此我只展示detect_stack_use_after_return设置作用.通过ddemangle管道输出,可便得到人类可读D函数名.

> ldc2 -fsanitize=address -disable-fp-elim scopeclass.d -g -O1 -dip1000
> ASAN_OPTIONS=detect_stack_use_after_return=1 ./scopeclass 2>&1 | ddemangle

阅读ASan的完整输出,你会了解该错误(退出域后使用栈分配变量):

==11446==ERROR: 地址清理器: stack-use-after-return on address 0x000104929050 at pc 0x0001007a9837 bp 0x7fff5f457510 sp 0x7fff5f457508
READ of size 4 at 0x000104929050 thread T0#0 0x1007a9836 in @safe void scopeclass.inc(scopeclass.A) scopeclass.d:6#1 0x1007a9a20 in _Dmain scopeclass.d:17#2 0x1008e40ce in _D2rt6dmain211_d_run_mainUiPPaPUAAaZiZ6runAllMFZ9__lambda1MFZv (scopeclass:x86_64+0x10013c0ce)#3 0x7fff9729b5ac in start (libdyld.dylib:x86_64+0x35ac)Address 0x000104929050 is located in stack of thread T0 at offset 80 in frame#0 0x1007a984f in pure nothrow @nogc @safe scopeclass.A scopeclass.makeA() scopeclass.d:9

大警告:ASan返回后使用栈检测的工作原理是在上用malloc分配栈变量,但该内存(尚未)向垃集器(GC)注册.
表明GC看不见栈分配指针,如果仅由栈指针指向,则会错误地收集内存,这很差!

未来工作:垃集器(GC)分配的内存

当前的LDC实现中,ASan不会检测与GC分配内存相关错误.这是因为GC管理的所有内存都在由malloc预先分配的大型GC池中,因此都(通过ASan覆盖malloc)标记为有效/未中毒.

用户代码()分配时,GC内存池的内存部分提供给用户.只有在用户分配后,内存才应有效访问.但是,因为malloc已标记它为有效,因此ASan不会在过度读取等时触发.

显然,这是DASan的主要缺失部分.在druntime中必须实现,毒化GC内存池,并且只解毒已提供给用户代码的部分(并在不再使用及被收集后再次毒化它).