sentry-native
sentry-native copied to clipboard
Crashes in WinUI not caught
Crashes in WinUI are not caught by Crashpad/Breakpad
When does the problem happen
- [ ] During build
- [ ] During run-time
- [x] When capturing a hard crash
Environment
- OS: Windows 11
- Compiler: MSVC 2022
- CMake version and config: https://github.com/microsoft/vcpkg/tree/master/ports/sentry-native 0.4.12 with Crashpad/Breakpad
Steps To Reproduce
- Create a WinUI 3 C++ packaged app project based on the template provided by Windows App SDK
- Initialize sentry in App::OnLaunched and use a custom logger to view output
void custom_sentry_logger(sentry_level_t level, const char* message, va_list args, void* userdata)
{
// TODO: generate formatted string
OutputDebugStringA(message);
}
sentry_options_t* options = sentry_options_new();
sentry_options_set_dsn(options, "xxx");
auto installedLocation = Windows::ApplicationModel::Package::Current().InstalledLocation();
sentry_options_set_handler_path(options, to_string(installedLocation.Path() + L"\\crashpad_handler.exe").c_str());
auto localFolder = Windows::Storage::ApplicationData::Current().LocalFolder();
auto path = localFolder.Path();
sentry_options_set_database_path(options, to_string(localFolder.Path()).c_str());
sentry_options_set_logger(options, custom_sentry_logger, nullptr);
sentry_options_set_debug(options, 1);
sentry_init(options);
- Crash the app in MainWindow::myButton_Click
volatile char* invalid_memory = nullptr;
*invalid_memory = '\0'; // crash
- Build the app in Release/x64, ensure sentry DLLs and crashpad_handler.exe are copied
- Run the app without a debugger, and see the log from DebugView
- Click the "Click Me" button which calls the code that will crash
- App crashes and crashpad_handler did not catch it (based on what is shown in DebugView)
Log output

If I crash directly after sentry_init(options);
in App::OnLaunched the crash can be caught correctly

I’m not quite sure how WinUI 3 works under the hood, but the guide defaults to C#, which makes me think that maybe WinUI 3 is actually managed code, and there were a bunch of previous reports about crashes that involve C# not being correctly captured.
This may also be related to #611.
WinUI 3 isn't managed code. You can write C# but it'll be compiled to native. C# Exceptions could be captured with the Sentry SDK for .NET but the native part wouldn't be covered there.
My guess is that's the same UWP limitations of sandboxing that doesn't work with the crashpad handler out of process nor with the Win32 API to create minidumps
@bruno-garcia the UWP limitation does not explain why crashes are being handled when crashing directly after setup.
I rather suspect that the UI code installs its own exception filter, overriding the sentry one.
@levinli303 do you know if you can hook into another lifecycle method of winui3? I’m not familiar with its API/lifecycle.
Its possible you need to call sentry_reinstall_backend
at a later time, depending on the winui3 lifecycle.
https://github.com/getsentry/sentry-native/blob/7b168d8545ac8b0ed7ba767ea97e534657d27b81/include/sentry.h#L1082-L1091
@Swatinem I tried the code below which reinstall the backend the first time button is clicked, and crash the second time it gets clicked, Crashpad is still not catching the crash. so that is pretty weird, if WinUI replaced the exception filter, it should have been done pretty early, certainly not after a button is clicked. is there a way to know what the existing exception filter(s?) are
void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
{
static int a = 0;
if (a == 0)
{
sentry_reinstall_backend();
myButton().Content(box_value(L"Clicked"));
a += 1;
}
else
{
volatile char* invalid_memory = nullptr;
*invalid_memory = '\0'; // crash
}
}
According to the docs, you can only set it, and it returns the previously set handler:
https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setunhandledexceptionfilter
Also according to the docs, the exception could also have been caught by another handler, which is possibly what happens here. Not sure though.
Hi @levinli303, Were you able to make any progress or find out why this happens? I too seem to be facing something similar, but with Qt. Any response/hint would be highly appreciated!
Hi all, Same issue using Qt and QML UI. Any news about this?
I think this might be the same problem as described in https://github.com/getsentry/sentry-native/issues/706.
It might be that some graphics drivers are overriding the UnhandlerExceptionHandler. The linked issue has a possible workaround. Though that workaround is quite a big hammer and we would rather not bake that into the SDK itself.
I think this might be the same problem as described in #706.
It might be that some graphics drivers are overriding the UnhandlerExceptionHandler. The linked issue has a possible workaround. Though that workaround is quite a big hammer and we would rather not bake that into the SDK itself.
I made another attempt today, in a few different scenarios
every case happens in void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
an event handler for button click which should be run in the window's thread.
- crash directly in event handler (crash not caught)
void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
{
int* ptr = nullptr;
*ptr = 1;
}
- start a std::thread in event handler and crash on the thread (crash caught)
void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
{
std::thread th([]
{
int* ptr = nullptr;
*ptr = 1;
});
th.detach();
}
- crash in the window's DispatcherQueue().TryEnqueue (crash not caught)
void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
{
DispatcherQueue().TryEnqueue([]()
{
int* ptr = nullptr;
*ptr = 1;
});
}
- Crash in ThreadPool.RunAsync (crash caught)
void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
{
globalWorker = Windows::System::Threading::ThreadPool::RunAsync([](Windows::Foundation::IAsyncAction)
{
int* ptr = nullptr;
*ptr = 1;
}, Windows::System::Threading::WorkItemPriority::High, Windows::System::Threading::WorkItemOptions::TimeSliced);
}
I believe the unhandled exception handler in SetUnhandledExceptionFilter was not overridden by some driver or WinUI itself
Thanks, @levinli303, for the examples. We'll investigate those if we decide to make WinUI a target platform for sentry-native.
In the meantime, I recommend you check with a native debugger whether the UnhandledExceptionFilter is overwritten along any path in your program initialization. You can follow the description i posted for a mixed dotnet assembly.
Be also aware that in some cases (mostly C++ exceptions and not hard crashes like in your case) Windows runtimes abort()
the program with uncaught exceptions raised in threads, in which case crashpad's SIGABRT
handler would report the crash instead of the UEF. This could explain the behavior you're seeing, although I must admit that I cannot base this on any insights with WinUI specifically.
Short update: I tried reproducing your examples and also checked with WinDbg: it seems that using threads, the stack to unwind from the crash bypasses local WinUI handling (because it is only a thread doing the crash) and thus propagates to the global UEF
. For every crash happening in the WinUI thread (which includes the DispatcherQueue
on the UI thread), another "local" mechanism catches the exceptions.
So, yes, UEF
overwriting isn't the culprit here, but there still seems to be a WinUI-specific catch-all mechanism that prevents the UEF
from being triggered. A relatively globally positioned __try
/__except
in the UI code would do the trick, but I did not investigate.
The question is, then, whether a WinUI configuration allows the exception to propagate from the UI library's local handler to the global UEF.