Skip to content

Commit 33b910c

Browse files
authored
[clang] Fix the post-filtering heuristic for GSLPointer. (#114044)
The lifetime analyzer processes GSL pointers: - when encountering a constructor for a `gsl::pointer`, the analyzer continues traversing the constructor argument, regardless of whether the parameter has a `lifetimebound` annotation. This aims to catch cases where a GSL pointer is constructed from a GSL owner, either directly (e.g., `FooPointer(FooOwner)`) or through a chain of GSL pointers (e.g., `FooPointer(FooPointer(FooOwner))`); - When a temporary object is reported in the callback, the analyzer has heuristics to exclude non-owner types, aiming to avoid false positives (like `FooPointer(FooPointer())`). In the problematic case (discovered in #112751 (comment)) of `return foo.get();`: - When the analyzer reports the local object `foo`, the `Path` is `[GslPointerInit, Lifetimebound]`. - The `Path` goes through [`pathOnlyHandlesGslPointer`](https://github.com/llvm/llvm-project/blob/main/clang/lib/Sema/CheckExprLifetime.cpp#L1136) and isn’t filtered out by the [[heuristics]](because `foo` is an owner type), the analyzer treats it as the `FooPointer(FooOwner())` scenario, thus triggering a diagnostic. Filtering out base on the object 'foo' is wrong, because the GSLPointer is constructed from the return result of the `foo.get()`. The patch fixes this by teaching the heuristic to use the return result (only `const GSLOwner&` is considered) of the lifetimebound annotated function.
1 parent b03470b commit 33b910c

File tree

4 files changed

+187
-24
lines changed

4 files changed

+187
-24
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,8 @@ Improvements to Clang's diagnostics
614614

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

617+
- Fix false positives when `[[gsl::Owner/Pointer]]` and `[[clang::lifetimebound]]` are used together.
618+
617619
- Improved diagnostic message for ``__builtin_bit_cast`` size mismatch (#GH115870).
618620

619621
- Clang now omits shadow warnings for enum constants in separate class scopes (#GH62588).

clang/lib/Sema/CheckExprLifetime.cpp

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,8 @@ static bool shouldTrackImplicitObjectArg(const CXXMethodDecl *Callee) {
367367
if (Callee->getReturnType()->isReferenceType()) {
368368
if (!Callee->getIdentifier()) {
369369
auto OO = Callee->getOverloadedOperator();
370+
if (!Callee->getParent()->hasAttr<OwnerAttr>())
371+
return false;
370372
return OO == OverloadedOperatorKind::OO_Subscript ||
371373
OO == OverloadedOperatorKind::OO_Star;
372374
}
@@ -1152,6 +1154,86 @@ static bool pathOnlyHandlesGslPointer(const IndirectLocalPath &Path) {
11521154
}
11531155
return false;
11541156
}
1157+
// Result of analyzing the Path for GSLPointer.
1158+
enum AnalysisResult {
1159+
// Path does not correspond to a GSLPointer.
1160+
NotGSLPointer,
1161+
1162+
// A relevant case was identified.
1163+
Report,
1164+
// Stop the entire traversal.
1165+
Abandon,
1166+
// Skip this step and continue traversing inner AST nodes.
1167+
Skip,
1168+
};
1169+
// Analyze cases where a GSLPointer is initialized or assigned from a
1170+
// temporary owner object.
1171+
static AnalysisResult analyzePathForGSLPointer(const IndirectLocalPath &Path,
1172+
Local L) {
1173+
if (!pathOnlyHandlesGslPointer(Path))
1174+
return NotGSLPointer;
1175+
1176+
// At this point, Path represents a series of operations involving a
1177+
// GSLPointer, either in the process of initialization or assignment.
1178+
1179+
// Note: A LifetimeBoundCall can appear interleaved in this sequence.
1180+
// For example:
1181+
// const std::string& Ref(const std::string& a [[clang::lifetimebound]]);
1182+
// string_view abc = Ref(std::string());
1183+
// The "Path" is [GSLPointerInit, LifetimeboundCall], where "L" is the
1184+
// temporary "std::string()" object. We need to check the return type of the
1185+
// function with the lifetimebound attribute.
1186+
if (Path.back().Kind == IndirectLocalPathEntry::LifetimeBoundCall) {
1187+
// The lifetimebound applies to the implicit object parameter of a method.
1188+
const FunctionDecl *FD =
1189+
llvm::dyn_cast_or_null<FunctionDecl>(Path.back().D);
1190+
// The lifetimebound applies to a function parameter.
1191+
if (const auto *PD = llvm::dyn_cast<ParmVarDecl>(Path.back().D))
1192+
FD = llvm::dyn_cast<FunctionDecl>(PD->getDeclContext());
1193+
1194+
if (isa_and_present<CXXConstructorDecl>(FD)) {
1195+
// Constructor case: the parameter is annotated with lifetimebound
1196+
// e.g., GSLPointer(const S& s [[clang::lifetimebound]])
1197+
// We still respect this case even the type S is not an owner.
1198+
return Report;
1199+
}
1200+
// Check the return type, e.g.
1201+
// const GSLOwner& func(const Foo& foo [[clang::lifetimebound]])
1202+
// GSLPointer func(const Foo& foo [[clang::lifetimebound]])
1203+
if (FD &&
1204+
((FD->getReturnType()->isReferenceType() &&
1205+
isRecordWithAttr<OwnerAttr>(FD->getReturnType()->getPointeeType())) ||
1206+
isPointerLikeType(FD->getReturnType())))
1207+
return Report;
1208+
1209+
return Abandon;
1210+
}
1211+
1212+
if (isa<DeclRefExpr>(L)) {
1213+
// We do not want to follow the references when returning a pointer
1214+
// originating from a local owner to avoid the following false positive:
1215+
// int &p = *localUniquePtr;
1216+
// someContainer.add(std::move(localUniquePtr));
1217+
// return p;
1218+
if (!pathContainsInit(Path) && isRecordWithAttr<OwnerAttr>(L->getType()))
1219+
return Report;
1220+
return Abandon;
1221+
}
1222+
1223+
// The GSLPointer is from a temporary object.
1224+
auto *MTE = dyn_cast<MaterializeTemporaryExpr>(L);
1225+
1226+
bool IsGslPtrValueFromGslTempOwner =
1227+
MTE && !MTE->getExtendingDecl() &&
1228+
isRecordWithAttr<OwnerAttr>(MTE->getType());
1229+
// Skipping a chain of initializing gsl::Pointer annotated objects.
1230+
// We are looking only for the final source to find out if it was
1231+
// a local or temporary owner or the address of a local
1232+
// variable/param.
1233+
if (!IsGslPtrValueFromGslTempOwner)
1234+
return Skip;
1235+
return Report;
1236+
}
11551237

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

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

1192-
bool IsGslPtrValueFromGslTempOwner = false;
1193-
if (pathOnlyHandlesGslPointer(Path)) {
1194-
if (isa<DeclRefExpr>(L)) {
1195-
// We do not want to follow the references when returning a pointer
1196-
// originating from a local owner to avoid the following false positive:
1197-
// int &p = *localUniquePtr;
1198-
// someContainer.add(std::move(localUniquePtr));
1199-
// return p;
1200-
if (pathContainsInit(Path) ||
1201-
!isRecordWithAttr<OwnerAttr>(L->getType()))
1202-
return false;
1203-
} else {
1204-
IsGslPtrValueFromGslTempOwner =
1205-
MTE && !MTE->getExtendingDecl() &&
1206-
isRecordWithAttr<OwnerAttr>(MTE->getType());
1207-
// Skipping a chain of initializing gsl::Pointer annotated objects.
1208-
// We are looking only for the final source to find out if it was
1209-
// a local or temporary owner or the address of a local variable/param.
1210-
if (!IsGslPtrValueFromGslTempOwner)
1211-
return true;
1212-
}
1274+
bool IsGslPtrValueFromGslTempOwner = true;
1275+
switch (analyzePathForGSLPointer(Path, L)) {
1276+
case Abandon:
1277+
return false;
1278+
case Skip:
1279+
return true;
1280+
case NotGSLPointer:
1281+
IsGslPtrValueFromGslTempOwner = false;
1282+
LLVM_FALLTHROUGH;
1283+
case Report:
1284+
break;
12131285
}
12141286

12151287
switch (LK) {

clang/test/Sema/Inputs/lifetime-analysis.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ struct reference_wrapper {
128128
template<typename T>
129129
reference_wrapper<T> ref(T& t) noexcept;
130130

131+
template <typename T>
132+
struct [[gsl::Pointer]] iterator {
133+
T& operator*() const;
134+
};
135+
131136
struct false_type {
132137
static constexpr bool value = false;
133138
constexpr operator bool() const noexcept { return value; }

clang/test/Sema/warn-lifetime-analysis-nocfg.cpp

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -604,8 +604,9 @@ struct [[gsl::Pointer]] Span {
604604

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

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

653654
// Pointer<Owner>> from Owner<Pointer<Owner>>
654655
Span<std::string> test11(StatusOr<Span<std::string>> aa) {
655-
return aa.valueLB(); // expected-warning {{address of stack memory}}
656+
return aa.valueLB(); // OK
656657
return aa.valueNoLB(); // OK.
657658
}
658659

@@ -693,3 +694,86 @@ void test() {
693694
auto y = std::set<int>{}.begin(); // expected-warning {{object backing the pointer}}
694695
}
695696
} // namespace GH118064
697+
698+
namespace LifetimeboundInterleave {
699+
700+
const std::string& Ref(const std::string& abc [[clang::lifetimebound]]);
701+
702+
std::string_view TakeSv(std::string_view abc [[clang::lifetimebound]]);
703+
std::string_view TakeStrRef(const std::string& abc [[clang::lifetimebound]]);
704+
std::string_view TakeStr(std::string abc [[clang::lifetimebound]]);
705+
706+
std::string_view test1() {
707+
std::string_view t1 = Ref(std::string()); // expected-warning {{object backing}}
708+
t1 = Ref(std::string()); // expected-warning {{object backing}}
709+
return Ref(std::string()); // expected-warning {{returning address}}
710+
711+
std::string_view t2 = TakeSv(std::string()); // expected-warning {{object backing}}
712+
t2 = TakeSv(std::string()); // expected-warning {{object backing}}
713+
return TakeSv(std::string()); // expected-warning {{returning address}}
714+
715+
std::string_view t3 = TakeStrRef(std::string()); // expected-warning {{temporary}}
716+
t3 = TakeStrRef(std::string()); // expected-warning {{object backing}}
717+
return TakeStrRef(std::string()); // expected-warning {{returning address}}
718+
719+
720+
std::string_view t4 = TakeStr(std::string());
721+
t4 = TakeStr(std::string());
722+
return TakeStr(std::string());
723+
}
724+
725+
template <typename T>
726+
struct Foo {
727+
const T& get() const [[clang::lifetimebound]];
728+
const T& getNoLB() const;
729+
};
730+
std::string_view test2(Foo<std::string> r1, Foo<std::string_view> r2) {
731+
std::string_view t1 = Foo<std::string>().get(); // expected-warning {{object backing}}
732+
t1 = Foo<std::string>().get(); // expected-warning {{object backing}}
733+
return r1.get(); // expected-warning {{address of stack}}
734+
735+
std::string_view t2 = Foo<std::string_view>().get();
736+
t2 = Foo<std::string_view>().get();
737+
return r2.get();
738+
739+
// no warning on no-LB-annotated method.
740+
std::string_view t3 = Foo<std::string>().getNoLB();
741+
t3 = Foo<std::string>().getNoLB();
742+
return r1.getNoLB();
743+
}
744+
745+
struct Bar {};
746+
struct [[gsl::Pointer]] Pointer {
747+
Pointer(const Bar & bar [[clang::lifetimebound]]);
748+
};
749+
Pointer test3(Bar bar) {
750+
Pointer p = Pointer(Bar()); // expected-warning {{temporary}}
751+
p = Pointer(Bar()); // expected-warning {{object backing}}
752+
return bar; // expected-warning {{address of stack}}
753+
}
754+
755+
template<typename T>
756+
struct MySpan {
757+
MySpan(const std::vector<T>& v);
758+
using iterator = std::iterator<T>;
759+
iterator begin() const [[clang::lifetimebound]];
760+
};
761+
template <typename T>
762+
typename MySpan<T>::iterator ReturnFirstIt(const MySpan<T>& v [[clang::lifetimebound]]);
763+
764+
void test4() {
765+
std::vector<int> v{1};
766+
// MySpan<T> doesn't own any underlying T objects, the pointee object of
767+
// the MySpan iterator is still alive when the whole span is destroyed, thus
768+
// no diagnostic.
769+
const int& t1 = *MySpan<int>(v).begin();
770+
const int& t2 = *ReturnFirstIt(MySpan<int>(v));
771+
// Ideally, we would diagnose the following case, but due to implementation
772+
// constraints, we do not.
773+
const int& t4 = *MySpan<int>(std::vector<int>{}).begin();
774+
775+
auto it1 = MySpan<int>(v).begin(); // expected-warning {{temporary whose address is use}}
776+
auto it2 = ReturnFirstIt(MySpan<int>(v)); // expected-warning {{temporary whose address is used}}
777+
}
778+
779+
} // namespace LifetimeboundInterleave

0 commit comments

Comments
 (0)