Skip to content

[analyzer] Limit Store by region-store-binding-limit #127602

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

balazs-benics-sonarsource
Copy link
Contributor

In our test pool, the max entry point RT was improved by this change: 1'181 seconds (~19.7 minutes) -> 94 seconds (1.6 minutes)

BTW, the 1.6 minutes is still really bad. But a few orders of magnitude better than it was before.

This was the most servere RT edge-case as you can see from the numbers. There are are more known RT bottlenecks, such as:

We have 3'075'607 entry points in our test set.
About 393'352 entry points ran longer than 1 second when measured.

To give a sense of the distribution, if we ignore the slowest 500 entry points, then the maximum entry point runs for about 14 seconds. These 500 slow entry points are in 332 translation units.

By this patch, out of the slowest 500 entry points, 72 entry points were improved by at least 10x after this change.

We measured no RT regression on the "usual" entry points.

slow-entrypoints-before-and-after-bind-limit
(The dashed lines represent the maximum of their RT)

CPP-6092

In our test pool, the max entry point RT was improved by this change:
1'181 seconds (~19.7 minutes) -> 94 seconds (1.6 minutes)

BTW, the 1.6 minutes is still really bad. But a few orders of magnitude
better than it was before.

This was the most servere RT edge-case as you can see from the numbers.
There are are more known RT bottlenecks, such as:

 - Large environment sizes, and `removeDead`. See more about the failed
   attempt on improving it at:
   https://discourse.llvm.org/t/unsuccessful-attempts-to-fix-a-slow-analysis-case-related-to-removedead-and-environment-size/84650

 - Large chunk of time could be spend inside `assume`, to reach a fixed
   point. This is something we want to look into a bit later if we have
   time.

We have 3'075'607 entry points in our test set.
About 393'352 entry points ran longer than 1 second when measured.

To give a sense of the distribution, if we ignore the slowest 500
entry points, then the maximum entry point runs for about 14 seconds.
These 500 slow entry points are in 332 translation units.

By this patch, out of the slowest 500 entry points, 72 entry points
were improved by at least 10x after this change.

We measured no RT regression on the "usual" entry points.

CPP-6092
Copy link

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot
Copy link
Member

llvmbot commented Feb 18, 2025

@llvm/pr-subscribers-clang

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

Author: Balázs Benics (balazs-benics-sonarsource)

Changes

In our test pool, the max entry point RT was improved by this change: 1'181 seconds (~19.7 minutes) -> 94 seconds (1.6 minutes)

BTW, the 1.6 minutes is still really bad. But a few orders of magnitude better than it was before.

This was the most servere RT edge-case as you can see from the numbers. There are are more known RT bottlenecks, such as:

We have 3'075'607 entry points in our test set.
About 393'352 entry points ran longer than 1 second when measured.

To give a sense of the distribution, if we ignore the slowest 500 entry points, then the maximum entry point runs for about 14 seconds. These 500 slow entry points are in 332 translation units.

By this patch, out of the slowest 500 entry points, 72 entry points were improved by at least 10x after this change.

We measured no RT regression on the "usual" entry points.

slow-entrypoints-before-and-after-bind-limit
(The dashed lines represent the maximum of their RT)

CPP-6092


Patch is 46.51 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/127602.diff

9 Files Affected:

  • (modified) clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.def (+8)
  • (modified) clang/include/clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h (+1-1)
  • (modified) clang/include/clang/StaticAnalyzer/Core/PathSensitive/Store.h (+9-1)
  • (modified) clang/lib/StaticAnalyzer/Core/ProgramState.cpp (+14-4)
  • (modified) clang/lib/StaticAnalyzer/Core/RegionStore.cpp (+149-61)
  • (modified) clang/lib/StaticAnalyzer/Core/Store.cpp (+5-2)
  • (modified) clang/test/Analysis/analyzer-config.c (+1)
  • (modified) clang/test/Analysis/region-store.cpp (+334-2)
  • (modified) clang/unittests/StaticAnalyzer/StoreTest.cpp (+4-3)
diff --git a/clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.def b/clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.def
index a9b8d0753673b..f05c8724d583d 100644
--- a/clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.def
+++ b/clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.def
@@ -483,6 +483,14 @@ ANALYZER_OPTION(
     "behavior, set the option to 0.",
     5)
 
+ANALYZER_OPTION(
+    unsigned, RegionStoreMaxBindingFanOut, "region-store-max-binding-fanout",
+    "This option limits how many sub-bindings a single binding operation can "
+    "scatter into. For example, binding an array would scatter into binding "
+    "each individual element. Setting this to zero means unlimited, but then "
+    "modelling large array initializers may take proportional time to their "
+    "size.", 100)
+
 //===----------------------------------------------------------------------===//
 // String analyzer options.
 //===----------------------------------------------------------------------===//
diff --git a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h
index 20c446e33ef9a..9fd07ce47175c 100644
--- a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h
+++ b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h
@@ -659,13 +659,13 @@ class ExprEngine {
                               SVal Loc, SVal Val,
                               const LocationContext *LCtx);
 
+public:
   /// A simple wrapper when you only need to notify checkers of pointer-escape
   /// of some values.
   ProgramStateRef escapeValues(ProgramStateRef State, ArrayRef<SVal> Vs,
                                PointerEscapeKind K,
                                const CallEvent *Call = nullptr) const;
 
-public:
   // FIXME: 'tag' should be removed, and a LocationContext should be used
   // instead.
   // FIXME: Comment on the meaning of the arguments, when 'St' may not
diff --git a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/Store.h b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/Store.h
index 332855a3c9c45..ebf00d49b6cc8 100644
--- a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/Store.h
+++ b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/Store.h
@@ -50,6 +50,14 @@ class SymbolReaper;
 
 using InvalidatedSymbols = llvm::DenseSet<SymbolRef>;
 
