| Issue |
208082
|
| Summary |
[BOLT] cloneExpression() corrupts DW_OP_entry_value when it wraps a BaseTypeRef operand during --update-debug-sections usage
|
| Labels |
|
| Assignees |
|
| Reporter |
not4juu
|
## Description
When BOLT clones a DWARF location/value _expression_, it re-encodes DIE-reference operands (`Encoding::BaseTypeRef`, e.g. the type operand of `DW_OP_regval_type`, `DW_OP_convert`, `DW_OP_deref_type`, `DW_OP_const_type`) as a **fixed 4-byte ULEB**. If such an operand is nested inside a `DW_OP_entry_value`, the padding grows the sub-_expression_ but the `DW_OP_entry_value` **length prefix is not updated**. The emitted _expression_ block becomes internally inconsistent and it violates **DWARF 5 §2.5.1.7**: entry_value length must equal block size in bytes (see the DWARF 5 references below)
`readelf` (which enforces the `DW_OP_entry_value` sub-_expression_ boundary) then aborts the whole `.debug_info` dump with:
```
readelf: Error: end of data encountered whilst reading LEB
```
## DWARF Debugging Information Format Version 5 (February 13, 2017)
The nesting GCC emits is standard DWARF 5 — GCC uses the standard opcodes `DW_OP_entry_value` (`0xa3`) and `DW_OP_regval_type` (`0xa5`), both "New in DWARF Version 5".
- **§2.5.1.7 Special Operations (`DW_OP_entry_value`)**: _"It has two operands: **an unsigned LEB128 length**, followed by a block containing a **DWARF _expression_** or a register location description. **The length operand specifies the length in bytes of the block.**"_ A "DWARF _expression_" (§2.5) is any sequence of operations, so nesting a typed op such as `DW_OP_regval_type` inside the block is explicitly allowed.
- **§2.5.1.2 Register Values (`DW_OP_regval_type`)**: the second operand is _"an unsigned LEB128 number that represents the offset of a debugging information entry in the current compilation unit, which must be a `DW_TAG_base_type` entry"_ — i.e. the CU-relative DIE offset (the `BaseTypeRef`) that BOLT re-encodes/pads.
- **§7.7.1 DWARF Expressions** (operation encodings):
- _`DW_OP_entry_value 0xa3` = "ULEB128 size, block of that size"_
- _`DW_OP_regval_type 0xa5` = "ULEB128 register number, ULEB128 constant offset"_.
- **Appendix D** shows `DW_OP_entry_value` wrapping expressions for call sites, e.g. `DW_AT_call_value(DW_OP_entry_value 4 DW_OP_breg0 0 DW_OP_deref_size 4)`.
The normative rule BOLT violates is the §2.5.1.7 requirement that the length operand equals the byte size of the block: padding the inner `BaseTypeRef` ULEB grows the block but the length is left unchanged.
## Affected code
`bolt/lib/Core/DIEBuilder.cpp`, `DIEBuilder::cloneExpression()`:
```cpp
uint8_t ULEB[16];
// Hard coding to max size so size doesn't change when we update the offset.
encodeULEB128(Offset, ULEB, 4);
ArrayRef<uint8_t> ULEBbytes(ULEB, 4);
OutputBuffer.append(ULEBbytes.begin(), ULEBbytes.end());
```
Root of the mismatch: LLVM's `DWARFExpression` describes `DW_OP_entry_value` (and `DW_OP_GNU_entry_value`) as a single opcode with **one** operand — the ULEB byte-length of the sub-_expression_ (`Op::SizeLEB`). The iterator does not recurse into the sub-_expression_ nor bound it (`llvm/lib/DebugInfo/DWARF/LowLevel/DWARFExpression.cpp`):
```cpp
Descriptions[DW_OP_entry_value] = Desc(Op::Dwarf5, Op::SizeLEB);
Descriptions[DW_OP_regval_type] = Desc(Op::Dwarf5, Op::SizeLEB, Op::BaseTypeRef);
```
so `cloneExpression` copies the `DW_OP_entry_value` opcode + length verbatim and then, separately, pads the nested `BaseTypeRef` operand — leaving the length stale.
## Bug observation
GCC commonly emits call-site info as:
```
DW_AT_call_value : DW_OP_entry_value( DW_OP_regval_type <reg> <type-DIE-offset> )
```
Input bytes (minimal ULEBs, as produced by GCC):
```
a3 03 DW_OP_entry_value, sub-_expression_ length = 3
a5 40 3f DW_OP_regval_type reg=64 type-DIE-offset=0x3f
```
After BOLT (`--update-debug-sections`) the type offset `0x3f` is padded to a 4-byte ULEB `bf 80 80 00`, but the `entry_value` length stays `0x03`:
```
BOLT output : a3 03 a5 40 bf 80 80 00 <- length says 3, body is 6 -> BROKEN
correct : a3 06 a5 40 bf 80 80 00 <- length updated to 6
```
Reading the inner `regval_type` type ULEB (`bf ...`) hits the declared 3-byte `entry_value` window before the ULEB terminates → `end of data ... reading LEB`.
This was observed on a real GCC `-gdwarf-5 -O3` binary (AArch64) at a `DW_AT_call_value` attribute. It is platform-agnostic and should also reproduce on `x86_64`, and applies to any `BaseTypeRef` op nested in `DW_OP_entry_value`.
> Note: BOLT always re-encodes a BaseTypeRef operand as a fixed 4-byte ULEB, even when the input uses a single byte (e.g. 0x3f). The defect therefore does not depend on whether the referenced offset actually grows — it is the padding itself that changes the block size.
## Potential fix which can be considered
Keep the `DW_OP_entry_value` length in sync with the (possibly padded) sub-_expression_. Because `BaseTypeRef` offsets are already emitted at a fixed 4-byte width, the output size is stable across the INIT/PATCH stages, so the length can be written once and patched in place.
Track open `entry_value` scopes; emit each length as a fixed-width placeholder and back-patch it when the sub-_expression_ ends. Sketch of `DIEBuilder::cloneExpression()` from `bolt/lib/Core/DIEBuilder.cpp`:
``` diff
@@ -742,8 +742,52 @@ bool DIEBuilder::cloneExpression(const DataExtractor &Data,
using Descr = DWARFExpression::Operation::Description;
uint64_t OpOffset = 0;
bool DoesContainReference = false;
+
+ // DW_OP_entry_value / DW_OP_GNU_entry_value carry a leading ULEB byte-length
+ // for their sub-_expression_. LLVM's DWARFExpression iterator does not recurse
+ // into that sub-_expression_ nor enforce its boundary, so its operations are
+ // visited flat, right after the entry_value operation. Because a nested
+ // BaseTypeRef operand may be re-encoded to a wider (padded) ULEB below, the
+ // enclosing length must be recomputed; otherwise the emitted block is
+ // internally inconsistent and strict consumers (e.g. GNU readelf) fail with
+ // "end of data encountered whilst reading LEB". Track open entry_value scopes
+ // and back-patch their length once their sub-_expression_ ends. The length is
+ // written at a fixed 4-byte width so the patch never shifts following bytes.
+
+ struct EntryValueScope {
+ size_t LenFieldPos; // position of the 4-byte length field in the output
+ size_t BodyStartPos; // position where the sub-_expression_ body begins
+ uint64_t InputEndOffset; // input offset where the sub-_expression_ ends
+ };
+ SmallVector<EntryValueScope, 2> EVScopes;
+ auto CloseFinishedScopes = [&](uint64_t CurInputOffset) {
+ while (!EVScopes.empty() &&
+ EVScopes.back().InputEndOffset <= CurInputOffset) {
+ const EntryValueScope S = EVScopes.pop_back_val();
+ const uint64_t BodyLen = OutputBuffer.size() - S.BodyStartPos;
+ encodeULEB128(BodyLen, &OutputBuffer[S.LenFieldPos], /*PadTo=*/4);
+ }
+ };
+
for (const DWARFExpression::Operation &Op : _expression_) {
+ CloseFinishedScopes(OpOffset);
const Descr &Description = Op.getDescription();
+
+ if (Op.getCode() == dwarf::DW_OP_entry_value ||
+ Op.getCode() == dwarf::DW_OP_GNU_entry_value) {
+ // Emit the opcode and reserve a fixed-width length placeholder; the real
+ // length is patched in once the (possibly padded) body has been emitted.
+ const uint64_t DeclaredLen = Op.getRawOperand(0);
+ OutputBuffer.push_back(Op.getCode());
+ const size_t LenFieldPos = OutputBuffer.size();
+ uint8_t Pad[4];
+ encodeULEB128(0, Pad, /*PadTo=*/4);
+ OutputBuffer.append(Pad, Pad + 4);
+ EVScopes.push_back({LenFieldPos, OutputBuffer.size(),
+ Op.getEndOffset() + DeclaredLen});
+ OpOffset = Op.getEndOffset();
+ continue;
+ }
+
// DW_OP_const_type is variable-length and has 3
// operands. Thus far we only support 2.
if ((Description.Op.size() == 2 &&
@@ -807,6 +851,8 @@ bool DIEBuilder::cloneExpression(const DataExtractor &Data,
}
OpOffset = Op.getEndOffset();
}
+ // Patch any entry_value scopes whose sub-_expression_ runs to the end.
+ CloseFinishedScopes(UINT64_MAX);
return DoesContainReference;
}
```
## Environment
- **BOLT:** `llvm-bolt` (`BOLT version: bd6adfedc776c07caf158e59367d9c246c933510`)
- **Target arch:** AArch64 (also expected on x86_64)
- **Compiler:** g++ (GCC) 14.2.0
- **Binutils:** GNU readelf (GNU Binutils) 2.46
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs