[compiler-rt] [llvm] [ORC][MachO] Use a per-graph compact-unwind dso_base (PR #208931)
Yaxing Cai via llvm-commits
llvm-commits at lists.llvm.org
Sat Jul 11 11:05:49 PDT 2026
https://github.com/cyx-6 created https://github.com/llvm/llvm-project/pull/208931
Fixes #208930.
### Problem
`MachOPlatform` pins the `__jitlink$libunwind_dso_base` symbol to the JITDylib header so all objects in a JITDylib share one compact-unwind base. The `__unwind_info` writer encodes function/LSDA/personality offsets as **unsigned** 32-bit deltas from that base (`CompactUnwindSupport.h`), so any object whose code is emitted below the header underflows and aborts linking with `... exceeds 32 bits` (or `... out of 32-bit delta range of compact-unwind base ...`).
### Fix
Stop pinning the shared base. The compact-unwind writer then falls back to synthesizing a **per-graph** local Mach-O header (`getOrCreateCompactUnwindBase` -> `getOrCreateLocalMachOHeader`), which is forced to lay out first and so is always `<=` every covered address — the same base the standalone (no-platform) path already uses.
libunwind reconstructs addresses at runtime as `offset + dso_base`, so the base used to **encode** the deltas must equal the `dso_base` used to **decode** them. The runtime previously hard-coded `dso_base` to the JITDylib header. This threads the per-graph base through the `register`/`deregister_object_platform_sections` wire (a new `DSOBase` field on `UnwindSectionInfo`) so orc-rt reports it as `dso_base`, falling back to the JITDylib header when no base was recorded (e.g. DWARF-only registration). The newer `UnwindInfoRegistrationPlugin`/`UnwindInfoManager` standalone path already threads an explicit `DSOBase`; this brings the built-in `MachOPlatform` path in line.
### Tests
- `llvm/test/ExecutionEngine/JITLink/{x86-64,AArch64}/MachO_compact_unwind_dso_base.s` — `-noexec` regression tests: the per-graph local-header fallback links cleanly even at a high slab address (positive), and a base pinned above the code underflows with `exceeds 32 bits` (negative). These run in ordinary Linux pre-merge CI, since the compact-unwind writer runs at graph-build time.
- `compiler-rt/test/orc/TestCases/Darwin/{arm64,x86-64}/lljit-per-graph-compact-unwind-base.cpp` — orc-rt execution tests (post-merge Darwin bots): an exception thrown across a separately-linked object's frames, exercising the runtime `dso_base` path.
### Notes
The link-time underflow was reproduced deterministically on real Apple Silicon with a released LLVM toolchain and cross-linking Mach-O on Linux. `x86_64` and `arm64` share the one `CompactUnwindManager` writer, so the fix and the JITLink tests cover both.
>From 860098a88c43634d6b64ca553d24f4f054c0f69f Mon Sep 17 00:00:00 2001
From: Yaxing Cai <caiyaxing666 at gmail.com>
Date: Sat, 11 Jul 2026 18:02:51 +0000
Subject: [PATCH] [ORC][MachO] Use a per-graph compact-unwind dso_base
MachOPlatform pinned the "__jitlink$libunwind_dso_base" symbol to the JITDylib
header so that all objects in a JITDylib shared one compact-unwind base. The
__unwind_info writer encodes function/LSDA/personality offsets as *unsigned*
32-bit deltas from that base (CompactUnwindSupport.h), so any object whose code
is emitted below the header underflows and aborts linking with
"... exceeds 32 bits".
Stop pinning the shared base. The compact-unwind writer then falls back to
synthesizing a per-graph local Mach-O header (getOrCreateCompactUnwindBase ->
getOrCreateLocalMachOHeader), which is forced to lay out first and so is always
<= every covered address.
Because libunwind reconstructs addresses at runtime as offset + dso_base, the
base used to *encode* the deltas must equal the dso_base used to *decode* them.
The runtime previously hard-coded dso_base to the JITDylib header. Thread the
per-graph base through the register/deregister wire (a new DSOBase field on
UnwindSectionInfo) so orc-rt reports it as dso_base, falling back to the
JITDylib header when no base was recorded (e.g. DWARF-only registration).
Add JITLink -noexec regression tests (x86-64 and AArch64) covering the
local-header fallback and the underflow, and Darwin orc-rt execution tests
(arm64 and x86-64) exercising an exception thrown across a separately-linked
object's frames.
---
compiler-rt/lib/orc/macho_platform.cpp | 25 +++++++---
.../lljit-per-graph-compact-unwind-base.cpp | 40 ++++++++++++++++
.../lljit-per-graph-compact-unwind-base.cpp | 40 ++++++++++++++++
.../llvm/ExecutionEngine/Orc/MachOPlatform.h | 1 +
.../lib/ExecutionEngine/Orc/MachOPlatform.cpp | 33 ++++++++-----
.../AArch64/MachO_compact_unwind_dso_base.s | 39 ++++++++++++++++
.../x86-64/MachO_compact_unwind_dso_base.s | 46 +++++++++++++++++++
7 files changed, 206 insertions(+), 18 deletions(-)
create mode 100644 compiler-rt/test/orc/TestCases/Darwin/arm64/lljit-per-graph-compact-unwind-base.cpp
create mode 100644 compiler-rt/test/orc/TestCases/Darwin/x86-64/lljit-per-graph-compact-unwind-base.cpp
create mode 100644 llvm/test/ExecutionEngine/JITLink/AArch64/MachO_compact_unwind_dso_base.s
create mode 100644 llvm/test/ExecutionEngine/JITLink/x86-64/MachO_compact_unwind_dso_base.s
diff --git a/compiler-rt/lib/orc/macho_platform.cpp b/compiler-rt/lib/orc/macho_platform.cpp
index 1443c6c078fde..254e8270f087e 100644
--- a/compiler-rt/lib/orc/macho_platform.cpp
+++ b/compiler-rt/lib/orc/macho_platform.cpp
@@ -114,30 +114,33 @@ class SPSSerializationTraits<SPSMachOJITDylibDepInfo, MachOJITDylibDepInfo> {
struct UnwindSectionInfo {
std::vector<ExecutorAddrRange> CodeRanges;
+ ExecutorAddr DSOBase;
ExecutorAddrRange DwarfSection;
ExecutorAddrRange CompactUnwindSection;
};
using SPSUnwindSectionInfo =
- SPSTuple<SPSSequence<SPSExecutorAddrRange>, SPSExecutorAddrRange,
- SPSExecutorAddrRange>;
+ SPSTuple<SPSSequence<SPSExecutorAddrRange>, SPSExecutorAddr,
+ SPSExecutorAddrRange, SPSExecutorAddrRange>;
template <>
class SPSSerializationTraits<SPSUnwindSectionInfo, UnwindSectionInfo> {
public:
static size_t size(const UnwindSectionInfo &USI) {
return SPSUnwindSectionInfo::AsArgList::size(
- USI.CodeRanges, USI.DwarfSection, USI.CompactUnwindSection);
+ USI.CodeRanges, USI.DSOBase, USI.DwarfSection, USI.CompactUnwindSection);
}
static bool serialize(SPSOutputBuffer &OB, const UnwindSectionInfo &USI) {
return SPSUnwindSectionInfo::AsArgList::serialize(
- OB, USI.CodeRanges, USI.DwarfSection, USI.CompactUnwindSection);
+ OB, USI.CodeRanges, USI.DSOBase, USI.DwarfSection,
+ USI.CompactUnwindSection);
}
static bool deserialize(SPSInputBuffer &IB, UnwindSectionInfo &USI) {
return SPSUnwindSectionInfo::AsArgList::deserialize(
- IB, USI.CodeRanges, USI.DwarfSection, USI.CompactUnwindSection);
+ IB, USI.CodeRanges, USI.DSOBase, USI.DwarfSection,
+ USI.CompactUnwindSection);
}
};
@@ -171,9 +174,11 @@ class MachOPlatformRuntimeState {
struct UnwindSections {
UnwindSections(const UnwindSectionInfo &USI)
- : DwarfSection(USI.DwarfSection.toSpan<char>()),
+ : DSOBase(USI.DSOBase.getValue()),
+ DwarfSection(USI.DwarfSection.toSpan<char>()),
CompactUnwindSection(USI.CompactUnwindSection.toSpan<char>()) {}
+ uintptr_t DSOBase;
span<char> DwarfSection;
span<char> CompactUnwindSection;
};
@@ -968,7 +973,13 @@ bool MachOPlatformRuntimeState::lookupUnwindSections(
auto &JD = KV.second;
auto I = JD.UnwindSections.find(reinterpret_cast<char *>(Addr));
if (I != JD.UnwindSections.end()) {
- Info.dso_base = reinterpret_cast<uintptr_t>(JD.Header);
+ // Decode unwind-info offsets against the same base the JITLink
+ // compact-unwind writer used to encode them (a per-graph local Mach-O
+ // header). Fall back to the JITDylib header if no base was recorded
+ // (e.g. DWARF-only registration).
+ Info.dso_base = I->second.DSOBase
+ ? I->second.DSOBase
+ : reinterpret_cast<uintptr_t>(JD.Header);
Info.dwarf_section =
reinterpret_cast<uintptr_t>(I->second.DwarfSection.data());
Info.dwarf_section_length = I->second.DwarfSection.size();
diff --git a/compiler-rt/test/orc/TestCases/Darwin/arm64/lljit-per-graph-compact-unwind-base.cpp b/compiler-rt/test/orc/TestCases/Darwin/arm64/lljit-per-graph-compact-unwind-base.cpp
new file mode 100644
index 0000000000000..fb713ce033bab
--- /dev/null
+++ b/compiler-rt/test/orc/TestCases/Darwin/arm64/lljit-per-graph-compact-unwind-base.cpp
@@ -0,0 +1,40 @@
+// Exercise compact-unwind through JIT-linked frames under MachOPlatform.
+//
+// Each object gets its own per-graph compact-unwind base (a local Mach-O
+// header) rather than sharing the JITDylib header, so unwinding works no
+// matter where in the address space a graph is emitted -- including below the
+// header. A throw that crosses a separately-linked object's frame forces the
+// unwinder to decode __unwind_info for a graph other than the one holding the
+// header.
+//
+// RUN: %clangxx -fexceptions -fPIC -emit-llvm -c -o %t.throw.bc %s
+// RUN: %clangxx -DMAIN -fexceptions -fPIC -emit-llvm -c -o %t.main.bc %s
+// RUN: %lli_orc_jitlink -relocation-model=pic -extra-module %t.throw.bc \
+// RUN: %t.main.bc | FileCheck %s
+
+// CHECK: in throw_it
+// CHECK-NEXT: caught 42
+
+#include <stdio.h>
+
+#ifdef MAIN
+
+void throw_it();
+
+int main() {
+ try {
+ throw_it();
+ } catch (int X) {
+ printf("caught %d\n", X);
+ }
+ return 0;
+}
+
+#else
+
+void throw_it() {
+ puts("in throw_it");
+ throw 42;
+}
+
+#endif
diff --git a/compiler-rt/test/orc/TestCases/Darwin/x86-64/lljit-per-graph-compact-unwind-base.cpp b/compiler-rt/test/orc/TestCases/Darwin/x86-64/lljit-per-graph-compact-unwind-base.cpp
new file mode 100644
index 0000000000000..fb713ce033bab
--- /dev/null
+++ b/compiler-rt/test/orc/TestCases/Darwin/x86-64/lljit-per-graph-compact-unwind-base.cpp
@@ -0,0 +1,40 @@
+// Exercise compact-unwind through JIT-linked frames under MachOPlatform.
+//
+// Each object gets its own per-graph compact-unwind base (a local Mach-O
+// header) rather than sharing the JITDylib header, so unwinding works no
+// matter where in the address space a graph is emitted -- including below the
+// header. A throw that crosses a separately-linked object's frame forces the
+// unwinder to decode __unwind_info for a graph other than the one holding the
+// header.
+//
+// RUN: %clangxx -fexceptions -fPIC -emit-llvm -c -o %t.throw.bc %s
+// RUN: %clangxx -DMAIN -fexceptions -fPIC -emit-llvm -c -o %t.main.bc %s
+// RUN: %lli_orc_jitlink -relocation-model=pic -extra-module %t.throw.bc \
+// RUN: %t.main.bc | FileCheck %s
+
+// CHECK: in throw_it
+// CHECK-NEXT: caught 42
+
+#include <stdio.h>
+
+#ifdef MAIN
+
+void throw_it();
+
+int main() {
+ try {
+ throw_it();
+ } catch (int X) {
+ printf("caught %d\n", X);
+ }
+ return 0;
+}
+
+#else
+
+void throw_it() {
+ puts("in throw_it");
+ throw 42;
+}
+
+#endif
diff --git a/llvm/include/llvm/ExecutionEngine/Orc/MachOPlatform.h b/llvm/include/llvm/ExecutionEngine/Orc/MachOPlatform.h
index 944f099c38b61..936a360c8623c 100644
--- a/llvm/include/llvm/ExecutionEngine/Orc/MachOPlatform.h
+++ b/llvm/include/llvm/ExecutionEngine/Orc/MachOPlatform.h
@@ -242,6 +242,7 @@ class LLVM_ABI MachOPlatform : public Platform {
private:
struct UnwindSections {
SmallVector<ExecutorAddrRange> CodeRanges;
+ ExecutorAddr DSOBase;
ExecutorAddrRange DwarfSection;
ExecutorAddrRange CompactUnwindSection;
};
diff --git a/llvm/lib/ExecutionEngine/Orc/MachOPlatform.cpp b/llvm/lib/ExecutionEngine/Orc/MachOPlatform.cpp
index d7c0eb6bba2fd..ae81a4e8e338d 100644
--- a/llvm/lib/ExecutionEngine/Orc/MachOPlatform.cpp
+++ b/llvm/lib/ExecutionEngine/Orc/MachOPlatform.cpp
@@ -834,12 +834,13 @@ void MachOPlatform::MachOPlatformPlugin::modifyPassConfig(
if (auto *CUSec = LG.findSectionByName(MachOCompactUnwindSectionName))
LG.removeSection(*CUSec);
- // Point the libunwind dso-base absolute symbol at the header for the
- // JITDylib. This will prevent us from synthesizing a new header for
- // every object.
- if (HeaderAddr)
- LG.addAbsoluteSymbol("__jitlink$libunwind_dso_base", HeaderAddr, 0,
- Linkage::Strong, Scope::Local, true);
+ // Note: we intentionally do *not* pin "__jitlink$libunwind_dso_base" to the
+ // JITDylib header. A shared base makes the __unwind_info writer encode
+ // unsigned 32-bit deltas from the header, so any object emitted below it
+ // underflows ("... exceeds 32 bits"). Instead the compact-unwind writer
+ // synthesizes a per-graph local header base (getOrCreateCompactUnwindBase),
+ // which always lays out first, and findUnwindSectionInfo threads that base to
+ // the runtime so the encode and decode (dso_base) bases match.
// If we're in the bootstrap phase then increment the active graphs.
if (LLVM_UNLIKELY(InBootstrapPhase))
@@ -1320,6 +1321,16 @@ MachOPlatform::MachOPlatformPlugin::findUnwindSectionInfo(
if (CodeBlocks.empty())
return std::nullopt;
+ // Record the compact-unwind base that the __unwind_info writer used to encode
+ // its deltas, so the runtime decodes them against the same base. This is the
+ // per-graph "__jitlink$libunwind_dso_base" symbol; if absent (e.g. DWARF-only)
+ // US.DSOBase stays null and the runtime falls back to the JITDylib header.
+ auto DSOBaseName = G.intern("__jitlink$libunwind_dso_base");
+ if (auto *DSOBaseSym = G.findAbsoluteSymbolByName(DSOBaseName))
+ US.DSOBase = DSOBaseSym->getAddress();
+ else if (auto *DSOBaseSym = G.findDefinedSymbolByName(DSOBaseName))
+ US.DSOBase = DSOBaseSym->getAddress();
+
// We have info to register. Sort the code blocks into address order and
// build a list of contiguous address ranges covering them all.
llvm::sort(CodeBlocks, [](const Block *LHS, const Block *RHS) {
@@ -1413,12 +1424,12 @@ Error MachOPlatform::MachOPlatformPlugin::registerObjectPlatformSections(
MachOPlatformSecs.push_back({SecName, R.getRange()});
}
- std::optional<std::tuple<SmallVector<ExecutorAddrRange>, ExecutorAddrRange,
- ExecutorAddrRange>>
+ std::optional<std::tuple<SmallVector<ExecutorAddrRange>, ExecutorAddr,
+ ExecutorAddrRange, ExecutorAddrRange>>
UnwindInfo;
if (auto UI = findUnwindSectionInfo(G))
- UnwindInfo = std::make_tuple(std::move(UI->CodeRanges), UI->DwarfSection,
- UI->CompactUnwindSection);
+ UnwindInfo = std::make_tuple(std::move(UI->CodeRanges), UI->DSOBase,
+ UI->DwarfSection, UI->CompactUnwindSection);
if (!MachOPlatformSecs.empty() || UnwindInfo) {
// Dump the scraped inits.
@@ -1431,7 +1442,7 @@ Error MachOPlatform::MachOPlatformPlugin::registerObjectPlatformSections(
assert(HeaderAddr && "Null header registered for JD");
using SPSRegisterObjectPlatformSectionsArgs = SPSArgList<
SPSExecutorAddr,
- SPSOptional<SPSTuple<SPSSequence<SPSExecutorAddrRange>,
+ SPSOptional<SPSTuple<SPSSequence<SPSExecutorAddrRange>, SPSExecutorAddr,
SPSExecutorAddrRange, SPSExecutorAddrRange>>,
SPSSequence<SPSTuple<SPSString, SPSExecutorAddrRange>>>;
diff --git a/llvm/test/ExecutionEngine/JITLink/AArch64/MachO_compact_unwind_dso_base.s b/llvm/test/ExecutionEngine/JITLink/AArch64/MachO_compact_unwind_dso_base.s
new file mode 100644
index 0000000000000..cd583c74a5429
--- /dev/null
+++ b/llvm/test/ExecutionEngine/JITLink/AArch64/MachO_compact_unwind_dso_base.s
@@ -0,0 +1,39 @@
+# RUN: rm -rf %t && mkdir -p %t
+# RUN: llvm-mc -triple=arm64-apple-darwin -filetype=obj -o %t/pos.o %s
+# RUN: llvm-mc -triple=arm64-apple-darwin --defsym DSO_BASE_ABOVE=1 \
+# RUN: -filetype=obj -o %t/neg.o %s
+#
+# Check that the __unwind_info writer chooses a compact-unwind base that is at
+# or below every covered address, so that the unsigned 32-bit deltas it encodes
+# never underflow -- even when the object's code is emitted at a high address.
+#
+# Positive: with no "__jitlink$libunwind_dso_base" symbol the writer synthesizes
+# a per-graph local Mach-O header (laid out first, so lowest), and linking the
+# object at a high slab address succeeds.
+# RUN: llvm-jitlink -noexec -num-threads=0 -entry=_main \
+# RUN: -slab-allocate 1Mb -slab-address 0x800000000000 -slab-page-size 4096 \
+# RUN: %t/pos.o
+#
+# Negative: pinning the base *above* the code (as a shared JITDylib header does
+# when a graph lands below it) underflows and aborts linking. This is the bug
+# that a per-graph base avoids.
+# RUN: not llvm-jitlink -noexec -num-threads=0 -entry=_main \
+# RUN: -slab-allocate 1Mb -slab-address 0x1000 -slab-page-size 4096 \
+# RUN: %t/neg.o 2>&1 | FileCheck %s
+#
+# CHECK: exceeds 32 bits
+
+ .section __TEXT,__text,regular,pure_instructions
+ .globl _main
+ .p2align 2
+_main:
+ .cfi_startproc
+ ret
+ .cfi_endproc
+
+.ifdef DSO_BASE_ABOVE
+ .globl __jitlink$libunwind_dso_base
+ .set __jitlink$libunwind_dso_base, 0xffff000000000000
+.endif
+
+.subsections_via_symbols
diff --git a/llvm/test/ExecutionEngine/JITLink/x86-64/MachO_compact_unwind_dso_base.s b/llvm/test/ExecutionEngine/JITLink/x86-64/MachO_compact_unwind_dso_base.s
new file mode 100644
index 0000000000000..299e7651cb818
--- /dev/null
+++ b/llvm/test/ExecutionEngine/JITLink/x86-64/MachO_compact_unwind_dso_base.s
@@ -0,0 +1,46 @@
+# RUN: rm -rf %t && mkdir -p %t
+# RUN: llvm-mc -triple=x86_64-apple-darwin11 -filetype=obj -o %t/pos.o %s
+# RUN: llvm-mc -triple=x86_64-apple-darwin11 --defsym DSO_BASE_ABOVE=1 \
+# RUN: -filetype=obj -o %t/neg.o %s
+#
+# Check that the __unwind_info writer chooses a compact-unwind base that is at
+# or below every covered address, so that the unsigned 32-bit deltas it encodes
+# never underflow -- even when the object's code is emitted at a high address.
+#
+# Positive: with no "__jitlink$libunwind_dso_base" symbol the writer synthesizes
+# a per-graph local Mach-O header (laid out first, so lowest), and linking the
+# object at a high slab address succeeds.
+# RUN: llvm-jitlink -noexec -num-threads=0 -entry=_main \
+# RUN: -slab-allocate 1Mb -slab-address 0x800000000000 -slab-page-size 4096 \
+# RUN: %t/pos.o
+#
+# Negative: pinning the base *above* the code (as a shared JITDylib header does
+# when a graph lands below it) underflows and aborts linking. This is the bug
+# that a per-graph base avoids.
+# RUN: not llvm-jitlink -noexec -num-threads=0 -entry=_main \
+# RUN: -slab-allocate 1Mb -slab-address 0x1000 -slab-page-size 4096 \
+# RUN: %t/neg.o 2>&1 | FileCheck %s
+#
+# CHECK: exceeds 32 bits
+
+ .section __TEXT,__text,regular,pure_instructions
+ .globl _main
+ .p2align 4, 0x90
+_main:
+ .cfi_startproc
+ pushq %rbp
+ .cfi_def_cfa_offset 16
+ .cfi_offset %rbp, -16
+ movq %rsp, %rbp
+ .cfi_def_cfa_register %rbp
+ xorl %eax, %eax
+ popq %rbp
+ retq
+ .cfi_endproc
+
+.ifdef DSO_BASE_ABOVE
+ .globl __jitlink$libunwind_dso_base
+ .set __jitlink$libunwind_dso_base, 0xffff000000000000
+.endif
+
+.subsections_via_symbols
More information about the llvm-commits
mailing list