[clang] [Clang][Sema] Fix crash in RemoveNestedImmediateInvocation with __builtin_dump_struct (PR #192880)
Vladislav Semykin via cfe-commits
cfe-commits at lists.llvm.org
Mon Apr 20 00:20:28 PDT 2026
https://github.com/ViNN280801 updated https://github.com/llvm/llvm-project/pull/192880
>From 122f1193a0a0c5b8d25b088ecc5499521585ae81 Mon Sep 17 00:00:00 2001
From: ViNN280801 <vladislav.semykin at gmail.com>
Date: Mon, 20 Apr 2026 01:46:40 +0300
Subject: [PATCH 1/2] [Clang][Sema] Fix crash in
RemoveNestedImmediateInvocation with __builtin_dump_struct
Signed-off-by: ViNN280801 <vladislav.semykin at gmail.com>
---
clang/docs/ReleaseNotes.rst | 3 +
clang/lib/Sema/SemaExpr.cpp | 66 +++++++++++++++++++
.../builtin-dump-struct-immediate-clean.cpp | 29 ++++++++
.../SemaCXX/builtin-dump-struct-immediate.cpp | 63 ++++++++++++++++++
4 files changed, 161 insertions(+)
create mode 100644 clang/test/SemaCXX/builtin-dump-struct-immediate-clean.cpp
create mode 100644 clang/test/SemaCXX/builtin-dump-struct-immediate.cpp
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 638a813ca105b..f4d2d7c63c902 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -527,6 +527,9 @@ Miscellaneous Clang Crashes Fixed
- Fixed an assertion failure when parsing an invalid out-of-line enum definition with template parameters. (#GH187909)
- Fixed an assertion failure on invalid template template parameter during typo correction. (#GH183983)
- Fixed an assertion failure in ``isAtEndOfMacroExpansion`` on macro expansions crossing the boundary of two fileIDs. (#GH115007), (#GH21755)
+- Fixed an assertion failure when ``__builtin_dump_struct`` is called with an
+ immediate-escalated callable (for example a function whose body contains
+ ``__builtin_is_within_lifetime``). (#GH192846)
OpenACC Specific Changes
------------------------
diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index cf235095d489d..204888e8503e5 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -18463,6 +18463,72 @@ static void RemoveNestedImmediateInvocation(
// Lambdas have already been processed inside their eval contexts.
return E;
}
+
+ // Default TreeTransform::TransformOpaqueValueExpr requires either no
+ // SourceExpr or AlreadyTransformed(getType()). ComplexRemove inherits the
+ // base AlreadyTransformed implementation (true only for the null type), so
+ // any OpaqueValueExpr that carries a SourceExpr would assert.
+ //
+ // That arises for __builtin_dump_struct: the record pointer is bound in an
+ // OpaqueValueExpr and reused as the base of synthesized MemberExprs. When
+ // peeling nested immediate invocations we must recurse through that source
+ // as well; otherwise ConstantExpr wrappers inside the source would survive
+ // incorrectly.
+ //
+ // Alternatives considered:
+ // - Returning the original OVE without traversing SourceExpr silences the
+ // assert but violates this pass's purpose for nested immediates under the
+ // binding expression.
+ // - Overriding AlreadyTransformed to always return true skips traversal for
+ // the same reason.
+ // - Restructuring __builtin_dump_struct's AST would touch many consumers.
+ // - In-place mutation of SourceExpr would preserve node identity, but
+ // OpaqueValueExpr exposes no mutator for SourceExpr; widening the AST API
+ // for this localized issue is disproportionate.
+ //
+ // Returning E without traversing SourceExpr could also rely on the
+ // enclosing PseudoObjectExpr to evaluate nested immediates correctly, but
+ // ComplexRemove is intended to strip nested immediate-invocation wrappers
+ // throughout the transformed subtree; recursing through SourceExpr matches
+ // that contract.
+ //
+ // Risk: code that keyed on the exact OpaqueValueExpr* for a dump-struct
+ // binding could theoretically desynchronize if we replace the node. No
+ // such registry is known; we only allocate a replacement when the
+ // transformed source is a different Expr*.
+ //
+ // Note: DenseMap may rehash on insert; do not hold iterators/pointers
+ // across TransformExpr(Src), since nested OVEs may insert into this map.
+ llvm::DenseMap<OpaqueValueExpr *, OpaqueValueExpr *> TransformedOpaqueValues;
+
+ ExprResult TransformOpaqueValueExpr(OpaqueValueExpr *E) {
+ if (auto It = TransformedOpaqueValues.find(E);
+ It != TransformedOpaqueValues.end())
+ return It->second;
+
+ // Provisional self-mapping breaks cycles if SourceExpr reaches this OVE.
+ TransformedOpaqueValues[E] = E;
+
+ Expr *Src = E->getSourceExpr();
+ if (!Src)
+ return E;
+
+ ExprResult NewSrc = getDerived().TransformExpr(Src);
+ if (NewSrc.isInvalid()) {
+ TransformedOpaqueValues.erase(E);
+ return ExprError();
+ }
+
+ if (NewSrc.get() == Src)
+ return E;
+
+ auto *Result = new (SemaRef.getASTContext())
+ OpaqueValueExpr(E->getLocation(), E->getType(), E->getValueKind(),
+ E->getObjectKind(), NewSrc.get());
+ TransformedOpaqueValues[E] = Result;
+ return Result;
+ }
+
bool AlwaysRebuild() { return false; }
bool ReplacingOriginal() { return true; }
bool AllowSkippingCXXConstructExpr() {
diff --git a/clang/test/SemaCXX/builtin-dump-struct-immediate-clean.cpp b/clang/test/SemaCXX/builtin-dump-struct-immediate-clean.cpp
new file mode 100644
index 0000000000000..bda7cb1a12dd1
--- /dev/null
+++ b/clang/test/SemaCXX/builtin-dump-struct-immediate-clean.cpp
@@ -0,0 +1,29 @@
+// RUN: %clang_cc1 -std=c++2c -fsyntax-only -Wno-unused -verify %s
+// RUN: %clang_cc1 -std=c++23 -fsyntax-only -Wno-unused -verify %s
+
+// expected-no-diagnostics
+
+// GH192846: minimal well-formed case - immediate-escalated printing function
+// with __builtin_dump_struct must not crash in ComplexRemove. See sibling
+// builtin-dump-struct-immediate.cpp for C++26 vs language-mode notes.
+
+#if !__has_builtin(__builtin_is_within_lifetime)
+#error "test requires __builtin_is_within_lifetime"
+#endif
+
+struct S {};
+
+template <typename... T>
+constexpr void F(S &out, const char *fmt, T... args) {
+ (void)__builtin_is_within_lifetime(&out);
+}
+
+template <class T>
+class C {
+ T value = {};
+};
+
+void bar() {
+ S s;
+ __builtin_dump_struct((C<int> *)nullptr, F, s);
+}
diff --git a/clang/test/SemaCXX/builtin-dump-struct-immediate.cpp b/clang/test/SemaCXX/builtin-dump-struct-immediate.cpp
new file mode 100644
index 0000000000000..f094e184178c1
--- /dev/null
+++ b/clang/test/SemaCXX/builtin-dump-struct-immediate.cpp
@@ -0,0 +1,63 @@
+// RUN: %clang_cc1 -std=c++23 -fsyntax-only -Wno-unused %s -DVALID_CASE
+// RUN: %clang_cc1 -std=c++2c -fsyntax-only -Wno-unused %s -DVALID_CASE
+// RUN: %clang_cc1 -std=c++23 -fsyntax-only -Wno-unused -verify %s
+// RUN: %clang_cc1 -std=c++2c -fsyntax-only -Wno-unused -verify %s
+
+// Source: GH192846 reported by @k-arrows with reproducer: https://godbolt.org/z/9aa43GE1a
+//
+// P2641R4 proposes std::is_within_lifetime(const T*) (consteval) for C++26 -
+// see https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2641r4.html
+// ("active union member" generalized to "within lifetime" for constexpr).
+// Clang: __builtin_is_within_lifetime (immediate builtin). The crash fixed here
+// is in Sema immediate-invocation handling, not in a particular -std mode, so
+// we run under both C++23 and C++2c.
+
+// Regression test for GH192846: ComplexRemove must traverse OpaqueValueExpr
+// sources (e.g. from __builtin_dump_struct) when stripping nested immediate
+// invocations; the default TreeTransform hook would assert.
+
+#if !__has_builtin(__builtin_is_within_lifetime)
+#error "test requires __builtin_is_within_lifetime"
+#endif
+
+#ifdef VALID_CASE
+
+// dump_struct may manifestly-constant-evaluate the printing callback; the
+// consteval builtin must only read objects usable in constant expressions.
+constexpr int foo = 0;
+
+template <typename T>
+struct C {
+ int n;
+};
+
+// The dump_struct machinery invokes this with a synthesized argument list; it
+// must not be a template (template arguments are not deduced from that call).
+void F(const char *fmt, ...) {
+ (void)__builtin_is_within_lifetime(&foo);
+}
+
+void bar() {
+ constexpr char s[] = "";
+ __builtin_dump_struct((C<int> *)nullptr, F, s);
+}
+
+#else
+
+template <typename T>
+struct C {
+ int n;
+};
+
+void F(const char *fmt, ...) {
+ // Error recovery can report both the bad identifier and an invalid use of the
+ // consteval builtin on the same expression.
+ (void)__builtin_is_within_lifetime(&foo); // expected-error {{use of undeclared identifier 'foo'}} expected-error {{cannot take address of consteval function '__builtin_is_within_lifetime' outside of an immediate invocation}}
+}
+
+void bar() {
+ constexpr char s[] = "";
+ __builtin_dump_struct((C<int> *)nullptr, F, s);
+}
+
+#endif
>From f1d96f2e0ee94f3fb0f81db08dcf5f511c5addc8 Mon Sep 17 00:00:00 2001
From: ViNN280801 <vladislav.semykin at gmail.com>
Date: Mon, 20 Apr 2026 10:20:15 +0300
Subject: [PATCH 2/2] [Clang][Sema] Reformatted the code properly
Signed-off-by: ViNN280801 <vladislav.semykin at gmail.com>
---
clang/lib/Sema/SemaExpr.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index 204888e8503e5..0355d5af2dbee 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -18499,7 +18499,8 @@ static void RemoveNestedImmediateInvocation(
//
// Note: DenseMap may rehash on insert; do not hold iterators/pointers
// across TransformExpr(Src), since nested OVEs may insert into this map.
- llvm::DenseMap<OpaqueValueExpr *, OpaqueValueExpr *> TransformedOpaqueValues;
+ llvm::DenseMap<OpaqueValueExpr *, OpaqueValueExpr *>
+ TransformedOpaqueValues;
ExprResult TransformOpaqueValueExpr(OpaqueValueExpr *E) {
if (auto It = TransformedOpaqueValues.find(E);
More information about the cfe-commits
mailing list