Describe the bug
After upgrading an Avalonia application from .NET 10 to .NET 11, the process crashes during normal application shutdown on macOS.
The application window closes normally, but process teardown subsequently ends with SIGABRT. The failure occurs when the native static ComPtr<IAvnDispatcher> is destroyed and releases its managed implementation after CoreCLR has already detached the current thread from its new Epoch-Based Reclamation infrastructure.
This appears to be a shutdown-ordering issue in Avalonia Native. Its native platform cleanup is registered through AppDomain.ProcessExit, but the dispatcher callback remains reachable from native static storage late enough to be released after managed execution is no longer safe.
Crash stack
The relevant part of the macOS crash report is:
Exception Type: EXC_CRASH (SIGABRT)
Termination Reason: Namespace SIGNAL, Code 6, Abort trap: 6
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 libsystem_kernel.dylib __pthread_kill
1 libsystem_pthread.dylib pthread_kill
2 libsystem_c.dylib abort
3 libcoreclr.dylib PROCAbort
4 libcoreclr.dylib RaiseFailFastException
5 libcoreclr.dylib FailFastOnAssert()
6 libcoreclr.dylib __FreeBuildAssertFail
7 libcoreclr.dylib EbrCollector::EnterCriticalRegion()
8 libcoreclr.dylib HashMap::LookupValueByUniqueKey(...)
9 libcoreclr.dylib TypeIDMap::LookupType(...)
10 libcoreclr.dylib VSD_ResolveWorker
11 libcoreclr.dylib ResolveWorkerAsmStub
...
14 libAvaloniaNative.dylib ComPtr<IAvnDispatcher>::~ComPtr()
15 libsystem_c.dylib __cxa_finalize_ranges
16 libsystem_c.dylib exit
The crash occurs after exit() has begun, while running native static destructors.
Relevant .NET 11 change
.NET 11 changed CoreCLR's internal asynchronous HashMap protection from cooperative GC transitions to Epoch-Based Reclamation:
Virtual stub dispatch uses this hash map through the following path:
VSD_ResolveWorker
TypeIDMap::LookupType
HashMap::LookupValueByUniqueKey
EbrCollector::EnterCriticalRegion
When CoreCLR tears down a thread, the EBR TLS destructor detaches it and poisons its collector state with DetachedCollector. A subsequent managed virtual call on that thread reaches this all-build assertion:
_ASSERTE_ALL_BUILDS(
pData->m_pCollector != DetachedCollector &&
"Attempt to reattach detached thread.");
Avalonia Native's late release of IAvnDispatcher causes exactly that sequence.
Under .NET 10, the same late managed callback did not use EBR, so the unsafe shutdown ordering did not fail deterministically.
Avalonia cleanup path
AvaloniaNativePlatform currently registers cleanup through AppDomain.ProcessExit:
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
The handler disposes the native factory:
private void OnProcessExit(object? sender, EventArgs e)
{
AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
_factory.Dispose();
}
The native factory destructor clears the static dispatcher:
virtual ~AvaloniaNative() override
{
ReleaseAvnAppEvents();
_deallocator = nullptr;
_dispatcher = nullptr;
}
However, the crash demonstrates that the static _dispatcher can still be released from native finalization after CoreCLR has detached the thread.
To Reproduce
Using a standard classic desktop lifetime (file-based app):
#:property TargetFramework=net11.0-macos
#:property SupportedOSPlatformVersion=15.0
#:property ApplicationId=com.example.avalonia-crash-repro
#:property ApplicationTitle=Avalonia Crash Repro
#:package Avalonia.Desktop@12.1.1
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
namespace AvaloniaCrashRepro;
internal static class Program
{
[STAThread]
public static int Main(string[] args) =>
AppBuilder.Configure<App>()
.UsePlatformDetect()
.StartWithClassicDesktopLifetime(args);
}
internal sealed class App : Application
{
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new Window
{
Title = "Avalonia Crash Repro",
Width = 480,
Height = 240,
Content = new TextBlock
{
Text = "Close this window to reproduce the shutdown crash.",
Margin = new Thickness(24)
}
};
}
base.OnFrameworkInitializationCompleted();
}
}
- Build the application for
net11.0-macos.
- Start it normally.
- Close the main window or quit the application.
- The window closes.
- macOS reports that the application terminated unexpectedly.
- A crash report is generated with the stack above.
In my application, this reproduces on every normal shutdown.
Workaround and confirmation
As a diagnostic workaround, I invoked Avalonia Native's existing OnProcessExit cleanup immediately after StartWithClassicDesktopLifetime returned, while managed execution was still valid:
var exitCode = BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
// Invoke AvaloniaNativePlatform.OnProcessExit here.
return exitCode;
Before applying that workaround, every shutdown generated a new macOS .ips crash report.
After moving the same native cleanup before Main returns:
- The application exited normally.
- No new crash report was generated.
- The CoreCLR EBR assertion no longer occurred.
This suggests that the native resources need to be released during normal Avalonia application or dispatcher shutdown, rather than relying exclusively on AppDomain.ProcessExit or native static destruction.
Expected behavior
An Avalonia desktop application should exit cleanly under .NET 11 without crashing.
Avalonia version
12.1.1
OS
macOS
Additional context
Environment
Avalonia: 12.1.1
Target framework: net11.0-macos
.NET SDK: 11.0.100-preview.7.26381.103
.NET runtime: 11.0.0-preview.7.26381.103
OS: macOS 26.5.2
Architecture: Apple Silicon / arm64
Related issues
Describe the bug
After upgrading an Avalonia application from .NET 10 to .NET 11, the process crashes during normal application shutdown on macOS.
The application window closes normally, but process teardown subsequently ends with
SIGABRT. The failure occurs when the native staticComPtr<IAvnDispatcher>is destroyed and releases its managed implementation after CoreCLR has already detached the current thread from its new Epoch-Based Reclamation infrastructure.This appears to be a shutdown-ordering issue in Avalonia Native. Its native platform cleanup is registered through
AppDomain.ProcessExit, but the dispatcher callback remains reachable from native static storage late enough to be released after managed execution is no longer safe.Crash stack
The relevant part of the macOS crash report is:
The crash occurs after
exit()has begun, while running native static destructors.Relevant .NET 11 change
.NET 11 changed CoreCLR's internal asynchronous
HashMapprotection from cooperative GC transitions to Epoch-Based Reclamation:Virtual stub dispatch uses this hash map through the following path:
When CoreCLR tears down a thread, the EBR TLS destructor detaches it and poisons its collector state with
DetachedCollector. A subsequent managed virtual call on that thread reaches this all-build assertion:Avalonia Native's late release of
IAvnDispatchercauses exactly that sequence.Under .NET 10, the same late managed callback did not use EBR, so the unsafe shutdown ordering did not fail deterministically.
Avalonia cleanup path
AvaloniaNativePlatformcurrently registers cleanup throughAppDomain.ProcessExit:The handler disposes the native factory:
The native factory destructor clears the static dispatcher:
However, the crash demonstrates that the static
_dispatchercan still be released from native finalization after CoreCLR has detached the thread.To Reproduce
Using a standard classic desktop lifetime (file-based app):
net11.0-macos.In my application, this reproduces on every normal shutdown.
Workaround and confirmation
As a diagnostic workaround, I invoked Avalonia Native's existing
OnProcessExitcleanup immediately afterStartWithClassicDesktopLifetimereturned, while managed execution was still valid:Before applying that workaround, every shutdown generated a new macOS
.ipscrash report.After moving the same native cleanup before
Mainreturns:This suggests that the native resources need to be released during normal Avalonia application or dispatcher shutdown, rather than relying exclusively on
AppDomain.ProcessExitor native static destruction.Expected behavior
An Avalonia desktop application should exit cleanly under .NET 11 without crashing.
Avalonia version
12.1.1
OS
macOS
Additional context
Environment
Related issues
_dispatcherhas been cleared.Attempt to execute managed code after the .NET runtime thread state has been destroyedwhen a native method holds astatic std::string? dotnet/runtime#118741 — discussion of managed execution after runtime thread teardown.HashMap.