Repository navigation
Closed
Labels
area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIruntime-async
Milestone
Description
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 theTransparentAwaitthe importer inserts after its ownMON_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).
Reacted by Copilot
Metadata
Metadata
Assignees
Labels
area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIruntime-async
Activity
dotnet-policy-service commentedon Sep 14, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.