Skip to content

JIT: (bug) Runtime-async version of a Synchronized method releases the monitor twice, replacing the real exception and corrupting the caller's lock #133865

Description

@EgorBo

When a [MethodImpl(MethodImplOptions.Synchronized)] method returning an awaitable is awaited by a runtime-async caller and the awaitable is already faulted, the JIT-generated async version calls Monitor.SynchronizedMethodExit twice.

Minimal Repro

Requires <Features>$(Features);runtime-async=on</Features> in the project file.

using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

public class Program
{
    [MethodImpl(MethodImplOptions.Synchronized)]
    public Task Ret(Task t) => t;

    static async Task Caller(Program p, Task t)
    {
        await p.Ret(t);
    }

    public static int Main()
    {
        Program p = new Program();
        Task faulted = Task.FromException(new InvalidOperationException("boom"));
        try
        {
            Caller(p, faulted).GetAwaiter().GetResult();
        }
        catch (Exception e)
        {
            Console.WriteLine("caught: " + e.GetType().Name + ": " + e.Message);
        }

        // Second symptom: the extra release drops a lock the caller owns.
        Program p2 = new Program();
        Task faulted2 = Task.FromException(new InvalidOperationException("boom2"));
        lock (p2)
        {
            try
            {
                Caller(p2, faulted2).GetAwaiter().GetResult();
            }
            catch (Exception e)
            {
                Console.WriteLine("caught2: " + e.GetType().Name + ": " + e.Message);
            }
            Console.WriteLine("caller still holds its own lock: " + Monitor.IsEntered(p2));
        }
        return 100;
    }
}

Expected

caught: InvalidOperationException: boom
caught2: InvalidOperationException: boom2
caller still holds its own lock: True

(exit code 100 — this is what the same source compiled without runtime-async=on prints)

Actual

caught: SynchronizationLockException: The calling thread does not hold the lock.
caught2: InvalidOperationException: boom2
caller still holds its own lock: False
Unhandled exception. System.Threading.SynchronizationLockException: The calling thread does not hold the lock.
   at System.Threading.Lock.Exit()
   at System.Threading.Monitor.Exit(Object obj)
   at Program.Main()

Notes

  • Compiler::fgAddSyncMethodEnterExit (src/coreclr/jit/flowgraph.cpp) wraps the whole imported body — including the TransparentAwait the importer inserts after its own MON_EXIT — inside the fault region, so a synchronously faulted awaitable runs the fault handler and exits the monitor a second time.
  • Symptom 1: the real exception is replaced by SynchronizationLockException.
  • Symptom 2 (silent lock corruption): when the caller holds the same monitor recursively, the extra release drops the caller's acquisition, so another thread can enter the still-"locked" critical section.
  • Also reproduces on the shipped 11.0.0-rc.1 Release runtime; N/A for .NET 10 (runtime-async is main-only).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIruntime-async

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions