Skip to content
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

Thread safety analysis: Fix a bug in handling temporary constructors #74020

Merged
merged 1 commit into from
Dec 8, 2023

Conversation

ziqingluo-90
Copy link
Contributor

The commit 54bfd04 introduces general handling of constructor and destructor calls. Consider a pair of TEMPORARY constructor and destructor calls of a "SCOPED_CAPABILITY" object. At the constructor call, a resource representing the object itself is added to the state. But since it is a temporary object, the analyzer may not be able to find an AST part to identify the resource (As opposed to the normal case of using VarDecl as an identifier). Later at the destructor call, the resource needs to be removed from the state. Since the resource cannot be identified by anything other than itself, the analyzer maintains a map from constructor calls to their created resources so that the analyzer could get the proper resource for removal at the destructor call via look up the map. The issue is that the map lives within a CFG block. That means in case the pair of temporary constructor and destructor calls are not in one CFG block, the analyzer is not able to remove the proper resource at the destructor call. Although the two calls stay in one CFG block usually, they separate in the following example:

if (f(MutexLock{lock}) || x) { // <-- the temporary Ctor and Dtor of MutexLock are in different CFG blocks

}

The fix is simple. It extends the life of the map to that of the entire CFG.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:analysis labels Dec 1, 2023
@llvmbot
Copy link
Collaborator

llvmbot commented Dec 1, 2023

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-analysis

Author: Ziqing Luo (ziqingluo-90)

Changes

The commit 54bfd04 introduces general handling of constructor and destructor calls. Consider a pair of TEMPORARY constructor and destructor calls of a "SCOPED_CAPABILITY" object. At the constructor call, a resource representing the object itself is added to the state. But since it is a temporary object, the analyzer may not be able to find an AST part to identify the resource (As opposed to the normal case of using VarDecl as an identifier). Later at the destructor call, the resource needs to be removed from the state. Since the resource cannot be identified by anything other than itself, the analyzer maintains a map from constructor calls to their created resources so that the analyzer could get the proper resource for removal at the destructor call via look up the map. The issue is that the map lives within a CFG block. That means in case the pair of temporary constructor and destructor calls are not in one CFG block, the analyzer is not able to remove the proper resource at the destructor call. Although the two calls stay in one CFG block usually, they separate in the following example:

if (f(MutexLock{lock}) || x) { // &lt;-- the temporary Ctor and Dtor of MutexLock are in different CFG blocks

}

The fix is simple. It extends the life of the map to that of the entire CFG.


Full diff: https://github.com/llvm/llvm-project/pull/74020.diff

2 Files Affected:

  • (modified) clang/lib/Analysis/ThreadSafety.cpp (+13-5)
  • (modified) clang/test/SemaCXX/warn-thread-safety-analysis.cpp (+9)
