Skip to content

release/23.x: [clang][win] Fix __global_delete wrappers for Arm64EC and cross-TU ::delete - #210144

Merged
dyung merged 1 commit into
llvm:release/23.xfrom
dpaoliello:23globaldelfix
Jul 22, 2026
Merged

dyung merged 1 commit into
llvm:release/23.xfrom
dpaoliello:23globaldelfix

Conversation

@dpaoliello

Copy link
Copy Markdown
Contributor

PR #188372 made Clang's MSVC-ABI deleting-destructor path route global deletes
through compiler-generated __global_delete / __global_array_delete wrappers
instead of referencing ::operator delete directly. This lets a deleting destructor be emitted in environments (e.g. kernel mode) where no global ::operator delete exists: each wrapper defaults to a trapping __empty_global_delete fallback, and a real forwarding body that calls ::operator delete is materialized only when the program actually uses ::delete.

This change refines how those wrappers and their fallbacks are emitted, fixing
two problems that made the #188372 mechanism fail in practice.

New behavior:

  • The trapping fallback is now emitted as a weak GlobalAlias to __empty_global_delete rather than via an /alternatename directive. This lowers
    to a COFF weak-external-with-default -- exactly what MSVC emits -- so a real
    __global_delete defined in any TU overrides the fallback in every other TU at
    link time.

  • A ::delete expression now registers the wrapper for its resolved global
    operator delete directly (in EmitCXXDeleteExpr), so the strong forwarding body
    is emitted in that TU regardless of where the class's deleting destructor is
    defined. This matches MSVC, which emits the forwarder at every ::delete site.

Bugs fixed:

  1. Arm64EC (miscompile -> LNK2019). /alternatename only names the plain symbol,
    not the backend-generated "$$h" hybrid EC symbol referenced by the exit thunk, so Arm64EC images left __global_delete$exit_thunk unresolved. The weak
    GlobalAlias correctly produces the "$$h" symbol, exit thunk, and plain alias
    (verified on both x64 and Arm64EC).

  2. Cross-TU ::delete (runtime crash). Previously the forwarding body was only
    emitted in a TU that both emitted a matching vector deleting destructor and
    contained a ::delete. A TU that only performed ::delete p -- with the class
    defined elsewhere -- emitted no forwarder, so nothing overrode the weak-alias
    trap and the program executed a trapping __empty_global_delete at runtime
    (STATUS_ILLEGAL_INSTRUCTION). Registering the wrapper at the ::delete site
    fixes this.

To share wrapper-creation logic between the deleting-destructor path and the
delete-expression path, getOrCreateMSVCGlobalDeleteWrapper is promoted from a
static helper in CGClass.cpp to a CodeGenModule method.

Verified end-to-end: a weak alias in one TU and a strong forwarder in another
resolve to the forwarder under both lld-link and MSVC link.exe, and the previously-crashing delete-only TU now runs cleanly.

Backport 5fc5c2e

@dpaoliello dpaoliello added this to the LLVM 23.x Release milestone Jul 16, 2026
@github-project-automation github-project-automation Bot moved this to Needs Triage in LLVM Release Status Jul 16, 2026
@llvmorg-github-actions llvmorg-github-actions Bot added clang Clang issues not falling into any other category clang:codegen IR generation bugs: mangling, exceptions, etc. labels Jul 16, 2026
@llvmorg-github-actions

llvmorg-github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-codegen

Author: Daniel Paoliello (dpaoliello)

Changes

PR #188372 made Clang's MSVC-ABI deleting-destructor path route global deletes
through compiler-generated __global_delete / __global_array_delete wrappers
instead of referencing ::operator delete directly. This lets a deleting destructor be emitted in environments (e.g. kernel mode) where no global ::operator delete exists: each wrapper defaults to a trapping __empty_global_delete fallback, and a real forwarding body that calls ::operator delete is materialized only when the program actually uses ::delete.

This change refines how those wrappers and their fallbacks are emitted, fixing
two problems that made the #188372 mechanism fail in practice.

