Skip to content

Commit fe06a6d

Browse files
authored
Reland: [clang] Diagnose dangling issues for the "Container<GSLPointer>" case. #107213 (#108344)
This relands #107213, with with fixes to address false positives (`make_optional(nullptr)`).
1 parent 1c984b8 commit fe06a6d

File tree

4 files changed

+351
-6
lines changed

4 files changed

+351
-6
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ Improvements to Clang's diagnostics
336336
local variables passed to function calls using the ``[[clang::musttail]]``
337337
attribute.
338338

339+
- Clang now diagnoses cases where a dangling ``GSLOwner<GSLPointer>`` object is constructed, e.g. ``std::vector<string_view> v = {std::string()};`` (#GH100526).
340+
339341
Improvements to Clang's time-trace
340342
----------------------------------
341343

clang/include/clang/Basic/AttrDocs.td

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6696,6 +6696,20 @@ When the Owner's lifetime ends, it will consider the Pointer to be dangling.
66966696
P.getInt(); // P is dangling
66976697
}
66986698

6699+
If a template class is annotated with ``[[gsl::Owner]]``, and the first
6700+
instantiated template argument is a pointer type (raw pointer, or ``[[gsl::Pointer]]``),
6701+
the analysis will consider the instantiated class as a container of the pointer.
6702+
When constructing such an object from a GSL owner object, the analysis will
6703+
assume that the container holds a pointer to the owner object. Consequently,
6704+
when the owner object is destroyed, the pointer will be considered dangling.
6705+
6706+
.. code-block:: c++
6707+
6708+
int f() {
6709+
std::vector<std::string_view> v = {std::string()}; // v holds a dangling pointer.
6710+
std::optional<std::string_view> o = std::string(); // o holds a dangling pointer.
6711+
}
6712+
66996713
}];
67006714
}
67016715

clang/lib/Sema/CheckExprLifetime.cpp

Lines changed: 145 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,49 @@ static bool isInStlNamespace(const Decl *D) {
271271
return DC->isStdNamespace();
272272
}
273273

