[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
Wed Jul 15 21:46:43 PDT 2026


https://github.com/cyx-6 updated https://github.com/llvm/llvm-project/pull/208931

>From e0f9c6ac4c55c94c58f8be7a1070b87fc23c11e7 Mon Sep 17 00:00:00 2001
From: Yaxing Cai <caiyaxing666 at gmail.com>
Date: Sun, 12 Jul 2026 06:43:40 +0000
Subject: [PATCH] [ORC][MachO] Use a per-graph compact-unwind dso_base

MachOPlatformPlugin::modifyPassConfig injected
"__jitlink$libunwind_dso_base" as an absolute symbol pointing at the
per-JITDylib MachO header, so all objects in a JITDylib shared one
compact-unwind base. CompactUnwindManager encodes each function / LSDA /
personality entry in __TEXT,__unwind_info as an *unsigned* 32-bit delta
from that base (CompactUnwindSupport.h).

The header and each user graph are independent JITLinkMemoryManager
allocations, mmap'd with no guaranteed relative order. When a user graph
lands below the header -- reliably reproducible on Darwin arm64 under the
mmap pressure of a process that has already dlopen'd several libraries --
the uint64_t delta underflows, isUInt<32> rejects it, and linking aborts
with e.g. "... exceeds 32 bits" / "out of 32-bit delta range of
compact-unwind base". |P - B| is often only a few KB; this is unrelated
to 4GB PC-relative limits.

The on-disk format is genuinely unsigned u32 offsets that libunwind adds
to the registered dso_base at runtime, so widening the writer to a signed
delta is not an option: the base itself must be at or below every covered
address.

Fix (LLVM + orc-rt, coordinated):

 * Stop injecting the absolute symbol.
   CompactUnwindManager::getOrCreateCompactUnwindBase then falls through
   to getOrCreateLocalMachOHeader, anchoring a per-graph
   __TEXT,__lcl_macho_hdr inside the user graph's own slab. Within one
   graph BasicLayout::segments() orders R -> RW -> RX in ascending
   addresses, so this base is always the lowest covered address and the
   u32 deltas stay non-negative at all four writer sites.

 * Because libunwind decodes addresses 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.
   Capture the per-graph base in findUnwindSectionInfo and thread it
   through the register/deregister_object_platform_sections wire (a new
   DSOBase field on UnwindSectionInfo), falling back to the JITDylib
   header when no base was recorded (e.g. DWARF-only registration).
   JD.Header keeps its role as the JITDylib identity for bookkeeping but
   stops doubling as the unwind dso_base.

The newer UnwindInfoRegistrationPlugin / UnwindInfoManager standalone
path already threads an explicit DSOBase; this brings the built-in
MachOPlatform path in line. Cost is one ~32-byte local header per user
object.

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        | 28 +++++++----
 .../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 | 34 +++++++++-----
 .../AArch64/MachO_compact_unwind_dso_base.s   | 39 ++++++++++++++++
 .../x86-64/MachO_compact_unwind_dso_base.s    | 46 +++++++++++++++++++
 7 files changed, 209 insertions(+), 19 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..7cbfe3684e9ff 100644
--- a/compiler-rt/lib/orc/macho_platform.cpp
+++ b/compiler-rt/lib/orc/macho_platform.cpp
@@ -114,30 +114,34 @@ 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);
+    return SPSUnwindSectionInfo::AsArgList::size(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 +175,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 +974,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..0bee63121e8ba 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,17 @@ 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 +1425,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 +1443,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