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检查器的源码实现目录如下:

plugin.unix.Malloc检查器的源码实现目录如下:


C 程序中典型的非法读写已释放内存

本节介绍了几个 C 程序中典型的非法读写已释放内存的示例,涉及mallocfreerealloc函数。这些示例均来自 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 行被释放了(由于局部变量xy指向同一块内存),而在第 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::Locationclang::ento::check::PreCallclang::ento::check::EndFunction程序点。

订阅clang::ento::check::Location程序点是为了捕获程序中读或写内存的行为。通过检查读或写的内存是否已释放,从而判断是否发生了非法读写已释放内存程序缺陷。

订阅clang::ento::check::PreCall程序点是为了捕获程序中传递参数给函数的行为。通过检查传递的参数是否指向已释放的内存,从而判断是否可能发生了非法读写已释放内存程序缺陷。

订阅clang::ento::check::EndFunction程序点是为了捕获程序中函数返回值的行为。通过检查函数返回值是否指向已释放的内存,从而判断是否可能发生了非法读写已释放内存程序缺陷。

返回上一级


自己动手实现非法读写已释放内存的检测

step 1: 定义检查器类

要实现检测 C 程序中非法读写已释放内存程序缺陷的功能,我们需要在第 2 版plugin.unix.Malloc检查器实现的基础上订阅clang::ento::check::Locationclang::ento::check::PreCallclang::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 }

上述代码的逻辑为:

需要注意的是, 如果将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 }

上述代码的逻辑为:

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 }

上述代码的逻辑为:

需要注意的是, 为了保持一致,这里也将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 }

上述代码的逻辑为:

需要注意的是, 为了保持一致,这里也将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 }

上述代码的逻辑为:

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无法检测出的一些非法读写已释放内存程序缺陷。


References


下一篇:LLVM 之 Clang 静态分析器篇(9):程序缺陷诊断——数组越界

上一篇:LLVM 之 Clang 静态分析器篇(7):程序缺陷诊断——内存泄露

首页