Skip to content

Object reference not set to an instance of an object exception in NotifySynchronizationContextOfCompletion() #93969

@matthew-a-thomas

Description

@matthew-a-thomas

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?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions