Skip to content

[analyzer] Fix crash in RegionStoreManager::bindArray from constructor array-to-pointer decay - #210649

Merged
steakhal merged 3 commits into
llvm:mainfrom
andyames-a11y:users/andyames/fix-analyzer-ctor-array-decay
Jul 21, 2026
Merged

[analyzer] Fix crash in RegionStoreManager::bindArray from constructor array-to-pointer decay#210649
steakhal merged 3 commits into
llvm:mainfrom
andyames-a11y:users/andyames/fix-analyzer-ctor-array-decay

Conversation

@andyames-a11y

@andyames-a11y andyames-a11y commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

ProcessInitializer() strips implicit casts from a CXXCtorInitializer's init expression via IgnoreImplicit(), then decides whether to treat the initializer as a direct array-to-array member copy by checking Init->getType()->isArrayType(). For a pointer member initialized via array-to-pointer decay of a reference-to-array constructor parameter (e.g. Foo(T (&arr)[N]) : ptr_(arr) {}), IgnoreImplicit() strips the ArrayToPointerDecay cast, exposing the underlying array-typed expression, so this check misfires even though the field itself is a pointer, not an array. That branch fetches the raw region address of the whole array, bypassing the normal decay logic (which produces an ElementRegion), so the pointer member ends up holding the address of the whole array typed as the array itself, instead of an ElementRegion at index 0.

Later, dereferencing and storing through that mistyped pointer routes into RegionStoreManager::bindArray() (instead of bindScalar()), which unconditionally casts its Init value to nonloc::CompoundVal, asserting in a debug build and segfaulting in a release build when Init is anything else, e.g. a nonloc::LocAsInteger produced by round-tripping a pointer through an integer type.

Fix the actual bug by checking the field's type instead of the initializer expression's type. Also generalize bindArray()'s existing guard (added by #178923 for issue #178797) from an enumeration of specific SVal kinds to the same exhaustive
!isa<nonloc::CompoundVal>() check already used by its siblings bindStruct() and bindVector(), so it doesn't need to be extended again every time a new SVal kind reaches this path -- this is what actually catches our case (nonloc::LocAsInteger), which the prior enumeration didn't cover.

This is the same underlying bug behind #147686 (fixed by #153177, which its own author noted was "more of a workaround") and #178797 (fixed by #178923); both those fixes patched symptoms at bindArray() without addressing the ProcessInitializer() root cause. Fixing the root cause also resolves two FIXME-annotated precision gaps in clang/test/Analysis/initializer.cpp's gh147686 regression test.

Fixes #210183

AI tool use disclosure: Claude Code (Anthropic) assisted in reducing the original crash to a minimal, dependency-free reproducer (via creduce plus manual bisection, verifying each reduction step against the actual crash), which informed root-causing this bug in RegionStoreManager::bindArray and ExprEngine::ProcessInitializer. The commits made here were drafted with Claude's assistance and reviewed by me before being pushed. I've reviewed all AI-assisted contributions here and take full responsibility for the correctness of this change.

…r array-to-pointer decay

ProcessInitializer() strips implicit casts from a CXXCtorInitializer's
init expression via IgnoreImplicit(), then decides whether to treat
the initializer as a direct array-to-array member copy by checking
Init->getType()->isArrayType(). For a pointer member initialized via
array-to-pointer decay of a reference-to-array constructor parameter
(e.g. `Foo(T (&arr)[N]) : ptr_(arr) {}`), IgnoreImplicit() strips the
ArrayToPointerDecay cast, exposing the underlying array-typed
expression, so this check misfires even though the field itself is a
pointer, not an array. That branch fetches the raw region address of
the whole array, bypassing the normal decay logic (which produces an
ElementRegion), so the pointer member ends up holding the address of
the whole array typed as the array itself, instead of an ElementRegion
at index 0.

Later, dereferencing and storing through that mistyped pointer routes
into RegionStoreManager::bindArray() (instead of bindScalar()), which
unconditionally casts its Init value to nonloc::CompoundVal, asserting
in a debug build and segfaulting in a release build when Init is
anything else, e.g. a nonloc::LocAsInteger produced by round-tripping
a pointer through an integer type.

