LLVM 之 Clang 静态分析器篇(5):程序缺陷诊断——fopen 和 fclose API 误用
Author: stormQ
Created: Thursday, 03. June 2021 09:11PM
Last Modified: Wednesday, 09. June 2021 10:07PM
本文基于release/12.x
版本的 LLVM 源码,研究了 Clang 静态分析器中alpha.unix.SimpleStream
检查器的工作原理。从而,有助于按照自身需求修改或扩展 Clang 静态分析器。
alpha.unix.SimpleStream
检查器用于检测以下两种程序缺陷:
重复释放。即对同一个资源重复调用fclose
函数。
资源泄露。即对同一个资源在调用了fopen
函数之后未再调用fclose
函数。
为了便于研究,本文以插件方式实现了plugin.alpha.unix.SimpleStream
检查器。相比于alpha.unix.SimpleStream
检查器,其源码实现略有不同。
alpha.unix.SimpleStream
检查器的源码实现目录如下:
clang/lib/StaticAnalyzer/Checkers/SimpleStreamChecker.cpp
plugin.alpha.unix.SimpleStream
检查器的源码实现目录如下:
clang/lib/Analysis/plugins/CheckerSimpleStream/SimpleStreamChecker.cpp
在alpha.unix.SimpleStream
检查器中,检查器类SimpleStreamChecker
用于实现fopen 和 fclose API 误用
相关程序缺陷的诊断功能,其源码实现如下(定义在 clang/lib/StaticAnalyzer/Checkers/SimpleStreamChecker.cpp 文件中):
27 namespace {
// 省略 ...
50 class SimpleStreamChecker : public Checker<check::PostCall,
51 check::PreCall,
52 check::DeadSymbols,
53 check::PointerEscape> {
// 省略 ...
83 };
84
85 } // end anonymous namespace
从上面的代码可以看出,该检查器类订阅了以下 4 个程序点:
clang::ento::check::PostCall
,表示分析器引擎在每个函数被调用之后才会调用检查器的程序点。这意味着,如果启用了该检查器,那么SimpleStreamChecker::checkPostCall()
函数在每个函数执行之后都会被调用。
clang::ento::check::PreCall
,表示分析器引擎在每个函数被调用之前就会调用检查器的程序点。这意味着,如果启用了该检查器,那么SimpleStreamChecker::checkPreCall()
函数在每个函数执行之前都会被调用。
clang::ento::check::DeadSymbols
,表示分析器引擎对任意符号进行垃圾回收(即在之后的分析过程中不会再遇到该符号)时就会调用检查器的程序点。这意味着,如果启用了该检查器,那么SimpleStreamChecker::checkDeadSymbols()
函数在每个符号被回收时都会被调用。
clang::ento::check::PointerEscape
,表示分析器引擎无法有效地跟踪符号时就会调用检查器的程序点。这意味着,如果启用了该检查器,那么SimpleStreamChecker::checkPointerEscape()
函数在任意符号无法被跟踪时都会被调用。
要实现alpha.unix.SimpleStream
检查器,需要获取fopen
函数的返回值(即新打开的文件描述符),从而用于跟踪文件描述符的打开情况。而该信息可以在回调函数checkPostCall()
中获取,但不能在回调函数checkPreCall()
中获取。因此,需要订阅clang::ento::check::PostCall
程序点。
另外,也需要获取fclose
函数的参数(即要关闭的文件描述符),从而用于跟踪文件描述符的关闭情况。该信息既可以在回调函数checkPostCall()
中获取,也可以在回调函数checkPreCall()
中获取。Clang 静态分析器在实现alpha.unix.SimpleStream
检查器时选择了后者。这样做,虽然逻辑更清晰,但不是必须的。因此,订阅了clang::ento::check::PreCall
程序点。
需要注意的是, 虽然程序点clang::ento::check::PreStmt<CallExpr>
和clang::ento::check::PreCall
的被调用时机是相同的,程序点clang::ento::check::PostStmt<CallExpr>
和clang::ento::check::PostCall
的被调用时机也是相同的,但实际上 Clang 静态分析器在实现alpha.unix.SimpleStream
检查器时订阅的是后者而不是前者。这样做,是因为通过后者回调函数中的CallEvent
参数可以更容易地获取被调用函数的相关信息,比如:函数返回值、函数参数列表等。
订阅clang::ento::check::DeadSymbols
程序点出于以下两个目的:
清除自定义的程序状态。如果一个符号在被回收时,当前程序状态的GMD
(Generic Data Map)中保存了该符号的相关信息,那么此时应该清除这些不会再用到的信息。
用于判断是否发生了资源泄露。如果一个符号在被回收时,该符号所对应的资源尚未释放,那么表明发生了资源泄露。
订阅clang::ento::check::PointerEscape
程序点是为了能够在分析器引擎无法有效地跟踪文件描述符时做一些处理,从而避免在某些情况下误报资源泄露
的程序缺陷。这些情况比如:将文件描述符赋值给全局变量、分析器引擎无法对其建模的函数等。
step 1: 定义检查器类
1) 需要订阅哪些程序点
要实现检测重复释放
(即对同一个资源重复调用fclose
函数)的功能,我们至少需要订阅clang::ento::check::PostCall
程序点。除此之外,最好也订阅clang::ento::check::DeadSymbols
程序点(但不是必须的)。
2) 定义检查器类
using namespace clang;
using namespace ento;
namespace {
class SimpleStreamChecker : public Checker<check::PostCall,
check::DeadSymbols> {
public:
void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
void checkDeadSymbols(SymbolReaper &SR, CheckerContext &C) const;
};
} // anonymous namespace
注:
如果确定仅在局部范围内使用该类,那么可以将检查器类的定义置于匿名空间中。从而,避免链接器将这些符号导出至外部。
订阅clang::ento::check::PostCall
程序点后,检查器类必须实现一个函数原型为void checkPostCall(const CallEvent &Call, CheckerContext &C) const
的成员函数。否则,会发生编译错误。
同样地,订阅clang::ento::check::DeadSymbols
程序点后,检查器类必须实现一个函数原型为void checkDeadSymbols(SymbolReaper &SR, CheckerContext &C) const
的成员函数。否则,也会发生编译错误。
3) 依赖的头文件
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
#include "clang/StaticAnalyzer/Core/Checker.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
#include "clang/StaticAnalyzer/Frontend/CheckerRegistry.h"
注:
以插件方式扩展时(即所实现的检查器是一个独立的共享库),需要包含头文件clang/StaticAnalyzer/Frontend/CheckerRegistry.h
。
而直接以源码方式扩展时(即所实现的检查器被集成到 Clang 中),需要包含头文件clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h
。
step 2: 自定义程序状态
Clang 静态分析器允许检查器自定义程序状态,从而跟踪检查器自身特定的信息。Clang 静态分析器通过提供以下宏定义来做到这一点(定义在 clang/include/clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h 文件中):
REGISTER_TRAIT_WITH_PROGRAMSTATE(Name, Type)
REGISTER_MAP_WITH_PROGRAMSTATE(Name, Key, Value)
REGISTER_MAP_FACTORY_WITH_PROGRAMSTATE(Name, Key, Value)
REGISTER_SET_WITH_PROGRAMSTATE(Name, Elem)
REGISTER_SET_FACTORY_WITH_PROGRAMSTATE(Name, Elem)
REGISTER_LIST_WITH_PROGRAMSTATE(Name, Elem)
REGISTER_LIST_FACTORY_WITH_PROGRAMSTATE(Name, Elem)
上述宏定义将这些检查器自身特定的信息保存到程序状态的GDM
(Generic Data Map)中而不是检查器类的内部。这样做,是因为如果保存到检查器类中,那么这些信息对于所有的执行路径来说都是相同的,而这一点显然是不合理的。
需要注意的是, 在调用上述宏定义时,不能将其置于任何命名空间内,并且末尾不可以带分号。否则,会发生编译错误。
要实现检测重复释放
的功能,我们需要跟踪同一个文件描述符的打开和关闭情况。宏定义REGISTER_MAP_WITH_PROGRAMSTATE(Name, Key, Value)
适合于该场景。
1) 定义状态枚举
namespace {
enum Kind {
Opened,
Closed,
};
} // anonymous namespace
其中,枚举值Opened
表示文件描述符处于已打开
的状态,枚举值Closed
表示文件描述符处于已关闭
的状态。
2) 调用注册宏
REGISTER_MAP_WITH_PROGRAMSTATE(SimpleStreamMap, SymbolRef, int)
在调用上述代码后,宏内部会定义一个数据类型为SimpleStreamMap
的类,用于表示自定义程序状态,以及定义一个实际数据类型为llvm::ImmutableMap<SymbolRef, int>
的using
声明——SimpleStreamMapTy
,并提供一组操作该自定义程序状态的函数等。
参数SimpleStreamMap
,表示自定义程序状态的类型(实质上是一个 C++ 类名,在该宏定义的内部进行定义)。
参数SymbolRef
,为llvm::ImmutableMap<Key, Value>
中Key
的数据类型,表示文件描述符所对应的符号表达式。
参数int
,为llvm::ImmutableMap<Key, Value>
中Value
的数据类型,表示文件描述符的状态(是已打开
还是已关闭
)。
3) 如何设置自定义程序状态(示例)
SymbolRef FileDesc = /*...*/;
ProgramStateRef State = /*...*/;
State = State->set<SimpleStreamMap>(FileDesc, Opened);
上述代码的逻辑为:返回一个新的程序状态。这个新的程序状态是基于程序状态State
(位于等号右侧)创建的,并添加一个Key
值为FileDesc
,Value
值为Opened
的元素到自定义程序状态SimpleStreamMap
中。
需要注意的是, 由于ProgramState
的不可修改性,等号两端的State
指向两个不同的ProgramState
对象。
4) 如何获取自定义程序状态(示例)
SymbolRef FileDesc = /*...*/;
ProgramStateRef State = /*...*/;
const auto K = State->get<SimpleStreamMap>(FileDesc);
上述代码的逻辑为:从程序状态State
的GDM
中查询自定义程序状态SimpleStreamMap
中的元素,要查询元素的Key
值为FileDesc
。如果成功,返回指向该元素的Value
值的指针;否则,返回NULL
。
step 3: 实现检查器类
1) 实现checkPostCall()
成员函数
a) 判断是否为 C 语言风格的全局函数
void SimpleStreamChecker::checkPostCall(const CallEvent &Call,
CheckerContext &C) const {
if (!Call.isGlobalCFunction()) {
return;
}
// 省略 ...
}
上述代码的逻辑为:如果被调用的函数不是 C 语言风格的全局函数,那么直接返回。从而,避免不必要的后续处理。
b) 实现fopen
函数被调用的处理逻辑
void SimpleStreamChecker::checkPostCall(const CallEvent &Call,
CheckerContext &C) const {
// 省略 ...
const IdentifierInfo *II = Call.getCalleeIdentifier();
if (!II) {
return;
}
if (II->isStr("fopen")) {
SymbolRef FileDesc = Call.getReturnValue().getAsSymbol();
if (!FileDesc) {
return;
}
ProgramStateRef State = C.getState();
State = State->set<SimpleStreamMap>(FileDesc, Opened);
C.addTransition(State);
return;
}
// 省略 ...
}
上述代码的逻辑为:如果被调用的函数是fopen
,那么在自定义程序状态SimpleStreamMap
中添加一个元素。其Key
值为文件描述符所对应的符号表达式(这里是FileDesc
),Value
值为表示文件描述符已打开
的枚举值。
需要注意的是, 这里用于判断被调用函数是否为fopen
的方法是低效的。因为clang::IdentifierInfo::isStr()
函数的实现是通过比较字符串。
c) 实现fclose
函数被调用的处理逻辑
void SimpleStreamChecker::checkPostCall(const CallEvent &Call,
CheckerContext &C) const {
// 省略 ...
if (II->isStr("fclose")) {
SymbolRef FileDesc = Call.getArgSVal(0).getAsSymbol();
if (!FileDesc) {
return;
}
ProgramStateRef State = C.getState();
if (const auto K = State->get<SimpleStreamMap>(FileDesc)) {
if (*K == Opened) {
State = State->set<SimpleStreamMap>(FileDesc, Closed);
C.addTransition(State);
}
else if (*K == Closed) {
reportDoubleClose(C);
}
}
return;
}
}
上述代码的逻辑为:如果被调用的函数是fclose
,那么从自定义程序状态SimpleStreamMap
中查询元素。要查询元素的Key
值为刚关闭的文件描述符所对应的符号表达式。如果查询成功并且其文件描述符状态为已打开
,那么将其设置为已关闭
,并扩展可达程序图,从而让后继节点获知这一信息。如果文件描述符的状态为已关闭
,那么表明发生了重复释放
,并报告该程序缺陷。
2) 报告程序缺陷
a) 定义相关数据成员和成员函数
namespace {
class SimpleStreamChecker : public Checker<check::PostCall> {
mutable std::unique_ptr<BugType> DoubleCloseBT;
void reportDoubleClose(CheckerContext &C) const;
// 省略 ...
};
} // anonymous namespace
我们需要定义一个数据类型为std::unique_ptr<BugType>
的数据成员(这里是DoubleCloseBT
),用于表示一个新的程序缺陷。为了在对象析构时能够自动释放内存,因此使用了智能指针。
另外,我们将报告该程序缺陷的处理逻辑封装到成员函数reportDoubleClose()
中。
b) 实现程序缺陷报告逻辑
void SimpleStreamChecker::reportDoubleClose(CheckerContext &C) const {
if (!DoubleCloseBT) {
DoubleCloseBT.reset(
new BugType(this, "Double fclose", "Unix Stream API Error"));
}
if (ExplodedNode *N = C.generateErrorNode()) {
auto R = std::make_unique<PathSensitiveBugReport>(
*DoubleCloseBT, DoubleCloseBT->getDescription(), N);
C.emitReport(std::move(R));
}
}
上述代码的逻辑为:报告重复释放
的程序缺陷,并终止该执行路径的探索。
3) 实现checkDeadSymbols()
成员函数
void SimpleStreamChecker::checkDeadSymbols(SymbolReaper &SR,
CheckerContext &C) const {
ProgramStateRef State = C.getState();
for (const auto &TrackedStream : State->get<SimpleStreamMap>()) {
SymbolRef Sym = TrackedStream.first;
if (SR.isDead(Sym)) {
State = State->remove<SimpleStreamMap>(Sym);
}
}
C.addTransition(State);
}
上述代码的逻辑为:首先,遍历当前程序状态State
中自定义程序状态SimpleStreamMap
的所有元素。如果一个元素中文件描述符所对应的符号表达式要被回收了,那么返回一个新的程序状态(变量名称也是State
),并且该新程序状态的GDM
中已经移除了该元素。最后,如果程序状态发生了变化,那么扩展程序状态图,从而让后继节点获知这一信息,并且可以继续探索该执行路径。
step 4: 构建测试
1) 构建
注:具体过程可参考笔者的另一篇文章:《LLVM 之 Clang 静态分析器篇(2):如何扩展 Clang 静态分析器》。
2) 运行
执行如下命令运行plugin.alpha.unix.SimpleStream
检查器(示例):
$ clang -cc1 -w -load ~/git-projects/llvm-project/build_ninja/lib/SimpleStreamCheckerPlugin.so -analyze -analyzer-checker=plugin.alpha.unix.SimpleStream simple-stream-checks.c -I ~/git-projects/llvm-project/clang/test/Analysis/
输出结果如下:
simple-stream-checks.c:13:5: warning: Double fclose [plugin.alpha.unix.SimpleStream]
fclose(F); // expected-warning {{Closing a previously closed file stream}}
^~~~~~~~~
1 warning generated.
simple-stream-checks.c
是 LLVM 项目中自带的测试文件,其中重复释放
的程序缺陷仅有一处,即 simple-stream-checks.c:13 行。也就是说,我们上述实现的检查器能够检测出该程序缺陷。
step 1: 需要订阅哪些程序点
要实现检测资源泄露
(即对同一个资源在调用了fopen
函数之后未再调用fclose
函数)的功能,我们需要订阅clang::ento::check::DeadSymbols
和clang::ento::check::PointerEscape
程序点。
因此,修改后的检查器类定义如下:
namespace {
// 省略 ...
class SimpleStreamChecker : public Checker<check::PostCall,
check::DeadSymbols,
check::PointerEscape> {
// 省略 ...
};
} // anonymous namespace
同样地,我们需要实现一个函数原型如下的成员函数:
ProgramStateRef checkPointerEscape(ProgramStateRef State,
const InvalidatedSymbols &Escaped,
const CallEvent *Call,
PointerEscapeKind Kind) const;
step 2: 实现检查器类
1) 修改checkDeadSymbols()
成员函数
如果一个文件描述符发生了资源泄露
,那么一定同时满足以下 3 个条件:
分析器引擎要对文件描述符所对应的符号进行回收了
符号被回收时文件描述符正处于已打开
状态
要被回收的符号的值不是NULL
需要注意的是, 上述条件是必要不充分条件。也就是说,如果同时满足了以上 3 个条件,那么不一定发生了资源泄露
。
修改后的checkDeadSymbols()
源码实现如下:
void SimpleStreamChecker::checkDeadSymbols(SymbolReaper &SR,
CheckerContext &C) const {
auto isNotNull = [&](ProgramStateRef State, SymbolRef Sym) {
// Return true if a symbol is not NULL.
return !State->getConstraintManager().isNull(State, Sym).isConstrainedTrue();
};
SymbolVector LeakedStreams;
ProgramStateRef State = C.getState();
for (const auto &TrackedStream : State->get<SimpleStreamMap>()) {
SymbolRef Sym = TrackedStream.first;
if (SR.isLive(Sym)) {
continue;
}
if (TrackedStream.second == Opened && isNotNull(State, Sym)) {
LeakedStreams.push_back(Sym);
}
State = State->remove<SimpleStreamMap>(Sym);
}
if (LeakedStreams.empty()) {
C.addTransition(State);
}
else {
reportLeaks(LeakedStreams, C, State);
}
}
上述代码的逻辑为:首先,遍历当前程序状态State
中自定义程序状态SimpleStreamMap
的所有元素。如果一个元素中文件描述符所对应的符号表达式要被回收了,那么依次执行如下两个操作:
如果符号被回收时文件描述符正处于已打开
状态并且该符号的值不是NULL
,那么表明发生了资源泄露
,并将该符号保存到一个向量中(这里是LeakedStreams
对象)。
创建一个新的程序状态(变量名称也是State
),并且该新程序状态的GDM
中已经移除了该元素。
在上述遍历结束后,判断是否发生了资源泄露
。如果有,则报告这些程序缺陷;否则,如果程序状态发生了变化,那么扩展程序状态图,从而让后继节点获知这一信息,并且可以继续探索该执行路径。
需要注意的是, 由于即使程序发生了资源泄露
仍可以继续执行。因此,用于报告资源泄露
程序缺陷的reportLeaks()
函数必须以最新的程序状态State
而不是当前程序状态(C.getState()
)来创建节点,从而让后继节点获知最新的程序状态。如果不这样做,会产生误报。
2) 报告程序缺陷
a) 定义相关数据成员和成员函数
namespace {
class SimpleStreamChecker : public Checker<check::PostCall> {
// 省略 ...
mutable std::unique_ptr<BugType> LeakBT;
// 省略 ...
void reportLeaks(ArrayRef<SymbolRef> LeakedStreams, CheckerContext &C,
ProgramStateRef State) const;
// 省略 ...
};
} // anonymous namespace
同样地,我们需要定义一个数据类型为std::unique_ptr<BugType>
的数据成员(这里是LeakBT
),用于表示一个新的程序缺陷。
另外,我们将报告该程序缺陷的处理逻辑封装到成员函数reportLeaks()
中。
b) 实现程序缺陷报告逻辑
void SimpleStreamChecker::reportLeaks(ArrayRef<SymbolRef> LeakedStreams,
CheckerContext &C,
ProgramStateRef State) const {
ExplodedNode *N = C.generateNonFatalErrorNode(State);
if (!N) {
return;
}
if (!LeakBT) {
LeakBT.reset(new BugType(this, "Resource Leak", "Unix Stream API Error",
/*SuppressOnSink=*/true));
}
for (const auto &LeakedStream : LeakedStreams) {
(void)LeakedStream;
auto R = std::make_unique<PathSensitiveBugReport>(
*LeakBT, LeakBT->getDescription(), N);
C.emitReport(std::move(R));
}
}
上述代码的逻辑为:为了在检测到资源泄露
后分析器引擎仍可以继续探索该执行路径。因此,使用clang::ento::CheckerContext::generateNonFatalErrorNode()
函数创建节点。如果创建成功,则报告所有的资源泄露
程序缺陷。
需要注意的是, 创建数据成员LeakBT
时,其构造函数的最后一个参数bool SuppressOnSink
的值设置为true
。这样做,是因为调用assert()
或exit()
函数会导致程序退出,在这种情况下,如果尚未关闭文件描述符,那么不认为发生了资源泄露
。如果不这样做,会产生误报。
3) 实现checkPointerEscape()
成员函数
ProgramStateRef
SimpleStreamChecker::checkPointerEscape(ProgramStateRef State,
const InvalidatedSymbols &Escaped,
const CallEvent *Call,
PointerEscapeKind Kind) const {
if (Kind == PSK_DirectEscapeOnCall && Call->isInSystemHeader()) {
return State;
}
for (SymbolRef Sym : Escaped) {
State = State->remove<SimpleStreamMap>(Sym);
}
return State;
}
上述代码的逻辑为:如果文件描述符作为实参传递给了某个函数并且该函数来自于系统文件(即调用fclose()
的情况),那么直接返回;否则,返回一个新的程序状态,并且该程序状态已经清除了所有无法跟踪的文件描述符。
step 3: 诊断结果对比
对于同一个测试文件simple-stream-checks.c
,分别运行自己实现的检查器plugin.alpha.unix.SimpleStream
和 Clang 静态分析器实现的检查器alpha.unix.SimpleStream
。
1) 运行检查器plugin.alpha.unix.SimpleStream
执行命令:
$ clang -cc1 -w -load ~/git-projects/llvm-project/build_ninja/lib/SimpleStreamCheckerPlugin.so -analyze -analyzer-checker=plugin.alpha.unix.SimpleStream simple-stream-checks.c -I ~/git-projects/llvm-project/clang/test/Analysis/
输出结果如下:
simple-stream-checks.c:13:5: warning: Double fclose [plugin.alpha.unix.SimpleStream]
fclose(F); // expected-warning {{Closing a previously closed file stream}}
^~~~~~~~~
simple-stream-checks.c:23:7: warning: Resource Leak [plugin.alpha.unix.SimpleStream]
if (Data) // expected-warning {{Opened file is never closed; potential resource leak}}
^~~~
simple-stream-checks.c:48:1: warning: Resource Leak [plugin.alpha.unix.SimpleStream]
} // expected-warning {{Opened file is never closed; potential resource leak}}
^
simple-stream-checks.c:52:3: warning: Resource Leak [plugin.alpha.unix.SimpleStream]
return; // expected-warning {{Opened file is never closed; potential resource leak}}
^~~~~~
simple-stream-checks.c:77:3: warning: Resource Leak [plugin.alpha.unix.SimpleStream]
return; // expected-warning {{Opened file is never closed; potential resource leak}}
^~~~~~
simple-stream-checks.c:84:3: warning: Resource Leak [plugin.alpha.unix.SimpleStream]
return; // expected-warning {{Opened file is never closed; potential resource leak}}
^~~~~~
simple-stream-checks.c:96:1: warning: Resource Leak [plugin.alpha.unix.SimpleStream]
} // expected-warning {{Opened file is never closed; potential resource leak}}
^
7 warnings generated.
2) 运行检查器alpha.unix.SimpleStream
执行命令:
$ clang -cc1 -w -analyze -analyzer-checker=alpha.unix.SimpleStream simple-stream-checks.c -I ~/git-projects/llvm-project/clang/test/Analysis/
输出结果如下:
simple-stream-checks.c:13:5: warning: Closing a previously closed file stream [alpha.unix.SimpleStream]
fclose(F); // expected-warning {{Closing a previously closed file stream}}
^~~~~~~~~
simple-stream-checks.c:23:7: warning: Opened file is never closed; potential resource leak [alpha.unix.SimpleStream]
if (Data) // expected-warning {{Opened file is never closed; potential resource leak}}
^~~~
simple-stream-checks.c:48:1: warning: Opened file is never closed; potential resource leak [alpha.unix.SimpleStream]
} // expected-warning {{Opened file is never closed; potential resource leak}}
^
simple-stream-checks.c:52:3: warning: Opened file is never closed; potential resource leak [alpha.unix.SimpleStream]
return; // expected-warning {{Opened file is never closed; potential resource leak}}
^~~~~~
simple-stream-checks.c:77:3: warning: Opened file is never closed; potential resource leak [alpha.unix.SimpleStream]
return; // expected-warning {{Opened file is never closed; potential resource leak}}
^~~~~~
simple-stream-checks.c:84:3: warning: Opened file is never closed; potential resource leak [alpha.unix.SimpleStream]
return; // expected-warning {{Opened file is never closed; potential resource leak}}
^~~~~~
simple-stream-checks.c:96:1: warning: Opened file is never closed; potential resource leak [alpha.unix.SimpleStream]
} // expected-warning {{Opened file is never closed; potential resource leak}}
^
7 warnings generated.
从上面的输出结果可以看出,对于同一个测试文件simple-stream-checks.c
而言,检查器plugin.alpha.unix.SimpleStream
的诊断效果与检查器alpha.unix.SimpleStream
是相同的。
要实现检测重复释放
(即对同一个资源重复调用fclose
函数)和资源泄露
(即对同一个资源在调用了fopen
函数之后未再调用fclose
函数)的功能,我们需要跟踪文件描述符的打开和关闭情况。通过自定义程序状态可以做到这一点。
检测重复释放
的工作原理:当程序调用fopen
函数后,将返回的文件描述符和该文件描述符已打开
的信息保存到自定义程序状态中。当程序调用fclose
函数时,如果要关闭的文件描述符处于已打开
的状态,那么将其更新为已关闭
状态。如果要关闭的文件描述符处于已关闭
状态,那么表明发生了重复释放
。
检测资源泄露
的工作原理:如果在分析器引擎要回收文件描述符所对应的符号时,文件描述符的状态处于已打开
状态并且其值不为NULL
,那么表明发生了资源泄露
。
为了避免误报资源泄露
,需要在分析器引擎无法跟踪文件描述符时清除相应的自定义程序状态。除此之外,如果程序在调用assert()
或exit()
函数时尚未关闭文件描述符,那么不认为发生了资源泄露
。
下一篇:LLVM 之 Clang 静态分析器篇(6):程序缺陷诊断——内存重复释放