Skip to content

Commit 608c4f6

Browse files
committed
Thread Safety Analysis: Very basic capability alias-analysis
Add a simple form of alias analysis for capabilities by substituting local pointer variables with their initializers if they are `const` or never reassigned. For example, the analysis will no longer generate false positives for cases such as: void testNestedAccess(Container *c) { Foo *ptr = &c->foo; ptr->mu.Lock(); c->foo.data = 42; // OK - no false positive ptr->mu.Unlock(); } void testNestedAcquire(Container *c) EXCLUSIVE_LOCK_FUNCTION(&c->foo.mu) { Foo *buf = &c->foo; buf->mu.Lock(); // OK - no false positive warning } This implementation would satisfy the basic needs of addressing the concerns for Linux kernel application [1]. Current limitations: * The analysis does not handle pointers that are reassigned; it conservatively assumes they could point to anything after the reassignment. * Aliases created through complex control flow are not tracked. Link: https://lore.kernel.org/all/CANpmjNPquO=W1JAh1FNQb8pMQjgeZAKCPQUAd7qUg=5pjJ6x=Q@mail.gmail.com/ [1]
1 parent 01f9dff commit 608c4f6

File tree

5 files changed

+196
-14
lines changed

5 files changed

+196
-14
lines changed

clang/include/clang/Analysis/Analyses/ThreadSafetyCommon.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ class SExprBuilder {
395395
CapabilityExpr translateAttrExpr(const Expr *AttrExp, CallingContext *Ctx);
396396

397397
// Translate a variable reference.
398-
til::LiteralPtr *createVariable(const VarDecl *VD);
398+
til::SExpr *createVariable(const VarDecl *VD, CallingContext *Ctx = nullptr);
399399

400400
// Translate a clang statement or expression to a TIL expression.
401401
// Also performs substitution of variables; Ctx provides the context.
@@ -501,6 +501,9 @@ class SExprBuilder {
501501
void mergeEntryMapBackEdge();
502502
void mergePhiNodesBackEdge(const CFGBlock *Blk);
503503

504+
// Returns true if a variable is assumed to be reassigned.
505+
bool isVariableReassigned(const VarDecl *VD);
506+
504507
private:
505508
// Set to true when parsing capability expressions, which get translated
506509
// inaccurately in order to hack around smart pointers etc.
@@ -531,6 +534,9 @@ class SExprBuilder {
531534
std::vector<til::Phi *> IncompleteArgs;
532535
til::BasicBlock *CurrentBB = nullptr;
533536
BlockInfo *CurrentBlockInfo = nullptr;
537+
538+
// Map caching if a local variable is reassigned.
539+
llvm::DenseMap<const VarDecl *, bool> LocalVariableReassigned;
534540
};
535541

536542
#ifndef NDEBUG

clang/lib/Analysis/ThreadSafety.cpp

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,11 +1141,10 @@ class ThreadSafetyAnalyzer {
11411141

11421142
void warnIfMutexNotHeld(const FactSet &FSet, const NamedDecl *D,
11431143
const Expr *Exp, AccessKind AK, Expr *MutexExp,
1144-
ProtectedOperationKind POK, til::LiteralPtr *Self,
1144+
ProtectedOperationKind POK, til::SExpr *Self,
11451145
SourceLocation Loc);
11461146
void warnIfMutexHeld(const FactSet &FSet, const NamedDecl *D, const Expr *Exp,
1147-
Expr *MutexExp, til::LiteralPtr *Self,
1148-
SourceLocation Loc);
1147+
Expr *MutexExp, til::SExpr *Self, SourceLocation Loc);
11491148

11501149
void checkAccess(const FactSet &FSet, const Expr *Exp, AccessKind AK,
11511150
ProtectedOperationKind POK);
@@ -1599,7 +1598,7 @@ class BuildLockset : public ConstStmtVisitor<BuildLockset> {
15991598
}
16001599

16011600
void handleCall(const Expr *Exp, const NamedDecl *D,
1602-
til::LiteralPtr *Self = nullptr,
1601+
til::SExpr *Self = nullptr,
16031602
SourceLocation Loc = SourceLocation());
16041603
void examineArguments(const FunctionDecl *FD,
16051604
CallExpr::const_arg_iterator ArgBegin,
@@ -1629,7 +1628,7 @@ class BuildLockset : public ConstStmtVisitor<BuildLockset> {
16291628
/// of at least the passed in AccessKind.
16301629
void ThreadSafetyAnalyzer::warnIfMutexNotHeld(
16311630
const FactSet &FSet, const NamedDecl *D, const Expr *Exp, AccessKind AK,
1632-
Expr *MutexExp, ProtectedOperationKind POK, til::LiteralPtr *Self,
1631+
Expr *MutexExp, ProtectedOperationKind POK, til::SExpr *Self,
16331632
SourceLocation Loc) {
16341633
LockKind LK = getLockKindFromAccessKind(AK);
16351634
CapabilityExpr Cp = SxBuilder.translateAttrExpr(MutexExp, D, Exp, Self);
@@ -1688,8 +1687,7 @@ void ThreadSafetyAnalyzer::warnIfMutexNotHeld(
16881687
/// Warn if the LSet contains the given lock.
16891688
void ThreadSafetyAnalyzer::warnIfMutexHeld(const FactSet &FSet,
16901689
const NamedDecl *D, const Expr *Exp,
1691-
Expr *MutexExp,
1692-
til::LiteralPtr *Self,
1690+
Expr *MutexExp, til::SExpr *Self,
16931691
SourceLocation Loc) {
16941692
CapabilityExpr Cp = SxBuilder.translateAttrExpr(MutexExp, D, Exp, Self);
16951693
if (Cp.isInvalid()) {
@@ -1857,7 +1855,7 @@ void ThreadSafetyAnalyzer::checkPtAccess(const FactSet &FSet, const Expr *Exp,
18571855
/// of an implicitly called cleanup function.
18581856
/// \param Loc If \p Exp = nullptr, the location.
18591857
void BuildLockset::handleCall(const Expr *Exp, const NamedDecl *D,
1860-
til::LiteralPtr *Self, SourceLocation Loc) {
1858+
til::SExpr *Self, SourceLocation Loc) {
18611859
CapExprSet ExclusiveLocksToAdd, SharedLocksToAdd;
18621860
CapExprSet ExclusiveLocksToRemove, SharedLocksToRemove, GenericLocksToRemove;
18631861
CapExprSet ScopedReqsAndExcludes;
@@ -1869,7 +1867,7 @@ void BuildLockset::handleCall(const Expr *Exp, const NamedDecl *D,
18691867
const auto *TagT = Exp->getType()->getAs<TagType>();
18701868
if (D->hasAttrs() && TagT && Exp->isPRValue()) {
18711869
til::LiteralPtr *Placeholder =
1872-
Analyzer->SxBuilder.createVariable(nullptr);
1870+
cast<til::LiteralPtr>(Analyzer->SxBuilder.createVariable(nullptr));
18731871
[[maybe_unused]] auto inserted =
18741872
Analyzer->ConstructedObjects.insert({Exp, Placeholder});
18751873
assert(inserted.second && "Are we visiting the same expression again?");

clang/lib/Analysis/ThreadSafetyCommon.cpp

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "clang/AST/Expr.h"
2020
#include "clang/AST/ExprCXX.h"
2121
#include "clang/AST/OperationKinds.h"
22+
#include "clang/AST/RecursiveASTVisitor.h"
2223
#include "clang/AST/Stmt.h"
2324
#include "clang/AST/Type.h"
2425
#include "clang/Analysis/Analyses/ThreadSafetyTIL.h"
@@ -241,7 +242,22 @@ CapabilityExpr SExprBuilder::translateAttrExpr(const Expr *AttrExp,
241242
return CapabilityExpr(E, AttrExp->getType(), Neg);
242243
}
243244

244-
til::LiteralPtr *SExprBuilder::createVariable(const VarDecl *VD) {
245+
til::SExpr *SExprBuilder::createVariable(const VarDecl *VD,
246+
CallingContext *Ctx) {
247+
if (VD) {
248+
// Substitute local pointer variables with their initializers if they are
249+
// explicitly const or never reassigned.
250+
QualType Ty = VD->getType();
251+
if (Ty->isPointerType() && VD->hasInit() && !isVariableReassigned(VD)) {
252+
const Expr *Init = VD->getInit()->IgnoreParenImpCasts();
253+
// Check for self-initialization to prevent infinite recursion.
254+
if (const auto *InitDRE = dyn_cast<DeclRefExpr>(Init)) {
255+
if (InitDRE->getDecl()->getCanonicalDecl() == VD->getCanonicalDecl())
256+
return new (Arena) til::LiteralPtr(VD);
257+
}
258+
return translate(Init, Ctx);
259+
}
260+
}
245261
return new (Arena) til::LiteralPtr(VD);
246262
}
247263

@@ -353,6 +369,9 @@ til::SExpr *SExprBuilder::translateDeclRefExpr(const DeclRefExpr *DRE,
353369
: cast<ObjCMethodDecl>(D)->getCanonicalDecl()->getParamDecl(I);
354370
}
355371

372+
if (const auto *VarD = dyn_cast<VarDecl>(VD))
373+
return createVariable(VarD, Ctx);
374+
356375
// For non-local variables, treat it as a reference to a named object.
357376
return new (Arena) til::LiteralPtr(VD);
358377
}
@@ -1012,6 +1031,63 @@ void SExprBuilder::exitCFG(const CFGBlock *Last) {
10121031
IncompleteArgs.clear();
10131032
}
10141033

1034+
bool SExprBuilder::isVariableReassigned(const VarDecl *VD) {
1035+
// Note: The search is performed lazily per-variable and result is cached. An
1036+
// alternative would have been to eagerly create a set of all reassigned
1037+
// variables, but that would consume significantly more memory. The number of
1038+
// variables needing the reassignment check in a single function is expected
1039+
// to be small. Also see createVariable().
1040+
struct ReassignmentFinder : RecursiveASTVisitor<ReassignmentFinder> {
1041+
explicit ReassignmentFinder(const VarDecl *VD) : QueryVD(VD) {
1042+
assert(QueryVD->getCanonicalDecl() == QueryVD);
1043+
}
1044+
1045+
bool VisitBinaryOperator(BinaryOperator *BO) {
1046+
if (!BO->isAssignmentOp())
1047+
return true;
1048+
auto *DRE = dyn_cast<DeclRefExpr>(BO->getLHS()->IgnoreParenImpCasts());
1049+
if (!DRE)
1050+
return true;
1051+
auto *AssignedVD = dyn_cast<VarDecl>(DRE->getDecl());
1052+
if (!AssignedVD)
1053+
return true;
1054+
// Don't count the initialization itself as a reassignment.
1055+
if (AssignedVD->hasInit() &&
1056+
AssignedVD->getInit()->getBeginLoc() == BO->getBeginLoc())
1057+
return true;
1058+
// If query variable appears as LHS of assignment.
1059+
if (QueryVD == AssignedVD->getCanonicalDecl()) {
1060+
FoundReassignment = true;
1061+
return false; // stop
1062+
}
1063+
return true;
1064+
}
1065+
1066+
const VarDecl *QueryVD;
1067+
bool FoundReassignment = false;
1068+
};
1069+
1070+
if (VD->getType().isConstQualified())
1071+
return false; // Assume UB-freedom.
1072+
if (!VD->isLocalVarDecl())
1073+
return true; // Not a local variable (assume reassigned).
1074+
auto *FD = dyn_cast<FunctionDecl>(VD->getDeclContext());
1075+
if (!FD)
1076+
return true; // Assume reassigned.
1077+
1078+
// Try to look up in cache; use the canonical declaration to ensure consistent
1079+
// lookup in the cache.
1080+
VD = VD->getCanonicalDecl();
1081+
auto It = LocalVariableReassigned.find(VD);
1082+
if (It != LocalVariableReassigned.end())
1083+
return It->second;
1084+
1085+
ReassignmentFinder Visitor(VD);
1086+
// const_cast ok: FunctionDecl not modified.
1087+
Visitor.TraverseDecl(const_cast<FunctionDecl *>(FD));
1088+
return LocalVariableReassigned[VD] = Visitor.FoundReassignment;
1089+
}
1090+
10151091
#ifndef NDEBUG
10161092
namespace {
10171093

clang/test/Sema/warn-thread-safety-analysis.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,13 @@ int main(void) {
184184
/// Cleanup functions
185185
{
186186
struct Mutex* const __attribute__((cleanup(unlock_scope))) scope = &mu1;
187-
mutex_exclusive_lock(scope); // Note that we have to lock through scope, because no alias analysis!
187+
mutex_exclusive_lock(scope); // Lock through scope works.
188188
// Cleanup happens automatically -> no warning.
189189
}
190+
{
191+
struct Mutex* const __attribute__((unused, cleanup(unlock_scope))) scope = &mu1;
192+
mutex_exclusive_lock(&mu1); // With basic alias analysis lock through mu1 also works.
193+
}
190194

191195
foo_.a_value = 0; // expected-warning {{writing variable 'a_value' requires holding mutex 'mu_' exclusively}}
192196
*foo_.a_ptr = 1; // expected-warning {{writing the value pointed to by 'a_ptr' requires holding mutex 'bar.other_mu' exclusively}}

clang/test/SemaCXX/warn-thread-safety-analysis.cpp

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,9 +1556,9 @@ void main() {
15561556
Child *c;
15571557
Base *b = c;
15581558

1559-
b->func1(); // expected-warning {{calling function 'func1' requires holding mutex 'b->mu_' exclusively}}
1559+
b->func1(); // expected-warning {{calling function 'func1' requires holding mutex 'c->mu_' exclusively}}
15601560
b->mu_.Lock();
1561-
b->func2(); // expected-warning {{cannot call function 'func2' while mutex 'b->mu_' is held}}
1561+
b->func2(); // expected-warning {{cannot call function 'func2' while mutex 'c->mu_' is held}}
15621562
b->mu_.Unlock();
15631563

15641564
c->func1(); // expected-warning {{calling function 'func1' requires holding mutex 'c->mu_' exclusively}}
@@ -7224,3 +7224,101 @@ class TestNegativeWithReentrantMutex {
72247224
};
72257225

72267226
} // namespace Reentrancy
7227+
7228+
// Tests for tracking aliases of capabilities.
7229+
namespace CapabilityAliases {
7230+
struct Foo {
7231+
Mutex mu;
7232+
int data GUARDED_BY(mu);
7233+
};
7234+
7235+
void testBasicPointerAlias(Foo *f) {
7236+
Foo *ptr = f;
7237+
ptr->mu.Lock(); // lock through alias
7238+
f->data = 42; // access through original
7239+
ptr->mu.Unlock(); // unlock through alias
7240+
}
7241+
7242+
// FIXME: No alias tracking for non-const reassigned pointers.
7243+
void testReassignment() {
7244+
Foo f1, f2;
7245+
Foo *ptr = &f1;
7246+
ptr->mu.Lock();
7247+
f1.data = 42; // expected-warning{{writing variable 'data' requires holding mutex 'f1.mu' exclusively}} \
7248+
// expected-note{{found near match 'ptr->mu'}}
7249+
ptr->mu.Unlock();
7250+
7251+
ptr = &f2; // reassign
7252+
ptr->mu.Lock();
7253+
f2.data = 42; // expected-warning{{writing variable 'data' requires holding mutex 'f2.mu' exclusively}} \
7254+
// expected-note{{found near match 'ptr->mu'}}
7255+
f1.data = 42; // expected-warning{{writing variable 'data' requires holding mutex 'f1.mu'}} \
7256+
// expected-note{{found near match 'ptr->mu'}}
7257+
ptr->mu.Unlock();
7258+
}
7259+
7260+
// Nested field access through pointer
7261+
struct Container {
7262+
Foo foo;
7263+
};
7264+
7265+
void testNestedAccess(Container *c) {
7266+
Foo *ptr = &c->foo; // pointer to nested field
7267+
ptr->mu.Lock();
7268+
c->foo.data = 42;
7269+
ptr->mu.Unlock();
7270+
}
7271+
7272+
void testNestedAcquire(Container *c) EXCLUSIVE_LOCK_FUNCTION(&c->foo.mu) {
7273+
Foo *buf = &c->foo;
7274+
buf->mu.Lock();
7275+
}
7276+
7277+
struct ContainerOfPtr {
7278+
Foo *foo_ptr;
7279+
};
7280+
7281+
void testIndirectAccess(ContainerOfPtr *fc) {
7282+
Foo *ptr = fc->foo_ptr; // get pointer
7283+
ptr->mu.Lock();
7284+
fc->foo_ptr->data = 42; // access via original
7285+
ptr->mu.Unlock();
7286+
}
7287+
7288+
// FIXME: No alias tracking through complex control flow.
7289+
void testSimpleControlFlow(Foo *f1, Foo *f2, bool cond) {
7290+
Foo *ptr;
7291+
if (cond) {
7292+
ptr = f1;
7293+
} else {
7294+
ptr = f2;
7295+
}
7296+
ptr->mu.Lock();
7297+
if (cond) {
7298+
f1->data = 42; // expected-warning{{writing variable 'data' requires holding mutex 'f1->mu' exclusively}} \
7299+
// expected-note{{found near match 'ptr->mu'}}
7300+
} else {
7301+
f2->data = 42; // expected-warning{{writing variable 'data' requires holding mutex 'f2->mu' exclusively}} \
7302+
// expected-note{{found near match 'ptr->mu'}}
7303+
}
7304+
ptr->mu.Unlock();
7305+
}
7306+
7307+
void testLockFunction(Foo *f) EXCLUSIVE_LOCK_FUNCTION(&f->mu) {
7308+
Mutex *mu = &f->mu;
7309+
mu->Lock();
7310+
}
7311+
7312+
void testUnlockFunction(Foo *f) UNLOCK_FUNCTION(&f->mu) {
7313+
Mutex *mu = &f->mu;
7314+
mu->Unlock();
7315+
}
7316+
7317+
// Semantically UB, but let's not crash the compiler with this (should be
7318+
// handled by -Wuninitialized).
7319+
void testSelfInit() {
7320+
Mutex *mu = mu; // don't do this at home
7321+
mu->Lock();
7322+
mu->Unlock();
7323+
}
7324+
} // namespace CapabilityAliases

0 commit comments

Comments
 (0)