[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