llvmbot wrote:

<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang-static-analyzer-1

Author: Balazs Benics (steakhal)

<details>
<summary>Changes</summary>

Tldr;
We can't unconditionally trivially copy empty classes because that would 
clobber the stored entries in the object that the optimized empty class 
overlaps with.

This regression was introduced by #<!-- -->115918, which introduced other 
clobbering issues, like the handling of `[[no_unique_address]]` fields in #<!-- 
-->137252.

Read issue #<!-- -->157467 for the detailed explanation, but in short, I'd 
propose reverting the original patch because these was a lot of problems with 
it for arguably not much gain.
In particular, that patch was motivated by unifying the handling of classes so 
that copy events would be triggered for a class no matter if it had data 
members or not.
So in hindsight, it was not worth it.

I plan to backport this to clang-21 as well, and mention in the release notes 
that this should fix the regression from clang-20.

Fixes #<!-- -->157467

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


4 Files Affected:

- (modified) clang/lib/StaticAnalyzer/Core/ExprEngineCXX.cpp (+22-13) 
- (modified) clang/test/Analysis/ctor-trivial-copy.cpp (+3-14) 
- (added) clang/test/Analysis/issue-157467.cpp (+39) 
- (modified) clang/test/Analysis/taint-generic.cpp (+2-1) 


``````````diff
diff --git a/clang/lib/StaticAnalyzer/Core/ExprEngineCXX.cpp 
b/clang/lib/StaticAnalyzer/Core/ExprEngineCXX.cpp
index c0b28d2ebb212..dee34e3e9d6a5 100644
--- a/clang/lib/StaticAnalyzer/Core/ExprEngineCXX.cpp
+++ b/clang/lib/StaticAnalyzer/Core/ExprEngineCXX.cpp
@@ -71,21 +71,30 @@ void ExprEngine::performTrivialCopy(NodeBuilder &Bldr, 
ExplodedNode *Pred,
   Bldr.takeNodes(Pred);
 
   assert(ThisRD);
-  SVal V = Call.getArgSVal(0);
-  const Expr *VExpr = Call.getArgExpr(0);
 
-  // If the value being copied is not unknown, load from its location to get
-  // an aggregate rvalue.
-  if (std::optional<Loc> L = V.getAs<Loc>())
-    V = Pred->getState()->getSVal(*L);
-  else
-    assert(V.isUnknownOrUndef());
+  if (!ThisRD->isEmpty()) {
+    SVal V = Call.getArgSVal(0);
+    const Expr *VExpr = Call.getArgExpr(0);
 
-  ExplodedNodeSet Tmp;
-  evalLocation(Tmp, CallExpr, VExpr, Pred, Pred->getState(), V,
-               /*isLoad=*/true);
-  for (ExplodedNode *N : Tmp)
-    evalBind(Dst, CallExpr, N, ThisVal, V, !AlwaysReturnsLValue);
+    // If the value being copied is not unknown, load from its location to get
+    // an aggregate rvalue.
+    if (std::optional<Loc> L = V.getAs<Loc>())
+      V = Pred->getState()->getSVal(*L);
+    else
+      assert(V.isUnknownOrUndef());
+
+    ExplodedNodeSet Tmp;
+    evalLocation(Tmp, CallExpr, VExpr, Pred, Pred->getState(), V,
+                 /*isLoad=*/true);
+    for (ExplodedNode *N : Tmp)
+      evalBind(Dst, CallExpr, N, ThisVal, V, !AlwaysReturnsLValue);
+  } else {
+    // We can't copy empty classes because of empty base class optimization.
+    // In that case, copying the empty base class subobject would overwrite the
+    // object that it overlaps with - so let's not do that.
+    // See issue-157467.cpp for an example.
+    Dst.Add(Pred);
+  }
 
   PostStmt PS(CallExpr, LCtx);
   for (ExplodedNode *N : Dst) {
diff --git a/clang/test/Analysis/ctor-trivial-copy.cpp 
b/clang/test/Analysis/ctor-trivial-copy.cpp
index 45c8ca4c51776..940ff9ba3ed9c 100644
--- a/clang/test/Analysis/ctor-trivial-copy.cpp
+++ b/clang/test/Analysis/ctor-trivial-copy.cpp
@@ -46,15 +46,10 @@ void _01_empty_structs() {
   empty Empty = conjure<empty>();
   empty Empty2 = Empty;
   empty Empty3 = Empty2;
-  // All of these should refer to the exact same symbol, because all of
-  // these trivial copies refer to the original conjured value.
-  // There were Unknown before:
-  clang_analyzer_denote(Empty, "$Empty");
-  clang_analyzer_express(Empty);  // expected-warning {{$Empty}}
-  clang_analyzer_express(Empty2); // expected-warning {{$Empty}}
-  clang_analyzer_express(Empty3); // expected-warning {{$Empty}}
 
-  // We should have the same Conjured symbol for "Empty", "Empty2" and 
"Empty3".
+  // We only have binding for the original Empty object, because copying empty
+  // objects is a no-op in the performTrivialCopy. This is fine, because empty
+  // objects don't have any data members that could be accessed anyway.
   clang_analyzer_printState();
   // CHECK:       "store": { "pointer": "0x{{[0-9a-f]+}}", "items": [
   // CHECK-NEXT:    { "cluster": "GlobalInternalSpaceRegion", "pointer": 
"0x{{[0-9a-f]+}}", "items": [
@@ -65,12 +60,6 @@ void _01_empty_structs() {
   // CHECK-NEXT:    ]},
   // CHECK-NEXT:    { "cluster": "Empty", "pointer": "0x{{[0-9a-f]+}}", 
"items": [
   // CHECK-NEXT:      { "kind": "Default", "offset": 0, "value": 
"[[EMPTY_CONJ:conj_\$[0-9]+{int, LC[0-9]+, S[0-9]+, #[0-9]+}]]" }
-  // CHECK-NEXT:    ]},
-  // CHECK-NEXT:    { "cluster": "Empty2", "pointer": "0x{{[0-9a-f]+}}", 
"items": [
-  // CHECK-NEXT:      { "kind": "Default", "offset": 0, "value": 
"[[EMPTY_CONJ]]" }
-  // CHECK-NEXT:    ]},
-  // CHECK-NEXT:    { "cluster": "Empty3", "pointer": "0x{{[0-9a-f]+}}", 
"items": [
-  // CHECK-NEXT:      { "kind": "Default", "offset": 0, "value": 
"[[EMPTY_CONJ]]" }
   // CHECK-NEXT:    ]}
   // CHECK-NEXT:  ]},
 
diff --git a/clang/test/Analysis/issue-157467.cpp 
b/clang/test/Analysis/issue-157467.cpp
new file mode 100644
index 0000000000000..8281ea1ee1aed
--- /dev/null
+++ b/clang/test/Analysis/issue-157467.cpp
@@ -0,0 +1,39 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=core -verify %s
+// expected-no-diagnostics
+
+template <class T, int Idx, bool CanBeEmptyBase = __is_empty(T) && 
(!__is_final(T))>
+struct compressed_pair_elem {
+  explicit compressed_pair_elem(T u) : value(u) {}
+  T value;
+};
+
+template <class T, int Idx>
+struct compressed_pair_elem<T, Idx, /*CanBeEmptyBase=*/true> : T {
+  explicit compressed_pair_elem(T u) : T(u) {}
+};
+
+template <class T1, class T2, class Base1 = compressed_pair_elem<T1, 0>, class 
Base2 = compressed_pair_elem<T2, 1>>
+struct compressed_pair : Base1, Base2 {
+  explicit compressed_pair(T1 t1, T2 t2) : Base1(t1), Base2(t2) {}
+};
+
+// empty deleter object
+template <class T>
+struct default_delete {
+  void operator()(T* p) {
+    delete p;
+  }
+};
+
+template <class T, class Deleter = default_delete<T> >
+struct some_unique_ptr {
+  // compressed_pair will employ the empty base class optimization, thus 
overlapping
+  // the `int*` and the empty `Deleter` object, clobbering the pointer.
+  compressed_pair<int*, Deleter> ptr;
+  some_unique_ptr(int* p, Deleter d) : ptr(p, d) {}
+  ~some_unique_ptr();
+};
+
+void entry_point() {
+  some_unique_ptr<int, default_delete<int> > u3(new int(12), 
default_delete<int>());
+}
diff --git a/clang/test/Analysis/taint-generic.cpp 
b/clang/test/Analysis/taint-generic.cpp
index c080313e4d172..fc7c37300d3fc 100644
--- a/clang/test/Analysis/taint-generic.cpp
+++ b/clang/test/Analysis/taint-generic.cpp
@@ -153,8 +153,9 @@ void top() {
   int Int = mySource1<int>();
   clang_analyzer_isTainted(Int); // expected-warning {{YES}}
 
+  // It's fine to not propagate taint to empty classes, since they don't have 
any data members.
   Empty E = mySource1<Empty>();
-  clang_analyzer_isTainted(E); // expected-warning {{YES}}
+  clang_analyzer_isTainted(E); // expected-warning {{NO}}
 
   Aggr A = mySource1<Aggr>();
   clang_analyzer_isTainted(A);      // expected-warning {{YES}}

``````````

</details>


https://github.com/llvm/llvm-project/pull/157480
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to