Skip to content

Thread Safety Analysis: Support passing scoped locks between functions with appropriate annotations #110523

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

malek203
Copy link
Contributor

@malek203 malek203 commented Sep 30, 2024

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 boundaries.
  • 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.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:analysis labels Sep 30, 2024
@llvmbot
Copy link
Member

llvmbot commented Sep 30, 2024

@llvm/pr-subscribers-clang-analysis

@llvm/pr-subscribers-clang

Author: Malek Ben Slimane (malek203)

Changes

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.

Patch is 47.05 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/110523.diff

9 Files Affected:

  • (modified) clang/docs/ThreadSafetyAnalysis.rst (+47-4)
  • (modified) clang/include/clang/Analysis/Analyses/ThreadSafety.h (+35)
  • (modified) clang/include/clang/Basic/Attr.td (+4-4)
  • (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+15)
  • (modified) clang/lib/Analysis/ThreadSafety.cpp (+164-9)
  • (modified) clang/lib/Sema/AnalysisBasedWarnings.cpp (+39)
  • (modified) clang/lib/Sema/SemaDeclAttr.cpp (+40)
  • (modified) clang/test/SemaCXX/warn-thread-safety-analysis.cpp (+366)
  • (modified) clang/test/SemaCXX/warn-thread-safety-parsing.cpp (+30-12)
diff --git a/clang/docs/ThreadSafetyAnalysis.rst b/clang/docs/ThreadSafetyAnalysis.rst
index cc4089b97b4923..6c513a52277cee 100644
--- a/clang/docs/ThreadSafetyAnalysis.rst
+++ b/clang/docs/ThreadSafetyAnalysis.rst
@@ -187,10 +187,13 @@ REQUIRES(...), REQUIRES_SHARED(...)
 
 *Previously*: ``EXCLUSIVE_LOCKS_REQUIRED``, ``SHARED_LOCKS_REQUIRED``
 
-``REQUIRES`` is an attribute on functions or methods, which
+``REQUIRES`` is an attribute on functions, methods or function parameters of
+reference to :ref:`scoped_capability`-annotated type, which
 declares that the calling thread must have exclusive access to the given
 capabilities.  More than one capability may be specified.  The capabilities
 must be held on entry to the function, *and must still be held on exit*.
+Additionally, if the attribute is on a function parameter, it declares that
+the scoped capability manages the specified capabilities in the given order.
 
 ``REQUIRES_SHARED`` is similar, but requires only shared access.
 
@@ -211,6 +214,21 @@ must be held on entry to the function, *and must still be held on exit*.
     mu1.Unlock();
   }
 
+  void require(MutexLocker& scope REQUIRES(mu1)) {
+    scope.Unlock();
+    a=0; // Warning!  Requires mu1.
+    scope.Lock();
+  }
+
+  void testParameter() {
+    MutexLocker scope(&mu1);
+    MutexLocker scope2(&mu2);
+    require(scope2); // Warning! Mutex managed by 'scope' is 'mu2' instead of 'mu1'
+    require(scope); // OK.
+    scope.Unlock();
+    require(scope); // Warning!  Requires mu1.
+  }
+
 
 ACQUIRE(...), ACQUIRE_SHARED(...), RELEASE(...), RELEASE_SHARED(...), RELEASE_GENERIC(...)
 ------------------------------------------------------------------------------------------
@@ -218,10 +236,13 @@ ACQUIRE(...), ACQUIRE_SHARED(...), RELEASE(...), RELEASE_SHARED(...), RELEASE_GE
 *Previously*: ``EXCLUSIVE_LOCK_FUNCTION``, ``SHARED_LOCK_FUNCTION``,
 ``UNLOCK_FUNCTION``
 
-``ACQUIRE`` and ``ACQUIRE_SHARED`` are attributes on functions or methods
-declaring that the function acquires a capability, but does not release it.
+``ACQUIRE`` and ``ACQUIRE_SHARED`` are attributes on functions, methods
+or function parameters of reference to :ref:`scoped_capability`-annotated type,
+which declare that the function acquires a capability, but does not release it.
 The given capability must not be held on entry, and will be held on exit
 (exclusively for ``ACQUIRE``, shared for ``ACQUIRE_SHARED``).
+Additionally, if the attribute is on a function parameter, it declares that
+the scoped capability manages the specified capabilities in the given order.
 
 ``RELEASE``, ``RELEASE_SHARED``, and ``RELEASE_GENERIC`` declare that the
 function releases the given capability.  The capability must be held on entry
@@ -249,6 +270,14 @@ shared for ``RELEASE_GENERIC``), and will no longer be held on exit.
     myObject.doSomething();  // Warning, mu is not locked.
   }
 
+  void release(MutexLocker& scope RELEASE(mu)) {
+  }                          // Warning!  Need to unlock mu.
+
+  void testParameter() {
+    MutexLocker scope(&mu);
+    release(scope);
+  }
+
 If no argument is passed to ``ACQUIRE`` or ``RELEASE``, then the argument is
 assumed to be ``this``, and the analysis will not check the body of the
 function.  This pattern is intended for use by classes which hide locking
@@ -283,10 +312,13 @@ EXCLUDES(...)
 
 *Previously*: ``LOCKS_EXCLUDED``
 
-``EXCLUDES`` is an attribute on functions or methods, which declares that
+``EXCLUDES`` is an attribute on functions, methods or function parameters
+of reference to :ref:`scoped_capability`-annotated type, which declares that
 the caller must *not* hold the given capabilities.  This annotation is
 used to prevent deadlock.  Many mutex implementations are not re-entrant, so
 deadlock can occur if the function acquires the mutex a second time.
+Additionally, if the attribute is on a function parameter, it declares that
+the scoped capability manages the specified capabilities in the given order.
 
 .. code-block:: c++
 
@@ -305,6 +337,16 @@ deadlock can occur if the function acquires the mutex a second time.
     mu.Unlock();
   }
 
+  void exclude(MutexLocker& scope LOCKS_EXCLUDED(mu)){
+    scope.Unlock(); // Warning! mu is not locked.
+    scope.Lock();
+  } // Warning! mu still held at the end of function.
+
+  void testParameter() {
+    MutexLocker scope(&mu);
+    exclude(scope); // Warning, mu is held.
+  }
+
 Unlike ``REQUIRES``, ``EXCLUDES`` is optional.  The analysis will not issue a
 warning if the attribute is missing, which can lead to false negatives in some
 cases.  This issue is discussed further in :ref:`negative`.
@@ -393,6 +435,7 @@ class can be used as a capability.  The string argument specifies the kind of
 capability in error messages, e.g. ``"mutex"``.  See the ``Container`` example
 given above, or the ``Mutex`` class in :ref:`mutexheader`.
 
+.. _scoped_capability:
 
 SCOPED_CAPABILITY
 -----------------
diff --git a/clang/include/clang/Analysis/Analyses/ThreadSafety.h b/clang/include/clang/Analysis/Analyses/ThreadSafety.h
index 0866b09bab2995..591891b07ce963 100644
--- a/clang/include/clang/Analysis/Analyses/ThreadSafety.h
+++ b/clang/include/clang/Analysis/Analyses/ThreadSafety.h
@@ -223,6 +223,41 @@ class ThreadSafetyHandler {
   virtual void handleFunExcludesLock(StringRef Kind, Name FunName,
                                      Name LockName, SourceLocation Loc) {}
 
+  /// Warn when an actual underlying mutex of a scoped lockable does not match
+  /// the expected.
+  /// \param Loc -- The location of the call expression.
+  /// \param DLoc -- The location of the function declaration.
+  /// \param ScopeName -- The name of the scope passed to the function.
+  /// \param Kind -- The kind of the expected mutex.
+  /// \param Expected -- The name of the expected mutex.
+  /// \param Actual -- The name of the actual mutex.
+  virtual void handleUnmatchedUnderlyingMutexes(SourceLocation Loc,
+                                                SourceLocation DLoc,
+                                                Name ScopeName, StringRef Kind,
+                                                Name Expected, Name Actual) {}
+
+  /// Warn when we get fewer underlying mutexes than expected.
+  /// \param Loc -- The location of the call expression.
+  /// \param DLoc -- The location of the function declaration.
+  /// \param ScopeName -- The name of the scope passed to the function.
+  /// \param Kind -- The kind of the expected mutex.
+  /// \param Expected -- The name of the expected mutex.
+  virtual void handleExpectMoreUnderlyingMutexes(SourceLocation Loc,
+                                                 SourceLocation DLoc,
+                                                 Name ScopeName, StringRef Kind,
+                                                 Name Expected) {}
+
+  /// Warn when we get more underlying mutexes than expected.
+  /// \param Loc -- The location of the call expression.
+  /// \param DLoc -- The location of the function declaration.
+  /// \param ScopeName -- The name of the scope passed to the function.
+  /// \param Kind -- The kind of the actual mutex.
+  /// \param Actual -- The name of the actual mutex.
+  virtual void handleExpectFewerUnderlyingMutexes(SourceLocation Loc,
+                                                  SourceLocation DLoc,
+                                                  Name ScopeName,
+                                                  StringRef Kind, Name Actual) {}
+
   /// Warn that L1 cannot be acquired before L2.
   virtual void handleLockAcquiredBefore(StringRef Kind, Name L1Name,
                                         Name L2Name, SourceLocation Loc) {}
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index ce86116680d7a3..c6f5a394829691 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -3685,7 +3685,7 @@ def AcquireCapability : InheritableAttr {
                    Clang<"acquire_shared_capability", 0>,
                    GNU<"exclusive_lock_function">,
                    GNU<"shared_lock_function">];
-  let Subjects = SubjectList<[Function]>;
+  let Subjects = SubjectList<[Function, ParmVar]>;
   let LateParsed = LateAttrParseStandard;
   let TemplateDependent = 1;
   let ParseArgumentsAsUnevaluated = 1;
@@ -3717,7 +3717,7 @@ def ReleaseCapability : InheritableAttr {
                    Clang<"release_shared_capability", 0>,
                    Clang<"release_generic_capability", 0>,
                    Clang<"unlock_function", 0>];
-  let Subjects = SubjectList<[Function]>;
+  let Subjects = SubjectList<[Function, ParmVar]>;
   let LateParsed = LateAttrParseStandard;
   let TemplateDependent = 1;
   let ParseArgumentsAsUnevaluated = 1;
@@ -3741,7 +3741,7 @@ def RequiresCapability : InheritableAttr {
   let TemplateDependent = 1;
   let ParseArgumentsAsUnevaluated = 1;
   let InheritEvenIfAlreadyPresent = 1;
-  let Subjects = SubjectList<[Function]>;
+  let Subjects = SubjectList<[Function, ParmVar]>;
   let Accessors = [Accessor<"isShared", [Clang<"requires_shared_capability", 0>,
                                          Clang<"shared_locks_required", 0>]>];
   let Documentation = [Undocumented];
@@ -3863,7 +3863,7 @@ def LocksExcluded : InheritableAttr {
   let TemplateDependent = 1;
   let ParseArgumentsAsUnevaluated = 1;
   let InheritEvenIfAlreadyPresent = 1;
-  let Subjects = SubjectList<[Function]>;
+  let Subjects = SubjectList<[Function, ParmVar]>;
   let Documentation = [Undocumented];
 }
 
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index e4e04bff8b5120..34e7a5ee0afb37 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -3984,6 +3984,10 @@ def warn_thread_attribute_decl_not_lockable : Warning<
 def warn_thread_attribute_decl_not_pointer : Warning<
   "%0 only applies to pointer types; type here is %1">,
   InGroup<ThreadSafetyAttributes>, DefaultIgnore;
+def warn_thread_attribute_not_on_scoped_lockable_param : Warning<
+  "%0 attribute applies to function parameters only if their type is a "
+  "reference to a 'scoped_lockable'-annotated type">,
+  InGroup<ThreadSafetyAttributes>, DefaultIgnore;
 def err_attribute_argument_out_of_bounds_extra_info : Error<
   "%0 attribute parameter %1 is out of bounds: "
   "%plural{0:no parameters to index into|"
@@ -4039,6 +4043,17 @@ def warn_acquired_before : Warning<
 def warn_acquired_before_after_cycle : Warning<
   "cycle in acquired_before/after dependencies, starting with '%0'">,
   InGroup<ThreadSafetyAnalysis>, DefaultIgnore;
+def warn_unmatched_underlying_mutexes : Warning<
+  "%0 managed by '%1' is '%3' instead of '%2'">,
+  InGroup<ThreadSafetyAnalysis>, DefaultIgnore;
+def warn_expect_more_underlying_mutexes : Warning<
+  "%0 '%2' not managed by '%1'">,
+  InGroup<ThreadSafetyAnalysis>, DefaultIgnore;
+def warn_expect_fewer_underlying_mutexes : Warning<
+  "did not expect %0 '%2' to be managed by '%1'">,
+  InGroup<ThreadSafetyAnalysis>, DefaultIgnore;
+def note_managed_mismatch_here_for_param : Note<
+  "see attribute on parameter here">;
 
 
 // Thread safety warnings negative capabilities
diff --git a/clang/lib/Analysis/ThreadSafety.cpp b/clang/lib/Analysis/ThreadSafety.cpp
index 5577f45aa5217f..f40a4f5f398b50 100644
--- a/clang/lib/Analysis/ThreadSafety.cpp
+++ b/clang/lib/Analysis/ThreadSafety.cpp
@@ -103,6 +103,8 @@ class FactSet;
 /// FIXME: this analysis does not currently support re-entrant locking.
 class FactEntry : public CapabilityExpr {
 public:
+  enum FactEntryKind { Lockable, ScopedLockable };
+
   /// Where a fact comes from.
   enum SourceKind {
     Acquired, ///< The fact has been directly acquired.
@@ -112,6 +114,8 @@ class FactEntry : public CapabilityExpr {
   };
 
 private:
+  const FactEntryKind Kind : 8;
+
   /// Exclusive or shared.
   LockKind LKind : 8;
 
@@ -122,13 +126,14 @@ class FactEntry : public CapabilityExpr {
   SourceLocation AcquireLoc;
 
 public:
-  FactEntry(const CapabilityExpr &CE, LockKind LK, SourceLocation Loc,
+  FactEntry(FactEntryKind FK, const CapabilityExpr &CE, LockKind LK, SourceLocation Loc,
             SourceKind Src)
-      : CapabilityExpr(CE), LKind(LK), Source(Src), AcquireLoc(Loc) {}
+      : CapabilityExpr(CE), Kind(FK), LKind(LK), Source(Src), AcquireLoc(Loc) {}
   virtual ~FactEntry() = default;
 
   LockKind kind() const { return LKind;      }
   SourceLocation loc() const { return AcquireLoc; }
+  FactEntryKind getFactEntryKind() const { return Kind; }
 
   bool asserted() const { return Source == Asserted; }
   bool declared() const { return Source == Declared; }
@@ -857,7 +862,7 @@ class LockableFactEntry : public FactEntry {
 public:
   LockableFactEntry(const CapabilityExpr &CE, LockKind LK, SourceLocation Loc,
                     SourceKind Src = Acquired)
-      : FactEntry(CE, LK, Loc, Src) {}
+      : FactEntry(Lockable, CE, LK, Loc, Src) {}
 
   void
   handleRemovalFromIntersection(const FactSet &FSet, FactManager &FactMan,
@@ -885,6 +890,10 @@ class LockableFactEntry : public FactEntry {
                                 !Cp, LK_Exclusive, UnlockLoc));
     }
   }
+
+  static bool classof(const FactEntry *A) {
+    return A->getFactEntryKind() == Lockable;
+  }
 };
 
 class ScopedLockableFactEntry : public FactEntry {
@@ -903,8 +912,16 @@ class ScopedLockableFactEntry : public FactEntry {
   SmallVector<UnderlyingCapability, 2> UnderlyingMutexes;
 
 public:
-  ScopedLockableFactEntry(const CapabilityExpr &CE, SourceLocation Loc)
-      : FactEntry(CE, LK_Exclusive, Loc, Acquired) {}
+  ScopedLockableFactEntry(const CapabilityExpr &CE, SourceLocation Loc,
+                          SourceKind Src)
+      : FactEntry(ScopedLockable, CE, LK_Exclusive, Loc, Src) {}
+
+  CapExprSet getUnderlyingMutexes() const {
+    CapExprSet UnderlyingMutexesSet;
+    for (const UnderlyingCapability &UnderlyingMutex : UnderlyingMutexes)
+      UnderlyingMutexesSet.push_back(UnderlyingMutex.Cap);
+    return UnderlyingMutexesSet;
+  }
 
   void addLock(const CapabilityExpr &M) {
     UnderlyingMutexes.push_back(UnderlyingCapability{M, UCK_Acquired});
@@ -971,6 +988,10 @@ class ScopedLockableFactEntry : public FactEntry {
       FSet.removeLock(FactMan, Cp);
   }
 
+  static bool classof(const FactEntry *A) {
+    return A->getFactEntryKind() == ScopedLockable;
+  }
+
 private:
   void lock(FactSet &FSet, FactManager &FactMan, const CapabilityExpr &Cp,
             LockKind kind, SourceLocation loc,
@@ -1806,7 +1827,7 @@ void BuildLockset::handleCall(const Expr *Exp, const NamedDecl *D,
   if (Exp) {
     assert(!Self);
     const auto *TagT = Exp->getType()->getAs<TagType>();
-    if (TagT && Exp->isPRValue()) {
+    if (D->hasAttrs() && TagT && Exp->isPRValue()) {
       std::pair<til::LiteralPtr *, StringRef> Placeholder =
           Analyzer->SxBuilder.createThisPlaceholder(Exp);
       [[maybe_unused]] auto inserted =
@@ -1915,6 +1936,101 @@ void BuildLockset::handleCall(const Expr *Exp, const NamedDecl *D,
     }
   }
 
+  std::optional<CallExpr::const_arg_range> Args;
+  if (Exp) {
+    if (const auto *CE = dyn_cast<CallExpr>(Exp))
+      Args = CE->arguments();
+    else if (const auto *CE = dyn_cast<CXXConstructExpr>(Exp))
+      Args = CE->arguments();
+    else
+      llvm_unreachable("Unknown call kind");
+  }
+  const FunctionDecl *CalledFunction = dyn_cast<FunctionDecl>(D);
+  if (CalledFunction && Args.has_value()) {
+    for (auto [Param, Arg] : zip(CalledFunction->parameters(), *Args)) {
+      CapExprSet DeclaredLocks;
+      for (const Attr *At : Param->attrs()) {
+        switch (At->getKind()) {
+        case attr::AcquireCapability: {
+          const auto *A = cast<AcquireCapabilityAttr>(At);
+          Analyzer->getMutexIDs(A->isShared() ? SharedLocksToAdd
+                                              : ExclusiveLocksToAdd,
+                                A, Exp, D, Self);
+          Analyzer->getMutexIDs(DeclaredLocks, A, nullptr, Param);
+          break;
+        }
+
+        case attr::ReleaseCapability: {
+          const auto *A = cast<ReleaseCapabilityAttr>(At);
+          if (A->isGeneric())
+            Analyzer->getMutexIDs(GenericLocksToRemove, A, Exp, D, Self);
+          else if (A->isShared())
+            Analyzer->getMutexIDs(SharedLocksToRemove, A, Exp, D, Self);
+          else
+            Analyzer->getMutexIDs(ExclusiveLocksToRemove, A, Exp, D, Self);
+          Analyzer->getMutexIDs(DeclaredLocks, A, nullptr, Param);
+          break;
+        }
+
+        case attr::RequiresCapability: {
+          const auto *A = cast<RequiresCapabilityAttr>(At);
+          for (auto *Arg : A->args())
+            Analyzer->warnIfMutexNotHeld(FSet, D, Exp,
+                                         A->isShared() ? AK_Read : AK_Written,
+                                         Arg, POK_FunctionCall, Self, Loc);
+          Analyzer->getMutexIDs(DeclaredLocks, A, nullptr, Param);
+          break;
+        }
+
+        case attr::LocksExcluded: {
+          const auto *A = cast<LocksExcludedAttr>(At);
+          for (auto *Arg : A->args())
+            Analyzer->warnIfMutexHeld(FSet, D, Exp, Arg, Self, Loc);
+          Analyzer->getMutexIDs(DeclaredLocks, A, nullptr, Param);
+          break;
+        }
+
+        default:
+          break;
+        }
+      }
+      if (DeclaredLocks.size() == 0)
+        continue;
+      CapabilityExpr Cp = Analyzer->SxBuilder.translateAttrExpr(Arg, nullptr);
+      if (const auto *CBTE = dyn_cast<CXXBindTemporaryExpr>(Arg->IgnoreCasts());
+          Cp.isInvalid() && CBTE) {
+        if (auto Object = Analyzer->ConstructedObjects.find(CBTE->getSubExpr());
+            Object != Analyzer->ConstructedObjects.end())
+          Cp = CapabilityExpr(Object->second, StringRef(), false);
+      }
+      const FactEntry *Fact = FSet.findLock(Analyzer->FactMan, Cp);
+      if (!Fact) {
+        Analyzer->Handler.handleMutexNotHeld(Cp.getKind(), D, POK_FunctionCall,
+                                             Cp.toString(), LK_Exclusive,
+                                             Exp->getExprLoc());
+        continue;
+      }
+      const auto *Scope =
+          cast<ScopedLockableFactEntry>(Fact);
+      for (const auto &[a, b] :
+           zip_longest(DeclaredLocks, Scope->getUnderlyingMutexes())) {
+        if (!a.has_value()) {
+          Analyzer->Handler.handleExpectFewerUnderlyingMutexes(
+              Exp->getExprLoc(), D->getLocation(), Scope->toString(),
+              b.value().getKind(), b.value().toString());
+        } else if (!b.has_value()) {
+          Analyzer->Handler.handleExpectMoreUnderlyingMutexes(
+              Exp->getExprLoc(), D->getLocation(), Scope->toString(),
+              a.value().getKind(), a.value().toString());
+        } else if (!a.value().matches(b.value())) {
+          Analyzer->Handler.handleUnmatchedUnderlyingMutexes(
+              Exp->getExprLoc(), D->getLocation(), Scope->toString(),
+              a.value().getKind(), a.value().toString(), b.value().toString());
+          break;
+        }
+      }
+    }
+  }
   // Remove locks first to allow lock upgrading/downgrading.
   // FIXME -- should only fully remove if the attribute refers to 'this'.
   bool Dtor = isa<CXXDestructorDecl>(D);
@@ -1937,7 +2053,8 @@ void BuildLockset::handleCall(const Expr *Exp, const NamedDecl *D,
 
   if (!Scp.shouldIgnore()) {
     // Add the managing object as a dummy mutex, mapped to the underlying mutex.
-    auto ScopedEntry = std::make_unique<ScopedLockableFactEntry>(Scp, Loc);
+    auto ScopedEntry = std::make_unique<ScopedLockableFactEntry>(
+        Scp, Loc, FactEntry::Acquired);
     for (const auto &M : ExclusiveLocksToAdd)
       ScopedEntry->addLock(M);
     for (const auto &M : SharedLocksToAdd)
@@ -2084,7 +2201,7 @@ void BuildLockset::VisitCallExpr(const CallExpr *Exp) {
   }
 
   auto *D = dyn_cast_or_null<NamedDecl>(Exp->getCalleeDecl());
-  if(!D || !D->hasAttrs())
+  if (!D)
     return;
   handleCall(Exp, D);
 }
@@ -2324,7 +2441,7 @@ void ThreadSafetyAnalyzer::runAnalysis(AnalysisDeclContext &AC) {
   // Add locks from exclusive_locks_required and shared_locks_required
   // to initial lockset. Also turn off checking for lock and unlock functions.
   // FIXME: is there a more intelligent way to check lock/unlock functions?
...
[truncated]

Copy link

github-actions bot commented Sep 30, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

@aaronpuchert
Copy link
Member

@delesley, your input on the conceptual side would also be appreciated as this is a substantial addition.

Copy link
Member

@aaronpuchert aaronpuchert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more idea for a test: can a parameter attribute reference another parameter? Then we could try something like

struct ObjectWithMutex { Mutex mu; };
void releaseMember(ObjectWithMutex& object, ReleasableMutexLock& scope EXCLUSIVE_UNLOCK_FUNCTION(object.mu));

Just to ensure we're parsing the attribute in the right context. My understanding is that the attributes can currently not reference this due to the mentioned parser issue, so we can't test that.

@malek203 malek203 force-pushed the support-passing-scoped-locks-between-functions-with-appropriate-annotations branch from f669df2 to b66684c Compare October 3, 2024 11:15
@malek203 malek203 force-pushed the support-passing-scoped-locks-between-functions-with-appropriate-annotations branch from b66684c to 2f4754d Compare October 18, 2024 09:33
Copy link
Member

@aaronpuchert aaronpuchert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! Let's wait a bit for the other reviewers, but otherwise I'm ready to merge it.

Copy link
Collaborator

@AaronBallman AaronBallman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, please add a release note to clang/docs/ReleaseNotes.rst so users know about the new functionality. Thank you for working on this!

@malek203 malek203 force-pushed the support-passing-scoped-locks-between-functions-with-appropriate-annotations branch from 2f4754d to 70b03bd Compare November 29, 2024 15:30
Copy link
Member

@aaronpuchert aaronpuchert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AaronBallman, please also have a look at the Release Note.

@malek203 malek203 force-pushed the support-passing-scoped-locks-between-functions-with-appropriate-annotations branch from 70b03bd to 07b0285 Compare December 4, 2024 15:39
@malek203 malek203 force-pushed the support-passing-scoped-locks-between-functions-with-appropriate-annotations branch from 07b0285 to d260b09 Compare December 4, 2024 20:35
…s with appropriate annotations

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.
@malek203 malek203 force-pushed the support-passing-scoped-locks-between-functions-with-appropriate-annotations branch from d260b09 to f5b5d42 Compare December 9, 2024 18:31
aaronpuchert pushed a commit that referenced this pull request Dec 20, 2024
…s 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.
@aaronpuchert
Copy link
Member

Submitted as c1e7e45.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:analysis clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants