Skip to content

Conversation

@usx95
Copy link
Contributor

@usx95 usx95 commented Nov 29, 2025

Prevent false positives in lifetime safety analysis when variables are moved using std::move.

When a value is moved using std::move, ownership is transferred from the original variable to another. The lifetime safety analysis was previously generating false positives by warning about use-after-lifetime when the original variable was destroyed after being moved. This change prevents those false positives by tracking moved declarations and exempting them from loan expiration checks.

  • Added tracking for declarations that have been moved via std::move in the FactsGenerator class
  • Added a MovedDecls set to track moved declarations in a flow-insensitive manner
  • Implemented detection of std::move calls in VisitCallExpr
  • Modified handleLifetimeEnds to skip loans for declarations that have been moved
  • Added a test case demonstrating that warnings are suppressed for moved variables
void silenced() {
  MyObj b;
  View v;
  {
    MyObj a;
    v = a;
    b = std::move(a); // No warning for 'a' being moved.
  }
  (void)v;
}

Fixes #152520

Copy link
Contributor Author

usx95 commented Nov 29, 2025

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link

github-actions bot commented Nov 29, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@github-actions
Copy link

github-actions bot commented Nov 29, 2025

🐧 Linux x64 Test Results

  • 111427 tests passed
  • 4450 tests skipped

@usx95 usx95 force-pushed the users/usx95/11-29-std_move_false_positive branch from a1b4509 to 00be6fa Compare December 2, 2025 17:38
@usx95 usx95 force-pushed the users/usx95/11-29-dereference_operator branch from 349c748 to a3842ac Compare December 2, 2025 17:38
@usx95 usx95 force-pushed the users/usx95/11-29-std_move_false_positive branch from 00be6fa to c21c11b Compare December 3, 2025 10:23
@usx95 usx95 force-pushed the users/usx95/11-29-dereference_operator branch 2 times, most recently from 3ead2ab to f53324e Compare December 3, 2025 10:30
@usx95 usx95 force-pushed the users/usx95/11-29-std_move_false_positive branch 2 times, most recently from 354ff2e to 8d7fc4d Compare December 3, 2025 10:33
@usx95 usx95 force-pushed the users/usx95/11-29-dereference_operator branch from f53324e to 1c7deeb Compare December 3, 2025 10:33
@usx95 usx95 force-pushed the users/usx95/11-29-std_move_false_positive branch from 8d7fc4d to 68895d3 Compare December 3, 2025 10:36
@usx95 usx95 force-pushed the users/usx95/11-29-dereference_operator branch from 1c7deeb to 1ad3ce7 Compare December 3, 2025 10:36
@usx95 usx95 force-pushed the users/usx95/11-29-std_move_false_positive branch from 68895d3 to 84eda37 Compare December 3, 2025 10:42
@usx95 usx95 force-pushed the users/usx95/11-29-dereference_operator branch 2 times, most recently from 4ce85f5 to ceda23e Compare December 4, 2025 07:18
@usx95 usx95 force-pushed the users/usx95/11-29-std_move_false_positive branch from 84eda37 to 13948c7 Compare December 4, 2025 07:18
@usx95 usx95 force-pushed the users/usx95/11-29-std_move_false_positive branch from 13948c7 to 9a4cf36 Compare December 4, 2025 07:42
@usx95 usx95 changed the title std_move false positive [LifetimeSafety] Track moved declarations to prevent false positives Dec 4, 2025
@usx95 usx95 marked this pull request as ready for review December 4, 2025 07:45
@usx95 usx95 requested review from Xazax-hun and ymand December 4, 2025 07:45
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:analysis clang:temporal-safety Issue/FR relating to the lifetime analysis in Clang (-Wdangling, -Wreturn-local-addr) labels Dec 4, 2025
@llvmbot
Copy link
Member

llvmbot commented Dec 4, 2025

@llvm/pr-subscribers-clang
@llvm/pr-subscribers-clang-analysis

@llvm/pr-subscribers-clang-temporal-safety

Author: Utkarsh Saxena (usx95)

Changes

Prevent false positives in lifetime safety analysis when variables are moved using std::move.

When a value is moved using std::move, ownership is transferred from the original variable to another. The lifetime safety analysis was previously generating false positives by warning about use-after-lifetime when the original variable was destroyed after being moved. This change prevents those false positives by tracking moved declarations and exempting them from loan expiration checks.

  • Added tracking for declarations that have been moved via std::move in the FactsGenerator class
  • Added a MovedDecls set to track moved declarations in a flow-insensitive manner
  • Implemented detection of std::move calls in VisitCallExpr
  • Modified handleLifetimeEnds to skip loans for declarations that have been moved
  • Added a test case demonstrating that warnings are suppressed for moved variables
void silenced() {
  MyObj b;
  View v;
  {
    MyObj a;
    v = a;
    b = std::move(a); // No warning for 'a' being moved.
  }
  (void)v;
}

Fixes #152520


Full diff: https://github.com/llvm/llvm-project/pull/170007.diff

3 Files Affected:

  • (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h (+5)
  • (modified) clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp (+23)
  • (modified) clang/test/Sema/warn-lifetime-safety.cpp (+18)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index 5b5626020e772..0c0581239ce34 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -101,6 +101,11 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
   // corresponding to the left-hand side is updated to be a "write", thereby
   // exempting it from the check.
   llvm::DenseMap<const DeclRefExpr *, UseFact *> UseFacts;
+
+  // Tracks declarations that have been moved via std::move. This is used to
+  // prevent false positives when the original owner is destroyed after the
+  // value has been moved. This tracking is flow-insensitive.
+  llvm::DenseSet<const ValueDecl *> MovedDecls;
 };
 
 } // namespace clang::lifetimes::internal
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index b27dcb6163449..ba88af2418056 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -163,9 +163,27 @@ void FactsGenerator::VisitCXXMemberCallExpr(const CXXMemberCallExpr *MCE) {
   }
 }
 
