Skip to content

Commit 7152266

Browse files
authored
Thread Safety Analysis: Support guarded_by/pt_guarded_by with multiple capabilities (llvm#186838)
Previously, `guarded_by` and `pt_guarded_by` only accepted a single capability argument. Introduce support for declaring that a variable is guarded by multiple capabilities, which exploits the following property: any writer must hold all capabilities, so holding any one of them (exclusive or shared) guarantees at least shared (read) access. Therefore, writing requires all listed capabilities to be held exclusively, while reading only requires at least one to be held. This synchronization pattern is frequently used where the underlying lock implementation does not support real reader locking, and instead several lock "shards" are used to reduce contention for readers. For example, the Linux kernel makes frequent use of this pattern [1]. Backwards compatibility is not affected by this change: for the time being we deliberately do not change the semantics of multiple stacked attributes (this retains existing semantics precisely, while giving a way to choose the "stricter" semantics if needed). Previous Version: llvm#185173 Link: https://lore.kernel.org/all/20250307085204.GJ16878@noisy.programming.kicks-ass.net/ [1]
1 parent b16efa6 commit 7152266

15 files changed

Lines changed: 244 additions & 53 deletions

clang/docs/ReleaseNotes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,13 @@ Attribute Changes in Clang
221221
foreign language personality with a given function. Note that this does not
222222
perform any ABI validation for the personality routine.
223223

224+
- The :doc:`ThreadSafetyAnalysis` attributes ``guarded_by`` and
225+
``pt_guarded_by`` now accept multiple capability arguments with refined
226+
access semantics: *writing* requires all listed capabilities to be held
227+
exclusively, while *reading* requires at least one to be held. This is
228+
sound because any writer must hold all capabilities, so holding any one
229+
prevents concurrent writes.
230+
224231
Improvements to Clang's diagnostics
225232
-----------------------------------
226233
- Added ``-Wlifetime-safety`` to enable lifetime safety analysis,

clang/docs/ThreadSafetyAnalysis.rst

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,16 +153,20 @@ general capability model. The prior names are still in use, and will be
153153
mentioned under the tag *previously* where appropriate.
154154

155155

156-
GUARDED_BY(c) and PT_GUARDED_BY(c)
157-
----------------------------------
156+
GUARDED_BY(...) and PT_GUARDED_BY(...)
157+
--------------------------------------
158158

159159
``GUARDED_BY`` is an attribute on data members, which declares that the data
160160
member is protected by the given capability. Read operations on the data
161161
require shared access, while write operations require exclusive access.
162162

163+
Multiple capabilities may be specified, subject to the following rules:
164+
a writer must hold *all* listed capabilities exclusively, so holding *any one*
165+
of them is sufficient to guarantee at least shared (read) access.
166+
163167
``PT_GUARDED_BY`` is similar, but is intended for use on pointers and smart
164168
pointers. There is no constraint on the data member itself, but the *data that
165-
it points to* is protected by the given capability.
169+
it points to* is protected by the given capabilities.
166170

167171
.. code-block:: c++
168172

@@ -181,6 +185,25 @@ it points to* is protected by the given capability.
181185
p3.reset(new int); // OK.
182186
}
183187
188+
When multiple capabilities are listed:
189+
190+
* **Write** access requires all listed capabilities to be held exclusively.
191+
* **Read** access requires at least one of them to be held (shared or exclusive).
192+
193+
.. code-block:: c++
194+
195+
Mutex mu1, mu2;
196+
int a GUARDED_BY(mu1, mu2);
197+
198+
void reader() REQUIRES_SHARED(mu1) {
199+
int x = a; // OK: at least one capability is held.
200+
a = 0; // Warning! Writing requires both mu1 and mu2.
201+
}
202+
203+
void writer() REQUIRES(mu1, mu2) {
204+
a = 0; // OK: both capabilities are held exclusively.
205+
}
206+
184207

185208
REQUIRES(...), REQUIRES_SHARED(...)
186209
-----------------------------------
@@ -860,11 +883,11 @@ implementation.
860883
#define SCOPED_CAPABILITY \
861884
THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable)
862885

