Skip to content

Commit 1a2594c

Browse files
committed
Thread Safety Analysis: Support reentrant capabilities
Introduce the `reentrant_capability` attribute, which may be specified alongside the `capability(..)` attribute to denote that the defined capability type is reentrant. Marking a capability as reentrant means that acquiring the same capability multiple times is safe, and does not produce warnings on attempted re-acquisition. The most significant changes required are plumbing to propagate if the attribute is present to a CapabilityExpr, and then introducing a ReentrancyCount to FactEntry that can be incremented while a fact remains in the FactSet. Care was taken to avoid increasing the size of both CapabilityExpr and FactEntry by carefully allocating free bits of CapabilityExpr::CapExpr and the bitset respectively.
1 parent d3324c1 commit 1a2594c

14 files changed

+509
-51
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ Improvements to Clang's diagnostics
343343
as function arguments or return value respectively. Note that
344344
:doc:`ThreadSafetyAnalysis` still does not perform alias analysis. The
345345
feature will be default-enabled with ``-Wthread-safety`` in a future release.
346+
- The :doc:`ThreadSafetyAnalysis` now supports reentrant capabilities.
346347
- Clang will now do a better job producing common nested names, when producing
347348
common types for ternary operator, template argument deduction and multiple return auto deduction.
348349
- The ``-Wsign-compare`` warning now treats expressions with bitwise not(~) and minus(-) as signed integers

clang/docs/ThreadSafetyAnalysis.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,21 @@ class can be used as a capability. The string argument specifies the kind of
434434
capability in error messages, e.g. ``"mutex"``. See the ``Container`` example
435435
given above, or the ``Mutex`` class in :ref:`mutexheader`.
436436

437+
REENTRANT_CAPABILITY
438+
--------------------
439+
440+
``REENTRANT_CAPABILITY`` is an attribute on capability classes, denoting that
441+
they are reentrant. Marking a capability as reentrant means that acquiring the
442+
same capability multiple times is safe. Acquiring the same capability with
443+
different access privileges (exclusive vs. shared) again is not considered
444+
reentrant by the analysis.
445+
446+
Note: In many cases this attribute is only required where a capability is
447+
acquired reentrant within the same function, such as via macros or other
448+
helpers. Otherwise, best practice is to avoid explicitly acquiring a capability
449+
multiple times within the same function, and letting the analysis produce
450+
warnings on double-acquisition attempts.
451+
437452
.. _scoped_capability:
438453

439454
SCOPED_CAPABILITY
@@ -846,6 +861,9 @@ implementation.
846861
#define CAPABILITY(x) \
847862
THREAD_ANNOTATION_ATTRIBUTE__(capability(x))
848863

