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 9 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
41 changes: 33 additions & 8 deletions clang/include/clang/Basic/AttrDocs.td
Original file line number Diff line number Diff line change
Expand Up @@ -3922,16 +3922,41 @@ 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``.
parameter or implicit object parameter indicates that objects that are referred to by
that parameter may also be referred to by a capturing entity ``X``.

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

- A reference param is considered to refer to its referenced object.
- A pointer param is considered to refer to its pointee.
- 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.
- View type param (type annotated with ``[[gsl::Pointer()]]``) is considered to refer
to its pointee. 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``.

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
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>;
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>;

// 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
95 changes: 73 additions & 22 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 @@ -193,6 +197,7 @@ struct IndirectLocalPathEntry {
VarInit,
LValToRVal,
LifetimeBoundCall,
LifetimeCapture,
TemporaryCopy,
LambdaCaptureInit,
GslReferenceInit,
Expand Down Expand Up @@ -1049,6 +1054,7 @@ static SourceRange nextPathEntryRange(const IndirectLocalPath &Path, unsigned I,
case IndirectLocalPathEntry::AddressOf:
case IndirectLocalPathEntry::LValToRVal:
case IndirectLocalPathEntry::LifetimeBoundCall:
case IndirectLocalPathEntry::LifetimeCapture:
case IndirectLocalPathEntry::TemporaryCopy:
case IndirectLocalPathEntry::GslReferenceInit:
case IndirectLocalPathEntry::GslPointerInit:
Expand Down Expand Up @@ -1082,6 +1088,7 @@ static bool pathOnlyHandlesGslPointer(const IndirectLocalPath &Path) {
case IndirectLocalPathEntry::VarInit:
case IndirectLocalPathEntry::AddressOf:
case IndirectLocalPathEntry::LifetimeBoundCall:
case IndirectLocalPathEntry::LifetimeCapture:
continue;
case IndirectLocalPathEntry::GslPointerInit:
case IndirectLocalPathEntry::GslReferenceInit:
Expand Down Expand Up @@ -1110,13 +1117,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,6 +1207,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;
assert(shouldLifetimeExtendThroughPath(Path) ==
PathLifetimeKind::NoExtend &&
"No lifetime extension in function calls");
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;
Expand Down Expand Up @@ -1359,6 +1384,7 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
break;

case IndirectLocalPathEntry::LifetimeBoundCall:
case IndirectLocalPathEntry::LifetimeCapture:
case IndirectLocalPathEntry::TemporaryCopy:
case IndirectLocalPathEntry::GslPointerInit:
case IndirectLocalPathEntry::GslReferenceInit:
Expand Down Expand Up @@ -1412,17 +1438,34 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
return false;
};

bool HasReferenceBinding = Init->isGLValue();
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: {
Path.push_back({IndirectLocalPathEntry::LifetimeCapture, Init});
if (isRecordWithAttr<PointerAttr>(Init->getType()))
HasReferenceBinding = false;
// Skip the top MTE if it is a temporary object of the pointer-like type
// itself.
if (auto *MTE = dyn_cast<MaterializeTemporaryExpr>(Init);
MTE && isPointerLikeType(Init->getType()))
Init = MTE->getSubExpr();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that this code originated from my patch that added assignment support for "container." That patch has some issues: although it passed the unit tests, it failed to diagnose cases in real STL headers.

I'm not sure I fully understand the implementation here (my initial idea was to leverage the existing code paths for lifetimebound and gsl::Pointer, so the need for IndirectLocalPathEntry::LifetimeCapture isn’t clear). I experimented this patch, and made some tweaks to simplify the logic, and here’s the diff I came up with:

--- a/clang/lib/Sema/CheckExprLifetime.cpp
+++ b/clang/lib/Sema/CheckExprLifetime.cpp
@@ -1212,9 +1212,9 @@ checkExprLifetimeImpl(Sema &SemaRef, const InitializedEntity *InitEntity,
       // and the capturing entity does too, so don't warn.
       if (!MTE)
         return false;
-      assert(shouldLifetimeExtendThroughPath(Path) ==
-                 PathLifetimeKind::NoExtend &&
-             "No lifetime extension in function calls");
+      // assert(shouldLifetimeExtendThroughPath(Path) ==
+      //            PathLifetimeKind::NoExtend &&
+      //        "No lifetime extension in function calls");
       if (CapEntity->Entity)
         SemaRef.Diag(DiagLoc, diag::warn_dangling_reference_captured)
             << CapEntity->Entity << DiagRange;
@@ -1451,14 +1451,8 @@ checkExprLifetimeImpl(Sema &SemaRef, const InitializedEntity *InitEntity,
     break;
   }
   case LK_LifetimeCapture: {
-    Path.push_back({IndirectLocalPathEntry::LifetimeCapture, Init});
-    if (isRecordWithAttr<PointerAttr>(Init->getType()))
-      HasReferenceBinding = false;
-    // Skip the top MTE if it is a temporary object of the pointer-like type
-    // itself.
-    if (auto *MTE = dyn_cast<MaterializeTemporaryExpr>(Init);
-        MTE && isPointerLikeType(Init->getType()))
-      Init = MTE->getSubExpr();
+    if (isPointerLikeType(Init->getType()))
+      Path.push_back({IndirectLocalPathEntry::GslPointerInit, Init});
     break;
   }
   default:

The tests passed, and it also fixes the known false positives:

error: 'expected-warning' diagnostics expected but not seen: 
  File /workspace/llvm-project/clang/test/Sema/warn-lifetime-analysis-capture-by.cpp Line 313: captured
  File /workspace/llvm-project/clang/test/Sema/warn-lifetime-analysis-capture-by.cpp Line 314: captured
  File /workspace/llvm-project/clang/test/Sema/warn-lifetime-analysis-capture-by.cpp Line 318: captured

Would you mind taking it further?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. Simplified. I have kept the assert though since it is important to not lifetimeextend.

So the need for IndirectLocalPathEntry::LifetimeCapture isn’t clear.

I think it is important to stop lifetime extension.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assert is being triggered because Path is empty. It seems that lifetime extension is only relevant in the LK_Extended case, and doesn't apply to other cases where we don’t check it. I think we can just remove the assert. (we should probably do the same thing for GSLPointerAssignment, I think this one can be merged with the GSLPointerInit).

Additionally, I’d prefer not to introduce a new LifetimeCapture kind unless necessary. Adding it would increase code complexity, requiring updates across multiple switch (Kind) statements.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I see this method being used only for LK_Extended so this seems safe. I always disliked dealing with lifetime extension as part of this analysis as it is the only AST/codegen-side effect of this analysis.

But this feels safe now. Thanks for digging into this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the latest revision doesn't contain the change.

break;
}
default:
break;
}

if (Init->isGLValue())
if (HasReferenceBinding)
visitLocalsRetainedByReferenceBinding(Path, Init, RK_ReferenceBinding,
TemporaryVisitor);
else
Expand All @@ -1432,23 +1475,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 +1503,15 @@ 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) {
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
Loading
Loading