-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Description
I seem to have encountered the same exception as in #40463. Our application died and left this message in Windows Event Viewer:
Application: Our Fantastic Application.exe
CoreCLR Version: 7.0.823.31807
.NET Version: 7.0.8
Description: The process was terminated due to an unhandled exception.
Exception Info: System.NullReferenceException: Object reference not set to an instance of an object.
at System.Runtime.CompilerServices.AsyncVoidMethodBuilder.NotifySynchronizationContextOfCompletion()
--- End of stack trace from previous location ---
at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__128_1(Object state)
at System.Threading.QueueUserWorkItemCallback.Execute()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
Reproduction Steps
Sorry! It's tough to recreate an exception that happened in the wild when the exception stack traces are this scant.
Expected behavior
No exception should be thrown
Actual behavior
Exception is thrown
Regression?
No response
Known Workarounds
No response
Configuration
No response
Other information
At first I was confused about how this exception could possibly occur in the first place. The AsyncVoidMethodBuilder._synchronizationContext field is initialized once and all paths leading to its use are guarded with null reference checks.
Except that's not really true. AsyncVoidMethodBuilder is a struct, so anytime an instance of that struct receives a new value the _synchronizationContext field is assigned again which races with the null checks.
Here's a small demo of what I mean. Eventually this program will die from a NullReferenceException:
while (true)
{
var x = new Struct(42);
await Task.WhenAll(
Task.Run(() =>
{
x = default;
}),
Task.Run(() =>
{
GC.KeepAlive(x.LooksFineAtFirstGlance());
})
);
}
readonly struct Struct
{
readonly object? _o;
public Struct(object o)
{
_o = o;
}
public int LooksFineAtFirstGlance()
{
if (_o != null)
return _o.GetHashCode();
return -1;
}
}The fix for my toy demo is quite simple: change it to _o?.GetHashCode().
Similarly the fix for AsyncVoidMethodBuilder might be simple: change it to _synchronizationContext?.OperationCompleted().
But that raises the question: why would .NET be reassigning AsyncVoidMethodBuilder fields while calls to old instances are still in flight?