[lld] [lld][ELF] refresh .eh_frame_hdr cache (PR #226402)

Martin Levesque via llvm-commits llvm-commits at lists.llvm.org
Fri Sep 25 01:58:07 PDT 2026


https://github.com/wehzzz created https://github.com/llvm/llvm-project/pull/226402

Fixes https://github.com/llvm/llvm-project/issues/226166

>From f831ff8ad936f8ab7b7cafc2e3e5e4718e1baea4 Mon Sep 17 00:00:00 2001
From: Martin Levesque <martin.levesque at datadoghq.com>
Date: Fri, 25 Sep 2026 08:53:43 +0000
Subject: [PATCH] [lld][ELF] refresh .eh_frame_hdr cache

---
 lld/ELF/SyntheticSections.cpp                 | 86 ++++++++++++-------
 lld/ELF/SyntheticSections.h                   | 20 ++++-
 lld/ELF/Writer.cpp                            | 11 +++
 lld/test/ELF/eh-frame-hdr-cfi-jump-tables.s   | 81 +++++++++++++++++
 lld/test/ELF/eh-frame-hdr-optimize-bb-jumps.s | 39 +++++++++
 5 files changed, 200 insertions(+), 37 deletions(-)
 create mode 100644 lld/test/ELF/eh-frame-hdr-cfi-jump-tables.s
 create mode 100644 lld/test/ELF/eh-frame-hdr-optimize-bb-jumps.s

diff --git a/lld/ELF/SyntheticSections.cpp b/lld/ELF/SyntheticSections.cpp
index f59e676c2e6f5..67c7b5c719225 100644
--- a/lld/ELF/SyntheticSections.cpp
+++ b/lld/ELF/SyntheticSections.cpp
@@ -40,6 +40,7 @@
 #include "llvm/Support/Parallel.h"
 #include "llvm/Support/TimeProfiler.h"
 #include <cinttypes>
+#include <cstddef>
 #include <cstdlib>
 
 using namespace llvm;
@@ -394,8 +395,9 @@ void EhFrameSection::writeTo(uint8_t *buf) {
   if (!hdr || !hdr->getParent())
     return;
 
-  // Write the .eh_frame_hdr section using cached FDE data from updateAllocSize.
-  bool large = hdr->large;
+  // Write the .eh_frame_hdr section using cached FDE data computed by
+  // computeFdeTable().
+  bool large = hdr->cache.large;
   int64_t ehFramePtr = getParent()->addr - hdr->getVA() - 4;
   auto writeField = [&](uint8_t *buf, uint64_t val) {
     large ? write64(ctx, buf, val) : write32(ctx, buf, val);
@@ -413,9 +415,9 @@ void EhFrameSection::writeTo(uint8_t *buf) {
   hdrBuf += 4;
   writeField(hdrBuf, ehFramePtr);
   hdrBuf += large ? 8 : 4;
-  write32(ctx, hdrBuf, hdr->fdes.size());
+  write32(ctx, hdrBuf, hdr->cache.fdes.size());
   hdrBuf += 4;
-  for (const FdeData &fde : hdr->fdes) {
+  for (const FdeData &fde : hdr->cache.fdes) {
     writeField(hdrBuf, fde.pcRel);
     writeField(hdrBuf + (large ? 8 : 4), fde.fdeVARel);
     hdrBuf += large ? 16 : 8;
@@ -433,27 +435,26 @@ bool EhFrameHeader::isNeeded() const {
   return isLive() && ctx.in.ehFrame->isNeeded();
 }
 
+static size_t ehFrameHdrSize(bool large, size_t numFdes) {
+  return 4 + (large ? 8 : 4) + 4 + numFdes * (large ? 16 : 8);
+}
+
 void EhFrameHeader::finalizeContents() {
   // Compute size: 4-byte header + eh_frame_ptr + fde_count + FDE table.
-  // Initially `large` is false; updateAllocSize may set it to true if addresses
-  // exceed the 32-bit range, then call finalizeContents again.
-  auto numFdes = ctx.in.ehFrame->numFdes;
-  size = 4 + (large ? 8 : 4) + 4 + numFdes * (large ? 16 : 8);
+  size = ehFrameHdrSize(cache.large, ctx.in.ehFrame->numFdes);
 }
 
-bool EhFrameHeader::updateAllocSize(Ctx &ctx) {
-  // This is called after `finalizeSynthetic`, so in the typical case without
-  // .relr.dyn, this function will not change the size and assignAddresses
-  // will not need another iteration.
+// Shared FDE-table computation for updateAllocSize() and refreshCache().
+EhFrameHeader::CachedFdeTable EhFrameHeader::computeFdeTable(Ctx &ctx) {
+  CachedFdeTable c;
   EhFrameSection *ehFrame = ctx.in.ehFrame.get();
   uint64_t hdrVA = getVA();
   int64_t ehFramePtr = ehFrame->getParent()->addr - hdrVA - 4;
   // Determine if 64-bit encodings are needed.
-  bool newLarge = !isInt<32>(ehFramePtr);
+  c.large = !isInt<32>(ehFramePtr);
 
   // Collect FDE entries. For each FDE, compute pcRel and fdeVARel relative to
   // .eh_frame_hdr's VA.
-  fdes.clear();
   for (CieRecord *rec : ehFrame->getCieRecords()) {
     uint8_t enc = getFdeEncoding(rec->cie);
     if ((enc & 0x70) != DW_EH_PE_absptr && (enc & 0x70) != DW_EH_PE_pcrel) {
@@ -468,38 +469,57 @@ bool EhFrameHeader::updateAllocSize(Ctx &ctx) {
       assert(isa<Defined>(reloc.sym) && "isFdeLive should have checked this");
       int64_t pcRel = reloc.sym->getVA(ctx) + reloc.addend - hdrVA;
       int64_t fdeVARel = ehFrame->getParent()->addr + fde->outputOff - hdrVA;
-      fdes.push_back({pcRel, fdeVARel});
-      newLarge |= !isInt<32>(pcRel) || !isInt<32>(fdeVARel);
+      c.fdes.push_back({pcRel, fdeVARel});
+      c.large |= !isInt<32>(pcRel) || !isInt<32>(fdeVARel);
     }
   }
 
   // Sort the FDE list by their PC and uniquify. Usually there is only one FDE
   // at an address, but there can be more than one FDEs pointing to the address.
-  llvm::stable_sort(
-      fdes, [](const EhFrameSection::FdeData &a,
-               const EhFrameSection::FdeData &b) { return a.pcRel < b.pcRel; });
-  fdes.erase(llvm::unique(fdes,
-                          [](const EhFrameSection::FdeData &a,
-                             const EhFrameSection::FdeData &b) {
-                            return a.pcRel == b.pcRel;
-                          }),
-             fdes.end());
-  ehFrame->numFdes = fdes.size();
-
-  large = newLarge;
-
-  // Compute size.
+  llvm::stable_sort(c.fdes, [](const EhFrameSection::FdeData &a,
+                               const EhFrameSection::FdeData &b) {
+    return a.pcRel < b.pcRel;
+  });
+  c.fdes.erase(llvm::unique(c.fdes,
+                            [](const EhFrameSection::FdeData &a,
+                               const EhFrameSection::FdeData &b) {
+                              return a.pcRel == b.pcRel;
+                            }),
+               c.fdes.end());
+  c.requiredSize = ehFrameHdrSize(c.large, c.fdes.size());
+  return c;
+}
+
+bool EhFrameHeader::updateAllocSize(Ctx &ctx) {
+  // This is called after `finalizeSynthetic`, so in the typical case without
+  // .relr.dyn, this function will not change the size and assignAddresses
+  // will not need another iteration.
   size_t oldSize = size;
-  finalizeContents();
+  EhFrameSection *ehFrame = ctx.in.ehFrame.get();
+  cache = computeFdeTable(ctx);
 
+  ehFrame->numFdes = cache.fdes.size();
   // Don't allow the section to shrink; otherwise the size of the section can
   // oscillate infinitely.
-  if (size < oldSize)
-    size = oldSize;
+  size = std::max(cache.requiredSize, oldSize);
 
   return size != oldSize;
 }
 
+void EhFrameHeader::refreshCache(Ctx &ctx) {
+  // Layout is frozen here, so the allocation cannot grow. A narrower table
+  // is fine. A table that does not fit is rejected.
+  CachedFdeTable c = computeFdeTable(ctx);
+  if (c.requiredSize > size) {
+    Err(ctx) << ".eh_frame_hdr needs " << c.requiredSize << " bytes but "
+             << size
+             << " were allocated; layout moved FDEs after the header was sized";
+    return;
+  }
+  ctx.in.ehFrame->numFdes = c.fdes.size();
+  cache = std::move(c);
+}
+
 GotSection::GotSection(Ctx &ctx)
     : SyntheticSection(ctx, ".got", SHT_PROGBITS, SHF_ALLOC | SHF_WRITE,
                        ctx.target->gotEntrySize) {
diff --git a/lld/ELF/SyntheticSections.h b/lld/ELF/SyntheticSections.h
index 85596e1339545..e3f737b35364a 100644
--- a/lld/ELF/SyntheticSections.h
+++ b/lld/ELF/SyntheticSections.h
@@ -103,11 +103,23 @@ class EhFrameHeader final : public SyntheticSection {
   void finalizeContents() override;
   bool updateAllocSize(Ctx &) override;
 
-  // Cached FDE data computed by updateAllocSize, used by
-  // EhFrameSection::writeTo.
-  SmallVector<EhFrameSection::FdeData, 0> fdes;
-  bool large = false; // Whether to use sdata8 encoding.
+  // Rebuild the cached search-table entries from the current addresses.
+  // Never changes size and never relayouts. If the rebuilt table does not
+  // fit the assigned capacity, reports an error.
+  void refreshCache(Ctx &);
+
+  // Cached FDE data computed by computeFdeTable(), used by
+  // EhFrameSection::writeTo().
+  struct CachedFdeTable {
+    SmallVector<EhFrameSection::FdeData, 0> fdes;
+    bool large = false; // Whether to use sdata8 encoding.
+    size_t requiredSize = 0;
+  };
+  CachedFdeTable cache;
   size_t size = 0;
+
+private:
+  CachedFdeTable computeFdeTable(Ctx &);
 };
 
 class GotSection final : public SyntheticSection {
diff --git a/lld/ELF/Writer.cpp b/lld/ELF/Writer.cpp
index 4fac47b548936..2de94ae2b69c7 100644
--- a/lld/ELF/Writer.cpp
+++ b/lld/ELF/Writer.cpp
@@ -1664,6 +1664,12 @@ template <class ELFT> void Writer<ELFT>::finalizeAddressDependentContent() {
   // Sizes are no longer allowed to grow, so all allowable spills have been
   // taken. Remove any leftover potential spills.
   ctx.script->erasePotentialSpillSections();
+
+  // Refresh the header from the final addresses as relaxCFIJumpTables() can
+  // move input sections without changing output-section start addresses, so the
+  // cache from inside the loop can be stale (#226166).
+  if (!errCount(ctx) && ctx.in.ehFrameHdr && ctx.in.ehFrameHdr->isNeeded())
+    ctx.in.ehFrameHdr->refreshCache(ctx);
 }
 
 // If Input Sections have been shrunk (basic block sections) then
@@ -1749,6 +1755,11 @@ template <class ELFT> void Writer<ELFT>::optimizeBasicBlockJumps() {
   for (OutputSection *osec : ctx.outputSections)
     for (InputSection *is : getInputSections(*osec, storage))
       is->trim();
+
+  // The jump deletions above shrank input sections and moved symbol values.
+  // Refresh the header so its keys match the final PCs.
+  if (ctx.in.ehFrameHdr && ctx.in.ehFrameHdr->isNeeded())
+    ctx.in.ehFrameHdr->refreshCache(ctx);
 }
 
 // Sections that finalizeAddressDependentContent may add to.
diff --git a/lld/test/ELF/eh-frame-hdr-cfi-jump-tables.s b/lld/test/ELF/eh-frame-hdr-cfi-jump-tables.s
new file mode 100644
index 0000000000000..7a41839f44fe4
--- /dev/null
+++ b/lld/test/ELF/eh-frame-hdr-cfi-jump-tables.s
@@ -0,0 +1,81 @@
+# REQUIRES: x86
+## Check that .eh_frame_hdr search-table keys match the final PCs of the
+## FDEs they reference after CFI jump-table relaxation (issue #226166).
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64 %s -o %t.o
+# RUN: ld.lld --eh-frame-hdr -O2 %t.o -shared -o %t
+# RUN: llvm-readelf --unwind %t | FileCheck %s
+# RUN: ld.lld --eh-frame-hdr -O1 --branch-to-branch %t.o -shared -o %t2
+# RUN: llvm-readelf --unwind %t2 | FileCheck %s
+
+# CHECK:      fde_count: 1
+# CHECK:      entry 0 {
+# CHECK-NEXT:   initial_location: [[PC:0x[0-9a-f]+]]
+# CHECK-NEXT:   address: [[FDE:0x[0-9a-f]+]]
+# CHECK:      [{{ *}}[[FDE]]{{ *}}] FDE
+# CHECK-NEXT:   initial_location: [[PC]]
+
+.section .text.jt,"ax", at llvm_cfi_jump_table,8
+.type f1, at function
+f1:
+  jmp f1.cfi
+  .balign 8, 0xcc
+.type f2, at function
+f2:
+  jmp f2.cfi
+  .balign 8, 0xcc
+.type f3, at function
+f3:
+  jmp f3.cfi
+  .balign 8, 0xcc
+.type f4, at function
+f4:
+  jmp f4.cfi
+  .balign 8, 0xcc
+
+.section .text.f1,"ax", at progbits
+f1.cfi:
+  ret
+  nop
+  nop
+  nop
+  nop
+  nop
+  nop
+  nop
+  nop
+  nop
+
+.section .text.f2,"ax", at progbits
+f2.cfi:
+  ret
+  nop
+  nop
+  nop
+  nop
+  nop
+  nop
+  nop
+  nop
+  nop
+
+.section .text.f3,"ax", at progbits
+f3.cfi:
+  ret
+  nop
+  nop
+  nop
+  nop
+  nop
+  nop
+  nop
+  nop
+  nop
+
+.section .text.f4,"ax", at progbits
+.type f4.cfi, at function
+f4.cfi:
+  .cfi_startproc
+  ret
+  .cfi_endproc
+.size f4.cfi, .-f4.cfi
\ No newline at end of file
diff --git a/lld/test/ELF/eh-frame-hdr-optimize-bb-jumps.s b/lld/test/ELF/eh-frame-hdr-optimize-bb-jumps.s
new file mode 100644
index 0000000000000..49dae76560c36
--- /dev/null
+++ b/lld/test/ELF/eh-frame-hdr-optimize-bb-jumps.s
@@ -0,0 +1,39 @@
+# REQUIRES: x86
+## Check that .eh_frame_hdr search-table keys match the final PCs of the
+## FDEs they reference after --optimize-bb-jumps.
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64 %s -o %t.o
+# RUN: ld.lld --eh-frame-hdr --optimize-bb-jumps %t.o -o %t
+# RUN: llvm-readelf --unwind %t | FileCheck %s
+
+# CHECK:      fde_count: 2
+# CHECK:      entry 0 {
+# CHECK-NEXT:   initial_location: [[PC0:0x[0-9a-f]+]]
+# CHECK-NEXT:   address: [[FDE0:0x[0-9a-f]+]]
+# CHECK:      entry 1 {
+# CHECK-NEXT:   initial_location: [[PC1:0x[0-9a-f]+]]
+# CHECK-NEXT:   address: [[FDE1:0x[0-9a-f]+]]
+# CHECK:      [{{ *}}[[FDE0]]{{ *}}] FDE
+# CHECK-NEXT:   initial_location: [[PC0]]
+# CHECK:      [{{ *}}[[FDE1]]{{ *}}] FDE
+# CHECK-NEXT:   initial_location: [[PC1]]
+
+.section .text,"ax", at progbits,unique,1
+.type foo, at function
+foo:
+  nop
+  jmp a.BB.foo
+
+.section .text,"ax", at progbits,unique,2
+.type a.BB.foo, at function
+a.BB.foo:
+  .cfi_startproc
+  nop
+  .cfi_endproc
+
+.section .text,"ax", at progbits,unique,3
+.type r.BB.foo, at function
+r.BB.foo:
+  .cfi_startproc
+  nop
+  .cfi_endproc



More information about the llvm-commits mailing list