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
