Skip to content

Commit c1e7e45

Browse files
malek203aaronpuchert
authored andcommitted
Thread Safety Analysis: Support passing scoped locks between functions with appropriate annotations (#110523)
This is helpful when multiple functions operate on the same capabilities, but we still want to use scoped lockable types for readability and exception safety. - Introduce support for thread safety annotations on function parameters marked with the 'scoped_lockable' attribute. - Add semantic checks for annotated function parameters, ensuring correct usage. - Enhance the analysis to recognize and handle parameters annotated for thread safety, extending the scope of analysis to track these across function boundries. - Verify that the underlying mutexes of function arguments match the expectations set by the annotations. Limitation: This does not work when the attribute arguments are class members, because attributes on function parameters are parsed differently from attributes on functions.
1 parent 415cfaf commit c1e7e45

10 files changed

+774
-30
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,31 @@ Improvements to Clang's diagnostics
708708
- Fixed a bug where Clang hung on an unsupported optional scope specifier ``::`` when parsing
709709
Objective-C. Clang now emits a diagnostic message instead of hanging.
710710

711+
- The :doc:`ThreadSafetyAnalysis` now supports passing scoped capabilities into functions:
712+
an attribute on the scoped capability parameter indicates both the expected associated capabilities and,
713+
like in the case of attributes on the function declaration itself, their state before and after the call.
714+
715+
.. code-block:: c++
716+
717+
#include "mutex.h"
718+
719+
Mutex mu1, mu2;
720+
int a GUARDED_BY(mu1);
721+
722+
void require(MutexLocker& scope REQUIRES(mu1)) {
723+
scope.Unlock();
724+
a = 0; // Warning! Requires mu1.
725+
scope.Lock();
726+
}
727+
728+
void testParameter() {
729+
MutexLocker scope(&mu1), scope2(&mu2);
730+
require(scope2); // Warning! Mutex managed by 'scope2' is 'mu2' instead of 'mu1'
731+
require(scope); // OK.
732+
scope.Unlock();
733+
require(scope); // Warning! Requires mu1.
734+
}
735+
711736
Improvements to Clang's time-trace
712737
----------------------------------
713738

clang/docs/ThreadSafetyAnalysis.rst

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,13 @@ REQUIRES(...), REQUIRES_SHARED(...)
187187

188188
*Previously*: ``EXCLUSIVE_LOCKS_REQUIRED``, ``SHARED_LOCKS_REQUIRED``
189189

190-
``REQUIRES`` is an attribute on functions or methods, which
190+
``REQUIRES`` is an attribute on functions, methods or function parameters of
191+
reference to :ref:`scoped_capability`-annotated type, which
191192
declares that the calling thread must have exclusive access to the given
192193
capabilities. More than one capability may be specified. The capabilities
193194
must be held on entry to the function, *and must still be held on exit*.
195+
Additionally, if the attribute is on a function parameter, it declares that
196+
the scoped capability manages the specified capabilities in the given order.
194197

195198
``REQUIRES_SHARED`` is similar, but requires only shared access.
196199

@@ -211,17 +214,34 @@ must be held on entry to the function, *and must still be held on exit*.
211214
mu1.Unlock();
212215
}
213216

217+
void require(MutexLocker& scope REQUIRES(mu1)) {
218+
scope.Unlock();
219+
a = 0; // Warning! Requires mu1.
220+
scope.Lock();
221+
}
222+
223+
void testParameter() {
224+
MutexLocker scope(&mu1), scope2(&mu2);
225+
require(scope2); // Warning! Mutex managed by 'scope2' is 'mu2' instead of 'mu1'
226+
require(scope); // OK.
227+
scope.Unlock();
228+
require(scope); // Warning! Requires mu1.
229+
}
230+
214231

215232
ACQUIRE(...), ACQUIRE_SHARED(...), RELEASE(...), RELEASE_SHARED(...), RELEASE_GENERIC(...)
216233
------------------------------------------------------------------------------------------
217234

218235
*Previously*: ``EXCLUSIVE_LOCK_FUNCTION``, ``SHARED_LOCK_FUNCTION``,
219236
``UNLOCK_FUNCTION``
220237

221-
``ACQUIRE`` and ``ACQUIRE_SHARED`` are attributes on functions or methods
222-
declaring that the function acquires a capability, but does not release it.
238+
``ACQUIRE`` and ``ACQUIRE_SHARED`` are attributes on functions, methods
239+
or function parameters of reference to :ref:`scoped_capability`-annotated type,
240+
which declare that the function acquires a capability, but does not release it.
223241
The given capability must not be held on entry, and will be held on exit
224242
(exclusively for ``ACQUIRE``, shared for ``ACQUIRE_SHARED``).
243+
Additionally, if the attribute is on a function parameter, it declares that
244+
the scoped capability manages the specified capabilities in the given order.
225245

