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 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. 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 112 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 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 150 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 173 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 190 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 204 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 222 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 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. 244 bool ento::shouldRegisterSimpleStreamChecker(const CheckerManager &mgr) { 245 return true; 246 } 247