[llvm-dwarfutil] DW_TAG_partial_unit roots are emitted into .debug_names, indexing a file name as if it were a program identifier
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
`llvm-dwarfutil --build-accelerator=DWARF` builds a `.debug_names` index, deciding DIE by DIE which ones earn an entry. A unit root DIE should never earn one: DWARFv5 section 6.1.1.1 says "The name index must contain an entry for each debugging information entry that defines a named subprogram, label, variable, type, or namespace", and a unit root defines none of those. Its `DW_AT_name` holds the source file path, not a program identifier. Both linker backends do exclude the unit root, and both do it by testing the root's tag against `DW_TAG_compile_unit` alone.
A `DW_TAG_partial_unit` root is not a `DW_TAG_compile_unit`, so it survives that test and gets indexed. In the reproducer below the index picks up a third name, `"dwz-common.h"`, tagged `DW_TAG_partial_unit`. The same input with a full compilation unit root — the only differences being that tag and its matching unit type — produces no such entry.
Indexing the root also alters entries that are themselves legitimate. `DW_IDX_parent` is the "Index of name table entry for parent" (DWARFv5 table 6.1), so once the root is in the name table both subprograms point at it, where the compile unit control reports ``. A consumer reconstructing a qualified name by walking the parent chain therefore picks up a header file name as one of its components.
`DW_TAG_partial_unit` is what `dwz` leaves behind, and `dwz` is a standard step in Fedora, RHEL and Debian debuginfo packaging, so this is the normal shape of a distribution debuginfo file rather than a corner case.
## Reproducer
The two inputs below differ only in the root DIE's tag and the matching unit type.
```yaml
# pu.yaml — the same file with DW_TAG_partial_unit/DW_UT_partial replaced by
# DW_TAG_compile_unit/DW_UT_compile is the control.
--- !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: DW_TAG_partial_unit
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
debug_info:
- Version: 5
UnitType: DW_UT_partial
Entries:
- AbbrCode: 1
Values:
- CStr: dwz-common.h
- Value: 0x1130
- Value: 0x1160
- Value: 0x8
- AbbrCode: 2
Values:
- CStr: foo1
- Value: 0x1130
- Value: 0x10
- AbbrCode: 2
Values:
- CStr: foo2
- Value: 0x1150
- Value: 0x10
- AbbrCode: 0
...
```
```console
$ yaml2obj pu.yaml -o pu.o
$ llvm-dwarfutil --build-accelerator=DWARF pu.o pu.out
$ llvm-dwarfdump --debug-names pu.out
Name count: 3
Tag: DW_TAG_subprogram
DW_IDX_parent: DW_FORM_ref4
Tag: DW_TAG_partial_unit
String: 0x0000000e "foo1"
Tag: DW_TAG_subprogram
DW_IDX_parent: Entry @ 0x83
String: 0x00000013 "foo2"
Tag: DW_TAG_subprogram
DW_IDX_parent: Entry @ 0x83
String: 0x00000001 "dwz-common.h"
Tag: DW_TAG_partial_unit
```
The control, with only the root tag changed:
```console
$ llvm-dwarfutil --build-accelerator=DWARF cu.o cu.out
$ llvm-dwarfdump --debug-names cu.out
Name count: 2
Tag: DW_TAG_subprogram
DW_IDX_parent: DW_FORM_flag_present
String: 0x0000000e "foo1"
Tag: DW_TAG_subprogram
DW_IDX_parent:
String: 0x00000013 "foo2"
Tag: DW_TAG_subprogram
DW_IDX_parent:
```
Both linker backends produce this. The output above is the default `classic` linker; `--linker parallel` emits the same three names, the same `DW_TAG_partial_unit` abbreviation, and the same parent entry.
## Analysis
Both backends exclude a unit root from the accelerator tables by testing its tag, and only `DW_TAG_compile_unit` is named.
Classic, in `DIECloner::cloneDIE`:
https://github.com/llvm/llvm-project/blob/93ff4de2b0946a45a7c83f63d8278ec8aa33f273/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp#L1955-L1958
```cpp
if ((Info.InDebugMap || AttrInfo.HasLowPc || AttrInfo.HasRanges) &&
Tag != dwarf::DW_TAG_compile_unit &&
getDIENames(InputDIE, AttrInfo, DebugStrPool, File, Unit,
Tag != dwarf::DW_TAG_inlined_subroutine)) {
```
A unit root has a live `DW_AT_low_pc`, so it satisfies the first condition, and the tag test is the only thing that kept it out. A `DW_TAG_partial_unit` root passes all three and is handed to `addNameAccelerator`.
Parallel, in `AcceleratorRecordsSaver::save`:
https://github.com/llvm/llvm-project/blob/93ff4de2b0946a45a7c83f63d8278ec8aa33f273/llvm/lib/DWARFLinker/Parallel/AcceleratorRecordsSaver.cpp#L159-L162
```cpp
case dwarf::DW_TAG_compile_unit:
case dwarf::DW_TAG_lexical_block: {
// Nothing to do.
} break;
```
`DW_TAG_partial_unit` falls through to `default:`, where `AttrInfo.HasLiveAddress` admits it to `saveNameRecord` on the same terms as a subprogram.
`DWARFContext::compile_units()` filters out only type units, so a partial unit reaches both of these on exactly the same path a full compilation unit does. `DW_TAG_skeleton_unit` roots would be admitted the same way, though I have not built an input that gets one this far.
The natural fix is to identify the unit root by being the root rather than by its tag — `&Die == Unit.getOutputUnitDIE()` in classic, `InputDIEIdx == 0` in parallel — but whether the exclusion belongs there or in the tag list is a call for whoever owns this code.
## Expected behavior
A unit root contributes no name index entry whatever its tag, so the output matches the compile unit control exactly: two names, `"foo1"` and `"foo2"`, both reporting ``.
## Versions
Reproduced on stock 22.1.8 (Fedora 44, `/usr/bin/llvm-dwarfutil`); the console output above is from that build. Both code sites are unchanged on `main` as of 93ff4de2b0946a45a7c83f63d8278ec8aa33f273, and a build of that tree produces the same index. `dsymutil` shares the classic code path.
This report was produced with AI assistance; I have reviewed it and am accountable for it.
Contributor guide
Research direction
Start with the reproducer using yaml2obj, llvm-dwarfutil --build-accelerator=DWARF, and llvm-dwarfdump --debug-names. Read DIECloner::cloneDIE in llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp and AcceleratorRecordsSaver::save in llvm/lib/DWARFLinker/Parallel/AcceleratorRecordsSaver.cpp, then compare both linker outputs with the compile-unit control. Done means a partial-unit root contributes no name entry and foo1 and foo2 report an unindexed parent.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100