clang 19.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 const CallDescription OpenFn{CDM::CLibrary, {"fopen"}, 2};
56 const CallDescription CloseFn{CDM::CLibrary, {"fclose"}, 1};
57
58 const BugType DoubleCloseBugType{this, "Double fclose",
59 "Unix Stream API Error"};
60 const BugType LeakBugType{this, "Resource Leak", "Unix Stream API Error",
61 /*SuppressOnSink=*/true};
62
63 void reportDoubleClose(SymbolRef FileDescSym,
64 const CallEvent &Call,
65 CheckerContext &C) const;
66
67 void reportLeaks(ArrayRef<SymbolRef> LeakedStreams, CheckerContext &C,
68 ExplodedNode *ErrNode) const;
69
70 bool guaranteedNotToCloseFile(const CallEvent &Call) const;
71
72public:
73 /// Process fopen.
74 void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
75 /// Process fclose.
76 void checkPreCall(const CallEvent &Call, CheckerContext &C) const;
77
78 void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const;
79
80 /// Stop tracking addresses which escape.
81 ProgramStateRef checkPointerEscape(ProgramStateRef State,
82 const InvalidatedSymbols &Escaped,
83 const CallEvent *Call,
84 PointerEscapeKind Kind) const;
85};
86
87} // end anonymous namespace
88
89/// The state of the checker is a map from tracked stream symbols to their
90/// state. Let's store it in the ProgramState.
91REGISTER_MAP_WITH_PROGRAMSTATE(StreamMap, SymbolRef, StreamState)
92
93void SimpleStreamChecker::checkPostCall(const CallEvent &Call,
94 CheckerContext &C) const {
95 if (!OpenFn.matches(Call))
96 return;
97
98 // Get the symbolic value corresponding to the file handle.
99 SymbolRef FileDesc = Call.getReturnValue().getAsSymbol();
100 if (!FileDesc)
101 return;
102
103 // Generate the next transition (an edge in the exploded graph).
104 ProgramStateRef State = C.getState();
105 State = State->set<StreamMap>(FileDesc, StreamState::getOpened());
106 C.addTransition(State);
107}
108
109void SimpleStreamChecker::checkPreCall(const CallEvent &Call,
110 CheckerContext &C) const {
111 if (!CloseFn.matches(Call))
112 return;
113
114 // Get the symbolic value corresponding to the file handle.
115 SymbolRef FileDesc = Call.getArgSVal(0).getAsSymbol();
116 if (!FileDesc)
117 return;
118
119 // Check if the stream has already been closed.
120 ProgramStateRef State = C.getState();
121 const StreamState *SS = State->get<StreamMap>(FileDesc);
122 if (SS && SS->isClosed()) {
123 reportDoubleClose(FileDesc, Call, C);
124 return;
125 }
126
127 // Generate the next transition, in which the stream is closed.
128 State = State->set<StreamMap>(FileDesc, StreamState::getClosed());
129 C.addTransition(State);
130}
131
132static bool isLeaked(SymbolRef Sym, const StreamState &SS,
133 bool IsSymDead, ProgramStateRef State) {
134 if (IsSymDead && SS.isOpened()) {
135 // If a symbol is NULL, assume that fopen failed on this path.
136 // A symbol should only be considered leaked if it is non-null.
137 ConstraintManager &CMgr = State->getConstraintManager();
138 ConditionTruthVal OpenFailed = CMgr.isNull(State, Sym);
139 return !OpenFailed.isConstrainedTrue();
140 }
141 return false;
142}
143
144void SimpleStreamChecker::checkDeadSymbols(SymbolReaper &SymReaper,
145 CheckerContext &C) const {
146 ProgramStateRef State = C.getState();
147 SymbolVector LeakedStreams;
148 StreamMapTy TrackedStreams = State->get<StreamMap>();
149 for (auto [Sym, StreamStatus] : TrackedStreams) {
150 bool IsSymDead = SymReaper.isDead(Sym);
151
152 // Collect leaked symbols.
153 if (isLeaked(Sym, StreamStatus, IsSymDead, State))
154 LeakedStreams.push_back(Sym);
155
156 // Remove the dead symbol from the streams map.
157 if (IsSymDead)
158 State = State->remove<StreamMap>(Sym);
159 }
160
161 ExplodedNode *N = C.generateNonFatalErrorNode(State);
162 if (!N)
163 return;
164 reportLeaks(LeakedStreams, C, N);
165}
166
167void SimpleStreamChecker::reportDoubleClose(SymbolRef FileDescSym,
168 const CallEvent &Call,
169 CheckerContext &C) const {
170 // We reached a bug, stop exploring the path here by generating a sink.
171 ExplodedNode *ErrNode = C.generateErrorNode();
172 // If we've already reached this node on another path, return.
173 if (!ErrNode)
174 return;
175
176 // Generate the report.
177 auto R = std::make_unique<PathSensitiveBugReport>(
178 DoubleCloseBugType, "Closing a previously closed file stream", ErrNode);
179 R->addRange(Call.getSourceRange());
180 R->markInteresting(FileDescSym);
181 C.emitReport(std::move(R));
182}
183
184void SimpleStreamChecker::reportLeaks(ArrayRef<SymbolRef> LeakedStreams,
186 ExplodedNode *ErrNode) const {
187 // Attach bug reports to the leak node.
188 // TODO: Identify the leaked file descriptor.
189 for (SymbolRef LeakedStream : LeakedStreams) {
190 auto R = std::make_unique<PathSensitiveBugReport>(
191 LeakBugType, "Opened file is never closed; potential resource leak",
192 ErrNode);
193 R->markInteresting(LeakedStream);
194 C.emitReport(std::move(R));
195 }
196}
197
198bool SimpleStreamChecker::guaranteedNotToCloseFile(const CallEvent &Call) const{
199 // If it's not in a system header, assume it might close a file.
200 if (!Call.isInSystemHeader())
201 return false;
202
203 // Handle cases where we know a buffer's /address/ can escape.
204 if (Call.argumentsMayEscape())
205 return false;
206
207 // Note, even though fclose closes the file, we do not list it here
208 // since the checker is modeling the call.
209
210 return true;
211}
212
213// If the pointer we are tracking escaped, do not track the symbol as
214// we cannot reason about it anymore.
216SimpleStreamChecker::checkPointerEscape(ProgramStateRef State,
217 const InvalidatedSymbols &Escaped,
218 const CallEvent *Call,
219 PointerEscapeKind Kind) const {
220 // If we know that the call cannot close a file, there is nothing to do.
221 if (Kind == PSK_DirectEscapeOnCall && guaranteedNotToCloseFile(*Call)) {
222 return State;
223 }
224
225 for (SymbolRef Sym : Escaped) {
226 // The symbol escaped. Optimistically, assume that the corresponding file
227 // handle will be closed somewhere else.
228 State = State->remove<StreamMap>(Sym);
229 }
230 return State;
231}
232
233void ento::registerSimpleStreamChecker(CheckerManager &mgr) {
234 mgr.registerChecker<SimpleStreamChecker>();
235}
236
237// This checker should be enabled regardless of how language options are set.
238bool ento::shouldRegisterSimpleStreamChecker(const CheckerManager &mgr) {
239 return true;
240}
#define X(type, name)
Definition: Value.h:143
#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)
A CallDescription is a pattern that can be used to match calls based on the qualified name and the ar...
Represents an abstract call to a function or method along a particular path.
Definition: CallEvent.h:153
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.
The JSON file list parser is used to communicate input to InstallAPI.
bool operator==(const CallGraphNode::CallRecord &LHS, const CallGraphNode::CallRecord &RHS)
Definition: CallGraph.h:207