[llvm] [ThinLTO] Record prevailing GUIDs by linker symbol name (PR #226911)

Fabian Meumertzheim via llvm-commits llvm-commits at lists.llvm.org
Mon Sep 28 00:48:12 PDT 2026


https://github.com/fmeum created https://github.com/llvm/llvm-project/pull/226911

`LTO::addThinLTO` updates `GlobalResolutions` using the IR name from a summary, but that map is keyed by mangled linker symbol names. On Mach-O, the IR name `_foo` can therefore match the resolution for `foo` (whose linker name is `_foo`). This assigns `_foo`'s GUID to `foo` and can incorrectly internalize `foo` even when another module references it, producing an undefined-symbol error.

Move the GUID assignment into the symbol loop, where `Sym.getName()` identifies the correct resolution and the GUID has already been obtained using `Sym.getIRName()`. Preserve the prevailing-definition check and the existing fallback for bitcode without a GUID table.

This fixes a regression introduced by 39dcb0ff91b312bb269334168d5e32be78b60417 (#201849). It surfaced when linking mimalloc 3.5.3 into a PGO/ThinLTO build of LLVM on macOS. The standalone reproducer below needs only Clang, LLD, and the macOS SDK.

Reproducer on macOS: set `LLVM_BIN` to the absolute path of the LLVM `bin` directory, then run:

```sh
set -eu
: "${LLVM_BIN:?Set LLVM_BIN to the directory containing clang and ld64.lld}"
repro_dir=$(mktemp -d)
cd "$repro_dir"

cat > defs.c <<'C'
volatile int v;
__attribute__((noinline)) int foo(void) { return v; }
__attribute__((noinline)) int _foo(void) { return v + 1; }
C
cat > main.c <<'C'
int foo(void);
int _foo(void);
int main(void) { return foo() + _foo() != 1; }
C

"$LLVM_BIN/clang" -O2 -flto=thin -fuse-ld=lld \
  --ld-path="$LLVM_BIN/ld64.lld" \
  -isysroot "$(xcrun --show-sdk-path)" \
  -Wl,-mllvm,-import-instr-limit=0 defs.c main.c -o repro
./repro
```

With unpatched LLVM 23.1.2, the link fails:

```text
ld64.lld: error: undefined symbol: foo
>>> did you mean: _foo
```

With the patch, it links successfully and `./repro` exits with status 0. Disabling ThinLTO importing keeps the cross-module references visible in this small example.

The added two-module `llvm-lto2` regression checks that both `foo` and `_foo` remain externally defined after importing. Before the fix, `foo` incorrectly becomes `internal`.

Local validation used LLVM 23.1.2 tools; this commit is based on current `main`:

- The new regression fails before the fix and passes after it.
- `hidden-escaped-symbols.ll` and `hidden-escaped-symbols-alt.ll` pass with the fix.
- The C reproducer above fails before the fix and links/runs after it.
- A complete macOS arm64 LLVM build with PGO, ThinLTO, and mimalloc links successfully and passes a threaded C++ compile/link/run smoke test.

Assisted-by: OpenAI Codex


>From 11daad9a264e11f4695470dce9d1dbb7c06e1fa3 Mon Sep 17 00:00:00 2001
From: Fabian Meumertzheim <fabian at meumertzhe.im>
Date: Mon, 28 Sep 2026 09:44:40 +0200
Subject: [PATCH] [ThinLTO] Record prevailing GUIDs by linker symbol name

GlobalResolutions is keyed by mangled linker symbol names, but the
readSummary callback looked up entries using IR names. On Mach-O, the
IR name _foo can match the linker name of foo, assigning the wrong GUID
to foo and allowing it to be incorrectly internalized despite references
from another module.

Set the prevailing definition's GUID in the symbol loop instead, using
Sym.getName() for GlobalResolutions and the GUID obtained from the
summary using Sym.getIRName(). Add a two-module regression test that
preserves both foo and _foo when ThinLTO importing is disabled.

This fixes a regression introduced by
39dcb0ff91b312bb269334168d5e32be78b60417 (#201849).

Assisted-by: OpenAI Codex
---
 llvm/lib/LTO/LTO.cpp                          |  6 +--
 llvm/test/ThinLTO/X86/macho-guid-collision.ll | 41 +++++++++++++++++++
 2 files changed, 42 insertions(+), 5 deletions(-)
 create mode 100644 llvm/test/ThinLTO/X86/macho-guid-collision.ll

diff --git a/llvm/lib/LTO/LTO.cpp b/llvm/lib/LTO/LTO.cpp
index 50232bf87d257..a8d4bed429593 100644
--- a/llvm/lib/LTO/LTO.cpp
+++ b/llvm/lib/LTO/LTO.cpp
@@ -1199,11 +1199,6 @@ LTO::addThinLTO(BitcodeModule BM, ArrayRef<InputFile::Symbol> Syms,
             auto IT = IRSpecifiedGUIDs.insert({VI.name(), VI.getGUID()});
             (void)IT;
             assert(IT.second);
-            if (auto GRIt = GlobalResolutions->find(VI.name());
-                GRIt != GlobalResolutions->end() &&
-                Prevailing.count(VI.name())) {
-              GRIt->second.setGUID(VI.getGUID());
-            }
           }))
     return Err;
   LLVM_DEBUG(dbgs() << "Module " << BMID << "\n");
@@ -1224,6 +1219,7 @@ LTO::addThinLTO(BitcodeModule BM, ArrayRef<InputFile::Symbol> Syms,
     if (!Sym.getIRName().empty() &&
         (R.Prevailing || R.FinalDefinitionInLinkageUnit)) {
       if (R.Prevailing) {
+        (*GlobalResolutions)[Sym.getName()].setGUID(GUID);
         ThinLTO.setPrevailingModuleForGUID(GUID, BMID);
         // For linker redefined symbols (via --wrap or --defsym) we want to
         // switch the linkage to `weak` to prevent IPOs from happening.
diff --git a/llvm/test/ThinLTO/X86/macho-guid-collision.ll b/llvm/test/ThinLTO/X86/macho-guid-collision.ll
new file mode 100644
index 0000000000000..ced1651fa42cd
--- /dev/null
+++ b/llvm/test/ThinLTO/X86/macho-guid-collision.ll
@@ -0,0 +1,41 @@
+; RUN: split-file %s %t
+; RUN: opt -module-summary %t/defs.ll -o %t/defs.bc
+; RUN: opt -module-summary %t/main.ll -o %t/main.bc
+; RUN: llvm-lto2 run %t/defs.bc %t/main.bc -o %t/out -save-temps \
+; RUN:   -import-instr-limit=0 \
+; RUN:   -r=%t/defs.bc,_foo,pl -r=%t/defs.bc,__foo,pl \
+; RUN:   -r=%t/main.bc,_main,plx -r=%t/main.bc,_foo,l \
+; RUN:   -r=%t/main.bc,__foo,l
+; RUN: llvm-dis %t/out.1.3.import.bc -o - | FileCheck %s
+
+;; Global resolutions use mangled symbol names, whereas the summary records IR
+;; names. Looking up the IR name _foo in the resolutions for Mach-O incorrectly
+;; assigns its GUID to foo and allows foo to be internalized.
+; CHECK: define dso_local i32 @foo()
+; CHECK: define dso_local i32 @_foo()
+
+;--- defs.ll
+target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-apple-macosx10.15.0"
+
+define i32 @foo() noinline {
+  ret i32 1
+}
+
+define i32 @_foo() noinline {
+  ret i32 2
+}
+
+;--- main.ll
+target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-apple-macosx10.15.0"
+
+declare i32 @foo()
+declare i32 @_foo()
+
+define i32 @main() {
+  %a = call i32 @foo()
+  %b = call i32 @_foo()
+  %sum = add i32 %a, %b
+  ret i32 %sum
+}



More information about the llvm-commits mailing list