-
Notifications
You must be signed in to change notification settings - Fork 10.8k
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
Conversation
@llvm/pr-subscribers-clang @llvm/pr-subscribers-clang-analysis Author: Ziqing Luo (ziqingluo-90) ChangesThe 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
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:
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;
|
✅ With the latest revision this PR passed the C/C++ code formatter. |
There was a problem hiding this 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.
clang/lib/Analysis/ThreadSafety.cpp
Outdated
/// 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. |
There was a problem hiding this comment.
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.
clang/lib/Analysis/ThreadSafety.cpp
Outdated
@@ -2392,6 +2397,8 @@ void ThreadSafetyAnalyzer::runAnalysis(AnalysisDeclContext &AC) { | |||
for (const auto &Lock : LocksReleased) | |||
ExpectedFunctionExitSet.removeLock(FactMan, Lock); | |||
|
|||
ConstructedObjectMapTy ConstructedObjects; |
There was a problem hiding this comment.
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
.
if (check(MutexLock{&mu1}) || x) { | ||
|
||
} |
There was a problem hiding this comment.
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
if (check(MutexLock{&mu1}) || x) { | |
} | |
check(MutexLock{&mu1}) || x; |
and should have the same CFG artifact.
if (check(MutexLock{&mu1}) || x) { | ||
|
||
} | ||
check(MutexLock{&mu1}); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
.
ecb13c3
to
00083bb
Compare
@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? |
clang/lib/Analysis/ThreadSafety.cpp
Outdated
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
clang/lib/Analysis/ThreadSafety.cpp
Outdated
@@ -1530,7 +1532,6 @@ void ThreadSafetyAnalyzer::getEdgeLockset(FactSet& Result, | |||
} | |||
|
|||
namespace { | |||
|
There was a problem hiding this comment.
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?
00083bb
to
81701bf
Compare
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.
81701bf
to
9c2718a
Compare
There was a problem hiding this 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!
Plan to merge this PR tomorrow. |
…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)
…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)
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:The fix is simple. It extends the life of the map to that of the entire CFG.