diff --git a/clang/lib/Analysis/ThreadSafety.cpp b/clang/lib/Analysis/ThreadSafety.cpp
index 7fdf22c2f3919cb..9549d1b398dfa08 100644
--- a/clang/lib/Analysis/ThreadSafety.cpp
+++ b/clang/lib/Analysis/ThreadSafety.cpp
@@ -1530,7 +1530,7 @@ void ThreadSafetyAnalyzer::getEdgeLockset(FactSet& Result,
 }
 
 namespace {
-
+using ConstructedObjectMapTy = llvm::SmallDenseMap<const Expr *, til::LiteralPtr *>;
 /// We use this class to visit different types of expressions in
 /// CFGBlocks, and build up the lockset.
 /// An expression may cause us to add or remove locks from the lockset, or else
@@ -1544,7 +1544,10 @@ class BuildLockset : public ConstStmtVisitor<BuildLockset> {
   // The fact set for the function on exit.
   const FactSet &FunctionExitFSet;
   /// Maps constructed objects to `this` placeholder prior to initialization.
-  llvm::SmallDenseMap<const Expr *, til::LiteralPtr *> ConstructedObjects;
+  /// The map lives longer than this builder so this is a reference to an
+  /// existing one. The map lives so long as the CFG while this builder is for
+  /// one CFG block.
+  ConstructedObjectMapTy &ConstructedObjects;
   LocalVariableMap::Context LVarCtx;
   unsigned CtxIndex;
 
@@ -1569,9 +1572,11 @@ class BuildLockset : public ConstStmtVisitor<BuildLockset> {
 
 public:
   BuildLockset(ThreadSafetyAnalyzer *Anlzr, CFGBlockInfo &Info,
-               const FactSet &FunctionExitFSet)
+               const FactSet &FunctionExitFSet,
+               ConstructedObjectMapTy &ConstructedObjects)
       : ConstStmtVisitor<BuildLockset>(), Analyzer(Anlzr), FSet(Info.EntrySet),
-        FunctionExitFSet(FunctionExitFSet), LVarCtx(Info.EntryContext),
+        FunctionExitFSet(FunctionExitFSet),
+        ConstructedObjects(ConstructedObjects), LVarCtx(Info.EntryContext),
         CtxIndex(Info.EntryIndex) {}
 
   void VisitUnaryOperator(const UnaryOperator *UO);
@@ -2392,6 +2397,8 @@ void ThreadSafetyAnalyzer::runAnalysis(AnalysisDeclContext &AC) {
   for (const auto &Lock : LocksReleased)
     ExpectedFunctionExitSet.removeLock(FactMan, Lock);
 
+  ConstructedObjectMapTy ConstructedObjects;
+
   for (const auto *CurrBlock : *SortedGraph) {
     unsigned CurrBlockID = CurrBlock->getBlockID();
     CFGBlockInfo *CurrBlockInfo = &BlockInfo[CurrBlockID];
@@ -2451,7 +2458,8 @@ void ThreadSafetyAnalyzer::runAnalysis(AnalysisDeclContext &AC) {
     if (!CurrBlockInfo->Reachable)
       continue;
 
-    BuildLockset LocksetBuilder(this, *CurrBlockInfo, ExpectedFunctionExitSet);
+    BuildLockset LocksetBuilder(this, *CurrBlockInfo, ExpectedFunctionExitSet,
+                                ConstructedObjects);
 
     // Visit all the statements in the basic block.
     for (const auto &BI : *CurrBlock) {
diff --git a/clang/test/SemaCXX/warn-thread-safety-analysis.cpp b/clang/test/SemaCXX/warn-thread-safety-analysis.cpp
index 205cfa284f6c9c9..691b8733d58fd03 100644
--- a/clang/test/SemaCXX/warn-thread-safety-analysis.cpp
+++ b/clang/test/SemaCXX/warn-thread-safety-analysis.cpp
@@ -1702,6 +1702,8 @@ struct TestScopedLockable {
 
   bool getBool();
 
+  bool check(MutexLock);
+
   void foo1() {
     MutexLock mulock(&mu1);
     a = 5;
@@ -1718,6 +1720,13 @@ struct TestScopedLockable {
     MutexLock{&mu1}, a = 5;
   }
 
+  void temporary2(int x) {
+    if (check(MutexLock{&mu1}) || x) {
+
+    }
+    check(MutexLock{&mu1});
+  }
+
   void lifetime_extension() {
     const MutexLock &mulock = MutexLock(&mu1);
     a = 5;

Copy link

github-actions bot commented Dec 1, 2023

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

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.

I have some suggestions, but in principle this is absolutely right. Thanks for finding this and providing a fix!

The issue is that the map lives within a CFG block.

It didn't cross my mind to check how long BuildLockset lived, I always assumed it lived for the entire function. But you're right, it does not, and is therefore an unsuitable home for the map.

Comment on lines 1547 to 1549
/// The map lives longer than this builder so this is a reference to an
/// existing one. The map lives so long as the CFG while this builder is for
/// one CFG block.
Copy link
Member

Choose a reason for hiding this comment

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

It's probably sufficient to explain this in the commit message.

@@ -2392,6 +2397,8 @@ void ThreadSafetyAnalyzer::runAnalysis(AnalysisDeclContext &AC) {
for (const auto &Lock : LocksReleased)
ExpectedFunctionExitSet.removeLock(FactMan, Lock);

ConstructedObjectMapTy ConstructedObjects;
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we should put it into the ThreadSafetyAnalyzer, where we already have the similar LocalVariableMap. Then we don't need to pass it into BuildLockset, because that already has a pointer to the ThreadSafetyAnalyzer.

Comment on lines 1724 to 1726
if (check(MutexLock{&mu1}) || x) {

}
Copy link
Member

Choose a reason for hiding this comment

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

The if is probably not needed here, right? We could just write

Suggested change
if (check(MutexLock{&mu1}) || x) {
}
check(MutexLock{&mu1}) || x;

and should have the same CFG artifact.

if (check(MutexLock{&mu1}) || x) {

}
check(MutexLock{&mu1});
Copy link
Member

Choose a reason for hiding this comment

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

The check here doesn't do anything, right?

@@ -1718,6 +1720,13 @@ struct TestScopedLockable {
MutexLock{&mu1}, a = 5;
}

void temporary2(int x) {
Copy link
Member

Choose a reason for hiding this comment

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

I would suggest a speaking name for the test, like temporary_cfg.

@ziqingluo-90
Copy link
Contributor Author

@aaronpuchert Thanks for your comment! I have addressed them. Do you mind to take an another look and approve it if it looks good to you?

Comment on lines 2490 to 2497
if (auto Object = LocksetBuilder.ConstructedObjects.find(
if (auto Object = LocksetBuilder.Analyzer->ConstructedObjects.find(
TD.getBindTemporaryExpr()->getSubExpr());
Object != LocksetBuilder.ConstructedObjects.end()) {
Object != LocksetBuilder.Analyzer->ConstructedObjects.end()) {
const auto *DD = TD.getDestructorDecl(AC.getASTContext());
if (DD->hasAttrs())
// TODO: the location here isn't quite correct.
LocksetBuilder.handleCall(nullptr, DD, Object->second,
TD.getBindTemporaryExpr()->getEndLoc());
LocksetBuilder.ConstructedObjects.erase(Object);
LocksetBuilder.Analyzer->ConstructedObjects.erase(Object);
Copy link
Member

Choose a reason for hiding this comment

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

We're in ThreadSafetyAnalyzer here, so you should be able to drop LocksetBuilder.Analyzer-> and refer to the member directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -1530,7 +1532,6 @@ void ThreadSafetyAnalyzer::getEdgeLockset(FactSet& Result,
}

namespace {

Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: can you undo the whitespace change?

Extends the lifetime of the map `ConstructedObjects` to be of the
whole CFG so that the map can connect temporary Ctor and Dtor in
different CFG blocks.
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.

Thanks, looks good to me!

@ziqingluo-90
Copy link
Contributor Author

Plan to merge this PR tomorrow.

@ziqingluo-90 ziqingluo-90 merged commit a341e17 into llvm:main Dec 8, 2023
3 checks passed
ziqingluo-90 added a commit to ziqingluo-90/apple-llvm-project that referenced this pull request Dec 11, 2023
…lvm#74020)

Extends the lifetime of the map `ConstructedObjects` to be of the
whole CFG so that the map can connect temporary Ctor and Dtor in
different CFG blocks.

(cherry picked from commit a341e17)
Michael137 pushed a commit to Michael137/llvm-project that referenced this pull request Dec 15, 2023
…lvm#74020)

Extends the lifetime of the map `ConstructedObjects` to be of the
whole CFG so that the map can connect temporary Ctor and Dtor in
different CFG blocks.

(cherry picked from commit a341e17)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:analysis clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants