https://github.com/dpaoliello updated 
https://github.com/llvm/llvm-project/pull/209585

>From aee831143de94dd8aeffd9e36a7ef114259109df Mon Sep 17 00:00:00 2001
From: Daniel Paoliello <[email protected]>
Date: Tue, 14 Jul 2026 11:13:24 -0700
Subject: [PATCH 1/2] [clang][win] Fix __global_delete wrappers for Arm64EC and
 cross-TU ::delete

The MSVC-compatible deleting-destructor path routes global deletes through
compiler-generated __global_delete / __global_array_delete wrappers, each
defaulting to a trapping __empty_global_delete fallback that a real forwarding
body overrides when the program actually uses ::delete. Two problems made this
fall over in practice:

1. Arm64EC (miscompile -> LNK2019). The fallback was previously wired up with
   an /alternatename directive. /alternatename only names the plain symbol, not
   the backend-generated "$$h" hybrid EC symbol that the exit thunk references,
   so linking an Arm64EC image left __global_delete$exit_thunk with an
   unresolved external. Emit the fallback as a weak GlobalAlias instead. That
   lowers to a COFF weak-external-with-default, which is exactly what MSVC does
   and which correctly produces the "$$h" symbol, exit thunk, and plain alias
   (verified on both x64 and Arm64EC).

2. Cross-TU ::delete (runtime crash). The strong forwarding body was only
   emitted in a TU that *both* emitted a matching vector deleting destructor and
   contained a ::delete. A TU that only performs `::delete p` -- with the class
   (and its deleting destructor) defined in another TU -- emitted no forwarder,
   so nothing overrode the weak-alias trap and the program executed a trapping
   __empty_global_delete at runtime (STATUS_ILLEGAL_INSTRUCTION). MSVC emits the
   forwarder at every ::delete site; do the same by registering the wrapper for
   the resolved global operator delete in EmitCXXDeleteExpr, so a strong body is
   emitted locally regardless of where the destructor lives.

To share the 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.
---
 clang/lib/CodeGen/CGClass.cpp                 | 113 +-------------
 clang/lib/CodeGen/CGExprCXX.cpp               |  12 +-
 clang/lib/CodeGen/CodeGenModule.cpp           | 140 ++++++++++++++++--
 clang/lib/CodeGen/CodeGenModule.h             |  13 +-
 .../microsoft-vector-deleting-dtors.cpp       |  29 ++--
 .../microsoft-vector-deleting-dtors2.cpp      |  10 +-
 ...lobal-delete-forwarding-at-delete-site.cpp |  34 +++++
 .../msvc-global-delete-scalar-array-split.cpp |  21 +--
 .../msvc-global-delete-scope-no-dtor.cpp      |  23 +--
 .../msvc-no-global-delete-forwarding.cpp      |  10 +-
 10 files changed, 239 insertions(+), 166 deletions(-)
 create mode 100644 
clang/test/CodeGenCXX/msvc-global-delete-forwarding-at-delete-site.cpp

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..4725403b42932 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -2108,8 +2108,18 @@ 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.
+      if (const FunctionDecl *OD = E->getOperatorDelete();
+          OD && !isa<CXXMethodDecl>(OD))
+        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 used in the TU, a real __global_array_delete
-// forwarding body is emitted that calls through to the actual ::operator 
delete[].
-// X64: define linkonce_odr void @"?__global_array_delete@@YAXPEAX_K@Z"(ptr 
noundef %0, i64 noundef %1)
-// X64-NEXT: call void @"??_V@YAXPEAX_K@Z"(ptr %0, i64 %1)
-// X64-NEXT: ret void
 
 // X64-LABEL: define weak dso_local noundef ptr 
@"??_EKernelDerived@@UEAAPEAXI@Z"
 // Verify the array delete path in the VDD uses __global_array_delete.
@@ -394,7 +392,14 @@ void foobartest() {
 // X86: define weak dso_local x86_thiscallcc noundef ptr 
@"??_EAllocatedAsArray@@UAEPAXI@Z"
 // CLANG21: define linkonce_odr dso_local noundef ptr 
@"??_GAllocatedAsArray@@UEAAPEAXI@Z"
 
-// Verify the /ALTERNATENAME linker directive.
-// X64: 
!{!"/alternatename:?__global_delete@@YAXPEAX_K@Z=?__empty_global_delete@@YAXPEAX_K@Z"}
+// The forwarding bodies for the global-delete wrappers are emitted at
+// end-of-module (after all the deleting-destructor helpers). Because 
::delete[]
+// is used in the TU, both wrappers get real forwarding bodies: the array
+// __global_array_delete forwards to ::operator delete[] (??_V@) and the scalar
+// __global_delete forwards to ::operator delete (??3@).
+// X64: define linkonce_odr void @"?__global_array_delete@@YAXPEAX_K@Z"(ptr 
noundef %0, i64 noundef %1)
+// X64-NEXT: call void @"??_V@YAXPEAX_K@Z"(ptr %0, i64 %1)
+// X64: define linkonce_odr void @"?__global_delete@@YAXPEAX_K@Z"(ptr noundef 
%0, i64 noundef %1)
+// X64-NEXT: call void @"??3@YAXPEAX_K@Z"(ptr %0, i64 %1)
 
 // CLANG21-NOT: __global_delete
diff --git a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp 
b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
index 0cb596b9a5717..b2f3a1b14d04d 100644
--- a/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
+++ b/clang/test/CodeGenCXX/microsoft-vector-deleting-dtors2.cpp
@@ -94,12 +94,14 @@ void TesttheTest() {
 // X64: define linkonce_odr dso_local void 
@"??_V?$RefCounted@UDrawingBuffer@@@@SAXPEAX@Z"(ptr noundef %p)
 // X86: define linkonce_odr dso_local void 
@"??_V?$RefCounted@UDrawingBuffer@@@@SAXPAX@Z"(ptr noundef %p)
 
-// Verify that the dllexport class triggers __global_array_delete forwarding
-// body emission even without a ::delete expression in the TU.
+// X86: define linkonce_odr dso_local x86_thiscallcc noundef ptr 
@"??_GNoExport@@UAEPAXI@Z"(ptr noundef nonnull align 4 dereferenceable(4) 
%this, i32 noundef %should_call_delete)
+// X64: define linkonce_odr dso_local noundef ptr 
@"??_GNoExport@@UEAAPEAXI@Z"(ptr noundef nonnull align 8 dereferenceable(8) 
%this, i32 noundef %should_call_delete)
+
+// The wrapper forwarding bodies are emitted at end-of-module. Verify that the
+// dllexport class triggers __global_array_delete forwarding body emission even
+// without a ::delete expression in the TU.
 // X64: define linkonce_odr void @"?__global_array_delete@@YAXPEAX_K@Z"(ptr 
noundef %0, i64 noundef %1)
 // X64-NEXT: call void @"??_V@YAXPEAX_K@Z"(ptr %0, i64 %1)
 // X64-NEXT: ret void
 
-// X86: define linkonce_odr dso_local x86_thiscallcc noundef ptr 
@"??_GNoExport@@UAEPAXI@Z"(ptr noundef nonnull align 4 dereferenceable(4) 
%this, i32 noundef %should_call_delete)
-// X64: define linkonce_odr dso_local noundef ptr 
@"??_GNoExport@@UEAAPEAXI@Z"(ptr noundef nonnull align 8 dereferenceable(8) 
%this, i32 noundef %should_call_delete)
 // CHECK-NOT: define {{.*}}_V{{.*}}NoExport
diff --git 
a/clang/test/CodeGenCXX/msvc-global-delete-forwarding-at-delete-site.cpp 
b/clang/test/CodeGenCXX/msvc-global-delete-forwarding-at-delete-site.cpp
new file mode 100644
index 0000000000000..e4a254f6d2ae8
--- /dev/null
+++ b/clang/test/CodeGenCXX/msvc-global-delete-forwarding-at-delete-site.cpp
@@ -0,0 +1,34 @@
+// RUN: %clang_cc1 -emit-llvm -fms-extensions %s 
-triple=x86_64-pc-windows-msvc -o - | FileCheck %s
+
+// A `::delete` on a class whose deleting destructor is NOT defined in this TU
+// (only declared, e.g. defined in another TU) must still emit a strong
+// __global_delete forwarding body HERE. MSVC emits the forwarder at every
+// `::delete` site (validated against cl.exe). Without this, a TU that only
+// performs `::delete` (with the vector deleting destructor emitted in a
+// different TU) would emit no forwarder, leaving the wrapper bound to the
+// trapping __empty_global_delete fallback and crashing at runtime.
+
+struct W {
+  // Declared, but ~W() is defined in another TU, so no vector deleting
+  // destructor (and hence no __global_delete wrapper reference) is emitted in
+  // this TU. The forwarder must come from the ::delete site below.
+  virtual ~W();
+  void operator delete(void *);
+  int x;
+};
+
+void sink(W *p) { ::delete p; }
+
+// The shared trapping fallback is emitted (kept alive via llvm.used).
+// CHECK: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K@Z"(ptr 
noundef %0, i64 noundef %1)
+// CHECK-NEXT: call void @llvm.trap()
+// CHECK-NEXT: unreachable
+
+// The scalar wrapper gets a real forwarding body that calls the global
+// ::operator delete (??3@), even though no deleting destructor in this TU
+// references the wrapper.
+// CHECK: define linkonce_odr void @"?__global_delete@@YAXPEAX_K@Z"(ptr 
noundef %0, i64 noundef %1)
+// CHECK-NEXT: call void @"??3@YAXPEAX_K@Z"(ptr %0, i64 %1)
+
+// No vector deleting destructor for W is emitted in this TU.
+// CHECK-NOT: define {{.*}}@"??_EW@@
diff --git a/clang/test/CodeGenCXX/msvc-global-delete-scalar-array-split.cpp 
b/clang/test/CodeGenCXX/msvc-global-delete-scalar-array-split.cpp
index 1ff622b0465bb..b558b2e6c78d0 100644
--- a/clang/test/CodeGenCXX/msvc-global-delete-scalar-array-split.cpp
+++ b/clang/test/CodeGenCXX/msvc-global-delete-scalar-array-split.cpp
@@ -43,21 +43,22 @@ void test() {
   ::delete[] a;
 }
 
