Skip to content

Crash due to non-thread-safe access to Exception stack frames when using async/await #70081

@jahmai-ca

Description

@jahmai-ca

Description

When an exception is re-thrown from an await from multiple observers (think a TaskCompletionSource where there are more than one caller awaiting on the result), an ArgumentException can be thrown from Exception.CaptureDispatchState due to the member field this.foreignExceptionsFrames being changed by Exception.RestoreDispatchState during the re-throw of the exception from awaiting the TaskCompletionSource.

I believe the issue is that Exception.CaptureDispatchState is accessing this.foreignExceptionsFrames in a non-thread-safe way, and due to the this.foreignExceptionsFrames.Length property of the array changing during execution of the method, insufficient array space is allocated to the variable monoStackFrameArray then Array.Copy fails due to a change in this.foreignExceptionsFrames.Length.

This issue happens on Android (net6.0-android), but may also happen on iOS (net6.0-ios) if it shares the same implementation (untested).
This issue does not happen on Windows as it appears that Exception.CaptureDispatchState is handled by some extern call.
This issue does not happen on Xamarin.iOS (xamarinios) or Xamarin.Android (monoandroid), in either Xamarin.Native or Xamarin.Forms apps.

FWIW this appears to be a fairly serious bug and will block me from switching our Xam apps to MAUI.

Steps to Reproduce

  1. Create a new Android MAUI Hello World! app
  2. Change the body of OnCounterClicked to the following:
    private void OnCounterClicked(object sender, EventArgs e)
    {
        count++;
        CounterLabel.Text = $"Current count: {count}";

        SemanticScreenReader.Announce(CounterLabel.Text);

        var theException = new Exception();

        var tasks = new List<Task>();
        var resetEvent = new ManualResetEventSlim();

        for (var awaits = 0; awaits < 10; ++awaits)
        {
            tasks.Add(Task.Run(async () =>
            {
                resetEvent.Wait();
                
                var tcs = new TaskCompletionSource();
                
                try
                {
                    // One thread calls SetException on the TCS which calls CaptureDispatchState on the Exception which accesses foreignExceptionsFrames in a non-atomic way
                    tcs.SetException(theException);
                }
                catch (ArgumentException ex)
                {
                    // THIS SHOULD NOT HAPPEN
                    Log.Wtf("TaskAwaitCrash", ex.ToString());
                }

                // Another thread re-throws the Exception which calls RestoreDispatchState which sets foreignExceptionsFrames to a new array of a different length
                await tcs.Task;
            }));
        }

        resetEvent.Set();

        try
        {
            Task.WhenAll(tasks).Wait();
        }
        catch
        {
        }
    }
  1. Run the app and click the Click me button repeatedly until the WTF error is seen in the logcat output.

Version with bug

Release Candidate 3 (current)

Last version that worked well

Unknown/Other

Affected platforms

Android

Affected platform versions

Android 12

Did you find any workaround?

No workaround :(

Relevant log output

05-18 12:19:19.680 E/TaskAwaitCrash(19152): System.ArgumentException: Destination array was not long enough. Check destIndex and length, and the array's lower bounds (Parameter 'destinationArray')
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at System.Array.Copy(Array sourceArray, Int32 sourceIndex, Array destinationArray, Int32 destinationIndex, Int32 length, Boolean reliable)
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at System.Array.Copy(Array sourceArray, Int32 sourceIndex, Array destinationArray, Int32 destinationIndex, Int32 length)
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at System.Exception.CaptureDispatchState()
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at System.Runtime.ExceptionServices.ExceptionDispatchInfo..ctor(Exception exception)
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(Exception source)
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at System.Threading.Tasks.TaskExceptionHolder.AddFaultException(Object exceptionObject)
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at System.Threading.Tasks.TaskExceptionHolder.Add(Object exceptionObject, Boolean representsCancellation)
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at System.Threading.Tasks.Task.AddException(Object exceptionObject, Boolean representsCancellation)
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at System.Threading.Tasks.Task.AddException(Object exceptionObject)
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at System.Threading.Tasks.Task.TrySetException(Object exceptionObject)
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at System.Threading.Tasks.TaskCompletionSource.TrySetException(Exception exception)
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at System.Threading.Tasks.TaskCompletionSource.SetException(Exception exception)
05-18 12:19:19.680 E/TaskAwaitCrash(19152):    at TaskAwaitCrash.MainPage.<>c__DisplayClass2_0.<<OnCounterClicked>b__0>d.MoveNext() in C:\Users\JahmaiLay\source\repos\TaskAwaitCrash\MainPage.xaml.cs:line 40

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions