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 | |
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 "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h" |
24 | #include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState_Fwd.h" |
25 | #include "clang/StaticAnalyzer/Core/PathSensitive/SVals.h" |
26 | #include "llvm/ADT/STLExtras.h" |
27 | #include "llvm/ADT/SmallString.h" |
28 | #include "llvm/ADT/StringExtras.h" |
29 | |
30 | #include <iterator> |
31 | #include <utility> |
32 | #include <variant> |
33 | |
34 | using namespace clang; |
35 | using namespace ento; |
36 | |
37 | namespace { |
38 | |
39 | struct CritSectionMarker { |
40 | const Expr *LockExpr{}; |
41 | const MemRegion *LockReg{}; |
42 | |
43 | void Profile(llvm::FoldingSetNodeID &ID) const { |
44 | ID.Add(x: LockExpr); |
45 | ID.Add(x: LockReg); |
46 | } |
47 | |
48 | [[nodiscard]] constexpr bool |
49 | operator==(const CritSectionMarker &Other) const noexcept { |
50 | return LockExpr == Other.LockExpr && LockReg == Other.LockReg; |
51 | } |
52 | [[nodiscard]] constexpr bool |
53 | operator!=(const CritSectionMarker &Other) const noexcept { |
54 | return !(*this == Other); |
55 | } |
56 | }; |
57 | |
58 | class CallDescriptionBasedMatcher { |
59 | CallDescription LockFn; |
60 | CallDescription UnlockFn; |
61 | |
62 | public: |
63 | CallDescriptionBasedMatcher(CallDescription &&LockFn, |
64 | CallDescription &&UnlockFn) |
65 | : LockFn(std::move(LockFn)), UnlockFn(std::move(UnlockFn)) {} |
66 | [[nodiscard]] bool matches(const CallEvent &Call, bool IsLock) const { |
67 | if (IsLock) { |
68 | return LockFn.matches(Call); |
69 | } |
70 | return UnlockFn.matches(Call); |
71 | } |
72 | }; |
73 | |
74 | class FirstArgMutexDescriptor : public CallDescriptionBasedMatcher { |
75 | public: |
76 | FirstArgMutexDescriptor(CallDescription &&LockFn, CallDescription &&UnlockFn) |
77 | : CallDescriptionBasedMatcher(std::move(LockFn), std::move(UnlockFn)) {} |
78 | |
79 | [[nodiscard]] const MemRegion *getRegion(const CallEvent &Call, bool) const { |
80 | return Call.getArgSVal(Index: 0).getAsRegion(); |
81 | } |
82 | }; |
83 | |
84 | class MemberMutexDescriptor : public CallDescriptionBasedMatcher { |
85 | public: |
86 | MemberMutexDescriptor(CallDescription &&LockFn, CallDescription &&UnlockFn) |
87 | : CallDescriptionBasedMatcher(std::move(LockFn), std::move(UnlockFn)) {} |
88 | |
89 | [[nodiscard]] const MemRegion *getRegion(const CallEvent &Call, bool) const { |
90 | return cast<CXXMemberCall>(Val: Call).getCXXThisVal().getAsRegion(); |
91 | } |
92 | }; |
93 | |
94 | class RAIIMutexDescriptor { |
95 | mutable const IdentifierInfo *Guard{}; |
96 | mutable bool IdentifierInfoInitialized{}; |
97 | mutable llvm::SmallString<32> GuardName{}; |
98 | |
99 | void initIdentifierInfo(const CallEvent &Call) const { |
100 | if (!IdentifierInfoInitialized) { |
101 | // In case of checking C code, or when the corresponding headers are not |
102 | // included, we might end up query the identifier table every time when |
103 | // this function is called instead of early returning it. To avoid this, a |
104 | // bool variable (IdentifierInfoInitialized) is used and the function will |
105 | // be run only once. |
106 | Guard = &Call.getCalleeAnalysisDeclContext()->getASTContext().Idents.get( |
107 | Name: GuardName); |
108 | IdentifierInfoInitialized = true; |
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 | return II == Guard; |
119 | } |
120 | |
121 | public: |
122 | RAIIMutexDescriptor(StringRef GuardName) : GuardName(GuardName) {} |
123 | [[nodiscard]] bool matches(const CallEvent &Call, bool IsLock) const { |
124 | initIdentifierInfo(Call); |
125 | if (IsLock) { |
126 | return matchesImpl<CXXConstructorCall>(Call); |
127 | } |
128 | return matchesImpl<CXXDestructorCall>(Call); |
129 | } |
130 | [[nodiscard]] const MemRegion *getRegion(const CallEvent &Call, |
131 | bool IsLock) const { |
132 | const MemRegion *LockRegion = nullptr; |
133 | if (IsLock) { |
134 | if (std::optional<SVal> Object = Call.getReturnValueUnderConstruction()) { |
135 | LockRegion = Object->getAsRegion(); |
136 | } |
137 | } else { |
138 | LockRegion = cast<CXXDestructorCall>(Val: Call).getCXXThisVal().getAsRegion(); |
139 | } |
140 | return LockRegion; |
141 | } |
142 | }; |
143 | |
144 | using MutexDescriptor = |
145 | std::variant<FirstArgMutexDescriptor, MemberMutexDescriptor, |
146 | RAIIMutexDescriptor>; |
147 | |
148 | class BlockInCriticalSectionChecker : public Checker<check::PostCall> { |
149 | private: |
150 | const std::array<MutexDescriptor, 8> MutexDescriptors{ |
151 | MemberMutexDescriptor( |
152 | CallDescription(/*QualifiedName=*/{"std" , "mutex" , "lock" }, |
153 | /*RequiredArgs=*/0), |
154 | CallDescription({"std" , "mutex" , "unlock" }, 0)), |
155 | FirstArgMutexDescriptor(CallDescription({"pthread_mutex_lock" }, 1), |
156 | CallDescription({"pthread_mutex_unlock" }, 1)), |
157 | FirstArgMutexDescriptor(CallDescription({"mtx_lock" }, 1), |
158 | CallDescription({"mtx_unlock" }, 1)), |
159 | FirstArgMutexDescriptor(CallDescription({"pthread_mutex_trylock" }, 1), |
160 | CallDescription({"pthread_mutex_unlock" }, 1)), |
161 | FirstArgMutexDescriptor(CallDescription({"mtx_trylock" }, 1), |
162 | CallDescription({"mtx_unlock" }, 1)), |
163 | FirstArgMutexDescriptor(CallDescription({"mtx_timedlock" }, 1), |
164 | CallDescription({"mtx_unlock" }, 1)), |
165 | RAIIMutexDescriptor("lock_guard" ), |
166 | RAIIMutexDescriptor("unique_lock" )}; |
167 | |
168 | const std::array<CallDescription, 5> BlockingFunctions{ |
169 | ArrayRef{StringRef{"sleep" }}, ArrayRef{StringRef{"getc" }}, |
170 | ArrayRef{StringRef{"fgets" }}, ArrayRef{StringRef{"read" }}, |
171 | ArrayRef{StringRef{"recv" }}}; |
172 | |
173 | const BugType BlockInCritSectionBugType{ |
174 | this, "Call to blocking function in critical section" , "Blocking Error" }; |
175 | |
176 | void reportBlockInCritSection(const CallEvent &call, CheckerContext &C) const; |
177 | |
178 | [[nodiscard]] const NoteTag *createCritSectionNote(CritSectionMarker M, |
179 | CheckerContext &C) const; |
180 | |
181 | [[nodiscard]] std::optional<MutexDescriptor> |
182 | checkDescriptorMatch(const CallEvent &Call, CheckerContext &C, |
183 | bool IsLock) const; |
184 | |
185 | void handleLock(const MutexDescriptor &Mutex, const CallEvent &Call, |
186 | CheckerContext &C) const; |
187 | |
188 | void handleUnlock(const MutexDescriptor &Mutex, const CallEvent &Call, |
189 | CheckerContext &C) const; |
190 | |
191 | [[nodiscard]] bool isBlockingInCritSection(const CallEvent &Call, |
192 | CheckerContext &C) const; |
193 | |
194 | public: |
195 | /// Process unlock. |
196 | /// Process lock. |
197 | /// Process blocking functions (sleep, getc, fgets, read, recv) |
198 | void checkPostCall(const CallEvent &Call, CheckerContext &C) const; |
199 | }; |
200 | |
201 | } // end anonymous namespace |
202 | |
203 | REGISTER_LIST_WITH_PROGRAMSTATE(ActiveCritSections, CritSectionMarker) |
204 | |
205 | namespace std { |
206 | // Iterator traits for ImmutableList data structure |
207 | // that enable the use of STL algorithms. |
208 | // TODO: Move these to llvm::ImmutableList when overhauling immutable data |
209 | // structures for proper iterator concept support. |
210 | template <> |
211 | struct iterator_traits< |
212 | typename llvm::ImmutableList<CritSectionMarker>::iterator> { |
213 | using iterator_category = std::forward_iterator_tag; |
214 | using value_type = CritSectionMarker; |
215 | using difference_type = std::ptrdiff_t; |
216 | using reference = CritSectionMarker &; |
217 | using pointer = CritSectionMarker *; |
218 | }; |
219 | } // namespace std |
220 | |
221 | std::optional<MutexDescriptor> |
222 | BlockInCriticalSectionChecker::checkDescriptorMatch(const CallEvent &Call, |
223 | CheckerContext &C, |
224 | bool IsLock) const { |
225 | const auto Descriptor = |
226 | llvm::find_if(Range: MutexDescriptors, P: [&Call, IsLock](auto &&Descriptor) { |
227 | return std::visit( |
228 | [&Call, IsLock](auto &&DescriptorImpl) { |
229 | return DescriptorImpl.matches(Call, IsLock); |
230 | }, |
231 | Descriptor); |
232 | }); |
233 | if (Descriptor != MutexDescriptors.end()) |
234 | return *Descriptor; |
235 | return std::nullopt; |
236 | } |
237 | |
238 | static const MemRegion *getRegion(const CallEvent &Call, |
239 | const MutexDescriptor &Descriptor, |
240 | bool IsLock) { |
241 | return std::visit( |
242 | visitor: [&Call, IsLock](auto &&Descriptor) { |
243 | return Descriptor.getRegion(Call, IsLock); |
244 | }, |
245 | variants: Descriptor); |
246 | } |
247 | |
248 | void BlockInCriticalSectionChecker::handleLock( |
249 | const MutexDescriptor &LockDescriptor, const CallEvent &Call, |
250 | CheckerContext &C) const { |
251 | const MemRegion *MutexRegion = |
252 | getRegion(Call, Descriptor: LockDescriptor, /*IsLock=*/true); |
253 | if (!MutexRegion) |
254 | return; |
255 | |
256 | const CritSectionMarker MarkToAdd{.LockExpr: Call.getOriginExpr(), .LockReg: MutexRegion}; |
257 | ProgramStateRef StateWithLockEvent = |
258 | C.getState()->add<ActiveCritSections>(K: MarkToAdd); |
259 | C.addTransition(State: StateWithLockEvent, Tag: createCritSectionNote(M: MarkToAdd, C)); |
260 | } |
261 | |
262 | void BlockInCriticalSectionChecker::handleUnlock( |
263 | const MutexDescriptor &UnlockDescriptor, const CallEvent &Call, |
264 | CheckerContext &C) const { |
265 | const MemRegion *MutexRegion = |
266 | getRegion(Call, Descriptor: UnlockDescriptor, /*IsLock=*/false); |
267 | if (!MutexRegion) |
268 | return; |
269 | |
270 | ProgramStateRef State = C.getState(); |
271 | const auto ActiveSections = State->get<ActiveCritSections>(); |
272 | const auto MostRecentLock = |
273 | llvm::find_if(Range: ActiveSections, P: [MutexRegion](auto &&Marker) { |
274 | return Marker.LockReg == MutexRegion; |
275 | }); |
276 | if (MostRecentLock == ActiveSections.end()) |
277 | return; |
278 | |
279 | // Build a new ImmutableList without this element. |
280 | auto &Factory = State->get_context<ActiveCritSections>(); |
281 | llvm::ImmutableList<CritSectionMarker> NewList = Factory.getEmptyList(); |
282 | for (auto It = ActiveSections.begin(), End = ActiveSections.end(); It != End; |
283 | ++It) { |
284 | if (It != MostRecentLock) |
285 | NewList = Factory.add(Data: *It, L: NewList); |
286 | } |
287 | |
288 | State = State->set<ActiveCritSections>(NewList); |
289 | C.addTransition(State); |
290 | } |
291 | |
292 | bool BlockInCriticalSectionChecker::isBlockingInCritSection( |
293 | const CallEvent &Call, CheckerContext &C) const { |
294 | return llvm::any_of(Range: BlockingFunctions, |
295 | P: [&Call](auto &&Fn) { return Fn.matches(Call); }) && |
296 | !C.getState()->get<ActiveCritSections>().isEmpty(); |
297 | } |
298 | |
299 | void BlockInCriticalSectionChecker::checkPostCall(const CallEvent &Call, |
300 | CheckerContext &C) const { |
301 | if (isBlockingInCritSection(Call, C)) { |
302 | reportBlockInCritSection(call: Call, C); |
303 | } else if (std::optional<MutexDescriptor> LockDesc = |
304 | checkDescriptorMatch(Call, C, /*IsLock=*/true)) { |
305 | handleLock(LockDescriptor: *LockDesc, Call, C); |
306 | } else if (std::optional<MutexDescriptor> UnlockDesc = |
307 | checkDescriptorMatch(Call, C, /*IsLock=*/false)) { |
308 | handleUnlock(UnlockDescriptor: *UnlockDesc, Call, C); |
309 | } |
310 | } |
311 | |
312 | void BlockInCriticalSectionChecker::reportBlockInCritSection( |
313 | const CallEvent &Call, CheckerContext &C) const { |
314 | ExplodedNode *ErrNode = C.generateNonFatalErrorNode(State: C.getState()); |
315 | if (!ErrNode) |
316 | return; |
317 | |
318 | std::string msg; |
319 | llvm::raw_string_ostream os(msg); |
320 | os << "Call to blocking function '" << Call.getCalleeIdentifier()->getName() |
321 | << "' inside of critical section" ; |
322 | auto R = std::make_unique<PathSensitiveBugReport>(args: BlockInCritSectionBugType, |
323 | args&: os.str(), args&: ErrNode); |
324 | R->addRange(R: Call.getSourceRange()); |
325 | R->markInteresting(V: Call.getReturnValue()); |
326 | C.emitReport(R: std::move(R)); |
327 | } |
328 | |
329 | const NoteTag * |
330 | BlockInCriticalSectionChecker::createCritSectionNote(CritSectionMarker M, |
331 | CheckerContext &C) const { |
332 | const BugType *BT = &this->BlockInCritSectionBugType; |
333 | return C.getNoteTag(Cb: [M, BT](PathSensitiveBugReport &BR, |
334 | llvm::raw_ostream &OS) { |
335 | if (&BR.getBugType() != BT) |
336 | return; |
337 | |
338 | // Get the lock events for the mutex of the current line's lock event. |
339 | const auto CritSectionBegins = |
340 | BR.getErrorNode()->getState()->get<ActiveCritSections>(); |
341 | llvm::SmallVector<CritSectionMarker, 4> LocksForMutex; |
342 | llvm::copy_if( |
343 | Range: CritSectionBegins, Out: std::back_inserter(x&: LocksForMutex), |
344 | P: [M](const auto &Marker) { return Marker.LockReg == M.LockReg; }); |
345 | if (LocksForMutex.empty()) |
346 | return; |
347 | |
348 | // As the ImmutableList builds the locks by prepending them, we |
349 | // reverse the list to get the correct order. |
350 | std::reverse(first: LocksForMutex.begin(), last: LocksForMutex.end()); |
351 | |
352 | // Find the index of the lock expression in the list of all locks for a |
353 | // given mutex (in acquisition order). |
354 | const auto Position = |
355 | llvm::find_if(Range: std::as_const(t&: LocksForMutex), P: [M](const auto &Marker) { |
356 | return Marker.LockExpr == M.LockExpr; |
357 | }); |
358 | if (Position == LocksForMutex.end()) |
359 | return; |
360 | |
361 | // If there is only one lock event, we don't need to specify how many times |
362 | // the critical section was entered. |
363 | if (LocksForMutex.size() == 1) { |
364 | OS << "Entering critical section here" ; |
365 | return; |
366 | } |
367 | |
368 | const auto IndexOfLock = |
369 | std::distance(first: std::as_const(t&: LocksForMutex).begin(), last: Position); |
370 | |
371 | const auto OrdinalOfLock = IndexOfLock + 1; |
372 | OS << "Entering critical section for the " << OrdinalOfLock |
373 | << llvm::getOrdinalSuffix(Val: OrdinalOfLock) << " time here" ; |
374 | }); |
375 | } |
376 | |
377 | void ento::registerBlockInCriticalSectionChecker(CheckerManager &mgr) { |
378 | mgr.registerChecker<BlockInCriticalSectionChecker>(); |
379 | } |
380 | |
381 | bool ento::shouldRegisterBlockInCriticalSectionChecker( |
382 | const CheckerManager &mgr) { |
383 | return true; |
384 | } |
385 | |