[llvm] [DWARFLinker] Keep partial unit roots out of the accelerator tables (PR #219641)

Arcadiy Ivanov via llvm-commits llvm-commits at lists.llvm.org
Sat Oct 3 10:27:32 PDT 2026


https://github.com/arcivanov updated https://github.com/llvm/llvm-project/pull/219641

>From 4e18b5a2b3c997c1bfa70084b7104e9ab9486afb Mon Sep 17 00:00:00 2001
From: Arcadiy Ivanov <arcadiy at ivanov.biz>
Date: Fri, 28 Aug 2026 03:09:13 -0400
Subject: [PATCH 1/4] [DWARFLinker] Key str_offsets_base and unit_type on the
 unit root

Fix DWARFv5 output for a unit whose root is DW_TAG_partial_unit, as
produced by dwz. The classic and parallel linkers rewrite strings into
DW_FORM_strx but add DW_AT_str_offsets_base only to a
DW_TAG_compile_unit root, so every string in a partial unit reads back
empty. Both also write DW_UT_compile into every unit header,
contradicting a partial unit root.

Add DW_AT_str_offsets_base to the unit root whatever its tag, and emit
DW_UT_partial for a DW_TAG_partial_unit root. Its header has the same
layout as DW_UT_compile.

The test links a partial unit with both linkers, with and without
garbage collection, and checks the unit type, the attribute, the strings
and --verify. The update mode runs pass --build-accelerator, because
--no-garbage-collection alone copies the file without linking it.

Fixes #219365.

Assisted-by: Claude Code
---
 llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp  |   4 +-
 .../lib/DWARFLinker/Classic/DWARFStreamer.cpp |   4 +-
 .../Parallel/DIEAttributeCloner.cpp           |   6 +-
 .../DWARFLinker/Parallel/DWARFEmitterImpl.cpp |   4 +-
 .../ELF/X86/dwarf5-partial-unit.test          | 120 ++++++++++++++++++
 5 files changed, 131 insertions(+), 7 deletions(-)
 create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit.test

diff --git a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
index 454143ec001209..9254a33dee60d8 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
@@ -2013,8 +2013,8 @@ DIE *DWARFLinker::DIECloner::cloneDIE(const DWARFDie &InputDIE,
     }
   }
 
