[clang] [LifetimeSafety] Fix crash on explicit object member functions (PR #212154)

via cfe-commits cfe-commits at lists.llvm.org
Mon Jul 27 02:59:11 PDT 2026


https://github.com/arhwx updated https://github.com/llvm/llvm-project/pull/212154

>From ef4a173b0d6a9c13a7fe99a490e34b09e4e2aa83 Mon Sep 17 00:00:00 2001
From: arhwx <arahwrm at gmail.com>
Date: Sun, 26 Jul 2026 18:08:56 -0400
Subject: [PATCH 1/4] [LifetimeSafety] Fix crash on explicit object member
 functions

handleMovedArgsInCall assumed the object argument is absent
from FunctionDecl::parameters() and applied a one parameter
offset. An explicit object parameter is a regular
ParmVarDecl, so the offset misaligned arguments with
parameters and the assert dereferenced a null OriginList.

Fixes #204210
---
 .../Analysis/LifetimeSafety/FactsGenerator.cpp |  3 ++-
 .../explicit-object-param-no-crash.cpp         | 18 ++++++++++++++++++
 2 files changed, 20 insertions(+), 1 deletion(-)
 create mode 100644 clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 466bb2185c6d8..e858e0b54688b 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -911,7 +911,8 @@ void FactsGenerator::handleMovedArgsInCall(const FunctionDecl *FD,
                                            ArrayRef<const Expr *> Args) {
   unsigned IsInstance = 0;
   if (const auto *MD = dyn_cast<CXXMethodDecl>(FD);
-      MD && MD->isInstance() && !isa<CXXConstructorDecl>(FD)) {
+      MD && !isa<CXXConstructorDecl>(FD) &&
+      MD->isImplicitObjectMemberFunction()) {
     IsInstance = 1;
     // std::unique_ptr::release() transfers ownership.
     // Treat it as a move to prevent false-positive warnings when the unique_ptr
diff --git a/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp b/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp
new file mode 100644
index 0000000000000..56615437ab1ae
--- /dev/null
+++ b/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp
@@ -0,0 +1,18 @@
+// RUN: %clang_cc1 %s -std=c++23 -verify -fsyntax-only -Wlifetime-safety
+
+// expected-no-diagnostics
+
+// Explicit object member functions must not be treated as having an implicit
+// object argument.
+struct Foo {
+  template <typename T>
+  int get(this Foo &&self, T) {
+    return self.field;
+  }
+
+  int field;
+};
+
+void call() {
+  Foo().get(0);
+}

>From 549b8ce565201f3839f43de668de0a3982251c87 Mon Sep 17 00:00:00 2001
From: arhwx <arahwrm at gmail.com>
Date: Mon, 27 Jul 2026 01:15:03 -0400
Subject: [PATCH 2/4] [LifetimeSafety] Add explicit object parameter test
 coverage

---
 .../explicit-object-param-no-crash.cpp        | 32 +++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp b/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp
index 56615437ab1ae..159c342f15527 100644
--- a/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp
+++ b/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp
@@ -13,6 +13,38 @@ struct Foo {
   int field;
 };
 
+struct Base {};
+struct Derived : Base {
+  int base(this Base &&, int);
+};
+
+template <typename T>
+struct Crtp {
+  template <typename Self>
+  int deduced(this Self &&self, T) {
+    return self.field;
+  }
+};
+
+struct Widget : Crtp<int> {
+  int field;
+};
+
+struct NoMove {
+  NoMove() = default;
+  NoMove(NoMove &&) = delete;
+  NoMove(const NoMove &) = delete;
+
+  int get(this NoMove &&self, int) {
+    return self.field;
+  }
+
+  int field;
+};
+
 void call() {
   Foo().get(0);
+  Derived().base(0);
+  Widget().deduced(0);
+  NoMove().get(0);
 }

>From 3cb27ed309f13003df61507d29d8095bb1f2012c Mon Sep 17 00:00:00 2001
From: arhwx <arahwrm at gmail.com>
Date: Mon, 27 Jul 2026 03:50:43 -0400
Subject: [PATCH 3/4] [LifetimeSafety] Avoid treating explicit object parameter
 as moved

---
 .../LifetimeSafety/FactsGenerator.cpp         |  2 ++
 .../explicit-object-param-no-crash.cpp        | 27 ++++++++++++++++++-
 2 files changed, 28 insertions(+), 1 deletion(-)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index e858e0b54688b..4b045731e92a8 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -930,6 +930,8 @@ void FactsGenerator::handleMovedArgsInCall(const FunctionDecl *FD,
   for (unsigned I = IsInstance;
        I < Args.size() && I < FD->getNumParams() + IsInstance; ++I) {
     const ParmVarDecl *PVD = FD->getParamDecl(I - IsInstance);
+    if (PVD->isExplicitObjectParameter())
+      continue;
     if (!PVD->getType()->isRValueReferenceType())
       continue;
     // Skip lifetime annotated r-value reference parameters. Lifetime annotation
diff --git a/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp b/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp
index 159c342f15527..56c170e188af7 100644
--- a/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp
+++ b/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 %s -std=c++23 -verify -fsyntax-only -Wlifetime-safety
+// RUN: %clang_cc1 %s -std=c++23 -verify -fsyntax-only -Wlifetime-safety -Wlifetime-safety-lifetimebound-violation
 
 // expected-no-diagnostics
 
@@ -42,9 +42,34 @@ struct NoMove {
   int field;
 };
 
+template <typename T>
+struct SharedPtr {
+  SharedPtr() = default;
+  SharedPtr(SharedPtr &&o) : p(o.p) { o.p = nullptr; }
+  SharedPtr(const SharedPtr &) = default;
+
+  int get(this SharedPtr &&self, int) {
+    return self.p->field;
+  }
+
+  T *p = nullptr;
+};
+
 void call() {
   Foo().get(0);
   Derived().base(0);
   Widget().deduced(0);
   NoMove().get(0);
+  SharedPtr<NoMove>().get(0);
+}
+
+struct Holder {
+  int field;
+  const int *borrow() [[clang::lifetimebound]] { return &field; }
+  void consume(this Holder &&self) {}
+};
+
+const int *object_arg_is_not_moved(Holder &&h [[clang::lifetimebound]]) {
+  static_cast<Holder &&>(h).consume();
+  return h.borrow();
 }

>From 600ea9d7d6136533258201b952a1cd32b456eda7 Mon Sep 17 00:00:00 2001
From: arhwx <arahwrm at gmail.com>
Date: Mon, 27 Jul 2026 05:58:56 -0400
Subject: [PATCH 4/4] [LifetimeSafety] Address review comments

---
 .../Analysis/LifetimeSafety/FactsGenerator.cpp    | 15 +++++++++------
 .../explicit-object-param-no-crash.cpp            | 14 +++++++++++++-
 2 files changed, 22 insertions(+), 7 deletions(-)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 4b045731e92a8..d32596388553e 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -909,11 +909,11 @@ void FactsGenerator::handleGSLPointerConstruction(const CXXConstructExpr *CCE) {
 
 void FactsGenerator::handleMovedArgsInCall(const FunctionDecl *FD,
                                            ArrayRef<const Expr *> Args) {
-  unsigned IsInstance = 0;
+  unsigned ImplicitObjectArgOffset = 0;
   if (const auto *MD = dyn_cast<CXXMethodDecl>(FD);
       MD && !isa<CXXConstructorDecl>(FD) &&
       MD->isImplicitObjectMemberFunction()) {
-    IsInstance = 1;
+    ImplicitObjectArgOffset = 1;
     // std::unique_ptr::release() transfers ownership.
     // Treat it as a move to prevent false-positive warnings when the unique_ptr
     // destructor runs after ownership has been transferred.
@@ -926,10 +926,13 @@ void FactsGenerator::handleMovedArgsInCall(const FunctionDecl *FD,
     }
   }
 
-  // Skip 'this' arg as it cannot be moved.
-  for (unsigned I = IsInstance;
-       I < Args.size() && I < FD->getNumParams() + IsInstance; ++I) {
-    const ParmVarDecl *PVD = FD->getParamDecl(I - IsInstance);
+  // Skip implicit 'this' arg as it cannot be moved.
+  for (unsigned I = ImplicitObjectArgOffset;
+       I < Args.size() && I < FD->getNumParams() + ImplicitObjectArgOffset;
+       ++I) {
+    const ParmVarDecl *PVD = FD->getParamDecl(I - ImplicitObjectArgOffset);
+    // In principle, explicit object parameters can be moved, but skip marking
+    // them as moved for consistency with implicit 'this'.
     if (PVD->isExplicitObjectParameter())
       continue;
     if (!PVD->getType()->isRValueReferenceType())
diff --git a/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp b/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp
index 56c170e188af7..71c86b9f793fb 100644
--- a/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp
+++ b/clang/test/Sema/LifetimeSafety/explicit-object-param-no-crash.cpp
@@ -1,6 +1,6 @@
 // RUN: %clang_cc1 %s -std=c++23 -verify -fsyntax-only -Wlifetime-safety -Wlifetime-safety-lifetimebound-violation
 
-// expected-no-diagnostics
+#include "Inputs/lifetime-analysis.h"
 
 // Explicit object member functions must not be treated as having an implicit
 // object argument.
@@ -73,3 +73,15 @@ const int *object_arg_is_not_moved(Holder &&h [[clang::lifetimebound]]) {
   static_cast<Holder &&>(h).consume();
   return h.borrow();
 }
+
+const int *t1(Holder h) {
+  const int *ptr = h.borrow(); // expected-warning {{stack memory associated with parameter 'h' is returned}}
+  static_cast<Holder &&>(h).consume();
+  return ptr; // expected-note {{returned here}}
+}
+
+const int *t2(Holder h) {
+  const int *ptr = h.borrow(); // expected-warning {{stack memory associated with parameter 'h' is returned}}
+  std::move(h).consume();
+  return ptr; // expected-note {{returned here}}
+}



More information about the cfe-commits mailing list