Skip to content

Commit c608a0b

Browse files
dmcardleAlexisPerry
authored andcommitted
[clang][ThreadSafety] Check trylock function success and return types (llvm#95290)
With this change, Clang will generate errors when trylock functions have improper return types. Today, it silently fails to apply the trylock attribute to these functions which may incorrectly lead users to believe they have correctly acquired locks before accessing guarded data. As a side effect of explicitly checking the success argument type, I seem to have fixed a false negative in the analysis that could occur when a trylock's success argument is an enumerator. I've added a regression test to warn-thread-safety-analysis.cpp named `TrylockSuccessEnumFalseNegative`. This change also improves the documentation with descriptions of of the subtle gotchas that arise from the analysis interpreting the success arg as a boolean. Issue llvm#92408
1 parent 22e8bf1 commit c608a0b

File tree

10 files changed

+369
-82
lines changed

10 files changed

+369
-82
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ C++ Specific Potentially Breaking Changes
7171
7272
To fix this, update libstdc++ to version 14.1.1 or greater.
7373

74+
- Clang now emits errors when Thread Safety Analysis trylock attributes are
75+
applied to functions or methods with incompatible return values, such as
76+
constructors, destructors, and void-returning functions. This only affects the
77+
``TRY_ACQUIRE`` and ``TRY_ACQUIRE_SHARED`` attributes (and any synonyms).
78+
7479
ABI Changes in This Version
7580
---------------------------
7681
- Fixed Microsoft name mangling of implicitly defined variables used for thread
@@ -729,6 +734,11 @@ Bug Fixes in This Version
729734

730735
- Fixed `static_cast` to array of unknown bound. Fixes (#GH62863).
731736

737+
- Clang's Thread Safety Analysis now evaluates trylock success arguments of enum
738+
types rather than silently defaulting to false. This fixes a class of false
739+
negatives where the analysis failed to detect unchecked access to guarded
740+
data.
741+
732742
Bug Fixes to Compiler Builtins
733743
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
734744

clang/docs/ThreadSafetyAnalysis.rst

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -420,10 +420,17 @@ TRY_ACQUIRE(<bool>, ...), TRY_ACQUIRE_SHARED(<bool>, ...)
420420
*Previously:* ``EXCLUSIVE_TRYLOCK_FUNCTION``, ``SHARED_TRYLOCK_FUNCTION``
421421

422422
These are attributes on a function or method that tries to acquire the given
423-
capability, and returns a boolean value indicating success or failure.
424-
The first argument must be ``true`` or ``false``, to specify which return value
425-
indicates success, and the remaining arguments are interpreted in the same way
426-
as ``ACQUIRE``. See :ref:`mutexheader`, below, for example uses.
423+
capability, and returns a boolean, integer, or pointer value indicating success
424+
or failure.
425+
426+
The attribute's first argument defines whether a zero or non-zero return value
427+
indicates success. Syntactically, it accepts ``NULL`` or ``nullptr``, ``bool``
428+
and ``int`` literals, as well as enumerator values. *The analysis only cares
429+
whether this success value is zero or non-zero.* This leads to some subtle
430+
consequences, discussed in the next section.
431+
432+
The remaining arguments are interpreted in the same way as ``ACQUIRE``. See
433+
:ref:`mutexheader`, below, for example uses.
427434

428435
Because the analysis doesn't support conditional locking, a capability is
429436
treated as acquired after the first branch on the return value of a try-acquire
@@ -445,6 +452,43 @@ function.
445452
}
446453
}
447454

455+
Subtle Consequences of Non-Boolean Success Values
456+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
457+
458+
The trylock attributes accept non-boolean expressions for the success value, but
459+
the analysis only cares whether the value is zero or non-zero.
460+
461+
Suppose you define an enum with two non-zero enumerators: ``LockAcquired = 1``
462+
and ``LockNotAcquired = 2``. If your trylock function returns ``LockAcquired``
463+
on success and ``LockNotAcquired`` on failure, the analysis may fail to detect
464+
access to guarded data without holding the mutex because they are both non-zero.
465+
466+
.. code-block:: c++
467+
// *** Beware: this code demonstrates incorrect usage. ***
468+
469+
enum TrylockResult { LockAcquired = 1, LockNotAcquired = 2 };
470+
471+
class CAPABILITY("mutex") Mutex {
472+
public:
473+
TrylockResult TryLock() TRY_ACQUIRE(LockAcquired);
474+
};
475+
476+
Mutex mu;
477+
int a GUARDED_BY(mu);
478+
479+
void foo() {
480+
if (mu.TryLock()) { // This branch satisfies the analysis, but permits
481+
// unguarded access to `a`!
482+
a = 0;
483+
mu.Unlock();
484+
}
485+
}
486+
487+
It's also possible to return a pointer from the trylock function. Similarly, all
488+
that matters is whether the success value is zero or non-zero. For instance, a
489+
success value of `true` means the function returns a non-null pointer on
490+
success.
491+
448492

