| //===-- BlockInCriticalSectionChecker.cpp -----------------------*- C++ -*-===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| // |
| // Defines a checker for blocks in critical sections. This checker should find |
| // the calls to blocking functions (for example: sleep, getc, fgets, read, |
| // recv etc.) inside a critical section. When sleep(x) is called while a mutex |
| // is held, other threades cannot lock the same mutex. This might take some |
| // time, leading to bad performance or even deadlock. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h" |
| #include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" |
| #include "clang/StaticAnalyzer/Core/Checker.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState_Fwd.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/SVals.h" |
| #include "llvm/ADT/STLExtras.h" |
| #include "llvm/ADT/SmallString.h" |
| #include "llvm/ADT/StringExtras.h" |
| |
| #include <iterator> |
| #include <utility> |
| #include <variant> |
| |
| using namespace clang; |
| using namespace ento; |
| |
| namespace { |
| |
| struct CritSectionMarker { |
| const Expr *LockExpr{}; |
| const MemRegion *LockReg{}; |
| |
| void Profile(llvm::FoldingSetNodeID &ID) const { |
| ID.Add(LockExpr); |
| ID.Add(LockReg); |
| } |
| |
| [[nodiscard]] constexpr bool |
| operator==(const CritSectionMarker &Other) const noexcept { |
| return LockExpr == Other.LockExpr && LockReg == Other.LockReg; |
| } |
| [[nodiscard]] constexpr bool |
| operator!=(const CritSectionMarker &Other) const noexcept { |
| return !(*this == Other); |
| } |
| }; |
| |
| class CallDescriptionBasedMatcher { |
| CallDescription LockFn; |
| CallDescription UnlockFn; |
| |
| public: |
| CallDescriptionBasedMatcher(CallDescription &&LockFn, |
| CallDescription &&UnlockFn) |
| : LockFn(std::move(LockFn)), UnlockFn(std::move(UnlockFn)) {} |
| [[nodiscard]] bool matches(const CallEvent &Call, bool IsLock) const { |
| if (IsLock) { |
| return LockFn.matches(Call); |
| } |
| return UnlockFn.matches(Call); |
| } |
| }; |
| |
| class FirstArgMutexDescriptor : public CallDescriptionBasedMatcher { |
| public: |
| FirstArgMutexDescriptor(CallDescription &&LockFn, CallDescription &&UnlockFn) |
| : CallDescriptionBasedMatcher(std::move(LockFn), std::move(UnlockFn)) {} |
| |
| [[nodiscard]] const MemRegion *getRegion(const CallEvent &Call, bool) const { |
| return Call.getArgSVal(0).getAsRegion(); |
| } |
| }; |
| |
| class MemberMutexDescriptor : public CallDescriptionBasedMatcher { |
| public: |
| MemberMutexDescriptor(CallDescription &&LockFn, CallDescription &&UnlockFn) |
| : CallDescriptionBasedMatcher(std::move(LockFn), std::move(UnlockFn)) {} |
| |
| [[nodiscard]] const MemRegion *getRegion(const CallEvent &Call, bool) const { |
| return cast<CXXMemberCall>(Call).getCXXThisVal().getAsRegion(); |
| } |
| }; |
| |
| class RAIIMutexDescriptor { |
| mutable const IdentifierInfo *Guard{}; |
| mutable bool IdentifierInfoInitialized{}; |
| mutable llvm::SmallString<32> GuardName{}; |
| |
| void initIdentifierInfo(const CallEvent &Call) const { |
| if (!IdentifierInfoInitialized) { |
| // In case of checking C code, or when the corresponding headers are not |
| // included, we might end up query the identifier table every time when |
| // this function is called instead of early returning it. To avoid this, a |
| // bool variable (IdentifierInfoInitialized) is used and the function will |
| // be run only once. |
| const auto &ASTCtx = Call.getState()->getStateManager().getContext(); |
| Guard = &ASTCtx.Idents.get(GuardName); |
| } |
| } |
| |
| template <typename T> bool matchesImpl(const CallEvent &Call) const { |
| const T *C = dyn_cast<T>(&Call); |
| if (!C) |
| return false; |
| const IdentifierInfo *II = |
| cast<CXXRecordDecl>(C->getDecl()->getParent())->getIdentifier(); |
| return II == Guard; |
| } |
| |
| public: |
| RAIIMutexDescriptor(StringRef GuardName) : GuardName(GuardName) {} |
| [[nodiscard]] bool matches(const CallEvent &Call, bool IsLock) const { |
| initIdentifierInfo(Call); |
| if (IsLock) { |
| return matchesImpl<CXXConstructorCall>(Call); |
| } |
| return matchesImpl<CXXDestructorCall>(Call); |
| } |
| [[nodiscard]] const MemRegion *getRegion(const CallEvent &Call, |
| bool IsLock) const { |
| const MemRegion *LockRegion = nullptr; |
| if (IsLock) { |
| if (std::optional<SVal> Object = Call.getReturnValueUnderConstruction()) { |
| LockRegion = Object->getAsRegion(); |
| } |
| } else { |
| LockRegion = cast<CXXDestructorCall>(Call).getCXXThisVal().getAsRegion(); |
| } |
| return LockRegion; |
| } |
| }; |
| |
| using MutexDescriptor = |
| std::variant<FirstArgMutexDescriptor, MemberMutexDescriptor, |
| RAIIMutexDescriptor>; |
| |
| class SuppressNonBlockingStreams : public BugReporterVisitor { |
| private: |
| const CallDescription OpenFunction{CDM::CLibrary, {"open"}, 2}; |
| SymbolRef StreamSym; |
| const int NonBlockMacroVal; |
| bool Satisfied = false; |
| |
| public: |
| SuppressNonBlockingStreams(SymbolRef StreamSym, int NonBlockMacroVal) |
| : StreamSym(StreamSym), NonBlockMacroVal(NonBlockMacroVal) {} |
| |
| static void *getTag() { |
| static bool Tag; |
| return &Tag; |
| } |
| |
| void Profile(llvm::FoldingSetNodeID &ID) const override { |
| ID.AddPointer(getTag()); |
| } |
| |
| PathDiagnosticPieceRef VisitNode(const ExplodedNode *N, |
| BugReporterContext &BRC, |
| PathSensitiveBugReport &BR) override { |
| if (Satisfied) |
| return nullptr; |
| |
| std::optional<StmtPoint> Point = N->getLocationAs<StmtPoint>(); |
| if (!Point) |
| return nullptr; |
| |
| const auto *CE = Point->getStmtAs<CallExpr>(); |
| if (!CE || !OpenFunction.matchesAsWritten(*CE)) |
| return nullptr; |
| |
| if (N->getSVal(CE).getAsSymbol() != StreamSym) |
| return nullptr; |
| |
| Satisfied = true; |
| |
| // Check if open's second argument contains O_NONBLOCK |
| const llvm::APSInt *FlagVal = N->getSVal(CE->getArg(1)).getAsInteger(); |
| if (!FlagVal) |
| return nullptr; |
| |
| if ((*FlagVal & NonBlockMacroVal) != 0) |
| BR.markInvalid(getTag(), nullptr); |
| |
| return nullptr; |
| } |
| }; |
| |
| class BlockInCriticalSectionChecker : public Checker<check::PostCall> { |
| private: |
| const std::array<MutexDescriptor, 8> MutexDescriptors{ |
| // NOTE: There are standard library implementations where some methods |
| // of `std::mutex` are inherited from an implementation detail base |
| // class, and those aren't matched by the name specification {"std", |
| // "mutex", "lock"}. |
| // As a workaround here we omit the class name and only require the |
| // presence of the name parts "std" and "lock"/"unlock". |
| // TODO: Ensure that CallDescription understands inherited methods. |
| MemberMutexDescriptor( |
| {/*MatchAs=*/CDM::CXXMethod, |
| /*QualifiedName=*/{"std", /*"mutex",*/ "lock"}, |
| /*RequiredArgs=*/0}, |
| {CDM::CXXMethod, {"std", /*"mutex",*/ "unlock"}, 0}), |
| FirstArgMutexDescriptor({CDM::CLibrary, {"pthread_mutex_lock"}, 1}, |
| {CDM::CLibrary, {"pthread_mutex_unlock"}, 1}), |
| FirstArgMutexDescriptor({CDM::CLibrary, {"mtx_lock"}, 1}, |
| {CDM::CLibrary, {"mtx_unlock"}, 1}), |
| FirstArgMutexDescriptor({CDM::CLibrary, {"pthread_mutex_trylock"}, 1}, |
| {CDM::CLibrary, {"pthread_mutex_unlock"}, 1}), |
| FirstArgMutexDescriptor({CDM::CLibrary, {"mtx_trylock"}, 1}, |
| {CDM::CLibrary, {"mtx_unlock"}, 1}), |
| FirstArgMutexDescriptor({CDM::CLibrary, {"mtx_timedlock"}, 1}, |
| {CDM::CLibrary, {"mtx_unlock"}, 1}), |
| RAIIMutexDescriptor("lock_guard"), |
| RAIIMutexDescriptor("unique_lock")}; |
| |
| const CallDescriptionSet BlockingFunctions{{CDM::CLibrary, {"sleep"}}, |
| {CDM::CLibrary, {"getc"}}, |
| {CDM::CLibrary, {"fgets"}}, |
| {CDM::CLibrary, {"read"}}, |
| {CDM::CLibrary, {"recv"}}}; |
| |
| const BugType BlockInCritSectionBugType{ |
| this, "Call to blocking function in critical section", "Blocking Error"}; |
| |
| using O_NONBLOCKValueTy = std::optional<int>; |
| mutable std::optional<O_NONBLOCKValueTy> O_NONBLOCKValue; |
| |
| void reportBlockInCritSection(const CallEvent &call, CheckerContext &C) const; |
| |
| [[nodiscard]] const NoteTag *createCritSectionNote(CritSectionMarker M, |
| CheckerContext &C) const; |
| |
| [[nodiscard]] std::optional<MutexDescriptor> |
| checkDescriptorMatch(const CallEvent &Call, CheckerContext &C, |
| bool IsLock) const; |
| |
| void handleLock(const MutexDescriptor &Mutex, const CallEvent &Call, |
| CheckerContext &C) const; |
| |
| void handleUnlock(const MutexDescriptor &Mutex, const CallEvent &Call, |
| CheckerContext &C) const; |
| |
| [[nodiscard]] bool isBlockingInCritSection(const CallEvent &Call, |
| CheckerContext &C) const; |
| |
| public: |
| /// Process unlock. |
| /// Process lock. |
| /// Process blocking functions (sleep, getc, fgets, read, recv) |
| void checkPostCall(const CallEvent &Call, CheckerContext &C) const; |
| }; |
| |
| } // end anonymous namespace |
| |
| REGISTER_LIST_WITH_PROGRAMSTATE(ActiveCritSections, CritSectionMarker) |
| |
| // Iterator traits for ImmutableList data structure |
| // that enable the use of STL algorithms. |
| // TODO: Move these to llvm::ImmutableList when overhauling immutable data |
| // structures for proper iterator concept support. |
| template <> |
| struct std::iterator_traits< |
| typename llvm::ImmutableList<CritSectionMarker>::iterator> { |
| using iterator_category = std::forward_iterator_tag; |
| using value_type = CritSectionMarker; |
| using difference_type = std::ptrdiff_t; |
| using reference = CritSectionMarker &; |
| using pointer = CritSectionMarker *; |
| }; |
| |
| std::optional<MutexDescriptor> |
| BlockInCriticalSectionChecker::checkDescriptorMatch(const CallEvent &Call, |
| CheckerContext &C, |
| bool IsLock) const { |
| const auto Descriptor = |
| llvm::find_if(MutexDescriptors, [&Call, IsLock](auto &&Descriptor) { |
| return std::visit( |
| [&Call, IsLock](auto &&DescriptorImpl) { |
| return DescriptorImpl.matches(Call, IsLock); |
| }, |
| Descriptor); |
| }); |
| if (Descriptor != MutexDescriptors.end()) |
| return *Descriptor; |
| return std::nullopt; |
| } |
| |
| static const MemRegion *skipStdBaseClassRegion(const MemRegion *Reg) { |
| while (Reg) { |
| const auto *BaseClassRegion = dyn_cast<CXXBaseObjectRegion>(Reg); |
| if (!BaseClassRegion || !isWithinStdNamespace(BaseClassRegion->getDecl())) |
| break; |
| Reg = BaseClassRegion->getSuperRegion(); |
| } |
| return Reg; |
| } |
| |
| static const MemRegion *getRegion(const CallEvent &Call, |
| const MutexDescriptor &Descriptor, |
| bool IsLock) { |
| return std::visit( |
| [&Call, IsLock](auto &Descr) -> const MemRegion * { |
| return skipStdBaseClassRegion(Descr.getRegion(Call, IsLock)); |
| }, |
| Descriptor); |
| } |
| |
| void BlockInCriticalSectionChecker::handleLock( |
| const MutexDescriptor &LockDescriptor, const CallEvent &Call, |
| CheckerContext &C) const { |
| const MemRegion *MutexRegion = |
| getRegion(Call, LockDescriptor, /*IsLock=*/true); |
| if (!MutexRegion) |
| return; |
| |
| const CritSectionMarker MarkToAdd{Call.getOriginExpr(), MutexRegion}; |
| ProgramStateRef StateWithLockEvent = |
| C.getState()->add<ActiveCritSections>(MarkToAdd); |
| C.addTransition(StateWithLockEvent, createCritSectionNote(MarkToAdd, C)); |
| } |
| |
| void BlockInCriticalSectionChecker::handleUnlock( |
| const MutexDescriptor &UnlockDescriptor, const CallEvent &Call, |
| CheckerContext &C) const { |
| const MemRegion *MutexRegion = |
| getRegion(Call, UnlockDescriptor, /*IsLock=*/false); |
| if (!MutexRegion) |
| return; |
| |
| ProgramStateRef State = C.getState(); |
| const auto ActiveSections = State->get<ActiveCritSections>(); |
| const auto MostRecentLock = |
| llvm::find_if(ActiveSections, [MutexRegion](auto &&Marker) { |
| return Marker.LockReg == MutexRegion; |
| }); |
| if (MostRecentLock == ActiveSections.end()) |
| return; |
| |
| // Build a new ImmutableList without this element. |
| auto &Factory = State->get_context<ActiveCritSections>(); |
| llvm::ImmutableList<CritSectionMarker> NewList = Factory.getEmptyList(); |
| for (auto It = ActiveSections.begin(), End = ActiveSections.end(); It != End; |
| ++It) { |
| if (It != MostRecentLock) |
| NewList = Factory.add(*It, NewList); |
| } |
| |
| State = State->set<ActiveCritSections>(NewList); |
| C.addTransition(State); |
| } |
| |
| bool BlockInCriticalSectionChecker::isBlockingInCritSection( |
| const CallEvent &Call, CheckerContext &C) const { |
| return BlockingFunctions.contains(Call) && |
| !C.getState()->get<ActiveCritSections>().isEmpty(); |
| } |
| |
| void BlockInCriticalSectionChecker::checkPostCall(const CallEvent &Call, |
| CheckerContext &C) const { |
| if (isBlockingInCritSection(Call, C)) { |
| reportBlockInCritSection(Call, C); |
| } else if (std::optional<MutexDescriptor> LockDesc = |
| checkDescriptorMatch(Call, C, /*IsLock=*/true)) { |
| handleLock(*LockDesc, Call, C); |
| } else if (std::optional<MutexDescriptor> UnlockDesc = |
| checkDescriptorMatch(Call, C, /*IsLock=*/false)) { |
| handleUnlock(*UnlockDesc, Call, C); |
| } |
| } |
| |
| void BlockInCriticalSectionChecker::reportBlockInCritSection( |
| const CallEvent &Call, CheckerContext &C) const { |
| ExplodedNode *ErrNode = C.generateNonFatalErrorNode(C.getState()); |
| if (!ErrNode) |
| return; |
| |
| std::string msg; |
| llvm::raw_string_ostream os(msg); |
| os << "Call to blocking function '" << Call.getCalleeIdentifier()->getName() |
| << "' inside of critical section"; |
| auto R = std::make_unique<PathSensitiveBugReport>(BlockInCritSectionBugType, |
| os.str(), ErrNode); |
| // for 'read' and 'recv' call, check whether it's file descriptor(first |
| // argument) is |
| // created by 'open' API with O_NONBLOCK flag or is equal to -1, they will |
| // not cause block in these situations, don't report |
| StringRef FuncName = Call.getCalleeIdentifier()->getName(); |
| if (FuncName == "read" || FuncName == "recv") { |
| SVal SV = Call.getArgSVal(0); |
| SValBuilder &SVB = C.getSValBuilder(); |
| ProgramStateRef state = C.getState(); |
| ConditionTruthVal CTV = |
| state->areEqual(SV, SVB.makeIntVal(-1, C.getASTContext().IntTy)); |
| if (CTV.isConstrainedTrue()) |
| return; |
| |
| if (SymbolRef SR = SV.getAsSymbol()) { |
| if (!O_NONBLOCKValue) |
| O_NONBLOCKValue = tryExpandAsInteger( |
| "O_NONBLOCK", C.getBugReporter().getPreprocessor()); |
| if (*O_NONBLOCKValue) |
| R->addVisitor<SuppressNonBlockingStreams>(SR, **O_NONBLOCKValue); |
| } |
| } |
| R->addRange(Call.getSourceRange()); |
| R->markInteresting(Call.getReturnValue()); |
| C.emitReport(std::move(R)); |
| } |
| |
| const NoteTag * |
| BlockInCriticalSectionChecker::createCritSectionNote(CritSectionMarker M, |
| CheckerContext &C) const { |
| const BugType *BT = &this->BlockInCritSectionBugType; |
| return C.getNoteTag([M, BT](PathSensitiveBugReport &BR, |
| llvm::raw_ostream &OS) { |
| if (&BR.getBugType() != BT) |
| return; |
| |
| // Get the lock events for the mutex of the current line's lock event. |
| const auto CritSectionBegins = |
| BR.getErrorNode()->getState()->get<ActiveCritSections>(); |
| llvm::SmallVector<CritSectionMarker, 4> LocksForMutex; |
| llvm::copy_if( |
| CritSectionBegins, std::back_inserter(LocksForMutex), |
| [M](const auto &Marker) { return Marker.LockReg == M.LockReg; }); |
| if (LocksForMutex.empty()) |
| return; |
| |
| // As the ImmutableList builds the locks by prepending them, we |
| // reverse the list to get the correct order. |
| std::reverse(LocksForMutex.begin(), LocksForMutex.end()); |
| |
| // Find the index of the lock expression in the list of all locks for a |
| // given mutex (in acquisition order). |
| const auto Position = |
| llvm::find_if(std::as_const(LocksForMutex), [M](const auto &Marker) { |
| return Marker.LockExpr == M.LockExpr; |
| }); |
| if (Position == LocksForMutex.end()) |
| return; |
| |
| // If there is only one lock event, we don't need to specify how many times |
| // the critical section was entered. |
| if (LocksForMutex.size() == 1) { |
| OS << "Entering critical section here"; |
| return; |
| } |
| |
| const auto IndexOfLock = |
| std::distance(std::as_const(LocksForMutex).begin(), Position); |
| |
| const auto OrdinalOfLock = IndexOfLock + 1; |
| OS << "Entering critical section for the " << OrdinalOfLock |
| << llvm::getOrdinalSuffix(OrdinalOfLock) << " time here"; |
| }); |
| } |
| |
| void ento::registerBlockInCriticalSectionChecker(CheckerManager &mgr) { |
| mgr.registerChecker<BlockInCriticalSectionChecker>(); |
| } |
| |
| bool ento::shouldRegisterBlockInCriticalSectionChecker( |
| const CheckerManager &mgr) { |
| return true; |
| } |