llvm / llvm/llvm-project

[BOLT] Dynamic relocations covering .gcc_except_table are dropped, corrupting the LSDA type table

Open
#220,298 1 comment 0 reactions 2 assignees Claimed by @aaupov View on GitHub
BOLT
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

I was enabling `-mlarge-eh-encoding` and we noticed that binaries applied with BOLT had corrupted LSDA.
AI was great here in helping me track down the issue because it showed up simply as the wrong `catch (...) { }` block being excercised.

## Context

When the LSDA TType encoding is `DW_EH_PE_absptr` (non-PIC x86-64, large code model which is triggered by `-large-eh-encoding`), each type table entry is an 8-byte absolute address. If the linker cannot resolve those statically it emits `R_X86_64_64` dynamic relocations into `.gcc_except_table`; the entries sit as zero in the file and the loader fills them at startup.

## Bug

BinaryEmitter::emitLSDA re-emits the type table with `Streamer.emitIntValue(TypeAddress, TTypeEncodingSize)`, a plain integer, with no relocation and no check for dynamic-relocation coverage on the input. The relocations stay pointed at the original section, so at runtime the loader populates `.bolt.org.gcc_except_table`, which nothing reads, while the live table stays zero.

A null type table entry (just zeroes) is a catch-all to `__gxx_personality_v0`, so the first catch clause silently swallows every exception. In the repro a std::invalid_argument is caught by catch (const std::bad_alloc&).

## Reproducer
`bolt/test/runtime/X86/exceptions-absptr-ttype-dynrelocs.cpp`:

```cpp
// RUN: %clangxx %cflags -O1 -fno-pic -flto -fuse-ld=lld -Wl,-q -Wl,-z,notext \
// RUN: -Wl,-plugin-opt=-large-eh-encoding %s -o %t.exe
// RUN: %t.exe
// RUN: llvm-bolt %t.exe -o %t.bolt --reorder-blocks=ext-tsp
// RUN: %t.bolt
// REQUIRES: system-linux
/// Check that BOLT preserves dynamic relocations covering the LSDA type table.
///
/// With DW_EH_PE_absptr TType encoding (selected for non-PIC x86-64 under the
/// large code model, which -large-eh-encoding also opts into), each type table
/// entry is an 8-byte absolute address. When the referenced typeinfo cannot be
/// resolved at static link time -- here because -z notext lets the linker place
/// R_X86_64_64 dynamic relocations in the read-only .gcc_except_table rather
/// than resolving them -- the entry is left as zero in the file and filled in
/// by the dynamic loader at startup.
///
/// BOLT re-emits .gcc_except_table but copies those entries as plain integers,
/// so the relocations are not carried over to the new section. The loader then
/// populates the original copy, which nothing reads, while the live table stays
/// zero. __gxx_personality_v0 treats a null type table entry as a catch-all, so
/// the *first* catch clause silently swallows every exception.
///
/// Before this is fixed the BOLT-processed binary exits non-zero because
/// classify() reports "bad_alloc" for a std::invalid_argument.
#include
#include
#include
#include
__attribute__((noinline)) void thrower() {
throw std::invalid_argument("payload");
}
/// bad_alloc is deliberately the first clause: if its type table entry reads as
/// null it becomes a catch-all and captures the invalid_argument below it.
__attribute__((noinline)) const char *classify() {
try {
throw;
} catch (const std::bad_alloc &) {
return "bad_alloc";
} catch (const std::bad_cast &) {
return "bad_cast";
} catch (const std::invalid_argument &) {
return "invalid_argument";
} catch (...) {
return "other";
}
}
int main() {
try {
thrower();
} catch (...) {
const char *Result = classify();
std::printf("caught as: %s\n", Result);
return std::strcmp(Result, "invalid_argument") == 0 ? 0 : 1;
}
return 2;
}
```

## Observed
```
$ ./repro.exe
caught as: invalid_argument # exit 0
$ llvm-bolt repro.exe -o repro.bolt --reorder-blocks=ext-tsp
$ ./repro.bolt
caught as: bad_alloc # exit 1 <-- wrong handler
```
Relocation coverage before and after, counting `.rela.dyn` entries whose
`r_offset` lands inside each section:
| section | before BOLT | after BOLT |
|---|---|---|
| `.gcc_except_table` (live) | 3 | **0** |
| `.bolt.org.gcc_except_table` (stale copy) | — | 3 |

The test passes the pre-BOLT run and fails the post-BOLT run. The program
self-checks, so no `FileCheck` is needed.

## Possible fixes
1. Carry over the dynamic relocations for `.gcc_except_table` contents that
BOLT rewrites, retargeted at the new section.
2. Resolve type table entries through their relocation's symbol at read time,
and re-emit them symbolically (as the `pcrel` path already does), so the
linker/loader path is preserved.
3. At minimum, refuse to process — or loudly warn about — a binary whose EH
tables carry dynamic relocations, rather than silently producing a binary
that dispatches exceptions to the wrong handler.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.