+static bool isStdMove(const FunctionDecl *FD) {
+  return FD && FD->isInStdNamespace() && FD->getIdentifier() &&
+         FD->getName() == "move";
+}
+
 void FactsGenerator::VisitCallExpr(const CallExpr *CE) {
   handleFunctionCall(CE, CE->getDirectCallee(),
                      {CE->getArgs(), CE->getNumArgs()});
+  // Track declarations that are moved via std::move.
+  // This is a flow-insensitive approximation: once a declaration is moved
+  // anywhere in the function, it's treated as moved everywhere. This can lead
+  // to false negatives on control flow paths where the value is not actually
+  // moved, but these are considered lower priority than the false positives
+  // this tracking prevents.
+  // TODO: The ideal solution would be flow-sensitive ownership tracking that
+  // records where values are moved from and to, but this is more complex.
+  if (isStdMove(CE->getDirectCallee()))
+    if (CE->getNumArgs() == 1)
+      if (auto *DRE =
+              dyn_cast<DeclRefExpr>(CE->getArg(0)->IgnoreParenImpCasts()))
+        MovedDecls.insert(DRE->getDecl());
 }
 
 void FactsGenerator::VisitCXXNullPtrLiteralExpr(
@@ -341,6 +359,11 @@ void FactsGenerator::handleLifetimeEnds(const CFGLifetimeEnds &LifetimeEnds) {
   // Iterate through all loans to see if any expire.
   for (const auto &Loan : FactMgr.getLoanMgr().getLoans()) {
     const AccessPath &LoanPath = Loan.Path;
+    // Skip loans for declarations that have been moved. When a value is moved,
+    // the original owner no longer has ownership and its destruction should not
+    // cause the loan to expire, preventing false positives.
+    if (MovedDecls.contains(LoanPath.D))
+      continue;
     // Check if the loan is for a stack variable and if that variable
     // is the one being destructed.
     if (LoanPath.D == LifetimeEndsVD)
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index f22c73cfeb784..97a79cc4ce102 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -1,9 +1,14 @@
 // RUN: %clang_cc1 -fsyntax-only -fexperimental-lifetime-safety -Wexperimental-lifetime-safety -Wno-dangling -verify %s
 
+#include "Inputs/lifetime-analysis.h"
+
 struct View;
 
 struct [[gsl::Owner]] MyObj {
   int id;
+  MyObj();
+  MyObj(int);
+  MyObj(const MyObj&);
   ~MyObj() {}  // Non-trivial destructor
   MyObj operator+(MyObj);
   
@@ -1297,3 +1302,16 @@ void add(int c, MyObj* node) {
   arr[4] = node;
 }
 } // namespace CppCoverage
+
+namespace do_not_warn_on_std_move {
+void silenced() {
+  MyObj b;
+  View v;
+  {
+    MyObj a;
+    v = a;
+    b = std::move(a); // No warning for 'a' being moved.
+  }
+  (void)v;
+}
+} // namespace do_not_warn_on_std_move

@hokein
Copy link
Collaborator

hokein commented Dec 4, 2025

The lifetime safety analysis was previously generating false positives by warning about use-after-lifetime when the original variable was destroyed after being moved. This change prevents those false positives by tracking moved declarations and exempting them from loan expiration checks.

Just a note.

While this fixes false positives, it introduces false negatives. The pointer is not always valid after the owner object is moved, an use-after-free example (when the string uses short string optimization) https://godbolt.org/z/eP7PbaMEn

int main() {
    std::string_view s;
    std::string b;
    {
      std::string a = "12345";  // small literal, stored in the string object, rather than the heap.
      s = a;
      b = std::move(a);
    }
    std::cout << s << "\n"; // oops, s refers to a dangling object.

}

Copy link
Collaborator

@Xazax-hun Xazax-hun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a step in the right direction but we should refine this a bit further.

}
}

static bool isStdMove(const FunctionDecl *FD) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure how I feel about this heuristic. People can also do "moves" by not using std::move but casting to an rvalue reference manually, or introducing their own wrapper (that might do some additional checks in some cases).

I think a better way to check if a value was moved whether it was passed to a move constructor. Unfortunately, sometimes we do not see the move ctor call. But we can assume move when the value is passed to a function as an rvalue reference.

So I think I'd rather check if something is passed as an rvalue ref (to move ctor or another function) rather than checking for std::move.

// Skip loans for declarations that have been moved. When a value is moved,
// the original owner no longer has ownership and its destruction should not
// cause the loan to expire, preventing false positives.
if (MovedDecls.contains(LoanPath.D))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to make this less intrusive? It would be nice if we could disable these checks only for the basic blocks that are reachable from the basic blocks where the move actually happened. So a move at the end of the function should not really prevent us finding bugs at the beginning of the function.

@Xazax-hun
Copy link
Collaborator

While this fixes false positives, it introduces false negatives.

I wonder if we should still warn about select classes like std::string in strict mode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:analysis clang:temporal-safety Issue/FR relating to the lifetime analysis in Clang (-Wdangling, -Wreturn-local-addr) clang Clang issues not falling into any other category

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants