Skip to content

Commit cc3ad20

Browse files
[analyzer] Revert incorrect LazyCoumpoundVal changes (#163461)
Reverts #115917 and its follow up #116840. Fixes #153782 and introduces regression tests. Reopens #114270.
1 parent 78769d5 commit cc3ad20

10 files changed

+131
-93
lines changed

clang/lib/StaticAnalyzer/Core/RegionStore.cpp

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -714,11 +714,6 @@ class RegionStoreManager : public StoreManager {
714714
return getBinding(getRegionBindings(S), L, T);
715715
}
716716

717-
std::optional<SVal> getUniqueDefaultBinding(RegionBindingsConstRef B,
718-
const TypedValueRegion *R) const;
719-
std::optional<SVal>
720-
getUniqueDefaultBinding(nonloc::LazyCompoundVal LCV) const;
721-
722717
std::optional<SVal> getDefaultBinding(Store S, const MemRegion *R) override {
723718
RegionBindingsRef B = getRegionBindings(S);
724719
// Default bindings are always applied over a base region so look up the
@@ -2465,11 +2460,6 @@ SVal RegionStoreManager::getBindingForStruct(RegionBindingsConstRef B,
24652460
// behavior doesn't depend on the struct layout.
24662461
// This way even an empty struct can carry taint, no matter if creduce drops
24672462
// the last field member or not.
2468-
2469-
// Try to avoid creating a LCV if it would anyways just refer to a single
2470-
// default binding.
2471-
if (std::optional<SVal> Val = getUniqueDefaultBinding(B, R))
2472-
return *Val;
24732463
return createLazyBinding(B, R);
24742464
}
24752465

@@ -2757,50 +2747,12 @@ RegionStoreManager::bindVector(LimitedRegionBindingsConstRef B,
27572747
return NewB;
27582748
}
27592749

2760-
std::optional<SVal>
2761-
RegionStoreManager::getUniqueDefaultBinding(RegionBindingsConstRef B,
2762-
const TypedValueRegion *R) const {
2763-
if (R != R->getBaseRegion())
2764-
return std::nullopt;
2765-
2766-
const auto *Cluster = B.lookup(R);
2767-
if (!Cluster || !llvm::hasSingleElement(*Cluster))
2768-
return std::nullopt;
2769-
2770-
const auto [Key, Value] = *Cluster->begin();
2771-
return Key.isDirect() ? std::optional<SVal>{} : Value;
2772-
}
2773-
2774-
std::optional<SVal>
2775-
RegionStoreManager::getUniqueDefaultBinding(nonloc::LazyCompoundVal LCV) const {
2776-
auto B = getRegionBindings(LCV.getStore());
2777-
return getUniqueDefaultBinding(B, LCV.getRegion());
2778-
}
2779-
27802750
std::optional<LimitedRegionBindingsRef> RegionStoreManager::tryBindSmallStruct(
27812751
LimitedRegionBindingsConstRef B, const TypedValueRegion *R,
27822752
const RecordDecl *RD, nonloc::LazyCompoundVal LCV) {
27832753
if (B.hasExhaustedBindingLimit())
27842754
return B.withValuesEscaped(LCV);
27852755

2786-
// If we try to copy a Conjured value representing the value of the whole
2787-
// struct, don't try to element-wise copy each field.
2788-
// That would unnecessarily bind Derived symbols slicing off the subregion for
2789-
// the field from the whole Conjured symbol.
2790-
//
2791-
// struct Window { int width; int height; };
2792-
// Window getWindow(); <-- opaque fn.
2793-
// Window w = getWindow(); <-- conjures a new Window.
2794-
// Window w2 = w; <-- trivial copy "w", calling "tryBindSmallStruct"
2795-
//
2796-
// We should not end up with a new Store for "w2" like this:
2797-
// Direct [ 0..31]: Derived{Conj{}, w.width}
2798-
// Direct [32..63]: Derived{Conj{}, w.height}
2799-
// Instead, we should just bind that Conjured value instead.
2800-
if (std::optional<SVal> Val = getUniqueDefaultBinding(LCV)) {
2801-
return B.addBinding(BindingKey::Make(R, BindingKey::Default), Val.value());
2802-
}
2803-
28042756
FieldVector Fields;
28052757

28062758
if (const CXXRecordDecl *Class = dyn_cast<CXXRecordDecl>(RD))

clang/test/Analysis/NewDelete-checker-test.cpp

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
// RUN: -analyzer-checker=core \
44
// RUN: -analyzer-checker=cplusplus.NewDelete
55
//
6-
// RUN: %clang_analyze_cc1 -DLEAKS -std=c++11 -fblocks %s \
6+
// RUN: %clang_analyze_cc1 -std=c++11 -fblocks %s \
77
// RUN: -verify=expected,newdelete,leak \
88
// RUN: -analyzer-checker=core \
99
// RUN: -analyzer-checker=cplusplus.NewDelete \
1010
// RUN: -analyzer-checker=cplusplus.NewDeleteLeaks
1111
//
12-
// RUN: %clang_analyze_cc1 -std=c++11 -fblocks -verify %s \
12+
// RUN: %clang_analyze_cc1 -std=c++11 -fblocks %s \
1313
// RUN: -verify=expected,leak \
1414
// RUN: -analyzer-checker=core \
1515
// RUN: -analyzer-checker=cplusplus.NewDeleteLeaks
@@ -19,13 +19,13 @@
1919
// RUN: -analyzer-checker=core \
2020
// RUN: -analyzer-checker=cplusplus.NewDelete
2121
//
22-
// RUN: %clang_analyze_cc1 -DLEAKS -std=c++17 -fblocks %s \
22+
// RUN: %clang_analyze_cc1 -std=c++17 -fblocks %s \
2323
// RUN: -verify=expected,newdelete,leak \
2424
// RUN: -analyzer-checker=core \
2525
// RUN: -analyzer-checker=cplusplus.NewDelete \
2626
// RUN: -analyzer-checker=cplusplus.NewDeleteLeaks
2727
//
28-
// RUN: %clang_analyze_cc1 -std=c++17 -fblocks -verify %s \
28+
// RUN: %clang_analyze_cc1 -std=c++17 -fblocks %s \
2929
// RUN: -verify=expected,leak,inspection \
3030
// RUN: -analyzer-checker=core \
3131
// RUN: -analyzer-checker=cplusplus.NewDeleteLeaks \
@@ -503,3 +503,75 @@ namespace optional_union {
503503
custom_union_t a;
504504
} // leak-warning{{Potential leak of memory pointed to by 'a.present.q'}}
505505
}
506+
507+
namespace gh153782 {
508+
509+
// Ensure we do not regress on the following use case.
510+
511+
namespace mutually_exclusive_test_case_1 {
512+
struct StorageWrapper {
513+
// Imagine the destructor and copy constructor both call a reset() function (among other things).
514+
~StorageWrapper() { delete parts; }
515+
StorageWrapper(StorageWrapper const&) = default;
516+
517+
// Mind that there is no `parts = other.parts` assignment -- this is the bug we would like to find.
518+
void operator=(StorageWrapper&& other) { delete parts; } // newdelete-warning{{Attempt to release already released memory}}
519+
520+
// Not provided, typically would do `parts = new long`.
521+
StorageWrapper();
522+
523+
long* parts;
524+
};
525+
526+
void test_non_trivial_struct_assignment() {
527+
StorageWrapper* object = new StorageWrapper[]{StorageWrapper()};
528+
object[0] = StorageWrapper(); // This assignment leads to the double-free.
529+
}
530+
} // mutually_exclusive_test_case_1
531+
532+
namespace mutually_exclusive_test_case_2 {
533+
struct StorageWrapper {
534+
// Imagine the destructor and copy constructor both call a reset() function (among other things).
535+
~StorageWrapper() { delete parts; }
536+
StorageWrapper(StorageWrapper const&) = default;
537+
538+
// Mind that there is no `parts = other.parts` assignment -- this is the bug we would like to find.
539+
void operator=(StorageWrapper&& other) { delete parts; }
540+
541+
// Not provided, typically would do `parts = new long`.
542+
StorageWrapper();
543+
544+
long* parts;
545+
};
546+
547+
void test_non_trivial_struct_assignment() {
548+
StorageWrapper* object = new StorageWrapper[]{StorageWrapper()};
549+
// object[0] = StorageWrapper(); // Remove the source of double free to make the potential leak appear.
550+
} // leak-warning{{Potential leak of memory pointed to by 'object'}}
551+
} // mutually_exclusive_test_case_2
552+
553+
namespace mutually_exclusive_test_case_3 {
554+
struct StorageWrapper {
555+
// Imagine the destructor and copy constructor both call a reset() function (among other things).
556+
~StorageWrapper() { delete parts; }
557+
StorageWrapper(StorageWrapper const&) = default;
558+
559+
// Mind that there is no `parts = other.parts` assignment -- this is the bug we would like to find.
560+
void operator=(StorageWrapper&& other) { delete parts; } // newdelete-warning{{Attempt to release already released memory}}
561+
562+
// Not provided, typically would do `parts = new long`.
563+
StorageWrapper();
564+
565+
long* parts;
566+
};
567+
568+
struct TestDoubleFreeWithInitializerList {
569+
StorageWrapper* Object;
570+
TestDoubleFreeWithInitializerList()
571+
: Object(new StorageWrapper[]{StorageWrapper()}) {
572+
Object[0] = StorageWrapper(); // This assignment leads to the double-free.
573+
}
574+
};
575+
} // mutually_exclusive_test_case_3
576+
577+
} // namespace gh153782

clang/test/Analysis/ctor-trivial-copy.cpp

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
void clang_analyzer_printState();
66
template <typename T> void clang_analyzer_dump_lref(T& param);
77
template <typename T> void clang_analyzer_dump_val(T param);
8-
template <typename T> void clang_analyzer_denote(T param, const char *name);
9-
template <typename T> void clang_analyzer_express(T param);
108
template <typename T> T conjure();
119
template <typename... Ts> void nop(const Ts &... args) {}
1210

@@ -42,10 +40,16 @@ void test_assign_return() {
4240
namespace trivial_struct_copy {
4341

4442
void _01_empty_structs() {
45-
clang_analyzer_dump_val(conjure<empty>()); // expected-warning {{conj_$}}
43+
clang_analyzer_dump_val(conjure<empty>()); // expected-warning {{lazyCompoundVal}}
4644
empty Empty = conjure<empty>();
4745
empty Empty2 = Empty;
4846
empty Empty3 = Empty2;
47+
// All of these should refer to the exact same LCV, because all of
48+
// these trivial copies refer to the original conjured value.
49+
// There were Unknown before:
50+
clang_analyzer_dump_val(Empty); // expected-warning {{lazyCompoundVal}}
51+
clang_analyzer_dump_val(Empty2); // expected-warning {{lazyCompoundVal}}
52+
clang_analyzer_dump_val(Empty3); // expected-warning {{lazyCompoundVal}}
4953

5054
// We only have binding for the original Empty object, because copying empty
5155
// objects is a no-op in the performTrivialCopy. This is fine, because empty
@@ -67,20 +71,18 @@ void _01_empty_structs() {
6771
}
6872

6973
void _02_structs_with_members() {
70-
clang_analyzer_dump_val(conjure<aggr>()); // expected-warning {{conj_$}}
74+
clang_analyzer_dump_val(conjure<aggr>()); // expected-warning {{lazyCompoundVal}}
7175
aggr Aggr = conjure<aggr>();
7276
aggr Aggr2 = Aggr;
7377
aggr Aggr3 = Aggr2;
74-
// All of these should refer to the exact same symbol, because all of
78+
// All of these should refer to the exact same LCV, because all of
7579
// these trivial copies refer to the original conjured value.
76-
clang_analyzer_denote(Aggr, "$Aggr");
77-
clang_analyzer_express(Aggr); // expected-warning {{$Aggr}}
78-
clang_analyzer_express(Aggr2); // expected-warning {{$Aggr}}
79-
clang_analyzer_express(Aggr3); // expected-warning {{$Aggr}}
80-
81-
// We should have the same Conjured symbol for "Aggr", "Aggr2" and "Aggr3".
82-
// We used to have Derived symbols for the individual fields that were
83-
// copied as part of copying the whole struct.
80+
clang_analyzer_dump_val(Aggr); // expected-warning {{lazyCompoundVal}}
81+
clang_analyzer_dump_val(Aggr2); // expected-warning {{lazyCompoundVal}}
82+
clang_analyzer_dump_val(Aggr3); // expected-warning {{lazyCompoundVal}}
83+
84+
// We have fields in the struct we copy, thus we also have the entries for the copies
85+
// (and for all of their fields).
8486
clang_analyzer_printState();
8587
// CHECK: "store": { "pointer": "0x{{[0-9a-f]+}}", "items": [
8688
// CHECK-NEXT: { "cluster": "GlobalInternalSpaceRegion", "pointer": "0x{{[0-9a-f]+}}", "items": [
@@ -93,10 +95,12 @@ void _02_structs_with_members() {
9395
// CHECK-NEXT: { "kind": "Default", "offset": 0, "value": "[[AGGR_CONJ:conj_\$[0-9]+{int, LC[0-9]+, S[0-9]+, #[0-9]+}]]" }
9496
// CHECK-NEXT: ]},
9597
// CHECK-NEXT: { "cluster": "Aggr2", "pointer": "0x{{[0-9a-f]+}}", "items": [
96-
// CHECK-NEXT: { "kind": "Default", "offset": 0, "value": "[[AGGR_CONJ]]" }
98+
// CHECK-NEXT: { "kind": "Direct", "offset": 0, "value": "derived_${{[0-9]+}}{[[AGGR_CONJ]],Aggr.x}" },
99+
// CHECK-NEXT: { "kind": "Direct", "offset": 32, "value": "derived_${{[0-9]+}}{[[AGGR_CONJ]],Aggr.y}" }
97100
// CHECK-NEXT: ]},
98101
// CHECK-NEXT: { "cluster": "Aggr3", "pointer": "0x{{[0-9a-f]+}}", "items": [
99-
// CHECK-NEXT: { "kind": "Default", "offset": 0, "value": "[[AGGR_CONJ]]" }
102+
// CHECK-NEXT: { "kind": "Direct", "offset": 0, "value": "derived_${{[0-9]+}}{[[AGGR_CONJ]],Aggr.x}" },
103+
// CHECK-NEXT: { "kind": "Direct", "offset": 32, "value": "derived_${{[0-9]+}}{[[AGGR_CONJ]],Aggr.y}" }
100104
// CHECK-NEXT: ]}
101105
// CHECK-NEXT: ]},
102106

@@ -113,3 +117,31 @@ void entrypoint() {
113117
}
114118

115119
} // namespace trivial_struct_copy
120+
121+
namespace gh153782 {
122+
123+
// Ensure we do not regress on the following use cases.
124+
// The assumption made on a field in `setPtr` should apply to the returned copy in `func`.
125+
struct Status { int error; };
126+
Status getError();
127+
128+
Status setPtr(int **outptr, int* ptr) {
129+
Status e = getError();
130+
if (e.error != 0) return e; // When assuming the error field is non-zero,
131+
*outptr = ptr; // this is not executed
132+
return e;
133+
}
134+
135+
int func() {
136+
int *ptr = nullptr;
137+
int x = 42;
138+
if (setPtr(&ptr, &x).error == 0) {
139+
// The assumption made in get() SHOULD match the assumption about
140+
// the returned value, hence the engine SHOULD NOT assume ptr is null.
141+
clang_analyzer_dump_val(ptr); // expected-warning {{&x}}
142+
return *ptr;
143+
}
144+
return 0;
145+
}
146+
147+
} // namespace gh153782

clang/test/Analysis/explain-svals.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class C {
9999
} // end of anonymous namespace
100100

101101
void test_6() {
102-
clang_analyzer_explain(conjure_S()); // expected-warning-re{{{{^symbol of type 'int' conjured at CFG element 'conjure_S\(\) \(CXXRecordTypedCall, \+0\)'$}}}}
102+
clang_analyzer_explain(conjure_S()); // expected-warning-re{{{{^lazily frozen compound value of 1st parameter of function 'clang_analyzer_explain\(\)'$}}}}
103103
clang_analyzer_explain(conjure_S().z); // expected-warning-re{{{{^value derived from \(symbol of type 'int' conjured at CFG element 'conjure_S\(\) \(CXXRecordTypedCall, \)'\) for field 'z' of temporary object constructed at statement 'conjure_S\(\)'$}}}}
104104
}
105105

clang/test/Analysis/iterator-modeling.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,7 +2035,6 @@ void print_state(std::vector<int> &V) {
20352035
// CHECK: "checker_messages": [
20362036
// CHECK: { "checker": "alpha.cplusplus.IteratorModeling", "messages": [
20372037
// CHECK-NEXT: "Iterator Positions :",
2038-
// CHECK-NEXT: "conj_$[[#]]{int, LC[[#]], S[[#]], #[[#]]} : Valid ; Container == SymRegion{reg_$[[#]]<std::vector<int> & V>} ; Offset == conj_$[[#]]{long, LC[[#]], S[[#]], #[[#]]}",
20392038
// CHECK-NEXT: "i0 : Valid ; Container == SymRegion{reg_$[[#]]<std::vector<int> & V>} ; Offset == conj_$[[#]]{long, LC[[#]], S[[#]], #[[#]]}"
20402039
// CHECK-NEXT: ]}
20412040

@@ -2046,7 +2045,6 @@ void print_state(std::vector<int> &V) {
20462045
// CHECK: "checker_messages": [
20472046
// CHECK: { "checker": "alpha.cplusplus.IteratorModeling", "messages": [
20482047
// CHECK-NEXT: "Iterator Positions :",
2049-
// CHECK-NEXT: "conj_$[[#]]{int, LC[[#]], S[[#]], #[[#]]} : Valid ; Container == SymRegion{reg_$[[#]]<std::vector<int> & V>} ; Offset == conj_$[[#]]{long, LC[[#]], S[[#]], #[[#]]}",
20502048
// CHECK-NEXT: "i1 : Valid ; Container == SymRegion{reg_$[[#]]<std::vector<int> & V>} ; Offset == conj_$[[#]]{long, LC[[#]], S[[#]], #[[#]]}"
20512049
// CHECK-NEXT: ]}
20522050

clang/test/Analysis/stl-algorithm-modeling-aggressive-std-find-modeling.cpp

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,6 @@
44
// RUN: -analyzer-config alpha.cplusplus.STLAlgorithmModeling:AggressiveStdFindModeling=true\
55
// RUN: -verify
66

7-
// STLAlgorithmModeling and DebugIteratorModeling are probably bugged because
8-
// these tests started failing after we just directly copy the symbol
9-
// representing the value of a variable instead of creating a LazyCompoundVal
10-
// of that single conjured value.
11-
// In theory, it shouldn't matter if we eagerly copy the value that we would
12-
// "load" from the LCV once requested or just directly binding the backing symbol.
13-
// Yet, these tests fail, so there is likely messed up how/what the checker
14-
// metadata is associated with.
15-
// XFAIL: *
16-
177
#include "Inputs/system-header-simulator-cxx.h"
188

199
void clang_analyzer_eval(bool);

clang/test/Analysis/stl-algorithm-modeling.cpp

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,6 @@
33
// RUN: -analyzer-config aggressive-binary-operation-simplification=true\
44
// RUN: -verify
55

6-
// STLAlgorithmModeling and DebugIteratorModeling are probably bugged because
7-
// these tests started failing after we just directly copy the symbol
8-
// representing the value of a variable instead of creating a LazyCompoundVal
9-
// of that single conjured value.
10-
// In theory, it shouldn't matter if we eagerly copy the value that we would
11-
// "load" from the LCV once requested or just directly binding the backing symbol.
12-
// Yet, these tests fail, so there is likely messed up how/what the checker
13-
// metadata is associated with.
14-
// XFAIL: *
15-
166
#include "Inputs/system-header-simulator-cxx.h"
177

188
void clang_analyzer_eval(bool);

clang/test/Analysis/store-dump-orders.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ void test_output(int n) {
4141
// CHECK-NEXT: { "kind": "Default", "offset": 0, "value": "conj_$
4242
// CHECK-NEXT: ]},
4343
// CHECK-NEXT: { "cluster": "objfirst", "pointer": "0x{{[0-9a-f]+}}", "items": [
44-
// CHECK-NEXT: { "kind": "Default", "offset": 0, "value": "conj_$
44+
// CHECK-NEXT: { "kind": "Default", "offset": 0, "value": "lazyCompoundVal
4545
// CHECK-NEXT: { "kind": "Direct", "offset": 320, "value": "1 S32b" },
4646
// CHECK-NEXT: { "kind": "Direct", "offset": 352, "value": "2 S32b" },
4747
// CHECK-NEXT: { "kind": "Direct", "offset": 384, "value": "3 S32b" }

clang/test/Analysis/taint-generic.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,11 @@ void top() {
158158
clang_analyzer_isTainted(E); // expected-warning {{NO}}
159159

160160
Aggr A = mySource1<Aggr>();
161-
clang_analyzer_isTainted(A); // expected-warning {{YES}}
161+
// FIXME Ideally, both A and A.data should be tainted. However, the
162+
// implementation used by e5ac9145ba29 ([analyzer][taint] Recognize
163+
// tainted LazyCompoundVals (4/4) (#115919), 2024-11-15) led to FPs and
164+
// FNs in various scenarios and had to be reverted to fix #153782.
165+
clang_analyzer_isTainted(A); // expected-warning {{NO}}
162166
clang_analyzer_isTainted(A.data); // expected-warning {{YES}}
163167
}
164168
} // namespace gh114270

clang/test/Analysis/template-param-objects.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ bool operator ==(Box lhs, Box rhs) {
1111
return lhs.value == rhs.value;
1212
}
1313
template <Box V> void dumps() {
14-
clang_analyzer_dump(V); // expected-warning {{Unknown}}
14+
clang_analyzer_dump(V); // expected-warning {{lazyCompoundVal}}
1515
clang_analyzer_dump(&V); // expected-warning {{Unknown}}
1616
clang_analyzer_dump(V.value); // expected-warning {{Unknown}} FIXME: It should be '6 S32b'.
1717
clang_analyzer_dump(&V.value); // expected-warning {{Unknown}}

0 commit comments

Comments
 (0)