226246
``RELEASE``, ``RELEASE_SHARED``, and ``RELEASE_GENERIC`` declare that the
227247
function releases the given capability. The capability must be held on entry
@@ -249,6 +269,14 @@ shared for ``RELEASE_GENERIC``), and will no longer be held on exit.
249269
myObject.doSomething(); // Warning, mu is not locked.
250270
}
251271

272+
void release(MutexLocker& scope RELEASE(mu)) {
273+
} // Warning! Need to unlock mu.
274+
275+
void testParameter() {
276+
MutexLocker scope(&mu);
277+
release(scope);
278+
}
279+
252280
If no argument is passed to ``ACQUIRE`` or ``RELEASE``, then the argument is
253281
assumed to be ``this``, and the analysis will not check the body of the
254282
function. This pattern is intended for use by classes which hide locking
@@ -283,10 +311,13 @@ EXCLUDES(...)
283311

284312
*Previously*: ``LOCKS_EXCLUDED``
285313

286-
``EXCLUDES`` is an attribute on functions or methods, which declares that
314+
``EXCLUDES`` is an attribute on functions, methods or function parameters
315+
of reference to :ref:`scoped_capability`-annotated type, which declares that
287316
the caller must *not* hold the given capabilities. This annotation is
288317
used to prevent deadlock. Many mutex implementations are not re-entrant, so
289318
deadlock can occur if the function acquires the mutex a second time.
319+
Additionally, if the attribute is on a function parameter, it declares that
320+
the scoped capability manages the specified capabilities in the given order.
290321

291322
.. code-block:: c++
292323

@@ -305,6 +336,16 @@ deadlock can occur if the function acquires the mutex a second time.
305336
mu.Unlock();
306337
}
307338

339+
void exclude(MutexLocker& scope LOCKS_EXCLUDED(mu)) {
340+
scope.Unlock(); // Warning! mu is not locked.
341+
scope.Lock();
342+
} // Warning! mu still held at the end of function.
343+
344+
void testParameter() {
345+
MutexLocker scope(&mu);
346+
exclude(scope); // Warning, mu is held.
347+
}
348+
308349
Unlike ``REQUIRES``, ``EXCLUDES`` is optional. The analysis will not issue a
309350
warning if the attribute is missing, which can lead to false negatives in some
310351
cases. This issue is discussed further in :ref:`negative`.
@@ -393,6 +434,7 @@ class can be used as a capability. The string argument specifies the kind of
393434
capability in error messages, e.g. ``"mutex"``. See the ``Container`` example
394435
given above, or the ``Mutex`` class in :ref:`mutexheader`.
395436

437+
.. _scoped_capability:
396438

397439
SCOPED_CAPABILITY
398440
-----------------