+// The shared __empty_global_delete fallback is emitted with a trapping body
+// (kept alive via llvm.used) even though both wrappers have real forwarding
+// bodies here.
+// CHECK: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K@Z"(ptr 
noundef %0, i64 noundef %1)
+// CHECK-NEXT: call void @llvm.trap()
+
 // The scalar deleting destructor's global path calls __global_delete.
 // CHECK: call void @"?__global_delete@@YAXPEAX_K@Z"(
 
-// It forwards to the scalar ??3@ (::operator delete).
-// CHECK: define linkonce_odr void @"?__global_delete@@YAXPEAX_K@Z"(ptr 
noundef %0, i64 noundef %1)
-// CHECK-NEXT: call void @"??3@YAXPEAX_K@Z"(ptr %0, i64 %1)
-
 // The vector deleting destructor's global array path calls 
__global_array_delete.
 // CHECK: call void @"?__global_array_delete@@YAXPEAX_K@Z"(
 
-// It forwards to the array ??_V@ (::operator delete[]).
+// The forwarding bodies are emitted at end-of-module. Each wrapper forwards to
+// its OWN global operator delete: __global_delete to the scalar ??3@,
+// __global_array_delete to the array ??_V@.
+// CHECK: define linkonce_odr void @"?__global_delete@@YAXPEAX_K@Z"(ptr 
noundef %0, i64 noundef %1)
+// CHECK-NEXT: call void @"??3@YAXPEAX_K@Z"(ptr %0, i64 %1)
 // CHECK: define linkonce_odr void @"?__global_array_delete@@YAXPEAX_K@Z"(ptr 
noundef %0, i64 noundef %1)
 // CHECK-NEXT: call void @"??_V@YAXPEAX_K@Z"(ptr %0, i64 %1)
-
-// Both wrappers share a single __empty_global_delete fallback, wired via
-// /ALTERNATENAME.
-// CHECK-DAG: 
!{!"/alternatename:?__global_delete@@YAXPEAX_K@Z=?__empty_global_delete@@YAXPEAX_K@Z"}
-// CHECK-DAG: 
!{!"/alternatename:?__global_array_delete@@YAXPEAX_K@Z=?__empty_global_delete@@YAXPEAX_K@Z"}
diff --git a/clang/test/CodeGenCXX/msvc-global-delete-scope-no-dtor.cpp 
b/clang/test/CodeGenCXX/msvc-global-delete-scope-no-dtor.cpp
index df0c2908267f0..09941600defa9 100644
--- a/clang/test/CodeGenCXX/msvc-global-delete-scope-no-dtor.cpp
+++ b/clang/test/CodeGenCXX/msvc-global-delete-scope-no-dtor.cpp
@@ -10,12 +10,12 @@
 // __global_delete machinery when a deleting destructor is involved, i.e. for a
 // `::delete` on a class type with a non-trivial destructor. A `::delete` on a
 // primitive or on a trivially-destructible class is lowered as a plain direct
