https://github.com/mzukovec updated 
https://github.com/llvm/llvm-project/pull/206317

>From e6de198930d93aab685b92186f369c5135cda6c0 Mon Sep 17 00:00:00 2001
From: mzukovec <[email protected]>
Date: Sun, 28 Jun 2026 09:51:02 +0200
Subject: [PATCH] [clang] Cap MS RTTI TypeDescriptor name strings like MSVC
 does

On the MSVC ABI, the RTTI TypeDescriptor embeds the type's decorated
name as a string. Unlike symbol names, this string was never run
through the MD5 capping that MSVC applies to over-long decorated
names, so deeply nested template types (e.g. containing local lambdas
whose mangled context re-expands the enclosing template arguments at
every level) produced exponentially growing writable .data COMDATs;
a 5-level nesting repro emitted an 82 MB object.

Wrap the TypeDescriptor name string in the same msvc_hashing_ostream
used for symbols. MSVC counts the leading '.' toward the 4096
character limit but excludes it from the hashed input, so the
threshold is one lower than for symbols. Both the hashed symbol and
the hashed name string match MSVC's output byte for byte (verified
against MSVC 19.51 at the threshold boundaries), and the repro drops
to a 3 KB object.

Name strings under the limit are unchanged. In particular, lambdas
keep their enclosing context, which is what keeps TypeDescriptors of
lambdas from different scopes distinct.

Co-Authored-By: Claude Fable 5 <[email protected]>
---
 clang/lib/AST/MicrosoftMangle.cpp             | 16 ++--
 clang/test/CodeGenCXX/mangle-ms-md5.cpp       | 36 ++++++++
 .../test/CodeGenCXX/mangle-ms-rtti-lambda.cpp | 84 +++++++++++++++++++
 3 files changed, 131 insertions(+), 5 deletions(-)
 create mode 100644 clang/test/CodeGenCXX/mangle-ms-rtti-lambda.cpp

