From a8231273c3d9aa8f72513d6ef5fa8f8af6427753 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 27 Aug 2025 16:26:26 +0200 Subject: [PATCH 1/4] JIT: Disallow widening tailcalls in async functions The JIT allows tailcalls that depend on widening happening in the callee: e.g. an int32 returning function can tailcall an int16 returning function because the managed ABI specifies that the callee will widen anyway. However, this is not legal for async methods. For async methods the int32 and int16 returning functions come with two different resumption stubs that are responsible for propagating the returned result into the next continuation in asynchronous cases. Allowing an int32 -> int16 tailcall means only a 16-bit value gets propagated into a caller that expects 32 bits to be written into its continuation. Fix #119060 --- src/coreclr/jit/importercalls.cpp | 7 +++-- .../sign-changing-tailcall.cs | 29 +++++++++++++++++++ .../sign-changing-tailcall.csproj | 8 +++++ 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/tests/async/sign-changing-tailcall/sign-changing-tailcall.cs create mode 100644 src/tests/async/sign-changing-tailcall/sign-changing-tailcall.csproj diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 4badae442d9051..7db020168b0abb 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -1098,9 +1098,10 @@ var_types Compiler::impImportCall(OPCODE opcode, } // For opportunistic tailcalls we allow implicit widening, i.e. tailcalls from int32 -> int16, since the - // managed calling convention dictates that the callee widens the value. For explicit tailcalls we don't - // want to require this detail of the calling convention to bubble up to the tailcall helpers - bool allowWidening = isImplicitTailCall; + // managed calling convention dictates that the callee widens the value. For explicit tailcalls or async + // functions we don't want to require this detail of the calling convention to bubble up to helper + // infrastructure. + bool allowWidening = isImplicitTailCall && !call->AsCall()->IsAsync(); if (canTailCall && !impTailCallRetTypeCompatible(allowWidening, info.compRetType, info.compMethodInfo->args.retTypeClass, info.compCallConv, callRetTyp, sig->retTypeClass, diff --git a/src/tests/async/sign-changing-tailcall/sign-changing-tailcall.cs b/src/tests/async/sign-changing-tailcall/sign-changing-tailcall.cs new file mode 100644 index 00000000000000..d125fcd91ba9db --- /dev/null +++ b/src/tests/async/sign-changing-tailcall/sign-changing-tailcall.cs @@ -0,0 +1,29 @@ +// 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.Tasks; +using Xunit; + +public class Async2Small +{ + [Fact] + public static void TestEntryPoint() + { + uint vr0 = (uint)M29().GetAwaiter().GetResult(); + Assert.Equal(uint.MaxValue, vr0); + } + + private static async Task M29() + { + return await M40(); + } + + private static sbyte s_38 = -1; + private static async Task M40() + { + await Task.Yield(); + return s_38; + } +} diff --git a/src/tests/async/sign-changing-tailcall/sign-changing-tailcall.csproj b/src/tests/async/sign-changing-tailcall/sign-changing-tailcall.csproj new file mode 100644 index 00000000000000..de6d5e08882e86 --- /dev/null +++ b/src/tests/async/sign-changing-tailcall/sign-changing-tailcall.csproj @@ -0,0 +1,8 @@ + + + True + + + + + From bccb0181b39adeaf1f1af770fa981b3e70303332 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 27 Aug 2025 16:48:35 +0200 Subject: [PATCH 2/4] Rename --- .../widening-tailcall.cs} | 0 .../widening-tailcall.csproj} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/tests/async/{sign-changing-tailcall/sign-changing-tailcall.cs => widening-tailcall/widening-tailcall.cs} (100%) rename src/tests/async/{sign-changing-tailcall/sign-changing-tailcall.csproj => widening-tailcall/widening-tailcall.csproj} (100%) diff --git a/src/tests/async/sign-changing-tailcall/sign-changing-tailcall.cs b/src/tests/async/widening-tailcall/widening-tailcall.cs similarity index 100% rename from src/tests/async/sign-changing-tailcall/sign-changing-tailcall.cs rename to src/tests/async/widening-tailcall/widening-tailcall.cs diff --git a/src/tests/async/sign-changing-tailcall/sign-changing-tailcall.csproj b/src/tests/async/widening-tailcall/widening-tailcall.csproj similarity index 100% rename from src/tests/async/sign-changing-tailcall/sign-changing-tailcall.csproj rename to src/tests/async/widening-tailcall/widening-tailcall.csproj From 3f1088c77d12dd3a35b76a35257f7d3793a94f3c Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 28 Aug 2025 14:08:00 +0200 Subject: [PATCH 3/4] Fix test class name --- src/tests/async/widening-tailcall/widening-tailcall.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/async/widening-tailcall/widening-tailcall.cs b/src/tests/async/widening-tailcall/widening-tailcall.cs index d125fcd91ba9db..1d3cfafc500587 100644 --- a/src/tests/async/widening-tailcall/widening-tailcall.cs +++ b/src/tests/async/widening-tailcall/widening-tailcall.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Xunit; -public class Async2Small +public class Async2WideningTailcall { [Fact] public static void TestEntryPoint() From 8cd934b7b3b20f7a9edc6f02dea019307e211617 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 28 Aug 2025 14:09:56 +0200 Subject: [PATCH 4/4] Comment out some asserts for now --- .../synchronization-context/synchronization-context.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/async/synchronization-context/synchronization-context.cs b/src/tests/async/synchronization-context/synchronization-context.cs index d1b053cd7ebf0a..3e3a3cd70d38d6 100644 --- a/src/tests/async/synchronization-context/synchronization-context.cs +++ b/src/tests/async/synchronization-context/synchronization-context.cs @@ -45,9 +45,9 @@ private static async Task TestSyncContextContinueAsync() // we check IsCompleted on the awaiter). //await WrappedYieldToThreadPool(suspend: true).ConfigureAwait(false); //Assert.Null(SynchronizationContext.Current); - - await WrappedYieldToThreadWithCustomSyncContext(); - Assert.Null(SynchronizationContext.Current); + // + //await WrappedYieldToThreadWithCustomSyncContext(); + //Assert.Null(SynchronizationContext.Current); } private static async Task WrappedYieldToThreadPool(bool suspend)