+struct BindResult {
+  StoreRef ResultingStore;
+
+  // If during the bind operation we exhaust the allowed binding budget, we set
+  // this to the beginning of the escaped part of the region.
+  llvm::SmallVector<SVal, 0> FailedToBindValues;
+};
+
 class StoreManager {
 protected:
   SValBuilder &svalBuilder;
@@ -105,7 +113,7 @@ class StoreManager {
   /// \return A StoreRef object that contains the same
   ///   bindings as \c store with the addition of having the value specified
   ///   by \c val bound to the location given for \c loc.
-  virtual StoreRef Bind(Store store, Loc loc, SVal val) = 0;
+  virtual BindResult Bind(Store store, Loc loc, SVal val) = 0;
 
   /// Return a store with the specified value bound to all sub-regions of the
   /// region. The region must not have previous bindings. If you need to
diff --git a/clang/lib/StaticAnalyzer/Core/ProgramState.cpp b/clang/lib/StaticAnalyzer/Core/ProgramState.cpp
index 34ab2388cbd2f..325b44c9cb05c 100644
--- a/clang/lib/StaticAnalyzer/Core/ProgramState.cpp
+++ b/clang/lib/StaticAnalyzer/Core/ProgramState.cpp
@@ -116,13 +116,23 @@ ProgramStateRef ProgramState::bindLoc(Loc LV,
                                       const LocationContext *LCtx,
                                       bool notifyChanges) const {
   ProgramStateManager &Mgr = getStateManager();
-  ProgramStateRef newState = makeWithStore(Mgr.StoreMgr->Bind(getStore(),
-                                                             LV, V));
+  ExprEngine &Eng = Mgr.getOwningEngine();
+  BindResult BindRes = Mgr.StoreMgr->Bind(getStore(), LV, V);
+  ProgramStateRef State = makeWithStore(BindRes.ResultingStore);
   const MemRegion *MR = LV.getAsRegion();
+
+  // We must always notify the checkers for failing binds because otherwise they
+  // may keep stale traits for these symbols.
+  // Eg., Malloc checker may report leaks if we failed to bind that symbol.
+  if (!BindRes.FailedToBindValues.empty()) {
+    State =
+        Eng.escapeValues(State, BindRes.FailedToBindValues, PSK_EscapeOnBind);
+  }
+
   if (MR && notifyChanges)
-    return Mgr.getOwningEngine().processRegionChange(newState, MR, LCtx);
+    return Eng.processRegionChange(State, MR, LCtx);
 
-  return newState;
+  return State;
 }
 
 ProgramStateRef
diff --git a/clang/lib/StaticAnalyzer/Core/RegionStore.cpp b/clang/lib/StaticAnalyzer/Core/RegionStore.cpp
index d01b6ae55f611..ee821f9b19e73 100644
--- a/clang/lib/StaticAnalyzer/Core/RegionStore.cpp
+++ b/clang/lib/StaticAnalyzer/Core/RegionStore.cpp
@@ -164,6 +164,7 @@ namespace {
 class RegionBindingsRef : public llvm::ImmutableMapRef<const MemRegion *,
                                  ClusterBindings> {
   ClusterBindings::Factory *CBFactory;
+  SmallVectorImpl<SVal> *EscapedValuesDuringBind;
 
   // This flag indicates whether the current bindings are within the analysis
   // that has started from main(). It affects how we perform loads from
@@ -176,31 +177,59 @@ class RegionBindingsRef : public llvm::ImmutableMapRef<const MemRegion *,
   // however that would have made the manager needlessly stateful.
   bool IsMainAnalysis;
 
+  unsigned BindingsLeft;
+
 public:
+  unsigned bindingsLeft() const { return BindingsLeft; }
+
+  bool hasExhaustedBindingLimit() const { return BindingsLeft == 0; }
+
+  RegionBindingsRef escapeValue(SVal V) const {
+    assert(EscapedValuesDuringBind);
+    EscapedValuesDuringBind->push_back(V);
+    return *this;
+  }
+  RegionBindingsRef escapeValues(nonloc::CompoundVal::iterator Begin,
+                                 nonloc::CompoundVal::iterator End) const {
+    for (SVal V : llvm::make_range(Begin, End))
+      escapeValue(V);
+    return *this;
+  }
+
   typedef llvm::ImmutableMapRef<const MemRegion *, ClusterBindings>
           ParentTy;
 
   RegionBindingsRef(ClusterBindings::Factory &CBFactory,
+                    SmallVectorImpl<SVal> *EscapedValuesDuringBind,
                     const RegionBindings::TreeTy *T,
-                    RegionBindings::TreeTy::Factory *F,
-                    bool IsMainAnalysis)
-      : llvm::ImmutableMapRef<const MemRegion *, ClusterBindings>(T, F),
-        CBFactory(&CBFactory), IsMainAnalysis(IsMainAnalysis) {}
-
-  RegionBindingsRef(const ParentTy &P,
-                    ClusterBindings::Factory &CBFactory,
-                    bool IsMainAnalysis)
-      : llvm::ImmutableMapRef<const MemRegion *, ClusterBindings>(P),
-        CBFactory(&CBFactory), IsMainAnalysis(IsMainAnalysis) {}
+                    RegionBindings::TreeTy::Factory *F, bool IsMainAnalysis,
+                    unsigned BindingsLeft)
+      : RegionBindingsRef(ParentTy(T, F), CBFactory, EscapedValuesDuringBind,
+                          IsMainAnalysis, BindingsLeft) {}
+
+  RegionBindingsRef(const ParentTy &P, ClusterBindings::Factory &CBFactory,
+                    SmallVectorImpl<SVal> *EscapedValuesDuringBind,
+                    bool IsMainAnalysis, unsigned BindingsLeft)
+      : ParentTy(P), CBFactory(&CBFactory),
+        EscapedValuesDuringBind(EscapedValuesDuringBind),
+        IsMainAnalysis(IsMainAnalysis), BindingsLeft(BindingsLeft) {}
+
+  RegionBindingsRef add(key_type_ref K, data_type_ref D,
+                        unsigned NewBindingsLeft) const {
+    return RegionBindingsRef(static_cast<const ParentTy *>(this)->add(K, D),
+                             *CBFactory, EscapedValuesDuringBind,
+                             IsMainAnalysis, NewBindingsLeft);
+  }
 
   RegionBindingsRef add(key_type_ref K, data_type_ref D) const {
-    return RegionBindingsRef(static_cast<const ParentTy *>(this)->add(K, D),
-                             *CBFactory, IsMainAnalysis);
+    unsigned NewBindingsLeft = BindingsLeft ? BindingsLeft - 1 : BindingsLeft;
+    return add(K, D, NewBindingsLeft);
   }
 
   RegionBindingsRef remove(key_type_ref K) const {
     return RegionBindingsRef(static_cast<const ParentTy *>(this)->remove(K),
-                             *CBFactory, IsMainAnalysis);
+                             *CBFactory, EscapedValuesDuringBind,
+                             IsMainAnalysis, BindingsLeft);
   }
 
   RegionBindingsRef addBinding(BindingKey K, SVal V) const;
@@ -345,14 +374,21 @@ RegionBindingsRef::getDefaultBinding(const MemRegion *R) const {
 }
 
 RegionBindingsRef RegionBindingsRef::addBinding(BindingKey K, SVal V) const {
+  // If we are about to exhaust the binding limit, highjack this bind call for
+  // the default binding.
+  if (BindingsLeft == 1) {
+    escapeValue(V);
+    K = BindingKey::Make(K.getRegion(), BindingKey::Default);
+    V = UnknownVal();
+  }
+
   const MemRegion *Base = K.getBaseRegion();
 
   const ClusterBindings *ExistingCluster = lookup(Base);
   ClusterBindings Cluster =
       (ExistingCluster ? *ExistingCluster : CBFactory->getEmptyMap());
 
-  ClusterBindings NewCluster = CBFactory->add(Cluster, K, V);
-  return add(Base, NewCluster);
+  return add(Base, CBFactory->add(Cluster, K, V));
 }
 
 
@@ -417,7 +453,7 @@ class RegionStoreManager : public StoreManager {
   ///
   /// This is controlled by 'region-store-small-struct-limit' option.
   /// To disable all small-struct-dependent behavior, set the option to "0".
-  unsigned SmallStructLimit;
+  const unsigned SmallStructLimit;
 
   /// The largest number of element an array can have and still be
   /// considered "small".
@@ -427,7 +463,13 @@ class RegionStoreManager : public StoreManager {
   ///
   /// This is controlled by 'region-store-small-struct-limit' option.
   /// To disable all small-struct-dependent behavior, set the option to "0".
-  unsigned SmallArrayLimit;
+  const unsigned SmallArrayLimit;
+
+  /// The number of bindings a single bind operation can scatter into.
+  /// For example, binding the initializer-list of an array would recurse and
+  /// bind all the individual array elements, potentially causing scalability
+  /// issues.
+  const unsigned RegionStoreMaxBindingFanOut;
 
   /// A helper used to populate the work list with the given set of
   /// regions.
@@ -435,15 +477,21 @@ class RegionStoreManager : public StoreManager {
                         ArrayRef<SVal> Values,
                         InvalidatedRegions *TopLevelRegions);
 
+  const AnalyzerOptions &getOptions() {
+    return StateMgr.getOwningEngine().getAnalysisManager().options;
+  }
+
 public:
   RegionStoreManager(ProgramStateManager &mgr)
       : StoreManager(mgr), RBFactory(mgr.getAllocator()),
-        CBFactory(mgr.getAllocator()), SmallStructLimit(0), SmallArrayLimit(0) {
-    ExprEngine &Eng = StateMgr.getOwningEngine();
-    AnalyzerOptions &Options = Eng.getAnalysisManager().options;
-    SmallStructLimit = Options.RegionStoreSmallStructLimit;
-    SmallArrayLimit = Options.RegionStoreSmallArrayLimit;
-  }
+        CBFactory(mgr.getAllocator()),
+        SmallStructLimit(getOptions().RegionStoreSmallStructLimit),
+        SmallArrayLimit(getOptions().RegionStoreSmallArrayLimit),
+        RegionStoreMaxBindingFanOut(
+            getOptions().RegionStoreMaxBindingFanOut == 0
+                ? -1U
+                : getOptions().RegionStoreMaxBindingFanOut +
+                      /*for the default binding*/ 1) {}
 
   /// setImplicitDefaultValue - Set the default binding for the provided
   ///  MemRegion to the value implicitly defined for compound literals when
@@ -465,9 +513,13 @@ class RegionStoreManager : public StoreManager {
     bool IsMainAnalysis = false;
     if (const auto *FD = dyn_cast<FunctionDecl>(InitLoc->getDecl()))
       IsMainAnalysis = FD->isMain() && !Ctx.getLangOpts().CPlusPlus;
-    return StoreRef(RegionBindingsRef(
-        RegionBindingsRef::ParentTy(RBFactory.getEmptyMap(), RBFactory),
-        CBFactory, IsMainAnalysis).asStore(), *this);
+    return StoreRef(
+        RegionBindingsRef(
+            RegionBindingsRef::ParentTy(RBFactory.getEmptyMap(), RBFactory),
+            CBFactory, /*EscapedValuesDuringBind=*/nullptr, IsMainAnalysis,
+            RegionStoreMaxBindingFanOut)
+            .asStore(),
+        *this);
   }
 
   //===-------------------------------------------------------------------===//
@@ -502,9 +554,13 @@ class RegionStoreManager : public StoreManager {
                                 QualType ElemT);
 
 public: // Part of public interface to class.
-
-  StoreRef Bind(Store store, Loc LV, SVal V) override {
-    return StoreRef(bind(getRegionBindings(store), LV, V).asStore(), *this);
+  BindResult Bind(Store store, Loc LV, SVal V) override {
+    llvm::SmallVector<SVal, 0> EscapedValuesDuringBind;
+    return BindResult{
+        StoreRef(bind(getRegionBindings(store, &EscapedValuesDuringBind), LV, V)
+                     .asStore(),
+                 *this),
+        EscapedValuesDuringBind};
   }
 
   RegionBindingsRef bind(RegionBindingsConstRef B, Loc LV, SVal V);
@@ -513,7 +569,7 @@ class RegionStoreManager : public StoreManager {
   // a default value.
   StoreRef BindDefaultInitial(Store store, const MemRegion *R,
                               SVal V) override {
-    RegionBindingsRef B = getRegionBindings(store);
+    RegionBindingsRef B = getRegionBindingsWithUnboundedLimit(store);
     // Use other APIs when you have to wipe the region that was initialized
     // earlier.
     assert(!(B.getDefaultBinding(R) || B.getDirectBinding(R)) &&
@@ -538,7 +594,7 @@ class RegionStoreManager : public StoreManager {
       if (BR->getDecl()->isEmpty())
         return StoreRef(store, *this);
 
-    RegionBindingsRef B = getRegionBindings(store);
+    RegionBindingsRef B = getRegionBindingsWithUnboundedLimit(store);
     SVal V = svalBuilder.makeZeroVal(Ctx.CharTy);
     B = removeSubRegionBindings(B, cast<SubRegion>(R));
     B = B.addBinding(BindingKey::Make(R, BindingKey::Default), V);
@@ -587,14 +643,14 @@ class RegionStoreManager : public StoreManager {
   StoreRef killBinding(Store ST, Loc L) override;
 
   void incrementReferenceCount(Store store) override {
-    getRegionBindings(store).manualRetain();
+    getRegionBindingsWithUnboundedLimit(store).manualRetain();
   }
 
   /// If the StoreManager supports it, decrement the reference count of
   /// the specified Store object.  If the reference count hits 0, the memory
   /// associated with the object is recycled.
   void decrementReferenceCount(Store store) override {
-    getRegionBindings(store).manualRelease();
+    getRegionBindingsWithUnboundedLimit(store).manualRelease();
   }
 
   bool includedInBindings(Store store, const MemRegion *region) const override;
@@ -613,7 +669,7 @@ class RegionStoreManager : public StoreManager {
   ///     else
   ///       return symbolic
   SVal getBinding(Store S, Loc L, QualType T) override {
-    return getBinding(getRegionBindings(S), L, T);
+    return getBinding(getRegionBindingsWithUnboundedLimit(S), L, T);
   }
 
   std::optional<SVal> getUniqueDefaultBinding(RegionBindingsConstRef B,
@@ -622,7 +678,7 @@ class RegionStoreManager : public StoreManager {
   getUniqueDefaultBinding(nonloc::LazyCompoundVal LCV) const;
 
   std::optional<SVal> getDefaultBinding(Store S, const MemRegion *R) override {
-    RegionBindingsRef B = getRegionBindings(S);
+    RegionBindingsRef B = getRegionBindingsWithUnboundedLimit(S);
     // Default bindings are always applied over a base region so look up the
     // base region's default binding, otherwise the lookup will fail when R
     // is at an offset from R->getBaseRegion().
@@ -700,21 +756,23 @@ class RegionStoreManager : public StoreManager {
   // Utility methods.
   //===------------------------------------------------------------------===//
 
-  RegionBindingsRef getRegionBindings(Store store) const {
-    llvm::PointerIntPair<Store, 1, bool> Ptr;
-    Ptr.setFromOpaqueValue(const_cast<void *>(store));
-    return RegionBindingsRef(
-        CBFactory,
-        static_cast<const RegionBindings::TreeTy *>(Ptr.getPointer()),
-        RBFactory.getTreeFactory(),
-        Ptr.getInt());
+  RegionBindingsRef
+  getRegionBindings(Store store,
+                    SmallVectorImpl<SVal> *EscapedValuesDuringBind) const {
+    return getRegionBindingsImpl(store, EscapedValuesDuringBind,
+                                 /*BindingsLeft=*/RegionStoreMaxBindingFanOut);
+  }
+
+  RegionBindingsRef getRegionBindingsWithUnboundedLimit(Store store) const {
+    return getRegionBindingsImpl(store, /*EscapedValuesDuringBind=*/nullptr,
+                                 /*BindingsLeft=*/-1U);
   }
 
   void printJson(raw_ostream &Out, Store S, const char *NL = "\n",
                  unsigned int Space = 0, bool IsDot = false) const override;
 
   void iterBindings(Store store, BindingsHandler& f) override {
-    RegionBindingsRef B = getRegionBindings(store);
+    RegionBindingsRef B = getRegionBindingsWithUnboundedLimit(store);
     for (const auto &[Region, Cluster] : B) {
       for (const auto &[Key, Value] : Cluster) {
         if (!Key.isDirect())
@@ -727,6 +785,19 @@ class RegionStoreManager : public StoreManager {
       }
     }
   }
+
+private:
+  RegionBindingsRef
+  getRegionBindingsImpl(Store store,
+                        SmallVectorImpl<SVal> *EscapedValuesDuringBind,
+                        unsigned BindingsLeft) const {
+    llvm::PointerIntPair<Store, 1, bool> Ptr;
+    Ptr.setFromOpaqueValue(const_cast<void *>(store));
+    return RegionBindingsRef(
+        CBFactory, EscapedValuesDuringBind,
+        static_cast<const RegionBindings::TreeTy *>(Ptr.getPointer()),
+        RBFactory.getTreeFactory(), Ptr.getInt(), BindingsLeft);
+  }
 };
 
 } // end anonymous namespace
@@ -852,7 +923,7 @@ class ClusterAnalysis  {
 bool RegionStoreManager::scanReachableSymbols(Store S, const MemRegion *R,
                                               ScanReachableSymbols &Callbacks) {
   assert(R == R->getBaseRegion() && "Should only be called for base regions");
-  RegionBindingsRef B = getRegionBindings(S);
+  RegionBindingsRef B = getRegionBindingsWithUnboundedLimit(S);
   const ClusterBindings *Cluster = B.lookup(R);
 
   if (!Cluster)
@@ -1038,7 +1109,9 @@ RegionStoreManager::removeSubRegionBindings(RegionBindingsConstRef B,
 
   if (Result.isEmpty())
     return B.remove(ClusterHead);
-  return B.add(ClusterHead, Result.asImmutableMap());
+  // Make this "add" free by using the old "BindingsLeft".
+  return B.add(ClusterHead, Result.asImmutableMap(),
+               /*BindingsLeft=*/B.bindingsLeft());
 }
 
 namespace {
@@ -1375,7 +1448,7 @@ StoreRef RegionStoreManager::invalidateRegions(
     GlobalsFilter = GFK_None;
   }
 
-  RegionBindingsRef B = getRegionBindings(store);
+  RegionBindingsRef B = getRegionBindingsWithUnboundedLimit(store);
   InvalidateRegionsWorker W(*this, StateMgr, B, S, Count, LCtx, IS, ITraits,
                             Invalidated, GlobalsFilter);
 
@@ -2136,8 +2209,9 @@ RegionStoreManager::getBindingForFieldOrElementCommon(RegionBindingsConstRef B,
   const SubRegion *lazyBindingRegion = nullptr;
   std::tie(lazyBindingStore, lazyBindingRegion) = findLazyBinding(B, R, R);
   if (lazyBindingRegion)
-    return getLazyBinding(lazyBindingRegion,
-                          getRegionBindings(lazyBindingStore));
+    return getLazyBinding(
+        lazyBindingRegion,
+        getRegionBindingsWithUnboundedLimit(lazyBindingStore));
 
   // Record whether or not we see a symbolic index.  That can completely
   // be out of scope of our lookup.
@@ -2314,7 +2388,7 @@ RegionStoreManager::getInterestingValues(nonloc::LazyCompoundVal LCV) {
   SValListTy List;
 
   const SubRegion *LazyR = LCV.getRegion();
-  RegionBindingsRef B = getRegionBindings(LCV.getStore());
+  RegionBindingsRef B = getRegionBindingsWithUnboundedLimit(LCV.getStore());
 
   // If this region had /no/ bindings at the time, there are no interesting
   // values to return.
@@ -2377,7 +2451,7 @@ SVal RegionStoreManager::getBindingForArray(RegionBindingsConstRef B,
 
 bool RegionStoreManager::includedInBindings(Store store,
                                             const MemRegion *region) const {
-  RegionBindingsRef B = getRegionBindings(store)...
[truncated]

@llvmbot llvmbot added the clang Clang issues not falling into any other category label Feb 18, 2025
@balazs-benics-sonarsource
Copy link
Contributor Author

@Flandini @necto

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.

Makes sense giving up on large arrays.

@NagyDonat
Copy link
Contributor

NagyDonat commented Feb 18, 2025

(I'm writing a review right now, please wait a bit -- at most a few hours -- before merging.)

@steakhal
Copy link
Contributor

(I'm writing a review right now, please wait a bit -- at most a few hours -- before merging.)

No worries. I wouldnt have merged tgis without your approval too.

Copy link
Contributor

@NagyDonat NagyDonat left a comment

Choose a reason for hiding this comment

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

I'm really happy that you managed to track down and fix these slow corner cases, and overall I like the code changes, but there were a few places where the code was difficult to understand for me (although this may be also related to the fact that I didn't sleep enough last night... 😉).

I added a few inline comments to mark some confusing parts. Moreover, I also felt that it would be possible to implement the exhaustion "event" more elegantly with a different approach:

If I understand correctly, your approach does the following steps:

  1. The constructor of RegionManagerStore (usually) initializes RegionStoreMaxBindingFanOut to getOptions().RegionStoreMaxBindingFanOut + 1 -- I think this off-by-one difference between identically named variables is a serious drawback of your approach.
  2. RegionManagerStore::getRegionBindings() uses this adjusted fan-out value to construct the RegionBindingsRef object and initialize its field BindingsLeft.
  3. BindingsLeft is decremented by the RegionBindingsRef::add method (which adds a cluster).
    • if BindingsLeft is already zero, it's not decremented (so it won't overflow)
      • is this branch actually used?
      • if not, then it should be replaced by an assertion
    • there is an exceptional case where removeSubRegionBindings uses the 3-argument form of the add method to avoid decrementing the BindingsLeft counter.
  4. If BindingsLeft == 1, then RegionBindingsRef::addBinding hijacks the call and adds a default binding instead of the one that's specified by its argument.
  5. If BindingsLeft == 0, then (as far as I see) early return branches with if (B.hasExhaustedBindingLimit()) stop most (all?) code paths that would add bindings or clusters.
  6. On these early return branches escapeValue() or escapeValues() calls ensure that the values that we ignored are marked as escaped.

In this model there are three phases of the binding creation process:

  1. BindingsLeft > 1, bindings are created normally
  2. BindingsLeft == 1, intermediate state:
    • add works, can add clusters and can decrement BindingsLeft to zero
      • if BindingsLeft is decremented to zero here, then I don't think that the "add a default binding" step happens
    • addBindings adds a default binding (which decrements BindingsLeft) and records that the SVal specified in its argument has escaped
  3. BindingsLeft == 0, exhausted state:
    • hasExhaustedBindingLimit() early returns prevent most (all?) addition of bindings
    • but e.g. the three-argument add within removeSubRegionBindings still works
    • and perhaps there are some other code paths...

(I spent hours on trying to understand this, but I'm still not completely confident that I got every corner case right.) I didn't find anything that is outright buggy, but sometimes I felt that the code tries to preserve an invariant (e.g. "default binding is always added just before exhaustion"), but there is another branch where I don't see why is it preserved.

To simplify the picture a bit, I'd suggest the following alternative approach:

  1. The constructor of RegionManagerStore always initializes RegionStoreMaxBindingFanOut to getOptions().RegionStoreMaxBindingFanOut -- to avoid discrepancies and confusion between them.
  2. If this fan-out value is nonzero, then RegionManagerStore::getRegionBindings() uses it directly to initialize RegionBindingsRef::BindingsLeft (i.e. it does not add one).
    • If the fan-out value is zero (which means unlimited), BindingsLeft may be initialized to -1u.
    • Alternatively, perhaps it would be more modern (but slightly more verbose) to say that BindingsLeft is an std::optional<unsigned> and it's initialized to std:nullopt when there is no limit (i.e. when using getRegionBindingsWithUnboundedLimit or when RegionStoreMaxBindingFanOut == 0).
  3. (unchanged) BindingsLeft is decremented by the RegionBindingsRef::add method (which adds a cluster).
    • if BindingsLeft is already zero, it's not decremented (so it won't overflow)
      • is this branch actually used? -- if not, then it should be replaced by an assertion
    • there is an exceptional case where removeSubRegionBindings uses the 3-argument form of the add method to avoid decrementing the BindingsLeft counter.
  4. There is no highjacking logic and BindingsLeft == 1 has no special meaning.
  5. (unchanged) If BindingsLeft == 0, then B.hasExhaustedBindingLimit() is true, which triggers early return on most (all?) code paths that would add bindings or clusters.
  6. escapeValue and escapeValues are replaced by more general functions that
    • still mark the "leftover" values as escaped
    • but also add a default binding (with unknown value) to mark that the RecordBindingsRef stores incomplete information.

WDYT about this alternative architecture?

@NagyDonat
Copy link
Contributor

As I thought a bit more about the reorganization that I suggested, I realized that it can be summarized as we should synchronize adding the default Unknown binding and calling escapeValue -- because they correspond to the two end-points of the same "this value is stored at this memory region" connection which wasn't properly recorded.

Of course there is some asymmetry in that escapeValue must escape each value individually (or perhaps in a loop with escapeValues), but the default binding to Unknown region is (if I understand correctly) a proper stand-in for all the connections from that side. This could be simply handled as "if (there is no default Unknown binding) { create one }" -- but if this happens to cause performance issues, then a boolean didCreateDefaultUnknownBinding can be used to cache the result.

@balazs-benics-sonarsource
Copy link
Contributor Author

Thanks for your long review. I'm sorry if the poor code quality hindered the comprehension.
My goal was to minimize the diff for easier review, but I admit that I should have attached some considerations as to why I implemented it this way, and also how different parts work under the hood.
I'll keep this in mind for next time!

I'm still working through your review, but I wanted to post a quick update because I think the renamings in place now may make refining your stance easier.

In short, I decided to put a strong type in place to track and enforce the bind limit. Now, we should have greater confidence of that nothing misses the checks. However, this comes at the cost of polluting the other APIs, like BindDefaultInitial or BindDefaultZero where it's highly unexpected to hit this binding limit - because all they usually do is basically add one default and maybe an additional direct binding. But now, looking at it, it led to a more consistent API, where it's harder to make mistakes, so I'm all for this change.

Now, I'll get back to the rest of your comments and respond eventually. Thanks!

Copy link
Contributor

@NagyDonat NagyDonat left a comment

Choose a reason for hiding this comment

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

I'm sorry if the poor code quality hindered the comprehension.

I wouldn't say "poor code quality" -- the code was difficult to understand, but mostly due to the inherent complexity of the logic that's being implemented here.

In short, I decided to put a strong type in place to track and enforce the bind limit.

I agree with this direction.

@balazs-benics-sonarsource
Copy link
Contributor Author

As I thought a bit more about the reorganization that I suggested, I realized that it can be summarized as we should synchronize adding the default Unknown binding and calling escapeValue -- because they correspond to the two end-points of the same "this value is stored at this memory region" connection which wasn't properly recorded.

Of course there is some asymmetry in that escapeValue must escape each value individually (or perhaps in a loop with escapeValues), but the default binding to Unknown region is (if I understand correctly) a proper stand-in for all the connections from that side. This could be simply handled as "if (there is no default Unknown binding) { create one }" -- but if this happens to cause performance issues, then a boolean didCreateDefaultUnknownBinding can be used to cache the result.

AFAIK it's not possible (or rather would be ugly) to tie "adding the default unknown binding" to escapeValues, because there may be multiple escapeValues call in the recursive callstack, while popping the frames until we leave the virtual .*Bind.* API.

For example, while binding a compound val like this: {{{a,b}, {c,d}}, {e}, {f,g}}, we may give up at c, which means, while leaving the method handling the bind of {c,d}, will react to hasExhaustedBindingLimit(), and escape only d. Then the parent frame of the recursive descent would conclude that it finished binding {c,d}, and return. Same goes for its parent: {{a,b}, {c,d}}, but then it checks hasExhaustedBindingLimit() and escapes {e} and {f,g}.

If we didn't add the default binding Unknown earlier, at this point it would be difficult to recover the memory region to the specific element from which the escapes happened, aka. the memregion of the access pattern to c - unless we of course store that MemRegion in some side storage, and employ some RAII technique to ensure that it's bound in the end.
I don't think it is worth this complexity.

I think I'm finished, I looked at everything again, and I still believe it's as good as it gets.
I'm happy to be challenged, but I don't think I'd spend too much time on this.
Feel free to directly push to this branch to demonstrate what you would propose.

@NagyDonat
Copy link
Contributor

I tried to write down my rough ideas as a concrete commit, but I don't have enough mental acuity for it today. I'll try to make another attempt on Monday -- but if you wish to close this commit, then it's also OK if I do this refactoring in a separate follow-up commit.

Copy link
Contributor

@NagyDonat NagyDonat left a comment

Choose a reason for hiding this comment

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

Anyway, let's just merge this as it is now.

The code is basically OK, I still don't have the brainpower to hold all the details in my mind (kudos for the fact that you managed to put this together) and if I'll catch some divine inspiration in the future, I can still refactor this as a follow-up commit.

@balazs-benics-sonarsource
Copy link
Contributor Author

Anyway, let's just merge this as it is now.

The code is basically OK, I still don't have the brainpower to hold all the details in my mind (kudos for the fact that you managed to put this together) and if I'll catch some divine inspiration in the future, I can still refactor this as a follow-up commit.

Thank you. I'll merge this later today.

@balazs-benics-sonarsource balazs-benics-sonarsource merged commit 22a5bb3 into llvm:main Feb 24, 2025
8 checks passed
@balazs-benics-sonarsource balazs-benics-sonarsource deleted the bb/upstreaming-cpp-6092-limit-store branch February 24, 2025 14:48
Copy link

@balazs-benics-sonarsource Congratulations on having your first Pull Request (PR) merged into the LLVM Project!

Your changes will be combined with recent changes from other authors, then tested by our build bots. If there is a problem with a build, you may receive a report in an email or a comment on this PR.

Please check whether problems have been caused by your change specifically, as the builds can include changes from many authors. It is not uncommon for your change to be included in a build that fails due to someone else's changes, or infrastructure issues.

How to do this, and the rest of the post-merge process, is covered in detail here.

If your change does cause a problem, it may be reverted, or you can revert it yourself. This is a normal part of LLVM development. You can fix your changes and open a new PR to merge them again.

If you don't get any reports, no action is required from you. Your changes are working as expected, well done!

@mikaelholmen
Copy link
Collaborator

Hello @balazs-benics-sonarsource

The following starts crashing with this patch:
clang --analyze bbi-104578.c
It crashes with

clang: ../../clang/lib/StaticAnalyzer/Core/RegionStore.cpp:375: LimitedRegionBindingsRef LimitedRegionBindingsRef::addBinding(BindingKey, SVal) const: Assertion `NewBindingsLeft.value() != 0' failed.
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace, preprocessed source, and associated run script.
Stack dump:
0.	Program arguments: build-all/bin/clang --analyze bbi-104578.c
1.	<eof> parser at end of file
2.	While analyzing stack: 
	#0 Calling c
3.	bbi-104578.c:5:3: Error evaluating statement
4.	bbi-104578.c:5:3: Error evaluating statement
 #0 0x000055dcb12b54f6 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) (build-all/bin/clang+0x856b4f6)
 #1 0x000055dcb12b2fde llvm::sys::RunSignalHandlers() (build-all/bin/clang+0x8568fde)
 #2 0x000055dcb12b4834 llvm::sys::CleanupOnSignal(unsigned long) (build-all/bin/clang+0x856a834)
 #3 0x000055dcb1215bfd CrashRecoverySignalHandler(int) CrashRecoveryContext.cpp:0:0
 #4 0x00007fa82a361d10 __restore_rt (/lib64/libpthread.so.0+0x12d10)
 #5 0x00007fa827d0152f raise (/lib64/libc.so.6+0x4e52f)
 #6 0x00007fa827cd4e65 abort (/lib64/libc.so.6+0x21e65)
 #7 0x00007fa827cd4d39 _nl_load_domain.cold.0 (/lib64/libc.so.6+0x21d39)
 #8 0x00007fa827cf9e86 (/lib64/libc.so.6+0x46e86)
 #9 0x000055dcb34a2bf5 LimitedRegionBindingsRef::addBinding((anonymous namespace)::BindingKey, clang::ento::SVal) const RegionStore.cpp:0:0
#10 0x000055dcb34a01f6 (anonymous namespace)::RegionStoreManager::bindArray(LimitedRegionBindingsRef const&, clang::ento::TypedValueRegion const*, clang::ento::SVal) RegionStore.cpp:0:0
#11 0x000055dcb34a03e3 (anonymous namespace)::RegionStoreManager::bindArray(LimitedRegionBindingsRef const&, clang::ento::TypedValueRegion const*, clang::ento::SVal) RegionStore.cpp:0:0
#12 0x000055dcb349fb68 (anonymous namespace)::RegionStoreManager::bind(LimitedRegionBindingsRef const&, clang::ento::Loc, clang::ento::SVal) RegionStore.cpp:0:0
#13 0x000055dcb3494678 (anonymous namespace)::RegionStoreManager::Bind(void const*, clang::ento::Loc, clang::ento::SVal) RegionStore.cpp:0:0
#14 0x000055dcb34609eb clang::ento::ProgramState::bindLoc(clang::ento::Loc, clang::ento::SVal, clang::LocationContext const*, bool) const (build-all/bin/clang+0xa7169eb)
#15 0x000055dcb340dea2 clang::ento::ExprEngine::processPointerEscapedOnBind(llvm::IntrusiveRefCntPtr<clang::ento::ProgramState const>, llvm::ArrayRef<std::pair<clang::ento::SVal, clang::ento::SVal>>, clang::LocationContext const*, clang::ento::PointerEscapeKind, clang::ento::CallEvent const*) (build-all/bin/clang+0xa6c3ea2)
#16 0x000055dcb3403650 clang::ento::ExprEngine::evalBind(clang::ento::ExplodedNodeSet&, clang::Stmt const*, clang::ento::ExplodedNode*, clang::ento::SVal, clang::ento::SVal, bool, clang::ProgramPoint const*) (build-all/bin/clang+0xa6b9650)
#17 0x000055dcb341e195 clang::ento::ExprEngine::VisitDeclStmt(clang::DeclStmt const*, clang::ento::ExplodedNode*, clang::ento::ExplodedNodeSet&) (build-all/bin/clang+0xa6d4195)
#18 0x000055dcb340261b clang::ento::ExprEngine::Visit(clang::Stmt const*, clang::ento::ExplodedNode*, clang::ento::ExplodedNodeSet&) (build-all/bin/clang+0xa6b861b)
#19 0x000055dcb33fe863 clang::ento::ExprEngine::ProcessStmt(clang::Stmt const*, clang::ento::ExplodedNode*) (build-all/bin/clang+0xa6b4863)
#20 0x000055dcb33fe555 clang::ento::ExprEngine::processCFGElement(clang::CFGElement, clang::ento::ExplodedNode*, unsigned int, clang::ento::NodeBuilderContext*) (build-all/bin/clang+0xa6b4555)
#21 0x000055dcb33e06b0 clang::ento::CoreEngine::HandlePostStmt(clang::CFGBlock const*, unsigned int, clang::ento::ExplodedNode*) (build-all/bin/clang+0xa6966b0)
#22 0x000055dcb33dfd1b clang::ento::CoreEngine::dispatchWorkItem(clang::ento::ExplodedNode*, clang::ProgramPoint, clang::ento::WorkListUnit const&) (build-all/bin/clang+0xa695d1b)
#23 0x000055dcb33df36d clang::ento::CoreEngine::ExecuteWorkList(clang::LocationContext const*, unsigned int, llvm::IntrusiveRefCntPtr<clang::ento::ProgramState const>) (build-all/bin/clang+0xa69536d)
#24 0x000055dcb313d2c7 (anonymous namespace)::AnalysisConsumer::HandleCode(clang::Decl*, unsigned int, clang::ento::ExprEngine::InliningModes, llvm::DenseSet<clang::Decl const*, llvm::DenseMapInfo<clang::Decl const*, void>>*) AnalysisConsumer.cpp:0:0
#25 0x000055dcb313b70b (anonymous namespace)::AnalysisConsumer::HandleTranslationUnit(clang::ASTContext&) AnalysisConsumer.cpp:0:0
#26 0x000055dcb34f6029 clang::ParseAST(clang::Sema&, bool, bool) (build-all/bin/clang+0xa7ac029)
#27 0x000055dcb20b15e4 clang::FrontendAction::Execute() (build-all/bin/clang+0x93675e4)
#28 0x000055dcb201a1cd clang::CompilerInstance::ExecuteAction(clang::FrontendAction&) (build-all/bin/clang+0x92d01cd)
#29 0x000055dcb21b37c5 clang::ExecuteCompilerInvocation(clang::CompilerInstance*) (build-all/bin/clang+0x94697c5)
#30 0x000055dcae7a9626 cc1_main(llvm::ArrayRef<char const*>, char const*, void*) (build-all/bin/clang+0x5a5f626)
#31 0x000055dcae7a5abd ExecuteCC1Tool(llvm::SmallVectorImpl<char const*>&, llvm::ToolContext const&) driver.cpp:0:0
#32 0x000055dcb1e4f979 void llvm::function_ref<void ()>::callback_fn<clang::driver::CC1Command::Execute(llvm::ArrayRef<std::optional<llvm::StringRef>>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>*, bool*) const::$_0>(long) Job.cpp:0:0
#33 0x000055dcb12158f6 llvm::CrashRecoveryContext::RunSafely(llvm::function_ref<void ()>) (build-all/bin/clang+0x84cb8f6)
#34 0x000055dcb1e4ee83 clang::driver::CC1Command::Execute(llvm::ArrayRef<std::optional<llvm::StringRef>>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>*, bool*) const (build-all/bin/clang+0x9104e83)
#35 0x000055dcb1e07517 clang::driver::Compilation::ExecuteCommand(clang::driver::Command const&, clang::driver::Command const*&, bool) const (build-all/bin/clang+0x90bd517)
#36 0x000055dcb1e07837 clang::driver::Compilation::ExecuteJobs(clang::driver::JobList const&, llvm::SmallVectorImpl<std::pair<int, clang::driver::Command const*>>&, bool) const (build-all/bin/clang+0x90bd837)
#37 0x000055dcb1e280c9 clang::driver::Driver::ExecuteCompilation(clang::driver::Compilation&, llvm::SmallVectorImpl<std::pair<int, clang::driver::Command const*>>&) (build-all/bin/clang+0x90de0c9)
#38 0x000055dcae7a4f16 clang_main(int, char**, llvm::ToolContext const&) (build-all/bin/clang+0x5a5af16)
#39 0x000055dcae7b5d06 main (build-all/bin/clang+0x5a6bd06)
#40 0x00007fa827ced7e5 __libc_start_main (/lib64/libc.so.6+0x3a7e5)
#41 0x000055dcae7a342e _start (build-all/bin/clang+0x5a5942e)
clang: error: clang frontend command failed with exit code 134 (use -v to see invocation)
clang version 21.0.0git
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /repo/uabelho/main-github/llvm/build-all/bin
Build config: +assertions
clang: note: diagnostic msg: 
********************

PLEASE ATTACH THE FOLLOWING FILES TO THE BUG REPORT:
Preprocessed source(s) and associated run script(s) are located at:
clang: note: diagnostic msg: /tmp/bbi-104578-9c6928.c
clang: note: diagnostic msg: /tmp/bbi-104578-9c6928.sh
clang: note: diagnostic msg: 

********************

bbi-104578.c.gz

@balazs-benics-sonarsource
Copy link
Contributor Author

Hello @balazs-benics-sonarsource

The following starts crashing with this patch: clang --analyze bbi-104578.c It crashes with

Thank you for the heads up @mikaelholmen. I'll switch to it ASAP. I'd expect the fix later today.

@balazs-benics-sonarsource
Copy link
Contributor Author

Confirmed crash, https://compiler-explorer.com/z/fzoqP36xq

balazs-benics-sonarsource added a commit to balazs-benics-sonarsource/llvm-project that referenced this pull request Feb 28, 2025
Basically, we may leave the loop because if exhaust the fields, array
elements or other subobjects to initialize.
In that case, the Bindings may be in an exhausted state, thus no further
addBinding calls are allowed.

Let's harden the code by sprinkling some early exists in the recursive
dispatcher functions.
And to actually fix the issue, I added a check guarding the single
unguarded addBinding right after a loop I mentioned.

Fixes llvm#129211
balazs-benics-sonarsource added a commit that referenced this pull request Feb 28, 2025
Basically, we may leave the loop because if exhaust the fields, array
elements or other subobjects to initialize.
In that case, the Bindings may be in an exhausted state, thus no further
addBinding calls are allowed.

Let's harden the code by sprinkling some early exists in the recursive
dispatcher functions.
And to actually fix the issue, I added a check guarding the single
unguarded addBinding right after a loop I mentioned.

Fixes #129211
cheezeburglar pushed a commit to cheezeburglar/llvm-project that referenced this pull request Feb 28, 2025
Basically, we may leave the loop because if exhaust the fields, array
elements or other subobjects to initialize.
In that case, the Bindings may be in an exhausted state, thus no further
addBinding calls are allowed.

Let's harden the code by sprinkling some early exists in the recursive
dispatcher functions.
And to actually fix the issue, I added a check guarding the single
unguarded addBinding right after a loop I mentioned.

Fixes llvm#129211
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:static analyzer clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants