[llvm] [DWARFLinker][Parallel] Identify a unit root by position, not by tag (PR #219615)

Arcadiy Ivanov via llvm-commits llvm-commits at lists.llvm.org
Fri Aug 28 19:32:29 PDT 2026


https://github.com/arcivanov created https://github.com/llvm/llvm-project/pull/219615


`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 exactly the same path a compile unit root does — `DWARFContext::compile_units()` filters out type units only.

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. Two failures follow:

1. The first gate lets the partial unit root through, `createTypeDIEandCloneAttributes()` asks for its type entry, and gets the null that every unit root holds — `Assertion 'Entry != nullptr' failed` where assertions are on, a null dereference and SIGSEGV where they are not.
2. Where the root's only kept content is types, the second gate then drops the entire subtree after its type names have already been registered, and patch application fails later 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 — `getUnitDIE()` is defined as `DieArray[0]` — which is the invariant the tag comparison was standing in for. `DependencyTracker` already identifies the root that way, and the producer side is already tag-agnostic: `getTypeDeduplicationCandidate()` returns `nullopt` for every unit tag, and `assignTypeNames()` names only children of `getDebugInfoEntry(0)`. This makes the consumer match.

Note, that only the parallel backend is affected. Classic leaves the type where it found it and does not go near this code.

Two lit tests are added, one per gate. Each fails on the unfixed tree and passes with the fix, and the second still fails when only the first gate is corrected. Both link a `DW_TAG_compile_unit` root alongside the partial unit as the control, so compile unit output is pinned to be unchanged.

Fixes #219613.


>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] [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
+...



More information about the llvm-commits mailing list