New behavior:

  • The trapping fallback is now emitted as a weak GlobalAlias to __empty_global_delete rather than via an /alternatename directive. This lowers
    to a COFF weak-external-with-default -- exactly what MSVC emits -- so a real
    __global_delete defined in any TU overrides the fallback in every other TU at
    link time.

  • A ::delete expression now registers the wrapper for its resolved global
    operator delete directly (in EmitCXXDeleteExpr), so the strong forwarding body
    is emitted in that TU regardless of where the class's deleting destructor is
    defined. This matches MSVC, which emits the forwarder at every ::delete site.

Bugs fixed:

  1. Arm64EC (miscompile -> LNK2019). /alternatename only names the plain symbol,
    not the backend-generated "$$h" hybrid EC symbol referenced by the exit thunk, so Arm64EC images left __global_delete$exit_thunk unresolved. The weak
    GlobalAlias correctly produces the "$$h" symbol, exit thunk, and plain alias
    (verified on both x64 and Arm64EC).

  2. Cross-TU ::delete (runtime crash). Previously the forwarding body was only
    emitted in a TU that both emitted a matching vector deleting destructor and
    contained a ::delete. A TU that only performed ::delete p -- with the class
    defined elsewhere -- emitted no forwarder, so nothing overrode the weak-alias
    trap and the program executed a trapping __empty_global_delete at runtime
    (STATUS_ILLEGAL_INSTRUCTION). Registering the wrapper at the ::delete site
    fixes this.

To share wrapper-creation logic between the deleting-destructor path and the
delete-expression path, getOrCreateMSVCGlobalDeleteWrapper is promoted from a
static helper in CGClass.cpp to a CodeGenModule method.

Verified end-to-end: a weak alias in one TU and a strong forwarder in another
resolve to the forwarder under both lld-link and MSVC link.exe, and the previously-crashing delete-only TU now runs cleanly.

Backport <5fc5c2e>


Patch is 31.36 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/210144.diff

10 Files Affected:

  • (modified) clang/lib/CodeGen/CGClass.cpp (+4-109)
  • (modified) clang/lib/CodeGen/CGExprCXX.cpp (+13-1)
  • (modified) clang/lib/CodeGen/CodeGenModule.cpp (+127-13)
  • (modified) clang/lib/CodeGen/CodeGenModule.h (+10-3)
  • (modified) clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp (+17-12)
  • (modified) clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp (+6-4)
  • (added) clang/test/CodeGenCXX/msvc-global-delete-forwarding-at-delete-site.cpp (+34)
  • (modified) clang/test/CodeGenCXX/msvc-global-delete-scalar-array-split.cpp (+11-10)
  • (modified) clang/test/CodeGenCXX/msvc-global-delete-scope-no-dtor.cpp (+12-11)
  • (modified) clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp (+7-3)
diff --git a/clang/lib/CodeGen/CGClass.cpp b/clang/lib/CodeGen/CGClass.cpp
index 70666dffdc903..39b8e50f68eaf 100644
--- a/clang/lib/CodeGen/CGClass.cpp
+++ b/clang/lib/CodeGen/CGClass.cpp
@@ -1410,112 +1410,6 @@ static bool CanSkipVTablePointerInitialization(CodeGenFunction &CGF,
   return true;
 }
 
-/// Get or create the MSVC-compatible __global_delete wrapper function.
-///
-/// Destructor helpers call __global_delete instead of ::operator delete
-/// directly. If this TU contains a ::delete expression (or a dllexport class
-/// whose deleting destructor takes the global-delete path), a real forwarding
-/// body is emitted at end-of-file. If ::delete is never used anywhere in the
-/// program, then no definition will exist and the `/ALTERNATENAME` linker
-/// directive will cause the linker to use __empty_global_delete as the
-/// definition. __empty_global_delete is never expected to actually be called,
-/// hence it is a trap function (a deliberate deviation from MSVC, whose empty
-/// is a no-op).
-///
-/// Array delete[] uses a parallel __global_array_delete wrapper, matching
-/// MSVC. The scalar and array wrappers of a given signature share a single
-/// __empty_global_delete fallback.
-static llvm::Constant *
-getOrCreateMSVCGlobalDeleteWrapper(CodeGenModule &CGM,
-                                   const FunctionDecl *GlobOD) {
-  assert(CGM.getTarget().getCXXABI().isMicrosoft() &&
-         "__global_delete wrapper is only used with the Microsoft ABI");
-  llvm::Module &M = CGM.getModule();
-  llvm::LLVMContext &LLVMCtx = M.getContext();
-
-  llvm::Constant *GlobDeleteCallee = CGM.GetAddrOfFunction(GlobOD);
-  auto *GlobDeleteFn = cast<llvm::Function>(GlobDeleteCallee);
-  llvm::FunctionType *FnTy = GlobDeleteFn->getFunctionType();
-
-  // Derive the wrapper and empty-fallback mangled names. MSVC uses distinct
-  // wrapper names for scalar vs array global delete, but a single shared empty
-  // fallback per signature:
-  //   Global ::operator delete   mangling: ??3@<signature>
-  //     -> wrapper ?__global_delete@@<signature>
-  //   Global ::operator delete[] mangling: ??_V@<signature>
-  //     -> wrapper ?__global_array_delete@@<signature>
-  //   shared fallback: ?__empty_global_delete@@<signature>
-  StringRef GlobDeleteMangledName = GlobDeleteFn->getName();
-  StringRef Signature;
-  const char *WrapperBase;
-  if (GlobDeleteMangledName.starts_with("??3@")) {
-    Signature = GlobDeleteMangledName.substr(4);
-    WrapperBase = "?__global_delete@@";
-  } else if (GlobDeleteMangledName.starts_with("??_V@")) {
-    Signature = GlobDeleteMangledName.substr(5);
-    WrapperBase = "?__global_array_delete@@";
-  } else {
-    llvm_unreachable("unexpected global operator delete mangling");
-  }
-
-  std::string GlobalDeleteName = (WrapperBase + Signature).str();
-  std::string EmptyGlobalDeleteName =
-      ("?__empty_global_delete@@" + Signature).str();
-
-  // Only set up the wrapper once per module.
-  if (llvm::Function *Existing = M.getFunction(GlobalDeleteName))
-    return Existing;
-
-  // Create the shared __empty_global_delete fallback if it doesn't already
-  // exist. The scalar and array wrappers of a given signature share one empty
-  // (matching MSVC, whose weak externals both point at a single
-  // __empty_global_delete). The body traps: this path is unreachable at
-  // runtime when ::delete is never used (a deliberate deviation from MSVC,
-  // whose empty is a no-op; see the doc comment above).
-  llvm::Function *EmptyFn = M.getFunction(EmptyGlobalDeleteName);
-  if (!EmptyFn) {
-    EmptyFn = llvm::Function::Create(
-        FnTy, llvm::GlobalValue::LinkOnceODRLinkage, EmptyGlobalDeleteName, &M);
-    EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName));
-    EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
-    CGM.SetLLVMFunctionAttributes(
-        GlobalDecl(GlobOD),
-        CGM.getTypes().arrangeGlobalDeclaration(GlobalDecl(GlobOD)), EmptyFn,
-        /*IsThunk=*/false);
-    CGM.SetLLVMFunctionAttributesForDefinition(GlobOD, EmptyFn);
-    CGM.getTargetCodeGenInfo().setTargetAttributes(GlobOD, EmptyFn, CGM);
-    auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn);
-    llvm::Function *TrapFn =
-        llvm::Intrinsic::getOrInsertDeclaration(&M, llvm::Intrinsic::trap);
-    auto *TrapCall = llvm::CallInst::Create(TrapFn, {}, "", BB);
-    TrapCall->setDoesNotReturn();
-    TrapCall->setDoesNotThrow();
-    new llvm::UnreachableInst(LLVMCtx, BB);
-
-    // Nothing directly uses the empty other than the /alternatename directive,
-    // so explicitly mark it as used.
-    appendToUsed(M, {EmptyFn});
-  }
-
-  // Emit /ALTERNATENAME linker directive: if this wrapper isn't provided,
-  // fall back to the trapping __empty_global_delete.
-  std::string AltOption =
-      "/alternatename:" + GlobalDeleteName + "=" + EmptyGlobalDeleteName;
-  auto *AltMD =
-      llvm::MDNode::get(LLVMCtx, {llvm::MDString::get(LLVMCtx, AltOption)});
-  M.getOrInsertNamedMetadata("llvm.linker.options")->addOperand(AltMD);
-
-  // Return the __global_delete wrapper function to call.
-  auto GlobalDeleteCallee = M.getOrInsertFunction(GlobalDeleteName, FnTy);
-  auto *GlobalDeleteFn = cast<llvm::Function>(GlobalDeleteCallee.getCallee());
-
-  // Register this variant so we can emit a real forwarding body at end-of-TU
-  // if this TU contains any direct use of global ::operator delete.
-  CGM.addPendingGlobalDelete(GlobalDeleteFn, GlobOD);
-
-  return GlobalDeleteFn;
-}
-
 static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
                                          CodeGenFunction &CGF,
                                          llvm::Value *ShouldDeleteCondition) {
@@ -1602,8 +1496,9 @@ static void EmitConditionalArrayDtorCall(const CXXDestructorDecl *DD,
       // Use __global_delete wrapper instead of directly calling
       // ::operator delete to match MSVC's behavior. See the doc comment on
       // getOrCreateMSVCGlobalDeleteWrapper for details.
-      llvm::Constant *GlobalDeleteWrapper = getOrCreateMSVCGlobalDeleteWrapper(
-          CGF.CGM, Dtor->getGlobalArrayOperatorDelete());
+      llvm::Constant *GlobalDeleteWrapper =
+          CGF.CGM.getOrCreateMSVCGlobalDeleteWrapper(
+              Dtor->getGlobalArrayOperatorDelete());
       // For dllexport classes, emit forwarding bodies since the dtor is
       // exported and another TU may not provide the forwarding body.
       if (Dtor->hasAttr<DLLExportAttr>())
@@ -1870,7 +1765,7 @@ void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
     // ::operator delete to match MSVC's behavior. See the doc comment on
     // getOrCreateMSVCGlobalDeleteWrapper for details.
     llvm::Constant *GlobalDeleteWrapper =
-        getOrCreateMSVCGlobalDeleteWrapper(CGF.CGM, GlobOD);
+        CGF.CGM.getOrCreateMSVCGlobalDeleteWrapper(GlobOD);
     // For dllexport classes, emit forwarding bodies since the dtor is
     // exported and another TU may not provide the forwarding body.
     if (Dtor->hasAttr<DLLExportAttr>())
diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 52e0fdbc59a11..1769ab00ed56d 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -2108,8 +2108,20 @@ void CodeGenFunction::EmitCXXDeleteExpr(const CXXDeleteExpr *E) {
   //     operator delete are both irrelevant to the trigger.
   if (E->isGlobalDelete() && CGM.getTarget().getCXXABI().isMicrosoft()) {
     const CXXRecordDecl *RD = E->getDestroyedType()->getAsCXXRecordDecl();
-    if (RD && RD->hasDefinition() && !RD->hasTrivialDestructor())
+    if (RD && RD->hasDefinition() && !RD->hasTrivialDestructor()) {
       CGM.noteDirectGlobalDelete();
+      // Ensure a __global_delete wrapper (and thus a strong forwarding body)
+      // is emitted in THIS TU for the resolved global ::operator delete, even
+      // when no vector deleting destructor here references it. Without this, a
+      // TU that only does ::delete (with the deleting destructor defined in
+      // another TU) would emit no forwarder, leaving the wrapper bound to the
+      // trapping empty fallback and crashing at runtime.
+      const FunctionDecl *OD = E->getOperatorDelete();
+      assert(!isa<CXXMethodDecl>(OD) &&
+             "global ::delete should resolve to a namespace-scope "
+             "operator delete");
+      CGM.getOrCreateMSVCGlobalDeleteWrapper(OD);
+    }
   }
 
   // Null check the pointer.
diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp
index 78627047b19ad..ca71458d85134 100644
--- a/clang/lib/CodeGen/CodeGenModule.cpp
+++ b/clang/lib/CodeGen/CodeGenModule.cpp
@@ -8906,45 +8906,159 @@ void CodeGenModule::requireVectorDestructorDefinition(const CXXRecordDecl *RD) {
 }
 
 void CodeGenModule::addPendingGlobalDelete(
-    llvm::Function *GlobalDeleteFn, const FunctionDecl *OperatorDeleteFD) {
+    llvm::GlobalAlias *GlobalDeleteAlias,
+    const FunctionDecl *OperatorDeleteFD) {
   // insert() is a no-op if this wrapper has already been recorded, keeping the
   // first FunctionDecl seen for it.
-  PendingMSVCGlobalDeletes.insert({GlobalDeleteFn, OperatorDeleteFD});
+  PendingMSVCGlobalDeletes.insert({GlobalDeleteAlias, OperatorDeleteFD});
 }
 
 void CodeGenModule::noteDirectGlobalDelete() { HasDirectGlobalDelete = true; }
 
+/// Get or create the MSVC-compatible __global_delete wrapper function.
+///
+/// Destructor helpers call __global_delete instead of ::operator delete
+/// directly. If this TU contains a ::delete expression (or a dllexport class
+/// whose deleting destructor takes the global-delete path), a real forwarding
+/// body is emitted at end-of-file. If ::delete is never used anywhere in the
+/// program, then no forwarding body is emitted and the wrapper defaults to a
+/// weak alias to __empty_global_delete. __empty_global_delete is never
+/// expected to actually be called, hence it is a trap function (a deliberate
+/// deviation from MSVC, whose empty is a no-op).
+///
+/// Array delete[] uses a parallel __global_array_delete wrapper, matching
+/// MSVC. The scalar and array wrappers of a given signature share a single
+/// __empty_global_delete fallback.
+llvm::Constant *
+CodeGenModule::getOrCreateMSVCGlobalDeleteWrapper(const FunctionDecl *GlobOD) {
+  assert(getTarget().getCXXABI().isMicrosoft() &&
+         "__global_delete wrapper is only used with the Microsoft ABI");
+  llvm::Module &M = getModule();
+  llvm::LLVMContext &LLVMCtx = M.getContext();
+
+  llvm::Constant *GlobDeleteCallee = GetAddrOfFunction(GlobOD);
+  auto *GlobDeleteFn = cast<llvm::Function>(GlobDeleteCallee);
+  llvm::FunctionType *FnTy = GlobDeleteFn->getFunctionType();
+
+  // Derive the wrapper and empty-fallback mangled names. MSVC uses distinct
+  // wrapper names for scalar vs array global delete, but a single shared empty
+  // fallback per signature:
+  //   Global ::operator delete   mangling: ??3@<signature>
+  //     -> wrapper ?__global_delete@@<signature>
+  //   Global ::operator delete[] mangling: ??_V@<signature>
+  //     -> wrapper ?__global_array_delete@@<signature>
+  //   shared fallback: ?__empty_global_delete@@<signature>
+  StringRef GlobDeleteMangledName = GlobDeleteFn->getName();
+  StringRef Signature;
+  const char *WrapperBase;
+  if (GlobDeleteMangledName.starts_with("??3@")) {
+    Signature = GlobDeleteMangledName.substr(4);
+    WrapperBase = "?__global_delete@@";
+  } else if (GlobDeleteMangledName.starts_with("??_V@")) {
+    Signature = GlobDeleteMangledName.substr(5);
+    WrapperBase = "?__global_array_delete@@";
+  } else {
+    llvm_unreachable("unexpected global operator delete mangling");
+  }
+
+  std::string GlobalDeleteName = (WrapperBase + Signature).str();
+  std::string EmptyGlobalDeleteName =
+      ("?__empty_global_delete@@" + Signature).str();
+
+  // Only set up the wrapper once per module. The wrapper may be a weak alias
+  // (the default fallback) or, once replaced, a real forwarding function.
+  if (llvm::GlobalValue *Existing = M.getNamedValue(GlobalDeleteName))
+    return Existing;
+
+  // Create the shared __empty_global_delete fallback if it doesn't already
+  // exist. The scalar and array wrappers of a given signature share one empty
+  // (matching MSVC, whose weak externals both point at a single
+  // __empty_global_delete). The body traps: this path is unreachable at
+  // runtime when ::delete is never used (a deliberate deviation from MSVC,
+  // whose empty is a no-op; see the doc comment above).
+  llvm::Function *EmptyFn = M.getFunction(EmptyGlobalDeleteName);
+  if (!EmptyFn) {
+    EmptyFn = llvm::Function::Create(
+        FnTy, llvm::GlobalValue::LinkOnceODRLinkage, EmptyGlobalDeleteName, &M);
+    EmptyFn->setComdat(M.getOrInsertComdat(EmptyGlobalDeleteName));
+    EmptyFn->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
+    SetLLVMFunctionAttributes(
+        GlobalDecl(GlobOD),
+        getTypes().arrangeGlobalDeclaration(GlobalDecl(GlobOD)), EmptyFn,
+        /*IsThunk=*/false);
+    SetLLVMFunctionAttributesForDefinition(GlobOD, EmptyFn);
+    getTargetCodeGenInfo().setTargetAttributes(GlobOD, EmptyFn, *this);
+    auto *BB = llvm::BasicBlock::Create(LLVMCtx, "", EmptyFn);
+    llvm::Function *TrapFn =
+        llvm::Intrinsic::getOrInsertDeclaration(&M, llvm::Intrinsic::trap);
+    auto *TrapCall = llvm::CallInst::Create(TrapFn, {}, "", BB);
+    TrapCall->setDoesNotReturn();
+    TrapCall->setDoesNotThrow();
+    new llvm::UnreachableInst(LLVMCtx, BB);
+
+    // The empty is referenced only by the wrapper's weak alias. When this TU
+    // uses ::delete that alias is replaced by a real forwarding body, leaving
+    // the empty otherwise unreferenced, so explicitly mark it used to ensure
+    // it is always emitted (matching MSVC).
+    appendToUsed(M, {EmptyFn});
+  }
+
+  // The wrapper defaults to a weak alias to the trapping __empty_global_delete
+  // fallback (see the doc comment above for why this is a weak alias rather
+  // than an /alternatename directive). If this TU directly uses global
+  // ::operator delete, the alias is replaced with a real forwarding body in
+  // emitGlobalDeleteForwardingBodies().
+  auto *GlobalDeleteAlias = llvm::GlobalAlias::create(
+      FnTy, GlobDeleteFn->getAddressSpace(), llvm::GlobalValue::WeakAnyLinkage,
+      GlobalDeleteName, EmptyFn, &M);
+
+  // Register this variant so we can replace the alias with a real forwarding
+  // body at end-of-TU if this TU contains any direct use of global
+  // ::operator delete.
+  addPendingGlobalDelete(GlobalDeleteAlias, GlobOD);
+
+  return GlobalDeleteAlias;
+}
+
 void CodeGenModule::emitGlobalDeleteForwardingBodies() {
   // MSVC-compatible __global_delete forwarding bodies.
   //
   // Destructor helpers call __global_delete but they are only needed if there
   // is a direct use of ::operator delete. When this TU contains a ::delete
   // expression (or a dllexport deleting destructor that takes the global-delete
-  // path), we know ::operator delete must exist, so we emit a real
-  // __global_delete definition that forwards to it.
+  // path), we know ::operator delete must exist, so we replace the wrapper's
+  // weak alias-to-empty fallback with a real __global_delete definition that
+  // forwards to it.
   if (!HasDirectGlobalDelete)
     return;
 
   for (const auto &Entry : PendingMSVCGlobalDeletes) {
-    llvm::Function *GlobDelFn = Entry.first;
-    if (!GlobDelFn->isDeclaration())
-      continue;
-
+    llvm::GlobalAlias *Alias = Entry.first;
     const FunctionDecl *OperatorDeleteFD = Entry.second;
     llvm::Constant *RealDeleteFn = GetAddrOfFunction(OperatorDeleteFD);
 
-    // Create the forwarding body: call ::operator delete with all args.
+    // Create the strong forwarding function. Use LinkOnceODR so multiple TUs
+    // can emit this without conflicts.
+    auto *FnTy = cast<llvm::FunctionType>(Alias->getValueType());
+    auto *GlobDelFn =
+        llvm::Function::Create(FnTy, llvm::GlobalValue::LinkOnceODRLinkage,
+                               Alias->getAddressSpace(), "", &getModule());
+
+    // Emit the forwarding body: call ::operator delete with all args.
     auto *BB =
         llvm::BasicBlock::Create(getModule().getContext(), "", GlobDelFn);
     llvm::SmallVector<llvm::Value *, 4> Args;
     for (auto &Arg : GlobDelFn->args())
       Args.push_back(&Arg);
-    llvm::CallInst::Create(GlobDelFn->getFunctionType(), RealDeleteFn, Args, "",
-                           BB);
+    llvm::CallInst::Create(FnTy, RealDeleteFn, Args, "", BB);
     llvm::ReturnInst::Create(getModule().getContext(), BB);
 
-    // Use LinkOnceODR so multiple TUs can emit this without conflicts.
-    GlobDelFn->setLinkage(llvm::GlobalValue::LinkOnceODRLinkage);
+    // Replace the weak alias fallback with the real forwarding body, taking
+    // over its name.
+    Alias->replaceAllUsesWith(GlobDelFn);
+    GlobDelFn->takeName(Alias);
+    Alias->eraseFromParent();
+
     GlobDelFn->setComdat(getModule().getOrInsertComdat(GlobDelFn->getName()));
     SetLLVMFunctionAttributes(
         GlobalDecl(OperatorDeleteFD),
diff --git a/clang/lib/CodeGen/CodeGenModule.h b/clang/lib/CodeGen/CodeGenModule.h
index 0abd75ccb0551..f62c761be0184 100644
--- a/clang/lib/CodeGen/CodeGenModule.h
+++ b/clang/lib/CodeGen/CodeGenModule.h
@@ -562,9 +562,9 @@ class CodeGenModule : public CodeGenTypeCache {
   llvm::SmallPtrSet<const CXXRecordDecl *, 16> RequireVectorDeletingDtor;
 
   /// Pending MSVC __global_delete variants that may need forwarding bodies.
-  /// Maps each __global_delete wrapper function to the corresponding global
+  /// Maps each __global_delete wrapper alias to the corresponding global
   /// ::operator delete FunctionDecl, in insertion order.
-  llvm::MapVector<llvm::Function *, const FunctionDecl *>
+  llvm::MapVector<llvm::GlobalAlias *, const FunctionDecl *>
       PendingMSVCGlobalDeletes;
 
   /// Whether this TU contains a direct use of global ::operator delete
@@ -1654,9 +1654,16 @@ class CodeGenModule : public CodeGenTypeCache {
   void requireVectorDestructorDefinition(const CXXRecordDecl *RD);
 
   /// Record a pending __global_delete variant that may need a forwarding body.
-  void addPendingGlobalDelete(llvm::Function *GlobalDeleteFn,
+  void addPendingGlobalDelete(llvm::GlobalAlias *GlobalDeleteAlias,
                               const FunctionDecl *OperatorDeleteFD);
 
+  /// Get or create the MSVC-compatible __global_delete wrapper for the given
+  /// global ::operator delete, registering it as a pending variant so a
+  /// forwarding body can be emitted if this TU directly uses global
+  /// ::operator delete.
+  llvm::Constant *
+  getOrCreateMSVCGlobalDeleteWrapper(const FunctionDecl *GlobOD);
+
   /// Note that global ::operator delete is directly used in this TU.
   void noteDirectGlobalDelete();
 
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
index da891a138739c..ba1760b49f2c9 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors.cpp
@@ -196,6 +196,14 @@ void kernelTest() {
 // CHECK: delete.end:
 // CHECK-NEXT:   ret void
 
+// The __empty_global_delete fallback is emitted at the first ::delete site,
+// which here lands before the deleting-destructor helpers. Verify it traps
+// (the fallback path is unreachable at runtime once a real forwarding body is
+// linked in).
+// X64: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K@Z"(ptr noundef %0, i64 noundef %1)
+// X64-NEXT: call void @llvm.trap()
+// X64-NEXT: unreachable
+
 // Vector dtor definition for Parrot.
 // X64-LABEL: define weak dso_local noundef ptr @"??_EParrot@@UEAAPEAXI@Z"(
 // X64-SAME: ptr {{.*}} %[[THIS:.*]], i32 {{.*}} %[[IMPLICIT_PARAM:.*]]) unnamed_addr
@@ -287,16 +295,6 @@ void kernelTest() {
 // destructor calls __global_delete instead of directly
 // referencing ::operator delete. This is critical for environments like
 // kernel mode where no global ::operator delete exists.
-// Verify __empty_global_delete traps (the code path is unreachable at runtime).
-// X64: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K@Z"(ptr noundef %0, i64 noundef %1)
-// X64-NEXT: call void @llvm.trap()
-// X64-NEXT: unreachable
-
-// Verify that when ::delete is us...
[truncated]

@dyung dyung moved this from Needs Triage to Needs Review in LLVM Release Status Jul 16, 2026
@dpaoliello
dpaoliello requested a review from efriedma-quic July 17, 2026 17:03

@efriedma-quic efriedma-quic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@github-project-automation github-project-automation Bot moved this from Needs Review to Needs Merge in LLVM Release Status Jul 17, 2026
…delete (llvm#209585)

PR llvm#188372 made Clang's MSVC-ABI deleting-destructor path route global
deletes
through compiler-generated __global_delete / __global_array_delete
wrappers
instead of referencing ::operator delete directly. This lets a deleting
destructor be emitted in environments (e.g. kernel mode) where no global
::operator delete exists: each wrapper defaults to a trapping
__empty_global_delete fallback, and a real forwarding body that calls
::operator delete is materialized only when the program actually uses
::delete.

This change refines how those wrappers and their fallbacks are emitted,
fixing
two problems that made the llvm#188372 mechanism fail in practice.

New behavior:

- The trapping fallback is now emitted as a weak GlobalAlias to
__empty_global_delete rather than via an /alternatename directive. This
lowers
to a COFF weak-external-with-default -- exactly what MSVC emits -- so a
real
__global_delete defined in any TU overrides the fallback in every other
TU at
  link time.

- A ::delete expression now registers the wrapper for its resolved
global
operator delete directly (in EmitCXXDeleteExpr), so the strong
forwarding body
is emitted in that TU regardless of where the class's deleting
destructor is
defined. This matches MSVC, which emits the forwarder at every ::delete
site.

Bugs fixed:

1. Arm64EC (miscompile -> LNK2019). /alternatename only names the plain
symbol,
not the backend-generated "$$h" hybrid EC symbol referenced by the exit
thunk, so Arm64EC images left __global_delete$exit_thunk unresolved. The
weak
GlobalAlias correctly produces the "$$h" symbol, exit thunk, and plain
alias
   (verified on both x64 and Arm64EC).

2. Cross-TU ::delete (runtime crash). Previously the forwarding body was
only
emitted in a TU that *both* emitted a matching vector deleting
destructor and
contained a ::delete. A TU that only performed `::delete p` -- with the
class
defined elsewhere -- emitted no forwarder, so nothing overrode the
weak-alias
trap and the program executed a trapping __empty_global_delete at
runtime
(STATUS_ILLEGAL_INSTRUCTION). Registering the wrapper at the ::delete
site
   fixes this.

To share wrapper-creation logic between the deleting-destructor path and
the
delete-expression path, getOrCreateMSVCGlobalDeleteWrapper is promoted
from a
static helper in CGClass.cpp to a CodeGenModule method.

Verified end-to-end: a weak alias in one TU and a strong forwarder in
another
resolve to the forwarder under both lld-link and MSVC link.exe, and the
previously-crashing delete-only TU now runs cleanly.
@dyung
dyung merged commit 71ee145 into llvm:release/23.x Jul 22, 2026
1 of 2 checks passed
@github-project-automation github-project-automation Bot moved this from Needs Merge to Done in LLVM Release Status Jul 22, 2026
@dpaoliello
dpaoliello deleted the 23globaldelfix branch July 23, 2026 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:codegen IR generation bugs: mangling, exceptions, etc. clang Clang issues not falling into any other category

Projects

Development

Successfully merging this pull request may close these issues.

3 participants