LLVM 之 Clang 静态分析器篇(6):程序缺陷诊断——内存重复释放
Author: stormQ
Created: Wednesday, 09. June 2021 08:21PM
Last Modified: Thursday, 17. June 2021 08:27PM
本文基于release/12.x
版本的 LLVM 源码,以插件方式实现了用于检测 C 程序中内存重复释放
程序缺陷的plugin.unix.Malloc
检查器。从而,便于真正地理解 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:
54 void f2() {
55 int *p = malloc(12);
56 free(p);
57 free(p); // expected-warning{{Attempt to free released memory}}
58 }
如果局部变量p
所指向的内存分配成功(大多数情况下),那么上述程序会对该内存重复调用free
函数。也就是说,在该情况下会发生内存重复释放。
示例 2:
60 void f2_realloc_0() {
61 int *p = malloc(12);
62 realloc(p,0);
63 realloc(p,0); // expected-warning{{Attempt to free released memory}}
64 }
如果realloc
函数的第一个参数oldmem
的值不为NULL
且第二个参数bytes
的值为 0,那么调用realloc(oldmem, bytes)
相当于调用free(oldmem)
,并且realloc
函数的返回值为NULL
。
因此,如果局部变量p
所指向的内存分配成功(大多数情况下),那么上述程序相当于对该内存重复调用free
函数。也就是说,在该情况下会发生内存重复释放。
示例 3:
109 void reallocSizeZero1() {
110 char *p = malloc(12);
111 char *r = realloc(p, 0);
112 if (!r) {
113 free(p); // expected-warning {{Attempt to free released memory}}
114 } else {
115 free(r);
116 }
117 }
如果局部变量p
所指向的内存分配成功(大多数情况下),那么在执行语句char *r = realloc(p, 0);
后,p
所指向的内存会被释放并且局部变量r
的值为NULL
。因此,if
语句中的条件表达式!r
的求值结果会为true
,从而调用free(p)
(再次释放p
所指向的内存)。也就是说,在该情况下会发生内存重复释放。
如果realloc
函数的第一个参数oldmem
的值为NULL
(无论第二个参数bytes
的值是什么),那么调用realloc(oldmem, bytes)
相当于调用malloc(bytes)
。
因此,如果局部变量p
所指向的内存分配失败(少数情况下),那么在执行语句char *r = realloc(p, 0);
后,如果局部变量r
所指向的内存也分配失败,那么会调用free(p);
语句(由于p
的值为NULL
,所以什么都不做);否则,会调用free(r);
语句(正常释放了r
所指向的内存)。因此,这两种情况下程序都是正确的。
示例 4:
1474 // Make sure we catch errors when we free in a function which does not allocate memory.
1475 void freeButNoMalloc(int *p, int x){
1476 if (x) {
1477 free(p);
1478 //user forgot a return here.
1479 }
1480 free(p); // expected-warning {{Attempt to free released memory}}
1481 }
如果参数p
的值不为NULL
且参数x
的求值结果为true
,那么上述程序会对p
所指向的内存重复调用free
函数。也就是说,在该情况下会发生内存重复释放。
示例 5:
1655 void testOffsetZeroDoubleFree() {
1656 int *array = malloc(sizeof(int)*2);
1657 int *p = &array[0];
1658 free(p);
1659 free(&array[0]); // expected-warning{{Attempt to free released memory}}
1660 }
如果局部变量array
所指向的内存分配成功,那么由于局部变量p
和&array[0]
都指向同一块内存,因此上述程序会对该内存重复调用free
函数。也就是说,在该情况下会发生内存重复释放。
在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 个程序点。如果只实现检测 C 程序中内存重复释放的程序缺陷,那么至少需要订阅clang::ento::check::PostCall
和clang::ento::eval::Assume
程序点。
clang::ento::check::PostCall
,表示分析器引擎在每个函数被调用之后才会调用检查器的程序点。这意味着,如果启用了该检查器,那么MallocChecker::checkPostCall()
函数在每个函数执行之后都会被调用。
clang::ento::eval::Assume
,表示分析器引擎在每次程序状态中出现新的范围约束时就会调用检查器的程序点。这意味着,如果启用了该检查器,那么MallocChecker::evalAssume()
函数在每次程序状态中出现新的范围约束时都会被调用。
订阅clang::ento::check::PostCall
程序点是为了获取malloc
、free
和realloc
函数的参数或返回值,从而用于跟踪内存相关的自定义程序状态。
订阅clang::ento::eval::Assume
程序点是为了当程序状态中表示已分配内存的符号被约束为空指针时做一些处理。从而避免在某些情况下误报内存重复释放
的程序缺陷。这些情况比如:realloc
函数重新分配内存失败。
step 1: 定义检查器类
1) 需要订阅哪些程序点
要实现检测内存重复释放
的功能,我们至少需要订阅clang::ento::check::PostCall
和clang::ento::eval::Assume
程序点。除此之外,最好也订阅clang::ento::check::DeadSymbols
程序点(但不是必须的),从而清除不会再用到的自定义程序状态。
2) 定义检查器类
10 namespace {
11
// 省略 ...
16
17 class MallocChecker : public Checker<check::PostCall, eval::Assume> {
// 省略 ...
36
37 public:
// 省略 ...
39
40 void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
41 ProgramStateRef evalAssume(ProgramStateRef State, const SVal &Cond,
42 bool Assumption) const;
43 };
44
45 } // anonymous namespace
注:
如果确定仅在局部范围内使用该类,那么可以将检查器类的定义置于匿名空间中。从而,避免链接器将这些符号导出至外部。
订阅clang::ento::check::PostCall
程序点后,检查器类必须实现一个函数原型为void checkPostCall(const CallEvent &Call, CheckerContext &C) const
的成员函数。否则,会发生编译错误。
同样地,订阅clang::ento::eval::Assume
程序点后,检查器类必须实现一个函数原型为ProgramStateRef evalAssume(ProgramStateRef State, const SVal &Cond, bool Assumption) const
的成员函数。否则,也会发生编译错误。
step 2: 自定义程序状态
1) 定义状态枚举
10 namespace {
11
12 enum Kind {
13 Allocated,
14 Released,
15 };
// 省略 ...
44
45 } // anonymous namespace
其中,枚举值Allocated
表示内存处于已分配
的状态,枚举值Released
表示内存处于已释放
的状态。
2) 调用注册宏
47 REGISTER_MAP_WITH_PROGRAMSTATE(RegionState, SymbolRef, int)
参数RegionState
,表示自定义程序状态的类型(实质上是一个 C++ 类名,在该宏定义的内部进行定义)。
参数SymbolRef
,为llvm::ImmutableMap<Key, Value>
中Key
的数据类型,表示所分配内存对应的符号表达式。
参数int
,为llvm::ImmutableMap<Key, Value>
中Value
的数据类型,表示所分配内存的状态(是已分配
还是已释放
)。
上述宏用于跟踪通过malloc
或realloc
函数分配的内存的状态,用于判断是否发生了内存重复释放
。
3) 调用注册宏
48 REGISTER_MAP_WITH_PROGRAMSTATE(ReallocPairs, SymbolRef, SymbolRef)
第一个参数ReallocPairs
,表示自定义程序状态的类型(实质上是一个 C++ 类名,在该宏定义的内部进行定义)。
第二个参数SymbolRef
,为llvm::ImmutableMap<Key, Value>
中Key
的数据类型,表示realloc
函数所分配的新内存对应的符号表达式。
第三个参数SymbolRef
,为llvm::ImmutableMap<Key, Value>
中Value
的数据类型,表示realloc
函数所释放的旧内存对应的符号表达式。
上述宏用于跟踪realloc
函数所分配的新内存和所释放的旧内存之间的关联关系,用于在适当的时候处理realloc
函数重新分配内存失败的情况,从而避免误报内存重复释放
。
step 3: 实现检查器类
1) 实现checkPostCall()
成员函数
50 void MallocChecker::checkPostCall(const CallEvent &Call,
51 CheckerContext &C) const {
52 if (!Call.isGlobalCFunction()) {
53 return;
54 }
55
56 if (Call.isCalled(MallocFn)) {
57 checkMalloc(Call, C);
58 return;
59 }
60
61 if (Call.isCalled(FreeFn)) {
62 checkFree(Call, C);
63 return;
64 }
65
66 if (Call.isCalled(ReallocFn)) {
67 checkRealloc(Call, C);
68 return;
69 }
70 }
上述代码的逻辑为:
第 52~54 行,如果被调用的函数不是 C 语言风格的全局函数,那么直接返回。从而,避免不必要的后续处理。
第 56~59 行,如果被调用的函数是malloc
,那么调用checkMalloc()
函数进行实际处理并返回。
第 61~64 行,如果被调用的函数是free
,那么调用checkFree()
函数进行实际处理并返回。
第 66~69 行,如果被调用的函数是realloc
,那么调用checkRealloc()
函数进行实际处理并返回。
a) 定义并初始化数据成员MallocFn
、FreeFn
和ReallocFn
10 namespace {
11
// 省略 ...
16
17 class MallocChecker : public Checker<check::PostCall, eval::Assume> {
// 省略 ...
20 CallDescription MallocFn;
21 CallDescription FreeFn;
22 CallDescription ReallocFn;
// 省略 ...
37 public:
38 MallocChecker() : MallocFn("malloc"), FreeFn("free"), ReallocFn("realloc") {}
// 省略 ...
43 };
44
45 } // anonymous namespace
2) 实现malloc
函数被调用的处理逻辑
malloc
函数被调用的处理逻辑封装在成员函数checkMalloc()
中,其源码实现如下:
72 void MallocChecker::checkMalloc(const CallEvent &Call,
73 CheckerContext &C) const {
74 ProgramStateRef State = C.getState();
75 State = MallocMemAux(Call, C, State);
76 C.addTransition(State);
77 }
上述代码的逻辑为:获取当前程序状态并调用辅助函数MallocMemAux()
进行实际的处理。如果程序状态发生了变化,那么扩展程序状态图,从而让后继节点获知这一信息,并且可以继续探索该执行路径。
需要注意的是, 函数MallocMemAux()
的第三个参数ProgramStateRef State
之所以存在,是因为 C 语言中除了free
函数之外realloc
函数也会释放内存,但后者基于的程序状态可以不是当前程序状态。所以,通过添加该参数从而可以在realloc
函数中复用该处理逻辑。
a) 实现MallocMemAux()
成员函数
79 ProgramStateRef
80 MallocChecker::MallocMemAux(const CallEvent &Call, CheckerContext &C,
81 ProgramStateRef State) const {
82 if (!State) {
83 return nullptr;
84 }
85
86 SymbolRef Mem = Call.getReturnValue().getAsSymbol();
87 if (!Mem) {
88 return nullptr;
89 }
90
91 return State->set<RegionState>(Mem, Allocated);
92 }
上述代码的逻辑为:
第 86~89 行,获取被调用函数(可以是malloc
函数,也可以是realloc
函数)的返回值(即指向已分配内存的指针)所对应的符号表达式。如果获取失败,则返回一个空的程序状态。
第 91 行,返回一个新的程序状态,并且该程序状态中保存了这一信息:所分配的内存的状态为已分配
。
3) 实现free
函数被调用的处理逻辑
free
函数被调用的处理逻辑封装在成员函数checkFree()
中,其源码实现如下:
94 void MallocChecker::checkFree(const CallEvent &Call,
95 CheckerContext &C) const {
96 ProgramStateRef State = C.getState();
97 State = FreeMemAux(Call, C, State);
98 C.addTransition(State);
99 }
上述代码的逻辑为:获取当前程序状态并调用辅助函数FreeMemAux()
进行实际的处理。如果程序状态发生了变化,那么扩展程序状态图,从而让后继节点获知这一信息,并且可以继续探索该执行路径。
a) 实现FreeMemAux()
成员函数
101 ProgramStateRef
102 MallocChecker::FreeMemAux(const CallEvent &Call, CheckerContext &C,
103 ProgramStateRef State) const {
104 if (!State) {
105 return nullptr;
106 }
107
108 SymbolRef Mem = Call.getArgSVal(0).getAsSymbol();
109 if (!Mem) {
110 return nullptr;
111 }
112
113 const auto K = State->get<RegionState>(Mem);
114 if (!K || *K == Allocated) {
115 return State->set<RegionState>(Mem, Released);
116 }
117 if (*K == Released) {
118 reportDoubleFree(C);
119 }
120
121 return nullptr;
122 }
上述代码的逻辑为:
第 108~111 行,获取被调用函数(可以是free
函数,也可以是realloc
函数)的第一个参数(即指向要释放内存的指针)所对应的符号表达式。如果获取失败,则返回一个空的程序状态。
第 113~116 行,获取要释放的内存的状态。如果获取失败或者要释放的内存的状态为已分配
,那么返回一个新的程序状态,并且该程序状态中保存了这一信息:所分配的内存的状态为已释放
。
第 117~119 行,如果要释放的内存的状态为已释放
,那么表明发生了内存重复释放
,报告该程序缺陷并返回一个空的程序状态。
需要注意的是:
函数FreeMemAux()
在获取要释放的内存的状态失败时仍将所分配的内存的状态设置为已释放
。这样做,是因为如果要释放的内存是以函数参数的形式传入的,那么可能不会跟踪到该内存的分配情况。在该情况下,如果返回一个空的程序状态,那么会发生漏报内存重复释放
。
函数FreeMemAux()
在检测到内存重复释放
时,返回了一个空的程序状态。这样做,是因为如果程序发生了内存重复释放
,那么应该终止该执行路径的探索。也就是说,该执行路径不会再有后继节点。因此,不需要更新程序状态。
4) 实现realloc
函数被调用的处理逻辑
realloc
函数被调用的处理逻辑封装在成员函数checkRealloc()
中,其源码实现如下:
124 void MallocChecker::checkRealloc(const CallEvent &Call,
125 CheckerContext &C) const {
126 ProgramStateRef State = C.getState();
127 State = ReallocMemAux(Call, C, State);
128 C.addTransition(State);
129 }
上述代码的逻辑为:获取当前程序状态并调用辅助函数ReallocMemAux()
进行实际的处理。如果程序状态发生了变化,那么扩展程序状态图,从而让后继节点获知这一信息,并且可以继续探索该执行路径。
a) 实现ReallocMemAux()
成员函数
131 ProgramStateRef
132 MallocChecker::ReallocMemAux(const CallEvent &Call, CheckerContext &C,
133 ProgramStateRef State) const {
134 if (!State) {
135 return nullptr;
136 }
137
138 Optional<DefinedSVal> Arg0 = Call.getArgSVal(0).getAs<DefinedSVal>();
139 Optional<DefinedSVal> Arg1 = Call.getArgSVal(1).getAs<DefinedSVal>();
140 if (!Arg0 || !Arg1) {
141 return nullptr;
142 }
143
144 ConstraintManager &CM = C.getConstraintManager();
145 ProgramStateRef StatePtrNotNull, StatePtrIsNull;
146 std::tie(StatePtrNotNull, StatePtrIsNull) = CM.assumeDual(State, *Arg0);
147 ProgramStateRef StateSizeNotZero, StateSizeIsZero;
148 std::tie(StateSizeNotZero, StateSizeIsZero) = CM.assumeDual(State, *Arg1);
149 const bool PtrIsNull = StatePtrIsNull && !StatePtrNotNull;
150 const bool SizeIsZero = StateSizeIsZero && !StateSizeNotZero;
151
152 if (PtrIsNull) {
153 ProgramStateRef StateMalloc = MallocMemAux(Call, C, StatePtrIsNull);
154 return StateMalloc;
155 }
156
157 if (!PtrIsNull && SizeIsZero) {
158 ProgramStateRef StateFree = FreeMemAux(Call, C, StateSizeIsZero);
159 return StateFree;
160 }
161
162 if (ProgramStateRef StateFree = FreeMemAux(Call, C, State)) {
163 ProgramStateRef StateRealloc = MallocMemAux(Call, C, StateFree);
164 if (!StateRealloc) {
165 return nullptr;
166 }
167
168 SymbolRef FromPtr = Call.getArgSVal(0).getAsSymbol();
169 SymbolRef ToPtr = Call.getReturnValue().getAsSymbol();
170 assert(FromPtr && ToPtr &&
171 "By this point, FreeMemAux and MallocMemAux should have checked "
172 "whether the argument or the return value is symbolic!");
173 return StateRealloc->set<ReallocPairs>(ToPtr, FromPtr);
174 }
175
176 return nullptr;
177 }
上述代码的逻辑为:
第 138~142 行,获取realloc
函数的第一个参数(指向要释放的内存)和第二个参数(新分配内存的大小,单位:字节)的符号并转换为已定义的符号
。如果失败,则返回一个空的程序状态。
第 144~150 行,评估realloc
函数的第一个参数和第二个参数的值,并将评估结果保存到局部变量PtrIsNull
和SizeIsZero
中。如果PtrIsNull
的值为true
,那么表示realloc
函数的第一个参数的值为NULL
。如果SizeIsZero
的值为true
,那么表示realloc
函数的第二个参数的值为 0。
第 152~155 行,处理realloc
函数第一个参数的值为NULL
(无论第二个参数的值是什么)的情况。在该情况下,调用realloc(oldmem, bytes)
相当于调用malloc(bytes)
。
第 157~160 行,处理realloc
函数第一个参数的值不为NULL
且第二个参数的值为 0 的情况。在该情况下,调用realloc(oldmem, bytes)
相当于调用free(oldmem)
,并且realloc
函数的返回值为NULL
。
第 162~174 行,处理realloc
函数第一个参数的值不为NULL
且第二个参数的值不为 0 的情况。在该情况下,调用realloc(oldmem, bytes)
相当于调用free(oldmem)
和malloc(bytes)
。
第 168~173 行,用于保存realloc
函数所分配的新内存和所释放的旧内存之间的关联关系。从而,可以在适当的时候处理realloc
函数重新分配内存失败的情况以避免误报内存重复释放
。
需要注意的是, 在plugin.unix.Malloc
检查器的实现中,如果realloc
函数第一个参数的值为NULL
且第二个参数的值为 0,那么相当于调用malloc(bytes)
。而在unix.Malloc
检查器的实现中,如果遇到该情况就直接返回传入的程序状态,未进行其他处理。
5) 报告程序缺陷
a) 定义相关数据成员和成员函数
10 namespace {
11
// 省略 ...
16
17 class MallocChecker : public Checker<check::PostCall, eval::Assume> {
18 mutable std::unique_ptr<BugType> DoubleFreeBT;
// 省略 ...
35 void reportDoubleFree(CheckerContext &C) const;
// 省略 ...
43 };
44
45 } // anonymous namespace
我们需要定义一个数据类型为std::unique_ptr<BugType>
的数据成员(这里是DoubleFreeBT
),用于表示一个新的程序缺陷。为了在对象析构时能够自动释放内存,因此使用了智能指针。
另外,我们将报告该程序缺陷的处理逻辑封装到成员函数reportDoubleFree()
中。
b) 实现程序缺陷报告逻辑
179 void MallocChecker::reportDoubleFree(CheckerContext &C) const {
180 if (!DoubleFreeBT) {
181 DoubleFreeBT.reset(new BugType(this, "Double free", "Memory Error"));
182 }
183
184 if (ExplodedNode *N = C.generateErrorNode()) {
185 auto R = std::make_unique<PathSensitiveBugReport>(
186 *DoubleFreeBT, DoubleFreeBT->getDescription(), N);
187 C.emitReport(std::move(R));
188 }
189 }
上述代码的逻辑为:报告内存重复释放
的程序缺陷,并终止该执行路径的探索。
6) 实现evalAssume()
成员函数
191 ProgramStateRef MallocChecker::evalAssume(ProgramStateRef State,
192 const SVal &Cond,
193 bool Assumption) const {
194 auto isNull = [&](ProgramStateRef State, SymbolRef Sym) {
195 // Return true if a symbol is NULL.
196 return State->getConstraintManager().isNull(State, Sym).isConstrainedTrue();
197 };
198
199 for (const auto &TrackedRegion : State->get<RegionState>()) {
200 SymbolRef R = TrackedRegion.first;
201 if (isNull(State, R)) {
202 State = State->remove<RegionState>(R);
203 }
204 }
205
206 for (const auto &ReallocPair : State->get<ReallocPairs>()) {
207 SymbolRef ToPtr = ReallocPair.first;
208 if (isNull(State, ToPtr)) {
209 State = State->remove<ReallocPairs>(ToPtr);
210 SymbolRef FromPtr = ReallocPair.second;
211 State = State->set<RegionState>(FromPtr, Allocated);
212 }
213 }
214
215 return State;
216 }
上述代码的逻辑为:
第 198~201 行,定义一个 lambda 表达式,用于判断符号的值是否NULL
(根据程序状态中的约束条件)。
第 203~208 行,清除自定义程序状态RegionState
中所分配的内存对应的符号的值为NULL
的信息(最好这样做,但不是必须的)。
第 210~217 行,处理realloc
函数重新分配内存失败的情况。也就是说,如果realloc
函数重新分配内存失败,那么将其要释放的内存的状态重新设置为已分配
,并清除自定义程序状态ReallocPairs
中的相关信息。这样做,是为了避免误报内存重复释放
。
step 4: 构建测试
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 malloc.c -I ~/git-projects/llvm-project/clang/test/Analysis/
输出结果如下:
malloc.c:57:3: warning: Double free [plugin.unix.Malloc]
free(p); // expected-warning{{Attempt to free released memory}}
^~~~~~~
malloc.c:63:3: warning: Double free [plugin.unix.Malloc]
realloc(p,0); // expected-warning{{Attempt to free released memory}}
^~~~~~~~~~~~
malloc.c:113:5: warning: Double free [plugin.unix.Malloc]
free(p); // expected-warning {{Attempt to free released memory}}
^~~~~~~
malloc.c:123:5: warning: Double free [plugin.unix.Malloc]
free(p); // expected-warning {{Attempt to free released memory}}
^~~~~~~
malloc.c:127:3: warning: Double free [plugin.unix.Malloc]
free(p); // expected-warning {{Attempt to free released memory}}
^~~~~~~
malloc.c:749:3: warning: Double free [plugin.unix.Malloc]
free(p); // expected-warning{{Attempt to free released memory}}
^~~~~~~
malloc.c:1480:3: warning: Double free [plugin.unix.Malloc]
free(p); // expected-warning {{Attempt to free released memory}}
^~~~~~~
malloc.c:1490:13: warning: Double free [plugin.unix.Malloc]
char *m = realloc(a->p, size); // expected-warning {{Attempt to free released memory}}
^~~~~~~~~~~~~~~~~~~
malloc.c:1659:3: warning: Double free [plugin.unix.Malloc]
free(&array[0]); // expected-warning{{Attempt to free released memory}}
^~~~~~~~~~~~~~~
9 warnings generated.
从上面的输出结果可以看出,检查器plugin.unix.malloc
检测到malloc.c
文件中存在 9 个内存重复释放
的程序缺陷。
2) 运行检查器unix.malloc
执行命令:
$ clang -cc1 -w -analyze -analyzer-checker=unix.Malloc malloc.c -I ~/git-projects/llvm-project/clang/test/Analysis/
输出结果如下(省略无关的程序缺陷):
// 省略 ...
malloc.c:57:3: warning: Attempt to free released memory [unix.Malloc]
free(p); // expected-warning{{Attempt to free released memory}}
^~~~~~~
malloc.c:63:3: warning: Attempt to free released memory [unix.Malloc]
realloc(p,0); // expected-warning{{Attempt to free released memory}}
^~~~~~~~~~~~
// 省略 ...
malloc.c:113:5: warning: Attempt to free released memory [unix.Malloc]
free(p); // expected-warning {{Attempt to free released memory}}
^~~~~~~
malloc.c:123:5: warning: Attempt to free released memory [unix.Malloc]
free(p); // expected-warning {{Attempt to free released memory}}
^~~~~~~
malloc.c:127:3: warning: Attempt to free released memory [unix.Malloc]
free(p); // expected-warning {{Attempt to free released memory}}
^~~~~~~
// 省略 ...
malloc.c:222:9: warning: Attempt to free released memory [unix.Malloc]
free(buf); // expected-warning {{Attempt to free released memory}}
^~~~~~~~~
// 省略 ...
malloc.c:749:3: warning: Attempt to free released memory [unix.Malloc]
free(p); // expected-warning{{Attempt to free released memory}}
^~~~~~~
// 省略 ...
malloc.c:1480:3: warning: Attempt to free released memory [unix.Malloc]
free(p); // expected-warning {{Attempt to free released memory}}
^~~~~~~
malloc.c:1490:13: warning: Attempt to free released memory [unix.Malloc]
char *m = realloc(a->p, size); // expected-warning {{Attempt to free released memory}}
^~~~~~~~~~~~~~~~~~~
// 省略 ...
malloc.c:1659:3: warning: Attempt to free released memory [unix.Malloc]
free(&array[0]); // expected-warning{{Attempt to free released memory}}
^~~~~~~~~~~~~~~
// 省略 ...
95 warnings generated.
从上面的输出结果可以看出,对于同一个测试文件malloc.c
而言,检查器unix.malloc
检测到该文件中存在 10 个内存重复释放
的程序缺陷。
其中,检查器plugin.unix.malloc
未检测到的程序缺陷如下:
217 void reallocfRadar6337483_3() {
218 char * buf = malloc(100);
219 char * tmp;
220 tmp = (char*)reallocf(buf, 0x1000000);
221 if (!tmp) {
222 free(buf); // expected-warning {{Attempt to free released memory}}
223 return;
224 }
225 buf = tmp;
226 free(buf);
227 }
上述程序中的reallocf
函数不是 C 语言提供的 API 而是用户自定义的,其行为与realloc
的唯一区别在于:前者在重新分配内存失败后仍会释放旧内存。由于检查器plugin.unix.malloc
中未对该函数进行建模。因此,不会检测到该程序缺陷。
step 1: gdb 调试
1) gdb 调试文件
plugin.malloc.gdb 的内容如下:
file /usr/local/bin/clang
set args -cc1 -w -load ~/git-projects/llvm-project/build_ninja/lib/MallocCheckerPlugin.so -analyze -analyzer-checker=plugin.unix.Malloc -I ~/git-projects/llvm-project/clang/test/Analysis/ malloc.c
set listsize 20
set breakpoint pending on
start
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:57
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:62
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:67
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:141
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:153
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:158
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:162
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:163
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:165
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:173
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:176
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:199
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:202
b clang/lib/Analysis/plugins/CheckerMalloc/MallocChecker.cpp:211
2) 如何启动
$ gdb -q -x plugin.malloc.gdb
step 2: 辅助调试的 Clang API
Clang API | 示例 | 作用 |
---|---|---|
clang::ento::ProgramState::dump() | 打印程序状态 | |
clang::ento::SymExpr::dump() | 打印符号 | |
clang::Stmt::dump() | 打印表达式的抽象语法树 |
要实现检测内存重复释放
的功能,我们需要跟踪内存的已分配和已释放情况。通过自定义程序状态RegionState
和ReallocPairs
可以做到这一点。前者用于跟踪内存的已分配和已释放情况。从而,在释放内存时可以根据该信息判断是否发生了内存重复释放
。后者用于跟踪realloc
函数所分配的新内存和所释放的旧内存之间的关联关系。从而,可以在适当的时候处理realloc
函数重新分配内存失败的情况以避免误报内存重复释放
。
检测内存重复释放
的工作原理:当程序调用malloc
函数后,将其所分配内存已分配
的信息保存到自定义程序状态RegionState
中。当程序调用free
函数时,如果要释放的内存处于已分配
状态或者无法获知确切状态,那么将其更新为已释放
状态;如果要释放的内存处于已释放
状态,那么表明发生了内存重复释放
。当程序调用realloc
函数后,根据参数取值的不同可以分为以下三种情况:
如果realloc
函数第一个参数的值为NULL
(无论第二个参数的值是什么),那么调用realloc(oldmem, bytes)
相当于调用malloc(bytes)
。
如果realloc
函数第一个参数的值不为NULL
且第二个参数的值为 0,那么调用realloc(oldmem, bytes)
相当于调用free(oldmem)
,并且realloc
函数的返回值为NULL
。
如果realloc
函数第一个参数的值不为NULL
且第二个参数的值不为 0,那么调用realloc(oldmem, bytes)
相当于调用free(oldmem)
和malloc(bytes)
。在该情况下,需要保存realloc
函数所分配的新内存和所释放的旧内存之间的关联关系。
下一篇:LLVM 之 Clang 静态分析器篇(7):程序缺陷诊断——内存泄露