clang 18.0.0git
SimpleStreamChecker.cpp
Go to the documentation of this file.
1//===-- SimpleStreamChecker.cpp -----------------------------------------*- C++ -*--//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// Defines a checker for proper use of fopen/fclose APIs.
10// - If a file has been closed with fclose, it should not be accessed again.
11// Accessing a closed file results in undefined behavior.
12// - If a file was opened with fopen, it must be closed with fclose before
13// the execution ends. Failing to do so results in a resource leak.
14//
15//===----------------------------------------------------------------------===//
16
23#include <utility>
24
25using namespace clang;
26using namespace ento;
27
28namespace {
29typedef SmallVector<SymbolRef, 2> SymbolVector;
30
31struct StreamState {
32private:
33 enum Kind { Opened, Closed } K;
34 StreamState(Kind InK) : K(InK) { }
35
36public:
37 bool isOpened() const { return K == Opened; }
38 bool isClosed() const { return K == Closed; }
39
40 static StreamState getOpened() { return StreamState(Opened); }
41 static StreamState getClosed() { return StreamState(Closed); }
42
43 bool operator==(const StreamState &X) const {
44 return K == X.K;
45 }
46 void Profile(llvm::FoldingSetNodeID &ID) const {
47 ID.AddInteger(K);
48 }
49};
50
51class SimpleStreamChecker : public Checker<check::PostCall,
52 check::PreCall,
53 check::DeadSymbols,
54 check::PointerEscape> {
55 CallDescription OpenFn, CloseFn;
56
57 std::unique_ptr<BugType> DoubleCloseBugType;
58 std::unique_ptr<BugType> LeakBugType;
59
60 void reportDoubleClose(SymbolRef FileDescSym,
61 const CallEvent &Call,
62 CheckerContext &C) const;
63
64 void reportLeaks(ArrayRef<SymbolRef> LeakedStreams, CheckerContext &C,
65 ExplodedNode *ErrNode) const;
66
67 bool guaranteedNotToCloseFile(const CallEvent &Call) const;
68
69public:
70 SimpleStreamChecker();
71
72 /// Process fopen.
73 void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
74 /// Process fclose.
75 void checkPreCall(const CallEvent &Call, CheckerContext &C) const;
76
77 void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const;
78
79 /// Stop tracking addresses which escape.
80 ProgramStateRef checkPointerEscape(ProgramStateRef State,
81 const InvalidatedSymbols &Escaped,
82 const CallEvent *Call,
83 PointerEscapeKind Kind) const;
84};
85
86} // end anonymous namespace
87
88/// The state of the checker is a map from tracked stream symbols to their
89/// state. Let's store it in the ProgramState.
90REGISTER_MAP_WITH_PROGRAMSTATE(StreamMap, SymbolRef, StreamState)
91
92SimpleStreamChecker::SimpleStreamChecker()
93 : OpenFn({"fopen"}, 2), CloseFn({"fclose"}, 1) {
94 // Initialize the bug types.
95 DoubleCloseBugType.reset(
96 new BugType(this, "Double fclose", "Unix Stream API Error"));
97
98 // Sinks are higher importance bugs as well as calls to assert() or exit(0).
99 LeakBugType.reset(
100 new BugType(this, "Resource Leak", "Unix Stream API Error",
101 /*SuppressOnSink=*/true));
102}
103
104void SimpleStreamChecker::checkPostCall(const CallEvent &Call,
105 CheckerContext &C) const {
106 if (!Call.isGlobalCFunction())
107 return;
108
109 if (!OpenFn.matches(Call))
110 return;
111
112 // Get the symbolic value corresponding to the file handle.
113 SymbolRef FileDesc = Call.getReturnValue().getAsSymbol();
114 if (!FileDesc)
115 return;
116
117 // Generate the next transition (an edge in the exploded graph).
118 ProgramStateRef State = C.getState();
119 State = State->set<StreamMap>(FileDesc, StreamState::getOpened());
120 C.addTransition(State);
121}
122
123void SimpleStreamChecker::checkPreCall(const CallEvent &Call,
124 CheckerContext &C) const {
125 if (!Call.isGlobalCFunction())
126 return;
127
128 if (!CloseFn.matches(Call))
129 return;
130
131 // Get the symbolic value corresponding to the file handle.
132 SymbolRef FileDesc = Call.getArgSVal(0).getAsSymbol();
133 if (!FileDesc)
134 return;
135
136 // Check if the stream has already been closed.
137 ProgramStateRef State = C.getState();
138 const StreamState *SS = State->get<StreamMap>(FileDesc);
139 if (SS && SS->isClosed()) {
140 reportDoubleClose(FileDesc, Call, C);
141 return;
142 }
143
144 // Generate the next transition, in which the stream is closed.
145 State = State->set<StreamMap>(FileDesc, StreamState::getClosed());
146 C.addTransition(State);
147}
148
149static bool isLeaked(SymbolRef Sym, const StreamState &SS,
150 bool IsSymDead, ProgramStateRef State) {
151 if (IsSymDead && SS.isOpened()) {
152 // If a symbol is NULL, assume that fopen failed on this path.
153 // A symbol should only be considered leaked if it is non-null.
154 ConstraintManager &CMgr = State->getConstraintManager();
155 ConditionTruthVal OpenFailed = CMgr.isNull(State, Sym);
156 return !OpenFailed.isConstrainedTrue();
157 }
158 return false;
159}
160
161void SimpleStreamChecker::checkDeadSymbols(SymbolReaper &SymReaper,
162 CheckerContext &C) const {
163 ProgramStateRef State = C.getState();
164 SymbolVector LeakedStreams;
165 StreamMapTy TrackedStreams = State->get<StreamMap>();
166 for (auto [Sym, StreamStatus] : TrackedStreams) {
167 bool IsSymDead = SymReaper.isDead(Sym);
168
169 // Collect leaked symbols.
170 if (isLeaked(Sym, StreamStatus, IsSymDead, State))
171 LeakedStreams.push_back(Sym);
172
173 // Remove the dead symbol from the streams map.
174 if (IsSymDead)
175 State = State->remove<StreamMap>(Sym);
176 }
177
178 ExplodedNode *N = C.generateNonFatalErrorNode(State);
179 if (!N)
180 return;
181 reportLeaks(LeakedStreams, C, N);
182}
183
184void SimpleStreamChecker::reportDoubleClose(SymbolRef FileDescSym,
185 const CallEvent &Call,
186 CheckerContext &C) const {
187 // We reached a bug, stop exploring the path here by generating a sink.
188 ExplodedNode *ErrNode = C.generateErrorNode();
189 // If we've already reached this node on another path, return.
190 if (!ErrNode)
191 return;
192
193 // Generate the report.
194 auto R = std::make_unique<PathSensitiveBugReport>(
195 *DoubleCloseBugType, "Closing a previously closed file stream", ErrNode);
196 R->addRange(Call.getSourceRange());
197 R->markInteresting(FileDescSym);
198 C.emitReport(std::move(R));
199}
200
201void SimpleStreamChecker::reportLeaks(ArrayRef<SymbolRef> LeakedStreams,
203 ExplodedNode *ErrNode) const {
204 // Attach bug reports to the leak node.
205 // TODO: Identify the leaked file descriptor.
206 for (SymbolRef LeakedStream : LeakedStreams) {
207 auto R = std::make_unique<PathSensitiveBugReport>(
208 *LeakBugType, "Opened file is never closed; potential resource leak",
209 ErrNode);
210 R->markInteresting(LeakedStream);
211 C.emitReport(std::move(R));
212 }
213}
214
215bool SimpleStreamChecker::guaranteedNotToCloseFile(const CallEvent &Call) const{
216 // If it's not in a system header, assume it might close a file.
217 if (!Call.isInSystemHeader())
218 return false;
219
220 // Handle cases where we know a buffer's /address/ can escape.
221 if (Call.argumentsMayEscape())
222 return false;
223
224 // Note, even though fclose closes the file, we do not list it here
225 // since the checker is modeling the call.
226
227 return true;
228}
229
230// If the pointer we are tracking escaped, do not track the symbol as
231// we cannot reason about it anymore.
233SimpleStreamChecker::checkPointerEscape(ProgramStateRef State,
234 const InvalidatedSymbols &Escaped,
235 const CallEvent *Call,
236 PointerEscapeKind Kind) const {
237 // If we know that the call cannot close a file, there is nothing to do.
238 if (Kind == PSK_DirectEscapeOnCall && guaranteedNotToCloseFile(*Call)) {
239 return State;
240 }
241
242 for (SymbolRef Sym : Escaped) {
243 // The symbol escaped. Optimistically, assume that the corresponding file
244 // handle will be closed somewhere else.
245 State = State->remove<StreamMap>(Sym);
246 }
247 return State;
248}
249
250void ento::registerSimpleStreamChecker(CheckerManager &mgr) {
251 mgr.registerChecker<SimpleStreamChecker>();
252}
253
254// This checker should be enabled regardless of how language options are set.
255bool ento::shouldRegisterSimpleStreamChecker(const CheckerManager &mgr) {
256 return true;
257}
#define X(type, name)
Definition: Value.h:142
#define REGISTER_MAP_WITH_PROGRAMSTATE(Name, Key, Value)
Declares an immutable map of type NameTy, suitable for placement into the ProgramState.
static bool isLeaked(SymbolRef Sym, const StreamState &SS, bool IsSymDead, ProgramStateRef State)
This class represents a description of a function call using the number of arguments and the name of ...
Represents an abstract call to a function or method along a particular path.
Definition: CallEvent.h:152
CHECKER * registerChecker(AT &&... Args)
Used to register checkers.
bool isConstrainedTrue() const
Return true if the constraint is perfectly constrained to 'true'.
ConditionTruthVal isNull(ProgramStateRef State, SymbolRef Sym)
Convenience method to query the state to see if a symbol is null or not null, or if neither assumptio...
Symbolic value.
Definition: SymExpr.h:30
A class responsible for cleaning up unused symbols.
bool isDead(SymbolRef sym)
Returns whether or not a symbol has been confirmed dead.
PointerEscapeKind
Describes the different reasons a pointer escapes during analysis.
@ PSK_DirectEscapeOnCall
The pointer has been passed to a function call directly.
bool operator==(const CallGraphNode::CallRecord &LHS, const CallGraphNode::CallRecord &RHS)
Definition: CallGraph.h:207