From 1c5273cd54981c7fc93425819e065dcc34186487 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:37:02 +0200 Subject: [PATCH] JIT: Fix double monitor release in synchronized runtime-async methods (#133866) Awaiting a faulted awaitable returned by a synchronized method can release the monitor twice, replacing the original exception or releasing a caller-owned recursive acquisition. - **JIT:** Clear `lvaMonAcquired` immediately after the importer-generated monitor exit, preventing a subsequent await exception from triggering another release. The released-state value is `0`; `1` means acquired. - **Regression coverage:** Extend the existing `Task` and `ValueTask` tests to verify exception identity and monitor ownership, with and without a caller-held lock. Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jakobbotsch <7887810+jakobbotsch@users.noreply.github.com> --- src/coreclr/jit/importer.cpp | 3 ++ .../regression/synchronized-async-version.cs | 43 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 4106ed3ece2ac0..feb456ffbdb241 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -11927,6 +11927,9 @@ bool Compiler::impWrapTopOfStackInAwait() info.compIsStatic ? fgGetCritSectOfStaticMethod() : gtNewLclvNode(info.compThisArg, TYP_REF); GenTree* exitMon = gtNewHelperCallNode(CORINFO_HELP_MON_EXIT, TYP_VOID, lockObject, varAddrNode); impAppendTree(exitMon, CHECK_SPILL_ALL, impCurStmtDI); + + // The fault handler must not release the monitor again if the await throws. + impStoreToTemp(lvaMonAcquired, gtNewZeroConNode(TYP_I_IMPL), CHECK_SPILL_ALL); } if (impFoldAwaitedTopOfStack()) diff --git a/src/tests/async/regression/synchronized-async-version.cs b/src/tests/async/regression/synchronized-async-version.cs index b032dd17eaef89..a7f239c994c741 100644 --- a/src/tests/async/regression/synchronized-async-version.cs +++ b/src/tests/async/regression/synchronized-async-version.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -8,6 +9,48 @@ public class Async2Synchronized { + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public static void FaultedAwaitable(bool useValueTask, bool callerHoldsLock) + { + Async2Synchronized p = new(); + InvalidOperationException expected = new("boom"); + Task task = Task.FromException(expected); + + if (callerHoldsLock) + { + Monitor.Enter(p); + } + + try + { + InvalidOperationException actual = Assert.Throws(() => + { + if (useValueTask) + { + p.FooValueTask(new ValueTask(task)).GetAwaiter().GetResult(); + } + else + { + p.Foo(task).GetAwaiter().GetResult(); + } + }); + + Assert.Same(expected, actual); + Assert.Equal(callerHoldsLock, Monitor.IsEntered(p)); + } + finally + { + if (Monitor.IsEntered(p)) + { + Monitor.Exit(p); + } + } + } + [Fact] public static void TestEntryPoint() {