llvm / llvm/llvm-project

[llvm-dwarfutil] DW_TAG_partial_unit roots are emitted into .debug_names, indexing a file name as if it were a program identifier

Open
#219,599 0 comments 0 reactions 0 assignees View on GitHub
llvm-dwarfutil
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.