[llvm] Users/mgcarrasco/di nonlocal scope crash (PR #218002)

Manuel Carrasco via llvm-commits llvm-commits at lists.llvm.org
Mon Aug 24 05:20:47 PDT 2026


https://github.com/mgcarrasco updated https://github.com/llvm/llvm-project/pull/218002

>From 5c2719a958522148f3d31c74da6ba5e921bf8156 Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Fri, 21 Aug 2026 13:15:31 -0500
Subject: [PATCH 1/2] [Verifier] Reject cyclic DIScope parent chains

A DIScope parent chain must be acyclic. Diagnose cycles so later
walks of getScope() cannot loop.
---
 llvm/lib/IR/Verifier.cpp                      | 70 ++++++++++++++++++-
 .../Transforms/Coroutines/coro-debug-O2.ll    |  2 +-
 .../Coroutines/coro-debug-coro-frame.ll       |  4 +-
 llvm/test/Verifier/DILocalScope-cycle.ll      | 27 +++++++
 llvm/test/Verifier/DIScope-cycle.ll           | 32 +++++++++
 5 files changed, 131 insertions(+), 4 deletions(-)
 create mode 100644 llvm/test/Verifier/DILocalScope-cycle.ll
 create mode 100644 llvm/test/Verifier/DIScope-cycle.ll

diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index 18425ca1cb1c4..02f20a2527f60 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -164,6 +164,9 @@ class Verifier : public InstVisitor<Verifier>, VerifierSupport {
   /// Keep track which DISubprogram is attached to which function.
   DenseMap<const DISubprogram *, const Function *> DISubprogramAttachments;
 
+  /// Cache of whether following a DIScope's scope chain repeats a node.
+  DenseMap<const Metadata *, bool> DIScopeCycleCache;
+
   /// Track all DICompileUnits visited.
   SmallPtrSet<const Metadata *, 2> CUVisited;
 
@@ -269,6 +272,7 @@ class Verifier : public InstVisitor<Verifier>, VerifierSupport {
 
     InstsInThisBlock.clear();
     DebugFnArgs.clear();
+    DIScopeCycleCache.clear();
     LandingPadResultTy = nullptr;
     SawFrameEscape = false;
     SiblingFuncletInfo.clear();
@@ -314,6 +318,7 @@ class Verifier : public InstVisitor<Verifier>, VerifierSupport {
 
     verifyDeoptimizeCallingConvs();
     DISubprogramAttachments.clear();
+    DIScopeCycleCache.clear();
     return !Broken;
   }
 
@@ -380,6 +385,9 @@ class Verifier : public InstVisitor<Verifier>, VerifierSupport {
 #include "llvm/IR/Metadata.def"
   void visitDIType(const DIType &N);
   void visitDIScope(const DIScope &N);
+  void visitDIScopeChain(const DIScope &N);
+  bool hasDIScopeCycle(const Metadata *S);
+  DISubprogram *getSubprogram(Metadata *LocalScope);
   void visitDIVariable(const DIVariable &N);
   void visitDILexicalBlockBase(const DILexicalBlockBase &N);
   void visitDITemplateParameter(const DITemplateParameter &N);
@@ -949,6 +957,55 @@ void Verifier::visitNamedMDNode(const NamedMDNode &NMD) {
   }
 }
 
+/// Parent scope operand of \p S, or null if \p S has no parent (a \c DIFile,
+/// \c DICompileUnit, or non-scope). Mirrors \c DIScope::getScope() without
+/// asserting on unexpected metadata kinds.
+static const Metadata *getRawDIScopeParent(const Metadata *S) {
+  if (auto *T = dyn_cast_or_null<DIType>(S))
+    return T->getRawScope();
+  if (auto *SP = dyn_cast_or_null<DISubprogram>(S))
+    return SP->getRawScope();
+  if (auto *LB = dyn_cast_or_null<DILexicalBlockBase>(S))
+    return LB->getRawScope();
+  if (auto *NS = dyn_cast_or_null<DINamespace>(S))
+    return NS->getRawScope();
+  if (auto *CB = dyn_cast_or_null<DICommonBlock>(S))
+    return CB->getRawScope();
+  if (auto *M = dyn_cast_or_null<DIModule>(S))
+    return M->getRawScope();
+  return nullptr;
+}
+
+/// True if following the scope operand from \p S repeats a node.
+bool Verifier::hasDIScopeCycle(const Metadata *S) {
+  SmallPtrSet<const Metadata *, 8> Seen;
+  auto CacheSeen = [&](bool HasCycle) {
+    for (const Metadata *M : Seen)
+      DIScopeCycleCache[M] = HasCycle;
+    return HasCycle;
+  };
+
+  while (auto *Scope = dyn_cast_or_null<DIScope>(S)) {
+    auto It = DIScopeCycleCache.find(Scope);
+    bool IsInCache = It != DIScopeCycleCache.end();
+    if (IsInCache)
+      return CacheSeen(It->second);
+    bool AlreadySeen = !Seen.insert(Scope).second;
+    if (AlreadySeen) // New cycle detected
+      return CacheSeen(true);
+    // No new cycle detected
+    S = getRawDIScopeParent(Scope);
+  }
+
+  // Finished walking node chain without detecting any cycles
+  return CacheSeen(false);
+}
+
+void Verifier::visitDIScopeChain(const DIScope &N) {
+  CheckDI(!hasDIScopeCycle(&N), "DIScope scope chain must not contain a cycle",
+          &N);
+}
+
 void Verifier::visitMDNode(const MDNode &BaseMD,
                            AreDebugLocsAllowed AllowLocs) {
   // Only visit each node once.  Metadata can be mutually recursive, so this
@@ -977,6 +1034,10 @@ void Verifier::visitMDNode(const MDNode &BaseMD,
 #include "llvm/IR/Metadata.def"
     }
 
+    // A scope chain must terminate.
+    if (const auto *S = dyn_cast<DIScope>(CurrentMD))
+      visitDIScopeChain(*S);
+
     for (const Metadata *Op : CurrentMD->operands()) {
       if (!Op)
         continue;
@@ -3379,6 +3440,10 @@ void Verifier::visitFunction(const Function &F) {
     if (!Seen.insert(Scope).second)
       return;
 
+    // Cycles are diagnosed when the DIScope nodes themselves are visited.
+    if (hasDIScopeCycle(Scope))
+      return;
+
     DISubprogram *SP = Scope->getSubprogram();
 
     // Scope and SP could be the same MDNode and we don't want to skip
@@ -7154,7 +7219,10 @@ void Verifier::visitIntrinsicCall(Intrinsic::ID ID, CallBase &Call) {
 ///
 /// This carefully grabs the subprogram from a local scope, avoiding the
 /// built-in assertions that would typically fire.
-static DISubprogram *getSubprogram(Metadata *LocalScope) {
+DISubprogram *Verifier::getSubprogram(Metadata *LocalScope) {
+  if (hasDIScopeCycle(LocalScope))
+    return nullptr;
+
   if (!LocalScope)
     return nullptr;
 
diff --git a/llvm/test/Transforms/Coroutines/coro-debug-O2.ll b/llvm/test/Transforms/Coroutines/coro-debug-O2.ll
index fce7c19ec8c9d..0590eac42009c 100644
--- a/llvm/test/Transforms/Coroutines/coro-debug-O2.ll
+++ b/llvm/test/Transforms/Coroutines/coro-debug-O2.ll
@@ -153,7 +153,7 @@ declare void @final_suspend()
 !5 = !{!"clang version 11.0.0"}
 !6 = !DILocalVariable(name: "__promise", scope: !7, file: !1, line: 24, type: !10)
 !7 = distinct !DILexicalBlock(scope: !8, file: !1, line: 23, column: 12)
-!8 = distinct !DISubprogram(name: "foo", linkageName: "_Z3foov", scope: !8, file: !1, line: 23, type: !9, scopeLine: 23, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !2)
+!8 = distinct !DISubprogram(name: "foo", linkageName: "_Z3foov", scope: !1, file: !1, line: 23, type: !9, scopeLine: 23, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !2)
 !9 = !DISubroutineType(types: !2)
 !10 = !DIDerivedType(tag: DW_TAG_typedef, name: "promise_type", scope: !8, file: !1, line: 15, baseType: !11)
 !11 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "promise_type", scope: !8, file: !1, line: 10, size: 128, flags: DIFlagTypePassByValue | DIFlagNonTrivial, elements: !12, identifier: "_ZTSN4coro12promise_typeE")
diff --git a/llvm/test/Transforms/Coroutines/coro-debug-coro-frame.ll b/llvm/test/Transforms/Coroutines/coro-debug-coro-frame.ll
index 629ab5be972f8..a17e89322823e 100644
--- a/llvm/test/Transforms/Coroutines/coro-debug-coro-frame.ll
+++ b/llvm/test/Transforms/Coroutines/coro-debug-coro-frame.ll
@@ -377,7 +377,7 @@ declare void @final_suspend()
 !5 = !{!"clang version 11.0.0"}
 !6 = !DILocalVariable(name: "__promise", scope: !7, file: !1, line: 24, type: !10)
 !7 = distinct !DILexicalBlock(scope: !8, file: !1, line: 23, column: 12)
-!8 = distinct !DISubprogram(name: "foo", linkageName: "_Z3foov", scope: !8, file: !1, line: 23, type: !9, scopeLine: 23, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !2)
+!8 = distinct !DISubprogram(name: "foo", linkageName: "_Z3foov", scope: !1, file: !1, line: 23, type: !9, scopeLine: 23, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !2)
 !9 = !DISubroutineType(types: !2)
 !10 = !DIDerivedType(tag: DW_TAG_typedef, name: "promise_type", scope: !8, file: !1, line: 15, baseType: !11)
 !11 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "promise_type", scope: !8, file: !1, line: 10, size: 128, flags: DIFlagTypePassByValue | DIFlagNonTrivial, elements: !12, identifier: "_ZTSN4coro12promise_typeE")
@@ -388,7 +388,7 @@ declare void @final_suspend()
 !16 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
 !17 = !DIBasicType(name: "double", size: 64, encoding: DW_ATE_float)
 !18 = !DILocation(line: 8, scope: !7)
-!19 = distinct !DISubprogram(name: "bar", linkageName: "_Z3barv", scope: !19, file: !1, line: 54, type: !9, scopeLine: 54, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !2)
+!19 = distinct !DISubprogram(name: "bar", linkageName: "_Z3barv", scope: !1, file: !1, line: 54, type: !9, scopeLine: 54, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !2)
 !20 = distinct !DILexicalBlock(scope: !19, file: !1, line: 23, column: 12)
 !21 = !DILocalVariable(name: "__promise", scope: !20, file: !1, line: 55, type: !10)
 !22 = !DILocation(line: 10, scope: !20)
diff --git a/llvm/test/Verifier/DILocalScope-cycle.ll b/llvm/test/Verifier/DILocalScope-cycle.ll
new file mode 100644
index 0000000000000..2164895d1aab8
--- /dev/null
+++ b/llvm/test/Verifier/DILocalScope-cycle.ll
@@ -0,0 +1,27 @@
+; RUN: llvm-as -disable-output %s 2>&1 | FileCheck %s
+
+; Reject a cycle in a DILocalScope chain instead of looping indefinitely while
+; looking for the enclosing DISubprogram.
+
+; CHECK: DIScope scope chain must not contain a cycle
+; CHECK: distinct !DILexicalBlock(scope: ![[BLOCK2:[0-9]+]]
+; CHECK: warning: ignoring invalid debug info
+
+define void @f() !dbg !5 {
+entry:
+  ret void, !dbg !11
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, emissionKind: FullDebug)
+!1 = !DIFile(filename: "scope-cycle.c", directory: "/")
+!2 = !{i32 2, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !DISubroutineType(types: !6)
+!5 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 1, type: !4, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!6 = !{null}
+!9 = distinct !DILexicalBlock(scope: !10, file: !1, line: 2, column: 3)
+!10 = distinct !DILexicalBlock(scope: !9, file: !1, line: 4, column: 5)
+!11 = !DILocation(line: 2, column: 3, scope: !9)
diff --git a/llvm/test/Verifier/DIScope-cycle.ll b/llvm/test/Verifier/DIScope-cycle.ll
new file mode 100644
index 0000000000000..85c14d63e6e28
--- /dev/null
+++ b/llvm/test/Verifier/DIScope-cycle.ll
@@ -0,0 +1,32 @@
+; RUN: llvm-as -disable-output %s 2>&1 | FileCheck %s
+
+; Reject a cycle in a DIScope parent chain (namespaces, modules, ...).
+
+; CHECK: DIScope scope chain must not contain a cycle
+; CHECK: !DINamespace(name: "a", scope: ![[NSB:[0-9]+]])
+; CHECK: DIScope scope chain must not contain a cycle
+; CHECK: !DIModule(scope: !{{[0-9]+}}, name: "m1"
+; CHECK: warning: ignoring invalid debug info
+
+ at g = global i32 0, !dbg !13
+ at h = global i32 0, !dbg !16
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !1, emissionKind: FullDebug, globals: !12)
+!1 = !DIFile(filename: "scope-cycle.cpp", directory: "/")
+!2 = !{i32 2, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+
+!7 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!8 = !DINamespace(name: "a", scope: !9)
+!9 = !DINamespace(name: "b", scope: !8)
+!10 = !DIModule(scope: !11, name: "m1")
+!11 = !DIModule(scope: !10, name: "m2")
+
+!12 = !{!13, !16}
+!13 = !DIGlobalVariableExpression(var: !14, expr: !DIExpression())
+!14 = distinct !DIGlobalVariable(name: "g", scope: !8, file: !1, line: 1, type: !7, isDefinition: true)
+!16 = !DIGlobalVariableExpression(var: !17, expr: !DIExpression())
+!17 = distinct !DIGlobalVariable(name: "h", scope: !10, file: !1, line: 2, type: !7, isDefinition: true)

>From 9de31fd14976d309209c962d71f09c457b7c72d7 Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Fri, 21 Aug 2026 12:33:26 -0500
Subject: [PATCH 2/2] [Verifier] Diagnose local scope chains that miss their
 DISubprogram

DILocalScope::getSubprogram() casts every parent to DILocalScope, so a
DILexicalBlock parented to a non-local scope such as a DIFile makes the
verifier abort on that cast before visitDILexicalBlockBase can report
"invalid local scope".
---
 llvm/lib/IR/Verifier.cpp                      | 17 +++++------
 .../Verifier/DILocalScope-non-local-parent.ll | 28 +++++++++++++++++++
 2 files changed, 37 insertions(+), 8 deletions(-)
 create mode 100644 llvm/test/Verifier/DILocalScope-non-local-parent.ll

diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index 02f20a2527f60..64eacd70c3a6f 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -1618,7 +1618,7 @@ void Verifier::visitDISubprogram(const DISubprogram &N) {
               "invalid retained nodes, retained node is not local", &N, Node,
               RetainedNode);
 
-      DISubprogram *RetainedNodeSP = RetainedNodeScope->getSubprogram();
+      DISubprogram *RetainedNodeSP = getSubprogram(RetainedNodeScope);
       DICompileUnit *RetainedNodeUnit =
           RetainedNodeSP ? RetainedNodeSP->getUnit() : nullptr;
       CheckDI(
@@ -3444,7 +3444,9 @@ void Verifier::visitFunction(const Function &F) {
     if (hasDIScopeCycle(Scope))
       return;
 
-    DISubprogram *SP = Scope->getSubprogram();
+    DISubprogram *SP = getSubprogram(Scope);
+    CheckDI(SP, "DILocalScope scope chain must terminate at a DISubprogram", DL,
+            Scope);
 
     // Scope and SP could be the same MDNode and we don't want to skip
     // validation in that case
@@ -6057,10 +6059,11 @@ void Verifier::visitInstruction(Instruction &I) {
 
     if (auto *DL = dyn_cast<DILocation>(N)) {
       if (DL->getAtomGroup()) {
-        CheckDI(DL->getScope()->getSubprogram()->getKeyInstructionsEnabled(),
+        DISubprogram *SP = getSubprogram(DL->getRawScope());
+        CheckDI(SP && SP->getKeyInstructionsEnabled(),
                 "DbgLoc uses atomGroup but DISubprogram doesn't have Key "
                 "Instructions enabled",
-                DL, DL->getScope()->getSubprogram());
+                DL, SP);
       }
     }
   }
@@ -7261,8 +7264,7 @@ void Verifier::visit(DbgLabelRecord &DLR) {
 
   CheckDI(LabelSP == LocSP,
           "mismatched subprogram between #dbg_label label and !dbg attachment",
-          &DLR, BB, F, Label, Label->getScope()->getSubprogram(), Loc,
-          Loc->getScope()->getSubprogram());
+          &DLR, BB, F, Label, LabelSP, Loc, LocSP);
 }
 
 void Verifier::visit(DbgVariableRecord &DVR) {
@@ -7351,8 +7353,7 @@ void Verifier::visit(DbgVariableRecord &DVR) {
 
   CheckDI(VarSP == LocSP,
           "mismatched subprogram between #dbg record variable and DILocation",
-          &DVR, BB, F, Var, Var->getScope()->getSubprogram(), Loc,
-          Loc->getScope()->getSubprogram(), BB, F);
+          &DVR, BB, F, Var, VarSP, Loc, LocSP, BB, F);
 
   verifyFnArgs(DVR);
 }
diff --git a/llvm/test/Verifier/DILocalScope-non-local-parent.ll b/llvm/test/Verifier/DILocalScope-non-local-parent.ll
new file mode 100644
index 0000000000000..b60f2c155d57b
--- /dev/null
+++ b/llvm/test/Verifier/DILocalScope-non-local-parent.ll
@@ -0,0 +1,28 @@
+; RUN: llvm-as -disable-output %s 2>&1 | FileCheck %s
+
+; The DILexicalBlock's parent is a DIFile rather than a DISubprogram. The chain
+; is acyclic, so it must be diagnosed instead of asserting in
+; DILocalScope::getSubprogram(), which casts every parent to DILocalScope.
+
+; CHECK: DILocalScope scope chain must terminate at a DISubprogram
+; CHECK: distinct !DILexicalBlock(scope: ![[FILE:[0-9]+]]
+; CHECK: invalid local scope
+; CHECK: warning: ignoring invalid debug info
+
+define void @f() !dbg !5 {
+entry:
+  ret void, !dbg !8
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, emissionKind: FullDebug)
+!1 = !DIFile(filename: "non-local-parent.c", directory: "/")
+!2 = !{i32 2, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !DISubroutineType(types: !6)
+!5 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 1, type: !4, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!6 = !{null}
+!7 = distinct !DILexicalBlock(scope: !1, file: !1, line: 2, column: 3)
+!8 = !DILocation(line: 2, column: 3, scope: !7)



More information about the llvm-commits mailing list