-// operator delete, so MSVC leaves __global_delete as an unresolved weak
-// external that falls back to the trapping __empty_global_delete via
-// /ALTERNATENAME. (The destructor's virtualness and whether the class has its
-// own operator delete are irrelevant to this trigger.) Emitting a forwarding
-// body here would add a hard reference to the global operator delete and could
-// reintroduce LNK2001 in environments without one.
+// operator delete, so MSVC leaves __global_delete as a weak external that 
falls
+// back to the trapping __empty_global_delete. Clang models this with a weak
+// alias to __empty_global_delete. (The destructor's virtualness and whether 
the
+// class has its own operator delete are irrelevant to this trigger.) Emitting 
a
+// forwarding body here would add a hard reference to the global operator 
delete
+// and could reintroduce LNK2001 in environments without one.
 
 struct Base {
   void *operator new[](__SIZE_TYPE__);
@@ -50,6 +50,12 @@ void scopeTrivial(Trivial *q) {
   ::delete q;
 }
 
+// The __global_array_delete wrapper defaults to a weak alias to the trapping
+// fallback (no forwarding body of its own is emitted, asserted via the
+// --implicit-check-not on the RUN line). The alias is emitted before the
+// function definitions.
+// CHECK: @"?__global_array_delete@@YAXPEAX_K@Z" = weak alias void (ptr, i64), 
ptr @"?__empty_global_delete@@YAXPEAX_K@Z"
+
 // The vector deleting destructor still routes its global array-delete path
 // through the __global_array_delete wrapper.
 // CHECK: call void @"?__global_array_delete@@YAXPEAX_K@Z"
@@ -59,8 +65,3 @@ void scopeTrivial(Trivial *q) {
 // CHECK: define linkonce_odr void @"?__empty_global_delete@@YAXPEAX_K@Z"(ptr 
noundef %0, i64 noundef %1)
 // CHECK-NEXT: call void @llvm.trap()
 // CHECK-NEXT: unreachable
-
-// The /ALTERNATENAME directive wires __global_array_delete to the trapping
-// fallback; no forwarding body of its own is emitted (asserted via the
-// --implicit-check-not on the RUN line).
-// CHECK: 
!{!"/alternatename:?__global_array_delete@@YAXPEAX_K@Z=?__empty_global_delete@@YAXPEAX_K@Z"}
diff --git a/clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp 
b/clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp
index a4ac2610926cd..1703b81c7f7f7 100644
--- a/clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp
+++ b/clang/test/CodeGenCXX/msvc-no-global-delete-forwarding.cpp
@@ -24,6 +24,13 @@ void test() {
   delete[] p;
 }
 
+// Each wrapper defaults to a weak alias to the trapping __empty_global_delete
+// (matching MSVC's weak-external-with-default; this also works under Arm64EC,
+// unlike an /alternatename directive). These aliases are emitted before the
+// function definitions.
+// CHECK-DAG: @"?__global_delete@@YAXPEAX_K@Z" = weak alias void (ptr, i64), 
ptr @"?__empty_global_delete@@YAXPEAX_K@Z"
+// CHECK-DAG: @"?__global_array_delete@@YAXPEAX_K@Z" = weak alias void (ptr, 
i64), ptr @"?__empty_global_delete@@YAXPEAX_K@Z"
+
 // The VDD dispatches between class and global delete: the array path uses the
 // __global_array_delete wrapper, the scalar path uses __global_delete.
 // CHECK-LABEL: define weak dso_local noundef ptr @"??_EDerived@@UEAAPEAXI@Z"
@@ -43,6 +50,3 @@ void test() {
 // type in this TU, and no dllexport class).
 // CHECK-NOT: define {{.*}}void @"?__global_delete@@YAXPEAX_K@Z"
 // CHECK-NOT: define {{.*}}void @"?__global_array_delete@@YAXPEAX_K@Z"
-
-// Verify the /ALTERNATENAME linker directive.
-// CHECK: 
!{!"/alternatename:?__global_delete@@YAXPEAX_K@Z=?__empty_global_delete@@YAXPEAX_K@Z"}

>From e3ee830d98408efc8d7ab08ceb835ffa275ea599 Mon Sep 17 00:00:00 2001
From: Daniel Paoliello <[email protected]>
Date: Thu, 16 Jul 2026 10:50:43 -0700
Subject: [PATCH 2/2] Address PR feedback

---
 clang/lib/CodeGen/CGExprCXX.cpp | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 4725403b42932..1769ab00ed56d 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -2116,9 +2116,11 @@ void CodeGenFunction::EmitCXXDeleteExpr(const 
CXXDeleteExpr *E) {
       // 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.
-      if (const FunctionDecl *OD = E->getOperatorDelete();
-          OD && !isa<CXXMethodDecl>(OD))
-        CGM.getOrCreateMSVCGlobalDeleteWrapper(OD);
+      const FunctionDecl *OD = E->getOperatorDelete();
+      assert(!isa<CXXMethodDecl>(OD) &&
+             "global ::delete should resolve to a namespace-scope "
+             "operator delete");
+      CGM.getOrCreateMSVCGlobalDeleteWrapper(OD);
     }
   }
 

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to