Skip to content

[win][clang] Align scalar deleting destructors with MSABI #139566

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ Potentially Breaking Changes
``endbr64`` instruction at the labels named as possible branch
destinations, so it is not safe to use a register-controlled branch
instruction to branch to one. (In line with gcc.)
- Scalar deleting destructor support has been aligned with MSVC when
targeting the MSVC ABI. Clang previously implemented support for
``::delete`` by calling the complete object destructor and then the
appropriate global delete operator (as is done for the Itanium ABI).
The scalar deleting destructor is now called to destroy the object
and deallocate its storage. This is an ABI change that can result in
memory corruption when a program built for the MSVC ABI has
portions compiled with clang 20 or earlier and portions compiled
with a version of clang 21 (or MSVC). Consider a class ``X`` that
declares a virtual destructor and an ``operator delete`` member
with the destructor defined in library ``A`` and a call to `::delete`` in
library ``B``. If library ``A`` is compiled with clang 20 and library ``B``
is compiled with clang 21, the ``::delete`` call might dispatch to the
scalar deleting destructor emitted in library ``A`` which will erroneously
call the member ``operator delete`` instead of the expected global
delete operator.

C/C++ Language Potentially Breaking Changes
-------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions clang/include/clang/AST/ASTMutationListener.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ class ASTMutationListener {
const FunctionDecl *Delete,
Expr *ThisArg) {}

/// A virtual destructor's operator global delete has been resolved.
virtual void ResolvedOperatorGlobDelete(const CXXDestructorDecl *DD,
const FunctionDecl *GlobDelete) {}

/// An implicit member got a definition.
virtual void CompletedImplicitDefinition(const FunctionDecl *D) {}

Expand Down
6 changes: 6 additions & 0 deletions clang/include/clang/AST/DeclCXX.h
Original file line number Diff line number Diff line change
Expand Up @@ -2854,6 +2854,7 @@ class CXXDestructorDecl : public CXXMethodDecl {
// FIXME: Don't allocate storage for these except in the first declaration
// of a virtual destructor.
FunctionDecl *OperatorDelete = nullptr;
FunctionDecl *OperatorGlobalDelete = nullptr;
Expr *OperatorDeleteThisArg = nullptr;

CXXDestructorDecl(ASTContext &C, CXXRecordDecl *RD, SourceLocation StartLoc,
Expand All @@ -2879,11 +2880,16 @@ class CXXDestructorDecl : public CXXMethodDecl {
static CXXDestructorDecl *CreateDeserialized(ASTContext &C, GlobalDeclID ID);

void setOperatorDelete(FunctionDecl *OD, Expr *ThisArg);
void setOperatorGlobalDelete(FunctionDecl *OD);

const FunctionDecl *getOperatorDelete() const {
return getCanonicalDecl()->OperatorDelete;
}

const FunctionDecl *getOperatorGlobalDelete() const {
return getCanonicalDecl()->OperatorGlobalDelete;
}

Expr *getOperatorDeleteThisArg() const {
return getCanonicalDecl()->OperatorDeleteThisArg;
}
Expand Down
8 changes: 8 additions & 0 deletions clang/include/clang/Basic/TargetInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -1741,6 +1741,14 @@ class TargetInfo : public TransferrableTargetInfo,
/// Clang backwards compatibility rather than GCC/Itanium ABI compatibility.
virtual bool areDefaultedSMFStillPOD(const LangOptions&) const;

/// Controls whether global operator delete is called by the deleting
/// destructor or at the point where ::delete was called. Historically Clang
/// called global operator delete outside of the deleting destructor for both
/// Microsoft and Itanium ABI. In Clang 21 support for ::delete was aligned
/// with Microsoft ABI, so it will call global operator delete in the deleting
/// destructor body.
virtual bool callGlobalDeleteInDeletingDtor(const LangOptions &) const;

/// Controls if __builtin_longjmp / __builtin_setjmp can be lowered to
/// llvm.eh.sjlj.longjmp / llvm.eh.sjlj.setjmp.
virtual bool hasSjLjLowering() const {
Expand Down
10 changes: 6 additions & 4 deletions clang/include/clang/Sema/Sema.h
Original file line number Diff line number Diff line change
Expand Up @@ -8523,10 +8523,12 @@ class Sema final : public SemaBase {
bool Diagnose = true);
FunctionDecl *FindUsualDeallocationFunction(SourceLocation StartLoc,
ImplicitDeallocationParameters,
DeclarationName Name);
FunctionDecl *FindDeallocationFunctionForDestructor(SourceLocation StartLoc,
CXXRecordDecl *RD,
bool Diagnose = true);
DeclarationName Name,
bool Diagnose = true);
FunctionDecl *
FindDeallocationFunctionForDestructor(SourceLocation StartLoc,
CXXRecordDecl *RD, bool Diagnose = true,
bool LookForGlobal = false);

/// ActOnCXXDelete - Parsed a C++ 'delete' expression (C++ 5.3.5), as in:
/// @code ::delete ptr; @endcode
Expand Down
2 changes: 2 additions & 0 deletions clang/include/clang/Serialization/ASTWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,8 @@ class ASTWriter : public ASTDeserializationListener,
void ResolvedOperatorDelete(const CXXDestructorDecl *DD,
const FunctionDecl *Delete,
Expr *ThisArg) override;
void ResolvedOperatorGlobDelete(const CXXDestructorDecl *DD,
const FunctionDecl *Delete) override;
void CompletedImplicitDefinition(const FunctionDecl *D) override;
void InstantiationRequested(const ValueDecl *D) override;
void VariableDefinitionInstantiated(const VarDecl *D) override;
Expand Down
16 changes: 16 additions & 0 deletions clang/lib/AST/DeclCXX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3101,6 +3101,22 @@ void CXXDestructorDecl::setOperatorDelete(FunctionDecl *OD, Expr *ThisArg) {
}
}

void CXXDestructorDecl::setOperatorGlobalDelete(FunctionDecl *OD) {
// FIXME: C++23 [expr.delete] specifies that the delete operator will be
// a usual deallocation function declared at global scope. A convenient
// function to assert that is lacking; Sema::isUsualDeallocationFunction()
// only works for CXXMethodDecl.
assert(!OD ||
(OD->getDeclName().getCXXOverloadedOperator() == OO_Delete &&
OD->getDeclContext()->getRedeclContext()->isTranslationUnit()));
auto *Canonical = cast<CXXDestructorDecl>(getCanonicalDecl());
if (!Canonical->OperatorGlobalDelete) {
Canonical->OperatorGlobalDelete = OD;
if (auto *L = getASTMutationListener())
L->ResolvedOperatorGlobDelete(Canonical, OD);
}
}

bool CXXDestructorDecl::isCalledByDelete(const FunctionDecl *OpDel) const {
// C++20 [expr.delete]p6: If the value of the operand of the delete-
// expression is not a null pointer value and the selected deallocation
Expand Down
8 changes: 8 additions & 0 deletions clang/lib/Basic/TargetInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,14 @@ TargetInfo::getCallingConvKind(bool ClangABICompat4) const {
return CCK_Default;
}

bool TargetInfo::callGlobalDeleteInDeletingDtor(
const LangOptions &LangOpts) const {
if (getCXXABI() == TargetCXXABI::Microsoft &&
LangOpts.getClangABICompat() > LangOptions::ClangABI::Ver20)
return true;
return false;
}

bool TargetInfo::areDefaultedSMFStillPOD(const LangOptions &LangOpts) const {
return LangOpts.getClangABICompat() > LangOptions::ClangABI::Ver15;
}
Expand Down
84 changes: 70 additions & 14 deletions clang/lib/CodeGen/CGClass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1587,28 +1587,84 @@ namespace {
}
};

// This function implements generation of scalar deleting destructor body for
// the case when the destructor also accepts an implicit flag. Right now only
// Microsoft ABI requires deleting destructors to accept implicit flags.
// The flag indicates whether an operator delete should be called and whether
// it should be a class-specific operator delete or a global one.
void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
llvm::Value *ShouldDeleteCondition,
bool ReturnAfterDelete) {
const CXXDestructorDecl *Dtor = cast<CXXDestructorDecl>(CGF.CurCodeDecl);
const CXXRecordDecl *ClassDecl = Dtor->getParent();
const FunctionDecl *OD = Dtor->getOperatorDelete();
assert(OD->isDestroyingOperatorDelete() == ReturnAfterDelete &&
"unexpected value for ReturnAfterDelete");
auto *CondTy = cast<llvm::IntegerType>(ShouldDeleteCondition->getType());
// Clang 20 calls global operator delete after dtor call. Clang 21 and newer
// call global operator delete inside of dtor body, as MSVC does.
ASTContext &Context = CGF.getContext();
bool Clang21AndNewer =
Context.getTargetInfo().callGlobalDeleteInDeletingDtor(
Context.getLangOpts());
if (Clang21AndNewer && OD->isDestroyingOperatorDelete()) {
llvm::BasicBlock *CallDtor = CGF.createBasicBlock("dtor.call_dtor");
llvm::BasicBlock *DontCallDtor = CGF.createBasicBlock("dtor.entry_cont");
// Third bit set signals that global operator delete is called. That means
// despite class having destroying operator delete which is responsible
// for calling dtor, we need to call dtor because global operator delete
// won't do that.
llvm::Value *Check3rdBit = CGF.Builder.CreateAnd(
ShouldDeleteCondition, llvm::ConstantInt::get(CondTy, 4));
llvm::Value *ShouldCallDtor = CGF.Builder.CreateIsNull(Check3rdBit);
CGF.Builder.CreateCondBr(ShouldCallDtor, DontCallDtor, CallDtor);
CGF.EmitBlock(CallDtor);
QualType ThisTy = Dtor->getFunctionObjectParameterType();
CGF.EmitCXXDestructorCall(Dtor, Dtor_Complete, /*ForVirtualBase=*/false,
/*Delegating=*/false, CGF.LoadCXXThisAddress(),
ThisTy);
CGF.Builder.CreateBr(DontCallDtor);
CGF.EmitBlock(DontCallDtor);
}
llvm::BasicBlock *callDeleteBB = CGF.createBasicBlock("dtor.call_delete");
llvm::BasicBlock *continueBB = CGF.createBasicBlock("dtor.continue");
llvm::Value *ShouldCallDelete
= CGF.Builder.CreateIsNull(ShouldDeleteCondition);
// First bit set signals that operator delete must be called.
llvm::Value *Check1stBit = CGF.Builder.CreateAnd(
ShouldDeleteCondition, llvm::ConstantInt::get(CondTy, 1));
llvm::Value *ShouldCallDelete = CGF.Builder.CreateIsNull(Check1stBit);
CGF.Builder.CreateCondBr(ShouldCallDelete, continueBB, callDeleteBB);

CGF.EmitBlock(callDeleteBB);
const CXXDestructorDecl *Dtor = cast<CXXDestructorDecl>(CGF.CurCodeDecl);
const CXXRecordDecl *ClassDecl = Dtor->getParent();
CGF.EmitDeleteCall(Dtor->getOperatorDelete(),
LoadThisForDtorDelete(CGF, Dtor),
CGF.getContext().getTagDeclType(ClassDecl));
assert(Dtor->getOperatorDelete()->isDestroyingOperatorDelete() ==
ReturnAfterDelete &&
"unexpected value for ReturnAfterDelete");
if (ReturnAfterDelete)
CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
else
CGF.Builder.CreateBr(continueBB);
auto EmitDeleteAndGoToEnd = [&](const FunctionDecl *DeleteOp) {
CGF.EmitDeleteCall(DeleteOp, LoadThisForDtorDelete(CGF, Dtor),
Context.getTagDeclType(ClassDecl));
if (ReturnAfterDelete)
CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
else
CGF.Builder.CreateBr(continueBB);
};
// If Sema only found a global operator delete previously, the dtor can
// always call it. Otherwise we need to check the third bit and call the
// appropriate operator delete, i.e. global or class-specific.
if (const FunctionDecl *GlobOD = Dtor->getOperatorGlobalDelete();
isa<CXXMethodDecl>(OD) && GlobOD && Clang21AndNewer) {
// Third bit set signals that global operator delete is called, i.e.
// ::delete appears on the callsite.
llvm::Value *CheckTheBitForGlobDeleteCall = CGF.Builder.CreateAnd(
ShouldDeleteCondition, llvm::ConstantInt::get(CondTy, 4));
llvm::Value *ShouldCallGlobDelete =
CGF.Builder.CreateIsNull(CheckTheBitForGlobDeleteCall);
llvm::BasicBlock *GlobDelete =
CGF.createBasicBlock("dtor.call_glob_delete");
llvm::BasicBlock *ClassDelete =
CGF.createBasicBlock("dtor.call_class_delete");
CGF.Builder.CreateCondBr(ShouldCallGlobDelete, ClassDelete, GlobDelete);
CGF.EmitBlock(GlobDelete);

EmitDeleteAndGoToEnd(GlobOD);
CGF.EmitBlock(ClassDelete);
}
EmitDeleteAndGoToEnd(OD);

CGF.EmitBlock(continueBB);
}
Expand Down
24 changes: 17 additions & 7 deletions clang/lib/CodeGen/MicrosoftCXXABI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -894,12 +894,19 @@ void MicrosoftCXXABI::emitVirtualObjectDelete(CodeGenFunction &CGF,
const CXXDestructorDecl *Dtor) {
// FIXME: Provide a source location here even though there's no
// CXXMemberCallExpr for dtor call.
bool UseGlobalDelete = DE->isGlobalDelete();
CXXDtorType DtorType = UseGlobalDelete ? Dtor_Complete : Dtor_Deleting;
llvm::Value *MDThis = EmitVirtualDestructorCall(CGF, Dtor, DtorType, Ptr, DE,
/*CallOrInvoke=*/nullptr);
if (UseGlobalDelete)
CGF.EmitDeleteCall(DE->getOperatorDelete(), MDThis, ElementType);
if (!getContext().getTargetInfo().callGlobalDeleteInDeletingDtor(
getContext().getLangOpts())) {
bool UseGlobalDelete = DE->isGlobalDelete();
CXXDtorType DtorType = UseGlobalDelete ? Dtor_Complete : Dtor_Deleting;
llvm::Value *MDThis =
EmitVirtualDestructorCall(CGF, Dtor, DtorType, Ptr, DE,
/*CallOrInvoke=*/nullptr);
if (UseGlobalDelete)
CGF.EmitDeleteCall(DE->getOperatorDelete(), MDThis, ElementType);
} else {
EmitVirtualDestructorCall(CGF, Dtor, Dtor_Deleting, Ptr, DE,
/*CallOrInvoke=*/nullptr);
}
}

void MicrosoftCXXABI::emitRethrow(CodeGenFunction &CGF, bool isNoReturn) {
Expand Down Expand Up @@ -2014,7 +2021,10 @@ llvm::Value *MicrosoftCXXABI::EmitVirtualDestructorCall(
ASTContext &Context = getContext();
llvm::Value *ImplicitParam = llvm::ConstantInt::get(
llvm::IntegerType::getInt32Ty(CGF.getLLVMContext()),
DtorType == Dtor_Deleting);
(DtorType == Dtor_Deleting) |
4 * (D && D->isGlobalDelete() &&
Context.getTargetInfo().callGlobalDeleteInDeletingDtor(
Context.getLangOpts())));

QualType ThisTy;
if (CE) {
Expand Down
7 changes: 7 additions & 0 deletions clang/lib/Frontend/MultiplexConsumer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ class MultiplexASTMutationListener : public ASTMutationListener {
void ResolvedOperatorDelete(const CXXDestructorDecl *DD,
const FunctionDecl *Delete,
Expr *ThisArg) override;
void ResolvedOperatorGlobDelete(const CXXDestructorDecl *DD,
const FunctionDecl *GlobDelete) override;
void CompletedImplicitDefinition(const FunctionDecl *D) override;
void InstantiationRequested(const ValueDecl *D) override;
void VariableDefinitionInstantiated(const VarDecl *D) override;
Expand Down Expand Up @@ -184,6 +186,11 @@ void MultiplexASTMutationListener::ResolvedOperatorDelete(
for (auto *L : Listeners)
L->ResolvedOperatorDelete(DD, Delete, ThisArg);
}
void MultiplexASTMutationListener::ResolvedOperatorGlobDelete(
const CXXDestructorDecl *DD, const FunctionDecl *GlobDelete) {
for (auto *L : Listeners)
L->ResolvedOperatorGlobDelete(DD, GlobDelete);
}
void MultiplexASTMutationListener::CompletedImplicitDefinition(
const FunctionDecl *D) {
for (size_t i = 0, e = Listeners.size(); i != e; ++i)
Expand Down
20 changes: 20 additions & 0 deletions clang/lib/Sema/SemaDeclCXX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11123,6 +11123,26 @@ bool Sema::CheckDestructor(CXXDestructorDecl *Destructor) {
DiagnoseUseOfDecl(OperatorDelete, Loc);
MarkFunctionReferenced(Loc, OperatorDelete);
Destructor->setOperatorDelete(OperatorDelete, ThisArg);

// Clang 20 calls global operator delete after dtor call. Clang 21 and
// newer call global operator delete inside of dtor body, as MSVC does.
// So we don't really need to fetch global operator delete for Clang 20
// ABI.
if (isa<CXXMethodDecl>(OperatorDelete) &&
Context.getTargetInfo().callGlobalDeleteInDeletingDtor(
Context.getLangOpts())) {
// In Microsoft ABI whenever a class has a defined operator delete,
// scalar deleting destructors check the 3rd bit of the implicit
// parameter and if it is set, then, global operator delete must be
// called instead of the class-specific one. Find and save the global
// operator delete for that case. Do not diagnose at this point because
// the lack of a global operator delete is not an error if there are no
// delete calls that require it.
FunctionDecl *GlobalOperatorDelete =
FindDeallocationFunctionForDestructor(Loc, RD, /*Diagnose*/ false,
/*LookForGlobal*/ true);
Destructor->setOperatorGlobalDelete(GlobalOperatorDelete);
}
}
}

Expand Down
19 changes: 11 additions & 8 deletions clang/lib/Sema/SemaExprCXX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3554,7 +3554,7 @@ void Sema::DeclareGlobalAllocationFunction(DeclarationName Name,
FunctionDecl *
Sema::FindUsualDeallocationFunction(SourceLocation StartLoc,
ImplicitDeallocationParameters IDP,
DeclarationName Name) {
DeclarationName Name, bool Diagnose) {
DeclareGlobalNewDelete();

LookupResult FoundDelete(*this, Name, StartLoc, LookupOrdinaryName);
Expand All @@ -3569,7 +3569,7 @@ Sema::FindUsualDeallocationFunction(SourceLocation StartLoc,
if (!Result)
return nullptr;

if (CheckDeleteOperator(*this, StartLoc, StartLoc, /*Diagnose=*/true,
if (CheckDeleteOperator(*this, StartLoc, StartLoc, Diagnose,
FoundDelete.getNamingClass(), Result.Found,
Result.FD))
return nullptr;
Expand All @@ -3580,7 +3580,8 @@ Sema::FindUsualDeallocationFunction(SourceLocation StartLoc,

FunctionDecl *Sema::FindDeallocationFunctionForDestructor(SourceLocation Loc,
CXXRecordDecl *RD,
bool Diagnose) {
bool Diagnose,
bool LookForGlobal) {
DeclarationName Name = Context.DeclarationNames.getCXXOperatorName(OO_Delete);

FunctionDecl *OperatorDelete = nullptr;
Expand All @@ -3589,18 +3590,20 @@ FunctionDecl *Sema::FindDeallocationFunctionForDestructor(SourceLocation Loc,
DeallocType, ShouldUseTypeAwareOperatorNewOrDelete(),
AlignedAllocationMode::No, SizedDeallocationMode::No};

if (FindDeallocationFunction(Loc, RD, Name, OperatorDelete, IDP, Diagnose))
return nullptr;
if (!LookForGlobal) {
if (FindDeallocationFunction(Loc, RD, Name, OperatorDelete, IDP, Diagnose))
return nullptr;

if (OperatorDelete)
return OperatorDelete;
if (OperatorDelete)
return OperatorDelete;
}

// If there's no class-specific operator delete, look up the global
// non-array delete.
IDP.PassAlignment = alignedAllocationModeFromBool(
hasNewExtendedAlignment(*this, DeallocType));
IDP.PassSize = SizedDeallocationMode::Yes;
return FindUsualDeallocationFunction(Loc, IDP, Name);
return FindUsualDeallocationFunction(Loc, IDP, Name, Diagnose);
}

bool Sema::FindDeallocationFunction(SourceLocation StartLoc, CXXRecordDecl *RD,
Expand Down
3 changes: 2 additions & 1 deletion clang/lib/Serialization/ASTCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ enum class DeclUpdateKind {
DeclMarkedOpenMPAllocate,
DeclMarkedOpenMPDeclareTarget,
DeclExported,
AddedAttrToRecord
AddedAttrToRecord,
CXXResolvedDtorGlobDelete
};

TypeIdx TypeIdxFromBuiltin(const BuiltinType *BT);
Expand Down
Loading