[llvm] [DWARFLinker][Parallel] Keep imports under any unit root in plain DWARF (PR #219732)
Arcadiy Ivanov via llvm-commits
llvm-commits at lists.llvm.org
Sat Aug 29 15:43:53 PDT 2026
https://github.com/arcivanov created https://github.com/llvm/llvm-project/pull/219732
Depends on #219615.
An imported unit, module or declaration entry is kept in plain DWARF when it sits directly under the unit root, and stays a candidate for the artificial type unit everywhere else. `collectRootsToKeep` decided between plain DWARF and the type table by asking whether the DIE holding the import was tagged `DW_TAG_compile_unit`.
`DW_TAG_partial_unit` is the root tag `dwz` emits, and in multifile mode the partial units `dwz` produces import one another, so this is the ordinary shape of a Fedora, RHEL or Debian debuginfo package. A partial unit root is not tagged `DW_TAG_compile_unit`, so the import under it is given type table placement, cloned into `__artificial_type_unit`, and cloning it there asks for the type name of its `DW_AT_import` target. That target is a unit root, which is never assigned a type name — an assertion in `cloneDieRefAttr` where assertions are on, a null dereference and SIGSEGV where they are not. Reaching that crash also needs ODR deduplication to have a type to move, so a partial unit on its own is not enough; the reproducer in the issue segfaults stock `llvm-dwarfutil` 22.1.8 and 23.1.0.
`collectRootsToKeep` now tests the DIE index instead of the tag. The unit root is always at index zero in `DWARFUnit`'s DIE array whatever tag the root carries — `getUnitDIE()` is defined as `DieArray[0]` — which is the invariant the tag comparison stood in for. `CompileUnit::isUnitRootDIE` is the helper #219615 introduces for the two `cloneDIE()` gates, reused here rather than respelled.
Only the parallel backend is affected. Classic leaves the type where it found it and does not go near this code.
The test links a `DW_TAG_compile_unit` root alongside as the control and pins both halves of the placement decision: two imported unit entries under unit roots, which have to stay in plain DWARF, and a using-declaration nested in a namespace, which has to remain a type table candidate. The nested half is worth the extra input, because it had no coverage anywhere in the tree — dropping the condition outright, so that every import is kept in plain DWARF, still passed all 182 existing `llvm-dwarfutil` and `dsymutil` tests. Against the input added here, that same variant fails on both roots.
The classic backend is not linked in the test. Classic aborts on this input for an unrelated reason, rewriting `DW_AT_import` to the referenced unit's start offset rather than to its root DIE, and it does so whatever tag the root carries, so it fails the control too. That defect is #219621.
Fixes #219622.
>From be6e72d198c28c4321ca528fbe946e4d26229059 Mon Sep 17 00:00:00 2001
From: Arcadiy Ivanov <arcadiy at ivanov.biz>
Date: Fri, 28 Aug 2026 22:31:55 -0400
Subject: [PATCH 1/2] [DWARFLinker][Parallel] Identify a unit root by position,
not by tag
cloneDIE() has two gates that must recognize a unit's root DIE. The root
is never assigned a type entry, so it must not be cloned into the
artificial type unit; and because it is not cloned there, the recursion
into its type children has to be enabled by a separate test. Both gates
asked whether the tag was DW_TAG_compile_unit.
A DW_TAG_partial_unit root answers no to both. That is the unit dwz
emits, so it arrives through any Fedora, RHEL or Debian debuginfo
package, and it reaches the linker on the same path a compile unit root
does. Marking a type for the artificial type unit sets KeepTypeChildren
on every ancestor of that type, unit root included, and
needToPlaceInTypeTable() is true for anything carrying that flag, so the
first gate let the partial unit root through and
createTypeDIEandCloneAttributes() asked for the type entry that no unit
root is ever assigned - an assertion failure on Entry != nullptr with
assertions enabled, a null dereference without them. Where the root's
only kept content is types, the second gate then dropped the whole
subtree after its type names had already been registered, which fails
later at patch application with "No data for type <name>".
Both gates now test the DIE index instead of the tag. The root is always
at index zero in DWARFUnit's DIE array whatever its tag, which is the
invariant the tag comparison was standing in for. The producer side is
already tag-agnostic - getTypeDeduplicationCandidate() returns nullopt
for every unit tag and assignTypeNames() names only children of
getDebugInfoEntry(0) - so this makes the consumer match.
Only the parallel backend is affected.
Fixes #219613.
Assisted-by: Claude Code
---
.../Parallel/DWARFLinkerCompileUnit.cpp | 6 +-
.../Parallel/DWARFLinkerCompileUnit.h | 5 +
.../X86/dwarf5-partial-unit-namespace.test | 178 ++++++++++++++++++
.../X86/dwarf5-partial-unit-types-only.test | 168 +++++++++++++++++
4 files changed, 353 insertions(+), 4 deletions(-)
create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-namespace.test
create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
index 4138a9fd69aca..126cac340a49d 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
@@ -1419,8 +1419,7 @@ std::pair<DIE *, TypeEntry *> CompileUnit::cloneDIE(
bool NeedToClonePlainDIE = Info.needToKeepInPlainDwarf();
bool NeedToCloneTypeDIE =
- (InputDieEntry->getTag() != dwarf::DW_TAG_compile_unit) &&
- Info.needToPlaceInTypeTable();
+ !isUnitRootDIE(InputDieIdx) && Info.needToPlaceInTypeTable();
std::pair<DIE *, TypeEntry *> ClonedDIE;
DIEGenerator PlainDIEGenerator(Allocator, *this);
@@ -1449,8 +1448,7 @@ std::pair<DIE *, TypeEntry *> CompileUnit::cloneDIE(
(ClonedDIE.first && Info.getKeepPlainChildren());
bool HasTypeChildrenToClone =
- ((ClonedDIE.second ||
- InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit) &&
+ ((ClonedDIE.second || isUnitRootDIE(InputDieIdx)) &&
Info.getKeepTypeChildren());
// Recursively clone children.
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
index c8c1b8822aa64..70b90727f4df4 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
@@ -335,6 +335,11 @@ class alignas(8) CompileUnit : public DwarfUnit {
///
/// @{
+ /// \p DieIdx index of the DIE.
+ /// \returns true if the DIE is the unit's root, which is always at index
+ /// zero, whatever tag it carries.
+ static bool isUnitRootDIE(uint32_t DieIdx) { return DieIdx == 0; }
+
/// \p Idx index of the DIE.
/// \returns DieInfo descriptor.
DIEInfo &getDIEInfo(unsigned Idx) { return DieInfoArray[Idx]; }
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-namespace.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-namespace.test
new file mode 100644
index 0000000000000..357d829a34b2c
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-namespace.test
@@ -0,0 +1,178 @@
+## A DW_TAG_partial_unit root - what dwz emits, and what Fedora, RHEL and
+## Debian debuginfo packaging therefore ships - was kept out of the artificial
+## type unit by nothing but a test on its tag, so it was cloned as a type DIE
+## and asked for the type entry that no unit root is ever assigned. Reaching
+## that point needs a scope between the type and the unit root, because the
+## walk that looks for a type's enclosing scope stops at a namespace but runs
+## past a unit root. A DW_TAG_compile_unit root is linked alongside as the
+## control.
+##
+## namespace ns {
+## struct S { int m1; };
+## }
+##
+## ns::S foo1() { ... }
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -o %t.cu.o
+# RUN: llvm-dwarfutil %t.cu.o %t.cu.classic
+# RUN: llvm-dwarfdump --debug-info %t.cu.classic | FileCheck %s --check-prefixes=CLASSIC,CLASSIC-CU
+# RUN: llvm-dwarfutil --linker parallel %t.cu.o %t.cu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.cu.parallel | FileCheck %s --check-prefixes=PARALLEL,PARALLEL-CU
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -o %t.pu.o
+# RUN: llvm-dwarfutil %t.pu.o %t.pu.classic
+# RUN: llvm-dwarfdump --debug-info %t.pu.classic | FileCheck %s --check-prefixes=CLASSIC,CLASSIC-PU
+# RUN: llvm-dwarfutil --linker parallel %t.pu.o %t.pu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.pu.parallel | FileCheck %s --check-prefixes=PARALLEL,PARALLEL-PU
+
+## The classic backend leaves the type where it found it, and the root tag
+## survives linking. llvm-dwarfdump qualifies a type name with the names of its
+## enclosing scopes and a partial unit root is one, which a compile unit root
+## is not, so the type is printed as "U01::ns::S" for the partial unit run and
+## as "ns::S" for the control. The reference target is the same DIE either way.
+# CLASSIC-CU: DW_TAG_compile_unit
+# CLASSIC-PU: DW_TAG_partial_unit
+# CLASSIC: DW_TAG_subprogram
+# CLASSIC-NEXT: DW_AT_name ("foo1")
+# CLASSIC-CU: DW_AT_type (0x00000000[[S:[0-9a-f]{8}]] "ns::S")
+# CLASSIC-PU: DW_AT_type (0x00000000[[S:[0-9a-f]{8}]] "U01::ns::S")
+# CLASSIC: DW_TAG_namespace
+# CLASSIC-NEXT: DW_AT_name ("ns")
+# CLASSIC: 0x[[S]]: DW_TAG_structure_type
+# CLASSIC-NEXT: DW_AT_name ("S")
+# CLASSIC: DW_TAG_member
+# CLASSIC-NEXT: DW_AT_name ("m1")
+
+## The parallel backend puts the type in the artificial type unit, under a copy
+## of the namespace that held it, and the unit's reference resolves there. The
+## unit root itself stays out of that unit, which is what this pins: it has no
+## name of its own to be deduplicated under, and cloning it as a type DIE is
+## what used to crash.
+# PARALLEL: DW_AT_name ("__artificial_type_unit")
+# PARALLEL: DW_TAG_namespace
+# PARALLEL-NEXT: DW_AT_name ("ns")
+# PARALLEL: 0x[[S:[0-9a-f]{8}]]: DW_TAG_structure_type
+# PARALLEL-NEXT: DW_AT_name ("S")
+# PARALLEL: DW_TAG_member
+# PARALLEL-NEXT: DW_AT_name ("m1")
+# PARALLEL-CU: DW_TAG_compile_unit
+# PARALLEL-PU: DW_TAG_partial_unit
+# PARALLEL-NEXT: DW_AT_producer ("by_hand")
+# PARALLEL-NEXT: DW_AT_name ("U01")
+# PARALLEL: DW_TAG_subprogram
+# PARALLEL-NEXT: DW_AT_name ("foo1")
+# PARALLEL: DW_AT_type (0x00000000[[S]] "ns::S")
+
+## One DWARFv5 unit with a function in .text returning struct S, which is
+## defined in namespace ns. The root carries its own DW_AT_str_offsets_base, so
+## the strings the linker rewrites into DW_FORM_strx stay resolvable on output;
+## the linker does not synthesize that attribute for a partial unit root by
+## itself.
+--- !ELF
+FileHeader:
+ Class: ELFCLASS64
+ Data: ELFDATA2LSB
+ Type: ET_REL
+ Machine: EM_X86_64
+Sections:
+ - Name: .text
+ Type: SHT_PROGBITS
+ Flags: [ SHF_ALLOC, SHF_EXECINSTR ]
+ Address: 0x1130
+ Size: 0x10
+ - Name: .debug_str_offsets
+ Type: SHT_PROGBITS
+ Flags: [ ]
+ Content: "0400000005000000"
+DWARF:
+ debug_abbrev:
+ - Table:
+ - Tag: [[ROOT]]
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_producer
+ Form: DW_FORM_string
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_language
+ Form: DW_FORM_data2
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_str_offsets_base
+ Form: DW_FORM_sec_offset
+ - Tag: DW_TAG_subprogram
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_type
+ Form: DW_FORM_ref4
+ - Tag: DW_TAG_namespace
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Tag: DW_TAG_structure_type
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_byte_size
+ Form: DW_FORM_data1
+ - Tag: DW_TAG_member
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_type
+ Form: DW_FORM_ref4
+ - Attribute: DW_AT_data_member_location
+ Form: DW_FORM_data1
+ - Tag: DW_TAG_base_type
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ debug_info:
+ - Version: 5
+ UnitType: [[UNITTYPE]]
+ Entries:
+ - AbbrCode: 1
+ Values:
+ - CStr: by_hand
+ - CStr: U01
+ - Value: 0x04
+ - Value: 0x1130
+ - Value: 0x10
+ - Value: 0x8
+ - AbbrCode: 2
+ Values:
+ - CStr: foo1
+ - Value: 0x1130
+ - Value: 0x10
+ - Value: 0x4d
+ - AbbrCode: 3
+ Values:
+ - CStr: ns
+ - AbbrCode: 4
+ Values:
+ - CStr: S
+ - Value: 0x8
+ - AbbrCode: 5
+ Values:
+ - CStr: m1
+ - Value: 0x5c
+ - Value: 0x0
+ - AbbrCode: 0
+ - AbbrCode: 0
+ - AbbrCode: 6
+ Values:
+ - CStr: int
+ - AbbrCode: 0
+...
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test
new file mode 100644
index 0000000000000..312b8d0e1921c
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test
@@ -0,0 +1,168 @@
+## A unit root is never assigned a type entry, so it is never cloned into the
+## artificial type unit and the recursion into its type children is allowed by
+## a separate test - which named DW_TAG_compile_unit only. A
+## DW_TAG_partial_unit root holding types and no code, the unit dwz produces to
+## carry the definitions it hoisted out of the compile units, therefore reached
+## neither branch and its subtree was dropped after the type name had already
+## been registered. A DW_TAG_compile_unit root is linked alongside as the
+## control.
+##
+## U01: namespace ns { struct S {}; }
+## U02: ns::S foo1() { ... } // DW_FORM_ref_addr reference into U01
+##
+## The classic backend is not linked here. It fails on this input for an
+## unrelated reason, a cross-unit reference to a DIE it never gave an output
+## unit, which is a separate defect from the one this test covers.
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -o %t.cu.o
+# RUN: llvm-dwarfutil --linker parallel %t.cu.o %t.cu.out
+# RUN: llvm-dwarfdump --debug-info %t.cu.out | FileCheck %s --implicit-check-not='DW_AT_name ("U01")'
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -o %t.pu.o
+# RUN: llvm-dwarfutil --linker parallel %t.pu.o %t.pu.out
+# RUN: llvm-dwarfdump --debug-info %t.pu.out | FileCheck %s --check-prefixes=CHECK,CHECK-PU
+
+## The type reaches the artificial type unit under a copy of the namespace that
+## held it, and the second unit's reference resolves into the type unit rather
+## than to a dropped DIE.
+# CHECK: DW_AT_name ("__artificial_type_unit")
+# CHECK: DW_TAG_namespace
+# CHECK-NEXT: DW_AT_name ("ns")
+# CHECK: 0x[[S:[0-9a-f]{8}]]: DW_TAG_structure_type
+# CHECK-NEXT: DW_AT_name ("S")
+# CHECK-NEXT: DW_AT_byte_size (0x08)
+
+## The first unit is emptied either way, since its only child moved to the type
+## unit, but the two roots are then treated differently: a compile unit with
+## nothing left is dropped whole, while a partial unit root is still emitted.
+## That difference is the linker's existing behaviour for a unit with no live
+## contents, not something this fix introduces, so it is pinned rather than
+## made uniform.
+# CHECK-PU: DW_TAG_partial_unit
+# CHECK-PU-NEXT: DW_AT_producer ("by_hand")
+# CHECK-PU-NEXT: DW_AT_name ("U01")
+# CHECK: DW_TAG_compile_unit
+# CHECK-NEXT: DW_AT_producer ("by_hand")
+# CHECK-NEXT: DW_AT_name ("U02")
+# CHECK: DW_TAG_subprogram
+# CHECK-NEXT: DW_AT_name ("foo1")
+# CHECK: DW_AT_type (0x00000000[[S]] "ns::S")
+
+## Two DWARFv5 units. The first defines struct S in namespace ns and has no
+## code of its own; the second has the only function, which returns S through a
+## DW_FORM_ref_addr reference into the first. S is deliberately memberless, so
+## that the first unit holds nothing the linker would keep in plain DWARF - a
+## member would pull in a DW_TAG_base_type, which is always kept, and that
+## alone would give the root plain children to descend for. Each root carries
+## its own DW_AT_str_offsets_base, so the strings the linker rewrites into
+## DW_FORM_strx stay resolvable on output; the linker does not synthesize that
+## attribute for a partial unit root by itself.
+--- !ELF
+FileHeader:
+ Class: ELFCLASS64
+ Data: ELFDATA2LSB
+ Type: ET_REL
+ Machine: EM_X86_64
+Sections:
+ - Name: .text
+ Type: SHT_PROGBITS
+ Flags: [ SHF_ALLOC, SHF_EXECINSTR ]
+ Address: 0x1130
+ Size: 0x10
+ - Name: .debug_str_offsets
+ Type: SHT_PROGBITS
+ Flags: [ ]
+ Content: "0400000005000000"
+DWARF:
+ debug_abbrev:
+ - ID: 0
+ Table:
+ - Tag: [[ROOT]]
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_producer
+ Form: DW_FORM_string
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_language
+ Form: DW_FORM_data2
+ - Attribute: DW_AT_str_offsets_base
+ Form: DW_FORM_sec_offset
+ - Tag: DW_TAG_namespace
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Tag: DW_TAG_structure_type
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_byte_size
+ Form: DW_FORM_data1
+ - Tag: DW_TAG_compile_unit
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_producer
+ Form: DW_FORM_string
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_language
+ Form: DW_FORM_data2
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_str_offsets_base
+ Form: DW_FORM_sec_offset
+ - Tag: DW_TAG_subprogram
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_type
+ Form: DW_FORM_ref_addr
+ debug_info:
+ - Version: 5
+ UnitType: [[UNITTYPE]]
+ AbbrevTableID: 0
+ Entries:
+ - AbbrCode: 1
+ Values:
+ - CStr: by_hand
+ - CStr: U01
+ - Value: 0x04
+ - Value: 0x8
+ - AbbrCode: 2
+ Values:
+ - CStr: ns
+ - AbbrCode: 3
+ Values:
+ - CStr: S
+ - Value: 0x8
+ - AbbrCode: 0
+ - AbbrCode: 0
+ - Version: 5
+ UnitType: DW_UT_compile
+ AbbrevTableID: 0
+ Entries:
+ - AbbrCode: 4
+ Values:
+ - CStr: by_hand
+ - CStr: U02
+ - Value: 0x04
+ - Value: 0x1130
+ - Value: 0x10
+ - Value: 0x8
+ - AbbrCode: 5
+ Values:
+ - CStr: foo1
+ - Value: 0x1130
+ - Value: 0x10
+ - Value: 0x23
+ - AbbrCode: 0
+...
>From f04ba4f4d047867076e2137f1ac4eff0de616990 Mon Sep 17 00:00:00 2001
From: Arcadiy Ivanov <arcadiy at ivanov.biz>
Date: Sat, 29 Aug 2026 18:43:23 -0400
Subject: [PATCH 2/2] [DWARFLinker][Parallel] Keep imports under any unit root
in plain DWARF
An imported unit, module or declaration entry is kept in plain DWARF
when it sits directly under the unit root, and stays a candidate for
the artificial type unit everywhere else. collectRootsToKeep decided
between plain DWARF and the type table by asking whether the DIE
holding the import was tagged DW_TAG_compile_unit.
DW_TAG_partial_unit is the root tag dwz emits, and in multifile mode
the partial units dwz produces import one another, so this is the
ordinary shape of a Fedora, RHEL or Debian debuginfo package. A
partial unit root is not tagged DW_TAG_compile_unit, so the import
under it is given type table placement, cloned into
__artificial_type_unit, and cloning it there asks for the type name of
its DW_AT_import target. That target is a unit root, which is never
assigned a type name: an assertion in cloneDieRefAttr where assertions
are on, a null dereference and SIGSEGV where they are not. Reaching
that crash also needs ODR deduplication to have a type to move, so a
partial unit on its own is not enough.
collectRootsToKeep now tests the DIE index instead of the tag. The
unit root is always at index zero in DWARFUnit's DIE array whatever
tag the root carries, which is the invariant the tag comparison stood
in for.
Only the parallel backend is affected. Classic leaves the type where
it found it and does not go near this code.
The test links a DW_TAG_compile_unit root alongside as the control and
pins both halves of the placement decision: two imported unit entries
under unit roots, which have to stay in plain DWARF, and a
using-declaration nested in a namespace, which has to remain a type
table candidate. The nested half had no coverage anywhere in the tree
- dropping the condition outright, so that every import is kept in
plain DWARF, still passed every existing llvm-dwarfutil and dsymutil
test.
Fixes #219622.
Assisted-by: Claude Code
---
.../Parallel/DependencyTracker.cpp | 6 +-
...dwarf5-partial-unit-imported-unit-odr.test | 222 ++++++++++++++++++
2 files changed, 226 insertions(+), 2 deletions(-)
create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-imported-unit-odr.test
diff --git a/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp b/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp
index 669ea2ceb16c9..7cacc3fc42dc4 100644
--- a/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp
@@ -220,8 +220,10 @@ void DependencyTracker::collectRootsToKeep(
case dwarf::DW_TAG_imported_module:
case dwarf::DW_TAG_imported_declaration:
case dwarf::DW_TAG_imported_unit: {
- // Always keep DIEs having DW_AT_import attribute.
- if (Entry.DieEntry->getTag() == dwarf::DW_TAG_compile_unit) {
+ // Always keep DIEs having DW_AT_import attribute. An import placed
+ // directly under the unit root belongs in plain DWARF whatever tag that
+ // root carries, so the root is identified by position.
+ if (CompileUnit::isUnitRootDIE(Entry.CU->getDIEIndex(Entry.DieEntry))) {
addActionToRootEntriesWorkList(
LiveRootWorklistActionTy::MarkSingleLiveEntry, ChildEntry,
ReferencedBy);
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-imported-unit-odr.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-imported-unit-odr.test
new file mode 100644
index 0000000000000..c5cf5a6757776
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-imported-unit-odr.test
@@ -0,0 +1,222 @@
+## An import sitting directly under a unit root is kept in plain DWARF, and
+## everywhere else it stays a candidate for the artificial type unit. The
+## linker chose between plain DWARF and the type table by the tag of the DIE
+## holding the import, and only DW_TAG_compile_unit was recognized there.
+## Under a DW_TAG_partial_unit root - the shape dwz produces in multifile mode,
+## where the partial units it emits import one another - an imported unit entry
+## was given type table placement instead, and cloning it there asks for the
+## type name of its DW_AT_import target. That target is a unit root, which is
+## never assigned a type name. A DW_TAG_compile_unit root is linked alongside
+## as the control.
+##
+## U01: struct T {};
+## namespace ns { struct S {}; using ::T; }
+## U02: DW_TAG_imported_unit -> U01
+## U03: DW_TAG_imported_unit -> U02
+## ns::S foo1() { ... } // DW_FORM_ref_addr reference into U01
+##
+## Reaching the failure needs ODR deduplication to have a type to move, which
+## is what foo1 and its return type are for; with nothing referencing ns::S no
+## artificial type unit is built and the import is never cloned into one. The
+## using-declaration nested in the namespace is the other half of the same
+## decision, and is the only coverage in the tree for an import that must stay
+## a type table candidate.
+##
+## The classic backend is not linked here. It aborts on this input for an
+## unrelated reason, rewriting DW_AT_import to the referenced unit's start
+## offset rather than to its root DIE, and it does so whatever tag the root
+## carries, so it fails the control too.
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -o %t.cu.o
+# RUN: llvm-dwarfutil --linker parallel %t.cu.o %t.cu.out
+# RUN: llvm-dwarfdump --debug-info %t.cu.out | FileCheck %s -DROOT=DW_TAG_compile_unit
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -o %t.pu.o
+# RUN: llvm-dwarfutil --linker parallel %t.pu.o %t.pu.out
+# RUN: llvm-dwarfdump --debug-info %t.pu.out | FileCheck %s -DROOT=DW_TAG_partial_unit
+
+## Both types move into the artificial type unit, so deduplication did engage
+## and the imports were cloned while there was a type unit to clone them into.
+## The using-declaration moves with them and its target is rewritten to the
+## copy of T that landed there.
+# CHECK: DW_AT_name ("__artificial_type_unit")
+# CHECK: DW_TAG_namespace
+# CHECK-NEXT: DW_AT_name ("ns")
+# CHECK: DW_TAG_imported_declaration
+# CHECK-NEXT: DW_AT_import (0x[[T:[0-9a-f]{8}]] "T")
+# CHECK: 0x[[S:[0-9a-f]{8}]]: DW_TAG_structure_type
+# CHECK-NEXT: DW_AT_name ("S")
+# CHECK: 0x[[T]]: DW_TAG_structure_type
+# CHECK-NEXT: DW_AT_name ("T")
+
+## Neither imported unit entry went with them. Both stay in the unit that held
+## them, and both resolve to the root DIE of the unit they name rather than to
+## a DIE that was dropped or never given an offset.
+# CHECK-NOT: DW_TAG_imported_unit
+# CHECK: 0x[[U01:[0-9a-f]{8}]]: [[ROOT]]
+# CHECK-NEXT: DW_AT_producer ("by_hand")
+# CHECK-NEXT: DW_AT_name ("U01")
+# CHECK-NOT: DW_TAG_imported_unit
+# CHECK: 0x[[U02:[0-9a-f]{8}]]: [[ROOT]]
+# CHECK-NEXT: DW_AT_producer ("by_hand")
+# CHECK-NEXT: DW_AT_name ("U02")
+# CHECK: DW_TAG_imported_unit
+# CHECK-NEXT: DW_AT_import (0x00000000[[U01]] "U01")
+# CHECK: DW_TAG_compile_unit
+# CHECK-NEXT: DW_AT_producer ("by_hand")
+# CHECK-NEXT: DW_AT_name ("U03")
+# CHECK: DW_TAG_imported_unit
+# CHECK-NEXT: DW_AT_import (0x00000000[[U02]] "U02")
+# CHECK: DW_TAG_subprogram
+# CHECK-NEXT: DW_AT_name ("foo1")
+# CHECK: DW_AT_type (0x00000000[[S]] "ns::S")
+
+## Three DWARFv5 units. The first defines the types, the second does nothing
+## but import the first, and the third imports the second and holds the only
+## function, which returns S through a DW_FORM_ref_addr reference into the
+## first. Each root carries its own DW_AT_str_offsets_base, so the strings the
+## linker rewrites into DW_FORM_strx stay resolvable on output; the linker does
+## not synthesize that attribute for a partial unit root by itself.
+--- !ELF
+FileHeader:
+ Class: ELFCLASS64
+ Data: ELFDATA2LSB
+ Type: ET_REL
+ Machine: EM_X86_64
+Sections:
+ - Name: .text
+ Type: SHT_PROGBITS
+ Flags: [ SHF_ALLOC, SHF_EXECINSTR ]
+ Address: 0x1130
+ Size: 0x10
+ - Name: .debug_str_offsets
+ Type: SHT_PROGBITS
+ Flags: [ ]
+ Content: "0400000005000000"
+DWARF:
+ debug_abbrev:
+ - ID: 0
+ Table:
+ - Tag: [[ROOT]]
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_producer
+ Form: DW_FORM_string
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_language
+ Form: DW_FORM_data2
+ - Attribute: DW_AT_str_offsets_base
+ Form: DW_FORM_sec_offset
+ - Tag: DW_TAG_namespace
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Tag: DW_TAG_structure_type
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_byte_size
+ Form: DW_FORM_data1
+ - Tag: DW_TAG_compile_unit
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_producer
+ Form: DW_FORM_string
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_language
+ Form: DW_FORM_data2
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_str_offsets_base
+ Form: DW_FORM_sec_offset
+ - Tag: DW_TAG_imported_unit
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_import
+ Form: DW_FORM_ref_addr
+ - Tag: DW_TAG_subprogram
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_type
+ Form: DW_FORM_ref_addr
+ - Tag: DW_TAG_imported_declaration
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_import
+ Form: DW_FORM_ref_addr
+ debug_info:
+ - Version: 5
+ UnitType: [[UNITTYPE]]
+ AbbrevTableID: 0
+ Entries:
+ - AbbrCode: 1
+ Values:
+ - CStr: by_hand
+ - CStr: U01
+ - Value: 0x04
+ - Value: 0x8
+ - AbbrCode: 3
+ Values:
+ - CStr: T
+ - Value: 0x8
+ - AbbrCode: 2
+ Values:
+ - CStr: ns
+ - AbbrCode: 3
+ Values:
+ - CStr: S
+ - Value: 0x8
+ - AbbrCode: 7
+ Values:
+ - Value: 0x1f
+ - AbbrCode: 0
+ - AbbrCode: 0
+ - Version: 5
+ UnitType: [[UNITTYPE]]
+ AbbrevTableID: 0
+ Entries:
+ - AbbrCode: 1
+ Values:
+ - CStr: by_hand
+ - CStr: U02
+ - Value: 0x04
+ - Value: 0x8
+ - AbbrCode: 5
+ Values:
+ - Value: 0x0c
+ - AbbrCode: 0
+ - Version: 5
+ UnitType: DW_UT_compile
+ AbbrevTableID: 0
+ Entries:
+ - AbbrCode: 4
+ Values:
+ - CStr: by_hand
+ - CStr: U03
+ - Value: 0x04
+ - Value: 0x1130
+ - Value: 0x10
+ - Value: 0x8
+ - AbbrCode: 5
+ Values:
+ - Value: 0x3e
+ - AbbrCode: 6
+ Values:
+ - CStr: foo1
+ - Value: 0x1130
+ - Value: 0x10
+ - Value: 0x27
+ - AbbrCode: 0
+...
More information about the llvm-commits
mailing list