864+
#define REENTRANT_CAPABILITY \
865+
THREAD_ANNOTATION_ATTRIBUTE__(reentrant_capability)
866+
849867
#define SCOPED_CAPABILITY \
850868
THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable)
851869

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ class ThreadSafetyHandler {
278278
/// Warn that there is a cycle in acquired_before/after dependencies.
279279
virtual void handleBeforeAfterCycle(Name L1Name, SourceLocation Loc) {}
280280

281+
/// Warn that the maximum supported reentrancy depth has been reached.
282+
virtual void handleReentrancyDepthLimit(StringRef Kind, Name LockName,
283+
SourceLocation Loc) {}
284+
281285
/// Called by the analysis when starting analysis of a function.
282286
/// Used to issue suggestions for changes to annotations.
283287
virtual void enterFunction(const FunctionDecl *FD) {}

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,27 +272,32 @@ class CFGWalker {
272272
class CapabilityExpr {
273273
private:
274274
static constexpr unsigned FlagNegative = 1u << 0;
275+
static constexpr unsigned FlagReentrant = 1u << 1;
275276

276277
/// The capability expression and flags.
277-
llvm::PointerIntPair<const til::SExpr *, 1, unsigned> CapExpr;
278+
llvm::PointerIntPair<const til::SExpr *, 2, unsigned> CapExpr;
278279

279280
/// The kind of capability as specified by @ref CapabilityAttr::getName.
280281
StringRef CapKind;
281282

282283
public:
283284
CapabilityExpr() : CapExpr(nullptr, 0) {}
284-
CapabilityExpr(const til::SExpr *E, StringRef Kind, bool Neg)
285-
: CapExpr(E, Neg ? FlagNegative : 0), CapKind(Kind) {}
285+
CapabilityExpr(const til::SExpr *E, StringRef Kind, bool Neg, bool Reentrant)
286+
: CapExpr(E, (Neg ? FlagNegative : 0) | (Reentrant ? FlagReentrant : 0)),
287+
CapKind(Kind) {}
286288

287289
// Don't allow implicitly-constructed StringRefs since we'll capture them.
288-
template <typename T> CapabilityExpr(const til::SExpr *, T, bool) = delete;
290+
template <typename T>
291+
CapabilityExpr(const til::SExpr *, T, bool, bool) = delete;
289292

290293
const til::SExpr *sexpr() const { return CapExpr.getPointer(); }
291294
StringRef getKind() const { return CapKind; }
292295
bool negative() const { return CapExpr.getInt() & FlagNegative; }
296+
bool reentrant() const { return CapExpr.getInt() & FlagReentrant; }
293297

294298
CapabilityExpr operator!() const {
295-
return CapabilityExpr(CapExpr.getPointer(), CapKind, !negative());
299+
return CapabilityExpr(CapExpr.getPointer(), CapKind, !negative(),
300+
reentrant());
296301
}
297302

298303
bool equals(const CapabilityExpr &other) const {
@@ -390,7 +395,7 @@ class SExprBuilder {
390395
til::LiteralPtr *createVariable(const VarDecl *VD);
391396

392397
// Create placeholder for this: we don't know the VarDecl on construction yet.
393-
std::pair<til::LiteralPtr *, StringRef>
398+
std::pair<til::LiteralPtr *, CapabilityExpr>
394399
createThisPlaceholder(const Expr *Exp);
395400

396401
// Translate a clang statement or expression to a TIL expression.

clang/include/clang/Basic/Attr.td

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3990,6 +3990,13 @@ def LocksExcluded : InheritableAttr {
39903990
let Documentation = [Undocumented];
39913991
}
39923992

3993+
def ReentrantCapability : InheritableAttr {
3994+
let Spellings = [Clang<"reentrant_capability">];
3995+
let Subjects = SubjectList<[Record, TypedefName]>;
3996+
let Documentation = [Undocumented];
3997+
let SimpleHandler = 1;
3998+
}
3999+
39934000
// C/C++ consumed attributes.
39944001

39954002
def Consumable : InheritableAttr {

clang/include/clang/Basic/DiagnosticSemaKinds.td

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4105,6 +4105,9 @@ def warn_expect_more_underlying_mutexes : Warning<
41054105
def warn_expect_fewer_underlying_mutexes : Warning<
41064106
"did not expect %0 '%2' to be managed by '%1'">,
41074107
InGroup<ThreadSafetyAnalysis>, DefaultIgnore;
4108+
def warn_reentrancy_depth_limit : Warning<
4109+
"reentrancy depth limit reached for %0 '%1'">,
4110+
InGroup<ThreadSafetyAnalysis>, DefaultIgnore;
41084111
def note_managed_mismatch_here_for_param : Note<
41094112
"see attribute on parameter here">;
41104113

clang/lib/Analysis/ThreadSafety.cpp

Lines changed: 104 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,6 @@ class FactSet;
9999
/// particular point in program execution. Currently, a fact is a capability,
100100
/// along with additional information, such as where it was acquired, whether
101101
/// it is exclusive or shared, etc.
102-
///
103-
/// FIXME: this analysis does not currently support re-entrant locking.
104102
class FactEntry : public CapabilityExpr {
105103
public:
106104
enum FactEntryKind { Lockable, ScopedLockable };
@@ -114,31 +112,39 @@ class FactEntry : public CapabilityExpr {
114112
};
115113

116114
private:
117-
const FactEntryKind Kind : 8;
115+
const FactEntryKind Kind : 4;
118116

119117
/// Exclusive or shared.
120-
LockKind LKind : 8;
118+
const LockKind LKind : 4;
119+
120+
/// How it was acquired.
121+
const SourceKind Source : 4;
121122

122-
// How it was acquired.
123-
SourceKind Source : 8;
123+
/// Reentrancy depth; 16 bits should be enough given that FactID is a short,
124+
/// and thus we can't store more than 65536 facts anyway.
125+
unsigned int ReentrancyDepth : 16;
124126

125127
/// Where it was acquired.
126-
SourceLocation AcquireLoc;
128+
const SourceLocation AcquireLoc;
127129

128130
public:
129131
FactEntry(FactEntryKind FK, const CapabilityExpr &CE, LockKind LK,
130132
SourceLocation Loc, SourceKind Src)
131-
: CapabilityExpr(CE), Kind(FK), LKind(LK), Source(Src), AcquireLoc(Loc) {}
133+
: CapabilityExpr(CE), Kind(FK), LKind(LK), Source(Src),
134+
ReentrancyDepth(0), AcquireLoc(Loc) {}
132135
virtual ~FactEntry() = default;
133136

134137
LockKind kind() const { return LKind; }
135138
SourceLocation loc() const { return AcquireLoc; }
136139
FactEntryKind getFactEntryKind() const { return Kind; }
140+
unsigned int getReentrancyDepth() const { return ReentrancyDepth; }
137141

138142
bool asserted() const { return Source == Asserted; }
139143
bool declared() const { return Source == Declared; }
140144
bool managed() const { return Source == Managed; }
141145

146+
virtual std::unique_ptr<FactEntry> clone() const = 0;
147+
142148
virtual void
143149
handleRemovalFromIntersection(const FactSet &FSet, FactManager &FactMan,
144150
SourceLocation JoinLoc, LockErrorKind LEK,
@@ -155,6 +161,29 @@ class FactEntry : public CapabilityExpr {
155161
bool isAtLeast(LockKind LK) const {
156162
return (LKind == LK_Exclusive) || (LK == LK_Shared);
157163
}
164+
165+
// Return an updated FactEntry if we can acquire this capability reentrant,
166+
// nullptr otherwise. Returns nullopt if reentrancy depth limit was reached.
167+
[[nodiscard]] std::unique_ptr<FactEntry>
168+
tryReenter(LockKind ReenterKind) const {
169+
if (!reentrant())
170+
return nullptr;
171+
if (kind() != ReenterKind)
172+
return nullptr;
173+
auto NewFact = clone();
174+
NewFact->ReentrancyDepth++;
175+
return NewFact;
176+
}
177+
178+
// Return an updated FactEntry if we are releasing a capability previously
179+
// acquired reentrant, nullptr otherwise.
180+
[[nodiscard]] std::unique_ptr<FactEntry> leaveReentrant() const {
181+
if (!ReentrancyDepth)
182+
return nullptr;
183+
auto NewFact = clone();
184+
NewFact->ReentrancyDepth--;
185+
return NewFact;
186+
}
158187
};
159188

160189
using FactID = unsigned short;
@@ -168,6 +197,8 @@ class FactManager {
168197
public:
169198
FactID newFact(std::unique_ptr<FactEntry> Entry) {
170199
Facts.push_back(std::move(Entry));
200+
assert(Facts.size() - 1 <= std::numeric_limits<unsigned short>::max() &&
201+
"FactID space exhausted");
171202
return static_cast<unsigned short>(Facts.size() - 1);
172203
}
173204

@@ -235,6 +266,20 @@ class FactSet {
235266
return false;
236267
}
237268

269+
std::optional<FactID> replaceLock(FactManager &FM, iterator It,
270+
std::unique_ptr<FactEntry> Entry) {
271+
if (It == end())
272+
return std::nullopt;
273+
FactID F = FM.newFact(std::move(Entry));
274+
*It = F;
275+
return F;
276+
}
277+
278+
std::optional<FactID> replaceLock(FactManager &FM, const CapabilityExpr &CapE,
279+
std::unique_ptr<FactEntry> Entry) {
280+
return replaceLock(FM, findLockIter(FM, CapE), std::move(Entry));
281+
}
282+
238283
iterator findLockIter(FactManager &FM, const CapabilityExpr &CapE) {
239284
return std::find_if(begin(), end(), [&](FactID ID) {
240285
return FM[ID].matches(CapE);
@@ -864,6 +909,10 @@ class LockableFactEntry : public FactEntry {
864909
SourceKind Src = Acquired)
865910
: FactEntry(Lockable, CE, LK, Loc, Src) {}
866911

912+
std::unique_ptr<FactEntry> clone() const override {
913+
return std::make_unique<LockableFactEntry>(*this);
914+
}
915+
867916
void
868917
handleRemovalFromIntersection(const FactSet &FSet, FactManager &FactMan,
869918
SourceLocation JoinLoc, LockErrorKind LEK,
@@ -876,14 +925,28 @@ class LockableFactEntry : public FactEntry {
876925

877926
void handleLock(FactSet &FSet, FactManager &FactMan, const FactEntry &entry,
878927
ThreadSafetyHandler &Handler) const override {
879-
Handler.handleDoubleLock(entry.getKind(), entry.toString(), loc(),
880-
entry.loc());
928+
if (std::unique_ptr<FactEntry> ReentrantFact = tryReenter(entry.kind())) {
929+
if (ReentrantFact->getReentrancyDepth() == 0)
930+
Handler.handleReentrancyDepthLimit(entry.getKind(), entry.toString(),
931+
entry.loc());
932+
// This capability has been reentrantly acquired.
933+
FSet.replaceLock(FactMan, entry, std::move(ReentrantFact));
934+
} else {
935+
Handler.handleDoubleLock(entry.getKind(), entry.toString(), loc(),
936+
entry.loc());
937+
}
881938
}
882939

883940
void handleUnlock(FactSet &FSet, FactManager &FactMan,
884941
const CapabilityExpr &Cp, SourceLocation UnlockLoc,
885942
bool FullyRemove,
886943
ThreadSafetyHandler &Handler) const override {
944+
if (std::unique_ptr<FactEntry> ReentrantFact = leaveReentrant()) {
945+
// This capability remains reentrantly acquired.
946+
FSet.replaceLock(FactMan, Cp, std::move(ReentrantFact));
947+
return;
948+
}
949+
887950
FSet.removeLock(FactMan, Cp);
888951
if (!Cp.negative()) {
889952
FSet.addLock(FactMan, std::make_unique<LockableFactEntry>(
@@ -916,6 +979,10 @@ class ScopedLockableFactEntry : public FactEntry {
916979
SourceKind Src)
917980
: FactEntry(ScopedLockable, CE, LK_Exclusive, Loc, Src) {}
918981

982+
std::unique_ptr<FactEntry> clone() const override {
983+
return std::make_unique<ScopedLockableFactEntry>(*this);
984+
}
985+
919986
CapExprSet getUnderlyingMutexes() const {
920987
CapExprSet UnderlyingMutexesSet;
921988
for (const UnderlyingCapability &UnderlyingMutex : UnderlyingMutexes)
@@ -996,10 +1063,16 @@ class ScopedLockableFactEntry : public FactEntry {
9961063
void lock(FactSet &FSet, FactManager &FactMan, const CapabilityExpr &Cp,
9971064
LockKind kind, SourceLocation loc,
9981065
ThreadSafetyHandler *Handler) const {
999-
if (const FactEntry *Fact = FSet.findLock(FactMan, Cp)) {
1000-
if (Handler)
1001-
Handler->handleDoubleLock(Cp.getKind(), Cp.toString(), Fact->loc(),
1002-
loc);
1066+
if (const auto It = FSet.findLockIter(FactMan, Cp); It != FSet.end()) {
1067+
const FactEntry &Fact = FactMan[*It];
1068+
if (std::unique_ptr<FactEntry> ReentrantFact = Fact.tryReenter(kind)) {
1069+
if (ReentrantFact->getReentrancyDepth() == 0 && Handler)
1070+
Handler->handleReentrancyDepthLimit(Cp.getKind(), Cp.toString(), loc);
1071+
// This capability has been reentrantly acquired.
1072+
FSet.replaceLock(FactMan, It, std::move(ReentrantFact));
1073+
} else if (Handler) {
1074+
Handler->handleDoubleLock(Cp.getKind(), Cp.toString(), Fact.loc(), loc);
1075+
}
10031076
} else {
10041077
FSet.removeLock(FactMan, !Cp);
10051078
FSet.addLock(FactMan,
@@ -1009,10 +1082,17 @@ class ScopedLockableFactEntry : public FactEntry {
10091082

10101083
void unlock(FactSet &FSet, FactManager &FactMan, const CapabilityExpr &Cp,
10111084
SourceLocation loc, ThreadSafetyHandler *Handler) const {
1012-
if (FSet.findLock(FactMan, Cp)) {
1085+
if (const auto It = FSet.findLockIter(FactMan, Cp); It != FSet.end()) {
1086+
const FactEntry &Fact = FactMan[*It];
1087+
if (std::unique_ptr<FactEntry> ReentrantFact = Fact.leaveReentrant()) {
1088+
// This capability remains reentrantly acquired.
1089+
FSet.replaceLock(FactMan, It, std::move(ReentrantFact));
1090+
return;
1091+
}
1092+
10131093
FSet.removeLock(FactMan, Cp);
1014-
FSet.addLock(FactMan, std::make_unique<LockableFactEntry>(
1015-
!Cp, LK_Exclusive, loc));
1094+
FSet.addLock(FactMan,
1095+
std::make_unique<LockableFactEntry>(!Cp, LK_Exclusive, loc));
10161096
} else if (Handler) {
10171097
SourceLocation PrevLoc;
10181098
if (const FactEntry *Neg = FSet.findLock(FactMan, !Cp))
@@ -1306,7 +1386,6 @@ void ThreadSafetyAnalyzer::addLock(FactSet &FSet,
13061386
Entry->loc(), Entry->getKind());
13071387
}
13081388

1309-
// FIXME: Don't always warn when we have support for reentrant locks.
13101389
if (const FactEntry *Cp = FSet.findLock(FactMan, *Entry)) {
13111390
if (!Entry->asserted())
13121391
Cp->handleLock(FSet, FactMan, *Entry, Handler);
@@ -1831,15 +1910,15 @@ void BuildLockset::handleCall(const Expr *Exp, const NamedDecl *D,
18311910
assert(!Self);
18321911
const auto *TagT = Exp->getType()->getAs<TagType>();
18331912
if (D->hasAttrs() && TagT && Exp->isPRValue()) {
1834-
std::pair<til::LiteralPtr *, StringRef> Placeholder =
1913+
std::pair<til::LiteralPtr *, CapabilityExpr> Placeholder =
18351914
Analyzer->SxBuilder.createThisPlaceholder(Exp);
18361915
[[maybe_unused]] auto inserted =
18371916
Analyzer->ConstructedObjects.insert({Exp, Placeholder.first});
18381917
assert(inserted.second && "Are we visiting the same expression again?");
18391918
if (isa<CXXConstructExpr>(Exp))
18401919
Self = Placeholder.first;
18411920
if (TagT->getDecl()->hasAttr<ScopedLockableAttr>())
1842-
Scp = CapabilityExpr(Placeholder.first, Placeholder.second, false);
1921+
Scp = Placeholder.second;
18431922
}
18441923

18451924
assert(Loc.isInvalid());
@@ -1977,12 +2056,13 @@ void BuildLockset::handleCall(const Expr *Exp, const NamedDecl *D,
19772056
if (DeclaredLocks.empty())
19782057
continue;
19792058
CapabilityExpr Cp(Analyzer->SxBuilder.translate(Arg, nullptr),
1980-
StringRef("mutex"), false);
2059+
StringRef("mutex"), /*Neg=*/false, /*Reentrant=*/false);
19812060
if (const auto *CBTE = dyn_cast<CXXBindTemporaryExpr>(Arg->IgnoreCasts());
19822061
Cp.isInvalid() && CBTE) {
19832062
if (auto Object = Analyzer->ConstructedObjects.find(CBTE->getSubExpr());
19842063
Object != Analyzer->ConstructedObjects.end())
1985-
Cp = CapabilityExpr(Object->second, StringRef("mutex"), false);
2064+
Cp = CapabilityExpr(Object->second, StringRef("mutex"), /*Neg=*/false,
2065+
/*Reentrant=*/false);
19862066
}
19872067
const FactEntry *Fact = FSet.findLock(Analyzer->FactMan, Cp);
19882068
if (!Fact) {
@@ -2491,7 +2571,8 @@ void ThreadSafetyAnalyzer::runAnalysis(AnalysisDeclContext &AC) {
24912571
}
24922572
if (UnderlyingLocks.empty())
24932573
continue;
2494-
CapabilityExpr Cp(SxBuilder.createVariable(Param), StringRef(), false);
2574+
CapabilityExpr Cp(SxBuilder.createVariable(Param), StringRef(),
2575+
/*Neg=*/false, /*Reentrant=*/false);
24952576
auto ScopedEntry = std::make_unique<ScopedLockableFactEntry>(
24962577
Cp, Param->getLocation(), FactEntry::Declared);
24972578
for (const CapabilityExpr &M : UnderlyingLocks)

0 commit comments

Comments
 (0)