diff --git a/clang/lib/AST/MicrosoftMangle.cpp 
b/clang/lib/AST/MicrosoftMangle.cpp
index adfe260a0d091..59732ff0fed11 100644
--- a/clang/lib/AST/MicrosoftMangle.cpp
+++ b/clang/lib/AST/MicrosoftMangle.cpp
@@ -57,16 +57,17 @@ static GlobalDecl getGlobalDeclAsDeclContext(const 
DeclContext *DC) {
 
 struct msvc_hashing_ostream : public llvm::raw_svector_ostream {
   raw_ostream &OS;
+  size_t Threshold;
   llvm::SmallString<64> Buffer;
 
-  msvc_hashing_ostream(raw_ostream &OS)
-      : llvm::raw_svector_ostream(Buffer), OS(OS) {}
+  msvc_hashing_ostream(raw_ostream &OS, size_t Threshold = 4096)
+      : llvm::raw_svector_ostream(Buffer), OS(OS), Threshold(Threshold) {}
   ~msvc_hashing_ostream() override {
     StringRef MangledName = str();
     bool StartsWithEscape = MangledName.starts_with("\01");
     if (StartsWithEscape)
       MangledName = MangledName.drop_front(1);
-    if (MangledName.size() < 4096) {
+    if (MangledName.size() < Threshold) {
       OS << str();
       return;
     }
@@ -4193,8 +4194,13 @@ void MicrosoftMangleContextImpl::mangleCXXRTTI(QualType 
T, raw_ostream &Out) {
 
 void MicrosoftMangleContextImpl::mangleCXXRTTIName(
     QualType T, raw_ostream &Out, bool NormalizeIntegers = false) {
-  MicrosoftCXXNameMangler Mangler(*this, Out);
-  Mangler.getStream() << '.';
+  Out << '.';
+  // MSVC caps the length of the TypeDescriptor's name string the same way it
+  // caps decorated names, substituting "??@<md5>@" for over-long names. The
+  // leading '.' counts toward the 4096-character limit but is not part of
+  // the hashed input, so the threshold is one lower than for symbols.
+  msvc_hashing_ostream MHO(Out, /*Threshold=*/4095);
+  MicrosoftCXXNameMangler Mangler(*this, MHO);
   Mangler.mangleType(T, SourceRange(), MicrosoftCXXNameMangler::QMM_Result);
 }
 
diff --git a/clang/test/CodeGenCXX/mangle-ms-md5.cpp 
b/clang/test/CodeGenCXX/mangle-ms-md5.cpp
index 6cd145d9166b6..78f0a56a495d4 100644
--- a/clang/test/CodeGenCXX/mangle-ms-md5.cpp
+++ b/clang/test/CodeGenCXX/mangle-ms-md5.cpp
@@ -34,6 +34,9 @@ struct Y4095 {
 Y4095::Y4095() {}
 // CHECK-DAG: @"??@a6a285da2eea70dba6b578022be61d81@??_R4@" = linkonce_odr 
constant %rtti.CompleteObjectLocator
 // CHECK-DAG: @"??@a6a285da2eea70dba6b578022be61d81@" = alias
+// The TypeDescriptor's name string is replaced by its md5 form as well, just
+// like MSVC does.
+// CHECK-DAG: @"??@c14087f0ec22b387aea7c59083f4f546@" = linkonce_odr global 
%rtti.TypeDescriptor37 { ptr @"??_7type_info@@6B@", ptr null, [38 x i8] 
c".??@5ca87b4c7c2322ca1e21f8b01a23e135@\00" }, comdat
 
 // RUN: %clang_cc1 -DTHROW -fcxx-exceptions -fms-compatibility-version=18.0 
-emit-llvm -o - -triple i686-pc-win32 %s | FileCheck --check-prefix=HAVECTOR %s
 // RUN: %clang_cc1 -DTHROW -fcxx-exceptions -fms-compatibility-version=19.0 
-emit-llvm -o - -triple i686-pc-win32 %s | FileCheck --check-prefix=OMITCTOR %s
@@ -71,3 +74,36 @@ int X4088(z) = 1515;
 // Use initialization to verify mangled name association in the il
 int X4089(z) = 1717;
 // CHECK-DAG: @"??@0269945400a3474730d6880df0967d8f@" = dso_local global i32 
1717, align 4
+
+// Verify the threshold where md5 mangling kicks in for the TypeDescriptor
+// name string. MSVC hashes the string once it reaches 4096 characters
+// counting the leading '.', which is one character sooner than for symbols
+// since the '.' is not part of the hashed input.
+
+// Name string is ".?AU" + 4089 + "@@" = 4095 characters: kept in full, even
+// though the "??_R0" symbol itself is over the limit and gets hashed.
+#define P4089 X4088(p)
+struct P4089 {
+  P4089();
+  virtual void f();
+};
+P4089::P4089() {}
+// CHECK-DAG: @"??@{{[0-9a-f]+}}@" = linkonce_odr global 
%rtti.TypeDescriptor4095 { ptr @"??_7type_info@@6B@", ptr null, [4096 x i8] 
c".?AU{{p+}}@@\00" }, comdat
+
+// Name string is 4096 characters: hashed. Hash value matches MSVC's output
+// for the same type byte for byte.
+#define R4090 X4089(r)
+struct R4090 {
+  R4090();
+  virtual void f();
+};
+R4090::R4090() {}
+// CHECK-DAG: @"??@{{[0-9a-f]+}}@" = linkonce_odr global 
%rtti.TypeDescriptor37 { ptr @"??_7type_info@@6B@", ptr null, [38 x i8] 
c".??@ad624783add47d25188bc51f70637e97@\00" }, comdat
+
+#define R4091 C2(X4089(s), s)
+struct R4091 {
+  R4091();
+  virtual void f();
+};
+R4091::R4091() {}
+// CHECK-DAG: @"??@{{[0-9a-f]+}}@" = linkonce_odr global 
%rtti.TypeDescriptor37 { ptr @"??_7type_info@@6B@", ptr null, [38 x i8] 
c".??@c9d63594821ede4ea693d109deaa7a17@\00" }, comdat
diff --git a/clang/test/CodeGenCXX/mangle-ms-rtti-lambda.cpp 
b/clang/test/CodeGenCXX/mangle-ms-rtti-lambda.cpp
new file mode 100644
index 0000000000000..af2745ef2beba
--- /dev/null
+++ b/clang/test/CodeGenCXX/mangle-ms-rtti-lambda.cpp
@@ -0,0 +1,84 @@
+// RUN: %clang_cc1 -triple x86_64-pc-windows-msvc -std=c++17 
-fms-compatibility -fms-extensions -emit-llvm -o - %s | FileCheck %s
+
+// Deeply nested template types containing local lambdas used to emit their
+// full, recursively expanded name into the RTTI TypeDescriptor's name string,
+// which grows exponentially with nesting depth. Like MSVC, over-long name
+// strings are replaced by their md5 form, which keeps the emitted data
+// bounded. Names under the limit keep the lambda's enclosing context, which
+// is what distinguishes lambdas from different scopes.
+
+namespace std {
+class type_info {
+public:
+  const char *name() const;
+};
+}
+
+using size_t = decltype(sizeof(0));
+
+template <typename... Ts> struct pack {};
+
+template <typename A, typename B> struct pair_like {
+  using first = A;
+  using second = B;
+};
+
+template <typename A, typename B, typename C, typename D> struct quad_like {
+  using a = A;
+  using b = B;
+  using c = C;
+  using d = D;
+};
+
+template <typename T> struct holder {
+  T *value;
+};
+
+template <typename T> auto make_layer() {
+  auto local_lambda = [](const T *, size_t) noexcept { return sizeof(T); };
+  using L = decltype(local_lambda);
+  using PairTL = pair_like<T, L>;
+  using PairLT = pair_like<L, T>;
+  using Quad = quad_like<PairTL, PairLT, holder<T>, holder<L>>;
+  return pack<T, L, PairTL, PairLT, Quad, holder<Quad>>{};
+}
+
+struct seed {
+  int value;
+};
+
+using t01 = decltype(make_layer<seed>());
+using t02 = decltype(make_layer<t01>());
+using t03 = decltype(make_layer<t02>());
+
+extern "C" const std::type_info *repro_type_infos[] = {
+    &typeid(t01),
+    &typeid(t02),
+    &typeid(t03),
+};
+
+extern "C" const char *repro_last_name() {
+  return typeid(t03).name();
+}
+
+// t01's name string is under the limit and keeps the lambda's enclosing
+// context in full.
+// CHECK-DAG: global %rtti.TypeDescriptor{{[0-9]+}} { ptr 
@"??_7type_info@@6B@", ptr null, [{{[0-9]+}} x i8] 
c".?AU?$pack@Useed@@V<lambda_1>@?0???$make_layer@Useed@@@@YA?A?<auto>@@XZ@
+// t02's and t03's name strings are over the limit and are emitted in their
+// md5 form. The distinct hashes keep the two types distinguishable.
+// CHECK-DAG: global %rtti.TypeDescriptor37 { ptr @"??_7type_info@@6B@", ptr 
null, [38 x i8] c".??@1824fd2653392aa825a0ee52ea402a6e@\00" }, comdat
+// CHECK-DAG: global %rtti.TypeDescriptor37 { ptr @"??_7type_info@@6B@", ptr 
null, [38 x i8] c".??@aa208075c0310e8859a51d3c4e878501@\00" }, comdat
+
+// Lambdas from different scopes share the unqualified name <lambda_1>; the
+// enclosing context in the name string is what keeps their TypeDescriptors
+// distinct.
+inline auto f1() { auto l = [](int *) { return 1; }; return l; }
+inline auto f2() { auto l = [](long *) { return 2.5; }; return l; }
+
+extern "C" const std::type_info *lambda_type_infos[] = {
+    &typeid(decltype(f1())),
+    &typeid(decltype(f2())),
+};
+
+// CHECK-DAG: @"??_R0?AV<lambda_1>@?0??f1@@YA?A?<auto>@@XZ@@8" = linkonce_odr 
global
+// CHECK-DAG: @"??_R0?AV<lambda_1>@?0??f2@@YA?A?<auto>@@XZ@@8" = linkonce_odr 
global

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to