https://github.com/Maoyao233 created
https://github.com/llvm/llvm-project/pull/235673
## Summary
On a Microsoft-ABI target, a module interface unit that instantiates a class
whose virtual
destructor is declared but not defined in that module makes an importing TU
crash while
generating LLVM IR.
**Root cause**: Because the destructor's declaration lives in another PCM, its
resolved `operator delete` can only travel as a `CXXResolvedDtorDelete`
`DeclUpdate`, which the reduced-BMI writer drops when the module never
references that declaration — leaving the importing TU with a null
`getOperatorDelete()` and crashing in `EnterDtorCleanups`. A release build
dereferences the null pointer instead.
This PR is a simple stopgap: it fixes this particular crash, but the underlying
issue -- wheter a `DeclUpdate` can be dropped is decided by per kind instead of
from what codegen will actually need -- affects other update kinds as well. I
plan to follow up with an RFC proposing an obligation-driven model.
## Reproducer
```C++
// a.cppm
export module a;
export template <class T> struct Base {
__attribute__((exclude_from_explicit_instantiation)) Base() {}
__attribute__((exclude_from_explicit_instantiation)) virtual ~Base();
};
extern template struct Base<int>;
// b.cppm
export module b;
import a;
export void make() { new Base<int>; }
// main.cpp
import a;
import b;
struct Local : Base<int> {};
Local l;
int main() { return 0; }
```
```bash
$ clang++ --target=x86_64-pc-windows-msvc -std=c++20 -c a.cppm -o a.o
$ clang++ --target=x86_64-pc-windows-msvc -std=c++20 -fmodule-file=a=a.pcm -c
b.cppm -o b.o
$ clang++ --target=x86_64-pc-windows-msvc -std=c++20 -fmodule-file=a=a.pcm
-fmodule-file=b=b.pcm -c main.cpp -o main.o
# the last command crashes
```
## Root Cause
1. `Sema::MarkVTableUsed()` (`SemaDeclCXX.cpp:19470`) has an MS-ABI-only hook:
"the deleting
destructor is emitted with the vtable, not with the destructor definition as
in the Itanium
ABI", so when the vtable is marked used it calls `CheckDestructor()`
(`SemaDeclCXX.cpp:11265`) for a user-declared, not-yet-defined virtual
destructor. That
resolves the `operator delete` (`CXXDestructorDecl::setOperatorDelete`,
`DeclCXX.cpp:3172` →
`ASTContext::addOperatorDeleteForVDtor`, `ASTContext.cpp:13770`). In
`b.cppm` the vtable use
comes from instantiating the inline constructor: `ActOnFinishFunctionBody`
(`SemaDecl.cpp:17063`) marks the vtable used for any constructor/destructor
whose body is
defined — including instantiated ones
(`SemaTemplateInstantiateDecl.cpp:6302`).
2. That destructor declaration is not owned by `b.cppm`: `extern template
struct Base<int>;`
placed it in `a.pcm`, so it is imported. The resolution is therefore
recorded as an update on
an imported declaration (`ASTWriter::ResolvedOperatorDelete`,
`ASTWriter.cpp:7847`):
```cpp
Chain->forEachImportedKeyDecl(DD, [&](const Decl *D) {
DeclUpdates[D].push_back(DeclUpdate(DeclUpdateKind::CXXResolvedDtorDelete,
Delete));
});
```
(A destructor owned by the module would be serialized into its own
declaration record,
`ASTWriterDecl.cpp:1791`.)
3. Module interface units are compiled as **reduced BMIs** by default (the
driver adds
`-fmodules-reduced-bmi`, `Clang.cpp:4328`). `ASTWriter::prepareLazyUpdates()`
(`ASTWriter.cpp:5710`) moves every update except
`CXXAddedFunctionDefinition` into
`DeclUpdatesLazy`; a lazy update is materialized only if the module writes a
declaration
reference to that declaration (`getLazyUpdates`, `ASTWriter.cpp:7233`,
called from
`GetDeclRef`). `b.cppm` never references `Base<int>::~Base` — the emitted
deleting
destructor and vtable slot are referred to by mangled name — so the update
is silently
dropped and `b.pcm` never carries the resolved `operator delete`.
4. The consumer emits `Base<int>`'s vtable (no key functions in the Microsoft
ABI) and its
deleting destructor: its locally emitted copy of the materialized
`Base<int>::Base()` stores
the vtable pointer → `CodeGenModule::addDeferredVTable`
(`CodeGenModule.h:1648`) →
`EmitDeferredVTables` → `GenerateClassData` →
`EnterDtorCleanups(Dtor_Deleting)`. That path
does not go through Sema's `MarkVTableUsed` hook, so nothing re-resolves the
`operator delete`
in the consumer.
An Itanium-ABI target does not crash: `Base<T>::~Base()` is the key function of
the
specialization (first user-declared, non-inline virtual member) and is never
defined, so
`Sema::DefineUsedVTables()` (`SemaDeclCXX.cpp:19501`) sets `DefineVTable =
false` and the vtable
is never emitted.
Instrumented runs confirm each step: the module TU resolves the `operator
delete` and queues the
update, `WriteDeclUpdatesBlocks` never emits a `DECL_UPDATES` record for it,
and the consumer's
`ASTReader::loadDeclUpdateRecords` reports no pending update for that
declaration.
## Fix
Keep the `CXXResolvedDtor*` updates eager, like `CXXAddedFunctionDefinition`.
An importer cannot recompute them -- the destructor may already have a body or
be defined in another TU -- but codegen
needs them to emit the deleting destructor with the vtable. The size cost is
negligible: these updates only exist when Sema resolved a delete operator for a
destructor declaration that is owned by another module.
An Itanium-ABI target does not crash: the not-yet-defined destructor is the key
function of the specialization, so no TU emits the vtable. Full BMIs (with
`-fno-modules-reduced-bmi`) do not crash either, which pins this on the lazy
update handling above.
Test: `clang/test/Modules/msvc-dtor-operator-delete-reduced-bmi.cpp`, a
split-file test that runs the three compilations above through %clang_cc1 with
`-fmodules-reduced-bmi`; it aborts on main without the fix and passes with it
(verified with an assertions build).
Fixes #231859.
Assisted by: Deepseek V4.1-flash-1017
>From d3dc5b27fac82821a2454558f6ec5b3e8eaa26bc Mon Sep 17 00:00:00 2001
From: Maoyao Ye <[email protected]>
Date: Mon, 26 Oct 2026 17:10:34 +0800
Subject: [PATCH] [Clang][modules] Don't drop resolved operator delete updates
in reduced BMIs
On a Microsoft ABI target, a module interface unit that instantiates a class
whose virtual destructor is declared but not defined in that module makes an
importing TU crash while generating code: CodeGenFunction::EnterDtorCleanups()
finds a null CXXDestructorDecl::getOperatorDelete(), which is an assertion
failure in an assertions build and a null pointer dereference otherwise.
In the Microsoft ABI the deleting destructor is emitted together with the
vtable, so Sema::MarkVTableUsed() calls CheckDestructor() for a user-declared,
not-yet-defined virtual destructor and that resolves the destructor's
'operator delete'. When the destructor declaration is owned by another PCM
(for example it is part of an 'extern template' specialization), the
resolution can only be recorded as a DeclUpdate. Module interface units are
compiled as reduced BMIs by default, and ASTWriter::prepareLazyUpdates() keeps
every update except CXXAddedFunctionDefinition lazy until the module writes a
declaration reference to the updated declaration; if it never does, the update
is dropped.
A consumer that emits the vtable and deleting destructor itself (there are no
key functions in the Microsoft ABI) does not re-resolve 'operator delete' on
that codegen path, so it ends up in EnterDtorCleanups() without one.
Keep the CXXResolvedDtor* updates eager, like CXXAddedFunctionDefinition: an
importer cannot recompute them, but codegen needs them to emit the deleting
destructor.
Fixes #231859.
Assisted by: Deepseek V4.1-flash-1017
---
clang/lib/Serialization/ASTWriter.cpp | 10 ++++
.../msvc-dtor-operator-delete-reduced-bmi.cpp | 46 +++++++++++++++++++
2 files changed, 56 insertions(+)
create mode 100644 clang/test/Modules/msvc-dtor-operator-delete-reduced-bmi.cpp
diff --git a/clang/lib/Serialization/ASTWriter.cpp
b/clang/lib/Serialization/ASTWriter.cpp
index 0d9ec7a460dfbf..35b1a17d015d12 100644
--- a/clang/lib/Serialization/ASTWriter.cpp
+++ b/clang/lib/Serialization/ASTWriter.cpp
@@ -5731,6 +5731,16 @@ void ASTWriter::prepareLazyUpdates() {
if (Kind == DeclUpdateKind::CXXAddedFunctionDefinition)
DeclUpdatesTmp[D].push_back(
ASTWriter::DeclUpdate(DeclUpdateKind::CXXAddedFunctionDefinition));
+ else if (Kind == DeclUpdateKind::CXXResolvedDtorDelete ||
+ Kind == DeclUpdateKind::CXXResolvedDtorGlobDelete ||
+ Kind == DeclUpdateKind::CXXResolvedDtorArrayDelete ||
+ Kind == DeclUpdateKind::CXXResolvedDtorGlobArrayDelete)
+ // These record the 'operator delete' that Sema resolved for a virtual
+ // destructor in this TU. An importer cannot recompute it: the
+ // destructor may already have a body or be defined in another TU. But
+ // codegen needs it, since in the Microsoft ABI the deleting destructor
+ // is emitted along with the vtable. Keep them eager.
+ DeclUpdatesTmp[D].push_back(Update);
else
DeclUpdatesLazy[D].push_back(Update);
}
diff --git a/clang/test/Modules/msvc-dtor-operator-delete-reduced-bmi.cpp
b/clang/test/Modules/msvc-dtor-operator-delete-reduced-bmi.cpp
new file mode 100644
index 00000000000000..4ab4dfa12ceea2
--- /dev/null
+++ b/clang/test/Modules/msvc-dtor-operator-delete-reduced-bmi.cpp
@@ -0,0 +1,46 @@
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+//
+// RUN: %clang_cc1 -triple x86_64-pc-windows-msvc -std=c++20 %t/a.cppm \
+// RUN: -emit-obj -fmodules-reduced-bmi -fmodule-output=%t/a.pcm -o %t/a.o
+// RUN: %clang_cc1 -triple x86_64-pc-windows-msvc -std=c++20 %t/b.cppm \
+// RUN: -emit-obj -fmodules-reduced-bmi -fmodule-output=%t/b.pcm \
+// RUN: -fmodule-file=a=%t/a.pcm -o %t/b.o
+// RUN: %clang_cc1 -triple x86_64-pc-windows-msvc -std=c++20 %t/main.cpp \
+// RUN: -emit-obj -fmodule-file=a=%t/a.pcm -fmodule-file=b=%t/b.pcm -o
%t/main.o
+
+// In the Microsoft ABI the deleting destructor is emitted together with the
+// vtable, so it needs the 'operator delete' that Sema resolved for the virtual
+// destructor. Here 'operator delete' is resolved for a destructor declaration
+// that is owned by module 'a' (the 'extern template' declaration puts it in
+// a.pcm), so the resolution is recorded as a DeclUpdate on an imported
+// declaration. With reduced BMIs such an update used to be kept lazily and
+// then dropped, because the module never wrote a declaration reference to the
+// destructor; a consumer that emits the vtable then crashed in
+// CGClass.cpp's EnterDtorCleanups with a null getOperatorDelete().
+
+//--- a.cppm
+export module a;
+
+export template <class T> struct Base {
+ __attribute__((exclude_from_explicit_instantiation)) Base() {}
+ __attribute__((exclude_from_explicit_instantiation)) virtual ~Base();
+};
+extern template struct Base<int>;
+
+//--- b.cppm
+export module b;
+import a;
+
+// Instantiating this constructor marks the vtable used
(ActOnFinishFunctionBody)
+// and Sema then resolves 'operator delete' for the imported destructor.
+export void make() { new Base<int>; }
+
+//--- main.cpp
+import a;
+import b;
+
+struct Local : Base<int> {};
+Local l;
+
+int main() { return 0; }
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits