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; 34 StreamState(Kind InK) : K(InK) { } 35 36 public: 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 51 class 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 69 public: 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. 90 REGISTER_MAP_WITH_PROGRAMSTATE(StreamMap, SymbolRef, StreamState) 91 92 namespace { 93 class StopTrackingCallback final : public SymbolVisitor { 94 ProgramStateRef state; 95 public: 96 StopTrackingCallback(ProgramStateRef st) : state(std::move(st)) {} 97 ProgramStateRef getState() const { return state; } 98 99 bool VisitSymbol(SymbolRef sym) override { 100 state = state->remove<StreamMap>(sym); 101 return true; 102 } 103 }; 104 } // end anonymous namespace 105 106 SimpleStreamChecker::SimpleStreamChecker() 107 : OpenFn("fopen"), CloseFn("fclose", 1) { 108 // Initialize the bug types. 109 DoubleCloseBugType.reset( 110 new BugType(this, "Double fclose", "Unix Stream API Error")); 111 112 // Sinks are higher importance bugs as well as calls to assert() or exit(0). 113 LeakBugType.reset( 114 new BugType(this, "Resource Leak", "Unix Stream API Error", 115 /*SuppressOnSink=*/true)); 116 } 117 118 void SimpleStreamChecker::checkPostCall(const CallEvent &Call, 119 CheckerContext &C) const { 120 if (!Call.isGlobalCFunction()) 121 return; 122 123 if (!OpenFn.matches(Call)) 124 return; 125 126 // Get the symbolic value corresponding to the file handle. 127 SymbolRef FileDesc = Call.getReturnValue().getAsSymbol(); 128 if (!FileDesc) 129 return; 130 131 // Generate the next transition (an edge in the exploded graph). 132 ProgramStateRef State = C.getState(); 133 State = State->set<StreamMap>(FileDesc, StreamState::getOpened()); 134 C.addTransition(State); 135 } 136 137 void SimpleStreamChecker::checkPreCall(const CallEvent &Call, 138 CheckerContext &C) const { 139 if (!Call.isGlobalCFunction()) 140 return; 141 142 if (!CloseFn.matches(Call)) 143 return; 144 145 // Get the symbolic value corresponding to the file handle. 146 SymbolRef FileDesc = Call.getArgSVal(0).getAsSymbol(); 147 if (!FileDesc) 148 return; 149 150 // Check if the stream has already been closed. 151 ProgramStateRef State = C.getState(); 152 const StreamState *SS = State->get<StreamMap>(FileDesc); 153 if (SS && SS->isClosed()) { 154 reportDoubleClose(FileDesc, Call, C); 155 return; 156 } 157 158 // Generate the next transition, in which the stream is closed. 159 State = State->set<StreamMap>(FileDesc, StreamState::getClosed()); 160 C.addTransition(State); 161 } 162 163 static bool isLeaked(SymbolRef Sym, const StreamState &SS, 164 bool IsSymDead, ProgramStateRef State) { 165 if (IsSymDead && SS.isOpened()) { 166 // If a symbol is NULL, assume that fopen failed on this path. 167 // A symbol should only be considered leaked if it is non-null. 168 ConstraintManager &CMgr = State->getConstraintManager(); 169 ConditionTruthVal OpenFailed = CMgr.isNull(State, Sym); 170 return !OpenFailed.isConstrainedTrue(); 171 } 172 return false; 173 } 174 175 void SimpleStreamChecker::checkDeadSymbols(SymbolReaper &SymReaper, 176 CheckerContext &C) const { 177 ProgramStateRef State = C.getState(); 178 SymbolVector LeakedStreams; 179 StreamMapTy TrackedStreams = State->get<StreamMap>(); 180 for (StreamMapTy::iterator I = TrackedStreams.begin(), 181 E = TrackedStreams.end(); I != E; ++I) { 182 SymbolRef Sym = I->first; 183 bool IsSymDead = SymReaper.isDead(Sym); 184 185 // Collect leaked symbols. 186 if (isLeaked(Sym, I->second, IsSymDead, State)) 187 LeakedStreams.push_back(Sym); 188 189 // Remove the dead symbol from the streams map. 190 if (IsSymDead) 191 State = State->remove<StreamMap>(Sym); 192 } 193 194 ExplodedNode *N = C.generateNonFatalErrorNode(State); 195 if (!N) 196 return; 197 reportLeaks(LeakedStreams, C, N); 198 } 199 200 void SimpleStreamChecker::reportDoubleClose(SymbolRef FileDescSym, 201 const CallEvent &Call, 202 CheckerContext &C) const { 203 // We reached a bug, stop exploring the path here by generating a sink. 204 ExplodedNode *ErrNode = C.generateErrorNode(); 205 // If we've already reached this node on another path, return. 206 if (!ErrNode) 207 return; 208 209 // Generate the report. 210 auto R = std::make_unique<PathSensitiveBugReport>( 211 *DoubleCloseBugType, "Closing a previously closed file stream", ErrNode); 212 R->addRange(Call.getSourceRange()); 213 R->markInteresting(FileDescSym); 214 C.emitReport(std::move(R)); 215 } 216 217 void SimpleStreamChecker::reportLeaks(ArrayRef<SymbolRef> LeakedStreams, 218 CheckerContext &C, 219 ExplodedNode *ErrNode) const { 220 // Attach bug reports to the leak node. 221 // TODO: Identify the leaked file descriptor. 222 for (SymbolRef LeakedStream : LeakedStreams) { 223 auto R = std::make_unique<PathSensitiveBugReport>( 224 *LeakBugType, "Opened file is never closed; potential resource leak", 225 ErrNode); 226 R->markInteresting(LeakedStream); 227 C.emitReport(std::move(R)); 228 } 229 } 230 231 bool SimpleStreamChecker::guaranteedNotToCloseFile(const CallEvent &Call) const{ 232 // If it's not in a system header, assume it might close a file. 233 if (!Call.isInSystemHeader()) 234 return false; 235 236 // Handle cases where we know a buffer's /address/ can escape. 237 if (Call.argumentsMayEscape()) 238 return false; 239 240 // Note, even though fclose closes the file, we do not list it here 241 // since the checker is modeling the call. 242 243 return true; 244 } 245 246 // If the pointer we are tracking escaped, do not track the symbol as 247 // we cannot reason about it anymore. 248 ProgramStateRef 249 SimpleStreamChecker::checkPointerEscape(ProgramStateRef State, 250 const InvalidatedSymbols &Escaped, 251 const CallEvent *Call, 252 PointerEscapeKind Kind) const { 253 // If we know that the call cannot close a file, there is nothing to do. 254 if (Kind == PSK_DirectEscapeOnCall && guaranteedNotToCloseFile(*Call)) { 255 return State; 256 } 257 258 for (InvalidatedSymbols::const_iterator I = Escaped.begin(), 259 E = Escaped.end(); 260 I != E; ++I) { 261 SymbolRef Sym = *I; 262 263 // The symbol escaped. Optimistically, assume that the corresponding file 264 // handle will be closed somewhere else. 265 State = State->remove<StreamMap>(Sym); 266 } 267 return State; 268 } 269 270 void ento::registerSimpleStreamChecker(CheckerManager &mgr) { 271 mgr.registerChecker<SimpleStreamChecker>(); 272 } 273 274 // This checker should be enabled regardless of how language options are set. 275 bool ento::shouldRegisterSimpleStreamChecker(const CheckerManager &mgr) { 276 return true; 277 } 278