-  if (Unit.getOrigUnit().getVersion() >= 5 && !AttrInfo.AttrStrOffsetBaseSeen &&
-      Die->getTag() == dwarf::DW_TAG_compile_unit) {
+  if (Die == Unit.getOutputUnitDIE() && Unit.getOrigUnit().getVersion() >= 5 &&
+      !AttrInfo.AttrStrOffsetBaseSeen) {
     // No DW_AT_str_offsets_base seen, add it to the DIE.
     Die->addValue(DIEAlloc, dwarf::DW_AT_str_offsets_base,
                   dwarf::DW_FORM_sec_offset, DIEInteger(8));
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp b/llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp
index c6ccc71889fc41..26c0bc00a73898 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp
@@ -188,7 +188,9 @@ void DwarfStreamer::emitCompileUnitHeader(CompileUnit &Unit,
   Asm->emitInt16(DwarfVersion);
 
   if (DwarfVersion >= 5) {
-    Asm->emitInt8(dwarf::DW_UT_compile);
+    dwarf::Tag RootTag = Unit.getOutputUnitDIE()->getTag();
+    Asm->emitInt8(RootTag == dwarf::DW_TAG_partial_unit ? dwarf::DW_UT_partial
+                                                        : dwarf::DW_UT_compile);
     Asm->emitInt8(Unit.getOrigUnit().getAddressByteSize());
     // We share one abbreviations table across all units so it's always at the
     // start of the section.
diff --git a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
index 68d7b9d72cf618..67545ee03d15b5 100644
--- a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
@@ -115,10 +115,10 @@ void DIEAttributeCloner::clone() {
   }
 
   // We convert source strings into the indexed form for DWARFv5.
-  // Check if original compile unit already has DW_AT_str_offsets_base
+  // Check if original unit already has DW_AT_str_offsets_base
   // attribute.
-  if (InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit &&
-      InUnit.getVersion() >= 5 && !AttrInfo.HasStringOffsetBaseAttr) {
+  if (InputDIEIdx == 0 && InUnit.getVersion() >= 5 &&
+      !AttrInfo.HasStringOffsetBaseAttr && OutUnit.isCompileUnit()) {
     DebugInfoOutputSection.notePatchWithOffsetUpdate(
         DebugOffsetPatch{AttrOutOffset,
                          &OutUnit->getOrCreateSectionDescriptor(
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFEmitterImpl.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFEmitterImpl.cpp
index 18d94ac2d834ee..efd70eaef0d266 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFEmitterImpl.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFEmitterImpl.cpp
@@ -139,7 +139,9 @@ void DwarfEmitterImpl::emitCompileUnitHeader(DwarfUnit &Unit) {
   Asm->emitInt16(Unit.getVersion());
 
   if (Unit.getVersion() >= 5) {
-    Asm->emitInt8(dwarf::DW_UT_compile);
+    dwarf::Tag RootTag = Unit.getTag();
+    Asm->emitInt8(RootTag == dwarf::DW_TAG_partial_unit ? dwarf::DW_UT_partial
+                                                        : dwarf::DW_UT_compile);
     Asm->emitInt8(Unit.getFormParams().AddrSize);
     // Proper offset to the abbreviations table will be set later.
     Asm->emitInt32(0);
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit.test
new file mode 100644
index 00000000000000..6b9185defb2af6
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit.test
@@ -0,0 +1,120 @@
+## Check that both linkers emit a DWARFv5 partial unit with a DW_UT_partial
+## header and a DW_AT_str_offsets_base.
+
+# RUN: yaml2obj %s -o %t.o
+
+# RUN: llvm-dwarfutil %t.o %t1
+# RUN: llvm-dwarfdump -a --verbose %t1 | FileCheck %s
+# RUN: llvm-dwarfdump --verify %t1 | FileCheck %s --check-prefix=VERIFY
+
+# RUN: llvm-dwarfutil --linker parallel %t.o %t2
+# RUN: llvm-dwarfdump -a --verbose %t2 | FileCheck %s
+# RUN: llvm-dwarfdump --verify %t2 | FileCheck %s --check-prefix=VERIFY
+
+## Without --build-accelerator, --no-garbage-collection does not run the linker.
+# RUN: llvm-dwarfutil --no-garbage-collection --build-accelerator=DWARF %t.o %t3
+# RUN: llvm-dwarfdump -a --verbose %t3 | FileCheck %s
+# RUN: llvm-dwarfdump --verify %t3 | FileCheck %s --check-prefix=VERIFY
+
+# RUN: llvm-dwarfutil --linker parallel --no-garbage-collection --build-accelerator=DWARF %t.o %t4
+# RUN: llvm-dwarfdump -a --verbose %t4 | FileCheck %s
+# RUN: llvm-dwarfdump --verify %t4 | FileCheck %s --check-prefix=VERIFY
+
+#CHECK: .debug_info contents:
+#CHECK: Compile Unit: {{.*}} version = 0x0005, unit_type = DW_UT_partial
+
+#CHECK: DW_TAG_partial_unit
+#CHECK: DW_AT_producer [DW_FORM_strx] {{.*}} "by_hand"
+#CHECK: DW_AT_name [DW_FORM_strx] {{.*}} "PU1"
+#CHECK: DW_AT_str_offsets_base [DW_FORM_sec_offset] (0x00000008)
+
+#CHECK: DW_TAG_subprogram
+#CHECK: DW_AT_name [DW_FORM_strx] {{.*}} "foo1"
+#CHECK: DW_TAG_subprogram
+#CHECK: DW_AT_name [DW_FORM_strx] {{.*}} "foo2"
+
+#VERIFY: No errors.
+
+## DW_AT_ranges keeps the unit alive under garbage collection, and
+## DW_AT_addr_base resolves the ranges the linker rewrites.
+--- !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:            0x30
+  - Name:            .debug_rnglists
+    Type:            SHT_PROGBITS
+    Flags:           [  ]
+    Content:        "1d0000000500080000000000073011000000000000100750110000000000001000"
+  - Name:            .debug_addr
+    Type:            SHT_PROGBITS
+    Flags:           [  ]
+    Content:        "0c000000050008003011000000000000"
+DWARF:
+  debug_abbrev:
+    - Table:
+      - Tag:      DW_TAG_partial_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_ranges
+            Form:      DW_FORM_sec_offset
+          - Attribute: DW_AT_low_pc
+            Form:      DW_FORM_addr
+          - Attribute: DW_AT_addr_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_base_type
+        Children: DW_CHILDREN_no
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+  debug_info:
+    - Version: 5
+      UnitType:   DW_UT_partial
+      Entries:
+        - AbbrCode: 1
+          Values:
+            - CStr: by_hand
+            - CStr: PU1
+            - Value:  0xc
+            - Value:  0x0
+            - Value:  0x8
+        - AbbrCode: 2
+          Values:
+            - CStr: foo1
+            - Value: 0x1130
+            - Value: 0x10
+## 0x5d is the offset of the DW_TAG_base_type DIE below.
+            - Value: 0x5d
+        - AbbrCode: 2
+          Values:
+            - CStr: foo2
+            - Value: 0x1150
+            - Value: 0x10
+            - Value: 0x5d
+        - AbbrCode: 3
+          Values:
+            - CStr: int
+        - AbbrCode: 0
+...

>From e0a8cc06a1580df7bf0271ac751be95c77ed4255 Mon Sep 17 00:00:00 2001
From: Arcadiy Ivanov <arcadiy at ivanov.biz>
Date: Fri, 28 Aug 2026 19:53:25 -0400
Subject: [PATCH 2/4] [DWARFLinker] Key the unit's address range on the unit
 root

Fix the address range of a DW_TAG_partial_unit root. Both linkers
rebuild a unit root's DW_AT_low_pc/DW_AT_high_pc or DW_AT_ranges from
the functions they kept, but only for a DW_TAG_compile_unit root. A
partial unit root keeps its input range, so after garbage collection it
still covers removed functions, and its DW_AT_ranges list is remapped
like an ordinary DIE's, which can leave it empty.

Test for the unit root instead of the tag: the classic linker compares
with the output unit DIE, the parallel linker checks for input DIE
index 0. DW_AT_high_pc is cloned as an address or as a scalar depending
on its form, so both clone functions change.

The tests cover DW_AT_low_pc with DW_AT_high_pc in both forms, a unit
with no live function, and DW_AT_ranges. The root covers three
functions of which only the middle one is live, so its range has to
shrink at both ends. Each test links a partial unit root and a compile
unit control with both linkers and verifies every output that still
holds a function.

Fixes #219392.

Assisted-by: Claude Code
---
 .../Classic/DWARFLinkerCompileUnit.h          |   5 +
 llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp  |  16 +-
 .../Classic/DWARFLinkerCompileUnit.cpp        |   2 +-
 .../Parallel/DIEAttributeCloner.cpp           |  20 +--
 .../DWARFLinker/Parallel/DIEAttributeCloner.h |   3 +
 .../Parallel/DWARFLinkerCompileUnit.cpp       |  10 +-
 .../lib/DWARFLinker/Parallel/OutputSections.h |   4 +-
 .../ELF/X86/dwarf5-partial-unit-pc-range.test | 167 ++++++++++++++++++
 .../ELF/X86/dwarf5-partial-unit-ranges.test   | 113 ++++++++++++
 9 files changed, 309 insertions(+), 31 deletions(-)
 create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-pc-range.test
 create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-ranges.test

diff --git a/llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h b/llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h
index 538572338dc8bc..8847a39c17ec25 100644
--- a/llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h
+++ b/llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h
@@ -145,6 +145,11 @@ class CompileUnit {
     return nullptr;
   }
 
+  /// Check whether \p Die is this unit's root DIE, whatever its tag.
+  bool isUnitRootDIE(const DIE &Die) const {
+    return &Die == getOutputUnitDIE();
+  }
+
   dwarf::Tag getTag() const { return OrigUnit.getUnitDIE().getTag(); }
 
   bool hasODR() const { return HasODR; }
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
index 9254a33dee60d8..3f76b71ebe0c6a 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
@@ -1493,14 +1493,12 @@ unsigned DWARFLinker::DIECloner::cloneAddressAttribute(
     return 0;
   }
 
-  if (InputDIE.getTag() == dwarf::DW_TAG_compile_unit &&
-      AttrSpec.Attr == dwarf::DW_AT_low_pc) {
+  if (Unit.isUnitRootDIE(Die) && AttrSpec.Attr == dwarf::DW_AT_low_pc) {
     if (std::optional<uint64_t> LowPC = Unit.getLowPc())
       Addr = *LowPC;
     else
       return 0;
-  } else if (InputDIE.getTag() == dwarf::DW_TAG_compile_unit &&
-             AttrSpec.Attr == dwarf::DW_AT_high_pc) {
+  } else if (Unit.isUnitRootDIE(Die) && AttrSpec.Attr == dwarf::DW_AT_high_pc) {
     if (uint64_t HighPc = Unit.getHighPc())
       Addr = HighPc;
     else
@@ -1647,8 +1645,7 @@ unsigned DWARFLinker::DIECloner::cloneScalarAttribute(
     Value = *Offset;
     AttrSpec.Form = dwarf::DW_FORM_sec_offset;
     AttrSize = Unit.getOrigUnit().getFormParams().getDwarfOffsetByteSize();
-  } else if (AttrSpec.Attr == dwarf::DW_AT_high_pc &&
-             Die.getTag() == dwarf::DW_TAG_compile_unit) {
+  } else if (AttrSpec.Attr == dwarf::DW_AT_high_pc && Unit.isUnitRootDIE(Die)) {
     std::optional<uint64_t> LowPC = Unit.getLowPc();
     if (!LowPC)
       return 0;
@@ -1667,10 +1664,9 @@ unsigned DWARFLinker::DIECloner::cloneScalarAttribute(
     return 0;
   }
 
-  // A compile unit's high_pc comes from the unit's own linked range and spans
-  // every symbol in it.
-  if (AttrSpec.Attr == dwarf::DW_AT_high_pc &&
-      Die.getTag() != dwarf::DW_TAG_compile_unit)
+  // A unit's high_pc comes from the unit's own linked range and spans every
+  // symbol in it.
+  if (AttrSpec.Attr == dwarf::DW_AT_high_pc && !Unit.isUnitRootDIE(Die))
     Value = constrainHighPC(InputDIE, Value, /*IsLength=*/true, Info.PCOffset,
                             *File.Addresses);
 
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp b/llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp
index 41dfd5a50f6e71..6e3dfbddb926e4 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp
@@ -170,7 +170,7 @@ void CompileUnit::addFunctionRange(uint64_t FuncLowPc, uint64_t FuncHighPc,
 }
 
 void CompileUnit::noteRangeAttribute(const DIE &Die, PatchLocation Attr) {
-  if (Die.getTag() == dwarf::DW_TAG_compile_unit) {
+  if (isUnitRootDIE(Die)) {
     UnitRangeAttribute = Attr;
     return;
   }
diff --git a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
index 67545ee03d15b5..6194bccf0473a7 100644
--- a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
@@ -479,8 +479,7 @@ size_t DIEAttributeCloner::cloneScalarAttr(
 
     Value = *Offset;
     ResultingForm = dwarf::DW_FORM_sec_offset;
-  } else if (AttrSpec.Attr == dwarf::DW_AT_high_pc &&
-             InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit) {
+  } else if (AttrSpec.Attr == dwarf::DW_AT_high_pc && isUnitRootDIE()) {
     if (!OutUnit.isCompileUnit())
       return 0;
 
@@ -505,9 +504,7 @@ size_t DIEAttributeCloner::cloneScalarAttr(
       AttrSpec.Attr == dwarf::DW_AT_start_scope) {
     // Create patch for the range offset value.
     DebugInfoOutputSection.notePatchWithOffsetUpdate(
-        DebugRangePatch{{AttrOutOffset},
-                        InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit},
-        PatchesOffsets);
+        DebugRangePatch{{AttrOutOffset}, isUnitRootDIE()}, PatchesOffsets);
     AttrInfo.HasRanges = true;
   } else if (DWARFAttribute::mayHaveLocationList(AttrSpec.Attr) &&
              dwarf::doesFormBelongToClass(AttrSpec.Form,
@@ -550,10 +547,9 @@ size_t DIEAttributeCloner::cloneScalarAttr(
       !OutUnit.isCompileUnit())
     return 0;
 
-  // A compile unit's high_pc comes from the unit's own linked range and spans
-  // every symbol in it.
-  if (AttrSpec.Attr == dwarf::DW_AT_high_pc &&
-      InputDieEntry->getTag() != dwarf::DW_TAG_compile_unit)
+  // A unit's high_pc comes from the unit's own linked range and spans every
+  // symbol in it.
+  if (AttrSpec.Attr == dwarf::DW_AT_high_pc && !isUnitRootDIE())
     Value = constrainHighPC(Value, /*IsLength=*/true);
 
   auto Result =
@@ -697,14 +693,12 @@ size_t DIEAttributeCloner::cloneAddressAttr(
     return 0;
   }
 
-  if (InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit &&
-      AttrSpec.Attr == dwarf::DW_AT_low_pc) {
+  if (isUnitRootDIE() && AttrSpec.Attr == dwarf::DW_AT_low_pc) {
     if (std::optional<uint64_t> LowPC = OutUnit.getAsCompileUnit()->getLowPc())
       Addr = *LowPC;
     else
       return 0;
-  } else if (InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit &&
-             AttrSpec.Attr == dwarf::DW_AT_high_pc) {
+  } else if (isUnitRootDIE() && AttrSpec.Attr == dwarf::DW_AT_high_pc) {
     if (uint64_t HighPc = OutUnit.getAsCompileUnit()->getHighPc())
       Addr = HighPc;
     else
diff --git a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.h b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.h
index 2a5a2250cc5b28..afe894a9c90041 100644
--- a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.h
+++ b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.h
@@ -140,6 +140,9 @@ class DIEAttributeCloner {
   /// is constrained as well.
   uint64_t constrainHighPC(uint64_t HighPC, bool IsLength);
 
+  /// Returns true if the DIE being cloned is the unit root, whatever its tag.
+  bool isUnitRootDIE() const { return InputDIEIdx == 0; }
+
   /// Returns true if attribute should be skipped.
   bool
   shouldSkipAttribute(DWARFAbbreviationDeclaration::AttributeSpec AttrSpec);
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
index 4138a9fd69aca8..315fb82f24b354 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
@@ -817,10 +817,10 @@ void CompileUnit::cloneAndEmitRangeList(DebugSectionKind RngSectionKind,
     std::optional<AddressRangeValuePair> CachedRange;
     uint64_t OffsetAfterUnitLength = emitRangeListHeader(OutRangeSection);
 
-    DebugRangePatch *CompileUnitRangePtr = nullptr;
+    DebugRangePatch *UnitRangePtr = nullptr;
     DebugInfoSection.ListDebugRangePatch.forEach([&](DebugRangePatch &Patch) {
-      if (Patch.IsCompileUnitRanges) {
-        CompileUnitRangePtr = &Patch;
+      if (Patch.IsUnitRanges) {
+        UnitRangePtr = &Patch;
       } else {
         // Get ranges from the source DWARF corresponding to the current
         // attribute.
@@ -859,10 +859,10 @@ void CompileUnit::cloneAndEmitRangeList(DebugSectionKind RngSectionKind,
       }
     });
 
-    if (CompileUnitRangePtr != nullptr) {
+    if (UnitRangePtr != nullptr) {
       // Emit compile unit ranges last to be binary compatible with classic
       // dsymutil.
-      DebugInfoSection.apply(CompileUnitRangePtr->PatchOffset,
+      DebugInfoSection.apply(UnitRangePtr->PatchOffset,
                              dwarf::DW_FORM_sec_offset,
                              OutRangeSection.OS.tell());
       emitRangeListFragment(LinkedFunctionRanges, OutRangeSection);
diff --git a/llvm/lib/DWARFLinker/Parallel/OutputSections.h b/llvm/lib/DWARFLinker/Parallel/OutputSections.h
index 10488f8e9f2b64..e7c121d2ed3e3b 100644
--- a/llvm/lib/DWARFLinker/Parallel/OutputSections.h
+++ b/llvm/lib/DWARFLinker/Parallel/OutputSections.h
@@ -56,8 +56,8 @@ struct DebugLineStrPatch : SectionPatch {
 /// This structure is used to update range list offset into
 /// .debug_ranges/.debug_rnglists.
 struct DebugRangePatch : SectionPatch {
-  /// Indicates patch which points to immediate compile unit's attribute.
-  bool IsCompileUnitRanges = false;
+  /// Indicates patch which points to the unit root's attribute.
+  bool IsUnitRanges = false;
 };
 
 /// This structure is used to update location list offset into
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-pc-range.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-pc-range.test
new file mode 100644
index 00000000000000..e30e541b983c4f
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-pc-range.test
@@ -0,0 +1,167 @@
+## Check that both linkers set the DW_AT_low_pc/DW_AT_high_pc of a
+## DW_TAG_partial_unit root to the linked range, as for a DW_TAG_compile_unit.
+## The root covers foo1, foo2 and foo3, but only foo2 is linked. DW_AT_high_pc
+## is cloned by different code as an address and as a length, so both are used.
+
+## DW_TAG_compile_unit root, DW_AT_high_pc as an address.
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -DHIGHPCFORM=DW_FORM_addr -DHIGHPC=0x1160 -DFOO2LOW=0x1140 -o %t.cu-addr.o
+# RUN: llvm-dwarfutil %t.cu-addr.o %t.cu-addr.classic
+# RUN: llvm-dwarfdump --debug-info %t.cu-addr.classic | FileCheck %s --check-prefixes=CHECK,CU --implicit-check-not=DW_TAG_subprogram
+# RUN: llvm-dwarfdump --verify %t.cu-addr.classic
+# RUN: llvm-dwarfutil --linker parallel %t.cu-addr.o %t.cu-addr.parallel
+# RUN: llvm-dwarfdump --debug-info %t.cu-addr.parallel | FileCheck %s --check-prefixes=CHECK,CU --implicit-check-not=DW_TAG_subprogram
+# RUN: llvm-dwarfdump --verify %t.cu-addr.parallel
+
+## DW_TAG_compile_unit root, DW_AT_high_pc as a length.
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -DHIGHPCFORM=DW_FORM_data8 -DHIGHPC=0x30 -DFOO2LOW=0x1140 -o %t.cu-len.o
+# RUN: llvm-dwarfutil %t.cu-len.o %t.cu-len.classic
+# RUN: llvm-dwarfdump --debug-info %t.cu-len.classic | FileCheck %s --check-prefixes=CHECK,CU --implicit-check-not=DW_TAG_subprogram
+# RUN: llvm-dwarfdump --verify %t.cu-len.classic
+# RUN: llvm-dwarfutil --linker parallel %t.cu-len.o %t.cu-len.parallel
+# RUN: llvm-dwarfdump --debug-info %t.cu-len.parallel | FileCheck %s --check-prefixes=CHECK,CU --implicit-check-not=DW_TAG_subprogram
+# RUN: llvm-dwarfdump --verify %t.cu-len.parallel
+
+## DW_TAG_partial_unit root, DW_AT_high_pc as an address.
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -DHIGHPCFORM=DW_FORM_addr -DHIGHPC=0x1160 -DFOO2LOW=0x1140 -o %t.pu-addr.o
+# RUN: llvm-dwarfutil %t.pu-addr.o %t.pu-addr.classic
+# RUN: llvm-dwarfdump --debug-info %t.pu-addr.classic | FileCheck %s --check-prefixes=CHECK,PU --implicit-check-not=DW_TAG_subprogram
+# RUN: llvm-dwarfdump --verify %t.pu-addr.classic
+# RUN: llvm-dwarfutil --linker parallel %t.pu-addr.o %t.pu-addr.parallel
+# RUN: llvm-dwarfdump --debug-info %t.pu-addr.parallel | FileCheck %s --check-prefixes=CHECK,PU --implicit-check-not=DW_TAG_subprogram
+# RUN: llvm-dwarfdump --verify %t.pu-addr.parallel
+
+## DW_TAG_partial_unit root, DW_AT_high_pc as a length.
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -DHIGHPCFORM=DW_FORM_data8 -DHIGHPC=0x30 -DFOO2LOW=0x1140 -o %t.pu-len.o
+# RUN: llvm-dwarfutil %t.pu-len.o %t.pu-len.classic
+# RUN: llvm-dwarfdump --debug-info %t.pu-len.classic | FileCheck %s --check-prefixes=CHECK,PU --implicit-check-not=DW_TAG_subprogram
+# RUN: llvm-dwarfdump --verify %t.pu-len.classic
+# RUN: llvm-dwarfutil --linker parallel %t.pu-len.o %t.pu-len.parallel
+# RUN: llvm-dwarfdump --debug-info %t.pu-len.parallel | FileCheck %s --check-prefixes=CHECK,PU --implicit-check-not=DW_TAG_subprogram
+# RUN: llvm-dwarfdump --verify %t.pu-len.parallel
+
+## The same inputs with foo2 tombstoned too, so only gvar keeps the unit.
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -DHIGHPCFORM=DW_FORM_addr -DHIGHPC=0x1160 -DFOO2LOW=0x0 -o %t.drop-cu-addr.o
+# RUN: llvm-dwarfutil %t.drop-cu-addr.o %t.drop-cu-addr.classic
+# RUN: llvm-dwarfdump --debug-info %t.drop-cu-addr.classic | FileCheck %s --check-prefixes=CU,DROP
+# RUN: llvm-dwarfutil --linker parallel %t.drop-cu-addr.o %t.drop-cu-addr.parallel
+# RUN: llvm-dwarfdump --debug-info %t.drop-cu-addr.parallel | FileCheck %s --check-prefixes=CU,DROP
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -DHIGHPCFORM=DW_FORM_data8 -DHIGHPC=0x30 -DFOO2LOW=0x0 -o %t.drop-cu-len.o
+# RUN: llvm-dwarfutil %t.drop-cu-len.o %t.drop-cu-len.classic
+# RUN: llvm-dwarfdump --debug-info %t.drop-cu-len.classic | FileCheck %s --check-prefixes=CU,DROP
+# RUN: llvm-dwarfutil --linker parallel %t.drop-cu-len.o %t.drop-cu-len.parallel
+# RUN: llvm-dwarfdump --debug-info %t.drop-cu-len.parallel | FileCheck %s --check-prefixes=CU,DROP
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -DHIGHPCFORM=DW_FORM_addr -DHIGHPC=0x1160 -DFOO2LOW=0x0 -o %t.drop-pu-addr.o
+# RUN: llvm-dwarfutil %t.drop-pu-addr.o %t.drop-pu-addr.classic
+# RUN: llvm-dwarfdump --debug-info %t.drop-pu-addr.classic | FileCheck %s --check-prefixes=PU,DROP
+# RUN: llvm-dwarfutil --linker parallel %t.drop-pu-addr.o %t.drop-pu-addr.parallel
+# RUN: llvm-dwarfdump --debug-info %t.drop-pu-addr.parallel | FileCheck %s --check-prefixes=PU,DROP
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -DHIGHPCFORM=DW_FORM_data8 -DHIGHPC=0x30 -DFOO2LOW=0x0 -o %t.drop-pu-len.o
+# RUN: llvm-dwarfutil %t.drop-pu-len.o %t.drop-pu-len.classic
+# RUN: llvm-dwarfdump --debug-info %t.drop-pu-len.classic | FileCheck %s --check-prefixes=PU,DROP
+# RUN: llvm-dwarfutil --linker parallel %t.drop-pu-len.o %t.drop-pu-len.parallel
+# RUN: llvm-dwarfdump --debug-info %t.drop-pu-len.parallel | FileCheck %s --check-prefixes=PU,DROP
+
+## The root's range is foo2's.
+# CU: DW_TAG_compile_unit
+# PU: DW_TAG_partial_unit
+# CHECK:        DW_AT_producer ("by_hand")
+# CHECK-NEXT:   DW_AT_name ("U1")
+# CHECK-NEXT:   DW_AT_low_pc (0x0000000000001140)
+# CHECK-NEXT:   DW_AT_high_pc (0x0000000000001150)
+# CHECK:        DW_TAG_subprogram
+# CHECK-NEXT:     DW_AT_name ("foo2")
+# CHECK-NEXT:     DW_AT_low_pc (0x0000000000001140)
+# CHECK-NEXT:     DW_AT_high_pc (0x0000000000001150)
+
+## With no live function, the root's DW_AT_low_pc and DW_AT_high_pc are
+## dropped.
+# DROP:          DW_AT_producer ("by_hand")
+# DROP-NEXT:     DW_AT_name ("U1")
+# DROP-NEXT:     DW_AT_str_offsets_base (0x00000008)
+# DROP:        DW_TAG_variable
+# DROP-NEXT:     DW_AT_name ("gvar")
+
+## foo1 and foo3 are tombstoned, and foo2 too when FOO2LOW is 0.
+--- !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:            0x30
+  - 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_low_pc
+            Form:      DW_FORM_addr
+          - Attribute: DW_AT_high_pc
+            Form:      [[HIGHPCFORM]]
+          - 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
+      - Tag:      DW_TAG_variable
+        Children: DW_CHILDREN_no
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_location
+            Form:      DW_FORM_exprloc
+  debug_info:
+    - Version:  5
+      UnitType: [[UNITTYPE]]
+      Entries:
+        - AbbrCode: 1
+          Values:
+            - CStr: by_hand
+            - CStr: U1
+            - Value: 0x1130
+            - Value: [[HIGHPC]]
+            - Value: 0x8
+        - AbbrCode: 2
+          Values:
+            - CStr: foo1
+            - Value: 0x0
+            - Value: 0x10
+        - AbbrCode: 2
+          Values:
+            - CStr: foo2
+            - Value: [[FOO2LOW]]
+            - Value: 0x10
+        - AbbrCode: 2
+          Values:
+            - CStr: foo3
+            - Value: 0x0
+            - Value: 0x10
+        - AbbrCode: 3
+          Values:
+            - CStr: gvar
+            - BlockData: [ 0x03, 0x40, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]
+        - AbbrCode: 0
+...
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-ranges.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-ranges.test
new file mode 100644
index 00000000000000..9b51c05f1236a2
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-ranges.test
@@ -0,0 +1,113 @@
+## Check that both linkers rebuild the DW_AT_ranges of a DW_TAG_partial_unit
+## root from the linked range, as for a DW_TAG_compile_unit. The root covers
+## foo1, foo2 and foo3, but only foo2 is linked. The input has DW_AT_addr_base
+## because the linker does not add a missing one.
+
+## DW_TAG_compile_unit root.
+# 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=CHECK,CU --implicit-check-not=DW_TAG_subprogram
+# RUN: llvm-dwarfdump --verify %t.cu.classic
+# RUN: llvm-dwarfutil --linker parallel %t.cu.o %t.cu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.cu.parallel | FileCheck %s --check-prefixes=CHECK,CU --implicit-check-not=DW_TAG_subprogram
+# RUN: llvm-dwarfdump --verify %t.cu.parallel
+
+## DW_TAG_partial_unit root.
+# 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=CHECK,PU --implicit-check-not=DW_TAG_subprogram
+# RUN: llvm-dwarfdump --verify %t.pu.classic
+# RUN: llvm-dwarfutil --linker parallel %t.pu.o %t.pu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.pu.parallel | FileCheck %s --check-prefixes=CHECK,PU --implicit-check-not=DW_TAG_subprogram
+# RUN: llvm-dwarfdump --verify %t.pu.parallel
+
+## The root's range is foo2's.
+# CU: DW_TAG_compile_unit
+# PU: DW_TAG_partial_unit
+# CHECK:        DW_AT_producer ("by_hand")
+# CHECK-NEXT:   DW_AT_name ("U1")
+# CHECK-NEXT:   DW_AT_ranges (0x0000000c
+# CHECK-NEXT:      [0x0000000000001140, 0x0000000000001150))
+# CHECK:        DW_TAG_subprogram
+# CHECK-NEXT:     DW_AT_name ("foo2")
+# CHECK-NEXT:     DW_AT_low_pc (0x0000000000001140)
+# CHECK-NEXT:     DW_AT_high_pc (0x0000000000001150)
+
+## The root's range list is one DW_RLE_start_length entry for [0x1130, 0x1160).
+--- !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:            0x30
+  - Name:            .debug_str_offsets
+    Type:            SHT_PROGBITS
+    Flags:           [  ]
+    Content:        "0400000005000000"
+  - Name:            .debug_addr
+    Type:            SHT_PROGBITS
+    Flags:           [  ]
+    Content:        "0c000000050008003011000000000000"
+  - Name:            .debug_rnglists
+    Type:            SHT_PROGBITS
+    Flags:           [  ]
+    Content:        "1300000005000800000000000730110000000000003000"
+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_ranges
+            Form:      DW_FORM_sec_offset
+          - Attribute: DW_AT_str_offsets_base
+            Form:      DW_FORM_sec_offset
+          - Attribute: DW_AT_addr_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
+  debug_info:
+    - Version:  5
+      UnitType: [[UNITTYPE]]
+      Entries:
+        - AbbrCode: 1
+          Values:
+            - CStr: by_hand
+            - CStr: U1
+            - Value: 0xc
+            - Value: 0x8
+            - Value: 0x8
+        - AbbrCode: 2
+          Values:
+            - CStr: foo1
+            - Value: 0x0
+            - Value: 0x10
+        - AbbrCode: 2
+          Values:
+            - CStr: foo2
+            - Value: 0x1140
+            - Value: 0x10
+        - AbbrCode: 2
+          Values:
+            - CStr: foo3
+            - Value: 0x0
+            - Value: 0x10
+        - AbbrCode: 0
+...

>From f35a3f9047973afd0c74076685d8dfefde360047 Mon Sep 17 00:00:00 2001
From: Arcadiy Ivanov <arcadiy at ivanov.biz>
Date: Sat, 29 Aug 2026 16:50:57 -0400
Subject: [PATCH 3/4] [DWARFVerifier] Do not demand a .debug_names entry for a
 unit root

llvm-dwarfdump --verify checks that .debug_names has an entry for every
named DIE, and excludes unit roots by tag, listing only
DW_TAG_compile_unit. A named DW_TAG_partial_unit root is therefore
reported as missing from the index, although its DW_AT_name is the path
of the primary source file and the index owes it no entry.

Exclude the unit root by position instead. It is the only DIE in a unit
without a parent, so this covers every unit root tag. DW_TAG_module
stays excluded by tag.

The test indexes a partial unit and a compile unit, neither root
indexed, and expects no errors. A second run leaves a subprogram under
the partial unit root out of the index and checks that it is the only
missing entry reported.

Fixes #219720.

Assisted-by: Claude Code
---
 llvm/lib/DebugInfo/DWARF/DWARFVerifier.cpp    |   7 +-
 ...g-names-verify-completeness-partial-unit.s | 173 ++++++++++++++++++
 2 files changed, 178 insertions(+), 2 deletions(-)
 create mode 100644 llvm/test/tools/llvm-dwarfdump/X86/debug-names-verify-completeness-partial-unit.s

diff --git a/llvm/lib/DebugInfo/DWARF/DWARFVerifier.cpp b/llvm/lib/DebugInfo/DWARF/DWARFVerifier.cpp
index ebd54e18bbca9d..01ae09450a0e0a 100644
--- a/llvm/lib/DebugInfo/DWARF/DWARFVerifier.cpp
+++ b/llvm/lib/DebugInfo/DWARF/DWARFVerifier.cpp
@@ -1966,14 +1966,17 @@ void DWARFVerifier::verifyNameIndexCompleteness(
   if (EntryNames.empty())
     return;
 
+  // Unit roots, whatever their tag, have names but shouldn't be indexed.
+  if (!Die.getParent())
+    return;
+
   // We deviate from the specification here, which says:
   // "The name index must contain an entry for each debugging information entry
   // that defines a named subprogram, label, variable, type, or namespace,
   // subject to ..."
   // Explicitly exclude all TAGs that we know shouldn't be indexed.
   switch (Die.getTag()) {
-  // Compile units and modules have names but shouldn't be indexed.
-  case DW_TAG_compile_unit:
+  // Modules have names but shouldn't be indexed.
   case DW_TAG_module:
     return;
 
diff --git a/llvm/test/tools/llvm-dwarfdump/X86/debug-names-verify-completeness-partial-unit.s b/llvm/test/tools/llvm-dwarfdump/X86/debug-names-verify-completeness-partial-unit.s
new file mode 100644
index 00000000000000..a5ecc541dc4312
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfdump/X86/debug-names-verify-completeness-partial-unit.s
@@ -0,0 +1,173 @@
+# Check that the .debug_names completeness check does not demand an entry for
+# a DW_TAG_partial_unit root, as for a DW_TAG_compile_unit root. One index
+# covers the partial unit U01 and the compile unit U02.
+
+# RUN: llvm-mc -triple x86_64-pc-linux %s -filetype=obj -o %t.o
+# RUN: llvm-dwarfdump --verify %t.o | FileCheck %s --implicit-check-not=error:
+
+# CHECK: No errors.
+
+# A DIE under the partial unit root that is missing from the index is still
+# reported.
+
+# RUN: llvm-mc -triple x86_64-pc-linux --defsym UNINDEXED=1 %s -filetype=obj -o %t.unindexed.o
+# RUN: not llvm-dwarfdump --verify %t.unindexed.o | FileCheck %s --check-prefix=UNINDEXED
+
+# UNINDEXED:      error: Name Index @ 0x0: Entry for DIE @ {{.*}} (DW_TAG_subprogram) with name unindexed missing.
+# UNINDEXED:      error: Aggregated error counts:
+# UNINDEXED-NEXT: error: Name Index DIE entry missing name occurred 1 time(s).
+
+	.section	.debug_str,"MS", at progbits,1
+.Lstring_producer:
+	.asciz	"Hand-written DWARF"
+.Lstring_dwz_common:
+	.asciz	"dwz-common.h"
+.Lstring_main:
+	.asciz	"main.c"
+.Lstring_foo:
+	.asciz	"foo"
+.Lstring_bar:
+	.asciz	"bar"
+.Lstring_unindexed:
+	.asciz	"unindexed"
+
+	.section	.debug_abbrev,"", at progbits
+.Lsection_abbrev:
+	.byte	1                       # Abbreviation Code
+	.byte	60                      # DW_TAG_partial_unit
+	.byte	1                       # DW_CHILDREN_yes
+	.byte	37                      # DW_AT_producer
+	.byte	14                      # DW_FORM_strp
+	.byte	3                       # DW_AT_name
+	.byte	14                      # DW_FORM_strp
+	.byte	19                      # DW_AT_language
+	.byte	5                       # DW_FORM_data2
+	.byte	0                       # EOM(1)
+	.byte	0                       # EOM(2)
+
+	.byte	2                       # Abbreviation Code
+	.byte	46                      # DW_TAG_subprogram
+	.byte	0                       # DW_CHILDREN_no
+	.byte	3                       # DW_AT_name
+	.byte	14                      # DW_FORM_strp
+	.byte	17                      # DW_AT_low_pc
+	.byte	1                       # DW_FORM_addr
+	.byte	18                      # DW_AT_high_pc
+	.byte	6                       # DW_FORM_data4
+	.byte	0                       # EOM(1)
+	.byte	0                       # EOM(2)
+
+	.byte	3                       # Abbreviation Code
+	.byte	17                      # DW_TAG_compile_unit
+	.byte	1                       # DW_CHILDREN_yes
+	.byte	37                      # DW_AT_producer
+	.byte	14                      # DW_FORM_strp
+	.byte	3                       # DW_AT_name
+	.byte	14                      # DW_FORM_strp
+	.byte	19                      # DW_AT_language
+	.byte	5                       # DW_FORM_data2
+	.byte	17                      # DW_AT_low_pc
+	.byte	1                       # DW_FORM_addr
+	.byte	18                      # DW_AT_high_pc
+	.byte	6                       # DW_FORM_data4
+	.byte	0                       # EOM(1)
+	.byte	0                       # EOM(2)
+
+	.byte	0                       # EOM(3)
+
+	.section	.debug_info,"", at progbits
+# U01, the partial unit. Its root has no address range, as in dwz output.
+.Lcu_begin0:
+	.long	.Lcu_end0-.Lcu_start0   # Length of Unit
+.Lcu_start0:
+	.short	5                       # DWARF version number
+	.byte	3                       # DW_UT_partial
+	.byte	8                       # Address Size (in bytes)
+	.long	.Lsection_abbrev        # Offset Into Abbrev. Section
+	.byte	1                       # Abbrev [1] DW_TAG_partial_unit
+	.long	.Lstring_producer       # DW_AT_producer
+	.long	.Lstring_dwz_common     # DW_AT_name
+	.short	12                      # DW_AT_language
+.Ldie_foo:
+	.byte	2                       # Abbrev [2] DW_TAG_subprogram
+	.long	.Lstring_foo            # DW_AT_name
+	.quad	0x1000                  # DW_AT_low_pc
+	.long	0x10                    # DW_AT_high_pc
+.ifdef UNINDEXED
+.Ldie_unindexed:
+	.byte	2                       # Abbrev [2] DW_TAG_subprogram
+	.long	.Lstring_unindexed      # DW_AT_name
+	.quad	0x1010                  # DW_AT_low_pc
+	.long	0x10                    # DW_AT_high_pc
+.endif
+	.byte	0                       # End Of Children Mark
+.Lcu_end0:
+
+# U02, the control.
+.Lcu_begin1:
+	.long	.Lcu_end1-.Lcu_start1   # Length of Unit
+.Lcu_start1:
+	.short	5                       # DWARF version number
+	.byte	1                       # DW_UT_compile
+	.byte	8                       # Address Size (in bytes)
+	.long	.Lsection_abbrev        # Offset Into Abbrev. Section
+	.byte	3                       # Abbrev [3] DW_TAG_compile_unit
+	.long	.Lstring_producer       # DW_AT_producer
+	.long	.Lstring_main           # DW_AT_name
+	.short	12                      # DW_AT_language
+	.quad	0x2000                  # DW_AT_low_pc
+	.long	0x100                   # DW_AT_high_pc
+.Ldie_bar:
+	.byte	2                       # Abbrev [2] DW_TAG_subprogram
+	.long	.Lstring_bar            # DW_AT_name
+	.quad	0x2000                  # DW_AT_low_pc
+	.long	0x10                    # DW_AT_high_pc
+	.byte	0                       # End Of Children Mark
+.Lcu_end1:
+
+	.section	.debug_names,"", at progbits
+	.long	.Lnames_end0-.Lnames_start0 # Header: contribution length
+.Lnames_start0:
+	.short	5                       # Header: version
+	.short	0                       # Header: padding
+	.long	2                       # Header: compilation unit count
+	.long	0                       # Header: local type unit count
+	.long	0                       # Header: foreign type unit count
+	.long	2                       # Header: bucket count
+	.long	2                       # Header: name count
+	.long	.Lnames_abbrev_end0-.Lnames_abbrev_start0 # Header: abbreviation table size
+	.long	0                       # Header: augmentation length
+	.long	.Lcu_begin0             # Compilation unit 0
+	.long	.Lcu_begin1             # Compilation unit 1
+	.long	1                       # Bucket 0
+	.long	2                       # Bucket 1
+	.long	193487034               # Hash in Bucket 0: bar
+	.long	193491849               # Hash in Bucket 1: foo
+	.long	.Lstring_bar            # String in Bucket 0: bar
+	.long	.Lstring_foo            # String in Bucket 1: foo
+	.long	.Lnames_bar-.Lnames_entries0 # Offset in Bucket 0
+	.long	.Lnames_foo-.Lnames_entries0 # Offset in Bucket 1
+.Lnames_abbrev_start0:
+	.byte	46                      # Abbrev code
+	.byte	46                      # DW_TAG_subprogram
+	.byte	1                       # DW_IDX_compile_unit
+	.byte	11                      # DW_FORM_data1
+	.byte	3                       # DW_IDX_die_offset
+	.byte	19                      # DW_FORM_ref4
+	.byte	0                       # End of abbrev
+	.byte	0                       # End of abbrev
+	.byte	0                       # End of abbrev list
+.Lnames_abbrev_end0:
+.Lnames_entries0:
+.Lnames_bar:
+	.byte	46                      # Abbrev code
+	.byte	1                       # DW_IDX_compile_unit
+	.long	.Ldie_bar-.Lcu_begin1   # DW_IDX_die_offset
+	.long	0                       # End of list: bar
+.Lnames_foo:
+	.byte	46                      # Abbrev code
+	.byte	0                       # DW_IDX_compile_unit
+	.long	.Ldie_foo-.Lcu_begin0   # DW_IDX_die_offset
+	.long	0                       # End of list: foo
+	.p2align	2
+.Lnames_end0:

>From 6f71eeae8ea374a55c426c8a28f268e52820f86e Mon Sep 17 00:00:00 2001
From: Arcadiy Ivanov <arcadiy at ivanov.biz>
Date: Sat, 29 Aug 2026 02:35:54 -0400
Subject: [PATCH 4/4] [DWARFLinker] Keep partial unit roots out of the
 accelerator tables

Keep DW_TAG_partial_unit roots out of .debug_names. Both linkers
exclude the unit root from the name index by testing for
DW_TAG_compile_unit only. A partial unit root with an address range
therefore gets an entry under its DW_AT_name, a source file path, and
becomes the DW_IDX_parent of every entry directly below it.

Test for the unit root instead of its tag: the classic linker compares
with the output unit DIE, the parallel linker returns early for the DIE
at index 0.

The test links a partial unit root and a compile unit root with both
linkers, with and without garbage collection, and checks all eight
outputs against the same index and for a clean --verify. A function in
a namespace must keep an indexed parent, so the test also fails if
DW_IDX_parent is dropped altogether.

Fixes #219599.

Assisted-by: Claude Code
---
 llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp  |   2 +-
 .../Parallel/AcceleratorRecordsSaver.cpp      |   5 +-
 .../Parallel/DWARFLinkerCompileUnit.h         |   3 +
 .../X86/dwarf5-partial-unit-debug-names.test  | 160 ++++++++++++++++++
 4 files changed, 168 insertions(+), 2 deletions(-)
 create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-debug-names.test

diff --git a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
index 3f76b71ebe0c6a..ca04ef97b7ef71 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
@@ -1949,7 +1949,7 @@ DIE *DWARFLinker::DIECloner::cloneDIE(const DWARFDie &InputDIE,
   // low_pc, but with AT_ranges might be interesting to get into the
   // accelerator tables too. For now stick with dsymutil's behavior.
   if ((Info.InDebugMap || AttrInfo.HasLowPc || AttrInfo.HasRanges) &&
-      Tag != dwarf::DW_TAG_compile_unit &&
+      !Unit.isUnitRootDIE(*Die) &&
       getDIENames(InputDIE, AttrInfo, DebugStrPool, File, Unit,
                   Tag != dwarf::DW_TAG_inlined_subroutine)) {
     if (AttrInfo.MangledName && AttrInfo.MangledName != AttrInfo.Name)
diff --git a/llvm/lib/DWARFLinker/Parallel/AcceleratorRecordsSaver.cpp b/llvm/lib/DWARFLinker/Parallel/AcceleratorRecordsSaver.cpp
index ab73a3fce8b4ad..c199196949cad6 100644
--- a/llvm/lib/DWARFLinker/Parallel/AcceleratorRecordsSaver.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/AcceleratorRecordsSaver.cpp
@@ -73,6 +73,10 @@ void AcceleratorRecordsSaver::save(const DWARFDebugInfoEntry *InputDieEntry,
     if (const char *ShortName = InputDIE.getShortName())
       AttrInfo.Name = GlobalData.getStringPool().insert(ShortName).first;
 
+  // Never index the unit root, whatever its tag (DWARFv5 section 6.1.1.1).
+  if (CompileUnit::isUnitRootDIE(InUnit.getDIEIndex(InputDieEntry)))
+    return;
+
   switch (InputDieEntry->getTag()) {
   case dwarf::DW_TAG_array_type:
   case dwarf::DW_TAG_class_type:
@@ -156,7 +160,6 @@ void AcceleratorRecordsSaver::save(const DWARFDebugInfoEntry *InputDieEntry,
       saveNamespaceRecord(InputDieEntry, AttrInfo.Name, OutDIE,
                           InputDieEntry->getTag(), TypeEntry);
   } break;
-  case dwarf::DW_TAG_compile_unit:
   case dwarf::DW_TAG_lexical_block: {
     // Nothing to do.
   } break;
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
index c8c1b8822aa642..ccde7e1fe5af88 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
@@ -335,6 +335,9 @@ class alignas(8) CompileUnit : public DwarfUnit {
   ///
   /// @{
 
+  /// \returns true if the DIE at \p DieIdx is the unit root, whatever its tag.
+  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-debug-names.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-debug-names.test
new file mode 100644
index 00000000000000..f631a3790638f8
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-debug-names.test
@@ -0,0 +1,160 @@
+## Check that neither linker puts a DW_TAG_compile_unit or DW_TAG_partial_unit
+## root into .debug_names.
+# DEFINE: %{check} = FileCheck %s --implicit-check-not='dwz-common.h' \
+# DEFINE:   --implicit-check-not=DW_TAG_partial_unit
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -o %t.cu.o
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -o %t.pu.o
+
+# RUN: llvm-dwarfutil --build-accelerator=DWARF %t.cu.o %t.cu.classic
+# RUN: llvm-dwarfdump --debug-names %t.cu.classic | %{check}
+# RUN: llvm-dwarfdump --verify %t.cu.classic | FileCheck %s --check-prefix=VERIFY
+# RUN: llvm-dwarfutil --build-accelerator=DWARF %t.pu.o %t.pu.classic
+# RUN: llvm-dwarfdump --debug-names %t.pu.classic | %{check}
+# RUN: llvm-dwarfdump --verify %t.pu.classic | FileCheck %s --check-prefix=VERIFY
+
+# RUN: llvm-dwarfutil --linker parallel --build-accelerator=DWARF %t.cu.o %t.cu.parallel
+# RUN: llvm-dwarfdump --debug-names %t.cu.parallel | %{check}
+# RUN: llvm-dwarfdump --verify %t.cu.parallel | FileCheck %s --check-prefix=VERIFY
+# RUN: llvm-dwarfutil --linker parallel --build-accelerator=DWARF %t.pu.o %t.pu.parallel
+# RUN: llvm-dwarfdump --debug-names %t.pu.parallel | %{check}
+# RUN: llvm-dwarfdump --verify %t.pu.parallel | FileCheck %s --check-prefix=VERIFY
+
+## Without --build-accelerator, --no-garbage-collection does not run the linker.
+# RUN: llvm-dwarfutil --no-garbage-collection --build-accelerator=DWARF %t.cu.o %t.cu.classic.upd
+# RUN: llvm-dwarfdump --debug-names %t.cu.classic.upd | %{check}
+# RUN: llvm-dwarfdump --verify %t.cu.classic.upd | FileCheck %s --check-prefix=VERIFY
+# RUN: llvm-dwarfutil --no-garbage-collection --build-accelerator=DWARF %t.pu.o %t.pu.classic.upd
+# RUN: llvm-dwarfdump --debug-names %t.pu.classic.upd | %{check}
+# RUN: llvm-dwarfdump --verify %t.pu.classic.upd | FileCheck %s --check-prefix=VERIFY
+
+# RUN: llvm-dwarfutil --linker parallel --no-garbage-collection \
+# RUN:   --build-accelerator=DWARF %t.cu.o %t.cu.parallel.upd
+# RUN: llvm-dwarfdump --debug-names %t.cu.parallel.upd | %{check}
+# RUN: llvm-dwarfdump --verify %t.cu.parallel.upd | FileCheck %s --check-prefix=VERIFY
+# RUN: llvm-dwarfutil --linker parallel --no-garbage-collection \
+# RUN:   --build-accelerator=DWARF %t.pu.o %t.pu.parallel.upd
+# RUN: llvm-dwarfdump --debug-names %t.pu.parallel.upd | %{check}
+# RUN: llvm-dwarfdump --verify %t.pu.parallel.upd | FileCheck %s --check-prefix=VERIFY
+
+# VERIFY: No errors.
+
+## Only foo3, inside ns, has an indexed parent.
+# CHECK:      Name Index @ 0x0 {
+# CHECK:        Bucket count: 4
+# CHECK-NEXT:   Name count: 4
+# CHECK:      Abbreviations [
+# CHECK-NEXT:   Abbreviation 0x1 {
+# CHECK-NEXT:     Tag: DW_TAG_subprogram
+# CHECK-NEXT:     DW_IDX_die_offset: DW_FORM_ref4
+# CHECK-NEXT:     DW_IDX_parent: DW_FORM_ref4
+# CHECK-NEXT:   }
+# CHECK-NEXT:   Abbreviation 0x2 {
+# CHECK-NEXT:     Tag: DW_TAG_namespace
+# CHECK-NEXT:     DW_IDX_die_offset: DW_FORM_ref4
+# CHECK-NEXT:     DW_IDX_parent: DW_FORM_flag_present
+# CHECK-NEXT:   }
+# CHECK-NEXT:   Abbreviation 0x3 {
+# CHECK-NEXT:     Tag: DW_TAG_subprogram
+# CHECK-NEXT:     DW_IDX_die_offset: DW_FORM_ref4
+# CHECK-NEXT:     DW_IDX_parent: DW_FORM_flag_present
+# CHECK-NEXT:   }
+# CHECK-NEXT: ]
+
+# CHECK:        String: {{.*}} "foo3"
+# CHECK:          Tag: DW_TAG_subprogram
+# CHECK-NEXT:     DW_IDX_die_offset: 0x{{[0-9a-f]+}}
+# CHECK-NEXT:     DW_IDX_parent: Entry @ 0x[[NS:[0-9a-f]+]]
+# CHECK:        String: {{.*}} "ns"
+# CHECK-NEXT:   Entry @ 0x[[NS]] {
+# CHECK-NEXT:     Abbrev: 0x2
+# CHECK-NEXT:     Tag: DW_TAG_namespace
+# CHECK-NEXT:     DW_IDX_die_offset: 0x{{[0-9a-f]+}}
+# CHECK-NEXT:     DW_IDX_parent: <parent not indexed>
+# CHECK:        String: {{.*}} "foo1"
+# CHECK:          Tag: DW_TAG_subprogram
+# CHECK-NEXT:     DW_IDX_die_offset: 0x{{[0-9a-f]+}}
+# CHECK-NEXT:     DW_IDX_parent: <parent not indexed>
+# CHECK:        String: {{.*}} "foo2"
+# CHECK:          Tag: DW_TAG_subprogram
+# CHECK-NEXT:     DW_IDX_die_offset: 0x{{[0-9a-f]+}}
+# CHECK-NEXT:     DW_IDX_parent: <parent not indexed>
+
+## The root's address range would otherwise qualify it for the index.
+--- !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:            0x40
+  - 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_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_low_pc
+            Form:      DW_FORM_addr
+          - Attribute: DW_AT_high_pc
+            Form:      DW_FORM_addr
+          - 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
+      - Tag:      DW_TAG_namespace
+        Children: DW_CHILDREN_yes
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+  debug_info:
+    - Version:  5
+      UnitType: [[UNITTYPE]]
+      AbbrevTableID: 0
+      Entries:
+        - AbbrCode: 1
+          Values:
+            - CStr: dwz-common.h
+            - Value: 0x1130
+            - Value: 0x1170
+            - Value: 0x8
+        - AbbrCode: 2
+          Values:
+            - CStr: foo1
+            - Value: 0x1130
+            - Value: 0x10
+        - AbbrCode: 2
+          Values:
+            - CStr: foo2
+            - Value: 0x1150
+            - Value: 0x10
+        - AbbrCode: 3
+          Values:
+            - CStr: ns
+        - AbbrCode: 2
+          Values:
+            - CStr: foo3
+            - Value: 0x1160
+            - Value: 0x10
+        - AbbrCode: 0
+        - AbbrCode: 0
+...



More information about the llvm-commits mailing list