JIT: (bug) Runtime-async version of a Synchronized method releases the monitor twice, replacing the real exception and corrupting the caller's lock
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
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);runtime-async=on` in the project file.
```csharp
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).
Contributor guide
Assessment
This issue has not been assessed yet.