449493
ASSERT_CAPABILITY(...) and ASSERT_SHARED_CAPABILITY(...)
450494
--------------------------------------------------------

clang/include/clang/Basic/DiagnosticSemaKinds.td

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3272,11 +3272,23 @@ def warn_aligned_attr_underaligned : Warning<err_alignas_underaligned.Summary>,
32723272
def err_attribute_sizeless_type : Error<
32733273
"%0 attribute cannot be applied to sizeless type %1">;
32743274
def err_attribute_argument_n_type : Error<
3275-
"%0 attribute requires parameter %1 to be %select{int or bool|an integer "
3276-
"constant|a string|an identifier|a constant expression|a builtin function}2">;
3275+
"%0 attribute requires parameter %1 to be %select{"
3276+
"int or bool"
3277+
"|an integer constant"
3278+
"|a string"
3279+
"|an identifier"
3280+
"|a constant expression"
3281+
"|a builtin function"
3282+
"|nullptr or a bool, int, or enum literal}2">;
32773283
def err_attribute_argument_type : Error<
3278-
"%0 attribute requires %select{int or bool|an integer "
3279-
"constant|a string|an identifier}1">;
3284+
"%0 attribute requires %select{"
3285+
"int or bool"
3286+
"|an integer constant"
3287+
"|a string"
3288+
"|an identifier"
3289+
"|a constant expression"
3290+
"|a builtin function"
3291+
"|a pointer or a bool, int, or enum literal}1">;
32803292
def err_attribute_argument_out_of_range : Error<
32813293
"%0 attribute requires integer constant between %1 and %2 inclusive">;
32823294
def err_init_priority_object_attr : Error<
@@ -3728,7 +3740,8 @@ def warn_attribute_wrong_decl_type : Warning<
37283740
"|types and namespaces"
37293741
"|variables, functions and classes"
37303742
"|kernel functions"
3731-
"|non-K&R-style functions}2">,
3743+
"|non-K&R-style functions"
3744+
"|functions that return bool, integer, or a pointer type}2">,
37323745
InGroup<IgnoredAttributes>;
37333746
def err_attribute_wrong_decl_type : Error<warn_attribute_wrong_decl_type.Summary>;
37343747
def warn_type_attribute_wrong_type : Warning<

clang/include/clang/Sema/ParsedAttr.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,7 @@ enum AttributeArgumentNType {
10831083
AANT_ArgumentIdentifier,
10841084
AANT_ArgumentConstantExpr,
10851085
AANT_ArgumentBuiltinFunction,
1086+
AANT_ArgumentNullptrOrBoolIntOrEnumLiteral,
10861087
};
10871088

10881089
/// These constants match the enumerated choices of
@@ -1101,6 +1102,7 @@ enum AttributeDeclKind {
11011102
ExpectedFunctionVariableOrClass,
11021103
ExpectedKernelFunction,
11031104
ExpectedFunctionWithProtoType,
1105+
ExpectedFunctionReturningPointerBoolIntOrEnum,
11041106
};
11051107

