[lld] [llvm] [LTO] Preserve aliasee definitions when dropping non-prevailing comdats (PR #194974)

Farid Zakaria via llvm-commits llvm-commits at lists.llvm.org
Wed Apr 29 15:58:11 PDT 2026


https://github.com/fzakaria created https://github.com/llvm/llvm-project/pull/194974

When `handleNonPrevailingComda`t drops a comdat group, it converts all members to `available_externally.` If a member is the target of an alias that is NOT in the comdat (e.g. C++ D1 destructor alias to D2), the alias survives while its target's definition is dropped. With `-export-dynamic,` the alias can't be internalized away, leaving it pointing to a declaration.

This happens in practice when different TUs compile the same template class with different views of the destructor (one out-of-line, one defaulted), producing D0+D2 in a unified comdat in one TU but D0 in a separate comdat in the other. When the separate D0 prevails, the unified comdat is dropped, killing D2 but leaving D1 dangling.

**ELI5**
C++ generates up to three versions of every destructor. Two of them (D1 and D2) often have identical code, so the compiler makes D1 a shortcut alias) pointing to D2's code instead of duplicating it.  D2 goes into a comdat group, but D1 (the alias) doesn't, because LLVM IR doesn't allow aliases in comdat groups. 

When two files compile the same template class slightly differently (which is a ODR violation technically), D2 can end up sharing a comdat group with another function (D0). If the linker picks a different file's copy of D0 as the winner, it throws away the entire comdat group (including D2).  D1 (the alias) however wasn't in the group, so it survives. Now D1 points to code that no longer exists :bug: 

Normally LTO cleans up the dangling alias by making it internal and optimizing it away. But -export-dynamic forces all symbols to be visible  in the final binary, so the alias can't be removed. The IR verifier catches the invalid alias and causes a crash.


#fixes https://github.com/llvm/llvm-project/issues/190737

Aside:
I also looked into how to make D1 in the same comdat however GlobalAlias inherits from GlobalValue, not GlobalObject, so aliases cannot have comdats in LLVM IR.  It would need a bigger change to the IR. We could also stop generating an alias and generate a real function for D1 so it gets into the same comdat but that defeats the purpose of the alias optimization.

>From 39aaa58f7bbfe4f41c93f3b84b4d4d631a256b6f Mon Sep 17 00:00:00 2001
From: Farid Zakaria <fmzakari at meta.com>
Date: Wed, 29 Apr 2026 15:38:59 -0700
Subject: [PATCH] [LTO] Preserve aliasee definitions when dropping
 non-prevailing comdats
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

When handleNonPrevailingComdat drops a comdat group, it converts all
members to available_externally. If a member is the target of an alias
that is NOT in the comdat (e.g. C++ D1 destructor alias to D2), the
alias survives while its target's definition is dropped. With
-export-dynamic, the alias can't be internalized away, leaving it
pointing to a declaration — which is invalid IR.

This happens in practice when different TUs compile the same template
class with different views of the destructor (one out-of-line, one
defaulted), producing D0+D2 in a unified comdat in one TU but D0 in
a separate comdat in the other. When the separate D0 prevails, the
unified comdat is dropped, killing D2 but leaving D1 dangling.

Fix: skip converting aliasee globals to available_externally in
handleNonPrevailingComdat, preserving their definitions so aliases
remain valid. The aliasee is still removed from the comdat.

Fixes https://github.com/llvm/llvm-project/issues/190737
---
 .../ELF/lto/comdat-alias-non-prevailing.ll    | 62 +++++++++++++++++++
 llvm/lib/LTO/LTO.cpp                          | 17 ++++-
 2 files changed, 77 insertions(+), 2 deletions(-)
 create mode 100644 lld/test/ELF/lto/comdat-alias-non-prevailing.ll