Fix the actual bug by checking the field's type instead of the
initializer expression's type. Also generalize bindArray()'s existing
guard (added by llvm#178923 for issue llvm#178797) from an enumeration of
specific SVal kinds to the same exhaustive
`!isa<nonloc::CompoundVal>()` check already used by its siblings
bindStruct() and bindVector(), so it doesn't need to be extended again
every time a new SVal kind reaches this path -- this is what actually
catches our case (nonloc::LocAsInteger), which the prior enumeration
didn't cover.

This is the same underlying bug behind llvm#147686 (fixed by llvm#153177,
which its own author noted was "more of a workaround") and llvm#178797
(fixed by llvm#178923); both those fixes patched symptoms at bindArray()
without addressing the ProcessInitializer() root cause. Fixing the
root cause also resolves two FIXME-annotated precision gaps in
clang/test/Analysis/initializer.cpp's gh147686 regression test.

Fixes llvm#210183
@github-actions

Copy link
Copy Markdown

Hello @andyames-a11y 👋

Thank you for submitting a Pull Request (PR) to the LLVM Project. Since this is your first PR, here are a few useful links covering our main contribution policies and review practices.

  • All contributions to LLVM must follow our LLVM AI Tool Use Policy. In particular, if you used AI while working on this PR, remember to add a note to the PR description.
  • The LLVM Code-Review Policy and Practices document contains practical information about the PR process, including how patches are reviewed and accepted, and who can review a PR.
  • Our LLVM Developer Policy describes our expectations for code quality, commit summaries and contains notes on our CI system.

Please reply to this message to confirm that you have read these policies, especially the LLVM AI Tool Use Policy, and that any AI tool usage has been noted in the PR description.


Frequently asked questions

How do I add reviewers?

This PR will be automatically labeled, and the relevant teams will be notified. For some parts of the project, reviewers may also be added automatically.

You can also add reviewers manually using the Reviewers section on this page. If you cannot use that section, it is probably because you do not have write permissions for the repository. In that case, you can request a review by tagging reviewers in a comment using @ followed by their GitHub username.

What if there are no comments?

If you have not received any comments on your PR after a week, you can request a review by pinging the PR with a comment such as “Ping”. The common courtesy ping rate is once a week. Please remember that you are asking for volunteer time from other developers.

Are any special GitHub settings required to contribute to LLVM?

We only require contributors to have a public email address associated with their GitHub commits, see this section of LLVM Developer Policy for details.


If you have questions, feel free to leave a comment on this PR, or ask on LLVM Discord or LLVM Discourse.

Thank you,
The LLVM Community

@llvmorg-github-actions llvmorg-github-actions Bot added clang Clang issues not falling into any other category clang:static analyzer labels Jul 20, 2026
@llvmorg-github-actions

Copy link
Copy Markdown

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

Author: Andy Ames (andyames-a11y)

Changes

ProcessInitializer() strips implicit casts from a CXXCtorInitializer's init expression via IgnoreImplicit(), then decides whether to treat the initializer as a direct array-to-array member copy by checking Init->getType()->isArrayType(). For a pointer member initialized via array-to-pointer decay of a reference-to-array constructor parameter (e.g. Foo(T (&amp;arr)[N]) : ptr_(arr) {}), IgnoreImplicit() strips the ArrayToPointerDecay cast, exposing the underlying array-typed expression, so this check misfires even though the field itself is a pointer, not an array. That branch fetches the raw region address of the whole array, bypassing the normal decay logic (which produces an ElementRegion), so the pointer member ends up holding the address of the whole array typed as the array itself, instead of an ElementRegion at index 0.

Later, dereferencing and storing through that mistyped pointer routes into RegionStoreManager::bindArray() (instead of bindScalar()), which unconditionally casts its Init value to nonloc::CompoundVal, asserting in a debug build and segfaulting in a release build when Init is anything else, e.g. a nonloc::LocAsInteger produced by round-tripping a pointer through an integer type.

Fix the actual bug by checking the field's type instead of the initializer expression's type. Also generalize bindArray()'s existing guard (added by #178923 for issue #178797) from an enumeration of specific SVal kinds to the same exhaustive
!isa&lt;nonloc::CompoundVal&gt;() check already used by its siblings bindStruct() and bindVector(), so it doesn't need to be extended again every time a new SVal kind reaches this path -- this is what actually catches our case (nonloc::LocAsInteger), which the prior enumeration didn't cover.

This is the same underlying bug behind #147686 (fixed by #153177, which its own author noted was "more of a workaround") and #178797 (fixed by #178923); both those fixes patched symptoms at bindArray() without addressing the ProcessInitializer() root cause. Fixing the root cause also resolves two FIXME-annotated precision gaps in clang/test/Analysis/initializer.cpp's gh147686 regression test.

Fixes #210183


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

4 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Core/ExprEngine.cpp (+1-1)
  • (modified) clang/lib/StaticAnalyzer/Core/RegionStore.cpp (+7-1)
  • (modified) clang/test/Analysis/initializer.cpp (+3-5)
  • (added) clang/test/Analysis/issue-210183.cpp (+34)
diff --git a/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp b/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp
index 2d86c779ba850..678b07ebba1b0 100644
--- a/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp
+++ b/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp
@@ -1196,7 +1196,7 @@ void ExprEngine::ProcessInitializer(const CFGInitializer CFGInit,
       }
 
       SVal InitVal;
-      if (Init->getType()->isArrayType()) {
+      if (Field->getType()->isArrayType()) {
         // Handle arrays of trivial type. We can represent this with a
         // primitive load/copy from the base array region.
         const ArraySubscriptExpr *ASE;
diff --git a/clang/lib/StaticAnalyzer/Core/RegionStore.cpp b/clang/lib/StaticAnalyzer/Core/RegionStore.cpp
index 0f7e03ce50858..9ddcf9aebee6f 100644
--- a/clang/lib/StaticAnalyzer/Core/RegionStore.cpp
+++ b/clang/lib/StaticAnalyzer/Core/RegionStore.cpp
@@ -2726,7 +2726,13 @@ RegionStoreManager::bindArray(LimitedRegionBindingsConstRef B,
     return bindAggregate(B, R, Init);
   }
 
-  if (isa<nonloc::SymbolVal, UnknownVal, UndefinedVal>(Init))
+  // We may get non-CompoundVal accidentally due to imprecise cast logic or
+  // that we are binding a genuinely symbolic/unknown/undefined array value.
+  // Preserve Init as a default binding rather than lossily converting to
+  // UnknownVal(), and handle every non-CompoundVal case exhaustively (like
+  // bindStruct()/bindVector() do) rather than enumerating specific SVal
+  // kinds one at a time.
+  if (!isa<nonloc::CompoundVal>(Init))
     return bindAggregate(B, R, Init);
 
   // Remaining case: explicit compound values.
diff --git a/clang/test/Analysis/initializer.cpp b/clang/test/Analysis/initializer.cpp
index 88758f7c3ac1d..dd0c91b7ead61 100644
--- a/clang/test/Analysis/initializer.cpp
+++ b/clang/test/Analysis/initializer.cpp
@@ -628,8 +628,7 @@ struct A {
 void test1() {
   A a;
   clang_analyzer_eval(a.m_buf[0] == 0); // expected-warning{{TRUE}}
-  // FIXME The next eval should result in TRUE.
-  clang_analyzer_eval(*a.m_ptr == 0); // expected-warning{{UNKNOWN}}
+  clang_analyzer_eval(*a.m_ptr == 0); // expected-warning{{TRUE}}
 }
 
 void test2() {
@@ -646,9 +645,8 @@ void test3() {
 
 void test3Bis(char arg) {
   A a(arg);
-  // FIXME This test should behave like test3.
-  clang_analyzer_eval(a.m_buf[0] == arg); // expected-warning{{FALSE}} // expected-warning{{TRUE}}
-  clang_analyzer_eval(*a.m_ptr == arg); // expected-warning{{UNKNOWN}}
+  clang_analyzer_eval(a.m_buf[0] == arg); // expected-warning{{TRUE}}
+  clang_analyzer_eval(*a.m_ptr == arg); // expected-warning{{TRUE}}
 }
 
 void test4(char arg) {
diff --git a/clang/test/Analysis/issue-210183.cpp b/clang/test/Analysis/issue-210183.cpp
new file mode 100644
index 0000000000000..ba443aa82ba53
--- /dev/null
+++ b/clang/test/Analysis/issue-210183.cpp
@@ -0,0 +1,34 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -std=c++17 -verify %s
+
+// https://github.com/llvm/llvm-project/issues/210183
+//
+// A pointer member initialized via array-to-pointer decay of a
+// reference-to-array constructor parameter used to be modeled as the
+// address of the whole array (instead of its first element). Dereferencing
+// and storing through that mistyped pointer then reached
+// RegionStoreManager::bindArray() with a scalar Init value, crashing on an
+// unchecked castAs<nonloc::CompoundVal>().
+
+template <class T> void clang_analyzer_dump(T);
+
+template <class T> struct Span {
+  template <int N>
+  Span(T (&arr)[N]) : ptr_(arr) {}
+  T *data() { return ptr_; }
+  T *ptr_;
+};
+
+char *ptr();
+char buffer[10];
+
+void test() {
+  char *p = Span<char>(buffer).data();
+
+  // p and buffer must resolve to the same address: the first element of
+  // buffer, not the whole array.
+  clang_analyzer_dump(buffer); // expected-warning{{&Element{buffer,0 S64b,char}}}
+  clang_analyzer_dump(p);      // expected-warning{{&Element{buffer,0 S64b,char}}}
+
+  int v = (int)(long)ptr();
+  *p = v; // no-crash
+}

@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-clang

Author: Andy Ames (andyames-a11y)

Changes

ProcessInitializer() strips implicit casts from a CXXCtorInitializer's init expression via IgnoreImplicit(), then decides whether to treat the initializer as a direct array-to-array member copy by checking Init->getType()->isArrayType(). For a pointer member initialized via array-to-pointer decay of a reference-to-array constructor parameter (e.g. Foo(T (&amp;arr)[N]) : ptr_(arr) {}), IgnoreImplicit() strips the ArrayToPointerDecay cast, exposing the underlying array-typed expression, so this check misfires even though the field itself is a pointer, not an array. That branch fetches the raw region address of the whole array, bypassing the normal decay logic (which produces an ElementRegion), so the pointer member ends up holding the address of the whole array typed as the array itself, instead of an ElementRegion at index 0.

Later, dereferencing and storing through that mistyped pointer routes into RegionStoreManager::bindArray() (instead of bindScalar()), which unconditionally casts its Init value to nonloc::CompoundVal, asserting in a debug build and segfaulting in a release build when Init is anything else, e.g. a nonloc::LocAsInteger produced by round-tripping a pointer through an integer type.

Fix the actual bug by checking the field's type instead of the initializer expression's type. Also generalize bindArray()'s existing guard (added by #178923 for issue #178797) from an enumeration of specific SVal kinds to the same exhaustive
!isa&lt;nonloc::CompoundVal&gt;() check already used by its siblings bindStruct() and bindVector(), so it doesn't need to be extended again every time a new SVal kind reaches this path -- this is what actually catches our case (nonloc::LocAsInteger), which the prior enumeration didn't cover.

This is the same underlying bug behind #147686 (fixed by #153177, which its own author noted was "more of a workaround") and #178797 (fixed by #178923); both those fixes patched symptoms at bindArray() without addressing the ProcessInitializer() root cause. Fixing the root cause also resolves two FIXME-annotated precision gaps in clang/test/Analysis/initializer.cpp's gh147686 regression test.

Fixes #210183


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

4 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Core/ExprEngine.cpp (+1-1)
  • (modified) clang/lib/StaticAnalyzer/Core/RegionStore.cpp (+7-1)
  • (modified) clang/test/Analysis/initializer.cpp (+3-5)
  • (added) clang/test/Analysis/issue-210183.cpp (+34)
diff --git a/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp b/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp
index 2d86c779ba850..678b07ebba1b0 100644
--- a/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp
+++ b/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp
@@ -1196,7 +1196,7 @@ void ExprEngine::ProcessInitializer(const CFGInitializer CFGInit,
       }
 
       SVal InitVal;
-      if (Init->getType()->isArrayType()) {
+      if (Field->getType()->isArrayType()) {
         // Handle arrays of trivial type. We can represent this with a
         // primitive load/copy from the base array region.
         const ArraySubscriptExpr *ASE;
diff --git a/clang/lib/StaticAnalyzer/Core/RegionStore.cpp b/clang/lib/StaticAnalyzer/Core/RegionStore.cpp
index 0f7e03ce50858..9ddcf9aebee6f 100644
--- a/clang/lib/StaticAnalyzer/Core/RegionStore.cpp
+++ b/clang/lib/StaticAnalyzer/Core/RegionStore.cpp
@@ -2726,7 +2726,13 @@ RegionStoreManager::bindArray(LimitedRegionBindingsConstRef B,
     return bindAggregate(B, R, Init);
   }
 
-  if (isa<nonloc::SymbolVal, UnknownVal, UndefinedVal>(Init))
+  // We may get non-CompoundVal accidentally due to imprecise cast logic or
+  // that we are binding a genuinely symbolic/unknown/undefined array value.
+  // Preserve Init as a default binding rather than lossily converting to
+  // UnknownVal(), and handle every non-CompoundVal case exhaustively (like
+  // bindStruct()/bindVector() do) rather than enumerating specific SVal
+  // kinds one at a time.
+  if (!isa<nonloc::CompoundVal>(Init))
     return bindAggregate(B, R, Init);
 
   // Remaining case: explicit compound values.
diff --git a/clang/test/Analysis/initializer.cpp b/clang/test/Analysis/initializer.cpp
index 88758f7c3ac1d..dd0c91b7ead61 100644
--- a/clang/test/Analysis/initializer.cpp
+++ b/clang/test/Analysis/initializer.cpp
@@ -628,8 +628,7 @@ struct A {
 void test1() {
   A a;
   clang_analyzer_eval(a.m_buf[0] == 0); // expected-warning{{TRUE}}
-  // FIXME The next eval should result in TRUE.
-  clang_analyzer_eval(*a.m_ptr == 0); // expected-warning{{UNKNOWN}}
+  clang_analyzer_eval(*a.m_ptr == 0); // expected-warning{{TRUE}}
 }
 
 void test2() {
@@ -646,9 +645,8 @@ void test3() {
 
 void test3Bis(char arg) {
   A a(arg);
-  // FIXME This test should behave like test3.
-  clang_analyzer_eval(a.m_buf[0] == arg); // expected-warning{{FALSE}} // expected-warning{{TRUE}}
-  clang_analyzer_eval(*a.m_ptr == arg); // expected-warning{{UNKNOWN}}
+  clang_analyzer_eval(a.m_buf[0] == arg); // expected-warning{{TRUE}}
+  clang_analyzer_eval(*a.m_ptr == arg); // expected-warning{{TRUE}}
 }
 
 void test4(char arg) {
diff --git a/clang/test/Analysis/issue-210183.cpp b/clang/test/Analysis/issue-210183.cpp
new file mode 100644
index 0000000000000..ba443aa82ba53
--- /dev/null
+++ b/clang/test/Analysis/issue-210183.cpp
@@ -0,0 +1,34 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -std=c++17 -verify %s
+
+// https://github.com/llvm/llvm-project/issues/210183
+//
+// A pointer member initialized via array-to-pointer decay of a
+// reference-to-array constructor parameter used to be modeled as the
+// address of the whole array (instead of its first element). Dereferencing
+// and storing through that mistyped pointer then reached
+// RegionStoreManager::bindArray() with a scalar Init value, crashing on an
+// unchecked castAs<nonloc::CompoundVal>().
+
+template <class T> void clang_analyzer_dump(T);
+
+template <class T> struct Span {
+  template <int N>
+  Span(T (&arr)[N]) : ptr_(arr) {}
+  T *data() { return ptr_; }
+  T *ptr_;
+};
+
+char *ptr();
+char buffer[10];
+
+void test() {
+  char *p = Span<char>(buffer).data();
+
+  // p and buffer must resolve to the same address: the first element of
+  // buffer, not the whole array.
+  clang_analyzer_dump(buffer); // expected-warning{{&Element{buffer,0 S64b,char}}}
+  clang_analyzer_dump(p);      // expected-warning{{&Element{buffer,0 S64b,char}}}
+
+  int v = (int)(long)ptr();
+  *p = v; // no-crash
+}

@steakhal steakhal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks correct to me.

Comment thread clang/lib/StaticAnalyzer/Core/RegionStore.cpp
@steakhal
steakhal requested review from NagyDonat and Xazax-hun July 20, 2026 13:56
@steakhal

Copy link
Copy Markdown
Contributor

The test failure seems relevant. On MSVC pointer stuff are different, so we may want to pin the target triple for the test.
@NagyDonat Could you please have a look at this and measure this change? Looks correct to me but I want to be 100% sure it's actually good.

@NagyDonat

NagyDonat commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

@NagyDonat Could you please have a look at this and measure this change? Looks correct to me but I want to be 100% sure it's actually good.

I started a CI run that analyzes a dozen open source projects with this revision (comparing it to its parent on the main branch).

The change LGTM overally if you follow the the suggestion to add an assertion and silence the windows-specific test failures (e.g. by pinning the target triple).

Per review feedback from @steakhal: the non-CompoundVal fallback path in
bindArray is only reached for SymbolVal/UnknownVal/UndefinedVal today, and
that invariant is exactly what led to discovering this crash in the first
place. Assert it explicitly so a future regression is caught immediately
rather than silently falling back to bindAggregate with the wrong kind of
value.
andyames-a11y added a commit to andyames-a11y/llvm-project that referenced this pull request Jul 20, 2026
Mirrors the upstream fix (llvm#210649) after review feedback
from @steakhal: the non-CompoundVal fallback path in bindArray is only
reached for a symbolic or undefined array value today, and that invariant
is exactly what led to discovering this crash in the first place. Assert
it explicitly so a future regression is caught immediately rather than
silently falling back to bindAggregate with the wrong kind of value.

UnknownVal is already excluded by this backport's earlier
Init.isUnknown() check, so the assert here is narrower than upstream's.
Comment thread clang/test/Analysis/issue-210183.cpp Outdated
Co-authored-by: Balázs Benics <benicsbalazs@gmail.com>
@steakhal

Copy link
Copy Markdown
Contributor

I'll merge this as soon as the bots are green.

Thank you for the patch, and it was a true delight to see somebody spending the time to deeply understand the context and willing to dig deep into this fairly niche and subtle semantic behavior. It was refreshing to see something like this from a new contributor.

@andyames-a11y

Copy link
Copy Markdown
Contributor Author

I'll merge this as soon as the bots are green.

Thank you for the patch, and it was a true delight to see somebody spending the time to deeply understand the context and willing to dig deep into this fairly niche and subtle semantic behavior. It was refreshing to see something like this from a new contributor.

The feeling is mutual. It was very motivating to see your team so responsive. Getting a good resolution on this issue quickly is giving my team a lot of confidence in our choice to use LLVM for our own tooling.

I had worked on compilers and analyzers once upon a time, so it was quite enjoyable to dip my toe in again. :)

@andyames-a11y

Copy link
Copy Markdown
Contributor Author

BTW, we have an internal custom build where we patched llvmorg-21.1.8, by branching off of that tag.

If there are any plans to patch the 21 release branch, I can make a PR against whatever that branch happens to be, as well.

Just let me know.

@steakhal

steakhal commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

We will likely backport this to release/23.x such that clang-23 gets this fix.
We dont backport fixes to release/22.x or before. Only main and the most recent release branches are maintained. And the release branch has a higher and higher bar over time.

@steakhal
steakhal merged commit 5b1fa37 into llvm:main Jul 21, 2026
11 of 12 checks passed
@github-actions

Copy link
Copy Markdown

@andyames-a11y 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!

@llvm-ci

llvm-ci commented Jul 21, 2026

Copy link
Copy Markdown

LLVM Buildbot has detected a new failure on builder clang-aarch64-quick running on linaro-clang-aarch64-quick while building clang at step 5 "ninja check 1".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/65/builds/37600

Here is the relevant piece of the build log for the reference
Step 5 (ninja check 1) failure: stage 1 checked (failure)
******************** TEST 'LLVM-Unit :: Support/./SupportTests/205/453' FAILED ********************
Script(shard):
--
GTEST_OUTPUT=json:/home/tcwg-buildbot/worker/clang-aarch64-quick/stage1/unittests/Support/./SupportTests-LLVM-Unit-4173283-205-453.json GTEST_SHUFFLE=0 GTEST_TOTAL_SHARDS=453 GTEST_SHARD_INDEX=205 /home/tcwg-buildbot/worker/clang-aarch64-quick/stage1/unittests/Support/./SupportTests
--

Script:
--
/home/tcwg-buildbot/worker/clang-aarch64-quick/stage1/unittests/Support/./SupportTests --gtest_filter=ProgramEnvTest.CreateProcessTrailingSlash
--
/home/tcwg-buildbot/worker/clang-aarch64-quick/llvm/llvm/unittests/Support/ProgramTest.cpp:204: Failure
Expected equality of these values:
  0
  rc
    Which is: -2


/home/tcwg-buildbot/worker/clang-aarch64-quick/llvm/llvm/unittests/Support/ProgramTest.cpp:204
Expected equality of these values:
  0
  rc
    Which is: -2



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


@steakhal

Copy link
Copy Markdown
Contributor

/cherry-pick 5b1fa37

@llvmbot

llvmbot commented Jul 21, 2026

Copy link
Copy Markdown
Member

/pull-request #211069

@NagyDonat

Copy link
Copy Markdown
Contributor

I started a CI run that analyzes a dozen open source projects with this revision (comparing it to its parent on the main branch).

By the way I forgot to mention, but this CI run finished and demonstrated that this PR doesn't break anything in our test projects.

dyung pushed a commit to llvmbot/llvm-project that referenced this pull request Jul 22, 2026
…r array-to-pointer decay (llvm#210649)

ProcessInitializer() strips implicit casts from a CXXCtorInitializer's
init expression via IgnoreImplicit(), then decides whether to treat the
initializer as a direct array-to-array member copy by checking
Init->getType()->isArrayType(). For a pointer member initialized via
array-to-pointer decay of a reference-to-array constructor parameter
(e.g. `Foo(T (&arr)[N]) : ptr_(arr) {}`), IgnoreImplicit() strips the
ArrayToPointerDecay cast, exposing the underlying array-typed
expression, so this check misfires even though the field itself is a
pointer, not an array. That branch fetches the raw region address of the
whole array, bypassing the normal decay logic (which produces an
ElementRegion), so the pointer member ends up holding the address of the
whole array typed as the array itself, instead of an ElementRegion at
index 0.

Later, dereferencing and storing through that mistyped pointer routes
into RegionStoreManager::bindArray() (instead of bindScalar()), which
unconditionally casts its Init value to nonloc::CompoundVal, asserting
in a debug build and segfaulting in a release build when Init is
anything else, e.g. a nonloc::LocAsInteger produced by round-tripping a
pointer through an integer type.

Fix the actual bug by checking the field's type instead of the
initializer expression's type. Also generalize bindArray()'s existing
guard (added by llvm#178923 for issue llvm#178797) from an enumeration of
specific SVal kinds to the same exhaustive
`!isa<nonloc::CompoundVal>()` check already used by its siblings
bindStruct() and bindVector(), so it doesn't need to be extended again
every time a new SVal kind reaches this path -- this is what actually
catches our case (nonloc::LocAsInteger), which the prior enumeration
didn't cover.

This is the same underlying bug behind llvm#147686 (fixed by llvm#153177, which
its own author noted was "more of a workaround") and llvm#178797 (fixed by
llvm#178923); both those fixes patched symptoms at bindArray() without
addressing the ProcessInitializer() root cause. Fixing the root cause
also resolves two FIXME-annotated precision gaps in
clang/test/Analysis/initializer.cpp's gh147686 regression test.

Fixes llvm#210183

AI tool use disclosure: Claude Code (Anthropic) assisted in reducing the
original crash to a minimal, dependency-free reproducer (via creduce
plus manual bisection, verifying each reduction step against the actual
crash), which informed root-causing this bug in
RegionStoreManager::bindArray and ExprEngine::ProcessInitializer. The
commits made here were drafted with Claude's assistance and reviewed by
me before being pushed. I've reviewed all AI-assisted contributions here
and take full responsibility for the correctness of this change.

---------

Co-authored-by: Andy Ames <andy.ames@joby.aero>
Co-authored-by: Balázs Benics <benicsbalazs@gmail.com>
(cherry picked from commit 5b1fa37)
midhuncodes7 pushed a commit to midhuncodes7/llvm-project that referenced this pull request Jul 28, 2026
…r array-to-pointer decay (llvm#210649)

ProcessInitializer() strips implicit casts from a CXXCtorInitializer's
init expression via IgnoreImplicit(), then decides whether to treat the
initializer as a direct array-to-array member copy by checking
Init->getType()->isArrayType(). For a pointer member initialized via
array-to-pointer decay of a reference-to-array constructor parameter
(e.g. `Foo(T (&arr)[N]) : ptr_(arr) {}`), IgnoreImplicit() strips the
ArrayToPointerDecay cast, exposing the underlying array-typed
expression, so this check misfires even though the field itself is a
pointer, not an array. That branch fetches the raw region address of the
whole array, bypassing the normal decay logic (which produces an
ElementRegion), so the pointer member ends up holding the address of the
whole array typed as the array itself, instead of an ElementRegion at
index 0.

Later, dereferencing and storing through that mistyped pointer routes
into RegionStoreManager::bindArray() (instead of bindScalar()), which
unconditionally casts its Init value to nonloc::CompoundVal, asserting
in a debug build and segfaulting in a release build when Init is
anything else, e.g. a nonloc::LocAsInteger produced by round-tripping a
pointer through an integer type.

Fix the actual bug by checking the field's type instead of the
initializer expression's type. Also generalize bindArray()'s existing
guard (added by llvm#178923 for issue llvm#178797) from an enumeration of
specific SVal kinds to the same exhaustive
`!isa<nonloc::CompoundVal>()` check already used by its siblings
bindStruct() and bindVector(), so it doesn't need to be extended again
every time a new SVal kind reaches this path -- this is what actually
catches our case (nonloc::LocAsInteger), which the prior enumeration
didn't cover.

This is the same underlying bug behind llvm#147686 (fixed by llvm#153177, which
its own author noted was "more of a workaround") and llvm#178797 (fixed by
llvm#178923); both those fixes patched symptoms at bindArray() without
addressing the ProcessInitializer() root cause. Fixing the root cause
also resolves two FIXME-annotated precision gaps in
clang/test/Analysis/initializer.cpp's gh147686 regression test.

Fixes llvm#210183

AI tool use disclosure: Claude Code (Anthropic) assisted in reducing the
original crash to a minimal, dependency-free reproducer (via creduce
plus manual bisection, verifying each reduction step against the actual
crash), which informed root-causing this bug in
RegionStoreManager::bindArray and ExprEngine::ProcessInitializer. The
commits made here were drafted with Claude's assistance and reviewed by
me before being pushed. I've reviewed all AI-assisted contributions here
and take full responsibility for the correctness of this change.

---------

Co-authored-by: Andy Ames <andy.ames@joby.aero>
Co-authored-by: Balázs Benics <benicsbalazs@gmail.com>
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

Development

Successfully merging this pull request may close these issues.

Clang Static Analyzer crash in ExprEngine::processPointerEscapedOnBind/ProgramState::bindLoc on store through a templated accessor's pointer

5 participants