[lld] [lld][MachO] Preserve __eh_frame ordering during BP section sorting (PR #191412)

via llvm-commits llvm-commits at lists.llvm.org
Fri Apr 10 06:09:01 PDT 2026


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lld

Author: Karim Alweheshy (karim-alweheshy)

<details>
<summary>Changes</summary>

The Balanced Partitioning section orderer collects all live `ConcatInputSection`s as candidates for content-similarity reordering. This includes `__eh_frame` CIE and FDE records, which have internal ordering constraints: each FDE contains a backward-relative 32-bit offset to its parent CIE, requiring CIEs to precede their FDEs.

When the BP orderer assigns priorities to `__eh_frame` subsections and `Writer.cpp` sorts by those priorities, FDEs can end up before their parent CIEs. The resulting CIE-pointer offsets resolve correctly with 32-bit wrapping arithmetic but underflow with 64-bit pointer arithmetic, causing DWARF consumers (crash reporters, debuggers) to silently lose unwind data.

## Fix

Add a `Section::orderSensitive` flag to mark sections whose subsection ordering is semantically meaningful. Set it in `splitEhFrames()` and check it in the BP orderer's outer loop, skipping the entire section before any per-subsection work.

This is the only MachO section with this constraint:
- `__unwind_info` is a `SyntheticSection` (not a `ConcatOutputSection`), so it never enters the BP pipeline
- `__gcc_except_tab` LSDA entries are referenced by absolute offset, so reordering is safe
- `-order_file` is unaffected because it assigns priorities through symbol definitions (which live in `__text`, not `__eh_frame`). Only the BP orderer enumerates sections directly.

## Production impact

Verified on a large iOS application (~218 MB binary, ~34,000 FDEs) linked with `lld` + `--bp-compression-sort=both`.

### Static analysis of `__eh_frame`

Simulating 64-bit CIE pointer resolution on the output binary across multiple builds:

| Build | `__eh_frame` layout | FDEs resolved (64-bit) | FDEs failed |
|---|---|---|---|
| lld + BP sort | FDEs first | 15 / 34,257 | **34,242 (99.96%)** |
| lld + fix | CIEs first | 30,558 / 30,558 | **0** |

### Runtime verification

Proxied crash report uploads from a device running both the affected and fixed binaries:

|  | Affected build | Fixed build |
|---|---|---|
| Threads captured | 3 | 24 |
| Total frames | 28 | 135 |
| Background threads | 0-2 | 23 |

The affected build lost ~85% of thread data. The crash reporter could only unwind the crashed thread (via compact unwind). All background thread unwind data was silently dropped.

## Reproducer

Minimal test case (ARM64). Requires `--bp-compression-sort=both` to trigger:

```bash
llvm-mc -filetype=obj -emit-compact-unwind-non-canonical=true \
  -triple=arm64-apple-macos11.0 test.s -o test.o
ld64.lld -arch arm64 -platform_version macos 11.0 11.0 \
  -syslibroot $(xcrun --show-sdk-path) -lSystem -lc++ \
  test.o -o test --bp-compression-sort=both
llvm-objdump --dwarf=frames test
# Without fix: "error: parsing FDE data at 0x0 failed due to missing CIE"
# With fix: CIE records correctly precede their FDEs
```

Standalone reproducer with 64-bit CIE simulation script:
https://gist.github.com/karim-alweheshy/ae28196c4fbb295f81cc793cfbe0c1b7

## Test

The lit test creates multiple functions with `cfi_escape` (forcing DWARF unwind mode) and different personality functions (producing separate CIEs), then links with `--bp-compression-sort=both`. Verified on both x86_64 and arm64.

Made with [Cursor](https://cursor.com)

---
Full diff: https://github.com/llvm/llvm-project/pull/191412.diff


4 Files Affected:

- (modified) lld/MachO/BPSectionOrderer.cpp (+2) 
- (modified) lld/MachO/InputFiles.cpp (+1) 
- (modified) lld/MachO/InputFiles.h (+4) 
- (added) lld/test/MachO/eh-frame-ordering.s (+109) 


``````````diff
diff --git a/lld/MachO/BPSectionOrderer.cpp b/lld/MachO/BPSectionOrderer.cpp
index a9b5d07ac55e5..7eea27430461a 100644
--- a/lld/MachO/BPSectionOrderer.cpp
+++ b/lld/MachO/BPSectionOrderer.cpp
@@ -122,6 +122,8 @@ DenseMap<const InputSection *, int> lld::macho::runBalancedPartitioning(
   DenseMap<CachedHashStringRef, std::set<unsigned>> rootSymbolToSectionIdxs;
   for (const auto *file : inputFiles) {
     for (auto *sec : file->sections) {
+      if (sec->orderSensitive)
+        continue;
       for (auto &subsec : sec->subsections) {
         auto *isec = subsec.isec;
         if (!isec || isec->data.empty() || !isec->data.data())
diff --git a/lld/MachO/InputFiles.cpp b/lld/MachO/InputFiles.cpp
index cc7eae51175bc..62823ab4a6380 100644
--- a/lld/MachO/InputFiles.cpp
+++ b/lld/MachO/InputFiles.cpp
@@ -447,6 +447,7 @@ void ObjFile::splitEhFrames(ArrayRef<uint8_t> data, Section &ehFrameSection) {
                                             /*align=*/1)});
   }
   ehFrameSection.doneSplitting = true;
+  ehFrameSection.orderSensitive = true;
 }
 
 template <class T>
diff --git a/lld/MachO/InputFiles.h b/lld/MachO/InputFiles.h
index 8f2cba89abf48..735fb40af24bd 100644
--- a/lld/MachO/InputFiles.h
+++ b/lld/MachO/InputFiles.h
@@ -81,6 +81,10 @@ class Section {
   Section(Section &&) = delete;
   Section &operator=(Section &&) = delete;
 
+  // Subsection ordering is semantically meaningful and must not be reordered
+  // by priority-based sorting (e.g. __eh_frame CIE/FDE records).
+  bool orderSensitive = false;
+
 private:
   // Whether we have already split this section into individual subsections.
   // For sections that cannot be split (e.g. literal sections), this is always
diff --git a/lld/test/MachO/eh-frame-ordering.s b/lld/test/MachO/eh-frame-ordering.s
new file mode 100644
index 0000000000000..9f2305b5a7501
--- /dev/null
+++ b/lld/test/MachO/eh-frame-ordering.s
@@ -0,0 +1,109 @@
+# REQUIRES: x86, aarch64
+## Test that __eh_frame CIE/FDE ordering is preserved even when
+## priority-based section sorting (from BP compression sort, order files,
+## etc.) would otherwise reorder input sections. CIE records must precede
+## the FDE records that reference them; reordering breaks CIE pointer
+## resolution.
+
+# RUN: rm -rf %t; split-file %s %t
+
+## x86_64
+# RUN: llvm-mc -filetype=obj -emit-compact-unwind-non-canonical=true -triple=x86_64-apple-macos10.15 %t/test.s -o %t/test-x86_64.o
+# RUN: %lld -lSystem -lc++ %t/test-x86_64.o -o %t/test-x86_64 --bp-compression-sort=both
+# RUN: llvm-objdump --dwarf=frames %t/test-x86_64 2>&1 | FileCheck %s
+
+## arm64
+# RUN: llvm-mc -filetype=obj -emit-compact-unwind-non-canonical=true -triple=arm64-apple-macos11.0 %t/test.s -o %t/test-arm64.o
+# RUN: %lld -arch arm64 -lSystem -lc++ %t/test-arm64.o -o %t/test-arm64 --bp-compression-sort=both
+# RUN: llvm-objdump --dwarf=frames %t/test-arm64 2>&1 | FileCheck %s
+
+## Verify that CIE records precede their FDE records and that
+## FDE records successfully reference their CIEs (no parse errors).
+# CHECK: .eh_frame contents:
+# CHECK: {{[0-9a-f]+}} {{.*}} CIE
+# CHECK-NOT: error
+# CHECK-NOT: warning
+# CHECK: {{[0-9a-f]+}} {{.*}} FDE
+# CHECK-NOT: error
+# CHECK-NOT: warning
+# CHECK: {{[0-9a-f]+}} {{.*}} FDE
+
+#--- test.s
+.globl _my_personality_a, _my_personality_b, _main
+
+.text
+## _func_a uses cfi_escape to force DWARF unwind (can't be compact-encoded).
+## Uses personality A -> produces CIE_A + FDE for _func_a.
+.p2align 2
+_func_a:
+  .cfi_startproc
+  .cfi_personality 155, _my_personality_a
+  .cfi_lsda 16, Lexception_a
+  .cfi_def_cfa_offset 16
+  .cfi_escape 0x2e, 0x10
+  ret
+  .cfi_endproc
+
+## _func_b also uses personality A + cfi_escape -> reuses CIE_A, new FDE.
+.p2align 2
+_func_b:
+  .cfi_startproc
+  .cfi_personality 155, _my_personality_a
+  .cfi_lsda 16, Lexception_b
+  .cfi_def_cfa_offset 16
+  .cfi_escape 0x2e, 0x10
+  ret
+  .cfi_endproc
+
+## _func_c uses personality B + cfi_escape -> produces CIE_B + FDE.
+.p2align 2
+_func_c:
+  .cfi_startproc
+  .cfi_personality 155, _my_personality_b
+  .cfi_lsda 16, Lexception_c
+  .cfi_def_cfa_offset 16
+  .cfi_escape 0x2e, 0x10
+  ret
+  .cfi_endproc
+
+## _func_d uses personality B + cfi_escape -> reuses CIE_B, new FDE.
+.p2align 2
+_func_d:
+  .cfi_startproc
+  .cfi_personality 155, _my_personality_b
+  .cfi_lsda 16, Lexception_d
+  .cfi_def_cfa_offset 16
+  .cfi_escape 0x2e, 0x10
+  ret
+  .cfi_endproc
+
+.p2align 2
+_my_personality_a:
+  ret
+
+.p2align 2
+_my_personality_b:
+  ret
+
+.p2align 2
+_main:
+  ret
+
+.section __TEXT,__gcc_except_tab
+GCC_except_table_a:
+Lexception_a:
+  .byte 255
+
+GCC_except_table_b:
+Lexception_b:
+  .byte 255
+
+GCC_except_table_c:
+Lexception_c:
+  .byte 255
+
+GCC_except_table_d:
+Lexception_d:
+  .byte 255
+
+.subsections_via_symbols

``````````

</details>


https://github.com/llvm/llvm-project/pull/191412


More information about the llvm-commits mailing list