diff --git a/lld/test/ELF/lto/comdat-alias-non-prevailing.ll b/lld/test/ELF/lto/comdat-alias-non-prevailing.ll
new file mode 100644
index 0000000000000..d80ac4701eb47
--- /dev/null
+++ b/lld/test/ELF/lto/comdat-alias-non-prevailing.ll
@@ -0,0 +1,62 @@
+; REQUIRES: x86
+
+;; Test that LTO doesn't crash when an alias's target (D2) is in a comdat group
+;; that gets dropped because another member (D0) is non-prevailing, while the
+;; alias (D1) itself is not in the comdat and survives. With -export-dynamic,
+;; the alias can't be internalized away, exposing the dangling reference.
+;;
+;; This reproduces https://github.com/llvm/llvm-project/issues/190737
+;; where -flto -rdynamic on SPEC2006 447.dealII caused:
+;;   "Alias must point to a definition"
+;;
+;; The bug: TU1 puts D0+D2 in comdat $D5 (unified destructor comdat).
+;; TU2 puts D0 in its own comdat $D0 (separate, from defaulted destructor).
+;; D1 is an alias to D2, not in any comdat.
+;; When LTO picks TU2's D0 as prevailing, comdat $D5 is marked non-prevailing.
+;; handleNonPrevailingComdat converts D2 to available_externally, but D1
+;; (the alias) survives. D2's body is later dropped, leaving D1 pointing
+;; to a declaration.
+
+; RUN: split-file %s %t
+; RUN: llvm-as %t/tu1.ll -o %t/tu1.o
+; RUN: llvm-as %t/tu2.ll -o %t/tu2.o
+; RUN: ld.lld -pie -export-dynamic %t/tu2.o %t/tu1.o -o %t/out
+; RUN: llvm-nm %t/out | FileCheck %s
+
+; CHECK: W _ZN7DerivedD1Ev
+
+;--- tu1.ll
+;; TU with real destructor body: D0 and D2 in same comdat, D1 aliases D2.
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+$_ZN7DerivedD5Ev = comdat any
+
+ at _ZN7DerivedD1Ev = weak_odr dso_local unnamed_addr alias void (ptr), ptr @_ZN7DerivedD2Ev
+
+define weak_odr dso_local void @_ZN7DerivedD2Ev(ptr %this) unnamed_addr comdat($_ZN7DerivedD5Ev) {
+  ret void
+}
+
+define weak_odr dso_local void @_ZN7DerivedD0Ev(ptr %this) unnamed_addr comdat($_ZN7DerivedD5Ev) {
+  call void @_ZN7DerivedD1Ev(ptr %this)
+  ret void
+}
+
+;--- tu2.ll
+;; TU with defaulted destructor: D0 in its own separate comdat.
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+$_ZN7DerivedD0Ev = comdat any
+
+define linkonce_odr dso_local void @_ZN7DerivedD0Ev(ptr %this) unnamed_addr comdat {
+  ret void
+}
+
+define dso_local void @_start() {
+  %p = alloca ptr
+  %obj = load ptr, ptr %p
+  call void @_ZN7DerivedD0Ev(ptr %obj)
+  ret void
+}
diff --git a/llvm/lib/LTO/LTO.cpp b/llvm/lib/LTO/LTO.cpp
index 1a6976eea8088..e3666b80ccccc 100644
--- a/llvm/lib/LTO/LTO.cpp
+++ b/llvm/lib/LTO/LTO.cpp
@@ -920,7 +920,8 @@ LTO::addModule(InputFile &Input, ArrayRef<SymbolResolution> InputRes,
 // in the regular LTO module without this cleanup.
 static void
 handleNonPrevailingComdat(GlobalValue &GV,
-                          std::set<const Comdat *> &NonPrevailingComdats) {
+                          std::set<const Comdat *> &NonPrevailingComdats,
+                          const DenseSet<GlobalObject *> &AliasedGlobals) {
   Comdat *C = GV.getComdat();
   if (!C)
     return;
@@ -928,6 +929,18 @@ handleNonPrevailingComdat(GlobalValue &GV,
   if (!NonPrevailingComdats.count(C))
     return;
 
+  // Don't convert aliasee globals to available_externally. An alias must
+  // point to a definition; if we drop the definition, any surviving alias
+  // (e.g. kept alive by -export-dynamic) would point to a declaration,
+  // which is invalid IR. This can happen when different TUs place the same
+  // symbol in different comdat groups (e.g. C++ D0/D2 destructors in
+  // unified vs separate comdats due to ODR differences).
+  if (auto *GO = dyn_cast<GlobalObject>(&GV))
+    if (AliasedGlobals.count(GO)) {
+      GO->setComdat(nullptr);
+      return;
+    }
+
   // Additionally need to drop all global values from the comdat to
   // available_externally, to satisfy the COMDAT requirement that all members
   // are discarded as a unit. The non-local linkage global values avoid
@@ -1096,7 +1109,7 @@ LTO::addRegularLTO(InputFile &Input, ArrayRef<SymbolResolution> InputRes,
 
   if (!M.getComdatSymbolTable().empty())
     for (GlobalValue &GV : M.global_values())
-      handleNonPrevailingComdat(GV, NonPrevailingComdats);
+      handleNonPrevailingComdat(GV, NonPrevailingComdats, AliasedGlobals);
 
   // Prepend ".lto_discard <sym>, <sym>*" directive to each module inline asm
   // block.



More information about the llvm-commits mailing list