Skip to content

[clang] Fix the post-filtering heuristic for GSLPointer. #114044

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 6 commits into from
Dec 12, 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
2 changes: 2 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,8 @@ Improvements to Clang's diagnostics

- Clang now diagnoses ``[[deprecated]]`` attribute usage on local variables (#GH90073).

- Fix false positives when `[[gsl::Owner/Pointer]]` and `[[clang::lifetimebound]]` are used together.

- Improved diagnostic message for ``__builtin_bit_cast`` size mismatch (#GH115870).

- Clang now omits shadow warnings for enum constants in separate class scopes (#GH62588).
Expand Down
114 changes: 93 additions & 21 deletions clang/lib/Sema/CheckExprLifetime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@ static bool shouldTrackImplicitObjectArg(const CXXMethodDecl *Callee) {
if (Callee->getReturnType()->isReferenceType()) {
if (!Callee->getIdentifier()) {
auto OO = Callee->getOverloadedOperator();
if (!Callee->getParent()->hasAttr<OwnerAttr>())
return false;
return OO == OverloadedOperatorKind::OO_Subscript ||
OO == OverloadedOperatorKind::OO_Star;
}
Expand Down Expand Up @@ -1152,6 +1154,86 @@ static bool pathOnlyHandlesGslPointer(const IndirectLocalPath &Path) {
}
return false;
}
// Result of analyzing the Path for GSLPointer.
enum AnalysisResult {
// Path does not correspond to a GSLPointer.
NotGSLPointer,

// A relevant case was identified.
Report,
// Stop the entire traversal.
Abandon,
// Skip this step and continue traversing inner AST nodes.
Skip,
};
// Analyze cases where a GSLPointer is initialized or assigned from a
// temporary owner object.
static AnalysisResult analyzePathForGSLPointer(const IndirectLocalPath &Path,
Local L) {
if (!pathOnlyHandlesGslPointer(Path))
return NotGSLPointer;

// At this point, Path represents a series of operations involving a
// GSLPointer, either in the process of initialization or assignment.

// Note: A LifetimeBoundCall can appear interleaved in this sequence.
// For example:
// const std::string& Ref(const std::string& a [[clang::lifetimebound]]);
// string_view abc = Ref(std::string());
// The "Path" is [GSLPointerInit, LifetimeboundCall], where "L" is the
// temporary "std::string()" object. We need to check the return type of the
// function with the lifetimebound attribute.
if (Path.back().Kind == IndirectLocalPathEntry::LifetimeBoundCall) {
// The lifetimebound applies to the implicit object parameter of a method.
const FunctionDecl *FD =
llvm::dyn_cast_or_null<FunctionDecl>(Path.back().D);
// The lifetimebound applies to a function parameter.
if (const auto *PD = llvm::dyn_cast<ParmVarDecl>(Path.back().D))
FD = llvm::dyn_cast<FunctionDecl>(PD->getDeclContext());

if (isa_and_present<CXXConstructorDecl>(FD)) {
// Constructor case: the parameter is annotated with lifetimebound
// e.g., GSLPointer(const S& s [[clang::lifetimebound]])
// We still respect this case even the type S is not an owner.
return Report;
}
// Check the return type, e.g.
// const GSLOwner& func(const Foo& foo [[clang::lifetimebound]])
// GSLPointer func(const Foo& foo [[clang::lifetimebound]])
if (FD &&
((FD->getReturnType()->isReferenceType() &&
isRecordWithAttr<OwnerAttr>(FD->getReturnType()->getPointeeType())) ||
isPointerLikeType(FD->getReturnType())))
return Report;

return Abandon;
}

if (isa<DeclRefExpr>(L)) {
// We do not want to follow the references when returning a pointer
// originating from a local owner to avoid the following false positive:
// int &p = *localUniquePtr;
// someContainer.add(std::move(localUniquePtr));
// return p;
if (!pathContainsInit(Path) && isRecordWithAttr<OwnerAttr>(L->getType()))
return Report;
return Abandon;
}

// The GSLPointer is from a temporary object.
auto *MTE = dyn_cast<MaterializeTemporaryExpr>(L);

bool IsGslPtrValueFromGslTempOwner =
MTE && !MTE->getExtendingDecl() &&
isRecordWithAttr<OwnerAttr>(MTE->getType());
// Skipping a chain of initializing gsl::Pointer annotated objects.
// We are looking only for the final source to find out if it was
// a local or temporary owner or the address of a local
// variable/param.
if (!IsGslPtrValueFromGslTempOwner)
return Skip;
return Report;
}

static bool isAssignmentOperatorLifetimeBound(CXXMethodDecl *CMD) {
return CMD && isNormalAssignmentOperator(CMD) && CMD->param_size() == 1 &&
Expand Down Expand Up @@ -1189,27 +1271,17 @@ checkExprLifetimeImpl(Sema &SemaRef, const InitializedEntity *InitEntity,

auto *MTE = dyn_cast<MaterializeTemporaryExpr>(L);

bool IsGslPtrValueFromGslTempOwner = false;
if (pathOnlyHandlesGslPointer(Path)) {
if (isa<DeclRefExpr>(L)) {
// We do not want to follow the references when returning a pointer
// originating from a local owner to avoid the following false positive:
// int &p = *localUniquePtr;
// someContainer.add(std::move(localUniquePtr));
// return p;
if (pathContainsInit(Path) ||
!isRecordWithAttr<OwnerAttr>(L->getType()))
return false;
} else {
IsGslPtrValueFromGslTempOwner =
MTE && !MTE->getExtendingDecl() &&
isRecordWithAttr<OwnerAttr>(MTE->getType());
// Skipping a chain of initializing gsl::Pointer annotated objects.
// We are looking only for the final source to find out if it was
// a local or temporary owner or the address of a local variable/param.
if (!IsGslPtrValueFromGslTempOwner)
return true;
}
bool IsGslPtrValueFromGslTempOwner = true;
switch (analyzePathForGSLPointer(Path, L)) {
case Abandon:
return false;
case Skip:
return true;
case NotGSLPointer:
IsGslPtrValueFromGslTempOwner = false;
LLVM_FALLTHROUGH;
case Report:
break;
}

switch (LK) {
Expand Down
5 changes: 5 additions & 0 deletions clang/test/Sema/Inputs/lifetime-analysis.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ struct reference_wrapper {
template<typename T>
reference_wrapper<T> ref(T& t) noexcept;

template <typename T>
struct [[gsl::Pointer]] iterator {
T& operator*() const;
};

struct false_type {
static constexpr bool value = false;
constexpr operator bool() const noexcept { return value; }
Expand Down
90 changes: 87 additions & 3 deletions clang/test/Sema/warn-lifetime-analysis-nocfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -604,8 +604,9 @@ struct [[gsl::Pointer]] Span {

// Pointer from Owner<Pointer>
std::string_view test5() {
std::string_view a = StatusOr<std::string_view>().valueLB(); // expected-warning {{object backing the pointer will be dest}}
return StatusOr<std::string_view>().valueLB(); // expected-warning {{returning address of local temporary}}
// The Owner<Pointer> doesn't own the object which its inner pointer points to.
std::string_view a = StatusOr<std::string_view>().valueLB(); // OK
return StatusOr<std::string_view>().valueLB(); // OK

// No dangling diagnostics on non-lifetimebound methods.
std::string_view b = StatusOr<std::string_view>().valueNoLB();
Expand Down Expand Up @@ -652,7 +653,7 @@ Span<std::string> test10(StatusOr<std::vector<std::string>> aa) {

// Pointer<Owner>> from Owner<Pointer<Owner>>
Span<std::string> test11(StatusOr<Span<std::string>> aa) {
return aa.valueLB(); // expected-warning {{address of stack memory}}
return aa.valueLB(); // OK
return aa.valueNoLB(); // OK.
}

Expand Down Expand Up @@ -693,3 +694,86 @@ void test() {
auto y = std::set<int>{}.begin(); // expected-warning {{object backing the pointer}}
}
} // namespace GH118064

namespace LifetimeboundInterleave {

const std::string& Ref(const std::string& abc [[clang::lifetimebound]]);

std::string_view TakeSv(std::string_view abc [[clang::lifetimebound]]);
std::string_view TakeStrRef(const std::string& abc [[clang::lifetimebound]]);
std::string_view TakeStr(std::string abc [[clang::lifetimebound]]);

std::string_view test1() {
std::string_view t1 = Ref(std::string()); // expected-warning {{object backing}}
t1 = Ref(std::string()); // expected-warning {{object backing}}
return Ref(std::string()); // expected-warning {{returning address}}

std::string_view t2 = TakeSv(std::string()); // expected-warning {{object backing}}
t2 = TakeSv(std::string()); // expected-warning {{object backing}}
return TakeSv(std::string()); // expected-warning {{returning address}}

std::string_view t3 = TakeStrRef(std::string()); // expected-warning {{temporary}}
t3 = TakeStrRef(std::string()); // expected-warning {{object backing}}
return TakeStrRef(std::string()); // expected-warning {{returning address}}


Copy link
Contributor

Choose a reason for hiding this comment

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

nit: delete extra new line

std::string_view t4 = TakeStr(std::string());
t4 = TakeStr(std::string());
return TakeStr(std::string());
}

template <typename T>
struct Foo {
const T& get() const [[clang::lifetimebound]];
const T& getNoLB() const;
};
std::string_view test2(Foo<std::string> r1, Foo<std::string_view> r2) {
std::string_view t1 = Foo<std::string>().get(); // expected-warning {{object backing}}
t1 = Foo<std::string>().get(); // expected-warning {{object backing}}
return r1.get(); // expected-warning {{address of stack}}

std::string_view t2 = Foo<std::string_view>().get();
t2 = Foo<std::string_view>().get();
return r2.get();

// no warning on no-LB-annotated method.
std::string_view t3 = Foo<std::string>().getNoLB();
t3 = Foo<std::string>().getNoLB();
return r1.getNoLB();
}

struct Bar {};
struct [[gsl::Pointer]] Pointer {
Pointer(const Bar & bar [[clang::lifetimebound]]);
};
Pointer test3(Bar bar) {
Pointer p = Pointer(Bar()); // expected-warning {{temporary}}
p = Pointer(Bar()); // expected-warning {{object backing}}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This was a false negative before, this patch also fixes it.

return bar; // expected-warning {{address of stack}}
}

template<typename T>
struct MySpan {
MySpan(const std::vector<T>& v);
using iterator = std::iterator<T>;
iterator begin() const [[clang::lifetimebound]];
};
template <typename T>
typename MySpan<T>::iterator ReturnFirstIt(const MySpan<T>& v [[clang::lifetimebound]]);

void test4() {
std::vector<int> v{1};
// MySpan<T> doesn't own any underlying T objects, the pointee object of
// the MySpan iterator is still alive when the whole span is destroyed, thus
// no diagnostic.
const int& t1 = *MySpan<int>(v).begin();
Comment on lines +755 to +769
Copy link
Contributor

Choose a reason for hiding this comment

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

I have mixed feelings about this.
I feel MySpan should be a gsl::Pointer in this case and we should deal view types similarly.
Doing this only for owner types and that too only implicit this doesn't make sense to me.

For example, this silences the warning on:
const int& t1 = *MySpan<int>(v).begin();
But what about non member functions ? :

iterator getBeginOff(const MySpan<int>& ms [[clang::lifetimebound]]);
const int& t1 = *getBeginOf(MySpan<int>(v));

(Can you also add this test case?)

Copy link
Collaborator Author

@hokein hokein Nov 19, 2024

Choose a reason for hiding this comment

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

We have this test case already, see the t2.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks.

const int& t2 = *ReturnFirstIt(MySpan<int>(v));
// Ideally, we would diagnose the following case, but due to implementation
// constraints, we do not.
const int& t4 = *MySpan<int>(std::vector<int>{}).begin();

auto it1 = MySpan<int>(v).begin(); // expected-warning {{temporary whose address is use}}
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think this is correct behaviour ?
Specially what about when MySpan is also a pointer ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, this behavior is expected per the lifetimebound annotation.

Specifically, what about when MySpan is also a pointer?

In this case, whether MySpan is a pointer or not doesn’t really change the situation. The begin() method is annotated with lifetimebound, which conceptually means that the returned iterator is tied to the lifetime of the MySpan object (e.g. the iterator has an internal pointer that refers to this).
It expresses that the iterator becomes invalid once the MySpan object is destroyed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm. Well if both iterator and MySpan are pointers then we could make it not warn. But I agree that this is expanding the scope of lifetimebound definition to places where it is not possible. Lifetimes of internal subobjects probably has a better solution with rust-like lifetimes/type system.
Thanks.

auto it2 = ReturnFirstIt(MySpan<int>(v)); // expected-warning {{temporary whose address is used}}
}

} // namespace LifetimeboundInterleave
Loading