LLVM 之 Clang 静态分析器篇(8):程序缺陷诊断——非法读写已释放的内存
Author: stormQ
Created: Tuesday, 29. June 2021 09:28PM
Last Modified: Friday, 02. July 2021 10:01PM
本文基于release/12.x
版本的 LLVM 源码,在第 2 版plugin.unix.Malloc
检查器(实现了检测 C 程序中的内存重复释放
和内存泄露
)的基础上实现了用于检测 C 程序中非法读写已释放内存
程序缺陷的功能。从而,便于真正地理解 Clang 静态分析器是如何在unix.Malloc
检查器中实现该功能的。除此之外,也有助于按照自身需求修改或扩展 Clang 静态分析器。
unix.Malloc
检查器的源码实现目录如下:
clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
plugin.unix.Malloc
检查器的源码实现目录如下:
clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp
本节介绍了几个 C 程序中典型的非法读写已释放内存的示例,涉及malloc
、free
和realloc
函数。这些示例均来自 LLVM 项目中的测试文件 malloc.c(定义在 clang/test/Analysis/malloc.c 文件中)。
示例 1:
429 void f7() {
430 char *x = (char*) malloc(4);
431 free(x);
432 x[0] = 'a'; // expected-warning{{Use of memory after it is freed}}
433 }
上述程序中局部变量x
所指向的内存已经在第 431 行被释放了,而在第 432 行仍向该内存写入数据。也就是说,上述程序存在非法读写已释放内存
程序缺陷。
示例 2:
441 void f7_realloc() {
442 char *x = (char*) malloc(4);
443 realloc(x,0);
444 x[0] = 'a'; // expected-warning{{Use of memory after it is freed}}
445 }
如果realloc()
函数重新分配内存成功(大多数情况下),那么局部变量x
所指向的内存在第 443 行会被释放。在该情况下,在第 444 行仍向该内存写入数据。也就是说,上述程序存在非法读写已释放内存
程序缺陷。
如果realloc()
函数重新分配内存失败(少数情况下),那么局部变量x
仍指向原内存。在该情况下,在第 444 行向该内存写入数据是合法的。
示例 3:
722 void paramFree(int *p) {
723 myfoo(p);
724 free(p); // no warning
725 myfoo(p); // expected-warning {{Use of memory after it is freed}}
726 }
上述程序中局部变量p
所指向的内存已经在第 724 行被释放了,而在第 725 行将该变量传递给myfoo()
函数。如果myfoo()
函数中会向该内存写入数据,那么上述程序存在非法读写已释放内存
程序缺陷。
示例 4:
778 void mallocBindFreeUse() {
779 int *x = malloc(12);
780 int *y = x;
781 free(y);
782 myfoo(x); // expected-warning{{Use of memory after it is freed}}
783 }
上述程序中局部变量x
所指向的内存已经在第 781 行被释放了(由于局部变量x
和y
指向同一块内存),而在第 782 行将该变量传递给myfoo()
函数。如果myfoo()
函数中会向该内存写入数据,那么上述程序存在非法读写已释放内存
程序缺陷。
示例 5:
828 int *mallocReturnFreed() {
829 int *p = malloc(12);
830 free(p);
831 return p; // expected-warning {{Use of memory after it is freed}}
832 }
上述程序中局部变量p
所指向的内存已经在第 830 行被释放了,而在第 831 行将该变量作为返回值传递给外部函数。如果外部函数中会向该内存写入数据,那么上述程序存在非法读写已释放内存
程序缺陷。
在unix.Malloc
检查器中,检查器类MallocChecker
用于实现内存相关错误(比如:内存泄露
、内存重复释放
)的诊断功能,其源码实现如下(定义在 clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp 文件中):
286 namespace {
287
288 class MallocChecker
289 : public Checker<check::DeadSymbols, check::PointerEscape,
290 check::ConstPointerEscape, check::PreStmt<ReturnStmt>,
291 check::EndFunction, check::PreCall, check::PostCall,
292 check::NewAllocator, check::PostStmt<BlockExpr>,
293 check::PostObjCMessage, check::Location, eval::Assume> {
// 省略 ...
728 };
// 省略 ...
859 } // end anonymous namespace
从上面的代码可以看出,该检查器类共订阅了 12 个程序点。如果要在第 2 版plugin.unix.Malloc
检查器的基础上实现用于检测 C 程序中非法读写已释放内存
程序缺陷的功能,那么需要订阅clang::ento::check::Location
、clang::ento::check::PreCall
和clang::ento::check::EndFunction
程序点。
clang::ento::check::Location
,表示分析器引擎在程序读或写内存时就会调用检查器的程序点。这意味着,如果启用了该检查器,那么MallocChecker::checkLocation()
函数在程序读或写内存时都会被调用。
clang::ento::check::PreCall
,表示分析器引擎在每个函数被调用之前就会调用检查器的程序点。这意味着,如果启用了该检查器,那么MallocChecker::checkPreCall()
函数在每个函数执行之前都会被调用。
clang::ento::check::EndFunction
,表示分析器引擎在函数离开函数体时(包括所有的函数退出点)就会调用检查器的程序点。这意味着,如果启用了该检查器,那么MallocChecker::checkEndFunction()
函数在函数离开函数体时都会被调用。
订阅clang::ento::check::Location
程序点是为了捕获程序中读或写内存的行为。通过检查读或写的内存是否已释放,从而判断是否发生了非法读写已释放内存
程序缺陷。
订阅clang::ento::check::PreCall
程序点是为了捕获程序中传递参数给函数的行为。通过检查传递的参数是否指向已释放的内存,从而判断是否可能发生了非法读写已释放内存
程序缺陷。
订阅clang::ento::check::EndFunction
程序点是为了捕获程序中函数返回值的行为。通过检查函数返回值是否指向已释放的内存,从而判断是否可能发生了非法读写已释放内存
程序缺陷。
step 1: 定义检查器类
要实现检测 C 程序中非法读写已释放内存
程序缺陷的功能,我们需要在第 2 版plugin.unix.Malloc
检查器实现的基础上订阅clang::ento::check::Location
、clang::ento::check::PreCall
和clang::ento::check::EndFunction
程序点。
因此,修改后的检查器类定义如下:
10 namespace {
11
省略 ...
18
19 class MallocChecker : public Checker<check::PostCall, eval::Assume,
20 check::DeadSymbols, check::PointerEscape,
21 check::Location, check::PreCall,
22 check::EndFunction> {
省略 ...
90 };
省略 ...
113
114 } // anonymous namespace
另外,我们需要实现函数原型如下的 3 个成员函数:
86 void checkLocation(const SVal &Location, bool IsLoad, const Stmt *S,
87 CheckerContext &C) const;
88 void checkPreCall(const CallEvent &Call, CheckerContext &C) const;
89 void checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const;
step 2: 实现检查器类
1) 实现checkLocation()
成员函数
488 void MallocChecker::checkLocation(const SVal &Location, bool IsLoad,
489 const Stmt *S, CheckerContext &C) const {
490 SymbolRef Sym = Location.getAsSymbol(true);
491 if (Sym) {
492 checkUseAfterFree(Sym, C);
493 }
494 }
上述代码的逻辑为:
第 490 行,获取进行读或写内存操作的符号。
第 491~493 行,如果获取成功,则检查该符号是否发生了非法读写已释放内存
程序缺陷。
需要注意的是, 如果将getAsSymbol()
函数的参数设置为true
,那么表示允许从符号的SuperRegion
中查找符号表达式。这样做是因为,笔者认为如下类似的程序可能发生非法读写已释放内存
程序缺陷。比如:
83 void paramFree_1(int *p) {
84 myfoo(p);
85 free(p); // no warning
86 p--; // no warning
87 myfoo(p); // expected-warning{{Use of memory after it is freed}}?
88 }
plugin.unix.Malloc
检查器的诊断结果:
test_arg.c:87:3: warning: Potential use of memory after it is freed [plugin.unix.Malloc]
myfoo(p); // no warning
^~~~~~~~
而对于上述程序,unix.Malloc
检查器不认为第 87 行发生了非法读写已释放内存
程序缺陷。
a) 实现checkUseAfterFree()
成员函数
496 void MallocChecker::checkUseAfterFree(SymbolRef Sym, CheckerContext &C,
497 const char *Msg /*= nullptr*/) const {
498 assert(Sym);
499 const auto K = C.getState()->get<RegionState>(Sym);
500 if (K && *K == Released) {
501 reportUseAfterFree(C, Msg);
502 }
503 }
上述代码的逻辑为:
第 499 行,从自定义状态RegionState
中获取符号所代表内存的状态。
第 500~502 行,如果获取成功并且符号所代表的内存处于已分配
状态,那么表明发生了非法读写已释放内存
,并报告该程序缺陷(可以通过参数Msg
更改诊断结果中的默认描述信息)。
2) 实现checkPreCall()
成员函数
519 void MallocChecker::checkPreCall(const CallEvent &Call,
520 CheckerContext &C) const {
521 if (isFreeingMemCall(Call)) {
522 return;
523 }
524
525 for (unsigned I = 0, E = Call.getNumArgs(); I < E; ++I) {
526 SymbolRef Sym = Call.getArgSVal(I).getAsSymbol(true);
527 if (Sym) {
528 checkUseAfterFree(Sym, C, "Potential use of memory after it is freed");
529 }
530 }
531 }
上述代码的逻辑为:
第 521~523 行,不处理free()
和realloc()
等释放内存的函数。这意味着,检查内存重复释放
程序缺陷的优先级高于检查非法读写已释放内存
程序缺陷。
第 525~530 行,遍历所有传递的参数,检查这些参数是否指向已释放的内存。
第 526 行,获取所传递参数的符号。
第 527~529 行,如果获取成功,则检查该符号是否可能发生了非法读写已释放内存
程序缺陷。
需要注意的是, 为了保持一致,这里也将getAsSymbol()
函数的参数设置为true
。
a) 实现isFreeingMemCall()
成员函数
533 bool MallocChecker::isFreeingMemCall(const CallEvent &Call) const {
534 if (!Call.isGlobalCFunction()) {
535 return false;
536 }
537 return FreeingMemFnMap.lookup(Call) || ReallocatingMemFnMap.lookup(Call);
538 }
b) 修改isMemCall()
成员函数
481 bool MallocChecker::isMemCall(const CallEvent &Call) const {
482 if (!Call.isGlobalCFunction()) {
483 return false;
484 }
485 return isFreeingMemCall(Call) || AllocatingMemFnMap.lookup(Call);
486 }
为了避免重复代码以便更容易地维护,isMemCall()
成员函数中通过调用isFreeingMemCall()
来判断是否为释放内存相关的函数。
3) 实现checkEndFunction()
成员函数
540 void MallocChecker::checkEndFunction(const ReturnStmt *RS,
541 CheckerContext &C) const {
542 if (!RS) {
543 return;
544 }
545
546 if (const Expr *E = RS->getRetValue()) {
547 SymbolRef Sym = C.getSVal(E).getAsSymbol(true);
548 if (Sym) {
549 checkUseAfterFree(Sym, C, "Potential use of memory after it is freed");
550 }
551 }
552 }
上述代码的逻辑为:
第 542~544 行,判断函数是否通过return
语句退出的。也就是说,如果不是通过显式地调用return
语句离开函数体的,那么表明该函数没有返回值(不包括通过参数传递返回值的情况),从而不需要进行后续处理。
第 546 行,获取return
语句中返回值所对应的表达式。如果是一个空的return
语句,那么将获取失败。在该情况下,也不需要进行后续处理。
第 547 行,获取函数返回值的符号。
第 548~550 行,如果获取成功,则检查该符号是否可能发生了非法读写已释放内存
程序缺陷。
需要注意的是, 为了保持一致,这里也将getAsSymbol()
函数的参数设置为true
。
4) 报告程序缺陷
a) 定义相关数据成员和成员函数
10 namespace {
11
省略 ...
18
19 class MallocChecker : public Checker<check::PostCall, eval::Assume,
20 check::DeadSymbols, check::PointerEscape,
21 check::Location, check::PreCall,
22 check::EndFunction> {
省略 ...
25 mutable std::unique_ptr<BugType> UseAfterFreeBT;
省略 ...
90 };
省略 ...
113
114 } // anonymous namespace
我们需要定义一个数据类型为std::unique_ptr<BugType>
的数据成员(这里是UseAfterFreeBT
),用于表示一个新的程序缺陷。为了在对象析构时能够自动释放内存,因此使用了智能指针。
另外,我们将报告该程序缺陷的处理逻辑封装到成员函数reportUseAfterFree()
中。
b) 实现程序缺陷报告逻辑
505 void MallocChecker::reportUseAfterFree(CheckerContext &C,
506 const char *Msg /*= nullptr*/) const {
507 if (!UseAfterFreeBT) {
508 UseAfterFreeBT.reset(new BugType(this, "Use of memory after it is freed",
509 "Memory Error"));
510 }
511
512 if (ExplodedNode *N = C.generateErrorNode()) {
513 auto R = std::make_unique<PathSensitiveBugReport>(
514 *UseAfterFreeBT, Msg ? Msg : UseAfterFreeBT->getDescription(), N);
515 C.emitReport(std::move(R));
516 }
517 }
上述代码的逻辑为:
第 507~510 行,创建非法读写已释放内存
程序缺陷实例(如果未创建的话)。
第 512~516 行,创建Sink
节点使分析器引擎不再继续探索该执行路径,并报告非法读写已释放内存
程序缺陷。
step 3: 构建测试
1) 构建
注:具体过程可参考笔者的另一篇文章:《LLVM 之 Clang 静态分析器篇(2):如何扩展 Clang 静态分析器》。
2) 诊断结果对比
对于同一个测试文件malloc.c
,分别运行自己实现的检查器plugin.unix.malloc
和 Clang 静态分析器实现的检查器unix.malloc
。
1) 运行检查器plugin.unix.malloc
执行命令:
$ clang -cc1 -w -load ~/git-projects/llvm-project/build_ninja/lib/MallocCheckerPlugin.so -analyze -analyzer-checker=plugin.unix.Malloc -analyzer-checker=unix.cstring.CStringModeling -I ~/git-projects/llvm-project/clang/test/Analysis/ malloc.c
输出结果如下(只保留非法读写已释放内存
程序缺陷):
省略 ...
malloc.c:432:8: warning: Use of memory after it is freed [plugin.unix.Malloc]
x[0] = 'a'; // expected-warning{{Use of memory after it is freed}}
~~~~~^~~~~
malloc.c:438:13: warning: Potential use of memory after it is freed [plugin.unix.Malloc]
char *y = strndup(x, 4); // expected-warning{{Use of memory after it is freed}}
^~~~~~~~~~~~~
malloc.c:444:8: warning: Use of memory after it is freed [plugin.unix.Malloc]
x[0] = 'a'; // expected-warning{{Use of memory after it is freed}}
~~~~~^~~~~
省略 ...
malloc.c:725:3: warning: Potential use of memory after it is freed [plugin.unix.Malloc]
myfoo(p); // expected-warning {{Use of memory after it is freed}}
^~~~~~~~
省略 ...
malloc.c:756:3: warning: Potential use of memory after it is freed [plugin.unix.Malloc]
myfoo(p); // expected-warning{{Use of memory after it is freed}}
^~~~~~~~
malloc.c:782:3: warning: Potential use of memory after it is freed [plugin.unix.Malloc]
myfoo(x); // expected-warning{{Use of memory after it is freed}}
^~~~~~~~
省略 ...
malloc.c:807:3: warning: Potential use of memory after it is freed [plugin.unix.Malloc]
myfoo(p); //expected-warning{{Use of memory after it is freed}}
^~~~~~~~
malloc.c:813:12: warning: Use of memory after it is freed [plugin.unix.Malloc]
myfooint(*p); //expected-warning{{Use of memory after it is freed}}
^~
malloc.c:831:3: warning: Potential use of memory after it is freed [plugin.unix.Malloc]
return p; // expected-warning {{Use of memory after it is freed}}
^~~~~~~~
malloc.c:838:10: warning: Use of memory after it is freed [plugin.unix.Malloc]
return px->g; // expected-warning {{Use of memory after it is freed}}
^~~~~
省略 ...
malloc.c:871:3: warning: Potential use of memory after it is freed [plugin.unix.Malloc]
myfoo(p); // expected-warning{{Use of memory after it is freed}}
^~~~~~~~
65 warnings generated.
从上面的输出结果可以看出,检查器plugin.unix.malloc
检测到malloc.c
文件中存在 11 个非法读写已释放内存
程序缺陷。
2) 运行检查器unix.malloc
执行命令:
$ clang -cc1 -w -analyze -analyzer-checker=unix.Malloc malloc.c -I ~/git-projects/llvm-project/clang/test/Analysis/
输出结果如下(只保留非法读写已释放内存
程序缺陷):
省略 ...
malloc.c:432:8: warning: Use of memory after it is freed [unix.Malloc]
x[0] = 'a'; // expected-warning{{Use of memory after it is freed}}
~~~~ ^
malloc.c:438:13: warning: Use of memory after it is freed [unix.Malloc]
char *y = strndup(x, 4); // expected-warning{{Use of memory after it is freed}}
^~~~~~~~~~~~~
malloc.c:444:8: warning: Use of memory after it is freed [unix.Malloc]
x[0] = 'a'; // expected-warning{{Use of memory after it is freed}}
~~~~ ^
省略 ...
malloc.c:725:3: warning: Use of memory after it is freed [unix.Malloc]
myfoo(p); // expected-warning {{Use of memory after it is freed}}
^~~~~~~~
省略 ...
malloc.c:756:3: warning: Use of memory after it is freed [unix.Malloc]
myfoo(p); // expected-warning{{Use of memory after it is freed}}
^~~~~~~~
malloc.c:782:3: warning: Use of memory after it is freed [unix.Malloc]
myfoo(x); // expected-warning{{Use of memory after it is freed}}
^~~~~~~~
省略 ...
malloc.c:807:3: warning: Use of memory after it is freed [unix.Malloc]
myfoo(p); //expected-warning{{Use of memory after it is freed}}
^~~~~~~~
malloc.c:813:12: warning: Use of memory after it is freed [unix.Malloc]
myfooint(*p); //expected-warning{{Use of memory after it is freed}}
^~
malloc.c:831:3: warning: Use of memory after it is freed [unix.Malloc]
return p; // expected-warning {{Use of memory after it is freed}}
^~~~~~~~
malloc.c:838:10: warning: Use of memory after it is freed [unix.Malloc]
return px->g; // expected-warning {{Use of memory after it is freed}}
^~~~~
省略 ...
malloc.c:871:3: warning: Use of memory after it is freed [unix.Malloc]
myfoo(p); // expected-warning{{Use of memory after it is freed}}
^~~~~~~~
省略 ...
95 warnings generated.
从上面的输出结果可以看出,检查器unix.malloc
检测到malloc.c
文件中存在 11 个非法读写已释放内存
程序缺陷。
将上述两个诊断结果进行对比,对于malloc.c
文件中所有的非法读写已释放内存
程序缺陷,两个检查器都检测出了。差异之一在于检查器plugin.unix.malloc
提供了更准确的诊断信息。
要实现用于检测 C 程序中非法读写已释放内存
程序缺陷的功能,我们需要捕获程序的如下行为:读或写内存;传递参数给函数;函数返回值。
如果读或写的内存处于已释放
状态,那么一定发生了非法读写已释放内存
程序缺陷。
如果函数参数或函数返回值指向的内存处于已释放
状态,那么可能会发生非法读写已释放内存
程序缺陷。
相比于目前 Clang 静态分析器中所实现的unix.malloc
检查器,检查器plugin.unix.malloc
提供了更准确的诊断信息,即在诊断信息中区分了非法读写已释放内存
程序缺陷是一定发生了
还是可能会发生
。
除此之外,检查器plugin.unix.malloc
在捕获上述程序行为后保持了一致的处理,即允许从符号的SuperRegion
中查找符号表达式。从而,可以检测出检查器unix.malloc
无法检测出的一些非法读写已释放内存
程序缺陷。
下一篇:LLVM 之 Clang 静态分析器篇(9):程序缺陷诊断——数组越界