Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#ifndef LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMEANNOTATIONS_H
#define LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMEANNOTATIONS_H

#include "clang/AST/Attr.h"
#include "clang/AST/DeclCXX.h"

namespace clang ::lifetimes {
Expand Down Expand Up @@ -45,6 +46,12 @@ bool isAssignmentOperatorLifetimeBound(const CXXMethodDecl *CMD);
/// method or because it's a normal assignment operator.
bool implicitObjectParamIsLifetimeBound(const FunctionDecl *FD);

/// Check if a function has a lifetimebound attribute on its function type
/// (which represents the implicit 'this' parameter for methods).
/// Returns the attribute if found, nullptr otherwise.
const LifetimeBoundAttr *
getLifetimeBoundAttrFromFunctionType(const TypeSourceInfo &TSI);

// Returns true if the implicit object argument (this) of a method call should
// be tracked for GSL lifetime analysis. This applies to STL methods that return
// pointers or references that depend on the lifetime of the object, such as
Expand Down
29 changes: 17 additions & 12 deletions clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,28 @@ bool isAssignmentOperatorLifetimeBound(const CXXMethodDecl *CMD) {
CMD->getParamDecl(0)->hasAttr<clang::LifetimeBoundAttr>();
}

const LifetimeBoundAttr *
getLifetimeBoundAttrFromFunctionType(const TypeSourceInfo &TSI) {
// Walk through the type layers looking for a lifetimebound attribute.
TypeLoc TL = TSI.getTypeLoc();
while (true) {
auto ATL = TL.getAsAdjusted<AttributedTypeLoc>();
if (!ATL)
break;
if (auto *LBAttr = ATL.getAttrAs<LifetimeBoundAttr>())
return LBAttr;
TL = ATL.getModifiedLoc();
}
return nullptr;
}

bool implicitObjectParamIsLifetimeBound(const FunctionDecl *FD) {
FD = getDeclWithMergedLifetimeBoundAttrs(FD);
const TypeSourceInfo *TSI = FD->getTypeSourceInfo();
if (!TSI)
return false;
// Don't declare this variable in the second operand of the for-statement;
// GCC miscompiles that by ending its lifetime before evaluating the
// third operand. See gcc.gnu.org/PR86769.
AttributedTypeLoc ATL;
for (TypeLoc TL = TSI->getTypeLoc();
(ATL = TL.getAsAdjusted<AttributedTypeLoc>());
TL = ATL.getModifiedLoc()) {
if (ATL.getAttrAs<clang::LifetimeBoundAttr>())
return true;
}

return isNormalAssignmentOperator(FD);
return getLifetimeBoundAttrFromFunctionType(*TSI) != nullptr ||
isNormalAssignmentOperator(FD);
}

bool isInStlNamespace(const Decl *D) {
Expand Down
46 changes: 40 additions & 6 deletions clang/lib/Sema/SemaDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "clang/AST/Randstruct.h"
#include "clang/AST/StmtCXX.h"
#include "clang/AST/Type.h"
#include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
#include "clang/Basic/Builtins.h"
#include "clang/Basic/DiagnosticComment.h"
#include "clang/Basic/HLSLRuntime.h"
Expand Down Expand Up @@ -4470,6 +4471,35 @@ bool Sema::MergeFunctionDecl(FunctionDecl *New, NamedDecl *&OldD, Scope *S,
return true;
}

/// Merge lifetimebound attribute on function type (implicit 'this')
/// from Old to New method declaration.
static void mergeLifetimeBoundAttrOnMethod(Sema &S, CXXMethodDecl *New,
const CXXMethodDecl *Old) {
const TypeSourceInfo *OldTSI = Old->getTypeSourceInfo();
const TypeSourceInfo *NewTSI = New->getTypeSourceInfo();

if (!OldTSI || !NewTSI)
return;

const LifetimeBoundAttr *OldLBAttr =
lifetimes::getLifetimeBoundAttrFromFunctionType(*OldTSI);
const LifetimeBoundAttr *NewLBAttr =
lifetimes::getLifetimeBoundAttrFromFunctionType(*NewTSI);

// If Old has lifetimebound but New doesn't, add it to New.
if (OldLBAttr && !NewLBAttr) {
QualType NewMethodType = New->getType();
QualType AttributedType =
S.Context.getAttributedType(OldLBAttr, NewMethodType, NewMethodType);
TypeLocBuilder TLB;
TLB.pushFullCopy(NewTSI->getTypeLoc());
AttributedTypeLoc TyLoc = TLB.push<AttributedTypeLoc>(AttributedType);
TyLoc.setAttr(OldLBAttr);
New->setType(AttributedType);
New->setTypeSourceInfo(TLB.getTypeSourceInfo(S.Context, AttributedType));
}
}
Comment on lines +4490 to +4501
Copy link
Contributor

Choose a reason for hiding this comment

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

What about the opposite scenario?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea is to read mostRecentDecl for analysis and it would reflect all the merged attributes seen until now.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we have an established convention how to deal with attributes? One way is just what @usx95 described, but another way is to use the canonical decl as the single source of truth.

Copy link
Contributor Author

@usx95 usx95 Jan 12, 2026

Choose a reason for hiding this comment

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

As far as merging declarations are concerned, it looks like this is the way that it has been done historically throughout this file, i.e., merging old decl into the new decl. E.g. docs for MergeCompatibleFunctionDecls

  /// Completes the merge of two function declarations that are
  /// known to be compatible.
  ///
  /// This routine handles the merging of attributes and other
  /// properties of function declarations from the old declaration to
  /// the new declaration, once we know that New is in fact a
  /// redeclaration of Old.
  ///
  /// \returns false
  bool MergeCompatibleFunctionDecls(FunctionDecl *New, FunctionDecl *Old,
                                    Scope *S, bool MergeTypeWithOld);

IIUC mostRecentDecl is the default decl that is retrieved:


/// Represents a function declaration or definition.
///
/// Since a given function can be declared several times in a program,
/// there may be several FunctionDecls that correspond to that
/// function. Only one of those FunctionDecls will be found when
/// traversing the list of declarations in the context of the
/// FunctionDecl (e.g., the translation unit); this FunctionDecl
/// contains all of the information known about the function. Other,
/// previous declarations of the function are available via the
/// getPreviousDecl() chain.
class FunctionDecl : public DeclaratorDecl,
                     public DeclContext,
                     public Redeclarable<FunctionDecl> {

I would be inclined to keep the same behaviour here and always use the mostRecentDecl (which should also be the default) to read the annotations.


bool Sema::MergeCompatibleFunctionDecls(FunctionDecl *New, FunctionDecl *Old,
Scope *S, bool MergeTypeWithOld) {
// Merge the attributes
Expand All @@ -4486,12 +4516,16 @@ bool Sema::MergeCompatibleFunctionDecls(FunctionDecl *New, FunctionDecl *Old,
// Merge attributes from the parameters. These can mismatch with K&R
// declarations.
if (New->getNumParams() == Old->getNumParams())
for (unsigned i = 0, e = New->getNumParams(); i != e; ++i) {
ParmVarDecl *NewParam = New->getParamDecl(i);
ParmVarDecl *OldParam = Old->getParamDecl(i);
mergeParamDeclAttributes(NewParam, OldParam, *this);
mergeParamDeclTypes(NewParam, OldParam, *this);
}
for (unsigned i = 0, e = New->getNumParams(); i != e; ++i) {
ParmVarDecl *NewParam = New->getParamDecl(i);
ParmVarDecl *OldParam = Old->getParamDecl(i);
mergeParamDeclAttributes(NewParam, OldParam, *this);
mergeParamDeclTypes(NewParam, OldParam, *this);
}

// Merge function type attributes (e.g., lifetimebound on implicit 'this').
if (auto *NewMethod = dyn_cast<CXXMethodDecl>(New))
mergeLifetimeBoundAttrOnMethod(*this, NewMethod, cast<CXXMethodDecl>(Old));

if (getLangOpts().CPlusPlus)
return MergeCXXFunctionDecl(New, Old, S);
Expand Down
138 changes: 138 additions & 0 deletions clang/test/Sema/warn-lifetime-analysis-nocfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -876,3 +876,141 @@ const char* foo() {
}

} // namespace GH127195

// Lifetimebound on definition vs declaration on implicit this param.
namespace GH175391 {
// Version A: Attribute on declaration only
class StringA {
public:
const char* data() const [[clang::lifetimebound]]; // Declaration with attribute
private:
char buffer[32] = "hello";
};
inline const char* StringA::data() const { // Definition WITHOUT attribute
return buffer;
}

// Version B: Attribute on definition only
class StringB {
public:
const char* data() const; // No attribute
private:
char buffer[32] = "hello";
};
inline const char* StringB::data() const [[clang::lifetimebound]] {
return buffer;
}

// Version C: Attribute on BOTH declaration and definition
class StringC {
public:
const char* data() const [[clang::lifetimebound]];
private:
char buffer[32] = "hello";
};
inline const char* StringC::data() const [[clang::lifetimebound]] {
return buffer;
}

// TEMPLATED VERSIONS

// Template Version A: Attribute on declaration only
template<typename T>
class StringTemplateA {
public:
const T* data() const [[clang::lifetimebound]]; // Declaration with attribute
private:
T buffer[32];
};
template<typename T>
inline const T* StringTemplateA<T>::data() const { // Definition WITHOUT attribute
return buffer;
}

// Template Version B: Attribute on definition only
template<typename T>
class StringTemplateB {
public:
const T* data() const; // No attribute
private:
T buffer[32];
};
template<typename T>
inline const T* StringTemplateB<T>::data() const [[clang::lifetimebound]] {
return buffer;
}

// Template Version C: Attribute on BOTH declaration and definition
template<typename T>
class StringTemplateC {
public:
const T* data() const [[clang::lifetimebound]];
private:
T buffer[32];
};
template<typename T>
inline const T* StringTemplateC<T>::data() const [[clang::lifetimebound]] {
return buffer;
}

// TEMPLATE SPECIALIZATION VERSIONS

// Template predeclarations for specializations
template<typename T> class StringTemplateSpecA;
template<typename T> class StringTemplateSpecB;
template<typename T> class StringTemplateSpecC;

// Template Specialization Version A: Attribute on declaration only - <char> specialization
template<>
class StringTemplateSpecA<char> {
public:
const char* data() const [[clang::lifetimebound]]; // Declaration with attribute
private:
char buffer[32] = "hello";
};
inline const char* StringTemplateSpecA<char>::data() const { // Definition WITHOUT attribute
return buffer;
}

// Template Specialization Version B: Attribute on definition only - <char> specialization
template<>
class StringTemplateSpecB<char> {
public:
const char* data() const; // No attribute
private:
char buffer[32] = "hello";
};
inline const char* StringTemplateSpecB<char>::data() const [[clang::lifetimebound]] {
return buffer;
}

// Template Specialization Version C: Attribute on BOTH declaration and definition - <char> specialization
template<>
class StringTemplateSpecC<char> {
public:
const char* data() const [[clang::lifetimebound]];
private:
char buffer[32] = "hello";
};
inline const char* StringTemplateSpecC<char>::data() const [[clang::lifetimebound]] {
return buffer;
}

void test() {
// Non-templated tests
const auto ptrA = StringA().data(); // Declaration-only attribute // expected-warning {{temporary whose address is used}}
const auto ptrB = StringB().data(); // Definition-only attribute // expected-warning {{temporary whose address is used}}
const auto ptrC = StringC().data(); // Both have attribute // expected-warning {{temporary whose address is used}}

// Templated tests (generic templates)
const auto ptrTA = StringTemplateA<char>().data(); // Declaration-only attribute // expected-warning {{temporary whose address is used}}
// FIXME: Definition is not instantiated until the end of TU. The attribute is not merged when this call is processed.
const auto ptrTB = StringTemplateB<char>().data(); // Definition-only attribute
const auto ptrTC = StringTemplateC<char>().data(); // Both have attribute // expected-warning {{temporary whose address is used}}

// Template specialization tests
const auto ptrTSA = StringTemplateSpecA<char>().data(); // Declaration-only attribute // expected-warning {{temporary whose address is used}}
const auto ptrTSB = StringTemplateSpecB<char>().data(); // Definition-only attribute // expected-warning {{temporary whose address is used}}
const auto ptrTSC = StringTemplateSpecC<char>().data(); // Both have attribute // expected-warning {{temporary whose address is used}}
}
} // namespace GH175391
22 changes: 22 additions & 0 deletions clang/test/Sema/warn-lifetime-safety.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1358,3 +1358,25 @@ void add(int c, MyObj* node) {
arr[4] = node;
}
} // namespace CppCoverage

// Implicit this annotations with redecls.
namespace GH172013 {
// https://github.com/llvm/llvm-project/issues/62072
// https://github.com/llvm/llvm-project/issues/172013
struct S {
View x() const [[clang::lifetimebound]];
MyObj i;
};

View S::x() const { return i; }

void bar() {
View x;
{
S s;
x = s.x(); // expected-warning {{object whose reference is captured does not live long enough}}
View y = S().x(); // FIXME: Handle temporaries.
} // expected-note {{destroyed here}}
(void)x; // expected-note {{used here}}
}
}
21 changes: 21 additions & 0 deletions clang/test/SemaCXX/attr-lifetimebound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,27 @@ namespace usage_ok {
r = A(1); // expected-warning {{object backing the pointer 'r' will be destroyed at the end of the full-expression}}
}

// Test that lifetimebound on implicit 'this' is propagated across redeclarations
struct B {
int *method() [[clang::lifetimebound]];
int i;
};
int *B::method() { return &i; }

// Test that lifetimebound on implicit 'this' is propagated across redeclarations
struct C {
int *method();
int i;
};
int *C::method() [[clang::lifetimebound]] { return &i; }

void test_lifetimebound_on_implicit_this() {
int *t = B().method(); // expected-warning {{temporary whose address is used as value of local variable 't' will be destroyed at the end of the full-expression}}
t = {B().method()}; // expected-warning {{object backing the pointer 't' will be destroyed at the end of the full-expression}}
t = C().method(); // expected-warning {{object backing the pointer 't' will be destroyed at the end of the full-expression}}
t = {C().method()}; // expected-warning {{object backing the pointer 't' will be destroyed at the end of the full-expression}}
}

struct FieldCheck {
struct Set {
int a;
Expand Down