[clang] use-after-scope through fields (PR #191865)

Utkarsh Saxena via cfe-commits cfe-commits at lists.llvm.org
Thu Apr 16 02:35:08 PDT 2026


https://github.com/usx95 updated https://github.com/llvm/llvm-project/pull/191865

>From 957197623e0d2fc62c06183ea090ebd1459d4d71 Mon Sep 17 00:00:00 2001
From: Utkarsh Saxena <usx at google.com>
Date: Mon, 13 Apr 2026 18:13:49 +0000
Subject: [PATCH 1/5] use-after-scope through fields

---
 .../LifetimeSafety/FactsGenerator.cpp         |  1 +
 clang/lib/Analysis/LifetimeSafety/Origins.cpp |  2 +-
 .../warn-lifetime-safety-dangling-field.cpp   | 24 +++++++++----------
 clang/test/Sema/warn-lifetime-safety.cpp      |  9 +++----
 4 files changed, 19 insertions(+), 17 deletions(-)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 5adf842e5f6d0..55d1961ff8e7f 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -259,6 +259,7 @@ void FactsGenerator::VisitMemberExpr(const MemberExpr *ME) {
     assert(Dst && "Field member should have an origin list as it is GL value");
     OriginList *Src = getOriginsList(*ME->getBase());
     assert(Src && "Base expression should be a pointer/reference type");
+    handleUse(ME);
     // The field's glvalue (outermost origin) holds the same loans as the base
     // expression.
     CurrentBlockFacts.push_back(FactMgr.createFact<OriginFlowFact>(
diff --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
index 033cbdd75352c..505ae5146d5ba 100644
--- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
@@ -240,7 +240,7 @@ OriginList *OriginManager::getOrCreateList(const Expr *E) {
     ReferencedDecl = DRE->getDecl();
   else if (auto *ME = dyn_cast<MemberExpr>(E))
     if (auto *Field = dyn_cast<FieldDecl>(ME->getMemberDecl());
-        Field && isa<CXXThisExpr>(ME->getBase()->IgnoreParenImpCasts()))
+        Field && isa<CXXThisExpr>(ME->getBase()->IgnoreImpCasts()))
       ReferencedDecl = Field;
   if (ReferencedDecl) {
     OriginList *Head = nullptr;
diff --git a/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp b/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
index 2afcd4a3dd97b..cecb0f795518f 100644
--- a/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
@@ -76,8 +76,8 @@ struct CtorPointerField {
 };
 
 struct MemberSetters {
-  std::string_view view;  // expected-note 6 {{this field dangles}}
-  const char* p;          // expected-note 6 {{this field dangles}}
+  std::string_view view;  // expected-note 5 {{this field dangles}}
+  const char* p;          // expected-note 5 {{this field dangles}}
 
   void setWithParam(std::string s) {
     view = s;     // expected-warning {{address of stack memory escapes to a field}}
@@ -140,21 +140,21 @@ struct MemberSetters {
   void use_after_scope() {
     {
       std::string local;
-      view = local;     // expected-warning {{address of stack memory escapes to a field}}
-      p = local.data(); // expected-warning {{address of stack memory escapes to a field}}
-    }
-    (void)view;
-    (void)p;
+      view = local;     // expected-warning {{object whose reference is captured does not live long enough}}
+      p = local.data(); // expected-warning {{object whose reference is captured does not live long enough}}
+    } // expected-note 2 {{destroyed here}}
+    (void)view; // expected-note {{later used here}}
+    (void)p;    // expected-note {{later used here}}
   }
 
   void use_after_scope_saved_after_reassignment() {
     {
       std::string local;
-      view = local;
-      p = local.data();
-    }
-    (void)view;
-    (void)p;
+      view = local;     // expected-warning {{object whose reference is captured does not live long enough}}
+      p = local.data(); // expected-warning {{object whose reference is captured does not live long enough}}
+    } // expected-note 2 {{destroyed here}}
+    (void)view; // expected-note {{later used here}}
+    (void)p;    // expected-note {{later used here}}
 
     view = kGlobal;
     p = kGlobal.data();
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index 082db5d5cbf15..68af1d4451f68 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -2634,15 +2634,16 @@ struct Holder {
 } // namespace CXXDefaultInitExprTests
 
 namespace base_class_fields {
-struct X { int* x; }; // expected-note {{this field dangles}}
+struct X { int* x; };
 struct Y : X {
   int* y;
   void bar() {
     {
       int a;
-      x = &a; // expected-warning {{address of stack memory escapes to a field}}
-    }
-    (void)x;
+      x = &a; // expected-warning {{object whose reference is captured does not live long enough}}
+    } // expected-note {{destroyed here}}
+    (void)x; // expected-note {{later used here}}
+    x = nullptr;
   }
 };
 } // namespace base_class_fields

>From 7bb42d83882f445cf9378b37d8f33f20501f0563 Mon Sep 17 00:00:00 2001
From: Utkarsh Saxena <usx at google.com>
Date: Thu, 16 Apr 2026 08:04:42 +0000
Subject: [PATCH 2/5] fix format

---
 .../Sema/warn-lifetime-safety-dangling-field.cpp     | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp b/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
index cecb0f795518f..600ff5ea7784a 100644
--- a/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
@@ -142,9 +142,9 @@ struct MemberSetters {
       std::string local;
       view = local;     // expected-warning {{object whose reference is captured does not live long enough}}
       p = local.data(); // expected-warning {{object whose reference is captured does not live long enough}}
-    } // expected-note 2 {{destroyed here}}
-    (void)view; // expected-note {{later used here}}
-    (void)p;    // expected-note {{later used here}}
+    }                   // expected-note 2 {{destroyed here}}
+    (void)view;         // expected-note {{later used here}}
+    (void)p;            // expected-note {{later used here}}
   }
 
   void use_after_scope_saved_after_reassignment() {
@@ -152,9 +152,9 @@ struct MemberSetters {
       std::string local;
       view = local;     // expected-warning {{object whose reference is captured does not live long enough}}
       p = local.data(); // expected-warning {{object whose reference is captured does not live long enough}}
-    } // expected-note 2 {{destroyed here}}
-    (void)view; // expected-note {{later used here}}
-    (void)p;    // expected-note {{later used here}}
+    }                   // expected-note 2 {{destroyed here}}
+    (void)view;         // expected-note {{later used here}}
+    (void)p;            // expected-note {{later used here}}
 
     view = kGlobal;
     p = kGlobal.data();

>From f4ad4ac1e9487efd817a8402ec0d4c360dbdda80 Mon Sep 17 00:00:00 2001
From: Utkarsh Saxena <usx at google.com>
Date: Thu, 16 Apr 2026 08:51:04 +0000
Subject: [PATCH 3/5] special case MemberExpr writes

---
 .../Analysis/Analyses/LifetimeSafety/FactsGenerator.h     | 2 +-
 clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp      | 8 +++++---
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index a86f22a181a70..1edbd8311ee58 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -121,7 +121,7 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
   // (e.g. on the left-hand side of an assignment in the case of a DeclRefExpr).
   void handleUse(const Expr *E);
 
-  void markUseAsWrite(const DeclRefExpr *DRE);
+  void markUseAsWrite(const Expr *E);
 
   bool escapesViaReturn(OriginID OID) const;
 
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 55d1961ff8e7f..44cd5f82b862e 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -410,6 +410,8 @@ void FactsGenerator::handleAssignment(const Expr *LHSExpr,
     } else
       markUseAsWrite(DRE_LHS);
   }
+  if (isa<MemberExpr>(LHSExpr))
+    markUseAsWrite(LHSExpr);
   if (!RHSList) {
     // RHS has no tracked origins (e.g., assigning a callable without origins
     // to std::function). Clear loans of the destination.
@@ -926,9 +928,9 @@ void FactsGenerator::handleUse(const Expr *E) {
   }
 }
 
-void FactsGenerator::markUseAsWrite(const DeclRefExpr *DRE) {
-  if (UseFacts.contains(DRE))
-    UseFacts[DRE]->markAsWritten();
+void FactsGenerator::markUseAsWrite(const Expr *E) {
+  if (UseFacts.contains(E))
+    UseFacts[E]->markAsWritten();
 }
 
 // Creates an IssueFact for a new placeholder loan for each pointer or reference

>From 1b339fd4c9923b7705f14963bbae030173a752b8 Mon Sep 17 00:00:00 2001
From: Utkarsh Saxena <usx at google.com>
Date: Thu, 16 Apr 2026 09:02:14 +0000
Subject: [PATCH 4/5] merge DRE and ME

---
 .../LifetimeSafety/FactsGenerator.cpp         | 21 ++++++++++++-------
 1 file changed, 13 insertions(+), 8 deletions(-)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 44cd5f82b862e..476701ec6dffc 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -387,31 +387,36 @@ void FactsGenerator::handleAssignment(const Expr *LHSExpr,
   // assigned.
   RHSList = getRValueOrigins(RHSExpr, RHSList);
 
-  if (const auto *DRE_LHS = dyn_cast<DeclRefExpr>(LHSExpr)) {
-    QualType QT = DRE_LHS->getDecl()->getType();
+  const ValueDecl *VD = nullptr;
+  if (const auto *DRE = dyn_cast<DeclRefExpr>(LHSExpr))
+    VD = DRE->getDecl();
+  else if (const auto *ME = dyn_cast<MemberExpr>(LHSExpr))
+    VD = ME->getMemberDecl();
+
+  if (VD) {
+    QualType QT = VD->getType();
     if (QT->isReferenceType()) {
       if (hasOrigins(QT->getPointeeType())) {
         // Writing through a reference uses the binding but overwrites the
         // pointee. Model this as a Read of the outer origin (keeping the
         // binding live) and a Write of the inner origins (killing the pointee's
         // liveness).
-        if (UseFact *UF = UseFacts.lookup(DRE_LHS)) {
+        if (UseFact *UF = UseFacts.lookup(LHSExpr)) {
           const OriginList *FullList = UF->getUsedOrigins();
           assert(FullList);
           UF->setUsedOrigins(FactMgr.getOriginMgr().createSingleOriginList(
               FullList->getOuterOriginID()));
           if (const OriginList *InnerList = FullList->peelOuterOrigin()) {
-            UseFact *WriteUF = FactMgr.createFact<UseFact>(DRE_LHS, InnerList);
+            UseFact *WriteUF = FactMgr.createFact<UseFact>(LHSExpr, InnerList);
             WriteUF->markAsWritten();
             CurrentBlockFacts.push_back(WriteUF);
           }
         }
       }
-    } else
-      markUseAsWrite(DRE_LHS);
+    } else {
+      markUseAsWrite(LHSExpr);
+    }
   }
-  if (isa<MemberExpr>(LHSExpr))
-    markUseAsWrite(LHSExpr);
   if (!RHSList) {
     // RHS has no tracked origins (e.g., assigning a callable without origins
     // to std::function). Clear loans of the destination.

>From 4f81f1010a5f641432e156e6f4a2e408fe217656 Mon Sep 17 00:00:00 2001
From: Utkarsh Saxena <usx at google.com>
Date: Thu, 16 Apr 2026 09:34:36 +0000
Subject: [PATCH 5/5] another danglng-field test case

---
 .../warn-lifetime-safety-dangling-field.cpp   |  5 +++++
 .../warn-lifetime-safety-invalidations.cpp    | 19 +++++++++++++++++++
 2 files changed, 24 insertions(+)

diff --git a/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp b/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
index 600ff5ea7784a..51159f561d5a7 100644
--- a/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-dangling-field.cpp
@@ -68,6 +68,11 @@ struct CtorRefField {
   CtorRefField(Dummy<1> ok, const std::string& s, const std::string_view& v): str(s), view(v) {}
 };
 
+struct CtorRefFieldTemporary {
+  const std::string& str; // expected-note {{this field dangles}}
+  CtorRefFieldTemporary(): str(std::vector<std::string>{"abcd"}[0]) {} // expected-warning {{address of stack memory escapes to a field}}
+};
+
 struct CtorPointerField {
   const char* ptr; // expected-note {{this field dangles}}
   CtorPointerField(std::string s) : ptr(s.data()) {}  // expected-warning {{address of stack memory escapes to a field}}
diff --git a/clang/test/Sema/warn-lifetime-safety-invalidations.cpp b/clang/test/Sema/warn-lifetime-safety-invalidations.cpp
index 973e095fb68b4..4829c080b1230 100644
--- a/clang/test/Sema/warn-lifetime-safety-invalidations.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-invalidations.cpp
@@ -539,3 +539,22 @@ void function_captured_ref_invalidated() {
 }
 
 } // namespace callable_wrappers
+
+namespace ReferenceFieldToVectorElement {
+struct S {
+  std::string& in;
+
+  S(std::vector<std::string> strings)
+    : in(strings[0]) {        // expected-warning {{object whose reference is captured is later invalidated}}
+    strings.push_back("42");  // expected-note {{invalidated here}}
+    in = "use-after-invalidation"; // expected-note {{later used here}}
+  }
+
+  // FIXME: Detect invalidation of reference parameter.
+  S(std::vector<std::string>& strings, int test2)
+    : in(strings[0]) {
+    strings.push_back("42");
+    in = "use-after-invalidation";
+  }
+};
+} // namespace ReferenceFieldToVectorElement



More information about the cfe-commits mailing list