[llvm] [BOLT][DWARF] Fix unit layout with forward DW_FORM_ref_udata references (PR #226076)

via llvm-commits llvm-commits at lists.llvm.org
Thu Sep 24 01:56:30 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-bolt

Author: Tim Besard (maleadt)

<details>
<summary>Changes</summary>

With `--update-debug-sections`, BOLT can emit a corrupt `.debug_info` when a unit has a *forward* DIE reference in `DW_FORM_ref_udata` form: the unit length, and every DIE offset after the reference, are off by one or more bytes. Consumers then misparse the section:

```
$ llvm-dwarfdump --debug-info libfoo.bolt.so
warning: DWARF unit from offset 0x002cbcfc incl. to offset 0x0031bd00 excl. extends past section size 0x0030caa6
```

This patch makes BOLT write those references as `DW_FORM_ref4`, and extends the existing `ref_udata` test to cover the failing case.

## Where this shows up

GNU as emits `DW_FORM_ref_udata` in the DWARF 5 units it generates for assembly files: each function gets a `DW_TAG_subprogram` whose `DW_AT_type` points forward to a `DW_TAG_unspecified_type`. For example, AArch64 code built with outline atomics links in such units from `lse.S` in GCC's `libgcc.a`, one per `__aarch64_*` helper used.

We hit this in Julia's CI, which BOLTs `libLLVM.so` and `libjulia-internal.so` on aarch64-linux. Symbolizing a backtrace printed the warning above to stderr, which failed tests that check stderr. The bug itself is target-independent.

## Root cause

`DW_FORM_ref_udata` is a ULEB128, so its size depends on its value. `DIEBuilder::finalizeDIEs()` lays out a unit in one depth-first pass, summing attribute sizes as it goes, and `DIEEntry::sizeOf()` sizes a reference as `getULEB128Size(Entry->getOffset())`, i.e. from the *referenced* DIE's offset. For a forward reference that DIE hasn't been laid out yet, so its offset is still the placeholder set by `allocDIE()`, which is the input offset relative to the start of the section:

```cpp
Die->setOffset(DDie.getOffset());
```

The value emitted later is the final offset relative to the unit. When the two encode to different lengths, e.g. for a DIE at least 0x80 bytes into `.debug_info` but less than 0x80 bytes into its unit, the computed layout doesn't match what gets written. Backward references are fine.

For example, with GCC 15.2's `lse.S` unit linked into a small AArch64 shared library, before this patch:

```
0x00000133:   DW_TAG_subprogram
                DW_AT_name [DW_FORM_strp]           ("__aarch64_ldadd4_acq_rel")
                DW_AT_type [DW_FORM_ref_udata]      (0x00000143)   <- one byte past the target
                ...
0x00000142:   DW_TAG_unspecified_type
0x00000143:   NULL

$ llvm-dwarfdump --verify liba.bolt.so
error: Unit Header Length: Unit type encoding is not valid occurred 1 time(s).
```

After, `DW_AT_type [DW_FORM_ref4] (0x00000145 "")` points at the `DW_TAG_unspecified_type` at 0x145, and `--verify` reports no errors.

## The fix

`DIEBuilder::cloneDieOffsetReferenceAttribute()` now clones `DW_FORM_ref_udata` references as `DW_FORM_ref4`:

```cpp
const dwarf::Form Form = AttrSpec.Form == dwarf::DW_FORM_ref_udata
                             ? dwarf::DW_FORM_ref4
                             : AttrSpec.Form;
```

Abbreviations are regenerated from the output DIEs, so nothing else changes. Reading `ref_udata` is still supported.

Why not keep `ref_udata`? That would require iterating the layout until sizes stop changing (sizes depend on offsets and vice versa, and one pass can push later offsets across another ULEB128 boundary). A fixed-size form is what BOLT already does for type references in location expressions (`cloneExpression()` pads them to 4 bytes "so size doesn't change when we update the offset"), and the parallel DWARFLinker also emits the local references it keeps as `DW_FORM_ref4`. The cost is a few bytes per reference, and `ref_udata` is rare.

---
Full diff: https://github.com/llvm/llvm-project/pull/226076.diff


2 Files Affected:

- (modified) bolt/lib/Core/DIEBuilder.cpp (+7-2) 
- (modified) bolt/test/X86/dwarf5-form-ref-udata.s (+66-14) 


``````````diff
diff --git a/bolt/lib/Core/DIEBuilder.cpp b/bolt/lib/Core/DIEBuilder.cpp
index fd2b872d66eae..f86a11331fcb7 100644
--- a/bolt/lib/Core/DIEBuilder.cpp
+++ b/bolt/lib/Core/DIEBuilder.cpp
@@ -701,8 +701,13 @@ void DIEBuilder::cloneDieOffsetReferenceAttribute(
     return;
   }
 
-  Die.addValue(getState().DIEAlloc, AttrSpec.Attr, AttrSpec.Form,
-               DIEEntry(*NewRefDie));
+  // The size of a DW_FORM_ref_udata value depends on the output offset of the
+  // referenced DIE, which for a forward reference is only assigned after this
+  // DIE has been laid out in finalizeDIEs(). Use a fixed-size form instead.
+  const dwarf::Form Form = AttrSpec.Form == dwarf::DW_FORM_ref_udata
+                               ? dwarf::DW_FORM_ref4
+                               : AttrSpec.Form;
+  Die.addValue(getState().DIEAlloc, AttrSpec.Attr, Form, DIEEntry(*NewRefDie));
 }
 
 void DIEBuilder::cloneStringAttribute(
diff --git a/bolt/test/X86/dwarf5-form-ref-udata.s b/bolt/test/X86/dwarf5-form-ref-udata.s
index 0b63a1711423d..32be643b59d26 100644
--- a/bolt/test/X86/dwarf5-form-ref-udata.s
+++ b/bolt/test/X86/dwarf5-form-ref-udata.s
@@ -2,16 +2,40 @@
 
 # RUN: llvm-mc -dwarf-version=5 -filetype=obj -triple x86_64-unknown-linux %s -o %t.o
 # RUN: %clang %cflags -dwarf-5 %t.o -o %t.exe -Wl,-q
+# RUN: llvm-dwarfdump --show-form --verbose --debug-info %t.exe | \
+# RUN:   FileCheck %s --check-prefix CHECK-INPUT
 # RUN: llvm-bolt %t.exe -o %t.bolt --update-debug-sections 2>&1 | \
 # RUN:   FileCheck %s --check-prefix CHECK-BOLT
-# RUN: llvm-dwarfdump --show-form --verbose --debug-info %t.bolt | FileCheck %s
+# RUN: llvm-dwarfdump --show-form --verbose --debug-info %t.bolt 2>&1 | \
+# RUN:   FileCheck %s --implicit-check-not=warning:
+# RUN: llvm-dwarfdump --verify %t.bolt | FileCheck %s --check-prefix CHECK-VERIFY
 
-## Verify BOLT preserves DW_FORM_ref_udata (CU-relative ULEB128 DIE reference),
-## a form GCC may emit instead of DW_FORM_ref4.
+## Verify BOLT handles DW_FORM_ref_udata (CU-relative ULEB128 DIE reference),
+## a form GNU as emits instead of DW_FORM_ref4. BOLT rewrites these
+## references as DW_FORM_ref4: the size of a ULEB128 reference depends on the
+## output offset of the referenced DIE, which is not known yet when a DIE with
+## a forward reference is laid out.
+##
+## Until a DIE is laid out, BOLT keeps its input offset, which is relative to
+## the start of .debug_info. The DIE referenced by the subprogram in the second
+## CU is at least 0x80 bytes into the section, so that offset needs a two-byte
+## ULEB128, while its CU-relative output offset needs only one byte. Sizing the
+## forward reference with the former and emitting the latter used to leave the
+## unit length and all subsequent DIE offsets of the unit off by one.
 
-# CHECK:      DW_TAG_subprogram
-# CHECK:      DW_AT_type [DW_FORM_ref_udata]
+## Check the input satisfies these conditions.
+# CHECK-INPUT: DW_AT_type [DW_FORM_ref_udata] (cu + 0x{{[0-7]?[0-9a-f]}} => {0x000000{{[89a-f][0-9a-f]}}} "long")
+
+# CHECK:      DW_TAG_compile_unit
+# CHECK:      DW_TAG_variable
+# CHECK:      DW_AT_type [DW_FORM_ref4]
 # CHECK-SAME: "int"
+# CHECK:      DW_TAG_compile_unit
+# CHECK:      DW_TAG_subprogram
+# CHECK:      DW_AT_type [DW_FORM_ref4]
+# CHECK-SAME: "long"
+
+# CHECK-VERIFY: No errors.
 
 # CHECK-BOLT-NOT: BOLT-WARNING
 
@@ -31,21 +55,30 @@ main:
 
 	.section	.debug_abbrev,"", at progbits
 	.byte	1, 17, 1                # CU, has children
+	.byte	37, 8                   # DW_AT_producer, DW_FORM_string
+	.byte	3, 8                    # DW_AT_name, DW_FORM_string
+	.byte	0, 0
+	.byte	2, 52, 0                # variable, no children
+	.byte	3, 8                    # DW_AT_name, DW_FORM_string
+	.byte	73, 21                  # DW_AT_type, DW_FORM_ref_udata
+	.byte	0, 0
+	.byte	3, 36, 0                # base_type, no children
+	.byte	3, 8                    # DW_AT_name, DW_FORM_string
+	.byte	0, 0
+	.byte	4, 17, 1                # CU, has children
 	.byte	17, 1                   # DW_AT_low_pc, DW_FORM_addr
 	.byte	18, 6                   # DW_AT_high_pc, DW_FORM_data4
 	.byte	16, 23                  # DW_AT_stmt_list, DW_FORM_sec_offset
 	.byte	0, 0
-	.byte	2, 46, 0                # subprogram, no children
+	.byte	5, 46, 0                # subprogram, no children
 	.byte	17, 1                   # DW_AT_low_pc, DW_FORM_addr
 	.byte	18, 6                   # DW_AT_high_pc, DW_FORM_data4
 	.byte	73, 21                  # DW_AT_type, DW_FORM_ref_udata
 	.byte	0, 0
-	.byte	3, 36, 0                # base_type, no children
-	.byte	3, 8                    # DW_AT_name, DW_FORM_string
-	.byte	0, 0
 	.byte	0
 
 	.section	.debug_info,"", at progbits
+## A CU without code, which moves the second CU further into .debug_info.
 .Lcu_begin0:
 	.long	.Ldebug_info_end0-.Ldebug_info_start0
 .Ldebug_info_start0:
@@ -54,17 +87,36 @@ main:
 	.byte	8                       # Address size
 	.long	.debug_abbrev           # Abbrev offset
 	.byte	1                       # CU
+	.asciz	"GNU C17 13.2.0 -mtune=generic -march=x86-64 -g -O2" # DW_AT_producer
+	.asciz	"/src/lib/global_data.c" # DW_AT_name
+	.byte	2                       # variable
+	.asciz	"data"                  # DW_AT_name
+	.uleb128 .Ltype_int-.Lcu_begin0    # DW_AT_type (DW_FORM_ref_udata)
+.Ltype_int:
+	.byte	3                       # base_type
+	.asciz	"int"                   # DW_AT_name
+	.byte	0                       # End children of CU
+.Ldebug_info_end0:
+
+.Lcu_begin1:
+	.long	.Ldebug_info_end1-.Ldebug_info_start1
+.Ldebug_info_start1:
+	.short	5                       # DWARF version
+	.byte	1                       # DW_UT_compile
+	.byte	8                       # Address size
+	.long	.debug_abbrev           # Abbrev offset
+	.byte	4                       # CU
 	.quad	.Lfunc_begin0           # DW_AT_low_pc
 	.long	.Lfunc_end0-.Lfunc_begin0  # DW_AT_high_pc
 	.long	.Lline_table_start0     # DW_AT_stmt_list
-	.byte	2                       # subprogram
+	.byte	5                       # subprogram
 	.quad	.Lfunc_begin0           # DW_AT_low_pc
 	.long	.Lfunc_end0-.Lfunc_begin0  # DW_AT_high_pc
-	.uleb128 .Ltype_int-.Lcu_begin0    # DW_AT_type (DW_FORM_ref_udata)
-.Ltype_int:
+	.uleb128 .Ltype_long-.Lcu_begin1   # DW_AT_type (DW_FORM_ref_udata)
+.Ltype_long:
 	.byte	3                       # base_type
-	.asciz	"int"                   # DW_AT_name
+	.asciz	"long"                  # DW_AT_name
 	.byte	0                       # End children of CU
-.Ldebug_info_end0:
+.Ldebug_info_end1:
 	.section	.debug_line,"", at progbits
 .Lline_table_start0:

``````````

</details>


https://github.com/llvm/llvm-project/pull/226076


More information about the llvm-commits mailing list