11061108
inline const StreamingDiagnostic &operator<<(const StreamingDiagnostic &DB,

clang/lib/Analysis/ThreadSafety.cpp

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include "clang/Basic/OperatorKinds.h"
3838
#include "clang/Basic/SourceLocation.h"
3939
#include "clang/Basic/Specifiers.h"
40+
#include "llvm/ADT/APSInt.h"
4041
#include "llvm/ADT/ArrayRef.h"
4142
#include "llvm/ADT/DenseMap.h"
4243
#include "llvm/ADT/ImmutableMap.h"
@@ -1034,9 +1035,10 @@ class ThreadSafetyAnalyzer {
10341035

10351036
template <class AttrType>
10361037
void getMutexIDs(CapExprSet &Mtxs, AttrType *Attr, const Expr *Exp,
1037-
const NamedDecl *D,
1038-
const CFGBlock *PredBlock, const CFGBlock *CurrBlock,
1039-
Expr *BrE, bool Neg);
1038+
const NamedDecl *D, const CFGBlock *PredBlock,
1039+
const CFGBlock *CurrBlock,
1040+
const ASTContext &TrylockAttrContext,
1041+
Expr *TrylockAttrSuccessValue, bool Neg);
10401042

10411043
const CallExpr* getTrylockCallExpr(const Stmt *Cond, LocalVarContext C,
10421044
bool &Negate);
@@ -1359,17 +1361,18 @@ void ThreadSafetyAnalyzer::getMutexIDs(CapExprSet &Mtxs, AttrType *Attr,
13591361
const Expr *Exp, const NamedDecl *D,
13601362
const CFGBlock *PredBlock,
13611363
const CFGBlock *CurrBlock,
1362-
Expr *BrE, bool Neg) {
1363-
// Find out which branch has the lock
1364-
bool branch = false;
1365-
if (const auto *BLE = dyn_cast_or_null<CXXBoolLiteralExpr>(BrE))
1366-
branch = BLE->getValue();
1367-
else if (const auto *ILE = dyn_cast_or_null<IntegerLiteral>(BrE))
1368-
branch = ILE->getValue().getBoolValue();
1369-
1370-
int branchnum = branch ? 0 : 1;
1371-
if (Neg)
1372-
branchnum = !branchnum;
1364+
const ASTContext &TrylockAttrContext,
1365+
Expr *TrylockAttrSuccess,
1366+
bool Neg) {
1367+
// Evaluate the trylock's success value as a boolean.
1368+
bool trylockSuccessValue = false;
1369+
if (!TrylockAttrSuccess->EvaluateAsBooleanCondition(
1370+
trylockSuccessValue, TrylockAttrContext,
1371+
/*InConstantContext=*/true)) {
1372+
llvm_unreachable("Trylock success value could not be evaluated.");
1373+
}
1374+
1375+
const int branchnum = Neg ? !!trylockSuccessValue : !trylockSuccessValue;
13731376

13741377
// If we've taken the trylock branch, then add the lock
13751378
int i = 0;
@@ -1390,8 +1393,15 @@ static bool getStaticBooleanValue(Expr *E, bool &TCond) {
13901393
} else if (const auto *ILE = dyn_cast<IntegerLiteral>(E)) {
13911394
TCond = ILE->getValue().getBoolValue();
13921395
return true;
1393-
} else if (auto *CE = dyn_cast<ImplicitCastExpr>(E))
1396+
} else if (auto *CE = dyn_cast<ImplicitCastExpr>(E)) {
13941397
return getStaticBooleanValue(CE->getSubExpr(), TCond);
1398+
} else if (auto *DRE = dyn_cast<DeclRefExpr>(E)) {
1399+
if (auto *ECD = dyn_cast<EnumConstantDecl>(DRE->getDecl())) {
1400+
llvm::APSInt IV = ECD->getInitVal();
1401+
TCond = IV.getBoolValue();
1402+
return true;
1403+
}
1404+
}
13951405
return false;
13961406
}
13971407