274+
static bool isPointerLikeType(QualType Type) {
275+
return isRecordWithAttr<PointerAttr>(Type) || Type->isPointerType() ||
276+
Type->isNullPtrType();
277+
}
278+
279+
// Returns true if the given Record decl is a form of `GSLOwner<Pointer>`
280+
// type, e.g. std::vector<string_view>, std::optional<string_view>.
281+
static bool isContainerOfPointer(const RecordDecl *Container) {
282+
if (const auto *CTSD =
283+
dyn_cast_if_present<ClassTemplateSpecializationDecl>(Container)) {
284+
if (!CTSD->hasAttr<OwnerAttr>()) // Container must be a GSL owner type.
285+
return false;
286+
const auto &TAs = CTSD->getTemplateArgs();
287+
return TAs.size() > 0 && TAs[0].getKind() == TemplateArgument::Type &&
288+
isPointerLikeType(TAs[0].getAsType());
289+
}
290+
return false;
291+
}
292+
static bool isContainerOfOwner(const RecordDecl *Container) {
293+
const auto *CTSD =
294+
dyn_cast_if_present<ClassTemplateSpecializationDecl>(Container);
295+
if (!CTSD)
296+
return false;
297+
if (!CTSD->hasAttr<OwnerAttr>()) // Container must be a GSL owner type.
298+
return false;
299+
const auto &TAs = CTSD->getTemplateArgs();
300+
return TAs.size() > 0 && TAs[0].getKind() == TemplateArgument::Type &&
301+
isRecordWithAttr<OwnerAttr>(TAs[0].getAsType());
302+
}
303+
304+
// Returns true if the given Record is `std::initializer_list<pointer>`.
305+
static bool isStdInitializerListOfPointer(const RecordDecl *RD) {
306+
if (const auto *CTSD =
307+
dyn_cast_if_present<ClassTemplateSpecializationDecl>(RD)) {
308+
const auto &TAs = CTSD->getTemplateArgs();
309+
return isInStlNamespace(RD) && RD->getIdentifier() &&
310+
RD->getName() == "initializer_list" && TAs.size() > 0 &&
311+
TAs[0].getKind() == TemplateArgument::Type &&
312+
isPointerLikeType(TAs[0].getAsType());
313+
}
314+
return false;
315+
}
316+
274317
static bool shouldTrackImplicitObjectArg(const CXXMethodDecl *Callee) {
275318
if (auto *Conv = dyn_cast_or_null<CXXConversionDecl>(Callee))
276319
if (isRecordWithAttr<PointerAttr>(Conv->getConversionType()) &&
@@ -282,8 +325,7 @@ static bool shouldTrackImplicitObjectArg(const CXXMethodDecl *Callee) {
282325
Callee->getFunctionObjectParameterType()) &&
283326
!isRecordWithAttr<OwnerAttr>(Callee->getFunctionObjectParameterType()))
284327
return false;
285-
if (Callee->getReturnType()->isPointerType() ||
286-
isRecordWithAttr<PointerAttr>(Callee->getReturnType())) {
328+
if (isPointerLikeType(Callee->getReturnType())) {
287329
if (!Callee->getIdentifier())
288330
return false;
289331
return llvm::StringSwitch<bool>(Callee->getName())
@@ -331,6 +373,103 @@ static bool shouldTrackFirstArgument(const FunctionDecl *FD) {
331373
return false;
332374
}
333375

376+
// Returns true if the given constructor is a copy-like constructor, such as
377+
// `Ctor(Owner<U>&&)` or `Ctor(const Owner<U>&)`.
378+
static bool isCopyLikeConstructor(const CXXConstructorDecl *Ctor) {
379+
if (!Ctor || Ctor->param_size() != 1)
380+
return false;
381+
const auto *ParamRefType =
382+
Ctor->getParamDecl(0)->getType()->getAs<ReferenceType>();
383+
if (!ParamRefType)
384+
return false;
385+
386+
// Check if the first parameter type is "Owner<U>".
387+
if (const auto *TST =
388+
ParamRefType->getPointeeType()->getAs<TemplateSpecializationType>())
389+
return TST->getTemplateName()
390+
.getAsTemplateDecl()
391+
->getTemplatedDecl()
392+
->hasAttr<OwnerAttr>();
393+
return false;
394+
}
395+
396+
// Returns true if we should perform the GSL analysis on the first argument for
397+
// the given constructor.
398+
static bool
399+
shouldTrackFirstArgumentForConstructor(const CXXConstructExpr *Ctor) {
400+
const auto *LHSRecordDecl = Ctor->getConstructor()->getParent();
401+
402+
// Case 1, construct a GSL pointer, e.g. std::string_view
403+
// Always inspect when LHS is a pointer.
404+
if (LHSRecordDecl->hasAttr<PointerAttr>())
405+
return true;
406+
407+
if (Ctor->getConstructor()->getNumParams() != 1 ||
408+
!isContainerOfPointer(LHSRecordDecl))
409+
return false;
410+
411+
// Now, the LHS is an Owner<Pointer> type, e.g., std::vector<string_view>.
412+
//
413+
// At a high level, we cannot precisely determine what the nested pointer
414+
// owns. However, by analyzing the RHS owner type, we can use heuristics to
415+
// infer ownership information. These heuristics are designed to be
416+
// conservative, minimizing false positives while still providing meaningful
417+
// diagnostics.
418+
//
419+
// While this inference isn't perfect, it helps catch common use-after-free
420+
// patterns.
421+
auto RHSArgType = Ctor->getArg(0)->getType();
422+
const auto *RHSRD = RHSArgType->getAsRecordDecl();
423+
// LHS is constructed from an intializer_list.
424+
//
425+
// std::initializer_list is a proxy object that provides access to the backing
426+
// array. We perform analysis on it to determine if there are any dangling
427+
// temporaries in the backing array.
428+
// E.g. std::vector<string_view> abc = {string()};
429+
if (isStdInitializerListOfPointer(RHSRD))
430+
return true;
431+
432+
// RHS must be an owner.
433+
if (!isRecordWithAttr<OwnerAttr>(RHSArgType))
434+
return false;
435+
436+
// Bail out if the RHS is Owner<Pointer>.
437+
//
438+
// We cannot reliably determine what the LHS nested pointer owns -- it could
439+
// be the entire RHS or the nested pointer in RHS. To avoid false positives,
440+
// we skip this case, such as:
441+
// std::stack<std::string_view> s(std::deque<std::string_view>{});
442+
//
443+
// TODO: this also has a false negative, it doesn't catch the case like:
444+
// std::optional<span<int*>> os = std::vector<int*>{}
445+
if (isContainerOfPointer(RHSRD))
446+
return false;
447+
448+
// Assume that the nested Pointer is constructed from the nested Owner.
449+
// E.g. std::optional<string_view> sv = std::optional<string>(s);
450+
if (isContainerOfOwner(RHSRD))
451+
return true;
452+
453+
// Now, the LHS is an Owner<Pointer> and the RHS is an Owner<X>, where X is
454+
// neither an `Owner` nor a `Pointer`.
455+
//
456+
// Use the constructor's signature as a hint. If it is a copy-like constructor
457+
// `Owner1<Pointer>(Owner2<X>&&)`, we assume that the nested pointer is
458+
// constructed from X. In such cases, we do not diagnose, as `X` is not an
459+
// owner, e.g.
460+
// std::optional<string_view> sv = std::optional<Foo>();
461+
if (const auto *PrimaryCtorTemplate =
462+
Ctor->getConstructor()->getPrimaryTemplate();
463+
PrimaryCtorTemplate &&
464+
isCopyLikeConstructor(dyn_cast_if_present<CXXConstructorDecl>(
465+
PrimaryCtorTemplate->getTemplatedDecl()))) {
466+
return false;
467+
}
468+
// Assume that the nested pointer is constructed from the whole RHS.
469+
// E.g. optional<string_view> s = std::string();
470+
return true;
471+
}
472+
334473
// Return true if this is an "normal" assignment operator.
335474
// We assuments that a normal assingment operator always returns *this, that is,
336475
// an lvalue reference that is the same type as the implicit object parameter
@@ -473,12 +612,12 @@ static void visitFunctionCallArguments(IndirectLocalPath &Path, Expr *Call,
473612
if (CheckCoroCall || Callee->getParamDecl(I)->hasAttr<LifetimeBoundAttr>())
474613
VisitLifetimeBoundArg(Callee->getParamDecl(I), Args[I]);
475614
else if (EnableGSLAnalysis && I == 0) {
615+
// Perform GSL analysis for the first argument
476616
if (shouldTrackFirstArgument(Callee)) {
477617
VisitGSLPointerArg(Callee, Args[0]);
478-
} else if (auto *CCE = dyn_cast<CXXConstructExpr>(Call);
479-
CCE &&
480-
CCE->getConstructor()->getParent()->hasAttr<PointerAttr>()) {
481-
VisitGSLPointerArg(CCE->getConstructor(), Args[0]);
618+
} else if (auto *Ctor = dyn_cast<CXXConstructExpr>(Call);
619+
Ctor && shouldTrackFirstArgumentForConstructor(Ctor)) {
620+
VisitGSLPointerArg(Ctor->getConstructor(), Args[0]);
482621
}
483622
}
484623
}

0 commit comments

Comments
 (0)