[dsymutil] The parallel DWARFLinker folds a DW_TAG_partial_unit root's name into the .apple_types qualified name hash
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
The Apple accelerator tables carry a `DW_ATOM_qual_name_hash` for every type: a hash of the type's fully qualified name. The parallel DWARFLinker computes it in `hashFullyQualifiedName`, which walks up the parents and stops when it reaches a unit root — recognizing one by testing the parent's tag against `DW_TAG_compile_unit` alone. Under a `DW_TAG_partial_unit` root, what `dwz` produces, the walk does not stop: it recurses into the root and hashes the root's `DW_AT_name`, a source file path, as a scope component of the type's name.
The classic backend does not have this problem. It ends the same walk positionally, on the parent index being zero, so it is correct whatever the root tag is. The two linkers therefore emit different `DW_ATOM_qual_name_hash` values for the same type on the same input, which is the one thing an accelerator table hash cannot afford, since a consumer looks types up by it.
Measured on the reproducer below, for a `struct Foo` at unit scope:
| root tag | `Atom[3]` |
| --- | --- |
| `DW_TAG_compile_unit` | `0x0c3993dd` |
| `DW_TAG_partial_unit` | `0xf9a84c7e` |
Only `dsymutil` reaches this. `llvm-dwarfutil` accepts `--build-accelerator=none` and `--build-accelerator=DWARF` and nothing else, and the hash is written only into the Apple tables, so the value is computed and discarded there.
## Reachability
This is not reproducible on `main` as it stands. A type under a `DW_TAG_partial_unit` root trips #219613 first — `assert(Entry != nullptr)` in `CompileUnit::createTypeDIEandCloneAttributes`, and a segmentation fault without assertions — before the hash is ever written out. The numbers above were measured with #219615, the fix for that, applied on top of 4cbcc02ed026f6f92c818656db4fca5a442870b5. Anyone reproducing this needs it applied first. The defect here is independent of that one and outlives it.
## Reproducer
`hash.s`, assembled twice with the root tag and unit type supplied by `--defsym`:
```asm
.section __TEXT,__text,regular,pure_instructions
.globl _foo
_foo:
Lfunc_begin0:
retq
Lfunc_end0:
.section __DWARF,__debug_abbrev,regular,debug
Lsection_abbrev:
.byte 1 ## Abbreviation Code
.byte ROOT ## root tag
.byte 1 ## DW_CHILDREN_yes
.byte 37 ## DW_AT_producer
.byte 8 ## DW_FORM_string
.byte 19 ## DW_AT_language
.byte 5 ## DW_FORM_data2
.byte 3 ## DW_AT_name
.byte 8 ## DW_FORM_string
.byte 0, 0
.byte 2 ## Abbreviation Code
.byte 46 ## DW_TAG_subprogram
.byte 0 ## DW_CHILDREN_no
.byte 3 ## DW_AT_name
.byte 8 ## DW_FORM_string
.byte 73 ## DW_AT_type
.byte 19 ## DW_FORM_ref4
.byte 17 ## DW_AT_low_pc
.byte 1 ## DW_FORM_addr
.byte 18 ## DW_AT_high_pc
.byte 6 ## DW_FORM_data4
.byte 0, 0
.byte 3 ## Abbreviation Code
.byte 19 ## DW_TAG_structure_type
.byte 0 ## DW_CHILDREN_no
.byte 3 ## DW_AT_name
.byte 8 ## DW_FORM_string
.byte 11 ## DW_AT_byte_size
.byte 11 ## DW_FORM_data1
.byte 0, 0
.byte 0 ## EOM(3)
.section __DWARF,__debug_info,regular,debug
Lsection_info:
.long Lcu_end - Lcu_start ## Length of Unit
Lcu_start:
.short 5 ## DWARF version number
.byte UNITTYPE ## DW_UT_compile / DW_UT_partial
.byte 8 ## Address Size (in bytes)
.long 0 ## Offset Into Abbrev. Section
.byte 1 ## Abbrev [1] root
.asciz "hand-written" ## DW_AT_producer
.short 0x0004 ## DW_AT_language (DW_LANG_C_plus_plus)
.asciz "dwz-common.h" ## DW_AT_name
.byte 2 ## Abbrev [2] DW_TAG_subprogram
.asciz "foo" ## DW_AT_name
.long Lfootype - Lsection_info ## DW_AT_type
.quad Lfunc_begin0 ## DW_AT_low_pc
.long Lfunc_end0 - Lfunc_begin0 ## DW_AT_high_pc
Lfootype:
.byte 3 ## Abbrev [3] DW_TAG_structure_type
.asciz "Foo" ## DW_AT_name
.byte 8 ## DW_AT_byte_size
.byte 0 ## End Of Children Mark (root)
Lcu_end:
```
`pu.map`, with `cu.map` differing only in the object it names:
```yaml
---
triple: 'x86_64-apple-darwin'
objects:
- filename: 'pu.o'
symbols:
- { sym: _foo, objAddr: 0x0, binAddr: 0x10000, size: 0x1 }
...
```
```console
$ llvm-mc -triple x86_64-apple-darwin -filetype=obj --defsym ROOT=60 --defsym UNITTYPE=3 hash.s -o pu.o
$ dsymutil --linker=parallel -accelerator=Apple -y pu.map -f -o pu.dSYM
$ llvm-dwarfdump --apple-types pu.dSYM
Hash 0xb87eb69 [
Name@0x38 {
String: 0x0000001f "Foo"
Data 0 [
Atom[0]: 0x0000001b
Atom[1]: 0x0013 (DW_TAG_structure_type)
Atom[2]: 0x00
Atom[3]: 0xf9a84c7e
]
```
The control, with only the root tag and unit type changed:
```console
$ llvm-mc -triple x86_64-apple-darwin -filetype=obj --defsym ROOT=17 --defsym UNITTYPE=1 hash.s -o cu.o
$ dsymutil --linker=parallel -accelerator=Apple -y cu.map -f -o cu.dSYM
$ llvm-dwarfdump --apple-types cu.dSYM
Hash 0xb87eb69 [
Name@0x38 {
String: 0x0000006a "Foo"
Data 0 [
Atom[0]: 0x0000001b
Atom[1]: 0x0013 (DW_TAG_structure_type)
Atom[2]: 0x00
Atom[3]: 0x0c3993dd
]
```
The bucket hash, which is computed from the short name, agrees in both. Only the qualified name hash differs.
## Analysis
Parallel, in `hashFullyQualifiedName`:
https://github.com/llvm/llvm-project/blob/4cbcc02ed026f6f92c818656db4fca5a442870b5/llvm/lib/DWARFLinker/Parallel/AcceleratorRecordsSaver.cpp#L53-L55
```cpp
DWARFDie ParentDie = InputDIE.getParent();
if (!ParentDie.isValid() || ParentDie.getTag() == dwarf::DW_TAG_compile_unit)
return djbHash(Name ? Name : "", djbHash(ChildRecurseDepth ? "" : "::"));
```
A `DW_TAG_partial_unit` root is a valid parent whose tag is not `DW_TAG_compile_unit`, so the function recurses one level further and folds the root's name in.
Classic, in `DWARFLinker::DIECloner::hashFullyQualifiedName`:
https://github.com/llvm/llvm-project/blob/4cbcc02ed026f6f92c818656db4fca5a442870b5/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp#L2708-L2712
```cpp
if (CU->getInfo(Idx).ParentIdx == 0 ||
// FIXME: dsymutil-classic compatibility. Ignore modules.
CU->getOrigUnit().getDIEAtIndex(CU->getInfo(Idx).ParentIdx).getTag() ==
dwarf::DW_TAG_module)
return djbHash(Name ? Name : "", djbHash(ChildRecurseDepth ? "" : "::"));
```
Index zero is the unit root whatever tag it carries, so this arm is already tag-independent and needs no change.
`DWARFContext::compile_units()` filters out only type units, so a partial unit reaches the parallel function on exactly the same path a full compilation unit does; the tag test is the only thing that separated them.
## Specification
The Apple accelerator tables are not a DWARF construct, so the specification does not govern the table. It does govern the thing being hashed, which is the type's fully qualified name, and that is enough to settle which of the two backends is right. DWARF Version 5, February 13, 2017.
Section 3.1.1 "Full and Partial Compilation Unit Entries" (page 60) makes both tags unit roots, and then states that a partial unit is not the containing scope of what it owns:
> A full compilation unit is represented by a debugging information entry with the tag `DW_TAG_compile_unit`. A partial compilation unit is represented by a debugging information entry with the tag `DW_TAG_partial_unit`.
> A full or partial compilation unit entry owns debugging information entries that represent all or part of the declarations made in the corresponding compilation. In the case of a partial compilation unit, the containing scope of its owned declarations is indicated by imported unit entries in one or more other compilation unit entries that refer to that partial compilation unit (see Section 3.2.5 on page 74).
Section 3.2.5 "Imported Unit Entries" (page 74) puts the scope at the import site rather than at the unit:
> An imported unit entry contains a `DW_AT_import` attribute whose value is a reference to the normal or partial compilation unit whose declarations logically belong at the place of the imported unit entry.
The name being folded in is a file path, not an identifier, per section 3.1.1 item 2 (page 60):
> A `DW_AT_name` attribute whose value is a null-terminated string containing the full or relative path name (relative to the value of the `DW_AT_comp_dir` attribute, see below) of the primary source file from which the compilation unit was derived.
And the type in the reproducer is at global scope, which DWARF gives no entry at all, per section 3.2.2 "Namespace Entries" (page 71):
> The C++ global namespace (the namespace referred to by `::f`, for example) is not explicitly represented in DWARF with a namespace entry (thus mirroring the situation in C++ source). Global items may be simply declared with no reference to a namespace.
So the qualified name of the type is `Foo`, there is no enclosing scope to contribute a component, and the classic backend's hash is the correct one.
There is a second consequence worth stating. Section 3.1.1 (page 60) says partial units exist to serve duplicate elimination:
> In a compilation employing the DWARF space compression and duplicate elimination techniques from Appendix E.1 on page 365, multiple compilation units using the tags `DW_TAG_compile_unit`, `DW_TAG_partial_unit` and/or `DW_TAG_type_unit` are used to represent portions of an object file.
A qualified name hash that varies with the name of the partial unit a type happens to have been hoisted into is therefore at its least reliable on exactly the inputs partial units were introduced to produce: two objects whose `dwz` runs named the shared unit differently yield different hashes for the same type.
## Expected behavior
Both backends produce the same `DW_ATOM_qual_name_hash` for `Foo`, equal to the compile unit control's `0x0c3993dd`, whatever tag the unit root carries.
## Versions
Both code sites are unchanged on `main` as of 4cbcc02ed026f6f92c818656db4fca5a442870b5. Released builds cannot be checked directly, since reaching the hash requires the #219613 fix, which has not shipped.
This report was produced with AI assistance; I have reviewed it and am accountable for it.
Contributor guide
Research direction
Start in llvm/lib/DWARFLinker/Parallel/AcceleratorRecordsSaver.cpp at hashFullyQualifiedName, then compare the classic implementation cited in the issue. Assemble hash.s with both root-tag variants and run dsymutil with llvm-dwarfdump --apple-types; done means a DW_TAG_partial_unit root produces the same qualified-name hash as the compile-unit control without including the root name.
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
- 62/100