241 lines · cpp
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-exception6//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 before13// 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 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 namespace88 89/// The state of the checker is a map from tracked stream symbols to their90/// 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,185 CheckerContext &C,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 here208 // 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 as214// we cannot reason about it anymore.215ProgramStateRef216SimpleStreamChecker::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 file227 // 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}241