[llvm] [DWARFLinker] Key str_offsets_base and unit_type on the unit root (PR #219398)
via llvm-commits
llvm-commits at lists.llvm.org
Fri Aug 28 00:10:33 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-debuginfo
Author: Arcadiy Ivanov (arcivanov)
<details>
<summary>Changes</summary>
`llvm-dwarfutil` silently produces DWARF that fails verification whenever a unit's root DIE is a `DW_TAG_partial_unit`, which is the shape `dwz` leaves behind. Both linker backends exit 0:
```console
$ llvm-dwarfutil partial.o partial.out
$ llvm-dwarfdump --verify partial.out
...
error: DW_FORM_strx used without a valid string offsets table:
error: Compilation unit type (DW_UT_compile) and root DIE (DW_TAG_partial_unit) do not match.
error: Aggregated error counts:
error: Invalid DW_FORM attribute occurred 5 time(s).
error: Mismatched unit type occurred 1 time(s).
Errors detected.
```
DWARFLinker has two backends, classic and parallel, selected with `--linker` and shared with `dsymutil`, and each assumed every unit root is a `DW_TAG_compile_unit`. `DWARFContext::compile_units()` filters out only type units, so a partial unit reaches the linker on exactly the same path a full compilation unit does, and two decisions keyed on the tag rather than on the DIE being the unit root.
Firstly, `DW_AT_str_offsets_base` was never synthesized for such a root, although the strings were still rewritten into the indexed form. DWARFv5 section 7.26 resolves those indices only through that attribute, so every indexed string in the unit read back empty. Nothing in the spec restricts the attribute to full compilation units: section 3.1.1 is titled "Full and Partial Compilation Unit Entries" and introduces its attribute list with "A full or partial compilation unit entry may have the following attributes", while section 3.1.2 lists `DW_AT_str_offsets_base` for skeleton units as well.
Secondly, the unit header carried `unit_type = DW_UT_compile` whatever the root tag was, so the header and the root DIE contradicted each other. DWARFv5 section 7.5.1.1 defines the field as "`DW_UT_compile` for a (non-split) full compilation unit or `DW_UT_partial` for a (non-split) partial compilation unit", so the correct value follows from the root that was actually cloned.
The attribute synthesis and the header byte now both key on the unit root. Classic compares against `Unit.getOutputUnitDIE()`; parallel tests `InputDIEIdx == 0`, and the emitter reads `DwarfUnit::getTag()`, which `setOutUnitDIE()` already caches from the output root. Full and partial units share the same five-field header, so no size accounting changes anywhere.
Skeleton roots are deliberately left emitting `DW_UT_compile`. Section 7.5.1.2 gives skeleton and split headers a sixth field, an 8-byte `dwo_id`, which the hardcoded 12-byte header size in both backends does not account for; emitting `DW_UT_skeleton` without widening that constant and writing the `dwo_id` would produce a header malformed in a new way. Such a unit therefore still fails `--verify` exactly as it did before. The `DW_AT_str_offsets_base` half does cover skeleton roots, since keying on the unit root covers them for free and section 3.1.2 lists the attribute for them.
`OutUnit.isCompileUnit()` guards the parallel synthesis, and it is not redundant. The tag test it replaced could never be true while cloning into the artificial type unit, whereas index 0 can reach it, and `AttrOutOffset` is DIE-relative rather than section-relative there, so the patch would be misplaced.
The test is a hand-written DWARFv5 partial unit with two subprograms, carrying its own `.debug_addr` and `DW_AT_addr_base` so the address base is never in question. It runs through both backends under garbage collection and in update mode, asserting the header type, the synthesized attribute, that the indexed strings resolve, and a clean `--verify`. Update mode is reached with `--build-accelerator` because `--no-garbage-collection` on its own copies the file without running the linker at all; that configuration also showed the empty string table corrupting `.debug_names`, since the accelerator entries are verified against the names read back out of `.debug_info`.
`check-llvm-tools-llvm-dwarfutil`, `check-llvm-tools-dsymutil` and `DWARFLinkerParallelTests` pass.
Roughly a dozen other `== DW_TAG_compile_unit` root tests remain across both backends, and they are not all the same question. Two of them are filed separately as #<!-- -->219392 (a partial unit root keeps unrelocated `DW_AT_low_pc`/`DW_AT_high_pc`) and #<!-- -->219393 (ODR deduplication is skipped for types under a partial unit root); both change emitted output or deduplication behaviour and want their own tests, so they are deliberately out of scope here.
Fixes: #<!-- -->219365
This change was produced with AI assistance; I have reviewed it and am accountable for it.
---
Full diff: https://github.com/llvm/llvm-project/pull/219398.diff
5 Files Affected:
- (modified) llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp (+9-3)
- (modified) llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp (+12-1)
- (modified) llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp (+12-5)
- (modified) llvm/lib/DWARFLinker/Parallel/DWARFEmitterImpl.cpp (+12-1)
- (added) llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit.test (+143)
``````````diff
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
index 46001d7a8a4d5..a3c887fc54be1 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
@@ -2013,9 +2013,15 @@ DIE *DWARFLinker::DIECloner::cloneDIE(const DWARFDie &InputDIE,
}
}
- if (Unit.getOrigUnit().getVersion() >= 5 && !AttrInfo.AttrStrOffsetBaseSeen &&
- Die->getTag() == dwarf::DW_TAG_compile_unit) {
- // No DW_AT_str_offsets_base seen, add it to the DIE.
+ // Comparing against the output unit DIE identifies the root of the unit,
+ // whatever its tag. cloneStringAttribute() rewrites strings into
+ // DW_FORM_strx for every DWARFv5 unit without consulting the root tag, and
+ // DWARFv5 section 7.26 resolves those indices only through
+ // DW_AT_str_offsets_base, so a DW_TAG_partial_unit or DW_TAG_skeleton_unit
+ // root needs the attribute on the same terms as a full compilation unit
+ // (DWARFv5 sections 3.1.1 and 3.1.2).
+ if (Die == Unit.getOutputUnitDIE() && Unit.getOrigUnit().getVersion() >= 5 &&
+ !AttrInfo.AttrStrOffsetBaseSeen) {
Die->addValue(DIEAlloc, dwarf::DW_AT_str_offsets_base,
dwarf::DW_FORM_sec_offset, DIEInteger(8));
OutOffset += 4;
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp b/llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp
index c6ccc71889fc4..2118da26c2f6f 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp
@@ -188,7 +188,18 @@ void DwarfStreamer::emitCompileUnitHeader(CompileUnit &Unit,
Asm->emitInt16(DwarfVersion);
if (DwarfVersion >= 5) {
- Asm->emitInt8(dwarf::DW_UT_compile);
+ // The header has to agree with the root DIE that was actually cloned, or
+ // llvm-dwarfdump --verify rejects the unit with "Mismatched unit type".
+ // Reading the tag off the output root covers DW_TAG_partial_unit without
+ // plumbing the input unit type through the DwarfEmitter interface. DWARFv5
+ // section 7.5.1.1 gives full and partial units the same five-field header,
+ // so the size below is unchanged. Skeleton roots are deliberately not
+ // mapped to DW_UT_skeleton: section 7.5.1.2 adds an 8-byte dwo_id to that
+ // header, which the 12-byte constant here and in computeNextUnitOffset()
+ // does not account for.
+ 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 68d7b9d72cf61..88c52d7d4e013 100644
--- a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
@@ -114,11 +114,18 @@ 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
- // attribute.
- if (InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit &&
- InUnit.getVersion() >= 5 && !AttrInfo.HasStringOffsetBaseAttr) {
+ // Index 0 is the root DIE of the unit, whatever its tag. cloneStringAttr()
+ // rewrites strings into DW_FORM_strx for every DWARFv5 unit without
+ // consulting the root tag, and DWARFv5 section 7.26 resolves those indices
+ // only through DW_AT_str_offsets_base, so a DW_TAG_partial_unit or
+ // DW_TAG_skeleton_unit root needs the attribute on the same terms as a full
+ // compilation unit (DWARFv5 sections 3.1.1 and 3.1.2).
+ //
+ // The isCompileUnit() test is not redundant: index 0 can also reach the
+ // artificial type unit, where AttrOutOffset is DIE-relative rather than
+ // section-relative, so the patch registered below would be misplaced.
+ 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 4009bfcd3018c..be8520f77a37b 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFEmitterImpl.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFEmitterImpl.cpp
@@ -141,7 +141,18 @@ void DwarfEmitterImpl::emitCompileUnitHeader(DwarfUnit &Unit) {
Asm->emitInt16(Unit.getVersion());
if (Unit.getVersion() >= 5) {
- Asm->emitInt8(dwarf::DW_UT_compile);
+ // The header has to agree with the root DIE that was actually cloned, or
+ // llvm-dwarfdump --verify rejects the unit with "Mismatched unit type".
+ // getTag() is the tag of that root, cached by setOutUnitDIE(); a TypeUnit's
+ // artificial root is a DW_TAG_compile_unit, so type units keep emitting
+ // DW_UT_compile. DWARFv5 section 7.5.1.1 gives full and partial units the
+ // same five-field header, so the size below is unchanged. Skeleton roots
+ // are deliberately not mapped to DW_UT_skeleton: section 7.5.1.2 adds an
+ // 8-byte dwo_id to that header, which the 12-byte constant here and in
+ // getDebugInfoHeaderSize() does not account for.
+ 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 0000000000000..82230dd5b1e20
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit.test
@@ -0,0 +1,143 @@
+## A DWARFv5 unit whose root is DW_TAG_partial_unit, which is the shape dwz
+## leaves behind. DWARFContext::compile_units() filters out only type units, so
+## such a unit reaches the linker exactly as a full compilation unit does, and
+## both backends used to key two decisions on the DW_TAG_compile_unit tag
+## rather than on the DIE being the unit root:
+##
+## 1. DW_AT_str_offsets_base was not synthesized, while the strings were
+## still rewritten into DW_FORM_strx, so every indexed string in the
+## output read back empty.
+## 2. The unit header was emitted with unit_type DW_UT_compile whatever the
+## root tag was, contradicting the root DIE.
+##
+## Both backends exited 0 and produced output that llvm-dwarfdump --verify
+## rejected on both counts.
+
+# 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
+
+## Update mode preserves the input index tables rather than regenerating them,
+## but the strings are still converted and the header comes from the same
+## emitter, so both defects reach it as well. It is paired with
+## --build-accelerator here because --no-garbage-collection on its own copies
+## the file without running the linker at all. The empty string table also
+## corrupted the accelerator entries, since .debug_names records the names it
+## reads back out of .debug_info.
+# 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.
+
+## A DWARFv5 partial unit carrying two function ranges. The .debug_addr
+## contribution and DW_AT_addr_base are required, not incidental: without them
+## the rewritten ranges resolve from 0 and the unit fails verification for an
+## unrelated reason. DW_AT_ranges and .debug_rnglists are likewise required,
+## since otherwise garbage collection drops the whole unit and the collecting
+## RUN lines stop testing anything.
+--- !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 .debug_info offset of the DW_TAG_base_type DIE below; it shifts
+## if the root DIE's attributes change.
+ - Value: 0x5d
+ - AbbrCode: 2
+ Values:
+ - CStr: foo2
+ - Value: 0x1150
+ - Value: 0x10
+ - Value: 0x5d
+ - AbbrCode: 3
+ Values:
+ - CStr: int
+ - AbbrCode: 0
+...
``````````
</details>
https://github.com/llvm/llvm-project/pull/219398
More information about the llvm-commits
mailing list