Skip to content

[clang] Implement lifetime analysis for lifetime_capture_by(X) #115921

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 additions & 9 deletions clang/include/clang/Basic/AttrDocs.td
Original file line number Diff line number Diff line change
Expand Up @@ -3921,17 +3921,42 @@ have their lifetimes extended.
def LifetimeCaptureByDocs : Documentation {
let Category = DocCatFunction;
let Content = [{
Similar to `lifetimebound`_, the ``lifetime_capture_by(X)`` attribute on a function
parameter or implicit object parameter indicates that that objects that are referred to
by that parameter may also be referred to by the capturing entity ``X``.
Similar to `lifetimebound`_, the ``lifetime_capture_by(X)`` attribute on a
function parameter or implicit object parameter indicates that the capturing
entity ``X`` may refer to the object referred by that parameter.

Below is a list of types of the parameters and what they're considered to refer to:

- A reference param (of non-view type) is considered to refer to its referenced object.
- A pointer param (of non-view type) is considered to refer to its pointee.
- View type param (type annotated with ``[[gsl::Pointer()]]``) is considered to refer
to its pointee (gsl owner). This holds true even if the view type appears as a reference
in the parameter. For example, both ``std::string_view`` and
``const std::string_view &`` are considered to refer to a ``std::string``.
- A ``std::initializer_list<T>`` is considered to refer to its underlying array.
- Aggregates (arrays and simple ``struct``\s) are considered to refer to all
objects that their transitive subobjects refer to.

Clang would diagnose when a temporary object is used as an argument to such an
annotated parameter.
In this case, the capturing entity ``X`` could capture a dangling reference to this
temporary object.

By default, a reference is considered to refer to its referenced object, a
pointer is considered to refer to its pointee, a ``std::initializer_list<T>``
is considered to refer to its underlying array, and aggregates (arrays and
simple ``struct``\s) are considered to refer to all objects that their
transitive subobjects refer to.
.. code-block:: c++

void addToSet(std::string_view a [[clang::lifetime_capture_by(s)]], std::set<std::string_view>& s) {
s.insert(a);
}
void use() {
std::set<std::string_view> s;
addToSet(std::string(), s); // Warning: object whose reference is captured by 's' will be destroyed at the end of the full-expression.
// ^^^^^^^^^^^^^
std::string local;
addToSet(local, s); // Ok.
}

The capturing entity ``X`` can be one of the following:

- Another (named) function parameter.

.. code-block:: c++
Expand All @@ -3951,7 +3976,7 @@ The capturing entity ``X`` can be one of the following:
std::set<std::string_view> s;
};

- 'global', 'unknown' (without quotes).
- `global`, `unknown`.

.. code-block:: c++

Expand Down Expand Up @@ -3983,6 +4008,22 @@ The attribute supports specifying more than one capturing entities:
s2.insert(a);
}

Limitation: The capturing entity ``X`` is not used by the analysis and is
used for documentation purposes only. This is because the analysis is
statement-local and only detects use of a temporary as an argument to the
annotated parameter.

.. code-block:: c++

void addToSet(std::string_view a [[clang::lifetime_capture_by(s)]], std::set<std::string_view>& s);
void use() {
std::set<std::string_view> s;
if (foo()) {
std::string str;
addToSet(str, s); // Not detected.
}
}

.. _`lifetimebound`: https://clang.llvm.org/docs/AttributeReference.html#lifetimebound
}];
}
Expand Down
2 changes: 2 additions & 0 deletions clang/include/clang/Basic/DiagnosticGroups.td
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ def ShiftOpParentheses: DiagGroup<"shift-op-parentheses">;
def OverloadedShiftOpParentheses: DiagGroup<"overloaded-shift-op-parentheses">;
def DanglingAssignment: DiagGroup<"dangling-assignment">;
def DanglingAssignmentGsl : DiagGroup<"dangling-assignment-gsl">;
def DanglingCapture : DiagGroup<"dangling-capture">;
def DanglingElse: DiagGroup<"dangling-else">;
def DanglingField : DiagGroup<"dangling-field">;
def DanglingInitializerList : DiagGroup<"dangling-initializer-list">;
Expand All @@ -462,6 +463,7 @@ def ReturnStackAddress : DiagGroup<"return-stack-address">;
def : DiagGroup<"return-local-addr", [ReturnStackAddress]>;
def Dangling : DiagGroup<"dangling", [DanglingAssignment,
DanglingAssignmentGsl,
DanglingCapture,
DanglingField,
DanglingInitializerList,
DanglingGsl,
Expand Down
11 changes: 9 additions & 2 deletions clang/include/clang/Basic/DiagnosticSemaKinds.td
Original file line number Diff line number Diff line change
Expand Up @@ -10132,10 +10132,11 @@ def err_lifetimebound_ctor_dtor : Error<
"%select{constructor|destructor}0">;
def err_lifetimebound_parameter_void_return_type : Error<
"'lifetimebound' attribute cannot be applied to a parameter of a function "
"that returns void">;
"that returns void; did you mean 'lifetime_capture_by(X)'">;
def err_lifetimebound_implicit_object_parameter_void_return_type : Error<
"'lifetimebound' attribute cannot be applied to an implicit object "
"parameter of a function that returns void">;
"parameter of a function that returns void; "
"did you mean 'lifetime_capture_by(X)'">;

// CHECK: returning address/reference of stack memory
def warn_ret_stack_addr_ref : Warning<
Expand Down Expand Up @@ -10230,6 +10231,12 @@ def warn_dangling_pointer_assignment : Warning<
"object backing %select{|the pointer }0%1 "
"will be destroyed at the end of the full-expression">,
InGroup<DanglingAssignment>;
def warn_dangling_reference_captured : Warning<
"object whose reference is captured by '%0' will be destroyed at the end of "
"the full-expression">, InGroup<DanglingCapture>, DefaultIgnore;
def warn_dangling_reference_captured_by_unknown : Warning<
"object whose reference is captured will be destroyed at the end of "
"the full-expression">, InGroup<DanglingCapture>, DefaultIgnore;

// For non-floating point, expressions of the form x == x or x != x
// should result in a warning, since these always evaluate to a constant.
Expand Down
3 changes: 3 additions & 0 deletions clang/include/clang/Sema/Sema.h
Original file line number Diff line number Diff line change
Expand Up @@ -2323,6 +2323,9 @@ class Sema final : public SemaBase {
bool BuiltinVectorMath(CallExpr *TheCall, QualType &Res, bool FPOnly = false);
bool BuiltinVectorToScalarMath(CallExpr *TheCall);

void checkLifetimeCaptureBy(FunctionDecl *FDecl, bool IsMemberFunction,
const Expr *ThisArg, ArrayRef<const Expr *> Args);

/// Handles the checks for format strings, non-POD arguments to vararg
/// functions, NULL arguments passed to non-NULL parameters, diagnose_if
/// attributes and AArch64 SME attributes.
Expand Down
87 changes: 63 additions & 24 deletions clang/lib/Sema/CheckExprLifetime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,14 @@ enum LifetimeKind {
/// a default member initializer), the program is ill-formed.
LK_MemInitializer,

/// The lifetime of a temporary bound to this entity probably ends too soon,
/// The lifetime of a temporary bound to this entity may end too soon,
/// because the entity is a pointer and we assign the address of a temporary
/// object to it.
LK_Assignment,

/// The lifetime of a temporary bound to this entity may end too soon,
/// because the entity may capture the reference to a temporary object.
LK_LifetimeCapture,
};
using LifetimeResult =
llvm::PointerIntPair<const InitializedEntity *, 3, LifetimeKind>;
Expand Down Expand Up @@ -1110,13 +1114,14 @@ static bool shouldRunGSLAssignmentAnalysis(const Sema &SemaRef,
isAssignmentOperatorLifetimeBound(Entity.AssignmentOperator)));
}

