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
17 #include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
18 #include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
19 #include "clang/StaticAnalyzer/Core/Checker.h"
20 #include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h"
21 #include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
22 #include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
23 #include <utility>
24
25 using namespace clang;
26 using namespace ento;
27
28 namespace {
29 typedef SmallVector<SymbolRef, 2> SymbolVector;
30
31 struct StreamState {
32 private:
33 enum Kind { Opened, Closed } K;
StreamState__anon16717fcf0111::StreamState34 StreamState(Kind InK) : K(InK) { }
35
36 public:
isOpened__anon16717fcf0111::StreamState37 bool isOpened() const { return K == Opened; }
isClosed__anon16717fcf0111::StreamState38 bool isClosed() const { return K == Closed; }
39
getOpened__anon16717fcf0111::StreamState40 static StreamState getOpened() { return StreamState(Opened); }
getClosed__anon16717fcf0111::StreamState41 static StreamState getClosed() { return StreamState(Closed); }
42
operator ==__anon16717fcf0111::StreamState43 bool operator==(const StreamState &X) const {
44 return K == X.K;
45 }
Profile__anon16717fcf0111::StreamState46 void Profile(llvm::FoldingSetNodeID &ID) const {
47 ID.AddInteger(K);
48 }
49 };
50
51 class SimpleStreamChecker : public Checker<check::PostCall,
52 check::PreCall,
53 check::DeadSymbols,
54 check::PointerEscape> {
55 const CallDescription OpenFn{{"fopen"}, 2};
56 const CallDescription CloseFn{{"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
72 public:
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.
REGISTER_MAP_WITH_PROGRAMSTATE(StreamMap,SymbolRef,StreamState)91 REGISTER_MAP_WITH_PROGRAMSTATE(StreamMap, SymbolRef, StreamState)
92
93 void SimpleStreamChecker::checkPostCall(const CallEvent &Call,
94 CheckerContext &C) const {
95 if (!Call.isGlobalCFunction())
96 return;
97
98 if (!OpenFn.matches(Call))
99 return;
100
101 // Get the symbolic value corresponding to the file handle.
102 SymbolRef FileDesc = Call.getReturnValue().getAsSymbol();
103 if (!FileDesc)
104 return;
105
106 // Generate the next transition (an edge in the exploded graph).
107 ProgramStateRef State = C.getState();
108 State = State->set<StreamMap>(FileDesc, StreamState::getOpened());
109 C.addTransition(State);
110 }
111
checkPreCall(const CallEvent & Call,CheckerContext & C) const112 void SimpleStreamChecker::checkPreCall(const CallEvent &Call,
113 CheckerContext &C) const {
114 if (!Call.isGlobalCFunction())
115 return;
116
117 if (!CloseFn.matches(Call))
118 return;
119
120 // Get the symbolic value corresponding to the file handle.
121 SymbolRef FileDesc = Call.getArgSVal(0).getAsSymbol();
122 if (!FileDesc)
123 return;
124
125 // Check if the stream has already been closed.
126 ProgramStateRef State = C.getState();
127 const StreamState *SS = State->get<StreamMap>(FileDesc);
128 if (SS && SS->isClosed()) {
129 reportDoubleClose(FileDesc, Call, C);
130 return;
131 }
132
133 // Generate the next transition, in which the stream is closed.
134 State = State->set<StreamMap>(FileDesc, StreamState::getClosed());
135 C.addTransition(State);
136 }
137
isLeaked(SymbolRef Sym,const StreamState & SS,bool IsSymDead,ProgramStateRef State)138 static bool isLeaked(SymbolRef Sym, const StreamState &SS,
139 bool IsSymDead, ProgramStateRef State) {
140 if (IsSymDead && SS.isOpened()) {
141 // If a symbol is NULL, assume that fopen failed on this path.
142 // A symbol should only be considered leaked if it is non-null.
143 ConstraintManager &CMgr = State->getConstraintManager();
144 ConditionTruthVal OpenFailed = CMgr.isNull(State, Sym);
145 return !OpenFailed.isConstrainedTrue();
146 }
147 return false;
148 }
149
checkDeadSymbols(SymbolReaper & SymReaper,CheckerContext & C) const150 void SimpleStreamChecker::checkDeadSymbols(SymbolReaper &SymReaper,
151 CheckerContext &C) const {
152 ProgramStateRef State = C.getState();
153 SymbolVector LeakedStreams;
154 StreamMapTy TrackedStreams = State->get<StreamMap>();
155 for (auto [Sym, StreamStatus] : TrackedStreams) {
156 bool IsSymDead = SymReaper.isDead(Sym);
157
158 // Collect leaked symbols.
159 if (isLeaked(Sym, StreamStatus, IsSymDead, State))
160 LeakedStreams.push_back(Sym);
161
162 // Remove the dead symbol from the streams map.
163 if (IsSymDead)
164 State = State->remove<StreamMap>(Sym);
165 }
166
167 ExplodedNode *N = C.generateNonFatalErrorNode(State);
168 if (!N)
169 return;
170 reportLeaks(LeakedStreams, C, N);
171 }
172
reportDoubleClose(SymbolRef FileDescSym,const CallEvent & Call,CheckerContext & C) const173 void SimpleStreamChecker::reportDoubleClose(SymbolRef FileDescSym,
174 const CallEvent &Call,
175 CheckerContext &C) const {
176 // We reached a bug, stop exploring the path here by generating a sink.
177 ExplodedNode *ErrNode = C.generateErrorNode();
178 // If we've already reached this node on another path, return.
179 if (!ErrNode)
180 return;
181
182 // Generate the report.
183 auto R = std::make_unique<PathSensitiveBugReport>(
184 DoubleCloseBugType, "Closing a previously closed file stream", ErrNode);
185 R->addRange(Call.getSourceRange());
186 R->markInteresting(FileDescSym);
187 C.emitReport(std::move(R));
188 }
189
reportLeaks(ArrayRef<SymbolRef> LeakedStreams,CheckerContext & C,ExplodedNode * ErrNode) const190 void SimpleStreamChecker::reportLeaks(ArrayRef<SymbolRef> LeakedStreams,
191 CheckerContext &C,
192 ExplodedNode *ErrNode) const {
193 // Attach bug reports to the leak node.
194 // TODO: Identify the leaked file descriptor.
195 for (SymbolRef LeakedStream : LeakedStreams) {
196 auto R = std::make_unique<PathSensitiveBugReport>(
197 LeakBugType, "Opened file is never closed; potential resource leak",
198 ErrNode);
199 R->markInteresting(LeakedStream);
200 C.emitReport(std::move(R));
201 }
202 }
203
guaranteedNotToCloseFile(const CallEvent & Call) const204 bool SimpleStreamChecker::guaranteedNotToCloseFile(const CallEvent &Call) const{
205 // If it's not in a system header, assume it might close a file.
206 if (!Call.isInSystemHeader())
207 return false;
208
209 // Handle cases where we know a buffer's /address/ can escape.
210 if (Call.argumentsMayEscape())
211 return false;
212
213 // Note, even though fclose closes the file, we do not list it here
214 // since the checker is modeling the call.
215
216 return true;
217 }
218
219 // If the pointer we are tracking escaped, do not track the symbol as
220 // we cannot reason about it anymore.
221 ProgramStateRef
checkPointerEscape(ProgramStateRef State,const InvalidatedSymbols & Escaped,const CallEvent * Call,PointerEscapeKind Kind) const222 SimpleStreamChecker::checkPointerEscape(ProgramStateRef State,
223 const InvalidatedSymbols &Escaped,
224 const CallEvent *Call,
225 PointerEscapeKind Kind) const {
226 // If we know that the call cannot close a file, there is nothing to do.
227 if (Kind == PSK_DirectEscapeOnCall && guaranteedNotToCloseFile(*Call)) {
228 return State;
229 }
230
231 for (SymbolRef Sym : Escaped) {
232 // The symbol escaped. Optimistically, assume that the corresponding file
233 // handle will be closed somewhere else.
234 State = State->remove<StreamMap>(Sym);
235 }
236 return State;
237 }
238
registerSimpleStreamChecker(CheckerManager & mgr)239 void ento::registerSimpleStreamChecker(CheckerManager &mgr) {
240 mgr.registerChecker<SimpleStreamChecker>();
241 }
242
243 // This checker should be enabled regardless of how language options are set.
shouldRegisterSimpleStreamChecker(const CheckerManager & mgr)244 bool ento::shouldRegisterSimpleStreamChecker(const CheckerManager &mgr) {
245 return true;
246 }
247