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

Reply via email to