863-
#define GUARDED_BY(x) \
864-
THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x))
886+
#define GUARDED_BY(...) \
887+
THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(__VA_ARGS__))
865888

866-
#define PT_GUARDED_BY(x) \
867-
THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(x))
889+
#define PT_GUARDED_BY(...) \
890+
THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(__VA_ARGS__))
868891

869892
#define ACQUIRED_BEFORE(...) \
870893
THREAD_ANNOTATION_ATTRIBUTE__(acquired_before(__VA_ARGS__))

clang/include/clang/Analysis/Analyses/ThreadSafety.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#define LLVM_CLANG_ANALYSIS_ANALYSES_THREADSAFETY_H
2020

2121
#include "clang/Basic/SourceLocation.h"
22+
#include "llvm/ADT/ArrayRef.h"
2223
#include "llvm/ADT/StringRef.h"
2324

2425
namespace clang {
@@ -193,6 +194,17 @@ class ThreadSafetyHandler {
193194
virtual void handleNoMutexHeld(const NamedDecl *D, ProtectedOperationKind POK,
194195
AccessKind AK, SourceLocation Loc) {}
195196

197+
/// Warn when a read of a multi-capability guarded_by variable occurs while
198+
/// none of the listed capabilities are held.
199+
/// \param D -- The decl for the protected variable
200+
/// \param POK -- The kind of protected operation (e.g. variable access)
201+
/// \param LockNames -- Names of the capabilities that were not held
202+
/// \param Loc -- The location of the read
203+
virtual void handleGuardedByAnyReadNotHeld(const NamedDecl *D,
204+
ProtectedOperationKind POK,
205+
ArrayRef<StringRef> LockNames,
206+
SourceLocation Loc) {}
207+
196208
/// Warn when a protected operation occurs while the specific mutex protecting
197209
/// the operation is not locked.
198210
/// \param Kind -- the capability's name parameter (role, mutex, etc).

clang/include/clang/Basic/Attr.td

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4155,7 +4155,7 @@ def NoThreadSafetyAnalysis : InheritableAttr {
41554155

41564156
def GuardedBy : InheritableAttr {
41574157
let Spellings = [GNU<"guarded_by">];
4158-
let Args = [ExprArgument<"Arg">];
4158+
let Args = [VariadicExprArgument<"Args">];
41594159
let LateParsed = LateAttrParseExperimentalExt;
41604160
let TemplateDependent = 1;
41614161
let ParseArgumentsAsUnevaluated = 1;
@@ -4166,7 +4166,7 @@ def GuardedBy : InheritableAttr {
41664166

41674167
def PtGuardedBy : InheritableAttr {
41684168
let Spellings = [GNU<"pt_guarded_by">];
4169-
let Args = [ExprArgument<"Arg">];
4169+
let Args = [VariadicExprArgument<"Args">];
41704170
let LateParsed = LateAttrParseExperimentalExt;
41714171
let TemplateDependent = 1;
41724172
let ParseArgumentsAsUnevaluated = 1;

clang/include/clang/Basic/DiagnosticSemaKinds.td

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4327,6 +4327,10 @@ def warn_var_deref_requires_any_lock : Warning<
43274327
"%select{reading|writing}1 the value pointed to by %0 requires holding "
43284328
"%select{any mutex|any mutex exclusively}1">,
43294329
InGroup<ThreadSafetyAnalysis>, DefaultIgnore;
4330+
def warn_requires_any_of_locks : Warning<
4331+
"reading %select{variable|the value pointed to by}1 %0 requires holding "
4332+
"at least one of %2">,
4333+
InGroup<ThreadSafetyAnalysis>, DefaultIgnore;
43304334
def warn_fun_excludes_mutex : Warning<
43314335
"cannot call function '%1' while %0 '%2' is held">,
43324336
InGroup<ThreadSafetyAnalysis>, DefaultIgnore;

clang/lib/AST/ASTImporter.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9749,12 +9749,16 @@ Expected<Attr *> ASTImporter::Import(const Attr *FromAttr) {
97499749
}
97509750
case attr::GuardedBy: {
97519751
const auto *From = cast<GuardedByAttr>(FromAttr);
9752-
AI.importAttr(From, AI.importArg(From->getArg()).value());
9752+
AI.importAttr(From,
9753+
AI.importArrayArg(From->args(), From->args_size()).value(),
9754+
From->args_size());
97539755
break;
97549756
}
97559757
case attr::PtGuardedBy: {
97569758
const auto *From = cast<PtGuardedByAttr>(FromAttr);
9757-
AI.importAttr(From, AI.importArg(From->getArg()).value());
9759+
AI.importAttr(From,
9760+
AI.importArrayArg(From->args(), From->args_size()).value(),
9761+
From->args_size());
97589762
break;
97599763
}
97609764
case attr::AcquiredAfter: {

clang/lib/Analysis/ThreadSafety.cpp

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,11 @@ class ThreadSafetyAnalyzer {
12791279
const Expr *Exp, AccessKind AK, Expr *MutexExp,
12801280
ProtectedOperationKind POK, til::SExpr *Self,
12811281
SourceLocation Loc);
1282+
void warnIfAnyMutexNotHeldForRead(const FactSet &FSet, const NamedDecl *D,
1283+
const Expr *Exp,
1284+
llvm::ArrayRef<Expr *> Args,
1285+
ProtectedOperationKind POK,
1286+
SourceLocation Loc);
12821287
void warnIfMutexHeld(const FactSet &FSet, const NamedDecl *D, const Expr *Exp,
12831288
Expr *MutexExp, til::SExpr *Self, SourceLocation Loc);
12841289

@@ -1848,6 +1853,38 @@ void ThreadSafetyAnalyzer::warnIfMutexNotHeld(
18481853
}
18491854
}
18501855

1856+
void ThreadSafetyAnalyzer::warnIfAnyMutexNotHeldForRead(
1857+
const FactSet &FSet, const NamedDecl *D, const Expr *Exp,
1858+
llvm::ArrayRef<Expr *> Args, ProtectedOperationKind POK,
1859+
SourceLocation Loc) {
1860+
SmallVector<CapabilityExpr, 2> Caps;
1861+
for (auto *Arg : Args) {
1862+
CapabilityExpr Cp = SxBuilder.translateAttrExpr(Arg, D, Exp, nullptr);
1863+
if (Cp.isInvalid()) {
1864+
warnInvalidLock(Handler, Arg, D, Exp, Cp.getKind());
1865+
continue;
1866+
}
1867+
if (Cp.shouldIgnore())
1868+
continue;
1869+
const FactEntry *LDat = FSet.findLockUniv(FactMan, Cp);
1870+
if (LDat && LDat->isAtLeast(LK_Shared))
1871+
return; // At least one held — read access is safe.
1872+
// FIXME: try findPartialMatch as a fallback to support
1873+
// -Wno-thread-safety-precise, as warnIfMutexNotHeld does.
1874+
Caps.push_back(Cp);
1875+
}
1876+
if (Caps.empty())
1877+
return;
1878+
// Materialize names only now that we know we are going to warn.
1879+
SmallVector<std::string, 2> NameStorage;
1880+
SmallVector<StringRef, 2> Names;
1881+
for (const auto &Cp : Caps) {
1882+
NameStorage.push_back(Cp.toString());
1883+
Names.push_back(NameStorage.back());
1884+
}
1885+
Handler.handleGuardedByAnyReadNotHeld(D, POK, Names, Loc);
1886+
}
1887+
18511888
/// Warn if the LSet contains the given lock.
18521889
void ThreadSafetyAnalyzer::warnIfMutexHeld(const FactSet &FSet,
18531890
const NamedDecl *D, const Expr *Exp,
@@ -1934,8 +1971,18 @@ void ThreadSafetyAnalyzer::checkAccess(const FactSet &FSet, const Expr *Exp,
19341971
Handler.handleNoMutexHeld(D, POK, AK, Loc);
19351972
}
19361973

1937-
for (const auto *I : D->specific_attrs<GuardedByAttr>())
1938-
warnIfMutexNotHeld(FSet, D, Exp, AK, I->getArg(), POK, nullptr, Loc);
1974+
for (const auto *I : D->specific_attrs<GuardedByAttr>()) {
1975+
if (AK == AK_Written || I->args_size() == 1) {
1976+
// Write requires all capabilities; single-arg read uses the normal
1977+
// per-lock warning path.
1978+
for (auto *Arg : I->args())
1979+
warnIfMutexNotHeld(FSet, D, Exp, AK, Arg, POK, nullptr, Loc);
1980+
} else {
1981+
// Multi-arg read: holding any one of the listed capabilities is
1982+
// sufficient (a writer must hold all, so any one prevents writes).
1983+
warnIfAnyMutexNotHeldForRead(FSet, D, Exp, I->args(), POK, Loc);
1984+
}
1985+
}
19391986
}
19401987

19411988
/// Checks pt_guarded_by and pt_guarded_var attributes.
@@ -1998,9 +2045,20 @@ void ThreadSafetyAnalyzer::checkPtAccess(const FactSet &FSet, const Expr *Exp,
19982045
if (D->hasAttr<PtGuardedVarAttr>() && FSet.isEmpty(FactMan))
19992046
Handler.handleNoMutexHeld(D, PtPOK, AK, Exp->getExprLoc());
20002047

2001-
for (auto const *I : D->specific_attrs<PtGuardedByAttr>())
2002-
warnIfMutexNotHeld(FSet, D, Exp, AK, I->getArg(), PtPOK, nullptr,
2003-
Exp->getExprLoc());
2048+
for (auto const *I : D->specific_attrs<PtGuardedByAttr>()) {
2049+
if (AK == AK_Written || I->args_size() == 1) {
2050+
// Write requires all capabilities; single-arg read uses the normal
2051+
// per-lock warning path.
2052+
for (auto *Arg : I->args())
2053+
warnIfMutexNotHeld(FSet, D, Exp, AK, Arg, PtPOK, nullptr,
2054+
Exp->getExprLoc());
2055+
} else {
2056+
// Multi-arg read: holding any one of the listed capabilities is
2057+
// sufficient (a writer must hold all, so any one prevents writes).
2058+
warnIfAnyMutexNotHeldForRead(FSet, D, Exp, I->args(), PtPOK,
2059+
Exp->getExprLoc());
2060+
}
2061+
}
20042062
}
20052063

20062064
/// Process a function call, method call, constructor call,

clang/lib/Sema/AnalysisBasedWarnings.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
#include "llvm/ADT/PostOrderIterator.h"
5656
#include "llvm/ADT/STLFunctionalExtras.h"
5757
#include "llvm/ADT/SmallVector.h"
58+
#include "llvm/ADT/StringExtras.h"
5859
#include "llvm/ADT/StringRef.h"
5960
#include "llvm/Support/Debug.h"
6061
#include "llvm/Support/TimeProfiler.h"
@@ -2141,6 +2142,39 @@ class ThreadSafetyReporter : public clang::threadSafety::ThreadSafetyHandler {
21412142
Warnings.emplace_back(std::move(Warning), getNotes());
21422143
}
21432144

2145+
void handleGuardedByAnyReadNotHeld(const NamedDecl *D,
2146+
ProtectedOperationKind POK,
2147+
ArrayRef<StringRef> LockNames,
2148+
SourceLocation Loc) override {
2149+
bool IsDeref;
2150+
switch (POK) {
2151+
case POK_VarAccess:
2152+
case POK_PassByRef:
2153+
case POK_ReturnByRef:
2154+
case POK_PassPointer:
2155+
case POK_ReturnPointer:
2156+
IsDeref = false;
2157+
break;
2158+
case POK_VarDereference:
2159+
case POK_PtPassByRef:
2160+
case POK_PtReturnByRef:
2161+
case POK_PtPassPointer:
2162+
case POK_PtReturnPointer:
2163+
IsDeref = true;
2164+
break;
2165+
case POK_FunctionCall:
2166+
llvm_unreachable("POK_FunctionCall not applicable here");
2167+
}
2168+
std::string Quoted;
2169+
llvm::raw_string_ostream OS(Quoted);
2170+
llvm::ListSeparator LS;
2171+
for (StringRef Name : LockNames)
2172+
OS << LS << "'" << Name << "'";
2173+
PartialDiagnosticAt Warning(Loc, S.PDiag(diag::warn_requires_any_of_locks)
2174+
<< D << IsDeref << Quoted);
2175+
Warnings.emplace_back(std::move(Warning), getNotes());
2176+
}
2177+
21442178
void handleMutexNotHeld(StringRef Kind, const NamedDecl *D,
21452179
ProtectedOperationKind POK, Name LockName,
21462180
LockKind LK, SourceLocation Loc,

clang/lib/Sema/SemaDeclAttr.cpp

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -460,36 +460,33 @@ static void handlePtGuardedVarAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
460460
}
461461

462462
static bool checkGuardedByAttrCommon(Sema &S, Decl *D, const ParsedAttr &AL,
463-
Expr *&Arg) {
464-
SmallVector<Expr *, 1> Args;
465-
// check that all arguments are lockable objects
466-
checkAttrArgsAreCapabilityObjs(S, D, AL, Args);
467-
unsigned Size = Args.size();
468-
if (Size != 1)
463+
SmallVectorImpl<Expr *> &Args) {
464+
if (!AL.checkAtLeastNumArgs(S, 1))
469465
return false;
470466

471-
Arg = Args[0];
472-
473-
return true;
467+
checkAttrArgsAreCapabilityObjs(S, D, AL, Args);
468+
return !Args.empty();
474469
}
475470

476471
static void handleGuardedByAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
477-
Expr *Arg = nullptr;
478-
if (!checkGuardedByAttrCommon(S, D, AL, Arg))
472+
SmallVector<Expr *, 1> Args;
473+
if (!checkGuardedByAttrCommon(S, D, AL, Args))
479474
return;
480475

481-
D->addAttr(::new (S.Context) GuardedByAttr(S.Context, AL, Arg));
476+
D->addAttr(::new (S.Context)
477+
GuardedByAttr(S.Context, AL, Args.data(), Args.size()));
482478
}
483479

484480
static void handlePtGuardedByAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
485-
Expr *Arg = nullptr;
486-
if (!checkGuardedByAttrCommon(S, D, AL, Arg))
481+
SmallVector<Expr *, 1> Args;
482+
if (!checkGuardedByAttrCommon(S, D, AL, Args))
487483
return;
488484

489485
if (!threadSafetyCheckIsPointer(S, D, AL))
490486
return;
491487

492-
D->addAttr(::new (S.Context) PtGuardedByAttr(S.Context, AL, Arg));
488+
D->addAttr(::new (S.Context)
489+
PtGuardedByAttr(S.Context, AL, Args.data(), Args.size()));
493490
}
494491

495492
static bool checkAcquireOrderAttrCommon(Sema &S, Decl *D, const ParsedAttr &AL,

clang/lib/Sema/SemaDeclCXX.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19476,9 +19476,9 @@ bool Sema::checkThisInStaticMemberFunctionAttributes(CXXMethodDecl *Method) {
1947619476
Expr *Arg = nullptr;
1947719477
ArrayRef<Expr *> Args;
1947819478
if (const auto *G = dyn_cast<GuardedByAttr>(A))
19479-
Arg = G->getArg();
19479+
Args = llvm::ArrayRef(G->args_begin(), G->args_size());
1948019480
else if (const auto *G = dyn_cast<PtGuardedByAttr>(A))
19481-
Arg = G->getArg();
19481+
Args = llvm::ArrayRef(G->args_begin(), G->args_size());
1948219482
else if (const auto *AA = dyn_cast<AcquiredAfterAttr>(A))
1948319483
Args = llvm::ArrayRef(AA->args_begin(), AA->args_size());
1948419484
else if (const auto *AB = dyn_cast<AcquiredBeforeAttr>(A))

0 commit comments

Comments
 (0)