static void checkExprLifetimeImpl(Sema &SemaRef,
const InitializedEntity *InitEntity,
const InitializedEntity *ExtendingEntity,
LifetimeKind LK,
const AssignedEntity *AEntity, Expr *Init) {
assert((AEntity && LK == LK_Assignment) ||
(InitEntity && LK != LK_Assignment));
static void
checkExprLifetimeImpl(Sema &SemaRef, const InitializedEntity *InitEntity,
const InitializedEntity *ExtendingEntity, LifetimeKind LK,
const AssignedEntity *AEntity,
const CapturingEntity *CapEntity, Expr *Init) {
assert(!AEntity || LK == LK_Assignment);
assert(!CapEntity || LK == LK_LifetimeCapture);
assert(!InitEntity || (LK != LK_Assignment && LK != LK_LifetimeCapture));
// If this entity doesn't have an interesting lifetime, don't bother looking
// for temporaries within its initializer.
if (LK == LK_FullExpression)
Expand Down Expand Up @@ -1199,12 +1204,23 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
break;
}

case LK_LifetimeCapture: {
// The captured entity has lifetime beyond the full-expression,
// and the capturing entity does too, so don't warn.
if (!MTE)
return false;
if (CapEntity->Entity)
SemaRef.Diag(DiagLoc, diag::warn_dangling_reference_captured)
<< CapEntity->Entity << DiagRange;
else
SemaRef.Diag(DiagLoc, diag::warn_dangling_reference_captured_by_unknown)
<< DiagRange;
return false;
}

case LK_Assignment: {
if (!MTE || pathContainsInit(Path))
return false;
assert(shouldLifetimeExtendThroughPath(Path) ==
PathLifetimeKind::NoExtend &&
"No lifetime extension for assignments");
if (IsGslPtrValueFromGslTempOwner)
SemaRef.Diag(DiagLoc, diag::warn_dangling_lifetime_pointer_assignment)
<< AEntity->LHS << DiagRange;
Expand Down Expand Up @@ -1413,13 +1429,23 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
};

llvm::SmallVector<IndirectLocalPathEntry, 8> Path;
if (LK == LK_Assignment &&
shouldRunGSLAssignmentAnalysis(SemaRef, *AEntity)) {
Path.push_back(
{isAssignmentOperatorLifetimeBound(AEntity->AssignmentOperator)
? IndirectLocalPathEntry::LifetimeBoundCall
: IndirectLocalPathEntry::GslPointerAssignment,
Init});
switch (LK) {
case LK_Assignment: {
if (shouldRunGSLAssignmentAnalysis(SemaRef, *AEntity))
Path.push_back(
{isAssignmentOperatorLifetimeBound(AEntity->AssignmentOperator)
? IndirectLocalPathEntry::LifetimeBoundCall
: IndirectLocalPathEntry::GslPointerAssignment,
Init});
break;
}
case LK_LifetimeCapture: {
if (isPointerLikeType(Init->getType()))
Path.push_back({IndirectLocalPathEntry::GslPointerInit, Init});
break;
}
default:
break;
}

if (Init->isGLValue())
Expand All @@ -1432,23 +1458,23 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
/*RevisitSubinits=*/!InitEntity);
}

