Skip to content

[llvm][DebugInfo] Avoid attaching retained nodes to unrelated subprograms in DIBuilder#180294

Merged
dzhidzhoev merged 3 commits intollvm:mainfrom
dzhidzhoev:debuginfo/rfc-krisb/4/fix-ctor-local-type/base
Feb 9, 2026
Merged

[llvm][DebugInfo] Avoid attaching retained nodes to unrelated subprograms in DIBuilder#180294
dzhidzhoev merged 3 commits intollvm:mainfrom
dzhidzhoev:debuginfo/rfc-krisb/4/fix-ctor-local-type/base

Conversation

@dzhidzhoev
Copy link
Copy Markdown
Member

@dzhidzhoev dzhidzhoev commented Feb 6, 2026

Fix a regression introduced by #165032, where DIBuilder could attach local metadata nodes to the wrong subprogram during finalization.

DIBuilder records freshly created local variables, labels, and types in DIBuilder::SubprogramTrackedNodes, and later attaches them to their parent subprogram's retainedNodes in finalizeSubprogram().

However, a temporary local type created via createReplaceableCompositeType() may later be replaced by a type with a different scope.
DIBuilder does not currently verify that the scopes of the original and replacement types match.

As a result, local types can be incorrectly attached to the retainedNodes of an unrelated subprogram. This issue is observable in clang with limited debug info mode (see clang/test/DebugInfo/CXX/ctor-homing-local-type.cpp).

This patch updates DIBuilder::finalizeSubprogram() to verify that tracked metadata nodes still belong to the subprogram being finalized, and avoids adding nodes whose scopes no longer match to retainedNodes field of an unrelated subprogram.

…s in DIBuilder

Fix a regression introduced by llvm#165032
where DIBuilder could attach local metadata nodes to the wrong subprogram during finalization.

DIBuilder tracks local variables, labels, and types in
`DIBuilder::SubprogramTrackedNodes` and later attaches them to their parent
subprogram's retainedNodes in `finalizeSubprogram()`.

However, temporary local types created via `createReplaceableCompositeType()`
may later be replaced by a type with a different scope. DIBuilder does not
currently verify that the scopes of the original and replacement types match.

As a result, local types can be incorrectly attached to the retainedNodes of an
unrelated subprogram. This issue is observable in clang with limited debug info
mode (see `clang/test/DebugInfo/CXX/ctor-homing-local-type.cpp`).

This patch updates DIBuilder::finalizeSubprogram() to verify that tracked
metadata nodes still belong to the subprogram being finalized, and skips nodes
whose scopes no longer match.
@llvmbot
Copy link
Copy Markdown
Member

llvmbot commented Feb 6, 2026

@llvm/pr-subscribers-debuginfo

@llvm/pr-subscribers-llvm-ir

Author: Vladislav Dzhidzhoev (dzhidzhoev)

Changes

Fix a regression introduced by #165032, where DIBuilder could attach local metadata nodes to the wrong subprogram during finalization.

DIBuilder tracks local variables, labels, and types in DIBuilder::SubprogramTrackedNodes, and later attaches them to their parent subprogram's retainedNodes in finalizeSubprogram().

However, temporary local types created via createReplaceableCompositeType() may later be replaced by a type with a different scope.
DIBuilder does not currently verify that the scopes of the original and replacement types match.

As a result, local types can be incorrectly attached to the retainedNodes of an unrelated subprogram. This issue is observable in clang with limited debug info mode (see clang/test/DebugInfo/CXX/ctor-homing-local-type.cpp).

This patch updates DIBuilder::finalizeSubprogram() to verify that tracked metadata nodes still belong to the subprogram being finalized, and skips nodes whose scopes no longer match.


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

3 Files Affected:

  • (added) clang/test/DebugInfo/CXX/ctor-homing-local-type.cpp (+40)
  • (modified) llvm/lib/IR/DIBuilder.cpp (+21-4)
  • (modified) llvm/unittests/IR/IRBuilderTest.cpp (+47)
