Skip to content

Commit c87be72

Browse files
[win][clang] Align scalar deleting destructors with MSABI (llvm#139566)
While working on vector deleting destructors support ([GH19772](llvm#19772)), I noticed that MSVC produces different code in scalar deleting destructor body depending on whether class defined its own operator delete. In MSABI deleting destructors accept an additional implicit flag parameter allowing some sort of flexibility. The mismatch I noticed is that whenever a global operator delete is called, i.e. `::delete`, in the code produced by MSVC the implicit flag argument has a value that makes the 3rd bit set, i.e. "5" for scalar deleting destructors "7" for vector deleting destructors. Prior to this patch, clang handled `::delete` via calling global operator delete direct after the destructor call and not calling class operator delete in scalar deleting destructor body by passing "0" as implicit flag argument value. This is fine until there is an attempt to link binaries compiled with clang with binaries compiled with MSVC. The problem is that in binaries produced by MSVC the callsite of the destructor won't call global operator delete because it is assumed that the destructor will do that and a destructor body generated by clang will never do. This patch removes call to global operator delete from the callsite and add additional check of the 3rd bit of the implicit parameter inside of scalar deleting destructor body. --------- Co-authored-by: Tom Honermann <[email protected]>
1 parent a393f20 commit c87be72

25 files changed

+529
-55
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,23 @@ Potentially Breaking Changes
5252
``--gcc-install-dir`` command line argument. This will silence the
5353
warning. It can also be disabled using the
5454
``-Wno-gcc-install-dir-libstdcxx`` command line flag.
55+
- Scalar deleting destructor support has been aligned with MSVC when
56+
targeting the MSVC ABI. Clang previously implemented support for
57+
``::delete`` by calling the complete object destructor and then the
58+
appropriate global delete operator (as is done for the Itanium ABI).
59+
The scalar deleting destructor is now called to destroy the object
60+
and deallocate its storage. This is an ABI change that can result in
61+
memory corruption when a program built for the MSVC ABI has
62+
portions compiled with clang 21 or earlier and portions compiled
63+
with a version of clang 22 (or MSVC). Consider a class ``X`` that
64+
declares a virtual destructor and an ``operator delete`` member
65+
with the destructor defined in library ``A`` and a call to `::delete`` in
66+
library ``B``. If library ``A`` is compiled with clang 21 and library ``B``
67+
is compiled with clang 22, the ``::delete`` call might dispatch to the
68+
scalar deleting destructor emitted in library ``A`` which will erroneously
69+
call the member ``operator delete`` instead of the expected global
70+
delete operator. The old behavior is retained under ``-fclang-abi-compat=21``
71+
flag.
5572

5673
C/C++ Language Potentially Breaking Changes
5774
-------------------------------------------

clang/include/clang/AST/ASTMutationListener.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ class ASTMutationListener {
8686
const FunctionDecl *Delete,
8787
Expr *ThisArg) {}
8888

89+
/// A virtual destructor's operator global delete has been resolved.
90+
virtual void ResolvedOperatorGlobDelete(const CXXDestructorDecl *DD,
91+
const FunctionDecl *GlobDelete) {}
92+
8993
/// An implicit member got a definition.
9094
virtual void CompletedImplicitDefinition(const FunctionDecl *D) {}
9195

clang/include/clang/AST/DeclCXX.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2873,6 +2873,7 @@ class CXXDestructorDecl : public CXXMethodDecl {
28732873
// FIXME: Don't allocate storage for these except in the first declaration
28742874
// of a virtual destructor.
28752875
FunctionDecl *OperatorDelete = nullptr;
2876+
FunctionDecl *OperatorGlobalDelete = nullptr;
28762877
Expr *OperatorDeleteThisArg = nullptr;
28772878

28782879
CXXDestructorDecl(ASTContext &C, CXXRecordDecl *RD, SourceLocation StartLoc,
@@ -2898,11 +2899,16 @@ class CXXDestructorDecl : public CXXMethodDecl {
28982899
static CXXDestructorDecl *CreateDeserialized(ASTContext &C, GlobalDeclID ID);
28992900

29002901
void setOperatorDelete(FunctionDecl *OD, Expr *ThisArg);
2902+
void setOperatorGlobalDelete(FunctionDecl *OD);
29012903

29022904
const FunctionDecl *getOperatorDelete() const {
29032905
return getCanonicalDecl()->OperatorDelete;
29042906
}
29052907

2908+
const FunctionDecl *getOperatorGlobalDelete() const {
2909+
return getCanonicalDecl()->OperatorGlobalDelete;
2910+
}
2911+
29062912
Expr *getOperatorDeleteThisArg() const {
29072913
return getCanonicalDecl()->OperatorDeleteThisArg;
29082914
}

clang/include/clang/Basic/ABIVersions.def

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ ABI_VER_MAJOR(19)
127127
/// - Incorrectly return C++ records in AVX registers on x86_64.
128128
ABI_VER_MAJOR(20)
129129

130+
/// Attempt to be ABI-compatible with code generated by Clang 21.0.x.
131+
/// This causes clang to:
132+
/// - When targeting Windows emit scalar deleting destructors that are not
133+
/// compatible with scalar deleting destructors emitted by MSVC for the
134+
/// cases when the class whose destructor is being emitted defines
135+
/// operator delete.
136+
ABI_VER_MAJOR(21)
137+
130138
/// Conform to the underlying platform's C and C++ ABIs as closely as we can.
131139
ABI_VER_LATEST(Latest)
132140

clang/include/clang/Basic/TargetInfo.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1762,6 +1762,14 @@ class TargetInfo : public TransferrableTargetInfo,
17621762
/// Clang backwards compatibility rather than GCC/Itanium ABI compatibility.
17631763
virtual bool areDefaultedSMFStillPOD(const LangOptions&) const;
17641764

1765+
/// Controls whether global operator delete is called by the deleting
1766+
/// destructor or at the point where ::delete was called. Historically Clang
1767+
/// called global operator delete outside of the deleting destructor for both
1768+
/// Microsoft and Itanium ABI. In Clang 21 support for ::delete was aligned
1769+
/// with Microsoft ABI, so it will call global operator delete in the deleting
1770+
/// destructor body.
1771+
virtual bool callGlobalDeleteInDeletingDtor(const LangOptions &) const;
1772+
17651773
/// Controls if __builtin_longjmp / __builtin_setjmp can be lowered to
17661774
/// llvm.eh.sjlj.longjmp / llvm.eh.sjlj.setjmp.
17671775
virtual bool hasSjLjLowering() const {

clang/include/clang/Sema/Sema.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8554,10 +8554,12 @@ class Sema final : public SemaBase {
85548554
bool Diagnose = true);
85558555
FunctionDecl *FindUsualDeallocationFunction(SourceLocation StartLoc,
85568556
ImplicitDeallocationParameters,
8557-
DeclarationName Name);
8557+
DeclarationName Name,
8558+
bool Diagnose = true);
85588559
FunctionDecl *FindDeallocationFunctionForDestructor(SourceLocation StartLoc,
85598560
CXXRecordDecl *RD,
8560-
bool Diagnose = true);
8561+
bool Diagnose,
8562+
bool LookForGlobal);
85618563

85628564
/// ActOnCXXDelete - Parsed a C++ 'delete' expression (C++ 5.3.5), as in:
85638565
/// @code ::delete ptr; @endcode

clang/include/clang/Serialization/ASTWriter.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,8 @@ class ASTWriter : public ASTDeserializationListener,
949949
void ResolvedOperatorDelete(const CXXDestructorDecl *DD,
950950
const FunctionDecl *Delete,
951951
Expr *ThisArg) override;
952+
void ResolvedOperatorGlobDelete(const CXXDestructorDecl *DD,
953+
const FunctionDecl *Delete) override;
952954
void CompletedImplicitDefinition(const FunctionDecl *D) override;
953955
void InstantiationRequested(const ValueDecl *D) override;
954956
void VariableDefinitionInstantiated(const VarDecl *D) override;

clang/lib/AST/DeclCXX.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3119,6 +3119,22 @@ void CXXDestructorDecl::setOperatorDelete(FunctionDecl *OD, Expr *ThisArg) {
31193119
}
31203120
}
31213121

3122+
void CXXDestructorDecl::setOperatorGlobalDelete(FunctionDecl *OD) {
3123+
// FIXME: C++23 [expr.delete] specifies that the delete operator will be
3124+
// a usual deallocation function declared at global scope. A convenient
3125+
// function to assert that is lacking; Sema::isUsualDeallocationFunction()
3126+
// only works for CXXMethodDecl.
3127+
assert(!OD ||
3128+
(OD->getDeclName().getCXXOverloadedOperator() == OO_Delete &&
3129+
OD->getDeclContext()->getRedeclContext()->isTranslationUnit()));
3130+
auto *Canonical = cast<CXXDestructorDecl>(getCanonicalDecl());
3131+
if (!Canonical->OperatorGlobalDelete) {
3132+
Canonical->OperatorGlobalDelete = OD;
3133+
if (auto *L = getASTMutationListener())
3134+
L->ResolvedOperatorGlobDelete(Canonical, OD);
3135+
}
3136+
}
3137+
31223138
bool CXXDestructorDecl::isCalledByDelete(const FunctionDecl *OpDel) const {
31233139
// C++20 [expr.delete]p6: If the value of the operand of the delete-
31243140
// expression is not a null pointer value and the selected deallocation

clang/lib/Basic/TargetInfo.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,14 @@ TargetInfo::getCallingConvKind(bool ClangABICompat4) const {
626626
return CCK_Default;
627627
}
628628

629+
bool TargetInfo::callGlobalDeleteInDeletingDtor(
630+
const LangOptions &LangOpts) const {
631+
if (getCXXABI() == TargetCXXABI::Microsoft &&
632+
LangOpts.getClangABICompat() > LangOptions::ClangABI::Ver21)
633+
return true;
634+
return false;
635+
}
636+
629637
bool TargetInfo::areDefaultedSMFStillPOD(const LangOptions &LangOpts) const {
630638
return LangOpts.getClangABICompat() > LangOptions::ClangABI::Ver15;
631639
}

clang/lib/CodeGen/CGClass.cpp

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,29 +1599,85 @@ namespace {
15991599
}
16001600
};
16011601

1602+
// This function implements generation of scalar deleting destructor body for
1603+
// the case when the destructor also accepts an implicit flag. Right now only
1604+
// Microsoft ABI requires deleting destructors to accept implicit flags.
1605+
// The flag indicates whether an operator delete should be called and whether
1606+
// it should be a class-specific operator delete or a global one.
16021607
void EmitConditionalDtorDeleteCall(CodeGenFunction &CGF,
16031608
llvm::Value *ShouldDeleteCondition,
16041609
bool ReturnAfterDelete) {
1610+
const CXXDestructorDecl *Dtor = cast<CXXDestructorDecl>(CGF.CurCodeDecl);
1611+
const CXXRecordDecl *ClassDecl = Dtor->getParent();
1612+
const FunctionDecl *OD = Dtor->getOperatorDelete();
1613+
assert(OD->isDestroyingOperatorDelete() == ReturnAfterDelete &&
1614+
"unexpected value for ReturnAfterDelete");
1615+
auto *CondTy = cast<llvm::IntegerType>(ShouldDeleteCondition->getType());
1616+
// MSVC calls global operator delete inside of the dtor body, but clang
1617+
// aligned with this behavior only after a particular version. This is not
1618+
// ABI-compatible with previous versions.
1619+
ASTContext &Context = CGF.getContext();
1620+
bool CallGlobDelete =
1621+
Context.getTargetInfo().callGlobalDeleteInDeletingDtor(
1622+
Context.getLangOpts());
1623+
if (CallGlobDelete && OD->isDestroyingOperatorDelete()) {
1624+
llvm::BasicBlock *CallDtor = CGF.createBasicBlock("dtor.call_dtor");
1625+
llvm::BasicBlock *DontCallDtor = CGF.createBasicBlock("dtor.entry_cont");
1626+
// Third bit set signals that global operator delete is called. That means
1627+
// despite class having destroying operator delete which is responsible
1628+
// for calling dtor, we need to call dtor because global operator delete
1629+
// won't do that.
1630+
llvm::Value *Check3rdBit = CGF.Builder.CreateAnd(
1631+
ShouldDeleteCondition, llvm::ConstantInt::get(CondTy, 4));
1632+
llvm::Value *ShouldCallDtor = CGF.Builder.CreateIsNull(Check3rdBit);
1633+
CGF.Builder.CreateCondBr(ShouldCallDtor, DontCallDtor, CallDtor);
1634+
CGF.EmitBlock(CallDtor);
1635+
QualType ThisTy = Dtor->getFunctionObjectParameterType();
1636+
CGF.EmitCXXDestructorCall(Dtor, Dtor_Complete, /*ForVirtualBase=*/false,
1637+
/*Delegating=*/false, CGF.LoadCXXThisAddress(),
1638+
ThisTy);
1639+
CGF.Builder.CreateBr(DontCallDtor);
1640+
CGF.EmitBlock(DontCallDtor);
1641+
}
16051642
llvm::BasicBlock *callDeleteBB = CGF.createBasicBlock("dtor.call_delete");
16061643
llvm::BasicBlock *continueBB = CGF.createBasicBlock("dtor.continue");
1607-
llvm::Value *ShouldCallDelete
1608-
= CGF.Builder.CreateIsNull(ShouldDeleteCondition);
1644+
// First bit set signals that operator delete must be called.
1645+
llvm::Value *Check1stBit = CGF.Builder.CreateAnd(
1646+
ShouldDeleteCondition, llvm::ConstantInt::get(CondTy, 1));
1647+
llvm::Value *ShouldCallDelete = CGF.Builder.CreateIsNull(Check1stBit);
16091648
CGF.Builder.CreateCondBr(ShouldCallDelete, continueBB, callDeleteBB);
16101649

16111650
CGF.EmitBlock(callDeleteBB);
1612-
const CXXDestructorDecl *Dtor = cast<CXXDestructorDecl>(CGF.CurCodeDecl);
1613-
const CXXRecordDecl *ClassDecl = Dtor->getParent();
1614-
CGF.EmitDeleteCall(Dtor->getOperatorDelete(),
1615-
LoadThisForDtorDelete(CGF, Dtor),
1616-
CGF.getContext().getCanonicalTagType(ClassDecl));
1617-
assert(Dtor->getOperatorDelete()->isDestroyingOperatorDelete() ==
1618-
ReturnAfterDelete &&
1619-
"unexpected value for ReturnAfterDelete");
1620-
if (ReturnAfterDelete)
1621-
CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
1622-
else
1623-
CGF.Builder.CreateBr(continueBB);
1624-
1651+
auto EmitDeleteAndGoToEnd = [&](const FunctionDecl *DeleteOp) {
1652+
CGF.EmitDeleteCall(DeleteOp, LoadThisForDtorDelete(CGF, Dtor),
1653+
Context.getCanonicalTagType(ClassDecl));
1654+
if (ReturnAfterDelete)
1655+
CGF.EmitBranchThroughCleanup(CGF.ReturnBlock);
1656+
else
1657+
CGF.Builder.CreateBr(continueBB);
1658+
};
1659+
// If Sema only found a global operator delete previously, the dtor can
1660+
// always call it. Otherwise we need to check the third bit and call the
1661+
// appropriate operator delete, i.e. global or class-specific.
1662+
if (const FunctionDecl *GlobOD = Dtor->getOperatorGlobalDelete();
1663+
isa<CXXMethodDecl>(OD) && GlobOD && CallGlobDelete) {
1664+
// Third bit set signals that global operator delete is called, i.e.
1665+
// ::delete appears on the callsite.
1666+
llvm::Value *CheckTheBitForGlobDeleteCall = CGF.Builder.CreateAnd(
1667+
ShouldDeleteCondition, llvm::ConstantInt::get(CondTy, 4));
1668+
llvm::Value *ShouldCallGlobDelete =
1669+
CGF.Builder.CreateIsNull(CheckTheBitForGlobDeleteCall);
1670+
llvm::BasicBlock *GlobDelete =
1671+
CGF.createBasicBlock("dtor.call_glob_delete");
1672+
llvm::BasicBlock *ClassDelete =
1673+
CGF.createBasicBlock("dtor.call_class_delete");
1674+
CGF.Builder.CreateCondBr(ShouldCallGlobDelete, ClassDelete, GlobDelete);
1675+
CGF.EmitBlock(GlobDelete);
1676+
1677+
EmitDeleteAndGoToEnd(GlobOD);
1678+
CGF.EmitBlock(ClassDelete);
1679+
}
1680+
EmitDeleteAndGoToEnd(OD);
16251681
CGF.EmitBlock(continueBB);
16261682
}
16271683

0 commit comments

Comments
 (0)