[llvm] [Verifier] Reject cyclic DIScope parent chains (PR #217997)

Manuel Carrasco via llvm-commits llvm-commits at lists.llvm.org
Wed Aug 26 03:56:45 PDT 2026


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

>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 d9d3692a9ed894c691466593137fc925176318a6 Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Wed, 26 Aug 2026 05:55:41 -0500
Subject: [PATCH 2/2] Cover missing cases and add new tests.

---
 llvm/lib/IR/Verifier.cpp                      |  7 ++--
 .../Verifier/DILocation-atomgroup-cycle.ll    | 30 +++++++++++++++++
 .../DISubprogram-retained-node-cycle.ll       | 33 +++++++++++++++++++
 llvm/test/Verifier/dbg-record-scope-cycle.ll  | 30 +++++++++++++++++
 4 files changed, 97 insertions(+), 3 deletions(-)
 create mode 100644 llvm/test/Verifier/DILocation-atomgroup-cycle.ll
 create mode 100644 llvm/test/Verifier/DISubprogram-retained-node-cycle.ll
 create mode 100644 llvm/test/Verifier/dbg-record-scope-cycle.ll

diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index 02f20a2527f60..1a8c65e9769d4 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(
@@ -6057,10 +6057,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);
       }
     }
   }
diff --git a/llvm/test/Verifier/DILocation-atomgroup-cycle.ll b/llvm/test/Verifier/DILocation-atomgroup-cycle.ll
new file mode 100644
index 0000000000000..6cc40dd2b9c68
--- /dev/null
+++ b/llvm/test/Verifier/DILocation-atomgroup-cycle.ll
@@ -0,0 +1,30 @@
+; RUN: llvm-as -disable-output %s 2>&1 | FileCheck %s
+
+; A !dbg location with atomGroup set whose scope is a cyclic DILexicalBlock
+; chain that never reaches a DISubprogram. 
+
+; CHECK: DIScope scope chain must not contain a cycle
+; CHECK: distinct !DILexicalBlock(scope: !{{[0-9]+}}
+; CHECK: DIScope scope chain must not contain a cycle
+; CHECK: distinct !DILexicalBlock(scope: !{{[0-9]+}}
+; CHECK: DbgLoc uses atomGroup but DISubprogram doesn't have Key Instructions enabled
+; 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: "atomgroup-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: 3, column: 5)
+!11 = !DILocation(line: 2, column: 3, scope: !9, atomGroup: 1, atomRank: 1)
diff --git a/llvm/test/Verifier/DISubprogram-retained-node-cycle.ll b/llvm/test/Verifier/DISubprogram-retained-node-cycle.ll
new file mode 100644
index 0000000000000..20595fc6a63bd
--- /dev/null
+++ b/llvm/test/Verifier/DISubprogram-retained-node-cycle.ll
@@ -0,0 +1,33 @@
+; RUN: llvm-as -disable-output %s 2>&1 | FileCheck %s
+
+; A retained node (DILocalVariable) whose scope is a cyclic DILexicalBlock
+; chain that never reaches a DISubprogram. 
+
+; CHECK: invalid retained nodes, retained node does not belong to subprogram
+; CHECK: DIScope scope chain must not contain a cycle
+; CHECK: distinct !DILexicalBlock(scope: !{{[0-9]+}}
+; CHECK: DIScope scope chain must not contain a cycle
+; CHECK: distinct !DILexicalBlock(scope: !{{[0-9]+}}
+; CHECK: warning: ignoring invalid debug info
+
+define void @f() !dbg !5 {
+entry:
+  ret void, !dbg !14
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, emissionKind: FullDebug)
+!1 = !DIFile(filename: "retained-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, retainedNodes: !7)
+!6 = !{null}
+!7 = !{!8}
+!8 = !DILocalVariable(name: "x", scope: !9, file: !1, line: 2, type: !13)
+!9 = distinct !DILexicalBlock(scope: !10, file: !1, line: 2, column: 3)
+!10 = distinct !DILexicalBlock(scope: !9, file: !1, line: 3, column: 5)
+!13 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!14 = !DILocation(line: 1, column: 1, scope: !5)
diff --git a/llvm/test/Verifier/dbg-record-scope-cycle.ll b/llvm/test/Verifier/dbg-record-scope-cycle.ll
new file mode 100644
index 0000000000000..0af47dee475ad
--- /dev/null
+++ b/llvm/test/Verifier/dbg-record-scope-cycle.ll
@@ -0,0 +1,30 @@
+; RUN: llvm-as -disable-output %s 2>&1 | FileCheck %s
+
+; Cyclic DILexicalBlock used as a DILocalVariable scope related to a DbgVariableRecord.
+
+; CHECK: DIScope scope chain must not contain a cycle
+; CHECK: warning: ignoring invalid debug info
+
+define void @f() !dbg !5 {
+entry:
+  call void @llvm.dbg.value(metadata i32 0, metadata !12, metadata !DIExpression()), !dbg !13
+  ret void, !dbg !13
+}
+
+declare void @llvm.dbg.value(metadata, metadata, metadata)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, emissionKind: FullDebug)
+!1 = !DIFile(filename: "dbg-record-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)
+!12 = !DILocalVariable(name: "x", scope: !9, file: !1, line: 2, type: !14)
+!13 = !DILocation(line: 2, column: 3, scope: !5)
+!14 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)



More information about the llvm-commits mailing list