diff --git a/clang/test/DebugInfo/CXX/ctor-homing-local-type.cpp b/clang/test/DebugInfo/CXX/ctor-homing-local-type.cpp
new file mode 100644
index 0000000000000..83d1c0548bfbb
--- /dev/null
+++ b/clang/test/DebugInfo/CXX/ctor-homing-local-type.cpp
@@ -0,0 +1,40 @@
+// RUN: %clang_cc1 -triple %itanium_abi_triple -emit-llvm -debug-info-kind=limited -dwarf-version=5 -O0 -disable-llvm-passes %s -o - \
+// RUN:        | FileCheck %s
+
+// When compiling this with limited debug info, a replaceable forward declaration DICompositeType
+// for "n" is created in the scope of base object constructor (C2) of class l, and it's not immediately
+// replaced with a distinct node (the type is not "completed").
+// Later, it gets replaced with distinct definition DICompositeType, which is created in the scope of
+// complete object constructor (C1).
+//
+// In contrast to that, in standalone debug info mode, the complete definition DICompositeType
+// for "n" is created sooner, right in the context of C2.
+//
+// Check that DIBuilder processes the limited debug info case correctly, and doesn't add the same
+// local type to retainedNodes fields of both DISubprograms (C1 and C2).
+
+// FIXME: Should we ensure that DICompositeType is emitted in the same scope in both limited and
+// standalone modes?
+
+// CHECK: ![[C2:[0-9]+]] = distinct !DISubprogram(name: "l", linkageName: "_ZN1lC2Ev", {{.*}}, retainedNodes: ![[EMPTY:[0-9]+]])
+// CHECK: ![[EMPTY]] = !{}
+// CHECK: ![[N:[0-9]+]] = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "n",
+// CHECK: ![[C1:[0-9]+]] = distinct !DISubprogram(name: "l", linkageName: "_ZN1lC1Ev", {{.*}}, retainedNodes: ![[RN:[0-9]+]])
+// CHECK: ![[RN]] = !{![[N]]}
+
+template <class d>
+struct k {
+  void i() {
+    new d;
+  }
+};
+
+struct l {
+  l();
+};
+
+l::l() {
+  struct n {};
+  k<n> m;
+  m.i();
+}
diff --git a/llvm/lib/IR/DIBuilder.cpp b/llvm/lib/IR/DIBuilder.cpp
index ec6fd8e8b895a..ee9ea84f132db 100644
--- a/llvm/lib/IR/DIBuilder.cpp
+++ b/llvm/lib/IR/DIBuilder.cpp
@@ -53,10 +53,27 @@ void DIBuilder::trackIfUnresolved(MDNode *N) {
 
 void DIBuilder::finalizeSubprogram(DISubprogram *SP) {
   auto PN = SubprogramTrackedNodes.find(SP);
-  if (PN != SubprogramTrackedNodes.end())
-    SP->replaceRetainedNodes(
-        MDTuple::get(VMContext, SmallVector<Metadata *, 16>(PN->second.begin(),
-                                                            PN->second.end())));
+  if (PN != SubprogramTrackedNodes.end()) {
+    SmallVector<Metadata *, 16> RetainedNodes(PN->second.begin(),
+                                              PN->second.end());
+
+    // If the tracked node PN was temporary, and the DIBuilder user replaced it
+    // with a node that does not belong to SP or is non-local, do not add PN to
+    // SP's retainedNodes list.
+    auto IsNodeAlien = [SP](Metadata *M) {
+      MDNode *N = cast<MDNode>(M);
+      DILocalScope *Scope = dyn_cast_or_null<DILocalScope>(
+          DISubprogram::getRawRetainedNodeScope(N));
+      if (!Scope)
+        return true;
+      return Scope->getSubprogram() != SP;
+    };
+    RetainedNodes.erase(
+        std::remove_if(RetainedNodes.begin(), RetainedNodes.end(), IsNodeAlien),
+        RetainedNodes.end());
+
+    SP->replaceRetainedNodes(MDTuple::get(VMContext, RetainedNodes));
+  }
 }
 
 void DIBuilder::finalize() {
diff --git a/llvm/unittests/IR/IRBuilderTest.cpp b/llvm/unittests/IR/IRBuilderTest.cpp
index 2d21e6f8b7148..4361594050668 100644
--- a/llvm/unittests/IR/IRBuilderTest.cpp
+++ b/llvm/unittests/IR/IRBuilderTest.cpp
@@ -1368,4 +1368,51 @@ TEST_F(IRBuilderTest, CTAD) {
   IRBuilder Builder7(BB, BB->end());
   static_assert(std::is_same_v<decltype(Builder7), IRBuilder<>>);
 }
+
+TEST_F(IRBuilderTest, finalizeSubprogram) {
+  IRBuilder<> Builder(BB);
+  DIBuilder DIB(*M);
+  auto File = DIB.createFile("main.c", "/");
+  auto CU = DIB.createCompileUnit(
+      DISourceLanguageName(dwarf::DW_LANG_C_plus_plus), File, "clang",
+      /*isOptimized=*/true, /*Flags=*/"",
+      /*Runtime Version=*/0);
+  auto FuncType = DIB.createSubroutineType(DIB.getOrCreateTypeArray({}));
+  auto FooSP = DIB.createFunction(
+      CU, "foo", /*LinkageName=*/"", File,
+      /*LineNo=*/1, FuncType, /*ScopeLine=*/2, DINode::FlagZero,
+      DISubprogram::SPFlagDefinition | DISubprogram::SPFlagOptimized);
+
+  F->setSubprogram(FooSP);
+  AllocaInst *I = Builder.CreateAlloca(Builder.getInt8Ty());
+  ReturnInst *R = Builder.CreateRetVoid();
+  I->setDebugLoc(DILocation::get(Ctx, 3, 2, FooSP));
+  R->setDebugLoc(DILocation::get(Ctx, 4, 2, FooSP));
+
+  auto BarSP = DIB.createFunction(
+      CU, "bar", /*LinkageName=*/"", File,
+      /*LineNo=*/1, FuncType, /*ScopeLine=*/2, DINode::FlagZero,
+      DISubprogram::SPFlagDefinition | DISubprogram::SPFlagOptimized);
+
+  // Create a temporary structure in scope of FooSP.
+  llvm::TempDIType ForwardDeclaredType =
+      llvm::TempDIType(DIB.createReplaceableCompositeType(
+          llvm::dwarf::DW_TAG_structure_type, "MyType", FooSP, File, 0, 0, 8, 8,
+          {}, "UniqueIdentifier"));
+
+  // Instantiate the real structure in scope of BarSP.
+  DICompositeType *Type = DIB.createStructType(
+      BarSP, "MyType", File, 0, 8, 8, {}, {}, {}, 0, {}, "UniqueIdentifier");
+  // Replace the temporary type with the real type.
+  DIB.replaceTemporary(std::move(ForwardDeclaredType), Type);
+
+  DIB.finalize();
+  EXPECT_FALSE(verifyModule(*M));
+
+  // After finalization, MyType should appear in retainedNodes of BarSP,
+  // not in FooSP's.
+  EXPECT_EQ(BarSP->getRetainedNodes().size(), 1u);
+  EXPECT_EQ(BarSP->getRetainedNodes()[0], Type);
+  EXPECT_TRUE(FooSP->getRetainedNodes().empty());
+}
 }

@dzhidzhoev dzhidzhoev changed the title [llvm][DebugInfo] Avoid attaching retained nodes from unrelated scopes in DIBuilder [llvm][DebugInfo] Avoid attaching retained nodes to unrelated scopes in DIBuilder Feb 6, 2026
@dzhidzhoev dzhidzhoev changed the title [llvm][DebugInfo] Avoid attaching retained nodes to unrelated scopes in DIBuilder [llvm][DebugInfo] Avoid attaching retained nodes to unrelated subprograms in DIBuilder Feb 6, 2026
Copy link
Copy Markdown
Member

@jmorse jmorse left a comment

Choose a reason for hiding this comment

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

LGTM; this feels like the kind of assumption/situation that we'd expect to be flushed out by fixing locally-scoped types. Great!

Comment on lines +16 to +17
// FIXME: Should we ensure that DICompositeType is emitted in the same scope in both limited and
// standalone modes?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It does feel wrong that there's a difference; I think in the grand scheme of things though, this is an acceptable variation as part of the greater aim of fixing locally-scoped types. Presumably if this created difficulties for consumers, it would present itself when someone consumed compilation-units that were compiled with different flags?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've noticed this quirk when building clang on clang in limited debug info mode on my own (and got a crash bc of DIBuilder attaching a type to two subprograms).
I haven't received feedback from consumers about the mismatch in where (in which constructor version) local types are emitted.
I will remove this FIXME for now, check a bit later how this affects debugging experience, and create an issue for that if there is something (not sure how much time I'll have this week as I'm out of office).

@dzhidzhoev
Copy link
Copy Markdown
Member Author

Thank you!

@dzhidzhoev dzhidzhoev merged commit 9069164 into llvm:main Feb 9, 2026
7 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants