https://github.com/ziqingluo-90 updated https://github.com/llvm/llvm-project/pull/209354
>From b218156690f92530f54a938fe015583bd6fc6ed8 Mon Sep 17 00:00:00 2001 From: Ziqing Luo <[email protected]> Date: Mon, 13 Jul 2026 18:44:23 -0700 Subject: [PATCH 1/4] [SSAF] Filter out type-constrained pointers from all reachable unsafe pointers Integrate the TypeConstrainedPointers analysis results into UnsafeBufferReachableAnalysis. The final result is filtered with type-constrainted pointers. The pointer flow graph is untouched. Removing type-constrained pointers from the graph will introduce unsoundness. Final step for rdar://179151541&179151882 --- .../UnsafeBufferUsageAnalysis.cpp | 74 +++++++++++++++---- .../WholeProgramAnalysis/AnalysisDriver.cpp | 18 ++--- .../external-inline-function-in-multi-tu.test | 2 +- .../PointerFlow/lref-to-rref-cast.test | 2 +- .../PointerFlow/multi-decl-contributor.cpp | 2 +- .../multi-dim-pointer-flow-constraint.test | 2 +- ...achable-excludes-type-constrained-main.cpp | 40 ++++++++++ ...e-excludes-type-constrained-new-delete.cpp | 74 +++++++++++++++++++ .../ssaf-analyzer/Outputs/empty-pairs.json | 26 +++++++ .../Scalable/ssaf-analyzer/analyzer.test | 10 +-- 10 files changed, 215 insertions(+), 35 deletions(-) create mode 100644 clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-main.cpp create mode 100644 clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-new-delete.cpp create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/empty-pairs.json diff --git a/clang/lib/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageAnalysis.cpp b/clang/lib/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageAnalysis.cpp index e404eb294ee4b..2d0a58f81a0f0 100644 --- a/clang/lib/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageAnalysis.cpp +++ b/clang/lib/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageAnalysis.cpp @@ -16,7 +16,9 @@ #include "clang/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.h" #include "clang/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevelFormat.h" #include "clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowAnalysis.h" +#include "clang/ScalableStaticAnalysis/Analyses/TypeConstrainedPointers/TypeConstrainedPointers.h" #include "clang/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsage.h" +#include "clang/ScalableStaticAnalysis/Core/Model/EntityId.h" #include "clang/ScalableStaticAnalysis/Core/Serialization/JSONFormat.h" #include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/AnalysisRegistry.h" #include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/SummaryAnalysis.h" @@ -124,12 +126,21 @@ JSONFormat::AnalysisResultRegistry::Add<UnsafeBufferReachableAnalysisResult> serializeUnsafeBufferReachableAnalysisResult, deserializeUnsafeBufferReachableAnalysisResult); -/// Computes all the reachable "nodes" (pointers) in a pointer flow graph from a -/// provided starter node set. Specifically, the starter set is the unsafe -/// pointers found by `UnsafeBufferUsageAnalysis`. +/// \brief Computes pointers (EPLs) that satisfy a specific set of constraints. +/// +/// The pointers must satisfy all of the following constraints: +/// +/// 1. **C1 (Unsafe):** Any pointer in `UnsafeBufferUsageAnalysisResult` +/// is considered unsafe. +/// 2. **C2 (Reachable):** If a pointer is reachable from an unsafe pointer in +/// the pointer flow graph (provided by `PointerFlowAnalysisResult`), it is +/// also unsafe. +/// 3. **C3 (Transformable):** Any pointer associated with type-constrained +/// entities is NOT transformable. class UnsafeBufferReachableAnalysis : public DerivedAnalysis<UnsafeBufferReachableAnalysisResult, PointerFlowAnalysisResult, + TypeConstrainedPointersAnalysisResult, UnsafeBufferUsageAnalysisResult> { /// BoundsPropagationGraph adds bounds propagation semantics to the @@ -186,6 +197,7 @@ class UnsafeBufferReachableAnalysis }; std::map<EntityId, BoundsPropagationGraph> BPG; + const std::set<EntityId> *TypeConstrainedEntities = nullptr; // Use pointers for efficiency. EPLs are in tree-based containers that only // grow. So pointers to them are stable. @@ -207,18 +219,9 @@ class UnsafeBufferReachableAnalysis } } -public: - llvm::Error - initialize(const PointerFlowAnalysisResult &PtrFlowGraph, - const UnsafeBufferUsageAnalysisResult &Starter) override { - for (auto &[Id, SubGraph] : PtrFlowGraph.Edges) - BPG.try_emplace(Id, BoundsPropagationGraph(SubGraph)); - assert(getResult().Reachables.empty()); - getResult().Reachables.insert(Starter.begin(), Starter.end()); - return llvm::Error::success(); - } - - llvm::Expected<bool> step() override { + // Expand the initial set of C1 pointers in `getResult().Reachables` by + // computing and appending all reachable pointers, satisfying both C1 and C2. + void computeReachableUnsafePointers() { auto &Reachables = getResult().Reachables; // Simple DFS: std::vector<EPLPtr> Worklist; @@ -233,6 +236,47 @@ class UnsafeBufferReachableAnalysis updateReachablesWithOutgoings(Node, Worklist); } + } + + // Filter out non-transformable pointers from `getResult().Result`, leaving + // only those that satisfy C3. + void filterTransformablePointers() { + assert(TypeConstrainedEntities && + "The initialize(...) method should initialize " + "TypeConstrainedEntities to non-null"); + auto &Result = getResult().Reachables; + + for (auto &[Key, EPLs] : Result) { + for (auto ConstrainedEntityId : *TypeConstrainedEntities) { + // FIXME: optimization chance here. Since both sets are sorted, the + // next 'equal_range' search can ignore everything before + // `NonTransEPLs.second`. + auto NonTransEPLs = EPLs.equal_range(ConstrainedEntityId); + + EPLs.erase(NonTransEPLs.first, NonTransEPLs.second); + } + } + } + +public: + llvm::Error + initialize(const PointerFlowAnalysisResult &PtrFlowGraph, + const TypeConstrainedPointersAnalysisResult &TypeConstraints, + const UnsafeBufferUsageAnalysisResult &UnsafePtrs) override { + for (auto &[Id, SubGraph] : PtrFlowGraph.Edges) + BPG.try_emplace(Id, BoundsPropagationGraph(SubGraph)); + TypeConstrainedEntities = &TypeConstraints.Entities; + assert(getResult().Reachables.empty()); + // C1: all pointers in UnsafeBufferUsageAnalysisResult are unsafe + getResult().Reachables.insert(UnsafePtrs.begin(), UnsafePtrs.end()); + return llvm::Error::success(); + } + + llvm::Expected<bool> step() override { + // result meets C1 & C2: + computeReachableUnsafePointers(); + // result meets C1 & C2 & C3: + filterTransformablePointers(); // This is not an iterative algorithm so stop iteration by retruning false: return false; } diff --git a/clang/lib/ScalableStaticAnalysis/Core/WholeProgramAnalysis/AnalysisDriver.cpp b/clang/lib/ScalableStaticAnalysis/Core/WholeProgramAnalysis/AnalysisDriver.cpp index f60c916e10b67..5f7940bbb8bb5 100644 --- a/clang/lib/ScalableStaticAnalysis/Core/WholeProgramAnalysis/AnalysisDriver.cpp +++ b/clang/lib/ScalableStaticAnalysis/Core/WholeProgramAnalysis/AnalysisDriver.cpp @@ -103,23 +103,19 @@ AnalysisDriver::toposort(llvm::ArrayRef<AnalysisName> Roots) { llvm::Error AnalysisDriver::executeSummaryAnalysis(SummaryAnalysisBase &Summary, WPASuite &Suite) const { SummaryName SN = Summary.getSummaryName(); - auto DataIt = LU->Data.find(SN); - if (DataIt == LU->Data.end()) { - return ErrorBuilder::create(std::errc::invalid_argument, - "no data for analysis '{0}' in LUSummary", - Summary.getAnalysisName()) - .build(); - } if (auto Err = Summary.initialize()) { return Err; } - for (auto &[Id, EntitySummary] : DataIt->second) { - if (auto Err = Summary.add(Id, *EntitySummary)) { - return Err; + auto DataIt = LU->Data.find(SN); + + if (DataIt != LU->Data.end()) + for (auto &[Id, EntitySummary] : DataIt->second) { + if (auto Err = Summary.add(Id, *EntitySummary)) { + return Err; + } } - } if (auto Err = Summary.finalize()) { return Err; diff --git a/clang/test/Analysis/Scalable/PointerFlow/external-inline-function-in-multi-tu.test b/clang/test/Analysis/Scalable/PointerFlow/external-inline-function-in-multi-tu.test index b0ffc1b3cf947..f0e98f23273d9 100644 --- a/clang/test/Analysis/Scalable/PointerFlow/external-inline-function-in-multi-tu.test +++ b/clang/test/Analysis/Scalable/PointerFlow/external-inline-function-in-multi-tu.test @@ -4,7 +4,7 @@ // during bounds propagation. -// DEFINE: %{extract} = %clang_cc1 -fsyntax-only -I %t --ssaf-extract-summaries=PointerFlow,UnsafeBufferUsage +// DEFINE: %{extract} = %clang_cc1 -fsyntax-only -I %t --ssaf-extract-summaries=PointerFlow,UnsafeBufferUsage,TypeConstrainedPointers // RUN: rm -rf %t // RUN: mkdir -p %t diff --git a/clang/test/Analysis/Scalable/PointerFlow/lref-to-rref-cast.test b/clang/test/Analysis/Scalable/PointerFlow/lref-to-rref-cast.test index ca5df041240aa..fdb57741864e3 100644 --- a/clang/test/Analysis/Scalable/PointerFlow/lref-to-rref-cast.test +++ b/clang/test/Analysis/Scalable/PointerFlow/lref-to-rref-cast.test @@ -6,7 +6,7 @@ // Extract per-TU PointerFlow + UnsafeBufferUsage summaries. // RUN: %clang_cc1 -fsyntax-only %t/tu.cpp \ -// RUN: --ssaf-extract-summaries=PointerFlow,UnsafeBufferUsage \ +// RUN: --ssaf-extract-summaries=PointerFlow,UnsafeBufferUsage,TypeConstrainedPointers \ // RUN: --ssaf-tu-summary-file=%t/tu.summary.json \ // RUN: --ssaf-compilation-unit-id="tu-1" diff --git a/clang/test/Analysis/Scalable/PointerFlow/multi-decl-contributor.cpp b/clang/test/Analysis/Scalable/PointerFlow/multi-decl-contributor.cpp index 717a2875636b2..2d27026708eca 100644 --- a/clang/test/Analysis/Scalable/PointerFlow/multi-decl-contributor.cpp +++ b/clang/test/Analysis/Scalable/PointerFlow/multi-decl-contributor.cpp @@ -2,7 +2,7 @@ // RUN: %clang_cc1 -fsyntax-only %s \ -// RUN: --ssaf-extract-summaries=PointerFlow,UnsafeBufferUsage \ +// RUN: --ssaf-extract-summaries=PointerFlow,UnsafeBufferUsage,TypeConstrainedPointers \ // RUN: --ssaf-tu-summary-file=%t/tu.summary.json \ // RUN: --ssaf-compilation-unit-id="tu-1" diff --git a/clang/test/Analysis/Scalable/PointerFlow/multi-dim-pointer-flow-constraint.test b/clang/test/Analysis/Scalable/PointerFlow/multi-dim-pointer-flow-constraint.test index 511b375c05f6c..d057f3e4c2ef9 100644 --- a/clang/test/Analysis/Scalable/PointerFlow/multi-dim-pointer-flow-constraint.test +++ b/clang/test/Analysis/Scalable/PointerFlow/multi-dim-pointer-flow-constraint.test @@ -6,7 +6,7 @@ // RUN: %clang_cc1 -fsyntax-only %t/src.cpp \ -// RUN: --ssaf-extract-summaries=PointerFlow,UnsafeBufferUsage \ +// RUN: --ssaf-extract-summaries=PointerFlow,UnsafeBufferUsage,TypeConstrainedPointers \ // RUN: --ssaf-compilation-unit-id="tu-1" \ // RUN: --ssaf-tu-summary-file=%t/src.summary.json diff --git a/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-main.cpp b/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-main.cpp new file mode 100644 index 0000000000000..f7ab07f517c03 --- /dev/null +++ b/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-main.cpp @@ -0,0 +1,40 @@ +// Test that UnsafeBufferReachableAnalysis excludes type-constrained pointers + +// RUN: rm -rf %t && mkdir -p %t + +// RUN: %clang_cc1 -fsyntax-only %s \ +// RUN: --ssaf-extract-summaries=PointerFlow,UnsafeBufferUsage,TypeConstrainedPointers \ +// RUN: --ssaf-tu-summary-file=%t/tu.summary.json \ +// RUN: --ssaf-compilation-unit-id="tu-1" + +// RUN: clang-ssaf-linker %t/tu.summary.json -o %t/lu.json + +// RUN: clang-ssaf-analyzer %t/lu.json -o %t/wpa.json \ +// RUN: -a UnsafeBufferReachableAnalysisResult + +// RUN: FileCheck %s --input-file=%t/wpa.json + + +int main(int argc, char **argv) { + argv[5] = 0; // unsafe use of a type-constrained pointer + return 0; +} + +// 'q' is an ordinary unsafe pointer parameter and must remain in the result. +void foo(int *q) { + q[5] = 0; +} + +// CHECK-DAG: "id": [[Q_ID:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "1",[[:space:]]+"usr": }}"c:@F@foo#*I#" +// CHECK-DAG: "id": [[ARGV_ID:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "2",[[:space:]]+"usr": }}"c:@F@main{{.*}}" + +// 'argv' is reported as type-constrained. +// CHECK: "analysis_name": "TypeConstrainedPointersAnalysisResult" +// CHECK: "@": [[ARGV_ID]] + +// In the reachable result 'q' is present but 'argv' is not. +// CHECK: "analysis_name": "UnsafeBufferReachableAnalysisResult" +// CHECK-DAG: {{\{[[:space:]]+}}"@": [[Q_ID]]{{[[:space:]]+\},[[:space:]]+1[[:space:]]+\]}} +// CHECK-NOT: "@": [[ARGV_ID]] + +// CHECK: "analysis_name" diff --git a/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-new-delete.cpp b/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-new-delete.cpp new file mode 100644 index 0000000000000..c5a7a17bbf381 --- /dev/null +++ b/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-new-delete.cpp @@ -0,0 +1,74 @@ +// Test that UnsafeBufferReachableAnalysis excludes the +// type-constrained pointers of 'operator new' / 'operator delete' +// overloads. +// +// RUN: rm -rf %t && mkdir -p %t + +// RUN: %clang_cc1 -fsyntax-only %s \ +// RUN: --ssaf-extract-summaries=PointerFlow,UnsafeBufferUsage,TypeConstrainedPointers \ +// RUN: --ssaf-tu-summary-file=%t/tu.summary.json \ +// RUN: --ssaf-compilation-unit-id="tu-1" + +// RUN: clang-ssaf-linker %t/tu.summary.json -o %t/lu.json + +// RUN: clang-ssaf-analyzer %t/lu.json -o %t/wpa.json \ +// RUN: -a UnsafeBufferReachableAnalysisResult + +// RUN: FileCheck %s --input-file=%t/wpa.json + +typedef __SIZE_TYPE__ size_t; + +// Return value and the 2nd parameter are type-constrained: +void *operator new(size_t size, void *place) noexcept { + int *new_local = (int *)place; + + return new_local; +} + +// The parameter is type-constrained: +void operator delete(void *ptr) noexcept { + int *delete_local = (int *)ptr; + + delete_local[5] = 0; +} + +void foo(int *p) { + void *r = ::operator new(10, p); + int *q = (int *)r; + + // 'q' is unsafe, it propagates along the path + // 'q -> r -> return_new -> new_local -> place' + // All pointers along the path are rechable but return_new and place + // are type-constrained. + q[5] = 0; + ::operator delete(q); +} + + +// CHECK: "id_table" +// CHECK-DAG: "id": [[NEW_RET:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "0",[[:space:]]+"usr": }}"c:@F@operator new#l#*v#" +// CHECK-DAG: "id": [[NEW_PLACE:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "2",[[:space:]]+"usr": }}"c:@F@operator new#l#*v#" +// CHECK-DAG: "id": [[DEL_PTR:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "1",[[:space:]]+"usr": }}"c:@F@operator delete#*v#" + +// CHECK-DAG: "id": [[FOO_Q:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}foo#*I#@q" +// CHECK-DAG: "id": [[FOO_R:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}foo#*I#@r" +// CHECK-DAG: "id": [[NEW_LOCAL:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}operator new#l#*v#@new_local" +// CHECK-DAG: "id": [[DELETE_LOCAL:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}operator delete#*v#@delete_local" + +// The return and every new/delete parameter are reported as type-constrained. +// CHECK: "analysis_name": "TypeConstrainedPointersAnalysisResult" +// CHECK-DAG: "@": [[NEW_RET]] +// CHECK-DAG: "@": [[NEW_PLACE]] +// CHECK-DAG: "@": [[DEL_PTR]] + +// CHECK: "analysis_name": "UnsafeBufferReachableAnalysisResult" +// CHECK-DAG: {{\{[[:space:]]+}}"@": [[FOO_Q]]{{[[:space:]]+\},[[:space:]]+1[[:space:]]+\]}} +// CHECK-DAG: {{\{[[:space:]]+}}"@": [[FOO_R]]{{[[:space:]]+\},[[:space:]]+1[[:space:]]+\]}} +// CHECK-DAG: {{\{[[:space:]]+}}"@": [[NEW_LOCAL]]{{[[:space:]]+\},[[:space:]]+1[[:space:]]+\]}} +// CHECK-DAG: {{\{[[:space:]]+}}"@": [[DELETE_LOCAL]]{{[[:space:]]+\},[[:space:]]+1[[:space:]]+\]}} + +// CHECK-NOT: {{"@": }}[[NEW_RET]]{{[[:space:]]}} +// CHECK-NOT: {{"@": }}[[NEW_PLACE]]{{[[:space:]]}} +// CHECK-NOT: {{"@": }}[[DEL_PTR]]{{[[:space:]]}} + +// CHECK: "analysis_name" diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/empty-pairs.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/empty-pairs.json new file mode 100644 index 0000000000000..2e59f60b3a3a6 --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/empty-pairs.json @@ -0,0 +1,26 @@ +{ + "id_table": [ + { + "id": 0, + "name": { + "namespace": [ + { + "kind": "LinkUnit", + "name": "test.exe" + } + ], + "suffix": "", + "usr": "c:@F@foo#" + } + } + ], + "results": [ + { + "analysis_name": "PairsAnalysisResult", + "result": { + "pair_counts": [] + } + } + ], + "type": "WPASuite" +} diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/analyzer.test b/clang/test/Analysis/Scalable/ssaf-analyzer/analyzer.test index 0abdcef15a449..f76b2b78b52f4 100644 --- a/clang/test/Analysis/Scalable/ssaf-analyzer/analyzer.test +++ b/clang/test/Analysis/Scalable/ssaf-analyzer/analyzer.test @@ -15,13 +15,13 @@ // UNKNOWN: no analysis registered for 'AnalysisName(NoSuchAnalysis)' // ============================================================================ -// Error: valid analysis name but LUSummary lacks entity data for it +// Success: valid analysis name but LUSummary lacks entity data for it yields an +// empty result (missing data is not an error) // ============================================================================ -// RUN: not %clang-ssaf-analyzer-with-plugin %S/Inputs/lu-tags-only.json \ -// RUN: -o %t/missing-data.json -a PairsAnalysisResult 2>&1 \ -// RUN: | FileCheck %s --check-prefix=MISSING-DATA -// MISSING-DATA: no data for analysis 'AnalysisName(PairsAnalysisResult)' in LUSummary +// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu-tags-only.json \ +// RUN: -o %t/empty-pairs.json -a PairsAnalysisResult +// RUN: diff %S/Outputs/empty-pairs.json %t/empty-pairs.json // ============================================================================ // Success: run TagsAnalysisResult only (single analysis) >From 486a7ad427b2bbcb6a48948287b78abd4fbfc694 Mon Sep 17 00:00:00 2001 From: Ziqing Luo <[email protected]> Date: Tue, 14 Jul 2026 13:25:02 -0700 Subject: [PATCH 2/4] fix unit tests that asserted empty analysis results are errors --- .../AnalysisDriverTest.cpp | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/clang/unittests/ScalableStaticAnalysis/WholeProgramAnalysis/AnalysisDriverTest.cpp b/clang/unittests/ScalableStaticAnalysis/WholeProgramAnalysis/AnalysisDriverTest.cpp index 95077675fb2ec..9e518f08cc7de 100644 --- a/clang/unittests/ScalableStaticAnalysis/WholeProgramAnalysis/AnalysisDriverTest.cpp +++ b/clang/unittests/ScalableStaticAnalysis/WholeProgramAnalysis/AnalysisDriverTest.cpp @@ -468,15 +468,20 @@ TEST_F(AnalysisDriverTest, RunByName) { "no result for 'AnalysisName(Analysis2)' in WPASuite")); } -// run(names) — error when a requested name has no data in LUSummary. -TEST_F(AnalysisDriverTest, RunByNameErrorMissingData) { +// run(names) — a requested name with no data in the LUSummary yields an empty +// (but initialized and finalized) result rather than an error. +TEST_F(AnalysisDriverTest, RunByNameEmptyWhenMissingData) { auto LU = makeLUSummary(); AnalysisDriver Driver(std::move(LU)); - EXPECT_THAT_EXPECTED( - Driver.run({AnalysisName("Analysis1")}), - llvm::FailedWithMessage( - "no data for analysis 'AnalysisName(Analysis1)' in LUSummary")); + auto WPAOrErr = Driver.run({AnalysisName("Analysis1")}); + ASSERT_THAT_EXPECTED(WPAOrErr, llvm::Succeeded()); + + auto R1OrErr = WPAOrErr->get<Analysis1Result>(); + ASSERT_THAT_EXPECTED(R1OrErr, llvm::Succeeded()); + EXPECT_TRUE(R1OrErr->Entries.empty()); + EXPECT_TRUE(R1OrErr->WasInitialized); + EXPECT_TRUE(R1OrErr->WasFinalized); } // run(names) — error when a requested name has no registered analysis. @@ -522,15 +527,20 @@ TEST_F(AnalysisDriverTest, RunByType) { "no result for 'AnalysisName(Analysis2)' in WPASuite")); } -// run<ResultTs...>() — error when a requested type has no data in LUSummary. -TEST_F(AnalysisDriverTest, RunByTypeErrorMissingData) { +// run<ResultTs...>() — a requested type with no data in the LUSummary yields an +// empty (but initialized and finalized) result rather than an error. +TEST_F(AnalysisDriverTest, RunByTypeEmptyWhenMissingData) { auto LU = makeLUSummary(); AnalysisDriver Driver(std::move(LU)); - EXPECT_THAT_EXPECTED( - Driver.run<Analysis1Result>(), - llvm::FailedWithMessage( - "no data for analysis 'AnalysisName(Analysis1)' in LUSummary")); + auto WPAOrErr = Driver.run<Analysis1Result>(); + ASSERT_THAT_EXPECTED(WPAOrErr, llvm::Succeeded()); + + auto R1OrErr = WPAOrErr->get<Analysis1Result>(); + ASSERT_THAT_EXPECTED(R1OrErr, llvm::Succeeded()); + EXPECT_TRUE(R1OrErr->Entries.empty()); + EXPECT_TRUE(R1OrErr->WasInitialized); + EXPECT_TRUE(R1OrErr->WasFinalized); } // contains() — present entries return true; absent entries return false. >From 97c6a8880a7344dbfd7305d28fc0aeb66c716ac6 Mon Sep 17 00:00:00 2001 From: Ziqing Luo <[email protected]> Date: Tue, 14 Jul 2026 16:52:59 -0700 Subject: [PATCH 3/4] USR is not platform-independent. CheckFile matches should be flexible on types --- ...uffer-reachable-excludes-type-constrained-new-delete.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-new-delete.cpp b/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-new-delete.cpp index c5a7a17bbf381..6b3d037952f74 100644 --- a/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-new-delete.cpp +++ b/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-new-delete.cpp @@ -46,13 +46,13 @@ void foo(int *p) { // CHECK: "id_table" -// CHECK-DAG: "id": [[NEW_RET:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "0",[[:space:]]+"usr": }}"c:@F@operator new#l#*v#" -// CHECK-DAG: "id": [[NEW_PLACE:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "2",[[:space:]]+"usr": }}"c:@F@operator new#l#*v#" +// CHECK-DAG: "id": [[NEW_RET:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "0",[[:space:]]+"usr": }}"c:@F@operator new#{{[^#]+}}#*v#" +// CHECK-DAG: "id": [[NEW_PLACE:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "2",[[:space:]]+"usr": }}"c:@F@operator new#{{[^#]+}}#*v#" // CHECK-DAG: "id": [[DEL_PTR:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "1",[[:space:]]+"usr": }}"c:@F@operator delete#*v#" // CHECK-DAG: "id": [[FOO_Q:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}foo#*I#@q" // CHECK-DAG: "id": [[FOO_R:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}foo#*I#@r" -// CHECK-DAG: "id": [[NEW_LOCAL:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}operator new#l#*v#@new_local" +// CHECK-DAG: "id": [[NEW_LOCAL:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}operator new#{{[^#]+}}#*v#@new_local" // CHECK-DAG: "id": [[DELETE_LOCAL:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}operator delete#*v#@delete_local" // The return and every new/delete parameter are reported as type-constrained. >From c216b849a3c044cb543b588678092daa7ff7b47d Mon Sep 17 00:00:00 2001 From: Ziqing Luo <[email protected]> Date: Sat, 18 Jul 2026 22:40:42 -0700 Subject: [PATCH 4/4] address comments --- .../UnsafeBufferUsageAnalysis.cpp | 81 ++++++++++++------- ...achable-excludes-type-constrained-main.cpp | 17 +++- ...e-excludes-type-constrained-new-delete.cpp | 66 ++++++++++----- 3 files changed, 108 insertions(+), 56 deletions(-) diff --git a/clang/lib/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageAnalysis.cpp b/clang/lib/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageAnalysis.cpp index 2d0a58f81a0f0..11a5d61645e36 100644 --- a/clang/lib/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageAnalysis.cpp +++ b/clang/lib/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageAnalysis.cpp @@ -15,6 +15,7 @@ #include "SSAFAnalysesCommon.h" #include "clang/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.h" #include "clang/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevelFormat.h" +#include "clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlow.h" #include "clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowAnalysis.h" #include "clang/ScalableStaticAnalysis/Analyses/TypeConstrainedPointers/TypeConstrainedPointers.h" #include "clang/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsage.h" @@ -22,6 +23,8 @@ #include "clang/ScalableStaticAnalysis/Core/Serialization/JSONFormat.h" #include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/AnalysisRegistry.h" #include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/SummaryAnalysis.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/iterator_range.h" #include "llvm/Support/Error.h" #include "llvm/Support/JSON.h" #include <memory> @@ -135,8 +138,7 @@ JSONFormat::AnalysisResultRegistry::Add<UnsafeBufferReachableAnalysisResult> /// 2. **C2 (Reachable):** If a pointer is reachable from an unsafe pointer in /// the pointer flow graph (provided by `PointerFlowAnalysisResult`), it is /// also unsafe. -/// 3. **C3 (Transformable):** Any pointer associated with type-constrained -/// entities is NOT transformable. +/// 3. **C3 (Constrained):** Type-constrained entities are NOT unsafe. class UnsafeBufferReachableAnalysis : public DerivedAnalysis<UnsafeBufferReachableAnalysisResult, PointerFlowAnalysisResult, @@ -170,11 +172,11 @@ class UnsafeBufferReachableAnalysis /// bound based on the maximum pointer level the pointer type can have. struct BoundsPropagationGraph { private: - const std::map<EntityPointerLevel, EntityPointerLevelSet> &PointerFlows; + EdgeSet PointerFlows; public: - BoundsPropagationGraph(const EdgeSet &PointerFlows) - : PointerFlows(PointerFlows) {} + BoundsPropagationGraph(EdgeSet PointerFlows) + : PointerFlows(std::move(PointerFlows)) {} /// Returns the EntityPointerLevelSet that are reachable from \p Src by /// one edge in the BoundsPropagationGraph. @@ -197,7 +199,6 @@ class UnsafeBufferReachableAnalysis }; std::map<EntityId, BoundsPropagationGraph> BPG; - const std::set<EntityId> *TypeConstrainedEntities = nullptr; // Use pointers for efficiency. EPLs are in tree-based containers that only // grow. So pointers to them are stable. @@ -238,24 +239,37 @@ class UnsafeBufferReachableAnalysis } } - // Filter out non-transformable pointers from `getResult().Result`, leaving - // only those that satisfy C3. - void filterTransformablePointers() { - assert(TypeConstrainedEntities && - "The initialize(...) method should initialize " - "TypeConstrainedEntities to non-null"); - auto &Result = getResult().Reachables; - - for (auto &[Key, EPLs] : Result) { - for (auto ConstrainedEntityId : *TypeConstrainedEntities) { - // FIXME: optimization chance here. Since both sets are sorted, the - // next 'equal_range' search can ignore everything before - // `NonTransEPLs.second`. - auto NonTransEPLs = EPLs.equal_range(ConstrainedEntityId); - - EPLs.erase(NonTransEPLs.first, NonTransEPLs.second); - } + /// \return a lazy range over the pointers in \p UnsafePtrs whose entity is + /// NOT type-constrained (per \p TypeConstraints). + auto filterNonConstrainedPointers( + const EntityPointerLevelSet &UnsafePtrs, + const TypeConstrainedPointersAnalysisResult &TypeConstraints) { + return llvm::make_filter_range( + UnsafePtrs, [&TypeConstraints](const EntityPointerLevel &EPL) { + return !TypeConstraints.contains(EPL.getEntity()); + }); + } + + /// \return a copy of \p Edges but with no edge involving a type-constrained + /// entity (per \p TypeConstraints). + EdgeSet filterNonConstrainedEdges( + const EdgeSet &Edges, + const TypeConstrainedPointersAnalysisResult &TypeConstraints) { + EdgeSet Result; + + for (const auto &[Src, Dsts] : Edges) { + if (TypeConstraints.contains(Src.getEntity())) + continue; + + EntityPointerLevelSet FilteredDsts; + + for (const EntityPointerLevel &Dst : Dsts) + if (!TypeConstraints.contains(Dst.getEntity())) + FilteredDsts.insert(Dst); + if (!FilteredDsts.empty()) + Result.emplace(Src, std::move(FilteredDsts)); } + return Result; } public: @@ -263,20 +277,25 @@ class UnsafeBufferReachableAnalysis initialize(const PointerFlowAnalysisResult &PtrFlowGraph, const TypeConstrainedPointersAnalysisResult &TypeConstraints, const UnsafeBufferUsageAnalysisResult &UnsafePtrs) override { + // Remove type-constrained pointers from `PtrFlowGraph` and `UnsafePtrs` so + // that the final result satisfies C3. for (auto &[Id, SubGraph] : PtrFlowGraph.Edges) - BPG.try_emplace(Id, BoundsPropagationGraph(SubGraph)); - TypeConstrainedEntities = &TypeConstraints.Entities; - assert(getResult().Reachables.empty()); - // C1: all pointers in UnsafeBufferUsageAnalysisResult are unsafe - getResult().Reachables.insert(UnsafePtrs.begin(), UnsafePtrs.end()); + BPG.try_emplace(Id, BoundsPropagationGraph(filterNonConstrainedEdges( + SubGraph, TypeConstraints))); + for (auto &[Contributor, EPLs] : UnsafePtrs) { + auto FilteredRange = filterNonConstrainedPointers(EPLs, TypeConstraints); + + getResult().Reachables[Contributor].insert(FilteredRange.begin(), + FilteredRange.end()); + } return llvm::Error::success(); } llvm::Expected<bool> step() override { - // result meets C1 & C2: + // Compute the reachable EPLs from the C1 unsafe pointers over the + // pointer-flow graph; both are already C3-filtered, so the result + // satisfies C1, C2, and C3. computeReachableUnsafePointers(); - // result meets C1 & C2 & C3: - filterTransformablePointers(); // This is not an iterative algorithm so stop iteration by retruning false: return false; } diff --git a/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-main.cpp b/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-main.cpp index f7ab07f517c03..ef6e32d72ef10 100644 --- a/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-main.cpp +++ b/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-main.cpp @@ -12,7 +12,16 @@ // RUN: clang-ssaf-analyzer %t/lu.json -o %t/wpa.json \ // RUN: -a UnsafeBufferReachableAnalysisResult -// RUN: FileCheck %s --input-file=%t/wpa.json +// The CHECK lines below use readable tokens instead of inline FileCheck regex. +// Expand the tokens into regex, then run FileCheck on the expanded copy: +// $NS - skip the "namespace" array, up to its closing ']'. +// $WS - whitespace, possibly spanning newlines. +// $PTR_L1 - a reachable-set entry closing as "}, 1 ]", i.e. pointer level 1. +// RUN: sed -e 's|$NS|{{([^]]\|[[:space:]])+\\],}}|g' \ +// RUN: -e 's|$WS|{{[[:space:]]+}}|g' \ +// RUN: -e 's|$PTR_L1|{{[[:space:]]+\\},[[:space:]]+1[[:space:]]+\\]}}|g' \ +// RUN: %s > %t/checks.txt +// RUN: FileCheck %t/checks.txt --input-file=%t/wpa.json int main(int argc, char **argv) { @@ -25,8 +34,8 @@ void foo(int *q) { q[5] = 0; } -// CHECK-DAG: "id": [[Q_ID:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "1",[[:space:]]+"usr": }}"c:@F@foo#*I#" -// CHECK-DAG: "id": [[ARGV_ID:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "2",[[:space:]]+"usr": }}"c:@F@main{{.*}}" +// CHECK-DAG: "id": [[Q_ID:[0-9]+]],$NS$WS"suffix": "1",$WS"usr": "c:@F@foo#*I#" +// CHECK-DAG: "id": [[ARGV_ID:[0-9]+]],$NS$WS"suffix": "2",$WS"usr": "c:@F@main{{.*}}" // 'argv' is reported as type-constrained. // CHECK: "analysis_name": "TypeConstrainedPointersAnalysisResult" @@ -34,7 +43,7 @@ void foo(int *q) { // In the reachable result 'q' is present but 'argv' is not. // CHECK: "analysis_name": "UnsafeBufferReachableAnalysisResult" -// CHECK-DAG: {{\{[[:space:]]+}}"@": [[Q_ID]]{{[[:space:]]+\},[[:space:]]+1[[:space:]]+\]}} +// CHECK-DAG: "@": [[Q_ID]]$PTR_L1 // CHECK-NOT: "@": [[ARGV_ID]] // CHECK: "analysis_name" diff --git a/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-new-delete.cpp b/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-new-delete.cpp index 6b3d037952f74..41cae10924f3d 100644 --- a/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-new-delete.cpp +++ b/clang/test/Analysis/Scalable/TypeConstrainedPointers/unsafe-buffer-reachable-excludes-type-constrained-new-delete.cpp @@ -14,7 +14,16 @@ // RUN: clang-ssaf-analyzer %t/lu.json -o %t/wpa.json \ // RUN: -a UnsafeBufferReachableAnalysisResult -// RUN: FileCheck %s --input-file=%t/wpa.json +// The CHECK lines below use readable tokens instead of inline FileCheck regex. +// Expand the tokens into regex, then run FileCheck on the expanded copy: +// $NS - skip the "namespace" array, up to its closing ']'. +// $WS - whitespace, possibly spanning newlines. +// $PTR_L1 - a reachable-set entry closing as "}, 1 ]", i.e. pointer level 1. +// RUN: sed -e 's|$NS|{{([^]]\|[[:space:]])+\\],}}|g' \ +// RUN: -e 's|$WS|{{[[:space:]]+}}|g' \ +// RUN: -e 's|$PTR_L1|{{[[:space:]]+\\},[[:space:]]+1[[:space:]]+\\]}}|g' \ +// RUN: %s > %t/checks.txt +// RUN: FileCheck %t/checks.txt --input-file=%t/wpa.json typedef __SIZE_TYPE__ size_t; @@ -36,24 +45,33 @@ void foo(int *p) { void *r = ::operator new(10, p); int *q = (int *)r; - // 'q' is unsafe, it propagates along the path - // 'q -> r -> return_new -> new_local -> place' - // All pointers along the path are rechable but return_new and place - // are type-constrained. - q[5] = 0; + // 'q' is unsafe and the original propagation path is + // 'q -> r -> return_new -> new_local -> place'. However, since 'return_new' + // (also 'place') is type-constrained, it is removed from the graph, resulting + // in no path from 'q' to 'new_local'. + q[5] = 0; ::operator delete(q); } +void delete_caller() { + char * foo; + delete(foo); +} + // CHECK: "id_table" -// CHECK-DAG: "id": [[NEW_RET:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "0",[[:space:]]+"usr": }}"c:@F@operator new#{{[^#]+}}#*v#" -// CHECK-DAG: "id": [[NEW_PLACE:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "2",[[:space:]]+"usr": }}"c:@F@operator new#{{[^#]+}}#*v#" -// CHECK-DAG: "id": [[DEL_PTR:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "1",[[:space:]]+"usr": }}"c:@F@operator delete#*v#" +// CHECK-DAG: "id": [[NEW_RET:[0-9]+]],$NS$WS"suffix": "0",$WS"usr": "c:@F@operator new#{{[^#]+}}#*v#" +// CHECK-DAG: "id": [[NEW_PLACE:[0-9]+]],$NS$WS"suffix": "2",$WS"usr": "c:@F@operator new#{{[^#]+}}#*v#" +// CHECK-DAG: "id": [[DEL_PTR:[0-9]+]],$NS$WS"suffix": "1",$WS"usr": "c:@F@operator delete#*v#" + +// CHECK-DAG: "id": [[FOO_Q:[0-9]+]],$NS$WS"suffix": "",$WS"usr": "{{[^"]+}}foo#*I#@q" +// CHECK-DAG: "id": [[FOO_R:[0-9]+]],$NS$WS"suffix": "",$WS"usr": "{{[^"]+}}foo#*I#@r" +// CHECK-DAG: "id": [[NEW_LOCAL:[0-9]+]],$NS$WS"suffix": "",$WS"usr": "{{[^"]+}}operator new#{{[^#]+}}#*v#@new_local" +// CHECK-DAG: "id": [[DELETE_LOCAL:[0-9]+]],$NS$WS"suffix": "",$WS"usr": "{{[^"]+}}operator delete#*v#@delete_local" -// CHECK-DAG: "id": [[FOO_Q:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}foo#*I#@q" -// CHECK-DAG: "id": [[FOO_R:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}foo#*I#@r" -// CHECK-DAG: "id": [[NEW_LOCAL:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}operator new#{{[^#]+}}#*v#@new_local" -// CHECK-DAG: "id": [[DELETE_LOCAL:[0-9]+]],{{([^]]|[[:space:]])+\],[[:space:]]+"suffix": "",[[:space:]]+"usr": "[^"]+}}operator delete#*v#@delete_local" +// Contributor (owner) functions, i.e. the keys of the reachable-result map: +// CHECK-DAG: "id": [[CONTRIBUTOR_FOO:[0-9]+]],$NS$WS"suffix": "",$WS"usr": "c:@F@foo#*I#" +// CHECK-DAG: "id": [[CONTRIBUTOR_DELETE_FN:[0-9]+]],$NS$WS"suffix": "",$WS"usr": "c:@F@operator delete#*v#" // The return and every new/delete parameter are reported as type-constrained. // CHECK: "analysis_name": "TypeConstrainedPointersAnalysisResult" @@ -62,13 +80,19 @@ void foo(int *p) { // CHECK-DAG: "@": [[DEL_PTR]] // CHECK: "analysis_name": "UnsafeBufferReachableAnalysisResult" -// CHECK-DAG: {{\{[[:space:]]+}}"@": [[FOO_Q]]{{[[:space:]]+\},[[:space:]]+1[[:space:]]+\]}} -// CHECK-DAG: {{\{[[:space:]]+}}"@": [[FOO_R]]{{[[:space:]]+\},[[:space:]]+1[[:space:]]+\]}} -// CHECK-DAG: {{\{[[:space:]]+}}"@": [[NEW_LOCAL]]{{[[:space:]]+\},[[:space:]]+1[[:space:]]+\]}} -// CHECK-DAG: {{\{[[:space:]]+}}"@": [[DELETE_LOCAL]]{{[[:space:]]+\},[[:space:]]+1[[:space:]]+\]}} - -// CHECK-NOT: {{"@": }}[[NEW_RET]]{{[[:space:]]}} -// CHECK-NOT: {{"@": }}[[NEW_PLACE]]{{[[:space:]]}} -// CHECK-NOT: {{"@": }}[[DEL_PTR]]{{[[:space:]]}} + +// 'foo' reaches its own unsafe pointers 'q' and 'r'. +// CHECK: "@": [[CONTRIBUTOR_FOO]]$WS},$WS[ +// CHECK-DAG: "@": [[FOO_Q]]$PTR_L1 +// CHECK-DAG: "@": [[FOO_R]]$PTR_L1 + +// 'operator delete' reaches its own unsafe pointer 'delete_local'. +// CHECK: "@": [[CONTRIBUTOR_DELETE_FN]]$WS},$WS[ +// CHECK-DAG: "@": [[DELETE_LOCAL]]$PTR_L1 + +// CHECK-NOT: "@": [[NEW_RET]]$WS +// CHECK-NOT: "@": [[NEW_PLACE]]$WS +// CHECK-NOT: "@": [[DEL_PTR]]$WS +// CHECK-NOT: "@": [[NEW_LOCAL]]$WS // CHECK: "analysis_name" _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
