D用地址清理器找内存错误
使用地址清理器查找D代码中的内存错误
自1.4.0
版本以来,LDC
改进了支持
地址清理器.(ASan)
地址清理器是运行时
内存写入/读取
检查器,可帮助发现和定位内存访问
错误的工具.
ASan
是LDC
官方发布二进制
文件的一部分;要使用它,必须用-fsanitize=address
构建.
地址清理器
地址清理器(或简称ASan
)是快速
内存访问错误检测器
.它检测到程序试从无效
内存地址读取/写入
时,会中止
该程序并输出
包含有关错误细节的错误报告
.
要使用ASan
,必须在允许ASan
时编译(-fsanitize=address
),并且必须与ASan
运行时库链接(与LDC
链接时,再次要用-fsanitize=address
).
ASan
是为抓C++
代码基中的错误
而开发的.尽管D是更加内存
安全的语言,但安全
措施确实需要一些开发者的努力和纪律.在D中也可能有C++
中的相同类型
的内存错误.
在CppCon2017
上,Facebook
工程总监LouisBrandy
指出:"地址清理器
可能在很长时间,是最重要
的事情,至少工具集中是这样.",而地址清理器
的意义很重要,它发现的漏洞很多,1,2.
ASan
通过在编译时指示
内存访问,来在运行时
抓错误
的内存访问,该指令
代码根据(存储在"影子"
内存区域中)可/无法
访问的内存表
,来检查
访问的地址区间
.
指令代码
包括添加检查
内存中每个读/写
,并添加调用
来,标记指定内存区域
为访问的有效("未中毒")或无效("中毒")
.
在有效
内存区域(栈变量和堆
)周围,检测和运行时库
添加有毒的"红色区域"
,以便可检测到缓冲溢出/下溢
.
ASan
运行时库提供标记/检查
内存和报告错误
功能,但也覆盖malloc
和free
,这样,还可检查
动态分配的内存区域
的访问有效性
.这样,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:6有1对象(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
很容易找到该错误.该错误是由Eyal
从Weka.io
报告的,我觉得他花了很多
时间来弄清
是什么
被破坏了:
在传递ulong
(8
字节)和int
(4字节)时,core.atomic.atomicStore
会过量读栈,导致atomicStore
在ulong
的一半
写入垃圾.用ulong
和int
调用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字节
的读取试.
教训很清楚:应该尽快开始运行允许ASan
的Phobos
和druntime
测试包!
黑名单
某些代码需要访问受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
文件中,我禁止了druntime
和Phobos
中一些函数的检测,这样ASan
就不会对有"无效"
内存操作的代码触发
.
注意:一些与此黑名单
匹配的函数可能真有问题!可用ldc-build-runtime
工具构建允许ASan
的druntime
和Phobos
标准库,其中包含以下黑名单
:
> ./ldc-build-runtime --dFlags='-fsanitize=address;-fsanitize-blacklist=asan_blacklist.txt' BUILD_SHARED_LIBS=OFF
现在为了运行Phobos
和druntime
测试包,也需要为链接器
显式指定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
不会在过度读取
等时触发.
显然,这是D
的ASan
的主要缺失部分.在druntime
中必须实现
,毒化GC
内存池,并且只解毒
已提供给用户代码的部分(并在不再
使用及被收集
后再次毒化它).