clang/include/clang/Analysis/Analyses/ThreadSafety.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,42 @@ class ThreadSafetyHandler {
223223
virtual void handleFunExcludesLock(StringRef Kind, Name FunName,
224224
Name LockName, SourceLocation Loc) {}
225225

226+
/// Warn when an actual underlying mutex of a scoped lockable does not match
227+
/// the expected.
228+
/// \param Loc -- The location of the call expression.
229+
/// \param DLoc -- The location of the function declaration.
230+
/// \param ScopeName -- The name of the scope passed to the function.
231+
/// \param Kind -- The kind of the expected mutex.
232+
/// \param Expected -- The name of the expected mutex.
233+
/// \param Actual -- The name of the actual mutex.
234+
virtual void handleUnmatchedUnderlyingMutexes(SourceLocation Loc,
235+
SourceLocation DLoc,
236+
Name ScopeName, StringRef Kind,
237+
Name Expected, Name Actual) {}
238+
239+
/// Warn when we get fewer underlying mutexes than expected.
240+
/// \param Loc -- The location of the call expression.
241+
/// \param DLoc -- The location of the function declaration.
242+
/// \param ScopeName -- The name of the scope passed to the function.
243+
/// \param Kind -- The kind of the expected mutex.
244+
/// \param Expected -- The name of the expected mutex.
245+
virtual void handleExpectMoreUnderlyingMutexes(SourceLocation Loc,
246+
SourceLocation DLoc,
247+
Name ScopeName, StringRef Kind,
248+
Name Expected) {}
249+
250+
/// Warn when we get more underlying mutexes than expected.
251+
/// \param Loc -- The location of the call expression.
252+
/// \param DLoc -- The location of the function declaration.
253+
/// \param ScopeName -- The name of the scope passed to the function.
254+
/// \param Kind -- The kind of the actual mutex.
255+
/// \param Actual -- The name of the actual mutex.
256+
virtual void handleExpectFewerUnderlyingMutexes(SourceLocation Loc,
257+
SourceLocation DLoc,
258+
Name ScopeName,
259+
StringRef Kind, Name Actual) {
260+
}
261+
226262
/// Warn that L1 cannot be acquired before L2.
227263
virtual void handleLockAcquiredBefore(StringRef Kind, Name L1Name,
228264
Name L2Name, SourceLocation Loc) {}

clang/include/clang/Basic/Attr.td

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3763,7 +3763,7 @@ def AcquireCapability : InheritableAttr {
37633763
Clang<"acquire_shared_capability", 0>,
37643764
GNU<"exclusive_lock_function">,
37653765
GNU<"shared_lock_function">];
3766-
let Subjects = SubjectList<[Function]>;
3766+
let Subjects = SubjectList<[Function, ParmVar]>;
37673767
let LateParsed = LateAttrParseStandard;
37683768
let TemplateDependent = 1;
37693769
let ParseArgumentsAsUnevaluated = 1;
@@ -3795,7 +3795,7 @@ def ReleaseCapability : InheritableAttr {
37953795
Clang<"release_shared_capability", 0>,
37963796
Clang<"release_generic_capability", 0>,
37973797
Clang<"unlock_function", 0>];
3798-
let Subjects = SubjectList<[Function]>;
3798+
let Subjects = SubjectList<[Function, ParmVar]>;
37993799
let LateParsed = LateAttrParseStandard;
38003800
let TemplateDependent = 1;
38013801
let ParseArgumentsAsUnevaluated = 1;
@@ -3819,7 +3819,7 @@ def RequiresCapability : InheritableAttr {
38193819
let TemplateDependent = 1;
38203820
let ParseArgumentsAsUnevaluated = 1;
38213821
let InheritEvenIfAlreadyPresent = 1;
3822-
let Subjects = SubjectList<[Function]>;
3822+
let Subjects = SubjectList<[Function, ParmVar]>;
38233823
let Accessors = [Accessor<"isShared", [Clang<"requires_shared_capability", 0>,
38243824
Clang<"shared_locks_required", 0>]>];
38253825
let Documentation = [Undocumented];
@@ -3941,7 +3941,7 @@ def LocksExcluded : InheritableAttr {
39413941
let TemplateDependent = 1;
39423942
let ParseArgumentsAsUnevaluated = 1;
39433943
let InheritEvenIfAlreadyPresent = 1;
3944-
let Subjects = SubjectList<[Function]>;
3944+
let Subjects = SubjectList<[Function, ParmVar]>;
39453945
let Documentation = [Undocumented];
39463946
}
39473947

clang/include/clang/Basic/DiagnosticSemaKinds.td

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3999,6 +3999,10 @@ def warn_thread_attribute_decl_not_lockable : Warning<
39993999
def warn_thread_attribute_decl_not_pointer : Warning<
40004000
"%0 only applies to pointer types; type here is %1">,
40014001
InGroup<ThreadSafetyAttributes>, DefaultIgnore;
4002+
def warn_thread_attribute_not_on_scoped_lockable_param : Warning<
4003+
"%0 attribute applies to function parameters only if their type is a "
4004+
"reference to a 'scoped_lockable'-annotated type">,
4005+
InGroup<ThreadSafetyAttributes>, DefaultIgnore;
40024006
def err_attribute_argument_out_of_bounds_extra_info : Error<
40034007
"%0 attribute parameter %1 is out of bounds: "
40044008
"%plural{0:no parameters to index into|"
@@ -4054,6 +4058,17 @@ def warn_acquired_before : Warning<
40544058
def warn_acquired_before_after_cycle : Warning<
40554059
"cycle in acquired_before/after dependencies, starting with '%0'">,
40564060
InGroup<ThreadSafetyAnalysis>, DefaultIgnore;
4061+
def warn_unmatched_underlying_mutexes : Warning<
4062+
"%0 managed by '%1' is '%3' instead of '%2'">,
4063+
InGroup<ThreadSafetyAnalysis>, DefaultIgnore;
4064+
def warn_expect_more_underlying_mutexes : Warning<
4065+
"%0 '%2' not managed by '%1'">,
4066+
InGroup<ThreadSafetyAnalysis>, DefaultIgnore;
4067+
def warn_expect_fewer_underlying_mutexes : Warning<
4068+
"did not expect %0 '%2' to be managed by '%1'">,
4069+
InGroup<ThreadSafetyAnalysis>, DefaultIgnore;
4070+
def note_managed_mismatch_here_for_param : Note<
4071+
"see attribute on parameter here">;
40574072

40584073

40594074
// Thread safety warnings negative capabilities

0 commit comments

Comments
 (0)