Skip to content

Conversation

NagyDonat
Copy link
Contributor

The method ExprEngine::evalCall handles multiple state transitions and activates various checker callbacks that take a CallEvent parameter (among other parameters). Unfortunately some of these callbacks (EvalCall and pointer escape) were called with a CallEvent instance whose attached state was obsolete. This commit fixes this inconsistency by attaching the right state to the CallEvents before their state becomes relevant.

I found these inconsistencies as I was trying to understand this part of the source code, so I don't know about any concrete bugs that are caused by them -- but they are definitely fishy.

I think it would be nice to handle the "has right state" / "has undefined state" distinction statically within the type system (to prevent the reoccurrence of similar inconsistencies), but I opted for just fixing the current issues as a first step.

The method `ExprEngine::evalCall` handles multiple state transitions and
activates various checker callbacks that take a `CallEvent` parameter
(among other parameters). Unfortunately some of these callbacks
(EvalCall and pointer escape) were called with a `CallEvent` instance
whose attached state was obsolete. This commit fixes this inconsistency
by attaching the right state to the `CallEvent`s before their state
becomes relevant.

I found these inconsistencies as I was trying to understand this part of
the source code, so I don't know about any concrete bugs that are caused
by them -- but they are definitely fishy.

I think it would be nice to handle the "has right state" / "has
undefined state" distinction statically within the type system (to
prevent the reoccurrence of similar inconsistencies), but I opted for
just fixing the current issues as a first step.
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:static analyzer labels Sep 25, 2025
@llvmbot
Copy link
Member

llvmbot commented Sep 25, 2025

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

Author: Donát Nagy (NagyDonat)

Changes

The method ExprEngine::evalCall handles multiple state transitions and activates various checker callbacks that take a CallEvent parameter (among other parameters). Unfortunately some of these callbacks (EvalCall and pointer escape) were called with a CallEvent instance whose attached state was obsolete. This commit fixes this inconsistency by attaching the right state to the CallEvents before their state becomes relevant.

I found these inconsistencies as I was trying to understand this part of the source code, so I don't know about any concrete bugs that are caused by them -- but they are definitely fishy.

I think it would be nice to handle the "has right state" / "has undefined state" distinction statically within the type system (to prevent the reoccurrence of similar inconsistencies), but I opted for just fixing the current issues as a first step.


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

2 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Core/CheckerManager.cpp (+9-6)
  • (modified) clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp (+39-30)
