llvm / llvm/llvm-project

[BOLT] cloneExpression() silently truncates expressions at DWARF 5 typed operations (no descriptors for: DW_OP_const_type, DW_OP_deref_type,DW_OP_xderef_type, DW_OP_reinterpret)

Open
#213,758 1 comment 0 reactions 0 assignees View on GitHub
BOLT
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## Description

`DIEBuilder::cloneExpression()` walks a DWARF expression with LLVM's `DWARFExpression` iterator. LLVM's operation table has **no descriptors** for four DWARF 5 typed operations, so `Operation::extract()` fails on them and the iterator jumps straight to the end of the expression. BOLT then emits only the bytes it managed to visit, so the unsupported operation *and everything after it* is dropped - with no diagnostic, because the guard that was meant to report this requires a two-operand description that an errored operation never has.

Operations with no descriptor (`llvm/lib/DebugInfo/DWARF/LowLevel/DWARFExpression.cpp`):

| Opcode | Operation | Operands per DWARF 5 §7.7.1 DWARF Expressions (Table 7.9: DWARF operation encodings)|
| --- | --- | --- |
| `0xa4` | `DW_OP_const_type` | ULEB base type DIE offset, 1-byte size, block of that size |
| `0xa6` | `DW_OP_deref_type` | 1-byte size, ULEB base type DIE offset |
| `0xa7` | `DW_OP_xderef_type` | 1-byte size, ULEB base type DIE offset |
| `0xa9` | `DW_OP_reinterpret` | ULEB base type DIE offset |

## Environment

- **BOLT:** `llvm-bolt` (`BOLT version:` bd6adfedc776c07caf158e59367d9c246c933510)
- **Target arch:** Aarch64 and x86_64
- **Compiler:** g++ (GCC) 14.2.0
- **Binutils:** GNU readelf (GNU Binutils) 2.46

## Reproducer

That problem was identified during https://github.com/llvm/llvm-project/issues/208082 reporducer creation

### main.cpp

```cpp
volatile double sink;

__attribute__((noinline)) double callee(double a, double b) {
sink = a + b;
return sink;
}

__attribute__((noinline)) int caller(double x) { return (int)callee(x, 1.0); }

int main() { return caller(1.0); }
```

```
$ g++ -gdwarf-5 -O3 -gz=none main.cpp -o main
$ readelf -w main | grep -F DW_AT_call_value
<7e> DW_AT_call_value : 11 byte block: a4 2a 8 0 0 0 0 0 0 f0 3f (DW_OP_const_type: <0x2a> 8 byte block: 0 0 0 0 0 0 f0 3f )

$ readelf -wi main | grep -A1 'DW_TAG_volatile_type'
<1><47>: Abbrev Number: 6 (DW_TAG_volatile_type)
<48> DW_AT_type : <0x2a>

$ llvm-bolt main -o main.bolt --update-debug-sections
$ readelf -w main.bolt | grep -F DW_AT_call_value
<87> DW_AT_call_value : 0 byte block: ()

$ readelf -wi main.bolt | grep -A1 'DW_TAG_volatile_type'
readelf: Error: end of data encountered whilst reading LEB
<1><50>: Abbrev Number: 4 (DW_TAG_volatile_type)
<51> DW_AT_type : <0x33>
```

The input says the parameter of this call site is the `double` constant `0x3FF0000000000000` (`1.0`); after BOLT the attribute is empty. `llvm-bolt` prints no warning/errors.

## Analysis

`llvm/lib/DebugInfo/DWARF/LowLevel/DWARFExpression.cpp` - `getOpDescriptions()` leaves those four entries default-constructed, so `getDescImpl()` returns a description with `Version == DwarfNA` and `extract()` bails out:

```cpp
Desc = getOpDesc(Opcode);
if (Desc.Version == Operation::DwarfNA)
return false;
```

`llvm/include/llvm/DebugInfo/DWARF/LowLevel/DWARFExpression.h` - on error the iterator skips the remainder of the expression:

```cpp
iterator &operator++() {
Offset = Op.isError() ? Expr->Data.getData().size() : Op.EndOffset;
```

`bolt/lib/Core/DIEBuilder.cpp` - the "unsupported DW_OP encoding" warning cannot fire for an errored operation, because `Description.Op` is then empty:

```cpp
if ((Description.Op.size() == 2 &&
Description.Op[0] == Encoding::BaseTypeRef) || ...
BC.outs() << "BOLT-WARNING: [internal-dwarf-error]: unsupported DW_OP "
"encoding.\n";
```

## Proposed fix

### 1. `llvm/lib/DebugInfo/DWARF/LowLevel/DWARFExpression.cpp`

```diff
--- a/llvm/lib/DebugInfo/DWARF/LowLevel/DWARFExpression.cpp
+++ b/llvm/lib/DebugInfo/DWARF/LowLevel/DWARFExpression.cpp
@@ -93,8 +93,14 @@ static std::vector getOpDescriptions() {
Descriptions[DW_OP_constx] = Desc(Op::Dwarf5, Op::SizeLEB);
Descriptions[DW_OP_entry_value] = Desc(Op::Dwarf5, Op::SizeLEB);
Descriptions[DW_OP_convert] = Desc(Op::Dwarf5, Op::BaseTypeRef);
+ Descriptions[DW_OP_reinterpret] = Desc(Op::Dwarf5, Op::BaseTypeRef);
Descriptions[DW_OP_regval_type] =
Desc(Op::Dwarf5, Op::SizeLEB, Op::BaseTypeRef);
+ Descriptions[DW_OP_deref_type] = Desc(Op::Dwarf5, Op::Size1, Op::BaseTypeRef);
+ Descriptions[DW_OP_xderef_type] =
+ Desc(Op::Dwarf5, Op::Size1, Op::BaseTypeRef);
+ Descriptions[DW_OP_const_type] =
+ Desc(Op::Dwarf5, Op::BaseTypeRef, Op::Size1, Op::SizeBlock);
Descriptions[DW_OP_WASM_location] =
Desc(Op::Dwarf4, Op::SizeLEB, Op::WasmLocationArg);
Descriptions[DW_OP_GNU_push_tls_address] = Desc(Op::Dwarf3);
```

