Skip to content

Commit 954a428

Browse files
committed
Refine the heuristic on inferring the nested pointer onwnership.
Fix another new false positive
1 parent 1dbf4a8 commit 954a428

File tree

2 files changed

+117
-14
lines changed

2 files changed

+117
-14
lines changed

clang/lib/Sema/CheckExprLifetime.cpp

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,18 @@ static bool isContainerOfPointer(const RecordDecl *Container) {
285285
}
286286
return false;
287287
}
288+
static bool isContainerOfOwner(const RecordDecl *Container) {
289+
if (const auto *CTSD =
290+
dyn_cast_if_present<ClassTemplateSpecializationDecl>(Container)) {
291+
if (!CTSD->hasAttr<OwnerAttr>()) // Container must be a GSL owner type.
292+
return false;
293+
const auto &TAs = CTSD->getTemplateArgs();
294+
return TAs.size() > 0 && TAs[0].getKind() == TemplateArgument::Type &&
295+
isRecordWithAttr<OwnerAttr>(TAs[0].getAsType());
296+
}
297+
return false;
298+
}
299+
288300
// Returns true if the given Record is `std::initializer_list<pointer>`.
289301
static bool isStdInitializerListOfPointer(const RecordDecl *RD) {
290302
if (const auto *CTSD =
@@ -357,35 +369,101 @@ static bool shouldTrackFirstArgument(const FunctionDecl *FD) {
357369
return false;
358370
}
359371

372+
// Returns true if the given constructor is a copy-like constructor, such as
373+
// `Ctor(Owner<U>&&)` or `Ctor(const Owner<U>&)`.
374+
static bool isCopyLikeConstructor(const CXXConstructorDecl *Ctor) {
375+
if (!Ctor || Ctor->param_size() != 1)
376+
return false;
377+
const auto *ParamRefType =
378+
Ctor->getParamDecl(0)->getType()->getAs<ReferenceType>();
379+
if (!ParamRefType)
380+
return false;
381+
382+
// Check if the first parameter type "Owner<U>".
383+
if (const auto *TST =
384+
ParamRefType->getPointeeType()->getAs<TemplateSpecializationType>())
385+
return TST->getTemplateName()
386+
.getAsTemplateDecl()
387+
->getTemplatedDecl()
388+
->hasAttr<OwnerAttr>();
389+
return false;
390+
}
391+
360392
// Returns true if we should perform the GSL analysis on the first argument for
361393
// the given constructor.
362394
static bool
363395
shouldTrackFirstArgumentForConstructor(const CXXConstructExpr *Ctor) {
364-
const auto *ClassD = Ctor->getConstructor()->getParent();
396+
const auto *LHSRecordDecl = Ctor->getConstructor()->getParent();
365397

366398
// Case 1, construct a GSL pointer, e.g. std::string_view
367-
if (ClassD->hasAttr<PointerAttr>())
399+
// Always inspect when LHS is a pointer.
400+
if (LHSRecordDecl->hasAttr<PointerAttr>())
368401
return true;
369402

370-
auto FirstArgType = Ctor->getArg(0)->getType();
371-
// Case 2, construct a container of pointer (std::vector<std::string_view>)
372-
// from a std::initilizer_list or an GSL owner
373403
if (Ctor->getConstructor()->getNumParams() != 1 ||
374-
!isContainerOfPointer(ClassD))
404+
!isContainerOfPointer(LHSRecordDecl))
375405
return false;
376406

377-
// For the typical case: `std::vector<std::string_view> abc = {std::string()};`
407+
// Now, the LHS is an Owner<Pointer> type, e.g., std::vector<string_view>.
408+
//
409+
// At a high level, we cannot precisely determine what the nested pointer
410+
// owns. However, by analyzing the RHS owner type, we can use heuristics to
411+
// infer ownership information. These heuristics are designed to be
412+
// conservative, minimizing false positives while still providing meaningful
413+
// diagnostics.
414+
//
415+
// While this inference isn't perfect, it helps catch common use-after-free
416+
// patterns.
417+
auto RHSArgType = Ctor->getArg(0)->getType();
418+
const auto *RHSRD = RHSArgType->getAsRecordDecl();
419+
// LHS is constructed from an intializer_list.
420+
//
378421
// std::initializer_list is a proxy object that provides access to the backing
379422
// array. We perform analysis on it to determine if there are any dangling
380423
// temporaries in the backing array.
381-
if (isStdInitializerListOfPointer(FirstArgType->getAsRecordDecl()))
424+
// E.g. std::vector<string_view> abc = {string()};
425+
if (isStdInitializerListOfPointer(RHSRD))
426+
return true;
427+
428+
// RHS must be an owner.
429+
if (!isRecordWithAttr<OwnerAttr>(RHSArgType))
430+
return false;
431+
432+
// Bail out if the RHS is Owner<Pointer>.
433+
//
434+
// We cannot reliably determine what the LHS nested pointer owns -- it could
435+
// be the entire RHS or the nested pointer in RHS. To avoid false positives,
436+
// we skip this case, such as:
437+
// std::stack<std::string_view> s(std::deque<std::string_view>{});
438+
//
439+
// TODO: this also has a false negative, it doesn't catch the case like:
440+
// std::optional<span<int*>> os = std::vector<int*>{}
441+
if (isContainerOfPointer(RHSRD))
442+
return false;
443+
444+
// Assume that the nested Pointer is constructed from the nested Owner.
445+
// E.g. std::optional<string_view> sv = std::optional<string>(s);
446+
if (isContainerOfOwner(RHSRD))
382447
return true;
383-
// For the case: `std::optional<std::string_view> abc = std::string();`
384-
// When constructing from a container of pointers, it's less likely to result
385-
// in a dangling pointer. Therefore, we try to be conservative to not track
386-
// the argument futher to avoid false positives.
387-
return isRecordWithAttr<OwnerAttr>(FirstArgType) &&
388-
!isContainerOfPointer(FirstArgType->getAsRecordDecl());
448+
449+
// Now, the LHS is an Owner<Pointer> and the RHS is an Owner<X>, where X is
450+
// neither an `Owner` nor a `Pointer`.
451+
//
452+
// Use the constructor's signature as a hint. If it is a copy-like constructor
453+
// `Owner1<Pointer>(Owner2<X>&&)`, we assume that the nested pointer is
454+
// constructed from X. In such cases, we do not diagnose, as `X` is not an
455+
// owner, e.g.
456+
// std::optional<string_view> sv = std::optional<Foo>();
457+
if (const auto *PrimaryCtorTemplate =
458+
Ctor->getConstructor()->getPrimaryTemplate();
459+
PrimaryCtorTemplate &&
460+
isCopyLikeConstructor(dyn_cast_if_present<CXXConstructorDecl>(
461+
PrimaryCtorTemplate->getTemplatedDecl()))) {
462+
return false;
463+
}
464+
// Assume that the nested pointer is constructed from the whole RHS.
465+
// E.g. optional<string_view> s = std::string();
466+
return true;
389467
}
390468

391469
// Return true if this is an "normal" assignment operator.

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,12 +655,37 @@ std::vector<std::string_view> test2(int i) {
655655
return std::vector<std::string_view>(t.begin(), t.end());
656656
}
657657

658+
class Foo {
659+
public:
660+
operator std::string_view() const { return ""; }
661+
};
662+
class [[gsl::Owner]] FooOwner {
663+
public:
664+
operator std::string_view() const { return ""; }
665+
};
666+
std::optional<Foo> GetFoo();
667+
std::optional<FooOwner> GetFooOwner();
668+
669+
template <typename T>
670+
struct [[gsl::Owner]] Container1 {
671+
Container1();
672+
};
673+
template <typename T>
674+
struct [[gsl::Owner]] Container2 {
675+
template<typename U>
676+
Container2(const Container1<U>& C2);
677+
};
678+
658679
std::optional<std::string_view> test3(int i) {
659680
std::string s;
660681
std::string_view sv;
661682
if (i)
662683
return s; // expected-warning {{address of stack memory associated}}
663684
return sv; // fine
685+
Container2<std::string_view> c1 = Container1<Foo>(); // no diagnostic as Foo is not an Owner.
686+
Container2<std::string_view> c2 = Container1<FooOwner>(); // expected-warning {{object backing the pointer will be destroyed}}
687+
return GetFoo(); // fine, we don't know Foo is owner or not, be conservative.
688+
return GetFooOwner(); // expected-warning {{returning address of local temporary object}}
664689
}
665690

666691
std::optional<int*> test4(int a) {

0 commit comments

Comments
 (0)