diff --git a/clang/lib/StaticAnalyzer/Core/CheckerManager.cpp b/clang/lib/StaticAnalyzer/Core/CheckerManager.cpp
index 44c6f9f52cca6..6d80518b010dd 100644
--- a/clang/lib/StaticAnalyzer/Core/CheckerManager.cpp
+++ b/clang/lib/StaticAnalyzer/Core/CheckerManager.cpp
@@ -731,33 +731,36 @@ void CheckerManager::runCheckersForEvalCall(ExplodedNodeSet &Dst,
     ExplodedNodeSet checkDst;
     NodeBuilder B(Pred, checkDst, Eng.getBuilderContext());
 
+    ProgramStateRef State = Pred->getState();
+    CallEventRef<> UpdatedCall = Call.cloneWithState(State);
+
     // Check if any of the EvalCall callbacks can evaluate the call.
     for (const auto &EvalCallChecker : EvalCallCheckers) {
       // TODO: Support the situation when the call doesn't correspond
       // to any Expr.
       ProgramPoint L = ProgramPoint::getProgramPoint(
-          Call.getOriginExpr(), ProgramPoint::PostStmtKind,
+          UpdatedCall->getOriginExpr(), ProgramPoint::PostStmtKind,
           Pred->getLocationContext(), EvalCallChecker.Checker);
       bool evaluated = false;
       { // CheckerContext generates transitions(populates checkDest) on
         // destruction, so introduce the scope to make sure it gets properly
         // populated.
         CheckerContext C(B, Eng, Pred, L);
-        evaluated = EvalCallChecker(Call, C);
+        evaluated = EvalCallChecker(*UpdatedCall, C);
       }
 #ifndef NDEBUG
       if (evaluated && evaluatorChecker) {
-        const auto toString = [](const CallEvent &Call) -> std::string {
+        const auto toString = [](CallEventRef<> Call) -> std::string {
           std::string Buf;
           llvm::raw_string_ostream OS(Buf);
-          Call.dump(OS);
+          Call->dump(OS);
           return Buf;
         };
         std::string AssertionMessage = llvm::formatv(
             "The '{0}' call has been already evaluated by the {1} checker, "
             "while the {2} checker also tried to evaluate the same call. At "
             "most one checker supposed to evaluate a call.",
-            toString(Call), evaluatorChecker,
+            toString(UpdatedCall), evaluatorChecker,
             EvalCallChecker.Checker->getDebugTag());
         llvm_unreachable(AssertionMessage.c_str());
       }
@@ -774,7 +777,7 @@ void CheckerManager::runCheckersForEvalCall(ExplodedNodeSet &Dst,
     // If none of the checkers evaluated the call, ask ExprEngine to handle it.
     if (!evaluatorChecker) {
       NodeBuilder B(Pred, Dst, Eng.getBuilderContext());
-      Eng.defaultEvalCall(B, Pred, Call, CallOpts);
+      Eng.defaultEvalCall(B, Pred, *UpdatedCall, CallOpts);
     }
   }
 }
diff --git a/clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp b/clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp
index 0c491b8c4ca90..9360b423f1c08 100644
--- a/clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp
+++ b/clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp
@@ -628,6 +628,8 @@ void ExprEngine::VisitCallExpr(const CallExpr *CE, ExplodedNode *Pred,
 
 ProgramStateRef ExprEngine::finishArgumentConstruction(ProgramStateRef State,
                                                        const CallEvent &Call) {
+  // WARNING: The state attached to 'Call' may be obsolete, do not call any
+  // methods that rely on it!
   const Expr *E = Call.getOriginExpr();
   // FIXME: Constructors to placement arguments of operator new
   // are not supported yet.
@@ -653,6 +655,8 @@ ProgramStateRef ExprEngine::finishArgumentConstruction(ProgramStateRef State,
 void ExprEngine::finishArgumentConstruction(ExplodedNodeSet &Dst,
                                             ExplodedNode *Pred,
                                             const CallEvent &Call) {
+  // WARNING: The state attached to 'Call' may be obsolete, do not call any
+  // methods that rely on it!
   ProgramStateRef State = Pred->getState();
   ProgramStateRef CleanedState = finishArgumentConstruction(State, Call);
   if (CleanedState == State) {
@@ -670,35 +674,40 @@ void ExprEngine::finishArgumentConstruction(ExplodedNodeSet &Dst,
 }
 
 void ExprEngine::evalCall(ExplodedNodeSet &Dst, ExplodedNode *Pred,
-                          const CallEvent &Call) {
-  // WARNING: At this time, the state attached to 'Call' may be older than the
-  // state in 'Pred'. This is a minor optimization since CheckerManager will
-  // use an updated CallEvent instance when calling checkers, but if 'Call' is
-  // ever used directly in this function all callers should be updated to pass
-  // the most recent state. (It is probably not worth doing the work here since
-  // for some callers this will not be necessary.)
+                          const CallEvent &CallTemplate) {
+  // WARNING: As this function performs transitions between several different
+  // states (perhaps in a branching structure) we must be careful to avoid
+  // referencing obsolete or irrelevant states. In particular, 'CallEvent'
+  // instances have an attached state (because this is is convenient within the
+  // checker callbacks) and it is our responsibility to keep these up-to-date.
+  // In fact, the parameter 'CallTemplate' is a "template" because its attached
+  // state may be older than the state of 'Pred' (which will be further
+  // transformed by the transitions within this method).
+  // (Note that 'runCheckersFor*Call' and 'finishArgumentConstruction' are
+  // prepared to take this template and and attach the proper state before
+  // forwarding it to the checkers.)
 
   // Run any pre-call checks using the generic call interface.
   ExplodedNodeSet dstPreVisit;
-  getCheckerManager().runCheckersForPreCall(dstPreVisit, Pred,
-                                            Call, *this);
+  getCheckerManager().runCheckersForPreCall(dstPreVisit, Pred, CallTemplate,
+                                            *this);
 
   // Actually evaluate the function call.  We try each of the checkers
   // to see if the can evaluate the function call, and get a callback at
   // defaultEvalCall if all of them fail.
   ExplodedNodeSet dstCallEvaluated;
-  getCheckerManager().runCheckersForEvalCall(dstCallEvaluated, dstPreVisit,
-                                             Call, *this, EvalCallOptions());
+  getCheckerManager().runCheckersForEvalCall(
+      dstCallEvaluated, dstPreVisit, CallTemplate, *this, EvalCallOptions());
 
   // If there were other constructors called for object-type arguments
   // of this call, clean them up.
   ExplodedNodeSet dstArgumentCleanup;
   for (ExplodedNode *I : dstCallEvaluated)
-    finishArgumentConstruction(dstArgumentCleanup, I, Call);
+    finishArgumentConstruction(dstArgumentCleanup, I, CallTemplate);
 
   ExplodedNodeSet dstPostCall;
   getCheckerManager().runCheckersForPostCall(dstPostCall, dstArgumentCleanup,
-                                             Call, *this);
+                                             CallTemplate, *this);
 
   // Escaping symbols conjured during invalidating the regions above.
   // Note that, for inlined calls the nodes were put back into the worklist,
@@ -708,12 +717,13 @@ void ExprEngine::evalCall(ExplodedNodeSet &Dst, ExplodedNode *Pred,
   // Run pointerEscape callback with the newly conjured symbols.
   SmallVector<std::pair<SVal, SVal>, 8> Escaped;
   for (ExplodedNode *I : dstPostCall) {
-    NodeBuilder B(I, Dst, *currBldrCtx);
     ProgramStateRef State = I->getState();
+    CallEventRef<> Call = CallTemplate.cloneWithState(State);
+    NodeBuilder B(I, Dst, *currBldrCtx);
     Escaped.clear();
     {
       unsigned Arg = -1;
-      for (const ParmVarDecl *PVD : Call.parameters()) {
+      for (const ParmVarDecl *PVD : Call->parameters()) {
         ++Arg;
         QualType ParamTy = PVD->getType();
         if (ParamTy.isNull() ||
@@ -722,13 +732,13 @@ void ExprEngine::evalCall(ExplodedNodeSet &Dst, ExplodedNode *Pred,
         QualType Pointee = ParamTy->getPointeeType();
         if (Pointee.isConstQualified() || Pointee->isVoidType())
           continue;
-        if (const MemRegion *MR = Call.getArgSVal(Arg).getAsRegion())
+        if (const MemRegion *MR = Call->getArgSVal(Arg).getAsRegion())
           Escaped.emplace_back(loc::MemRegionVal(MR), State->getSVal(MR, Pointee));
       }
     }
 
     State = processPointerEscapedOnBind(State, Escaped, I->getLocationContext(),
-                                        PSK_EscapeOutParameters, &Call);
+                                        PSK_EscapeOutParameters, &*Call);
 
     if (State == I->getState())
       Dst.insert(I);
@@ -1212,48 +1222,47 @@ static bool isTrivialObjectAssignment(const CallEvent &Call) {
 }
 
 void ExprEngine::defaultEvalCall(NodeBuilder &Bldr, ExplodedNode *Pred,
-                                 const CallEvent &CallTemplate,
+                                 const CallEvent &Call,
                                  const EvalCallOptions &CallOpts) {
   // Make sure we have the most recent state attached to the call.
   ProgramStateRef State = Pred->getState();
-  CallEventRef<> Call = CallTemplate.cloneWithState(State);
 
   // Special-case trivial assignment operators.
-  if (isTrivialObjectAssignment(*Call)) {
-    performTrivialCopy(Bldr, Pred, *Call);
+  if (isTrivialObjectAssignment(Call)) {
+    performTrivialCopy(Bldr, Pred, Call);
     return;
   }
 
   // Try to inline the call.
   // The origin expression here is just used as a kind of checksum;
   // this should still be safe even for CallEvents that don't come from exprs.
-  const Expr *E = Call->getOriginExpr();
+  const Expr *E = Call.getOriginExpr();
 
   ProgramStateRef InlinedFailedState = getInlineFailedState(State, E);
   if (InlinedFailedState) {
     // If we already tried once and failed, make sure we don't retry later.
     State = InlinedFailedState;
   } else {
-    RuntimeDefinition RD = Call->getRuntimeDefinition();
-    Call->setForeign(RD.isForeign());
+    RuntimeDefinition RD = Call.getRuntimeDefinition();
+    Call.setForeign(RD.isForeign());
     const Decl *D = RD.getDecl();
-    if (shouldInlineCall(*Call, D, Pred, CallOpts)) {
+    if (shouldInlineCall(Call, D, Pred, CallOpts)) {
       if (RD.mayHaveOtherDefinitions()) {
         AnalyzerOptions &Options = getAnalysisManager().options;
 
         // Explore with and without inlining the call.
         if (Options.getIPAMode() == IPAK_DynamicDispatchBifurcate) {
-          BifurcateCall(RD.getDispatchRegion(), *Call, D, Bldr, Pred);
+          BifurcateCall(RD.getDispatchRegion(), Call, D, Bldr, Pred);
           return;
         }
 
         // Don't inline if we're not in any dynamic dispatch mode.
         if (Options.getIPAMode() != IPAK_DynamicDispatch) {
-          conservativeEvalCall(*Call, Bldr, Pred, State);
+          conservativeEvalCall(Call, Bldr, Pred, State);
           return;
         }
       }
-      ctuBifurcate(*Call, D, Bldr, Pred, State);
+      ctuBifurcate(Call, D, Bldr, Pred, State);
       return;
     }
   }
@@ -1261,10 +1270,10 @@ void ExprEngine::defaultEvalCall(NodeBuilder &Bldr, ExplodedNode *Pred,
   // If we can't inline it, clean up the state traits used only if the function
   // is inlined.
   State = removeStateTraitsUsedForArrayEvaluation(
-      State, dyn_cast_or_null<CXXConstructExpr>(E), Call->getLocationContext());
+      State, dyn_cast_or_null<CXXConstructExpr>(E), Call.getLocationContext());
 
   // Also handle the return value and invalidate the regions.
-  conservativeEvalCall(*Call, Bldr, Pred, State);
+  conservativeEvalCall(Call, Bldr, Pred, State);
 }
 
 void ExprEngine::BifurcateCall(const MemRegion *BifurReg,

@llvmbot
Copy link
Member

llvmbot commented Sep 25, 2025

@llvm/pr-subscribers-clang

Author: Donát Nagy (NagyDonat)

Changes

The method ExprEngine::evalCall handles multiple state transitions and activates various checker callbacks that take a CallEvent parameter (among other parameters). Unfortunately some of these callbacks (EvalCall and pointer escape) were called with a CallEvent instance whose attached state was obsolete. This commit fixes this inconsistency by attaching the right state to the CallEvents before their state becomes relevant.

I found these inconsistencies as I was trying to understand this part of the source code, so I don't know about any concrete bugs that are caused by them -- but they are definitely fishy.

I think it would be nice to handle the "has right state" / "has undefined state" distinction statically within the type system (to prevent the reoccurrence of similar inconsistencies), but I opted for just fixing the current issues as a first step.


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

2 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Core/CheckerManager.cpp (+9-6)
  • (modified) clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp (+39-30)
diff --git a/clang/lib/StaticAnalyzer/Core/CheckerManager.cpp b/clang/lib/StaticAnalyzer/Core/CheckerManager.cpp
index 44c6f9f52cca6..6d80518b010dd 100644
--- a/clang/lib/StaticAnalyzer/Core/CheckerManager.cpp
+++ b/clang/lib/StaticAnalyzer/Core/CheckerManager.cpp
@@ -731,33 +731,36 @@ void CheckerManager::runCheckersForEvalCall(ExplodedNodeSet &Dst,
     ExplodedNodeSet checkDst;
     NodeBuilder B(Pred, checkDst, Eng.getBuilderContext());
 
+    ProgramStateRef State = Pred->getState();
+    CallEventRef<> UpdatedCall = Call.cloneWithState(State);
+
     // Check if any of the EvalCall callbacks can evaluate the call.
     for (const auto &EvalCallChecker : EvalCallCheckers) {
       // TODO: Support the situation when the call doesn't correspond
       // to any Expr.
       ProgramPoint L = ProgramPoint::getProgramPoint(
-          Call.getOriginExpr(), ProgramPoint::PostStmtKind,
+          UpdatedCall->getOriginExpr(), ProgramPoint::PostStmtKind,
           Pred->getLocationContext(), EvalCallChecker.Checker);
       bool evaluated = false;
       { // CheckerContext generates transitions(populates checkDest) on
         // destruction, so introduce the scope to make sure it gets properly
         // populated.
         CheckerContext C(B, Eng, Pred, L);
-        evaluated = EvalCallChecker(Call, C);
+        evaluated = EvalCallChecker(*UpdatedCall, C);
       }
 #ifndef NDEBUG
       if (evaluated && evaluatorChecker) {
-        const auto toString = [](const CallEvent &Call) -> std::string {
+        const auto toString = [](CallEventRef<> Call) -> std::string {
           std::string Buf;
           llvm::raw_string_ostream OS(Buf);
-          Call.dump(OS);
+          Call->dump(OS);
           return Buf;
         };
         std::string AssertionMessage = llvm::formatv(
             "The '{0}' call has been already evaluated by the {1} checker, "
             "while the {2} checker also tried to evaluate the same call. At "
             "most one checker supposed to evaluate a call.",
-            toString(Call), evaluatorChecker,
+            toString(UpdatedCall), evaluatorChecker,
             EvalCallChecker.Checker->getDebugTag());
         llvm_unreachable(AssertionMessage.c_str());
       }
@@ -774,7 +777,7 @@ void CheckerManager::runCheckersForEvalCall(ExplodedNodeSet &Dst,
     // If none of the checkers evaluated the call, ask ExprEngine to handle it.
     if (!evaluatorChecker) {
       NodeBuilder B(Pred, Dst, Eng.getBuilderContext());
-      Eng.defaultEvalCall(B, Pred, Call, CallOpts);
+      Eng.defaultEvalCall(B, Pred, *UpdatedCall, CallOpts);
     }
   }
 }
diff --git a/clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp b/clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp
index 0c491b8c4ca90..9360b423f1c08 100644
--- a/clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp
+++ b/clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp
@@ -628,6 +628,8 @@ void ExprEngine::VisitCallExpr(const CallExpr *CE, ExplodedNode *Pred,
 
 ProgramStateRef ExprEngine::finishArgumentConstruction(ProgramStateRef State,
                                                        const CallEvent &Call) {
+  // WARNING: The state attached to 'Call' may be obsolete, do not call any
+  // methods that rely on it!
   const Expr *E = Call.getOriginExpr();
   // FIXME: Constructors to placement arguments of operator new
   // are not supported yet.
@@ -653,6 +655,8 @@ ProgramStateRef ExprEngine::finishArgumentConstruction(ProgramStateRef State,
 void ExprEngine::finishArgumentConstruction(ExplodedNodeSet &Dst,
                                             ExplodedNode *Pred,
                                             const CallEvent &Call) {
+  // WARNING: The state attached to 'Call' may be obsolete, do not call any
+  // methods that rely on it!
   ProgramStateRef State = Pred->getState();
   ProgramStateRef CleanedState = finishArgumentConstruction(State, Call);
   if (CleanedState == State) {
@@ -670,35 +674,40 @@ void ExprEngine::finishArgumentConstruction(ExplodedNodeSet &Dst,
 }
 
 void ExprEngine::evalCall(ExplodedNodeSet &Dst, ExplodedNode *Pred,
-                          const CallEvent &Call) {
-  // WARNING: At this time, the state attached to 'Call' may be older than the
-  // state in 'Pred'. This is a minor optimization since CheckerManager will
-  // use an updated CallEvent instance when calling checkers, but if 'Call' is
-  // ever used directly in this function all callers should be updated to pass
-  // the most recent state. (It is probably not worth doing the work here since
-  // for some callers this will not be necessary.)
+                          const CallEvent &CallTemplate) {
+  // WARNING: As this function performs transitions between several different
+  // states (perhaps in a branching structure) we must be careful to avoid
+  // referencing obsolete or irrelevant states. In particular, 'CallEvent'
+  // instances have an attached state (because this is is convenient within the
+  // checker callbacks) and it is our responsibility to keep these up-to-date.
+  // In fact, the parameter 'CallTemplate' is a "template" because its attached
+  // state may be older than the state of 'Pred' (which will be further
+  // transformed by the transitions within this method).
+  // (Note that 'runCheckersFor*Call' and 'finishArgumentConstruction' are
+  // prepared to take this template and and attach the proper state before
+  // forwarding it to the checkers.)
 
   // Run any pre-call checks using the generic call interface.
   ExplodedNodeSet dstPreVisit;
-  getCheckerManager().runCheckersForPreCall(dstPreVisit, Pred,
-                                            Call, *this);
+  getCheckerManager().runCheckersForPreCall(dstPreVisit, Pred, CallTemplate,
+                                            *this);
 
   // Actually evaluate the function call.  We try each of the checkers
   // to see if the can evaluate the function call, and get a callback at
   // defaultEvalCall if all of them fail.
   ExplodedNodeSet dstCallEvaluated;
-  getCheckerManager().runCheckersForEvalCall(dstCallEvaluated, dstPreVisit,
-                                             Call, *this, EvalCallOptions());
+  getCheckerManager().runCheckersForEvalCall(
+      dstCallEvaluated, dstPreVisit, CallTemplate, *this, EvalCallOptions());
 
   // If there were other constructors called for object-type arguments
   // of this call, clean them up.
   ExplodedNodeSet dstArgumentCleanup;
   for (ExplodedNode *I : dstCallEvaluated)
-    finishArgumentConstruction(dstArgumentCleanup, I, Call);
+    finishArgumentConstruction(dstArgumentCleanup, I, CallTemplate);
 
   ExplodedNodeSet dstPostCall;
   getCheckerManager().runCheckersForPostCall(dstPostCall, dstArgumentCleanup,
-                                             Call, *this);
+                                             CallTemplate, *this);
 
   // Escaping symbols conjured during invalidating the regions above.
   // Note that, for inlined calls the nodes were put back into the worklist,
@@ -708,12 +717,13 @@ void ExprEngine::evalCall(ExplodedNodeSet &Dst, ExplodedNode *Pred,
   // Run pointerEscape callback with the newly conjured symbols.
   SmallVector<std::pair<SVal, SVal>, 8> Escaped;
   for (ExplodedNode *I : dstPostCall) {
-    NodeBuilder B(I, Dst, *currBldrCtx);
     ProgramStateRef State = I->getState();
+    CallEventRef<> Call = CallTemplate.cloneWithState(State);
+    NodeBuilder B(I, Dst, *currBldrCtx);
     Escaped.clear();
     {
       unsigned Arg = -1;
-      for (const ParmVarDecl *PVD : Call.parameters()) {
+      for (const ParmVarDecl *PVD : Call->parameters()) {
         ++Arg;
         QualType ParamTy = PVD->getType();
         if (ParamTy.isNull() ||
@@ -722,13 +732,13 @@ void ExprEngine::evalCall(ExplodedNodeSet &Dst, ExplodedNode *Pred,
         QualType Pointee = ParamTy->getPointeeType();
         if (Pointee.isConstQualified() || Pointee->isVoidType())
           continue;
-        if (const MemRegion *MR = Call.getArgSVal(Arg).getAsRegion())
+        if (const MemRegion *MR = Call->getArgSVal(Arg).getAsRegion())
           Escaped.emplace_back(loc::MemRegionVal(MR), State->getSVal(MR, Pointee));
       }
     }
 
     State = processPointerEscapedOnBind(State, Escaped, I->getLocationContext(),
-                                        PSK_EscapeOutParameters, &Call);
+                                        PSK_EscapeOutParameters, &*Call);
 
     if (State == I->getState())
       Dst.insert(I);
@@ -1212,48 +1222,47 @@ static bool isTrivialObjectAssignment(const CallEvent &Call) {
 }
 
 void ExprEngine::defaultEvalCall(NodeBuilder &Bldr, ExplodedNode *Pred,
-                                 const CallEvent &CallTemplate,
+                                 const CallEvent &Call,
                                  const EvalCallOptions &CallOpts) {
   // Make sure we have the most recent state attached to the call.
   ProgramStateRef State = Pred->getState();
-  CallEventRef<> Call = CallTemplate.cloneWithState(State);
 
   // Special-case trivial assignment operators.
-  if (isTrivialObjectAssignment(*Call)) {
-    performTrivialCopy(Bldr, Pred, *Call);
+  if (isTrivialObjectAssignment(Call)) {
+    performTrivialCopy(Bldr, Pred, Call);
     return;
   }
 
   // Try to inline the call.
   // The origin expression here is just used as a kind of checksum;
   // this should still be safe even for CallEvents that don't come from exprs.
-  const Expr *E = Call->getOriginExpr();
+  const Expr *E = Call.getOriginExpr();
 
   ProgramStateRef InlinedFailedState = getInlineFailedState(State, E);
   if (InlinedFailedState) {
     // If we already tried once and failed, make sure we don't retry later.
     State = InlinedFailedState;
   } else {
-    RuntimeDefinition RD = Call->getRuntimeDefinition();
-    Call->setForeign(RD.isForeign());
+    RuntimeDefinition RD = Call.getRuntimeDefinition();
+    Call.setForeign(RD.isForeign());
     const Decl *D = RD.getDecl();
-    if (shouldInlineCall(*Call, D, Pred, CallOpts)) {
+    if (shouldInlineCall(Call, D, Pred, CallOpts)) {
       if (RD.mayHaveOtherDefinitions()) {
         AnalyzerOptions &Options = getAnalysisManager().options;
 
         // Explore with and without inlining the call.
         if (Options.getIPAMode() == IPAK_DynamicDispatchBifurcate) {
-          BifurcateCall(RD.getDispatchRegion(), *Call, D, Bldr, Pred);
+          BifurcateCall(RD.getDispatchRegion(), Call, D, Bldr, Pred);
           return;
         }
 
         // Don't inline if we're not in any dynamic dispatch mode.
         if (Options.getIPAMode() != IPAK_DynamicDispatch) {
-          conservativeEvalCall(*Call, Bldr, Pred, State);
+          conservativeEvalCall(Call, Bldr, Pred, State);
           return;
         }
       }
-      ctuBifurcate(*Call, D, Bldr, Pred, State);
+      ctuBifurcate(Call, D, Bldr, Pred, State);
       return;
     }
   }
@@ -1261,10 +1270,10 @@ void ExprEngine::defaultEvalCall(NodeBuilder &Bldr, ExplodedNode *Pred,
   // If we can't inline it, clean up the state traits used only if the function
   // is inlined.
   State = removeStateTraitsUsedForArrayEvaluation(
-      State, dyn_cast_or_null<CXXConstructExpr>(E), Call->getLocationContext());
+      State, dyn_cast_or_null<CXXConstructExpr>(E), Call.getLocationContext());
 
   // Also handle the return value and invalidate the regions.
-  conservativeEvalCall(*Call, Bldr, Pred, State);
+  conservativeEvalCall(Call, Bldr, Pred, State);
 }
 
 void ExprEngine::BifurcateCall(const MemRegion *BifurReg,

const EvalCallOptions &CallOpts) {
// Make sure we have the most recent state attached to the call.
ProgramStateRef State = Pred->getState();
CallEventRef<> Call = CallTemplate.cloneWithState(State);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm removing this cloneWithState call because this method is only called by CheckerManager::runCheckersForEvalCall and I had to add a cloneWithState call within that method.

@NagyDonat
Copy link
Contributor Author

NagyDonat commented Sep 25, 2025

As I'm digging around the codebase, I see that it's a common (anti)pattern that some function takes both a CallEvent and a ProgramStateRef (or an exploded node which has its own state) in a way that the State in the CallEvent is probably obsolete and the code tries to not use it. (This is especially relevant in the engine code related to calls, but even a checker can perform several consecutive state updates while using the same unchanged CallEvent instance.)

This seems to be a high caliber footgun which can very easily lead to mistakes where the developer accidentally uses the wrong state (e.g. by a seemingly innocent call to CallEvent::getArgSVal). @steakhal @Xazax-hun @ anybody else What do you think about this issue and potential mitigations?

Copy link
Contributor

@steakhal steakhal left a comment

Choose a reason for hiding this comment

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

I agree that CallEvents have this trap. I've seen it. I don't think it's too widespread. I think we usually catch the use of stale states of CallEvents during review. It also helps that the State is part of our vocabulary and its passed liberally as a parameter, which sets a good example.

This is a critical piece of code.
Many of the changed lines look aesthetic, aka. refactor.
You mention some bug if I recall my sloppy reading, but I could not spot behavioral changes or at least it didn't stand out to me.

If this is NFC, let's adjust the PR title. If has behavioral change, let's have a test exposing that and split off the refactor parts.

Comment on lines 753 to 756
const auto toString = [](CallEventRef<> Call) -> std::string {
std::string Buf;
llvm::raw_string_ostream OS(Buf);
Call.dump(OS);
Call->dump(OS);
Copy link
Contributor

Choose a reason for hiding this comment

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

I figured this could works. Why did you change this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The dump probably doesn't change after the clone.

But generally, in the checker loop, the updated call event indicates that the call was evaluated by multiple checkers. Which should be impossible in practice but it looks like we've decided to act gracefully when assertions are turned off.

That said, we aren't doing a great job at that, given that we're using llvm_unreachable() that translates to pure undefined behavior (aka __builtin_unreachable()) when assertions are turned off. (See also LLVM_UNREACHABLE_OPTIMIZE. These UBs are sometimes incredibly fun to debug.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why did you change this?

It is true that this dump (which IIUC just prints the function name) doesn't rely on the State within the CallEvent object, so it would work with the non-updated instance -- but as we needed to create the UpdatedCall (because we need to pass that to the checkers), I thought that it would be better to consistently use it.

I don't exactly recall why I changed the type of this lambda, I think the compiler complained about my first attempt. I can try to avoid these changes (CallEventRef<> is a somewhat esoteric smart pointer type, but it should be possible to get a const CallEvent & from it).

<offtopic>
By the way I feel an urge to get rid of this lambda, because it is much more verbose than e.g.

std::string FunctionName;
Call.dump(llvm:raw_string_ostream(FunctionName));

(I don't see a reason why we would need to declare a variable for the stream instead of just using a temporary object -- but I'd test this before applying it.) However, I acknowledge that this is subjective bikeshedding, and I won't remove the lambda if you prefer to keep it.
</offtopic>

But generally, in the checker loop, the updated call event indicates that the call was evaluated by multiple checkers. Which should be impossible in practice but it looks like we've decided to act gracefully when assertions are turned off.

What do you mean by this? My understanding of this situation is that:

  • When assertions are turned off (#ifdef NDEBUG block a few lines below the displayed area) we break and leave the loop eagerly when one checker evaluated the call.
  • When assertions are turned on, we always iterate over all the checker callback, and do this formatted error printout if a second checker callback evaluated the call.

That said, we aren't doing a great job at that, given that we're using llvm_unreachable() that translates to pure undefined behavior (aka __builtin_unreachable()) when assertions are turned off.

Good to know in general, but this particular llvm_unreachable() call is within an #ifndef NDEBUG block so I would guess that it is completely skipped when assertions are turned off. (Is this correct? What is the relationship between assertions and NDEBUG?)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh right, it's the other way round. We're doing a great job and everything's fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In commit 5b8416f I'm reverting the NFC tweaks that affect this part of the code.

// WARNING: As this function performs transitions between several different
// states (perhaps in a branching structure) we must be careful to avoid
// referencing obsolete or irrelevant states. In particular, 'CallEvent'
// instances have an attached state (because this is is convenient within the
Copy link
Contributor

Choose a reason for hiding this comment

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

is is

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 1649033

// state may be older than the state of 'Pred' (which will be further
// transformed by the transitions within this method).
// (Note that 'runCheckersFor*Call' and 'finishArgumentConstruction' are
// prepared to take this template and and attach the proper state before
Copy link
Contributor

Choose a reason for hiding this comment

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

and and

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 1649033

@NagyDonat
Copy link
Contributor Author

NagyDonat commented Sep 29, 2025

I agree that CallEvents have this trap. I've seen it. I don't think it's too widespread. I think we usually catch the use of stale states of CallEvents during review.

Noted. It is true that good enough code review can prevent any bugs, but I still think that we should prefer solutions that automatically prevent the errors.

It also helps that the State is part of our vocabulary and its passed liberally as a parameter, which sets a good example.

If the State is passed around liberally (to ensure that we always inject the most recent one), then why do we have a second State within the CallEvent as well? I would prefer an approach where CallEvent doesn't own a state (and perhaps there is a CallEventWithState type if there are use cases where that is needed).

This is a critical piece of code. Many of the changed lines look aesthetic, aka. refactor.

I had to change lots of lines because I needed to switch between use of const CallEvent & (a reference type, the members can be accessed by dot) and CallEventRef<> (a pointer type, members can be accessed by arrow).

You mention some bug if I recall my sloppy reading, but I could not spot behavioral changes or at least it didn't stand out to me.

This commit fixes three bugs that are present in the current code:

(1) The eval::Call checkers are called with a CallEvent object whose associated state was obsolete: it definitely didn't include the state changes from the PreCall checkers and (according to the comment at the beginning of ExprEngine::EvalCall might have been even more obsolete).

(2) The use of Call.getArgSVal(Arg) within the "run PointerEscape for the newly conjured symbols" part (at the end of the ExprEngine::evalCall) relies on the state within the CallEvent (which may have been obsolete even at the beginning of this method call) instead of the variable State (which is up-to date and corresponds to one of the potentially multiple nodes resulting from the pre/eval/post steps).

(3) A bit later (still in the "run PointerEscape for the newly conjured symbols" step) the CallEvent with the ancient state is also passed to processPointerEscapedOnBind which directly forwards it to checkers (without updating the state).

I think it is definitely a bug if a CallEvent with an obsolete state is passed to a checker callback (even if there is no checker where it causes a concrete issue), because the engine/checker boundary should be a clean interface -- checker writers should be able to assume that the parameters passed to the checker are consistent and correct.

If this is NFC, let's adjust the PR title. If has behavioral change, let's have a test exposing that and split off the refactor parts.

I will create a test with a new debug checker which modifies the state in its PreCall callback and validates that the modification is visible through the CallEvent objects received by the EvalCall and PointerEscape callbacks. Is this sufficient, or would you prefer a different kind of test?

I don't think that it is possible to meaningfully simplify this commit by splitting off "refactor parts". There are only a few non-essential changes (comment changes, renamed variables) and without them the "core" change would be more difficult to understand.

@steakhal
Copy link
Contributor

For the record, I got bitten by this today and wasted about 5 minutes of my time.
I had a pretty good instinct that Call.getReturnValue() must refer to the State bundled with Call, and not the one I just prepared with bindExpr binding the return value. (I was inside an evalCall, where all of these are common operations)
So this should underline your suggestion. We should separate out State from CallEvent or have some layer that would ensure that the State attached with a Call is always up to date.

If this is NFC, let's adjust the PR title. If has behavioral change, let's have a test exposing that and split off the refactor parts.

I will create a test with a new debug checker which modifies the state in its PreCall callback and validates that the modification is visible through the CallEvent objects received by the EvalCall and PointerEscape callbacks. Is this sufficient, or would you prefer a different kind of test?

I think the best way to go about this is a unittest with a custom bug report visitor that prints a special trait along the path. Like a logger. And the visitor would print these values alongside the ProgramPoint it is attached to.

@steakhal
Copy link
Contributor

@haoNoQ Do you recall why is a State bundled with a CallEvent? Sometimes that gets outdated as we manipulate the State.
For example, within an eval::Call callback, by definition, we must always bind the return value, while the Call.getReturnValue will always return Unknown (because at Call construction time, that was not yet bound).
Would it make sense to just not bundle a State alongside a CallEvent and be always explicit about the State?
Alternatively, we could consider some strong typing technique to grant static guarantees about the validity of the attached State, but frankly that seems like an overkill.

Copy link
Collaborator

@haoNoQ haoNoQ left a comment

Choose a reason for hiding this comment

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

Not really, this is some ancient history even by my standards 😅 But, yes, this has bummed me out a few times too.

I think the whole point of the CallEvent was to provide user-friendly access to path-sensitive aspects of the call such as CallEvent.getArgSVal() and CallEvent.getReturnValue(). But yeah, the way these checker callbacks accept the state twice, is absolutely weird. I'm pretty sure there even used to be a place where it wasn't true that CheckerContext.getState() == CallEvent.getState(). (IIRC I fixed one but there could be more places like that. I didn't have the guts to add that as an assertion at the invocation sites of these callbacks.)

Now, when it comes to the way the state mutates during the callback (such as making sure getReturnValue() produces the value you've just bound to the call expression), I don't think CallEvent can reasonably handle it, even with a layer of indirection that keeps the state up to date. Like, state splits are a thing. If your evalCall() splits the path in two and binds two different return values, which one do you return?

From this perspective, yeah, it may be slightly useful to keep the original pre-call state around. Say, if you're modeling a function void foo(int *arg) that effectively does something like if (*arg == 0) *arg = 1;, and your checker callback is sufficiently complex, then at some point you may need to ask "Uhh can you please remind me what the original value was?" - and then CallEvent comes in helpful. But I don't think this minor benefit outweighs the confusion caused by calling that very specific program state "the state". I think it's much easier to handle that by deliberately remembering the original state in a local variable, and then pass that state around. It's probably not a popular situation in the first place.

(You're probably aware of a significantly more annoying version of the problem: the problem of reading the original value of *arg from inside checkPostCall. This comes up in the taint checker a lot. But that's much more expensive to provide uniformly.)

So ultimately you'll need to be careful either way. But there's definitely a few ways we could eliminate the confusion.

First, yeah, like you said, we could have a static or dynamical typestate that indicates whether getReturnValue() makes sense. (It only ever makes sense in checkPostCall.)

Then, it might be a good idea to simply make CallEvent.getState() private. It wasn't our goal to provide convenient access to the entire state. We just wanted a nice getArgSVal() and such. Maybe let's provide only the actually-useful, non-confusing methods? Or we could rename the method to something more descriptive, like getStateBeforeCall() and getStateAfterCall() - and make those available/unavailable depending on the typestate of CallEvent. If we're so worried about the *arg situation we may also provide a getSValBeforeCall(const MemRegion *).

Alternatively, it may be a good idea to implement these methods in CheckerContext itself, and then stop showing CallEvent to the user. So instead of CallEvent.getArgSVal() you'd need to type CheckerContext.getCallArgSVal(). Unfortunately this disconnects the user from the entire polymorphism of the CallEvent class. There's actually a lot of methods that would need to be reimplemented (like AnyCXXConstructorCall.getCXXThisVal() or `CXXAllocatorCall.getArraySizeVal()) and most of them wouldn't make sense most of the time.

Finally, if you're particularly interested in getReturnValue() updating in sync, it may be possible to do that if you somehow indicate your intent to never split the state in your callback. And that's valuable on its own because of all the accidental state splits that you can introduce by forgetting to chain your addTransition()s or generateNonFatalErrorNode()s. (The latter is particularly brutal because it really doesn't sound like it has anything to do with state splitting. Like, I want to emit two non-fatal errors, it's only natural that I invoke the generation of two non-fatal error nodes. With fatal errors it's actually a perfectly valid thing to do. But if later you decide that the errors aren't all that fatal, all of a sudden your entire checker breaks down. And good luck noticing that during testing.) So if there was a way to indicate the callback's intention explicitly, eg. CheckerContext.setOutgoingNodeLimit(1) (maybe even set it by default to 1; most checkers need either that or 2) then we'd avoid all the subtle bugs of that nature. Then, naturally, we'd also be able to have a CheckerContext.setReturnValue() method that's available only in evalCall() and only when the outgoing node limit is 1. And then you could look up that value as much as you want!

(Though, of course, you'd still never want to set the return value more than once.)

Comment on lines 753 to 756
const auto toString = [](CallEventRef<> Call) -> std::string {
std::string Buf;
llvm::raw_string_ostream OS(Buf);
Call.dump(OS);
Call->dump(OS);
Copy link
Collaborator

Choose a reason for hiding this comment

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

The dump probably doesn't change after the clone.

But generally, in the checker loop, the updated call event indicates that the call was evaluated by multiple checkers. Which should be impossible in practice but it looks like we've decided to act gracefully when assertions are turned off.

That said, we aren't doing a great job at that, given that we're using llvm_unreachable() that translates to pure undefined behavior (aka __builtin_unreachable()) when assertions are turned off. (See also LLVM_UNREACHABLE_OPTIMIZE. These UBs are sometimes incredibly fun to debug.)

@haoNoQ
Copy link
Collaborator

haoNoQ commented Sep 29, 2025

If this is NFC, let's adjust the PR title. If has behavioral change, let's have a test exposing that and split off the refactor parts.

I will create a test with a new debug checker which modifies the state in its PreCall callback and validates that the modification is visible through the CallEvent objects received by the EvalCall and PointerEscape callbacks. Is this sufficient, or would you prefer a different kind of test?

I think the best way to go about this is a unittest with a custom bug report visitor that prints a special trait along the path. Like a logger. And the visitor would print these values alongside the ProgramPoint it is attached to.

I think the assertion I propose is the easiest way to test this patch. I.e., the fact that the newly added assertion used to crash without the rest of the patch but doesn't crash anymore, is a sufficient defense against regressions in and of itself. And that's very TDD: you first add a new contract to the system, then you change the system to fulfill the contract. Like, even if currently the patch doesn't change the observed behavior. But if it does, that'd also be an easy way to identify some real-world code that misbehaves this way, and then you can easily creduce it into a LIT test if we don't have one already.

(But this could get tedious if more sources of broken states are identified this way and you're not in a position to fix them all at once.)

@haoNoQ
Copy link
Collaborator

haoNoQ commented Sep 29, 2025

FWIW getStateBeforeCall() may still cause confusion. Eg., if multiple checkers process checkPreCall(), judging by the name you'd expect the returned state to be the state before the first checker callback invocation, as opposed to the state at the start of your checker callback invocation.

@NagyDonat
Copy link
Contributor Author

NagyDonat commented Oct 1, 2025

Thanks for all the valuable feedback!

We should separate out State from CallEvent or have some layer that would ensure that the State attached with a Call is always up to date.

@steakhal In an ideal world I agree with this, but I fear that both suggestions have difficulties:

  • There are lots of methods that rely on the State within the CallEvent – even seemingly innocent methods like SimpleFunctionCall::getDecl() can depend on the state (when a function is called through a pointer). Before checking this I thought that it would be nice to separate out State from the CallEvent, but based on this I fear that this wouldn't be practical.
  • Ensuring that the Call always refers to the most recent state is practically impossible, because the "most recent state" is only tracked informally: the developer knows which state variable is the most recent, but this is not represented in a machine-readable way. (In some functional languages there are so called "uniqueness types" or the State monad which can express invariants like "always use the most recent state" but C++ doesn't provide a suitable abstraction for this goal.)

Based on this right now I feel that the best approach would be gradually improving the situation by smaller changes.

I will create a test with a new debug checker which modifies the state in its PreCall callback and validates that the modification is visible through the CallEvent objects received by the EvalCall and PointerEscape callbacks. Is this sufficient, or would you prefer a different kind of test?

I think the best way to go about this is a unittest with a custom bug report visitor that prints a special trait along the path. Like a logger. And the visitor would print these values alongside the ProgramPoint it is attached to.

I don't understand how could a bug reporter visitor help in this situation. The bug (which is fixed by this PR) is that checker callbacks receive CallEvent objects whose attached state is old (and differs from the state available through the CheckerContext which is also passed to the same callback). Testing this requires two components:

  • Checker callbacks that validate that the CallEvent and the CheckerContext have (references to) the same state.
  • Checker callbacks that modify the state (at suitable points e.g. the PreCall callback) to ensure that states that may be different are actually different. It wouldn't be too difficult to find "real" checkers that cause suitable state changes, but to make the test simple and self-contained I'm leaning towards a "synthetic" state change from a debug checker (the same one that also does the validation).
    A bug report visitor doesn't help with either of these components, because it cannot observe the state attached to the CallEvent (it is just passed to the checker callback, but not persisted in the ExplodedGraph for later use by e.g. visitors) and it (obviously) cannot update the state.

From this perspective, yeah, it may be slightly useful to keep the original pre-call state around. Say, if you're modeling a function void foo(int *arg) that effectively does something like if (*arg == 0) *arg = 1;, and your checker callback is sufficiently complex, then at some point you may need to ask "Uhh can you please remind me what the original value was?" - and then CallEvent comes in helpful. But I don't think this minor benefit outweighs the confusion caused by calling that very specific program state "the state". I think it's much easier to handle that by deliberately remembering the original state in a local variable, and then pass that state around. It's probably not a popular situation in the first place.

I didn't even think about this usecase of the "archived" State attached to the call event. I agree that this could be useful within checker code, and I also agree that this is probably a rare situation and should be handled by explicitly saving the original state in a local variable. (Note: this "state attached to call becomes obsolete within the checker" situation is distinct from the "checker callback receives a CallEvent with a state that is already obsolete at the beginning of the call" bug which is fixed by this PR.)

First, yeah, like you said, we could have a static or dynamical typestate that indicates whether getReturnValue() makes sense. (It only ever makes sense in checkPostCall.)

I support this idea -- writing a dynamic check would be trivial, and even a static check is not too difficult.

Then, it might be a good idea to simply make CallEvent.getState() private. It wasn't our goal to provide convenient access to the entire state. We just wanted a nice getArgSVal() and such. Maybe let's provide only the actually-useful, non-confusing methods?

Making CallEvent.getState() private would be a very easy "harm prevention" change that would reduce the damage potential of this footgun. A quick search reveals that outside of CallEvent.{cpp,h} this method is only used twice, and in both of those locations it would be very simple to get the state from the readily available CheckerContext instead. (This is very fortunate -- I would have guessed that there is more of this...) In fact I'll soon create a trivial separate PR for this proposal, because it's a trivial change and I don't see any downside.

EDIT: The jump-to-references functionality of my editor was insufficient, there are at least five or six references to getState() in checker code, including some where the CheckerContext is not readily available. Making CallEvent.getState() private protected is probably not too difficult, but not entirely trivial.

Unfortunately this is just "sweeping the problems under the rug" and not a complete solution -- even if we cannot directly access the obsolete old state object, the analyzer may still run into logically wrong behavior if methods like getArgSVal produce obsolete results from the old state. There are probably situations where the old state is not too old and the values that are looked up from it happen to be correct (e.g. a PreCall callback can't rebind the value of an expression in the Environment), but reasoning about the partial correctness of old states is not a safe long-term solution.

Alternatively, it may be a good idea to implement these methods in CheckerContext itself, and then stop showing CallEvent to the user. So instead of CallEvent.getArgSVal() you'd need to type CheckerContext.getCallArgSVal(). Unfortunately this disconnects the user from the entire polymorphism of the CallEvent class. There's actually a lot of methods that would need to be reimplemented (like AnyCXXConstructorCall.getCXXThisVal() or `CXXAllocatorCall.getArraySizeVal()) and most of them wouldn't make sense most of the time.

WDYT about a change that keeps the CallEvent type hierarchy but ensures that CallEvents can only be constructed by the CheckerContext? I can envision a situation where:

  • There are "empty"/"stateless"/"template" CallEvent objects that are passed around in the engine e.g. in ExprEngine::evalCall.
  • The constructor of CheckerContext can (optionally) own a CallEvent (of this empty stateless kind).
  • Checkers can use a new method called CheckerContext::getCallEvent() which combines the stateless call event with the state of the CheckerContext to return a "regular" CallEvent.

I think the assertion I propose is the easiest way to test this patch. I.e., the fact that the newly added assertion used to crash without the rest of the patch but doesn't crash anymore, is a sufficient defense against regressions in and of itself.

@haoNoQ Are you speaking about the assertion to validate that CheckerContext.getState() == CallEvent.getState()? I agree that this assertion enforces an invariant that should be held, but unfortunately I don't see a "natural place" for it.

  • Placing this on the engine side of the engine/checker boundary (just before calling the callback) seems natural, but that's very close to the location where the CheckerContext and the (updated) CallEvent are created -- so we would just write a tautologically passing check that almost looks like foo = bar; assert(foo == bar);.
  • Placing this assertion into every checker callback (that takes a CallEvent) would be too much "pollution" IMO.
  • Finally, we could place this assertion in a debug checker that is designed for this purpose and activated in a test that ensures that its callbacks are triggered (with frequent state changes to reveal the use of obsolete states). This is essentially equivalent to the testing approach that I suggested.

I hope I answered everything where I have a strong opinion -- let me know if I missed something where my opinion would be relevant.

@haoNoQ
Copy link
Collaborator

haoNoQ commented Oct 2, 2025

Unfortunately this is just "sweeping the problems under the rug" and not a complete solution -- even if we cannot directly access the obsolete old state object, the analyzer may still run into logically wrong behavior if methods like getArgSVal() produce obsolete results from the old state.

Yeah my point is that these specific results aren't allowed to become obsolete.

The argument value is looked up from the Environment. A value of an expression in Environment shall not change for as long as the expression remains live. (Dang. It's almost like this should be an assertion.) The call expression, together with its arguments, remains live until after checkPostCall. So even if you take the very first CallEvent that was fed into checkPreCall, the values of the arguments in that CallEvent would remain correct at least until the end of checkPostCall. Like, even if the call is inlined and an indeterminate amount of steps takes place between pre-call and post-call, it'd still be fine. Even if the callee code assigns values directly to the parameter variable, that'd only go into the Store, so the Environment in the new state would still correctly reflect the argument value from before the call, the same one as the old state.

I hope that the same is true for all these actually-somewhat-irreplaceable methods provided by CallEvent. If not, we'll need to think of a different solution. But things like getArgSVal() simply don't get obsolete so easily. They're not allowed to.

so we would just write a tautologically passing check that almost looks like foo = bar; assert(foo == bar);

Hmm yes you're right. I was like, CheckCallContext::runChecker(), but looks like they already do that. So it was only a problem for evalCall because it's so hand-crafted.

So, like, do you know whether this assertion would fail without your patch? If you stuff it into the old code and then analyze a bunch of code, would it crash a lot?

@haoNoQ
Copy link
Collaborator

haoNoQ commented Oct 2, 2025

Maybe it makes sense to only include the Environment with the CallEvent, not the entire State. That should prevent it from rotting. (It may be necessary to include a few other Environment-like traits that are also safe from rotting, such as Objects Under Construction which is basically an Environment for a particularly spicy part of the AST.)

@NagyDonat
Copy link
Contributor Author

NagyDonat commented Oct 2, 2025

Yeah my point is that these specific results aren't allowed to become obsolete.

The argument value is looked up from the Environment. A value of an expression in Environment shall not change for as long as the expression remains live. [...] Even if the callee code assigns values directly to the parameter variable, that'd only go into the Store, so the Environment in the new state would still correctly reflect the argument value from before the call, the same one as the old state.

Thanks for clarifying this! I already suspected that something similar could be true and justify the use of old (but not too old) States, but I wasn't confident enough to rely on this.

I hope that the same is true for all these actually-somewhat-irreplaceable methods provided by CallEvent. If not, we'll need to think of a different solution. But things like getArgSVal() simply don't get obsolete so easily. They're not allowed to.

I examined all code that relies on CallEvent::getState() and I found the following applications:

  • It is used by several code fragments (including CallEvent::getSVal which is used by CallEvent::getArgSVal and other similar functions) to call the method ProgramState::getSVal(const Stmt*, const LocationContext*), which queries the Environment within the state. (Fortunately I found no references to the overloads like ProgramState::getSVal(const MemRegion*, ...) that query the RegionStore.)
    • (Tangentially related remark: it's a bit annoying that ProgramState::getSVal has overloads that get SVals from very different sources -- reading code that uses them would be easier if they had different names.)
  • It is passed to ExprEngine::getObjectUnderConstruction(...) and ExprEngine::computeObjectUnderConstruction(...) in a few cases.
  • There are several calls like getState()->getStateManager().getContext().getSourceManager() or getState()->getStateManager().getOwningEngine() or getState()->getStateManager().getSValBuilder() that access the ProgramStateManager, the ASTContext, the SourceManager, the ExprEngine or the SValBuilder through the state instance attached to the CallEvent. This is awkward and a bit counter-intuitive, but seems to be harmless, as AFAIK these are all (essentially) global singleton objects.
    • This also happens in checkers, sometimes in locations where the CheckerContext is not available, but the CallEvent can be used to access the ASTContext. (If we make CallEvent::getState() protected, we will perhaps need to expose a public CallEvent::getASTContext().)
    • (Tangentially related idea: perhaps we should introduce more straightforward ways to access these utility objects.)
  • A few checkers (CheckObjCDealloc.cpp, StdVariantChecker.cpp, TaggedUnionModeling.h) use the state attached to the CallEvent to do arbitrary operations (e.g. ->assume()), these should be replaced by use of the state from the CheckerContext (which is also available at all of these call sites).
  • The method CallEvent::invalidateRegions(unsigned BlockCount, ProgramStateRef Orig) starts with Result = (Orig ? Orig : getState()) (before updating the variable Result) -- that is, if the state passed as an argument is null (the default value), then defaults to using its own attached state. Unfortunately this method seems to be called in several locations (e.g. in ExprEngine::VisitCXXNewExpr) and can leak the state of the CallEvent.
    • I strongly suspect that here the right decision is to eliminate this "by default, use the state attached to the call event" shortcut and ensure that all call sites explicitly pass a State (which they should have).
  • The method CXXInstanceCall::getDeclForDynamicType() calls getDynamicTypeInfo(getState(), R) where R = getCXXThisVal().getAsRegion(). This seems to be a suspicious call where an obsolete state could perhaps cause problems (although I'm not sure about this -- I just presume that the dynamic type info can change dynamically). If needed, we can modify this method to take the State as an explicit parameter.
    • Similar use of dynamic type info also happens in ObjCMethodCall::getRuntimeDefinition()
  • The method ObjCMethodCall::getExtraInvalidatedValues calls getState()->getLValue(PropIvar, getReceiverSVal()) which looks up a value from the region store IIUC. I'm not familiar with handling of Objective C, so I cannot form an opinion about this method -- my wild guess is that it should explicitly take a State argument.
    • Similar calls also happen in ObjCMethodCall::getReceiverSVal(), ObjCMethodCall::isReceiverSelfOrSuper() and ObjCMethodCall::getRuntimeDefinition().

So, like, do you know whether this assertion would fail without your patch? If you stuff it into the old code and then analyze a bunch of code, would it crash a lot?

I don't have a strong opinion, but my wild guess is that without my patch this assertion would cause a significant amount of crashes (but not extremely many) in the EvalCall and PointerEscape callbacks (if the assert was added so that applies to these callbacks).

Maybe it makes sense to only include the Environment with the CallEvent, not the entire State. That should prevent it from rotting.

I think it is OK to keep the whole state as a private data member as long as it is marked with MAY BE SLIGHTLY OBSOLETE! comments (within the implementation of CallEvent) and the public interface of CallEvent only exposes parts (e.g. the Environment) that are stable enough. We should still pay some attention to keeping the state of the CallEvents reasonably up-to-date on a best effort basis -- just to be safe -- but if we limit the use of the state attached to the call event, then this is no longer a bugprone area where we need to be extra careful. I feel that this pragmatic approach would be more practical than a principled solution that reimplements a "stable" subset of the state and perhaps duplicates some logic to do so.

Objects Under Construction which is basically an Environment for a particularly spicy part of the AST

I really like this description 😂 🌶️

@haoNoQ
Copy link
Collaborator

haoNoQ commented Oct 4, 2025

(Tangentially related remark: it's a bit annoying that ProgramState::getSVal has overloads that get SVals from very different sources -- reading code that uses them would be easier if they had different names.)

Yes I completely agree 😅 The dream of "hey just give me, like, some value" hasn't quite materialized. It's way more complicated than that.

getState()->getStateManager().getContext()

This is indeed a bit silly but legal. They could probably also do a LocationContext->getAnalysisDeclContext()->getASTContext() instead. You can probably give them a direct accessor method for ASTContext.

The method CXXInstanceCall::getDeclForDynamicType() calls getDynamicTypeInfo(getState(), R) where R = getCXXThisVal().getAsRegion(). This seems to be a suspicious call where an obsolete state could perhaps cause problems (although I'm not sure about this -- I just presume that the dynamic type info can change dynamically). If needed, we can modify this method to take the State as an explicit parameter.

Yes, the dynamic type can change over time. It usually acts as a constraint, i.e. "what we know about the runtime type behind the pointer". Our knowledge can improve over time but it can't evolve in contradictory ways. (At least not until the program performs a placement-new or something of that nature.)

So this is one of the payoffs for tracking dynamic types. It helps us resolve virtual calls as part of getRuntimeDefinition(). The thing about getRuntimeDefinition() is that it's a complex piece of imperative code that arguably needs to be invoked exactly once per function call, by the engine, and then the value should probably be simply stored somewhere and communicated through other means. Eg., the chosen LocationContext is a good way to keep track of it.

Given that it'd be invoked exactly once and very carefully, the question of multiple out-of-sync states hopefully doesn't come up. Maybe it should be moved out of CallEvent to somewhere else, have the virtual part of it minimized somehow, so that it only extracted the relevant bits and pieces of information from the sub-class but didn't try to load stuff from the Store? And then the rest of the implementation could use CallEvent through its public interface?

Similar use of dynamic type info also happens in ObjCMethodCall::getRuntimeDefinition()

This one's a bit worse because ObjC self, unlike this, is an actual variable. It can be, and often is, overwritten in the middle of the method. Like for example in a constructor they often reassign self to the return value of the manually-invoked superclass constructor, and then they check self for null to see if the superclass constructor failed. If it failed, they sometimes try to construct a completely different object and put it back into self and then proceed with the subclass constructor normally. So in this case there are two points of failure: the contents of the variable self and the dynamic type information may both mutate.

But the rough idea is probably still the same: it should be a one-off thing that lives its own life somewhere in the engine, outside of our little utility class.

The method CallEvent::invalidateRegions(unsigned BlockCount, ProgramStateRef Orig) starts with Result = (Orig ? Orig : getState())

Same story as getRuntimeDefinition() imho. I think this one needs to be invoked exactly once per function call. It's virtual because it picks up additional regions and/or per-region invalidation modes from subclasses. Maybe that's the only thing CallEvent should be doing, and the rest should be done by someone else.

The slightly annoying part with this one is that it also needs to be invoked by some of the evalCall checkers when they're giving up on modeling the call precisely and need to model it conservatively instead. Maybe they should be given a different API that does exactly that and comes with a guarantee that it's exactly equivalent to conservative evaluation.

The method ObjCMethodCall::getExtraInvalidatedValues calls getState()->getLValue(PropIvar, getReceiverSVal()) which looks up a value from the region store IIUC.

Yeah getReceiverSVal()/getSelfSVal() have the same problem as the other ObjC bullet point: the self value may change over time. The getLValue() method is of course non-rotting. But yeah, this is just part of the invalidateRegions() thing.

@NagyDonat
Copy link
Contributor Author

NagyDonat commented Oct 6, 2025

getState()->getStateManager().getContext()

This is indeed a bit silly but legal. They could probably also do a LocationContext->getAnalysisDeclContext()->getASTContext() instead. You can probably give them a direct accessor method for ASTContext.

I'll do so if I have time.

The method CXXInstanceCall::getDeclForDynamicType() calls getDynamicTypeInfo(getState(), R) where R = getCXXThisVal().getAsRegion(). This seems to be a suspicious call where an obsolete state could perhaps cause problems (although I'm not sure about this -- I just presume that the dynamic type info can change dynamically). If needed, we can modify this method to take the State as an explicit parameter.

Yes, the dynamic type can change over time. It usually acts as a constraint, i.e. "what we know about the runtime type behind the pointer". Our knowledge can improve over time but it can't evolve in contradictory ways. (At least not until the program performs a placement-new or something of that nature.)

So this is one of the payoffs for tracking dynamic types. It helps us resolve virtual calls as part of getRuntimeDefinition(). The thing about getRuntimeDefinition() is that it's a complex piece of imperative code that arguably needs to be invoked exactly once per function call, by the engine, and then the value should probably be simply stored somewhere and communicated through other means. Eg., the chosen LocationContext is a good way to keep track of it.

Thanks for the information!

Given that it'd be invoked exactly once and very carefully, the question of multiple out-of-sync states hopefully doesn't come up. Maybe it should be moved out of CallEvent to somewhere else, have the virtual part of it minimized somehow, so that it only extracted the relevant bits and pieces of information from the sub-class but didn't try to load stuff from the Store? And then the rest of the implementation could use CallEvent through its public interface?

I don't have a strong opinion about this, but your suggestions sound vaguely right.

Similar use of dynamic type info also happens in ObjCMethodCall::getRuntimeDefinition()

This one's a bit worse because ObjC self, unlike this, is an actual variable. It can be, and often is, overwritten in the middle of the method [...]

Eww...

The method CallEvent::invalidateRegions(unsigned BlockCount, ProgramStateRef Orig) starts with Result = (Orig ? Orig : getState())

Same story as getRuntimeDefinition() imho. I think this one needs to be invoked exactly once per function call. It's virtual because it picks up additional regions and/or per-region invalidation modes from subclasses. Maybe that's the only thing CallEvent should be doing, and the rest should be done by someone else.

I agree that it's reasonable to have this as a virtual method of the CallEvent class hieararcy. As it already takes the state as an optional parameter, I'd say that it would be straightforward to enusre that it always takes the state explicitly, which would ensure that it behaves cleanly.

The slightly annoying part with this one is that it also needs to be invoked by some of the evalCall checkers when they're giving up on modeling the call precisely and need to model it conservatively instead. Maybe they should be given a different API that does exactly that and comes with a guarantee that it's exactly equivalent to conservative evaluation.

I agree that it would be probably useful to have a "do exactly the same thing as conservative evaluation" function.


Based on this discussion, I'm planning the following improvements:

  1. Within 1-2 weeks (after doing one mostly unrelated task) I'll finalize this commit:
  • I'll add tests (probably a new debug checker + a simple lit test that invokes it – I think this would be easier to implement than unit tests).
  • I'll fix the duplicated word typos.
  1. After that I will probably create a PR that makes CallEvent::getState() private protected and exposes a CallEvent::getASTContext() utility method (because it's harmless and would be useful for the checker code that currently calls getState()).
  2. Perhaps I'll also create a commit which ensures that CallEvent::invalidateRegions() always takes the state explicitly (instead of defaulting to the state within the event).

I'm not planning to work on the other topics (runtime definition, Objective-C trickery) because they seem to be more complex and I'm less familiar with those areas. I feel that picking the (above mentioned) low hanging fruit footguns will be sufficient to make this are as safe as other areas of the analyzer codebase.

@NagyDonat
Copy link
Contributor Author

NagyDonat commented Oct 7, 2025

I realized that there is an unfortunately incompatibility within my plans:

  • I'll add tests (probably a new debug checker + a simple lit test that invokes it – I think this would be easier to implement than unit tests).
    [...]
  • After that I will probably create a PR that makes CallEvent::getState() private protected and exposes a CallEvent::getASTContext() utility method (because it's harmless and would be useful for the checker code that currently calls getState()).

When CallEvent::getState() becomes protected (which is undoubtedly a step forward) then it will be impossible to have tests which validate that the checker callback can access the same state through the CallEvent and the CheckerContext (because it cannot look at the state of the CallEvent directly). This means that I don't see a good way for testing the effects of this change:

  • I don't want to expose a public CallEvent::getStateForTesting() -- we shouldn't pollute the codebase with this kind of thing.
  • Causing tricky state changes that are (barely) observable through the public CallEvent API is even worse -- that is basically exploiting bugs for testing purposes.
  • I don't think that unit tests offer a simple solution (visibility also limits them) and I'm not willing to write some complex mocks / workarounds that solve it somehow.

Based on this I would like to merge this PR without adding tests:

  • I am confident that this patch fixes logically incorrect internal behavior (some checkers receive CallEvents with obsolete State objects), although I don't know whether this can cause buggy output. (As a rough guess, I would assign 60% chance to the existence of corner cases where this bad logic causes buggy output, but it would be very difficult to find such cases.)
  • Merging this PR wouldn't reduce the test coverage -- the State attached to the CallEvent wasn't tested before this change either.
  • I can evaluate the effects of this change on our usual collection of open source projects to validate that it doesn't break anything.

@steakhal @haoNoQ What do you think about this?

(If you don't accept this PR without tests and can't suggest a simple way for testing it, I'll abandon it, because I already feel that I spent too much time on it.)

@NagyDonat
Copy link
Contributor Author

@steakhal With 1649033 and 5b8416f I think I handled all your requests -- with the exception of adding tests, which turned out to be problematic (see my previous comment).


The change abccb02 is just drive-by cleanup -- I noticed a typo (missing space) in a comment and I felt that it would be nice to fix it. I'm not strongly attached to it -- I can keep the typo if you would prefer minimizing the footprint of this change.

@haoNoQ
Copy link
Collaborator

haoNoQ commented Oct 7, 2025

By making getState() private/protected you effectively prove it at compile time that the code no longer makes the mistake you're describing. I see this as a much better thing to do than making a runtime test. Like, why would we even be here if we didn't believe in the superiority of compile-time bug prevention? 😅

Also we're not even trying to prevent the CallEvent's inner state from going out of sync. We explicitly allow it to go out of sync. It doesn't make sense to test that it stays in sync.

What makes sense to test, what we really care about, is that the output of the remaining public accessors that look things up from the state stays in sync. And that's a contract that has been followed from the start, your patch didn't improve it, so it's not exactly in the spirit of TDD. But these tests are at least useful for documenting the contract, even if it's currently impossible to write code that would violate that contract. They'd be able to catch an unexpected future situation in which something really goes horribly wrong, like if the argument values in the Environment actively mutate during the lifetime of the CallEvent.

Maybe you could also add a few assertions around the call sites of the CallEvent's accessors in the engine. Like, if you're accessing getArgSVal(), confirm that the same access to your current state outside of CallEvent yields the same value. These assertions may be less trivial and more on-point. But, again, they won't be new.

Or you could, like, make a SFINAE static assertion to confirm that T.getState() leads to a substitution failure when T is substituted with CallEvent. This would make buildbots red when somebody accidentally removes the access qualifier, so we no longer have to rely on a good-faith agreement that mindlessly widening access qualifiers is similar to mindlessly deleting tests that your patch has broken. But this doesn't prevent people from adding more accessor methods that are incorrect, and I'm not sure how that'd work. Unless you find a way to limit the information available to CallEvent itself, eg. don't bundle the entire State with it, but only the Environment. But, again, it's not like you can prevent people from adding more data fields in the future. Or maybe you could still do that with a static assertion or SFINAE? Like, confirm that each of the intended fields is available and is of the right type, and the total size of the object accounts for all these fields? This may be a good guarantee that every time somebody wants to add or access more data or change the type of the existing data they'd make buildbots angry and they'll be forced to read your angry warning comment. But, again, that's not something we usually require for every commit. If that's your best option, I'm actually OK with having no new tests in this patch that were failing before the patch. It sounds like you already plan to do more than enough to make your change difficult to regress.

@NagyDonat
Copy link
Contributor Author

NagyDonat commented Oct 8, 2025

By making getState() private/protected you effectively prove it at compile time that the code no longer makes the mistake you're describing. I see this as a much better thing to do than making a runtime test. Like, why would we even be here if we didn't believe in the superiority of compile-time bug prevention? 😅

Also we're not even trying to prevent the CallEvent's inner state from going out of sync.

No, this commit is trying to prevent the CallEvent's inner state from going out of sinc.

I am proposing a hybrid approach:

  1. This commit ensures that the inner state of the CallEvent is up-to-date (for the EvalCall and PointerEscape callbacks that are targeted by this commit).
  2. The planned followup commit will make getState() protected to limit access to the inner state of CallEvent and ensure that the analyzer works correctly even if the inner state of the CallEvent is obsolete.

These two commits are mostly redundant with each other (either of them would fix 90% of the problems alone), but I see minor reasons for applying both of them:

  • If we make getState() protected but don't apply this commit the method CXXInstanceCall::getDeclForDynamicType() and the analogous tricky Objective-C logic – which I don't understand – could theoretically leak the obsolete parts of the state attached to the CallEvent. (I don't know whether it actually leaks problematic parts of the state, but I cannot rule it out.)
    • Also, I would prefer applying this commit from an aesthetic "Why not be accurate?" point of view – if we know the accurate state and can easily attach it to the CallEvent, then I prefer just attaching it instead of reasoning like "actually, the old state is inaccurate but not too inaccurate, so we can keep and use it..."
  • If we apply this commit but don't make getState() protected, then state attached to the CallEvent will be up-to-date at the start of the the EvalCall and PointerEscape callbacks (btw it is also up-to-date at the start of Pre/PostCall even without this commit), but for the sake of elegance and consistency I still don't want to see checkers that get the state from the CallEvent instead of getting the same state from the CheckerContext.

@haoNoQ If you suggest that

We explicitly allow it to go out of sync.

then that would be equivalent to abandoning this PR and and only merging commit 2. (which would make getState() protected).

I can also accept this approach if you would prefer.

@haoNoQ
Copy link
Collaborator

haoNoQ commented Oct 8, 2025

Ohh. Right right right. Misread. My bad.

You probably shouldn't write a lot of code that you're about to delete anyway. So in my opinion it doesn't make sense to make a unit test or a debug checker just for this patch.

Maybe just assert that the states are the same before you pass it to the checker in evalCall? It's very slightly unobvious there with the whole loop thing and that's where the problem was anyway. Or just leave it as-is.

If you won't be able to do the follow-up commit then you can add a clever follow-up test instead.

@haoNoQ
Copy link
Collaborator

haoNoQ commented Oct 8, 2025

Actually maybe just keep the strict bare minimum of information in the call event? Like a SmallVector<SVal, ...> of args, a potential return value as Optional<SVal>, the runtime definition that's been already computed elsewhere as a Decl *, and more subclass-specific stuff in the subclasses.

If you don't want the data to go out of sync, just normalize your database. Our codebase has plenty of room for magical one-of-a-kind solutions but this one is arguably mundane.

The CallEvent object will be a tiny bit slower to build but we also won't be cloning and rebuilding it nearly as often. It can also be made mutable, so eg. when the return value is computed we simply call a setter to put it there, no need to rebuild everything else.

@NagyDonat
Copy link
Contributor Author

You probably shouldn't write a lot of code that you're about to delete anyway. So in my opinion it doesn't make sense to make a unit test or a debug checker just for this patch.

Maybe just assert that the states are the same before you pass it to the checker in evalCall? It's very slightly unobvious there with the whole loop thing and that's where the problem was anyway. Or just leave it as-is.

I think I will leave it as-is because the initialization of UpdatedCall and the CheckerContext are really close and they are both relying on the same Pred state:

    ProgramStateRef State = Pred->getState();
    CallEventRef<> UpdatedCall = Call.cloneWithState(State);

    // Check if any of the EvalCall callbacks can evaluate the call.
    for (const auto &EvalCallChecker : EvalCallCheckers) {
      // TODO: Support the situation when the call doesn't correspond
      // to any Expr.
      ProgramPoint L = ProgramPoint::getProgramPoint(
          UpdatedCall->getOriginExpr(), ProgramPoint::PostStmtKind,
          Pred->getLocationContext(), EvalCallChecker.Checker);
      bool evaluated = false;
      { // CheckerContext generates transitions (populates checkDest) on
        // destruction, so introduce the scope to make sure it gets properly
        // populated.
        CheckerContext C(B, Eng, Pred, L);
        evaluated = EvalCallChecker(*UpdatedCall, C);
      }                                                        

If you won't be able to do the follow-up commit then you can add a clever follow-up test instead.

I'm pretty sure that I will do the follow-up commit -- it is probably simpler than designing a testcase.


Actually maybe just keep the strict bare minimum of information in the call event? Like a SmallVector<SVal, ...> of args, a potential return value as Optional<SVal>, the runtime definition that's been already computed elsewhere as a Decl *, and more subclass-specific stuff in the subclasses.

If you don't want the data to go out of sync, just normalize your database.

I would really like to have CallEvent implemented this way (I strongly agree that this would be more elegant than the status quo), but I fear that reaching this ideal state would be a difficult undertaking -- especially on the weird Objective-C areas that I really don't understand.

I strongly support this "keep the bare minimum in the call event" idea as a long-term development plan, but I cannot promise to implement it in the foreseeable future, so I would still like to merge this PR as a temporary solution. (If call events do end up being minimized, then it will be possible to discard the logic tweaked in this PR along with all the other code that updates the states attached to CallEvents.)

Also note that this development plan is compatible with my other plans (making CallEvent::getState() protected and ensuring that CallEvent::invalidateRegions() always takes the state explicitly) because those are necessary first steps towards implementing a setup where CallEvent doesn't have a full state (not even as a private member).

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.

4 participants