WindowsAppSDK
WindowsAppSDK copied to clipboard
Windows.Data.Pdf.PdfDocument's destructor re-enters the message loop (C++/WinRT)
Describe the bug
This makes:
struct Foo : public Bar {
PdfDocument pdf;
};
... unsafe, if Foo is destructed from the main event loop;
this leads to:
- Entering message loop
- deleting Foo for some application-specific reason
- Entering
~Foo - Entering
~PdfDocument - ... re-entering the message loop while we're still in
~Foo, so:
- the pointer (raw/unique/shared/whatever) hasn't yet been cleared because deletion is in progress
- the object now has an invalid vtable
- depending on the application logic, this may lead to a double-free
Steps to reproduce the bug
- create struct like above in event loop, with make_unique
- delete in event loop if set
- double-free
Expected behavior
destructors should not enter message loop
Screenshots
No response
NuGet package version
Windows App SDK 1.2.4: 1.2.230217.4
Packaging type
Unpackaged
Windows version
Windows 10 version 22H2 (19045, 2022 Update)
IDE
Other
Additional context
Workaround:
PDFFilePageSource::~PDFFilePageSource() {
if (mPDFDocument) {
// Windows.Data.Pdf.PdfDocument's destructor re-enters the message loop -
// which means we can do:
// 1. Enter message loop
// 2. Enter ~PDFFilePageSource()
// 3. Enter ~PdfDocument()
// 4. Re-enter the message loop while `this` is partially destructed, and
// has an invalid vtable
// 5. make pure virtual calls, double-free, or other badness
// Work around this by rescheduling the deletion until 'later'
[](auto deleteLater) -> winrt::fire_and_forget {
// wil::resume_foreground guarantees that we will be rescheduled, even
// though we're not changing threads.
co_await wil::resume_foreground(winrt::Microsoft::UI::Dispatching::
DispatcherQueue::GetForCurrentThread());
}(std::move(mPDFDocument));
}
}
Thanks for the report & workaround, we will add this to our Win32 documentation.
Shouldn't this issue be fixed, instead of a workaround being glorified in docs?
This bug makes anything using the PDF libraries defy standard expectations: it makes it so that the destructor can call any other code in your program - i.e. it makes the destructor have all the potential side effects of a co_await. This is not just a documentation issue, it completely breaks the C++ programming model.