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