Skip to content

Commit 548af3e

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 548af3e

File tree

5 files changed

+174
-14
lines changed

5 files changed

+174
-14
lines changed

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

Lines changed: 1 addition & 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.

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: 61 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"
@@ -114,6 +115,46 @@ static bool isCalleeArrow(const Expr *E) {
114115
return ME ? ME->isArrow() : false;
115116
}
116117

118+
static bool isPointerReassigned(const VarDecl *VD) {
119+
class AssignmentFinder : public RecursiveASTVisitor<AssignmentFinder> {
120+
const VarDecl *VD;
121+
122+
public:
123+
explicit AssignmentFinder(const VarDecl *VD) : VD(VD) {}
124+
125+
bool VisitBinaryOperator(BinaryOperator *BO) {
126+
if (!BO->isAssignmentOp())
127+
return true;
128+
129+
if (const auto *DRE =
130+
dyn_cast<DeclRefExpr>(BO->getLHS()->IgnoreParenImpCasts())) {
131+
// If target variable appears as LHS of assignment
132+
if (DRE->getDecl()->getCanonicalDecl() == VD->getCanonicalDecl()) {
133+
// Skip the initializer
134+
if (BO->getBeginLoc() != VD->getInit()->getBeginLoc()) {
135+
FoundReassignment = true;
136+
return false; // stop
137+
}
138+
}
139+
}
140+
141+
return true;
142+
}
143+
144+
bool FoundReassignment = false;
145+
};
146+
147+
const DeclContext *DC = VD->getDeclContext();
148+
if (const auto *FD = dyn_cast<FunctionDecl>(DC)) {
149+
AssignmentFinder Visitor(VD);
150+
Visitor.TraverseDecl(const_cast<FunctionDecl *>(FD));
151+
return Visitor.FoundReassignment;
152+
}
153+
154+
// Assume it might be reassigned.
155+
return true;
156+
}
157+
117158
/// Translate a clang expression in an attribute to a til::SExpr.
118159
/// Constructs the context from D, DeclExp, and SelfDecl.
119160
///
@@ -241,7 +282,23 @@ CapabilityExpr SExprBuilder::translateAttrExpr(const Expr *AttrExp,
241282
return CapabilityExpr(E, AttrExp->getType(), Neg);
242283
}
243284

244-
til::LiteralPtr *SExprBuilder::createVariable(const VarDecl *VD) {
285+
til::SExpr *SExprBuilder::createVariable(const VarDecl *VD,
286+
CallingContext *Ctx) {
287+
if (VD) {
288+
// Substitute local pointer variables with their initializers if they are
289+
// explicitly const or never reassigned.
290+
QualType Ty = VD->getType();
291+
if (VD->isLocalVarDecl() && Ty->isPointerType() && VD->hasInit() &&
292+
(Ty.isConstQualified() || !isPointerReassigned(VD))) {
293+
const Expr *Init = VD->getInit()->IgnoreParenImpCasts();
294+
// Check for self-initialization to prevent infinite recursion.
295+
if (const auto *InitDRE = dyn_cast<DeclRefExpr>(Init)) {
296+
if (InitDRE->getDecl()->getCanonicalDecl() == VD->getCanonicalDecl())
297+
return new (Arena) til::LiteralPtr(VD);
298+
}
299+
return translate(Init, Ctx);
300+
}
301+
}
245302
return new (Arena) til::LiteralPtr(VD);
246303
}
247304

@@ -353,6 +410,9 @@ til::SExpr *SExprBuilder::translateDeclRefExpr(const DeclRefExpr *DRE,
353410
: cast<ObjCMethodDecl>(D)->getCanonicalDecl()->getParamDecl(I);
354411
}
355412

413+
if (const auto *VarD = dyn_cast<VarDecl>(VD))
414+
return createVariable(VarD, Ctx);
415+
356416
// For non-local variables, treat it as a reference to a named object.
357417
return new (Arena) til::LiteralPtr(VD);
358418
}

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)