void checkExprLifetime(Sema &SemaRef, const InitializedEntity &Entity,
void checkInitLifetime(Sema &SemaRef, const InitializedEntity &Entity,
Expr *Init) {
auto LTResult = getEntityLifetime(&Entity);
LifetimeKind LK = LTResult.getInt();
const InitializedEntity *ExtendingEntity = LTResult.getPointer();
checkExprLifetimeImpl(SemaRef, &Entity, ExtendingEntity, LK,
/*AEntity*/ nullptr, Init);
/*AEntity=*/nullptr, /*CapEntity=*/nullptr, Init);
}

void checkExprLifetimeMustTailArg(Sema &SemaRef,
const InitializedEntity &Entity, Expr *Init) {
checkExprLifetimeImpl(SemaRef, &Entity, nullptr, LK_MustTail,
/*AEntity*/ nullptr, Init);
/*AEntity=*/nullptr, /*CapEntity=*/nullptr, Init);
}

void checkExprLifetime(Sema &SemaRef, const AssignedEntity &Entity,
Expr *Init) {
void checkAssignmentLifetime(Sema &SemaRef, const AssignedEntity &Entity,
Expr *Init) {
bool EnableDanglingPointerAssignment = !SemaRef.getDiagnostics().isIgnored(
diag::warn_dangling_pointer_assignment, SourceLocation());
bool RunAnalysis = (EnableDanglingPointerAssignment &&
Expand All @@ -1460,7 +1486,20 @@ void checkExprLifetime(Sema &SemaRef, const AssignedEntity &Entity,

checkExprLifetimeImpl(SemaRef, /*InitEntity=*/nullptr,
/*ExtendingEntity=*/nullptr, LK_Assignment, &Entity,
Init);
/*CapEntity=*/nullptr, Init);
}

void checkCaptureByLifetime(Sema &SemaRef, const CapturingEntity &Entity,
Expr *Init) {
if (SemaRef.getDiagnostics().isIgnored(diag::warn_dangling_reference_captured,
SourceLocation()) &&
SemaRef.getDiagnostics().isIgnored(
diag::warn_dangling_reference_captured_by_unknown, SourceLocation()))
return;
return checkExprLifetimeImpl(SemaRef, /*InitEntity=*/nullptr,
/*ExtendingEntity=*/nullptr, LK_LifetimeCapture,
/*AEntity=*/nullptr,
/*CapEntity=*/&Entity, Init);
}

} // namespace clang::sema
20 changes: 18 additions & 2 deletions clang/lib/Sema/CheckExprLifetime.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,31 @@ struct AssignedEntity {
CXXMethodDecl *AssignmentOperator = nullptr;
};

struct CapturingEntity {
// In an function call involving a lifetime capture, this would be the
// argument capturing the lifetime of another argument.
// void addToSet(std::string_view sv [[clang::lifetime_capture_by(setsv)]],
// set<std::string_view>& setsv);
// set<std::string_view> setsv;
// addToSet(std::string(), setsv); // Here 'setsv' is the 'Entity'.
//
// This is 'nullptr' when the capturing entity is 'global' or 'unknown'.
Expr *Entity = nullptr;
};

/// Check that the lifetime of the given expr (and its subobjects) is
/// sufficient for initializing the entity, and perform lifetime extension
/// (when permitted) if not.
void checkExprLifetime(Sema &SemaRef, const InitializedEntity &Entity,
void checkInitLifetime(Sema &SemaRef, const InitializedEntity &Entity,
Expr *Init);

/// Check that the lifetime of the given expr (and its subobjects) is
/// sufficient for assigning to the entity.
void checkExprLifetime(Sema &SemaRef, const AssignedEntity &Entity, Expr *Init);
void checkAssignmentLifetime(Sema &SemaRef, const AssignedEntity &Entity,
Expr *Init);

void checkCaptureByLifetime(Sema &SemaRef, const CapturingEntity &Entity,
Expr *Init);

/// Check that the lifetime of the given expr (and its subobjects) is
/// sufficient, assuming that it is passed as an argument to a musttail
Expand Down
45 changes: 44 additions & 1 deletion clang/lib/Sema/SemaChecking.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//
//===----------------------------------------------------------------------===//

#include "CheckExprLifetime.h"
#include "clang/AST/APValue.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Attr.h"
Expand Down Expand Up @@ -3229,6 +3230,47 @@ void Sema::CheckArgAlignment(SourceLocation Loc, NamedDecl *FDecl,
<< ParamName << (FDecl != nullptr) << FDecl;
}

void Sema::checkLifetimeCaptureBy(FunctionDecl *FD, bool IsMemberFunction,
const Expr *ThisArg,
ArrayRef<const Expr *> Args) {
if (!FD || Args.empty())
return;
auto GetArgAt = [&](int Idx) -> const Expr * {
if (Idx == LifetimeCaptureByAttr::GLOBAL ||
Idx == LifetimeCaptureByAttr::UNKNOWN)
return nullptr;
if (IsMemberFunction && Idx == 0)
return ThisArg;
return Args[Idx - IsMemberFunction];
};
auto HandleCaptureByAttr = [&](const LifetimeCaptureByAttr *Attr,
unsigned ArgIdx) {
if (!Attr)
return;
Expr *Captured = const_cast<Expr *>(GetArgAt(ArgIdx));
for (int CapturingParamIdx : Attr->params()) {
Expr *Capturing = const_cast<Expr *>(GetArgAt(CapturingParamIdx));
CapturingEntity CE{Capturing};
// Ensure that 'Captured' outlives the 'Capturing' entity.
checkCaptureByLifetime(*this, CE, Captured);
}
};
for (unsigned I = 0; I < FD->getNumParams(); ++I)
HandleCaptureByAttr(FD->getParamDecl(I)->getAttr<LifetimeCaptureByAttr>(),
I + IsMemberFunction);
// Check when the implicit object param is captured.
if (IsMemberFunction) {
TypeSourceInfo *TSI = FD->getTypeSourceInfo();
if (!TSI)
return;
AttributedTypeLoc ATL;
for (TypeLoc TL = TSI->getTypeLoc();
(ATL = TL.getAsAdjusted<AttributedTypeLoc>());
TL = ATL.getModifiedLoc())
HandleCaptureByAttr(ATL.getAttrAs<LifetimeCaptureByAttr>(), 0);
}
}

void Sema::checkCall(NamedDecl *FDecl, const FunctionProtoType *Proto,
const Expr *ThisArg, ArrayRef<const Expr *> Args,
bool IsMemberFunction, SourceLocation Loc,
Expand Down Expand Up @@ -3269,7 +3311,8 @@ void Sema::checkCall(NamedDecl *FDecl, const FunctionProtoType *Proto,
}
}
}

if (FD)
checkLifetimeCaptureBy(FD, IsMemberFunction, ThisArg, Args);
if (FDecl || Proto) {
CheckNonNullArguments(*this, FDecl, Proto, Args, Loc);

Expand Down
2 changes: 1 addition & 1 deletion clang/lib/Sema/SemaExpr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13822,7 +13822,7 @@ QualType Sema::CheckAssignmentOperands(Expr *LHSExpr, ExprResult &RHS,
CheckForNullPointerDereference(*this, LHSExpr);

AssignedEntity AE{LHSExpr};
checkExprLifetime(*this, AE, RHS.get());
checkAssignmentLifetime(*this, AE, RHS.get());

if (getLangOpts().CPlusPlus20 && LHSType.isVolatileQualified()) {
if (CompoundType.isNull()) {
Expand Down
Loading