### 2. `bolt/lib/Core/DIEBuilder.cpp`
```diff
--- a/bolt/lib/Core/DIEBuilder.cpp
+++ b/bolt/lib/Core/DIEBuilder.cpp
@@ -770,6 +770,15 @@

for (const DWARFExpression::Operation &Op : Expression) {
+ // An operation that fails to decode makes the iterator jump straight to the
+ // end of the expression, so the rest of it would be dropped silently.
+ if (Op.isError()) {
+ BC.errs() << "BOLT-WARNING: [internal-dwarf-error]: unsupported DW_OP 0x"
+ << Twine::utohexstr(
+ static_cast(Data.getData()[OpOffset]))
+ << ", expression truncated.\n";
+ break;
+ }
const Descr &Description = Op.getDescription();

- // DW_OP_const_type is variable-length and has 3
- // operands. Thus far we only support 2.
- if ((Description.Op.size() == 2 &&
- Description.Op[0] == Encoding::BaseTypeRef) ||
- (Description.Op.size() == 2 &&
- Description.Op[1] == Encoding::BaseTypeRef &&
- Description.Op[0] != Encoding::Size1 &&
- Description.Op[0] != Encoding::SizeLEB))
- BC.outs() << "BOLT-WARNING: [internal-dwarf-error]: unsupported DW_OP "
- "encoding.\n";
-
- if ((Description.Op.size() == 1 &&
- Description.Op[0] == Encoding::BaseTypeRef) ||
- (Description.Op.size() == 2 &&
- Description.Op[1] == Encoding::BaseTypeRef &&
- (Description.Op[0] == Encoding::Size1 ||
- Description.Op[0] == Encoding::SizeLEB))) {
+ // A BaseTypeRef operand may appear at any position, and operands may
+ // follow it (DW_OP_const_type). Relocate the reference in place and copy
+ // the surrounding operands verbatim.
+ const Encoding *BaseTypeIt =
+ llvm::find(Description.Op, Encoding::BaseTypeRef);
+ if (BaseTypeIt != Description.Op.end()) {
+ const unsigned RefIdx = std::distance(Description.Op.begin(), BaseTypeIt);
assert(OpOffset < Op.getEndOffset());
- const uint32_t ULEBsize = Op.getEndOffset() - OpOffset - 1;
- (void)ULEBsize;
- assert(ULEBsize <= 16);

- // Copy over the operation.
+ // Copy over the operation and any operands preceding the reference.
OutputBuffer.push_back(Op.getCode());
- uint64_t RefOffset;
- if (Description.Op.size() == 1) {
- RefOffset = Op.getRawOperand(0);
- } else {
- const StringRef FirstOpBytes =
- Data.getData().slice(OpOffset + 1, Op.getOperandEndOffset(0));
- OutputBuffer.append(FirstOpBytes.begin(), FirstOpBytes.end());
- RefOffset = Op.getRawOperand(1);
- }
+ const uint64_t RefStart =
+ RefIdx == 0 ? OpOffset + 1 : Op.getOperandEndOffset(RefIdx - 1);
+ const StringRef LeadingOpBytes =
+ Data.getData().slice(OpOffset + 1, RefStart);
+ OutputBuffer.append(LeadingOpBytes.begin(), LeadingOpBytes.end());
+ const uint64_t RefOffset = Op.getRawOperand(RefIdx);
uint32_t Offset = 0;
if (RefOffset > 0 || Op.getCode() != dwarf::DW_OP_convert) {
DoesContainReference = true;
@@ -844,6 +837,11 @@
encodeULEB128(Offset, ULEB, 4);
ArrayRef ULEBbytes(ULEB, 4);
OutputBuffer.append(ULEBbytes.begin(), ULEBbytes.end());
+
+ // Copy over any operands following the reference.
+ const StringRef TrailingOpBytes = Data.getData().slice(
+ Op.getOperandEndOffset(RefIdx), Op.getEndOffset());
+ OutputBuffer.append(TrailingOpBytes.begin(), TrailingOpBytes.end());
} else {
// Copy over everything else unmodified.
const StringRef Bytes = Data.getData().slice(OpOffset, Op.getEndOffset());
```

> Please note that the lines of changes may differ from a clean master, as other fixes on the local branch were applied

With proposed fix:
```
<87> DW_AT_call_value : 14 byte block: a4 b3 80 80 0 8 0 0 0 0 0 0 f0 3f (DW_OP_const_type: <0x33> 8 byte block: 0 0 0 0 0 0 f0 3f )
```
The constant is preserved and, as a side effect of routing `DW_OP_const_type` through the reference path, its base type reference is now relocated as well (`0x2a` → `0x33`).

Contributor guide

Open the contributing guide

Research direction

Start with llvm/lib/DebugInfo/DWARF/LowLevel/DWARFExpression.cpp and llvm/include/llvm/DebugInfo/DWARF/LowLevel/DWARFExpression.h to trace decoding and iterator error handling, then inspect DIEBuilder::cloneExpression() in bolt/lib/Core/DIEBuilder.cpp. Reproduce with the supplied main.cpp, llvm-bolt, and readelf commands; done means typed DWARF operations and following bytes are preserved, references are relocated, and truncation produces a warning.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.