clang 23.0.0git
BlockInCriticalSectionChecker.cpp
Go to the documentation of this file.
1//===-- BlockInCriticalSectionChecker.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 blocks in critical sections. This checker should find
10// the calls to blocking functions (for example: sleep, getc, fgets, read,
11// recv etc.) inside a critical section. When sleep(x) is called while a mutex
12// is held, other threades cannot lock the same mutex. This might take some
13// time, leading to bad performance or even deadlock.
14//
15//===----------------------------------------------------------------------===//
16
27#include "llvm/ADT/STLExtras.h"
28#include "llvm/ADT/SmallString.h"
29#include "llvm/ADT/StringExtras.h"
30
31#include <iterator>
32#include <utility>
33#include <variant>
34
35using namespace clang;
36using namespace ento;
37
38namespace {
39
40struct CritSectionMarker {
41 const Expr *LockExpr{};
42 const MemRegion *LockReg{};
43
44 void Profile(llvm::FoldingSetNodeID &ID) const {
45 ID.Add(LockExpr);
46 ID.Add(LockReg);
47 }
48
49 [[nodiscard]] constexpr bool
50 operator==(const CritSectionMarker &Other) const noexcept {
51 return LockExpr == Other.LockExpr && LockReg == Other.LockReg;
52 }
53 [[nodiscard]] constexpr bool
54 operator!=(const CritSectionMarker &Other) const noexcept {
55 return !(*this == Other);
56 }
57};
58
59class CallDescriptionBasedMatcher {
60 CallDescription LockFn;
61 CallDescription UnlockFn;
62
63public:
64 CallDescriptionBasedMatcher(CallDescription &&LockFn,
65 CallDescription &&UnlockFn)
66 : LockFn(std::move(LockFn)), UnlockFn(std::move(UnlockFn)) {}
67 [[nodiscard]] bool matches(const CallEvent &Call, bool IsLock) const {
68 if (IsLock) {
69 return LockFn.matches(Call);
70 }
71 return UnlockFn.matches(Call);
72 }
73};
74
75class FirstArgMutexDescriptor : public CallDescriptionBasedMatcher {
76public:
77 FirstArgMutexDescriptor(CallDescription &&LockFn, CallDescription &&UnlockFn)
78 : CallDescriptionBasedMatcher(std::move(LockFn), std::move(UnlockFn)) {}
79
80 [[nodiscard]] const MemRegion *getRegion(const CallEvent &Call, bool) const {
81 return Call.getArgSVal(0).getAsRegion();
82 }
83};
84
85class MemberMutexDescriptor : public CallDescriptionBasedMatcher {
86public:
87 MemberMutexDescriptor(CallDescription &&LockFn, CallDescription &&UnlockFn)
88 : CallDescriptionBasedMatcher(std::move(LockFn), std::move(UnlockFn)) {}
89
90 [[nodiscard]] const MemRegion *getRegion(const CallEvent &Call, bool) const {
91 return cast<CXXMemberCall>(Call).getCXXThisVal().getAsRegion();
92 }
93};
94
95class RAIIMutexDescriptor {
96 mutable const IdentifierInfo *Guard{};
97 mutable bool IdentifierInfoInitialized{};
98 mutable llvm::SmallString<32> GuardName{};
99
100 void initIdentifierInfo(const CallEvent &Call) const {
101 if (!IdentifierInfoInitialized) {
102 // In case of checking C code, or when the corresponding headers are not
103 // included, we might end up query the identifier table every time when
104 // this function is called instead of early returning it. To avoid this, a
105 // bool variable (IdentifierInfoInitialized) is used and the function will
106 // be run only once.
107 const auto &ASTCtx = Call.getASTContext();
108 Guard = &ASTCtx.Idents.get(GuardName);
109 }
110 }
111
112 template <typename T> bool matchesImpl(const CallEvent &Call) const {
113 const T *C = dyn_cast<T>(&Call);
114 if (!C)
115 return false;
116 const IdentifierInfo *II =
117 cast<CXXRecordDecl>(C->getDecl()->getParent())->getIdentifier();
118 if (II != Guard)
119 return false;
120
121 // For unique_lock, check if it's constructed with a ctor that takes the tag
122 // type defer_lock_t. In this case, the lock is not acquired.
123 if constexpr (std::is_same_v<T, CXXConstructorCall>) {
124 if (GuardName == "unique_lock" && C->getNumArgs() >= 2) {
125 const Expr *SecondArg = C->getArgExpr(1);
126 QualType ArgType = SecondArg->getType().getNonReferenceType();
127 if (const auto *RD = ArgType->getAsRecordDecl();
128 RD && RD->getName() == "defer_lock_t" && RD->isInStdNamespace()) {
129 return false;
130 }
131 }
132 }
133
134 return true;
135 }
136
137public:
138 RAIIMutexDescriptor(StringRef GuardName) : GuardName(GuardName) {}
139 [[nodiscard]] bool matches(const CallEvent &Call, bool IsLock) const {
140 initIdentifierInfo(Call);
141 if (IsLock) {
142 return matchesImpl<CXXConstructorCall>(Call);
143 }
144 return matchesImpl<CXXDestructorCall>(Call);
145 }
146 [[nodiscard]] const MemRegion *getRegion(const CallEvent &Call,
147 bool IsLock) const {
148 const MemRegion *LockRegion = nullptr;
149 if (IsLock) {
150 if (std::optional<SVal> Object = Call.getReturnValueUnderConstruction()) {
151 LockRegion = Object->getAsRegion();
152 }
153 } else {
154 LockRegion = cast<CXXDestructorCall>(Call).getCXXThisVal().getAsRegion();
155 }
156 return LockRegion;
157 }
158};
159
160using MutexDescriptor =
161 std::variant<FirstArgMutexDescriptor, MemberMutexDescriptor,
162 RAIIMutexDescriptor>;
163
164class SuppressNonBlockingStreams : public BugReporterVisitor {
165private:
166 const CallDescription OpenFunction{CDM::CLibrary, {"open"}, 2};
167 SymbolRef StreamSym;
168 const int NonBlockMacroVal;
169 bool Satisfied = false;
170
171public:
172 SuppressNonBlockingStreams(SymbolRef StreamSym, int NonBlockMacroVal)
173 : StreamSym(StreamSym), NonBlockMacroVal(NonBlockMacroVal) {}
174
175 static void *getTag() {
176 static bool Tag;
177 return &Tag;
178 }
179
180 void Profile(llvm::FoldingSetNodeID &ID) const override {
181 ID.AddPointer(getTag());
182 }
183
184 PathDiagnosticPieceRef VisitNode(const ExplodedNode *N,
185 BugReporterContext &BRC,
186 PathSensitiveBugReport &BR) override {
187 if (Satisfied)
188 return nullptr;
189
190 std::optional<StmtPoint> Point = N->getLocationAs<StmtPoint>();
191 if (!Point)
192 return nullptr;
193
194 const auto *CE = Point->getStmtAs<CallExpr>();
195 if (!CE || !OpenFunction.matchesAsWritten(*CE))
196 return nullptr;
197
198 if (N->getSVal(CE).getAsSymbol() != StreamSym)
199 return nullptr;
200
201 Satisfied = true;
202
203 // Check if open's second argument contains O_NONBLOCK
204 const llvm::APSInt *FlagVal = N->getSVal(CE->getArg(1)).getAsInteger();
205 if (!FlagVal)
206 return nullptr;
207
208 if ((*FlagVal & NonBlockMacroVal) != 0)
209 BR.markInvalid(getTag(), nullptr);
210
211 return nullptr;
212 }
213};
214
215class BlockInCriticalSectionChecker : public Checker<check::PostCall> {
216private:
217 const std::array<MutexDescriptor, 9> MutexDescriptors{
218 // NOTE: There are standard library implementations where some methods
219 // of `std::mutex` are inherited from an implementation detail base
220 // class, and those aren't matched by the name specification {"std",
221 // "mutex", "lock"}.
222 // As a workaround here we omit the class name and only require the
223 // presence of the name parts "std" and "lock"/"unlock".
224 // TODO: Ensure that CallDescription understands inherited methods.
225 MemberMutexDescriptor(
226 {/*MatchAs=*/CDM::CXXMethod,
227 /*QualifiedName=*/{"std", /*"mutex",*/ "lock"},
228 /*RequiredArgs=*/0},
229 {CDM::CXXMethod, {"std", /*"mutex",*/ "unlock"}, 0}),
230 FirstArgMutexDescriptor({CDM::CLibrary, {"pthread_mutex_lock"}, 1},
231 {CDM::CLibrary, {"pthread_mutex_unlock"}, 1}),
232 FirstArgMutexDescriptor({CDM::CLibrary, {"mtx_lock"}, 1},
233 {CDM::CLibrary, {"mtx_unlock"}, 1}),
234 FirstArgMutexDescriptor({CDM::CLibrary, {"pthread_mutex_trylock"}, 1},
235 {CDM::CLibrary, {"pthread_mutex_unlock"}, 1}),
236 FirstArgMutexDescriptor({CDM::CLibrary, {"mtx_trylock"}, 1},
237 {CDM::CLibrary, {"mtx_unlock"}, 1}),
238 FirstArgMutexDescriptor({CDM::CLibrary, {"mtx_timedlock"}, 1},
239 {CDM::CLibrary, {"mtx_unlock"}, 1}),
240 RAIIMutexDescriptor("lock_guard"),
241 RAIIMutexDescriptor("unique_lock"),
242 RAIIMutexDescriptor("scoped_lock")};
243
244 const CallDescriptionSet BlockingFunctions{{CDM::CLibrary, {"sleep"}},
245 {CDM::CLibrary, {"getc"}},
246 {CDM::CLibrary, {"fgets"}},
247 {CDM::CLibrary, {"read"}},
248 {CDM::CLibrary, {"recv"}}};
249
250 const BugType BlockInCritSectionBugType{
251 this, "Call to blocking function in critical section", "Blocking Error"};
252
253 using O_NONBLOCKValueTy = std::optional<int>;
254 mutable std::optional<O_NONBLOCKValueTy> O_NONBLOCKValue;
255
256 void reportBlockInCritSection(const CallEvent &call, CheckerContext &C) const;
257
258 [[nodiscard]] const NoteTag *createCritSectionNote(CritSectionMarker M,
259 CheckerContext &C) const;
260
261 [[nodiscard]] std::optional<MutexDescriptor>
262 checkDescriptorMatch(const CallEvent &Call, CheckerContext &C,
263 bool IsLock) const;
264
265 void handleLock(const MutexDescriptor &Mutex, const CallEvent &Call,
266 CheckerContext &C) const;
267
268 void handleUnlock(const MutexDescriptor &Mutex, const CallEvent &Call,
269 CheckerContext &C) const;
270
271 [[nodiscard]] bool isBlockingInCritSection(const CallEvent &Call,
272 CheckerContext &C) const;
273
274public:
275 /// Process unlock.
276 /// Process lock.
277 /// Process blocking functions (sleep, getc, fgets, read, recv)
278 void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
279};
280
281} // end anonymous namespace
282
283REGISTER_LIST_WITH_PROGRAMSTATE(ActiveCritSections, CritSectionMarker)
284
285std::optional<MutexDescriptor>
286BlockInCriticalSectionChecker::checkDescriptorMatch(const CallEvent &Call,
288 bool IsLock) const {
289 const auto Descriptor =
290 llvm::find_if(MutexDescriptors, [&Call, IsLock](auto &&Descriptor) {
291 return std::visit(
292 [&Call, IsLock](auto &&DescriptorImpl) {
293 return DescriptorImpl.matches(Call, IsLock);
294 },
295 Descriptor);
296 });
297 if (Descriptor != MutexDescriptors.end())
298 return *Descriptor;
299 return std::nullopt;
300}
301
302static const MemRegion *skipStdBaseClassRegion(const MemRegion *Reg) {
303 while (Reg) {
304 const auto *BaseClassRegion = dyn_cast<CXXBaseObjectRegion>(Reg);
305 if (!BaseClassRegion || !isWithinStdNamespace(BaseClassRegion->getDecl()))
306 break;
307 Reg = BaseClassRegion->getSuperRegion();
308 }
309 return Reg;
310}
311
312static const MemRegion *getRegion(const CallEvent &Call,
313 const MutexDescriptor &Descriptor,
314 bool IsLock) {
315 return std::visit(
316 [&Call, IsLock](auto &Descr) -> const MemRegion * {
317 return skipStdBaseClassRegion(Descr.getRegion(Call, IsLock));
318 },
319 Descriptor);
320}
321
322void BlockInCriticalSectionChecker::handleLock(
323 const MutexDescriptor &LockDescriptor, const CallEvent &Call,
324 CheckerContext &C) const {
325 const MemRegion *MutexRegion =
326 getRegion(Call, LockDescriptor, /*IsLock=*/true);
327 if (!MutexRegion)
328 return;
329
330 const CritSectionMarker MarkToAdd{Call.getOriginExpr(), MutexRegion};
331 ProgramStateRef StateWithLockEvent =
332 C.getState()->add<ActiveCritSections>(MarkToAdd);
333 C.addTransition(StateWithLockEvent, createCritSectionNote(MarkToAdd, C));
334}
335
336void BlockInCriticalSectionChecker::handleUnlock(
337 const MutexDescriptor &UnlockDescriptor, const CallEvent &Call,
338 CheckerContext &C) const {
339 const MemRegion *MutexRegion =
340 getRegion(Call, UnlockDescriptor, /*IsLock=*/false);
341 if (!MutexRegion)
342 return;
343
344 ProgramStateRef State = C.getState();
345 const auto ActiveSections = State->get<ActiveCritSections>();
346 const auto MostRecentLock =
347 llvm::find_if(ActiveSections, [MutexRegion](auto &&Marker) {
348 return Marker.LockReg == MutexRegion;
349 });
350 if (MostRecentLock == ActiveSections.end())
351 return;
352
353 // Build a new ImmutableList without this element.
354 auto &Factory = State->get_context<ActiveCritSections>();
355 llvm::ImmutableList<CritSectionMarker> NewList = Factory.getEmptyList();
356 for (auto It = ActiveSections.begin(), End = ActiveSections.end(); It != End;
357 ++It) {
358 if (It != MostRecentLock)
359 NewList = Factory.add(*It, NewList);
360 }
361
362 State = State->set<ActiveCritSections>(NewList);
363 C.addTransition(State);
364}
365
366bool BlockInCriticalSectionChecker::isBlockingInCritSection(
367 const CallEvent &Call, CheckerContext &C) const {
368 return BlockingFunctions.contains(Call) &&
369 !C.getState()->get<ActiveCritSections>().isEmpty();
370}
371
372void BlockInCriticalSectionChecker::checkPostCall(const CallEvent &Call,
373 CheckerContext &C) const {
374 if (isBlockingInCritSection(Call, C)) {
375 reportBlockInCritSection(Call, C);
376 } else if (std::optional<MutexDescriptor> LockDesc =
377 checkDescriptorMatch(Call, C, /*IsLock=*/true)) {
378 handleLock(*LockDesc, Call, C);
379 } else if (std::optional<MutexDescriptor> UnlockDesc =
380 checkDescriptorMatch(Call, C, /*IsLock=*/false)) {
381 handleUnlock(*UnlockDesc, Call, C);
382 }
383}
384
385void BlockInCriticalSectionChecker::reportBlockInCritSection(
386 const CallEvent &Call, CheckerContext &C) const {
387 ExplodedNode *ErrNode = C.generateNonFatalErrorNode(C.getState());
388 if (!ErrNode)
389 return;
390
391 std::string msg;
392 llvm::raw_string_ostream os(msg);
393 os << "Call to blocking function '" << Call.getCalleeIdentifier()->getName()
394 << "' inside of critical section";
395 auto R = std::make_unique<PathSensitiveBugReport>(BlockInCritSectionBugType,
396 os.str(), ErrNode);
397 // for 'read' and 'recv' call, check whether it's file descriptor(first
398 // argument) is
399 // created by 'open' API with O_NONBLOCK flag or is equal to -1, they will
400 // not cause block in these situations, don't report
401 StringRef FuncName = Call.getCalleeIdentifier()->getName();
402 if (FuncName == "read" || FuncName == "recv") {
403 SVal SV = Call.getArgSVal(0);
404 SValBuilder &SVB = C.getSValBuilder();
405 ProgramStateRef state = C.getState();
406 ConditionTruthVal CTV =
407 state->areEqual(SV, SVB.makeIntVal(-1, C.getASTContext().IntTy));
408 if (CTV.isConstrainedTrue())
409 return;
410
411 if (SymbolRef SR = SV.getAsSymbol()) {
412 if (!O_NONBLOCKValue)
413 O_NONBLOCKValue = tryExpandAsInteger(
414 "O_NONBLOCK", C.getBugReporter().getPreprocessor());
415 if (*O_NONBLOCKValue)
416 R->addVisitor<SuppressNonBlockingStreams>(SR, **O_NONBLOCKValue);
417 }
418 }
419 R->addRange(Call.getSourceRange());
420 R->markInteresting(Call.getReturnValue());
421 C.emitReport(std::move(R));
422}
423
424const NoteTag *
425BlockInCriticalSectionChecker::createCritSectionNote(CritSectionMarker M,
426 CheckerContext &C) const {
427 const BugType *BT = &this->BlockInCritSectionBugType;
428 return C.getNoteTag([M, BT](PathSensitiveBugReport &BR,
429 llvm::raw_ostream &OS) {
430 if (&BR.getBugType() != BT)
431 return;
432
433 // Get the lock events for the mutex of the current line's lock event.
434 const auto CritSectionBegins =
435 BR.getErrorNode()->getState()->get<ActiveCritSections>();
436 llvm::SmallVector<CritSectionMarker, 4> LocksForMutex;
437 llvm::copy_if(
438 CritSectionBegins, std::back_inserter(LocksForMutex),
439 [M](const auto &Marker) { return Marker.LockReg == M.LockReg; });
440 if (LocksForMutex.empty())
441 return;
442
443 // As the ImmutableList builds the locks by prepending them, we
444 // reverse the list to get the correct order.
445 std::reverse(LocksForMutex.begin(), LocksForMutex.end());
446
447 // Find the index of the lock expression in the list of all locks for a
448 // given mutex (in acquisition order).
449 const auto Position =
450 llvm::find_if(std::as_const(LocksForMutex), [M](const auto &Marker) {
451 return Marker.LockExpr == M.LockExpr;
452 });
453 if (Position == LocksForMutex.end())
454 return;
455
456 // If there is only one lock event, we don't need to specify how many times
457 // the critical section was entered.
458 if (LocksForMutex.size() == 1) {
459 OS << "Entering critical section here";
460 return;
461 }
462
463 const auto IndexOfLock =
464 std::distance(std::as_const(LocksForMutex).begin(), Position);
465
466 const auto OrdinalOfLock = IndexOfLock + 1;
467 OS << "Entering critical section for the " << OrdinalOfLock
468 << llvm::getOrdinalSuffix(OrdinalOfLock) << " time here";
469 });
470}
471
472void ento::registerBlockInCriticalSectionChecker(CheckerManager &mgr) {
473 mgr.registerChecker<BlockInCriticalSectionChecker>();
474}
475
476bool ento::shouldRegisterBlockInCriticalSectionChecker(
477 const CheckerManager &mgr) {
478 return true;
479}
static const MemRegion * skipStdBaseClassRegion(const MemRegion *Reg)
static const MemRegion * getRegion(const CallEvent &Call, const MutexDescriptor &Descriptor, bool IsLock)
llvm::json::Object Object
#define REGISTER_LIST_WITH_PROGRAMSTATE(Name, Elem)
Declares an immutable list type NameTy, suitable for placement into the ProgramState.
QualType getType() const
Definition Expr.h:144
StringRef getName() const
Get the name of identifier for this declaration as a StringRef.
Definition Decl.h:301
QualType getNonReferenceType() const
If Type is a reference type (e.g., const int&), returns the type that the reference refers to ("const...
Definition TypeBase.h:8632
RecordDecl * getAsRecordDecl() const
Retrieves the RecordDecl this type refers to.
Definition Type.h:41
const BugType & getBugType() const
BugReporterVisitors are used to add custom diagnostics along a path.
bool contains(const CallEvent &Call) const
Represents an abstract call to a function or method along a particular path.
Definition CallEvent.h:152
CHECKER * registerChecker(AT &&...Args)
Register a single-part checker (derived from Checker): construct its singleton instance,...
Simple checker classes that implement one frontend (i.e.
Definition Checker.h:565
bool isConstrainedTrue() const
Return true if the constraint is perfectly constrained to 'true'.
const ProgramStateRef & getState() const
SVal getSVal(const Expr *E) const
Get the value of an arbitrary expression at this node.
std::optional< T > getLocationAs() const &
MemRegion - The root abstract class for all memory regions.
Definition MemRegion.h:97
const ExplodedNode * getErrorNode() const
void markInvalid(const void *Tag, const void *Data)
Marks the current report as invalid, meaning that it is probably a false positive and should not be r...
nonloc::ConcreteInt makeIntVal(const IntegerLiteral *integer)
SymbolRef getAsSymbol(bool IncludeBaseRegions=false) const
If this SVal wraps a symbol return that SymbolRef.
Definition SVals.cpp:103
const llvm::APSInt * getAsInteger() const
If this SVal is loc::ConcreteInt or nonloc::ConcreteInt, return a pointer to APSInt which is held in ...
Definition SVals.cpp:111
bool isWithinStdNamespace(const Decl *D)
Returns true if declaration D is in std namespace or any nested namespace or class scope.
IntrusiveRefCntPtr< const ProgramState > ProgramStateRef
const SymExpr * SymbolRef
Definition SymExpr.h:133
@ OS
Indicates that the tracking object is a descendant of a referenced-counted OSObject,...
std::optional< int > tryExpandAsInteger(StringRef Macro, const Preprocessor &PP)
Try to parse the value of a defined preprocessor macro.
std::shared_ptr< PathDiagnosticPiece > PathDiagnosticPieceRef
bool matches(const til::SExpr *E1, const til::SExpr *E2)
The JSON file list parser is used to communicate input to InstallAPI.
bool operator==(const CallGraphNode::CallRecord &LHS, const CallGraphNode::CallRecord &RHS)
Definition CallGraph.h:207
bool operator!=(CanQual< T > x, CanQual< U > y)
U cast(CodeGen::Address addr)
Definition Address.h:327
@ Other
Other implicit parameter.
Definition Decl.h:1772