@@ -1497,27 +1507,27 @@ void ThreadSafetyAnalyzer::getEdgeLockset(FactSet& Result,
14971507
// If the condition is a call to a Trylock function, then grab the attributes
14981508
for (const auto *Attr : FunDecl->attrs()) {
14991509
switch (Attr->getKind()) {
1500-
case attr::TryAcquireCapability: {
1501-
auto *A = cast<TryAcquireCapabilityAttr>(Attr);
1502-
getMutexIDs(A->isShared() ? SharedLocksToAdd : ExclusiveLocksToAdd, A,
1503-
Exp, FunDecl, PredBlock, CurrBlock, A->getSuccessValue(),
1504-
Negate);
1505-
break;
1506-
};
1507-
case attr::ExclusiveTrylockFunction: {
1508-
const auto *A = cast<ExclusiveTrylockFunctionAttr>(Attr);
1509-
getMutexIDs(ExclusiveLocksToAdd, A, Exp, FunDecl, PredBlock, CurrBlock,
1510-
A->getSuccessValue(), Negate);
1511-
break;
1512-
}
1513-
case attr::SharedTrylockFunction: {
1514-
const auto *A = cast<SharedTrylockFunctionAttr>(Attr);
1515-
getMutexIDs(SharedLocksToAdd, A, Exp, FunDecl, PredBlock, CurrBlock,
1516-
A->getSuccessValue(), Negate);
1517-
break;
1518-
}
1519-
default:
1520-
break;
1510+
case attr::TryAcquireCapability: {
1511+
auto *A = cast<TryAcquireCapabilityAttr>(Attr);
1512+
getMutexIDs(A->isShared() ? SharedLocksToAdd : ExclusiveLocksToAdd, A,
1513+
Exp, FunDecl, PredBlock, CurrBlock, FunDecl->getASTContext(),
1514+
A->getSuccessValue(), Negate);
1515+
break;
1516+
};
1517+
case attr::ExclusiveTrylockFunction: {
1518+
const auto *A = cast<ExclusiveTrylockFunctionAttr>(Attr);
1519+
getMutexIDs(ExclusiveLocksToAdd, A, Exp, FunDecl, PredBlock, CurrBlock,
1520+
FunDecl->getASTContext(), A->getSuccessValue(), Negate);
1521+
break;
1522+
}
1523+
case attr::SharedTrylockFunction: {
1524+
const auto *A = cast<SharedTrylockFunctionAttr>(Attr);
1525+
getMutexIDs(SharedLocksToAdd, A, Exp, FunDecl, PredBlock, CurrBlock,
1526+
FunDecl->getASTContext(), A->getSuccessValue(), Negate);
1527+
break;
1528+
}
1529+
default:
1530+
break;
15211531
}
15221532
}
15231533

clang/lib/Sema/SemaDeclAttr.cpp

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "clang/AST/ASTContext.h"
1515
#include "clang/AST/ASTMutationListener.h"
1616
#include "clang/AST/CXXInheritance.h"
17+
#include "clang/AST/Decl.h"
1718
#include "clang/AST/DeclCXX.h"
1819
#include "clang/AST/DeclObjC.h"
1920
#include "clang/AST/DeclTemplate.h"
@@ -25,6 +26,7 @@
2526
#include "clang/Basic/CharInfo.h"
2627
#include "clang/Basic/Cuda.h"
2728
#include "clang/Basic/DarwinSDKInfo.h"
29+
#include "clang/Basic/DiagnosticSema.h"
2830
#include "clang/Basic/HLSLRuntime.h"
2931
#include "clang/Basic/IdentifierTable.h"
3032
#include "clang/Basic/LangOptions.h"
@@ -65,10 +67,13 @@
6567
#include "llvm/Demangle/Demangle.h"
6668
#include "llvm/IR/Assumptions.h"
6769
#include "llvm/MC/MCSectionMachO.h"
70+
#include "llvm/Support/Casting.h"
6871
#include "llvm/Support/Error.h"
72+
#include "llvm/Support/ErrorHandling.h"
6973
#include "llvm/Support/MathExtras.h"
7074
#include "llvm/Support/raw_ostream.h"
7175
#include "llvm/TargetParser/Triple.h"
76+
#include <cassert>
7277
#include <optional>
7378

7479
using namespace clang;
@@ -166,13 +171,6 @@ bool Sema::checkStringLiteralArgumentAttr(const ParsedAttr &AL, unsigned ArgNum,
166171
return checkStringLiteralArgumentAttr(AL, ArgExpr, Str, ArgLocation);
167172
}
168173

169-
/// Check if the passed-in expression is of type int or bool.
170-
static bool isIntOrBool(Expr *Exp) {
171-
QualType QT = Exp->getType();
172-
return QT->isBooleanType() || QT->isIntegerType();
173-
}
174-
175-
176174
// Check to see if the type is a smart pointer of some kind. We assume
177175
// it's a smart pointer if it defines both operator-> and operator*.
178176
static bool threadSafetyCheckIsSmartPointer(Sema &S, const RecordType* RT) {
@@ -608,15 +606,31 @@ static bool checkTryLockFunAttrCommon(Sema &S, Decl *D, const ParsedAttr &AL,
608606
if (!AL.checkAtLeastNumArgs(S, 1))
609607
return false;
610608

611-
if (!isIntOrBool(AL.getArgAsExpr(0))) {
609+
// The attribute's first argument defines the success value.
610+
const Expr *SuccessArg = AL.getArgAsExpr(0);
611+
if (!isa<CXXNullPtrLiteralExpr>(SuccessArg) &&
612+
!isa<GNUNullExpr>(SuccessArg) && !isa<CXXBoolLiteralExpr>(SuccessArg) &&
613+
!isa<IntegerLiteral>(SuccessArg) && !SuccessArg->getEnumConstantDecl()) {
612614
S.Diag(AL.getLoc(), diag::err_attribute_argument_n_type)
613-
<< AL << 1 << AANT_ArgumentIntOrBool;
615+
<< AL << 1 << AANT_ArgumentNullptrOrBoolIntOrEnumLiteral;
614616
return false;
615617
}
616618

617-
// check that all arguments are lockable objects
619+
// All remaining arguments must be lockable objects.
618620
checkAttrArgsAreCapabilityObjs(S, D, AL, Args, 1);
619621

622+
// The function must return a pointer, boolean, integer, or enum. We already
623+
// know that `D` is a function because `ExclusiveTrylockFunction` and friends
624+
// are defined in Attr.td with subject lists that only include functions.
625+
QualType ReturnType = D->getAsFunction()->getReturnType();
626+
if (!ReturnType->isPointerType() && !ReturnType->isBooleanType() &&
627+
!ReturnType->isIntegerType() && !ReturnType->isEnumeralType()) {
628+
S.Diag(AL.getLoc(), diag::err_attribute_wrong_decl_type)
629+
<< AL << AL.isRegularKeywordAttribute()
630+
<< ExpectedFunctionReturningPointerBoolIntOrEnum;
631+
return false;
632+
}
633+
620634
return true;
621635
}
622636

clang/test/Sema/attr-capabilities.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@ void Func18(void) __attribute__((release_capability())) {} // expected-warning {
5454
void Func19(void) __attribute__((release_shared_capability())) {} // expected-warning {{'release_shared_capability' attribute without capability arguments can only be applied to non-static methods of a class}}
5555
void Func20(void) __attribute__((release_generic_capability())) {} // expected-warning {{'release_generic_capability' attribute without capability arguments can only be applied to non-static methods of a class}}
5656

57-
void Func21(void) __attribute__((try_acquire_capability(1))) {} // expected-warning {{'try_acquire_capability' attribute without capability arguments can only be applied to non-static methods of a class}}
58-
void Func22(void) __attribute__((try_acquire_shared_capability(1))) {} // expected-warning {{'try_acquire_shared_capability' attribute without capability arguments can only be applied to non-static methods of a class}}
57+
int Func21(void) __attribute__((try_acquire_capability(1))) {} // expected-warning {{'try_acquire_capability' attribute without capability arguments can only be applied to non-static methods of a class}}
58+
int Func22(void) __attribute__((try_acquire_shared_capability(1))) {} // expected-warning {{'try_acquire_shared_capability' attribute without capability arguments can only be applied to non-static methods of a class}}
5959

60-
void Func23(void) __attribute__((try_acquire_capability(1, GUI))) {}
61-
void Func24(void) __attribute__((try_acquire_shared_capability(1, GUI))) {}
60+
int Func23(void) __attribute__((try_acquire_capability(1, GUI))) {}
61+
int Func24(void) __attribute__((try_acquire_shared_capability(1, GUI))) {}
6262

63-
void Func25(void) __attribute__((try_acquire_capability())) {} // expected-error {{'try_acquire_capability' attribute takes at least 1 argument}}
64-
void Func26(void) __attribute__((try_acquire_shared_capability())) {} // expected-error {{'try_acquire_shared_capability' attribute takes at least 1 argument}}
63+
int Func25(void) __attribute__((try_acquire_capability())) {} // expected-error {{'try_acquire_capability' attribute takes at least 1 argument}}
64+
int Func26(void) __attribute__((try_acquire_shared_capability())) {} // expected-error {{'try_acquire_shared_capability' attribute takes at least 1 argument}}
6565

6666
// Test that boolean logic works with capability attributes
6767
void Func27(void) __attribute__((requires_capability(!GUI)));

0 commit comments

Comments
 (0)