diff --git a/eng/testing/tests.targets b/eng/testing/tests.targets
index 76637130d65a3f..36d10135593d48 100644
--- a/eng/testing/tests.targets
+++ b/eng/testing/tests.targets
@@ -12,7 +12,6 @@
   <PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net11.0'))
     and '$(RuntimeAsyncSupported)' == 'true'
     and '$(UseRuntimeAsync)' != 'false'">
-    <Features>$(Features);runtime-async=on</Features>
   </PropertyGroup>
   <PropertyGroup>
     <RunScriptWindowsCmd Condition="'$(OS)' == 'Windows_NT' and '$(RunScriptWindowsCmd)' == ''">true</RunScriptWindowsCmd>
diff --git a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj
index 9e02584f4c34f1..9d2ac8dc23fbc5 100644
--- a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj
+++ b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj
@@ -44,7 +44,6 @@
     <!-- https://github.com/dotnet/dotnet/issues/5640 -->
     <NoWarn>$(NoWarn);CS9368</NoWarn>
     <Nullable>enable</Nullable>
-    <Features>$(Features);runtime-async=on</Features>
 
     <!-- Ignore all previous constants since SPCL is sensitive to what is defined and the Sdk adds some by default -->
     <DefineConstants>CORECLR;NETCOREAPP;SYSTEM_PRIVATE_CORELIB</DefineConstants>
diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs
index efe5f8e9969326..fbf1bf86e20b9f 100644
--- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs
+++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs
@@ -664,6 +664,61 @@ private static unsafe T TransparentAwaitOfT<T>(Task<T> task)
             return default!;
         }
 
+        [BypassReadyToRun]
+        [MethodImpl(MethodImplOptions.Async)]
+        private static void TransparentAwaitWithResult(Task task)
+        {
+            if (!task.IsCompleted)
+            {
+                TailAwait();
+                TransparentAwait(task);
+                return;
+            }
+
+            TaskAwaiter.ValidateEnd(task);
+        }
+
+        [BypassReadyToRun]
+        [MethodImpl(MethodImplOptions.Async)]
+        private static void TransparentAwaitWithResult(ValueTask task)
+        {
+            if (!task.IsCompleted)
+            {
+                TailAwait();
+                TransparentAwaitValueTask(task);
+                return;
+            }
+
+            task.ThrowIfCompletedUnsuccessfully();
+        }
+
+        [BypassReadyToRun]
+        [MethodImpl(MethodImplOptions.Async)]
+        private static T TransparentAwaitWithResult<T>(Task<T> task)
+        {
+            if (!task.IsCompleted)
+            {
+                TailAwait();
+                return TransparentAwaitOfT(task);
+            }
+
+            TaskAwaiter.ValidateEnd(task);
+            return task.ResultOnSuccess;
+        }
+
+        [BypassReadyToRun]
+        [MethodImpl(MethodImplOptions.Async)]
+        private static T TransparentAwaitWithResult<T>(ValueTask<T> task)
+        {
+            if (!task.IsCompleted)
+            {
+                TailAwait();
+                return TransparentAwaitValueTaskOfT(task);
+            }
+
+            return task.Result;
+        }
+
         // Represents execution of a chain of suspended and resuming runtime
         // async functions.
         private sealed class RuntimeAsyncTask<T> : Task<T>
diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h
index 93c37d0a7e5c3a..fe3391cbee838c 100644
--- a/src/coreclr/inc/corinfo.h
+++ b/src/coreclr/inc/corinfo.h
@@ -716,6 +716,7 @@ enum CorInfoOptions
                                                CORINFO_GENERICS_CTXT_FROM_METHODTABLE),
     CORINFO_GENERICS_CTXT_KEEP_ALIVE        = 0x00000100, // Keep the generics context alive throughout the method even if there is no explicit use, and report its location to the CLR
     CORINFO_ASYNC_SAVE_CONTEXTS             = 0x00000200, // Runtime async method must save and restore contexts
+    CORINFO_ASYNC_VERSION                   = 0x00000400, // This is an async version whose IL belongs to a non-async method
 };
 
 //
@@ -3140,6 +3141,10 @@ class ICorStaticInfo
         CORINFO_ASYNC_INFO* pAsyncInfoOut
     ) = 0;
 
+    // Get information about which await call to use to await the return type
+    // of the non-async version of an async call.
+    virtual CORINFO_METHOD_HANDLE getAwaitReturnCall(CORINFO_METHOD_HANDLE callerHandle, CORINFO_LOOKUP* instArg) = 0;
+
     /*********************************************************************************/
     //
     // Diagnostic methods
diff --git a/src/coreclr/inc/icorjitinfoimpl_generated.h b/src/coreclr/inc/icorjitinfoimpl_generated.h
index 8fc80cda20018c..3c5a48ca5c3b3e 100644
--- a/src/coreclr/inc/icorjitinfoimpl_generated.h
+++ b/src/coreclr/inc/icorjitinfoimpl_generated.h
@@ -507,6 +507,10 @@ void getEEInfo(
 void getAsyncInfo(
           CORINFO_ASYNC_INFO* pAsyncInfoOut) override;
 
+CORINFO_METHOD_HANDLE getAwaitReturnCall(
+          CORINFO_METHOD_HANDLE callerHandle,
+          CORINFO_LOOKUP* instArg) override;
+
 mdMethodDef getMethodDefFromMethod(
           CORINFO_METHOD_HANDLE hMethod) override;
 
diff --git a/src/coreclr/inc/jiteeversionguid.h b/src/coreclr/inc/jiteeversionguid.h
index 74b2c30bccd41f..12347eb227a951 100644
--- a/src/coreclr/inc/jiteeversionguid.h
+++ b/src/coreclr/inc/jiteeversionguid.h
@@ -37,11 +37,11 @@
 
 #include <minipal/guid.h>
 
-constexpr GUID JITEEVersionIdentifier = { /* 2fa4c0dd-1b2b-4c3f-8a88-cc7b79c4667f */
-    0x2fa4c0dd,
-    0x1b2b,
-    0x4c3f,
-    {0x8a, 0x88, 0xcc, 0x7b, 0x79, 0xc4, 0x66, 0x7f}
+constexpr GUID JITEEVersionIdentifier = { /* dcace1ba-8b1e-45ad-890f-055c4e190ddb */
+    0xdcace1ba,
+    0x8b1e,
+    0x45ad,
+    {0x89, 0x0f, 0x05, 0x5c, 0x4e, 0x19, 0x0d, 0xdb}
   };
 
 #endif // JIT_EE_VERSIONING_GUID_H
diff --git a/src/coreclr/interpreter/compiler.cpp b/src/coreclr/interpreter/compiler.cpp
index a88ef5af2e4471..f033059c57c62a 100644
--- a/src/coreclr/interpreter/compiler.cpp
+++ b/src/coreclr/interpreter/compiler.cpp
@@ -2274,6 +2274,13 @@ bool InterpCompiler::CompileMethod()
     {
         INTERP_DUMP("Method is async with context save/restore\n");
     }
+
+    m_isAsyncVersionOfSyncMethod = m_methodInfo->options & CORINFO_ASYNC_VERSION;
+    if (m_isAsyncVersionOfSyncMethod)
+    {
+        INTERP_DUMP("Method is an async version of a synchronous method; IL belongs to synchronous method\n");
+    }
+
     CreateILVars();
 
     GenerateCode(m_methodInfo);
@@ -4628,7 +4635,13 @@ static OpcodePeepElement peepRuntimeAsyncCallConfigureAwaitValueTask_EXACT_L[] =
     // LDC_I4_0 or LDC_I4_1 goes here (at offset 10)
     { 11, CEE_CALL},
     { 16, CEE_CALL },
-    { 19, CEE_ILLEGAL } // End marker
+    { 21, CEE_ILLEGAL } // End marker
+};
+
+static OpcodePeepElement peepRuntimeAsyncCallRetInAsyncVersion[] = {
+    // call or callvirt at the start
+    { 5, CEE_RET },
+    { 6, CEE_ILLEGAL } // End marker
 };
 
 class InterpAsyncCallPeeps
@@ -4641,9 +4654,10 @@ class InterpAsyncCallPeeps
     OpcodePeep peepCallConfigureAwaitValueTask_S_S = { peepRuntimeAsyncCallConfigureAwaitValueTask_S_S, &InterpCompiler::IsRuntimeAsyncCallConfigureAwaitValueTask, &InterpCompiler::ApplyRuntimeAsyncCall, "CallConfigureAwaitValueTask_S_S" };
     OpcodePeep peepCallConfigureAwaitValueTask_EXACT_S = { peepRuntimeAsyncCallConfigureAwaitValueTask_EXACT_S, &InterpCompiler::IsRuntimeAsyncCallConfigureAwaitValueTaskExactStLoc, &InterpCompiler::ApplyRuntimeAsyncCall, "CallConfigureAwaitValueTask_EXACT_S" };
     OpcodePeep peepCallConfigureAwaitValueTask_EXACT_L = { peepRuntimeAsyncCallConfigureAwaitValueTask_EXACT_L, &InterpCompiler::IsRuntimeAsyncCallConfigureAwaitValueTaskExactStLoc, &InterpCompiler::ApplyRuntimeAsyncCall, "CallConfigureAwaitValueTask_EXACT_L" };
+    OpcodePeep peepCallRetInAsyncVersion = { peepRuntimeAsyncCallRetInAsyncVersion, &InterpCompiler::IsRuntimeAsyncCallRetInAsyncVersion, &InterpCompiler::ApplyRuntimeAsyncCallRetInAsyncVersion, "CallRetInAsyncVersion" };
 
 public:
-    OpcodePeep* Peeps[9] = {
+    OpcodePeep* Peeps[10] = {
         &peepCall,
         &peepCallConfigureAwaitTask,
         &peepCallConfigureAwaitValueTask_L_L,
@@ -4652,6 +4666,7 @@ class InterpAsyncCallPeeps
         &peepCallConfigureAwaitValueTask_S_S,
         &peepCallConfigureAwaitValueTask_EXACT_S,
         &peepCallConfigureAwaitValueTask_EXACT_L,
+        &peepCallRetInAsyncVersion,
         NULL };
 
 
@@ -4937,7 +4952,9 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re
     }
     else
     {
-        const uint8_t* origIP = m_ip;
+        // We expect this bool to be reset when we handle the RET instruction after it gets set.
+        assert(!m_matchedAsyncCallRetInAsyncVersion);
+
         if (!newObj && m_methodInfo->args.isAsyncCall() && AsyncCallPeeps.FindAndApplyPeep(this))
         {
             resolvedCallToken = m_resolvedAsyncCallToken;
@@ -4968,26 +4985,6 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re
             BADCODE("We're trying to emit an async call, but the async resolved context didn't find one");
         }
 
-        if (continuationContextHandling != ContinuationContextHandling::None && callInfo.kind == CORINFO_CALL)
-        {
-            bool isSyncCallThunk;
-            m_compHnd->getAsyncOtherVariant(callInfo.hMethod, &isSyncCallThunk);
-            if (!isSyncCallThunk)
-            {
-                // The async variant that we got is a thunk. Switch back to the
-                // non-async task-returning call. There is no reason to create
-                // and go through the thunk.
-                ResolveToken(token, CORINFO_TOKENKIND_Method, &resolvedCallToken);
-
-                // Reset back to the IP after the original call instruction.
-                // FindAndApplyPeep above modified it to be after the "Await"
-                // call, but now we want to process the await separately.
-                m_ip = origIP + 5;
-                continuationContextHandling = ContinuationContextHandling::None;
-                m_compHnd->getCallInfo(&resolvedCallToken, pConstrainedToken, m_methodInfo->ftn, flags, &callInfo);
-            }
-        }
-
         if (callInfo.sig.isVarArg())
         {
             BADCODE("Vararg methods are not supported in interpreted code");
@@ -5356,7 +5353,7 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re
 
     if (continuationArgLocation != INT_MAX)
     {
-        PushStackType(StackTypeI, NULL);
+        PushStackType(StackTypeO, NULL);
         m_pStackPointer--;
         int32_t continuationArg = m_pStackPointer[0].var;
 
@@ -5720,7 +5717,7 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re
 
     if (callInfo.sig.isAsyncCall() && m_methodInfo->args.isAsyncCall()) // Async2 functions may need to suspend
     {
-        EmitSuspend(callInfo, continuationContextHandling);
+        EmitSuspend(callInfo.sig.retType, continuationContextHandling);
     }
 }
 
@@ -5729,6 +5726,30 @@ void InterpCompiler::EmitRet(CORINFO_METHOD_INFO* methodInfo)
     CORINFO_SIG_INFO sig = methodInfo->args;
     InterpType retType = GetInterpType(sig.retType);
 
+    if (m_isAsyncVersionOfSyncMethod)
+    {
+        if (m_matchedAsyncCallRetInAsyncVersion)
+        {
+            m_matchedAsyncCallRetInAsyncVersion = false;
+
+            if (retType == InterpTypeVoid && m_pStackPointer > m_pStackBase)
+            {
+                INTERP_DUMP("Popping stack for Task<T> in void-returning async version\n");
+                // For covariant cases like a tailcall to a Task<int>-returning
+                // callee from a Task-returning function we will have a
+                // superfluous value on the stack after the call.
+                m_pStackPointer--;
+            }
+        }
+        else
+        {
+            CheckStackExact(1);
+
+            INTERP_DUMP("Wrapping top of stack in await\n");
+            WrapTopOfStackInAwait();
+        }
+    }
+
     if ((retType != InterpTypeVoid) && (retType != InterpTypeByRef))
     {
         CheckStackExact(1);
@@ -5835,7 +5856,98 @@ void InterpCompiler::EmitRet(CORINFO_METHOD_INFO* methodInfo)
     }
 }
 
-void SetSlotToTrue(TArray<bool, MemPoolAllocator> &gcRefMap, int32_t slotOffset)
+void InterpCompiler::WrapTopOfStackInAwait()
+{
+    CORINFO_LOOKUP instArgLookup;
+    CORINFO_METHOD_HANDLE awaitMethod = m_compHnd->getAwaitReturnCall(m_methodHnd, &instArgLookup);
+
+    CORINFO_SIG_INFO awaitSig;
+    m_compHnd->getMethodSig(awaitMethod, &awaitSig);
+
+    assert(awaitSig.isAsyncCall());
+    assert(awaitSig.numArgs == 1);
+    assert(!awaitSig.hasThis());
+
+    // Pop the awaitable off the stack; it becomes the last argument.
+    int32_t awaitableVar = m_pStackPointer[-1].var;
+    m_pStackPointer--;
+
+    int extraParamArgLocation = awaitSig.hasTypeArg() ? 0 : INT_MAX;
+    int continuationArgLocation = awaitSig.hasTypeArg() ? 1 : 0;
+    int numArgs = 1 + (awaitSig.hasTypeArg() ? 1 : 0) + 1; // task + (optional inst) + continuation
+
+    int32_t* callArgs = getAllocator(IMK_CallInfo).allocate<int32_t>(numArgs + 1);
+
+    if (awaitSig.hasTypeArg())
+    {
+        int32_t instParamVar;
+        if (instArgLookup.lookupKind.needsRuntimeLookup)
+        {
+            EmitPushCORINFO_LOOKUP(instArgLookup);
+            m_pStackPointer--;
+            instParamVar = m_pStackPointer[0].var;
+        }
+        else
+        {
+            assert(instArgLookup.constLookup.accessType == IAT_VALUE);
+            PushStackType(StackTypeI, NULL);
+            m_pStackPointer--;
+            instParamVar = m_pStackPointer[0].var;
+            AddIns(INTOP_LDPTR);
+            m_pLastNewIns->SetDVar(instParamVar);
+            m_pLastNewIns->data[0] = GetDataItemIndex(instArgLookup.constLookup.handle);
+        }
+        callArgs[extraParamArgLocation] = instParamVar;
+    }
+
+    // Continuation arg: pass null; the suspend logic emitted below sets it up.
+    PushStackType(StackTypeO, NULL);
+    m_pStackPointer--;
+    int32_t continuationArg = m_pStackPointer[0].var;
+    AddIns(INTOP_LDNULL);
+    m_pLastNewIns->SetDVar(continuationArg);
+    callArgs[continuationArgLocation] = continuationArg;
+
+    callArgs[numArgs - 1] = awaitableVar;
+    callArgs[numArgs] = CALL_ARGS_TERMINATOR;
+
+    // Result var. Must be on top of the stack so EmitSuspend treats it as the
+    // return value of the call (and not as an unrelated live stack var).
+    int32_t dVar;
+    if (awaitSig.retType != CORINFO_TYPE_VOID)
+    {
+        InterpType interpType = GetInterpType(awaitSig.retType);
+        if (interpType == InterpTypeVT)
+        {
+            int32_t size = m_compHnd->getClassSize(awaitSig.retTypeClass);
+            PushTypeVT(awaitSig.retTypeClass, size);
+        }
+        else
+        {
+            PushInterpType(interpType, NULL);
+        }
+        dVar = m_pStackPointer[-1].var;
+    }
+    else
+    {
+        // Create a new dummy var to serve as the dVar of the call.
+        PushStackType(StackTypeI4, NULL);
+        m_pStackPointer--;
+        dVar = m_pStackPointer[0].var;
+    }
+
+    AddIns(INTOP_CALL);
+    m_pLastNewIns->data[0] = GetMethodDataItemIndex(awaitMethod);
+    m_pLastNewIns->SetDVar(dVar);
+    m_pLastNewIns->SetSVar(CALL_ARGS_SVAR);
+    m_pLastNewIns->flags |= INTERP_INST_FLAG_CALL;
+    m_pLastNewIns->info.pCallInfo = new (getAllocator(IMK_CallInfo)) InterpCallInfo();
+    m_pLastNewIns->info.pCallInfo->pCallArgs = callArgs;
+
+    EmitSuspend(awaitSig.retType, ContinuationContextHandling::None);
+}
+
+static void SetSlotToTrue(TArray<bool, MemPoolAllocator> &gcRefMap, int32_t slotOffset)
 {
     assert(slotOffset % sizeof(void*) == 0);
     int32_t slotIndex = slotOffset / sizeof(void*);
@@ -5849,7 +5961,7 @@ void SetSlotToTrue(TArray<bool, MemPoolAllocator> &gcRefMap, int32_t slotOffset)
     gcRefMap.Set(slotIndex, true);
 }
 
-void InterpCompiler::EmitSuspend(const CORINFO_CALL_INFO &callInfo, ContinuationContextHandling continuationContextHandling)
+void InterpCompiler::EmitSuspend(CorInfoType callRetType, ContinuationContextHandling continuationContextHandling)
 {
     if (m_nextAwaitIsTail)
     {
@@ -5899,7 +6011,7 @@ void InterpCompiler::EmitSuspend(const CORINFO_CALL_INFO &callInfo, Continuation
     // Step 2: Handle live stack vars (excluding return value)
     int32_t stackDepth = (int32_t)(m_pStackPointer - m_pStackBase);
     int32_t returnValueVar = -1;
-    if (stackDepth > 0 && callInfo.sig.retType != CORINFO_TYPE_VOID)
+    if (stackDepth > 0 && callRetType != CORINFO_TYPE_VOID)
     {
         // The return value var is written explicitly during resume, it is not included in the set of liveVars
         returnValueVar = m_pStackPointer[-1].var;
@@ -6319,7 +6431,7 @@ void InterpCompiler::EmitSuspend(const CORINFO_CALL_INFO &callInfo, Continuation
     // Once we've resumed, if the return type is a primitive integral type which
     // isn't an I4/U4 but is represented as such on the evaluation stack, sign/zero
     // extend it to the proper size.
-    switch (callInfo.sig.retType)
+    switch (callRetType)
     {
         case CORINFO_TYPE_UBYTE:
         case CORINFO_TYPE_BOOL:
@@ -7441,6 +7553,29 @@ bool InterpCompiler::IsRuntimeAsyncCallConfigureAwaitValueTask(const uint8_t* ip
     return ResolveAsyncCallToken(ip);
 }
 
+bool InterpCompiler::IsRuntimeAsyncCallRetInAsyncVersion(const uint8_t* ip, OpcodePeepElement* pattern, void** ppComputedInfo)
+{
+    if (!m_isAsyncVersionOfSyncMethod)
+    {
+        return false;
+    }
+
+    if (!ResolveAsyncCallToken(ip))
+    {
+        return false;
+    }
+
+    m_currentContinuationContextHandling = ContinuationContextHandling::None;
+    m_matchedAsyncCallRetInAsyncVersion = true;
+    return true;
+}
+
+int InterpCompiler::ApplyRuntimeAsyncCallRetInAsyncVersion(const uint8_t* ip, OpcodePeepElement* peep, void* computedInfo)
+{
+    // Only skip the call, not the RET we matched.
+    return 5;
+}
+
 int InterpCompiler::ApplyConvRUnR4Peep(const uint8_t* ip, OpcodePeepElement* peep, void* computedInfo)
 {
     // Replace with CONV_R4_UN
diff --git a/src/coreclr/interpreter/compiler.h b/src/coreclr/interpreter/compiler.h
index cdcd1aa2b847c2..94f0c5ecd486f3 100644
--- a/src/coreclr/interpreter/compiler.h
+++ b/src/coreclr/interpreter/compiler.h
@@ -700,6 +700,7 @@ class InterpCompiler
     // directly returns the continuation of the call instead of creating a new
     // suspension point.
     bool m_nextAwaitIsTail = false;
+    bool m_matchedAsyncCallRetInAsyncVersion = false;
 
     // Table of mappings of leave instructions to the first finally call island the leave
     // needs to execute.
@@ -905,6 +906,7 @@ class InterpCompiler
 
     void *m_asyncResumeFuncPtr = NULL;
     bool m_isAsyncMethodWithContextSaveRestore = false;
+    bool m_isAsyncVersionOfSyncMethod = false;
     int32_t m_asyncFinallyStartOffset = -1; // If the method is async, this is the offset of the start of the fault handler
 
     bool m_shadowCopyOfThisPointerActuallyNeeded = false;
@@ -989,8 +991,10 @@ class InterpCompiler
     bool    IsRuntimeAsyncCallConfigureAwaitTask(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo);
     bool    IsRuntimeAsyncCallConfigureAwaitValueTask(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo);
     bool    IsRuntimeAsyncCallConfigureAwaitValueTaskExactStLoc(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo);
+    bool    IsRuntimeAsyncCallRetInAsyncVersion(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo);
 
     int     ApplyRuntimeAsyncCall(const uint8_t* ip, OpcodePeepElement* peep, void* computedInfo) { return -1; }
+    int     ApplyRuntimeAsyncCallRetInAsyncVersion(const uint8_t* ip, OpcodePeepElement* peep, void* computedInfo);
     ContinuationContextHandling m_currentContinuationContextHandling = ContinuationContextHandling::None;
     CORINFO_RESOLVED_TOKEN m_resolvedAsyncCallToken;
 
@@ -1003,8 +1007,9 @@ class InterpCompiler
     void    EmitShiftOp(int32_t opBase);
     void    EmitCompareOp(int32_t opBase);
     void    EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool readonly, bool tailcall, bool newObj, bool isCalli);
+    void    WrapTopOfStackInAwait();
     void    EmitRet(CORINFO_METHOD_INFO* methodInfo);
-    void    EmitSuspend(const CORINFO_CALL_INFO &callInfo, ContinuationContextHandling ContinuationContextHandling);
+    void    EmitSuspend(CorInfoType callRetType, ContinuationContextHandling ContinuationContextHandling);
     void    EmitCalli(bool isTailCall, void* calliCookie, int callIFunctionPointerVar, CORINFO_SIG_INFO* callSiteSig);
     bool    EmitNamedIntrinsicCall(NamedIntrinsic ni, bool nonVirtualCall, CORINFO_CLASS_HANDLE clsHnd, CORINFO_METHOD_HANDLE method, CORINFO_SIG_INFO sig);
     void    EmitLdind(InterpType type, CORINFO_CLASS_HANDLE clsHnd, int32_t offset);
diff --git a/src/coreclr/jit/ICorJitInfo_names_generated.h b/src/coreclr/jit/ICorJitInfo_names_generated.h
index 57781f560ce0d5..ea2e65c04ba71a 100644
--- a/src/coreclr/jit/ICorJitInfo_names_generated.h
+++ b/src/coreclr/jit/ICorJitInfo_names_generated.h
@@ -126,6 +126,7 @@ DEF_CLR_API(runWithErrorTrap)
 DEF_CLR_API(runWithSPMIErrorTrap)
 DEF_CLR_API(getEEInfo)
 DEF_CLR_API(getAsyncInfo)
+DEF_CLR_API(getAwaitReturnCall)
 DEF_CLR_API(getMethodDefFromMethod)
 DEF_CLR_API(printMethodName)
 DEF_CLR_API(getMethodNameFromMetadata)
diff --git a/src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp b/src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp
index 87e541b608f155..e0d783dc6b0ccb 100644
--- a/src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp
+++ b/src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp
@@ -1200,6 +1200,16 @@ void WrapICorJitInfo::getAsyncInfo(
     API_LEAVE(getAsyncInfo);
 }
 
+CORINFO_METHOD_HANDLE WrapICorJitInfo::getAwaitReturnCall(
+          CORINFO_METHOD_HANDLE callerHandle,
+          CORINFO_LOOKUP* instArg)
+{
+    API_ENTER(getAwaitReturnCall);
+    CORINFO_METHOD_HANDLE temp = wrapHnd->getAwaitReturnCall(callerHandle, instArg);
+    API_LEAVE(getAwaitReturnCall);
+    return temp;
+}
+
 mdMethodDef WrapICorJitInfo::getMethodDefFromMethod(
           CORINFO_METHOD_HANDLE hMethod)
 {
diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp
index f78d4d33e60457..f53bb37f21efb8 100644
--- a/src/coreclr/jit/compiler.cpp
+++ b/src/coreclr/jit/compiler.cpp
@@ -3138,6 +3138,12 @@ void Compiler::compInitOptions(JitFlags* jitFlags)
         {
             printf("OPTIONS: compilation is an async state machine\n");
         }
+
+        if (compIsAsyncVersion())
+        {
+            printf(
+                "OPTIONS: compilation is for an async version of a synchronous method; IL belongs to synchronous method\n");
+        }
     }
 #endif
 
diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h
index fb55f2b4e7ba5a..587395510a86b6 100644
--- a/src/coreclr/jit/compiler.h
+++ b/src/coreclr/jit/compiler.h
@@ -4951,6 +4951,7 @@ class Compiler
         // This call is a task await
         PREFIX_IS_TASK_AWAIT = 0x00000080,
         PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT = 0x00000100,
+        PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT = 0x00000200,
     };
 
     static void impValidateMemoryAccessOpcode(const BYTE* codeAddr, const BYTE* codeEndp, bool volatilePrefix);
@@ -5639,6 +5640,7 @@ class Compiler
     void impLoadArg(unsigned ilArgNum, IL_OFFSET offset);
     void impLoadLoc(unsigned ilLclNum, IL_OFFSET offset);
     bool impReturnInstruction(int prefixFlags, OPCODE& opcode);
+    bool impWrapTopOfStackInAwait();
     void impPoisonImplicitByrefsBeforeReturn();
 
     // A free list of linked list nodes used to represent to-do stacks of basic blocks.
@@ -11804,11 +11806,18 @@ class Compiler
 #endif // TARGET_AMD64
     }
 
+    // Does this function have async calling convention and can have suspension points?
     bool compIsAsync() const
     {
         return opts.jitFlags->IsSet(JitFlags::JIT_FLAG_ASYNC);
     }
 
+    // Is this the async version of a non-async method? IL belongs to non-async method.
+    bool compIsAsyncVersion() const
+    {
+        return (info.compMethodInfo->options & CORINFO_ASYNC_VERSION) != 0;
+    }
+
     //------------------------------------------------------------------------
     // compMethodReturnsMultiRegRetType: Does this method return a multi-reg value?
     //
diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp
index 9c860424faa158..85aeffad119d86 100644
--- a/src/coreclr/jit/importer.cpp
+++ b/src/coreclr/jit/importer.cpp
@@ -9029,22 +9029,41 @@ void Compiler::impImportBlockCode(BasicBlock* block)
                     int         configVal          = -1; // -1 not configured, 0/1 configured to false/true
                     const BYTE* codeAddrAfterMatch = nullptr;
                     IL_OFFSET   awaitOffset        = BAD_IL_OFFSET;
+
+                    if (compIsAsyncVersion())
+                    {
+                        if ((codeAddr + sz < codeEndp) && (getU1LittleEndian(codeAddr + sz) == CEE_RET))
+                        {
+                            JITDUMP("\nRecognized tail-call in async version\n");
+                            awaitOffset = (IL_OFFSET)(codeAddr - 1 - info.compCode);
+                            isAwait     = true;
+                            prefixFlags |= PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT;
+
+                            // Consume the ret opcode. Note `codeAddr` points at the unconsumed token;
+                            // the main loop will still do `codeAddr += sz` (token size) after this case.
+                            codeAddrAfterMatch = codeAddr + 1;
+                        }
+                    }
+                    else
+                    {
 #ifdef DEBUG
-                    if (compIsAsync() && JitConfig.JitOptimizeAwait())
+                        if (compIsAsync() && JitConfig.JitOptimizeAwait())
 #else
-                    if (compIsAsync())
+                        if (compIsAsync())
 #endif
-                    {
-                        codeAddrAfterMatch = impMatchTaskAwaitPattern(codeAddr, codeEndp, &configVal, &awaitOffset);
-                        if (codeAddrAfterMatch != nullptr)
                         {
-                            JITDUMP("Recognized await%s\n", configVal == 0 ? " (with ConfigureAwait(false))" : "");
-
-                            isAwait = true;
-                            prefixFlags |= PREFIX_IS_TASK_AWAIT;
-                            if (configVal != 0)
+                            codeAddrAfterMatch = impMatchTaskAwaitPattern(codeAddr, codeEndp, &configVal, &awaitOffset);
+                            if (codeAddrAfterMatch != nullptr)
                             {
-                                prefixFlags |= PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT;
+                                JITDUMP("\nRecognized await%s\n",
+                                        configVal == 0 ? " (with ConfigureAwait(false))" : "");
+
+                                isAwait = true;
+                                prefixFlags |= PREFIX_IS_TASK_AWAIT;
+                                if (configVal != 0)
+                                {
+                                    prefixFlags |= PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT;
+                                }
                             }
                         }
                     }
@@ -9059,10 +9078,11 @@ void Compiler::impImportBlockCode(BasicBlock* block)
                             // It can also happen generally if the VM does not think using the async entry point
                             // is worth it. Treat these as a regular call that is Awaited.
                             _impResolveToken(CORINFO_TOKENKIND_Method);
-                            prefixFlags &= ~(PREFIX_IS_TASK_AWAIT | PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT);
+                            prefixFlags &= ~(PREFIX_IS_TASK_AWAIT | PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT |
+                                             PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT);
                             isAwait = false;
 
-                            JITDUMP("No async variant provided by VM, treating as regular call that is awaited\n");
+                            JITDUMP("\nNo async variant provided by VM, treating as regular call that is awaited\n");
                         }
                     }
                     else
@@ -9080,7 +9100,8 @@ void Compiler::impImportBlockCode(BasicBlock* block)
                                   (prefixFlags & PREFIX_CONSTRAINED) ? &constrainedResolvedToken : nullptr, flags,
                                   &callInfo);
 
-                    if (isAwait && (callInfo.kind == CORINFO_CALL))
+                    // TODO: crossgen2 cannot handle us removing this
+                    if (isAwait && IsReadyToRun() && (callInfo.kind == CORINFO_CALL))
                     {
                         assert(callInfo.sig.isAsyncCall());
                         bool isSyncCallThunk;
@@ -9091,11 +9112,10 @@ void Compiler::impImportBlockCode(BasicBlock* block)
                             // back to the non-async task-returning call. There
                             // is no reason to go through the thunk.
                             _impResolveToken(CORINFO_TOKENKIND_Method);
-                            prefixFlags &= ~(PREFIX_IS_TASK_AWAIT | PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT);
+                            prefixFlags &= ~(PREFIX_IS_TASK_AWAIT | PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT |
+                                             PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT);
                             isAwait = false;
 
-                            JITDUMP(
-                                "Async variant provided by VM is a thunk, switching direct call to synchronous task-returning method\n");
                             eeGetCallInfo(&resolvedToken,
                                           (prefixFlags & PREFIX_CONSTRAINED) ? &constrainedResolvedToken : nullptr,
                                           flags, &callInfo);
@@ -9274,9 +9294,31 @@ void Compiler::impImportBlockCode(BasicBlock* block)
                     return;
                 }
 
-                if (explicitTailCall || newBBcreatedForTailcallStress) // If newBBcreatedForTailcallStress is true, we
-                                                                       // have created a new BB after the "call"
-                // instruction in fgMakeBasicBlocks(). So we need to jump to RET regardless.
+                // For tail awaits we also import the ret right after.
+                // Also, we may have covariant cases like Task<int> return from a Task method,
+                // and in those cases we end up with an extra IL stack entry here.
+                if ((prefixFlags & PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT) != 0)
+                {
+                    if ((info.compRetType == TYP_VOID) && (stackState.esStackDepth > 0))
+                    {
+                        JITDUMP("\nHave extra IL stack entry after tail await\n");
+                        GenTree* val = impPopStack().val;
+                        if (varTypeIsStruct(val))
+                        {
+                            val = impNormStructVal(val, CHECK_SPILL_ALL);
+                        }
+
+                        impAppendTree(gtUnusedValNode(val), CHECK_SPILL_ALL, impCurStmtDI);
+                    }
+
+                    goto RET;
+                }
+
+                // For explicit tailcalls import the ret as part of it.
+                // If newBBcreatedForTailcallStress is true we have created a
+                // new BB after the "call" instruction in fgMakeBasicBlocks().
+                // So we need to jump to RET regardless.
+                if (explicitTailCall || newBBcreatedForTailcallStress)
                 {
                     assert(!compIsForInlining());
                     goto RET;
@@ -11219,7 +11261,7 @@ GenTree* Compiler::impStoreMultiRegValueToVar(GenTree*                    op,
 //
 // Arguments:
 //     prefixFlags -- active IL prefixes
-//     opcode -- [in, out] IL opcode
+//     opcode      -- [in, out] IL opcode
 //
 // Returns:
 //     True if import was successful (may fail for some inlinees)
@@ -11247,6 +11289,20 @@ bool Compiler::impReturnInstruction(int prefixFlags, OPCODE& opcode)
     GenTree* op2 = nullptr;
     GenTree* op1 = nullptr;
 
+    if (((prefixFlags & PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT) == 0) && compIsAsyncVersion())
+    {
+        JITDUMP("\nWrapping return value in await\n");
+        if (!impWrapTopOfStackInAwait())
+        {
+            return false;
+        }
+
+        if (info.compRetType == TYP_VOID)
+        {
+            impAppendTree(impPopStack().val, CHECK_SPILL_ALL, impCurStmtDI);
+        }
+    }
+
     if (info.compRetType != TYP_VOID)
     {
         op2 = impPopStack().val;
@@ -11593,6 +11649,117 @@ bool Compiler::impReturnInstruction(int prefixFlags, OPCODE& opcode)
     return true;
 }
 
+//------------------------------------------------------------------------
+// impWrapTopOfStackInAwait:
+//   Wrap the value on the top of the stack in AsyncHelpers.TransparentAwait.
+//
+// Returns:
+//   True if successful. False if he EE could not create the call (only during
+//   inlining), in which case inlining should be aborted.
+//
+// Remarks:
+//   Async versions of non-async task-returning methods are compiled with the
+//   exact same IL as the original method. This means the return value is
+//   mistyped; the original IL returns a Task or ValueTask, but the runtime
+//   async version expects to return the unwrapped result. This function
+//   accomplishes the unwrapping by inserting an async call to
+//   AsyncHelpers.TransparentAwait around the value on the top of the stack.
+//
+bool Compiler::impWrapTopOfStackInAwait()
+{
+    CORINFO_LOOKUP        instArgLookup;
+    CORINFO_METHOD_HANDLE awaitMethod = info.compCompHnd->getAwaitReturnCall(info.compMethodHnd, &instArgLookup);
+
+    CORINFO_SIG_INFO awaitSig;
+    info.compCompHnd->getMethodSig(awaitMethod, &awaitSig);
+
+    assert(awaitSig.isAsyncCall());
+
+    var_types            callRetType = JITtype2varType(awaitSig.retType);
+    GenTreeCall*         awaitCall   = gtNewCallNode(CT_USER_FUNC, awaitMethod, callRetType);
+    CORINFO_CLASS_HANDLE taskTypeHnd;
+    CorInfoType          taskType = strip(info.compCompHnd->getArgType(&awaitSig, awaitSig.args, &taskTypeHnd));
+
+    GenTree*   awaitable   = impPopStack().val;
+    var_types  taskJitType = JITtype2varType(taskType);
+    NewCallArg taskArg;
+    if (taskJitType == TYP_STRUCT)
+    {
+        awaitable = impNormStructVal(awaitable, CHECK_SPILL_ALL);
+        taskArg   = NewCallArg::Struct(awaitable, TYP_STRUCT, typGetObjLayout(taskTypeHnd));
+    }
+    else
+    {
+        taskArg = NewCallArg::Primitive(awaitable, taskJitType);
+    }
+
+    awaitCall->gtArgs.PushFront(this, taskArg);
+
+    NewCallArg asyncContArg = NewCallArg::Primitive(gtNewNull()).WellKnown(WellKnownArg::AsyncContinuation);
+
+    NewCallArg instArg;
+    if (awaitSig.hasTypeArg())
+    {
+        GenTree* instArgTree = impLookupToTree(&instArgLookup, GTF_ICON_METHOD_HDL, awaitMethod);
+        if (instArgTree == nullptr)
+        {
+            // Can fail if runtime lookup cannot be represented properly during inlining
+            return false;
+        }
+
+        instArg = NewCallArg::Primitive(instArgTree).WellKnown(WellKnownArg::InstParam);
+    }
+
+    if (Target::g_tgtArgOrder == Target::ARG_ORDER_R2L)
+    {
+        awaitCall->gtArgs.PushFront(this, asyncContArg);
+
+        if (awaitSig.hasTypeArg())
+        {
+            awaitCall->gtArgs.PushFront(this, instArg);
+        }
+    }
+    else
+    {
+        awaitCall->gtArgs.PushBack(this, asyncContArg);
+
+        if (awaitSig.hasTypeArg())
+        {
+            awaitCall->gtArgs.PushBack(this, instArg);
+        }
+    }
+
+    GenTree* toPush = awaitCall;
+    if (varTypeIsStruct(callRetType))
+    {
+        toPush = impFixupCallStructReturn(awaitCall, awaitSig.retTypeClass);
+    }
+
+    AsyncCallInfo* asyncInfo = new (this, CMK_Async) AsyncCallInfo;
+
+    if (impInlineRoot()->compIsAsyncVersion())
+    {
+        asyncInfo->IsTailAwait = !compIsForInlining() || impInlineInfo->iciCall->GetAsyncInfo().IsTailAwait;
+    }
+    else
+    {
+        // We are inlining into an async method. This means we have a proper
+        // async await, and we require proper handling.
+        assert(compIsForInlining() && impInlineInfo->iciCall->IsAsync());
+        GenTreeCall* inlCall = impInlineInfo->iciCall;
+
+        JITDUMP("Inheriting continuation handling %d from caller [%06u]\n",
+                (unsigned)inlCall->GetAsyncInfo().ContinuationContextHandling, dspTreeID(inlCall));
+        asyncInfo->ContinuationContextHandling = inlCall->GetAsyncInfo().ContinuationContextHandling;
+        impInheritAsyncContextsFromInliner(awaitCall);
+    }
+
+    awaitCall->SetIsAsync(asyncInfo);
+
+    impPushOnStack(toPush, makeTypeInfo(awaitSig.retType, awaitSig.retTypeClass));
+    return true;
+}
+
 #ifdef DEBUG
 //------------------------------------------------------------------------
 // impPoisonImplicitByrefsBeforeReturn:
diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp
index 73ae8326e07cde..d2c1b9a2483891 100644
--- a/src/coreclr/jit/importercalls.cpp
+++ b/src/coreclr/jit/importercalls.cpp
@@ -7062,23 +7062,37 @@ void Compiler::impSetupAsyncCall(GenTreeCall* call, OPCODE opcode, unsigned pref
 
     if (compIsForInlining())
     {
-        if (!m_nextAwaitIsTail)
+        if (!m_nextAwaitIsTail && !compIsAsyncVersion())
         {
             compInlineResult->NoteFatal(InlineObservation::CALLEE_AWAIT);
             return;
         }
 
+        // For async versions of synchronous methods all async calls are in
+        // tail position. Inlining is simple for these cases: we can just
+        // inherit all context handling from the inlining call.
+        assert(!compIsAsyncVersion() || ((prefixFlags & PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT) != 0));
+
         GenTreeCall* inlCall = impInlineInfo->iciCall;
-        JITDUMP("Call [%06u] is to call with a tail async call [%06u]\n", dspTreeID(inlCall), dspTreeID(call));
+        JITDUMP("Call [%06u] is to function with a tail async call [%06u]\n", dspTreeID(inlCall), dspTreeID(call));
 
         assert(inlCall->IsAsync());
 
         asyncInfo.ContinuationContextHandling = inlCall->GetAsyncInfo().ContinuationContextHandling;
         // Validate that below code won't override the handling
         assert((prefixFlags & PREFIX_IS_TASK_AWAIT) == 0);
-        m_nextAwaitIsTail = false;
 
-        asyncInfo.IsTailAwait = inlCall->GetAsyncInfo().IsTailAwait;
+        asyncInfo.IsTailAwait =
+            inlCall->GetAsyncInfo().IsTailAwait && (m_nextAwaitIsTail || (call->gtReturnType == info.compRetType));
+        m_nextAwaitIsTail = false;
+    }
+    else
+    {
+        if (opts.OptimizationEnabled() && ((prefixFlags & PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT) != 0))
+        {
+            // We can only do an actual tail await if the caller and callee agree on return type.
+            asyncInfo.IsTailAwait = call->gtReturnType == info.compRetType;
+        }
     }
 
     unsigned newSourceTypes = ICorDebugInfo::ASYNC;
diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
index ffa900b4b08198..f41132ca3c4a8e 100644
--- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
+++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
@@ -1,4 +1,4 @@
-﻿<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
     <!-- https://github.com/dotnet/dotnet/issues/5640 -->
@@ -43,7 +43,6 @@
   </PropertyGroup>
 
   <PropertyGroup>
-    <Features>$(Features);runtime-async=on</Features>
   </PropertyGroup>
 
   <!-- Sources -->
diff --git a/src/coreclr/tools/Common/Compiler/AsyncMethodVariant.cs b/src/coreclr/tools/Common/Compiler/AsyncMethodVariant.cs
index 78b0c9c76b174b..b3c0996384f84c 100644
--- a/src/coreclr/tools/Common/Compiler/AsyncMethodVariant.cs
+++ b/src/coreclr/tools/Common/Compiler/AsyncMethodVariant.cs
@@ -160,6 +160,11 @@ public static bool IsCompilerGeneratedILBodyForAsync(this MethodDesc method)
             return method.IsAsyncThunk() || method is AsyncResumptionStub;
         }
 
+        public static bool RequiresSaveRestoreOfAsyncContexts(this MethodDesc method)
+        {
+            return method.IsAsync && method.IsAsyncVariant();
+        }
+
         public static MethodDesc GetAsyncVariant(this MethodDesc method)
         {
             Debug.Assert(!method.IsAsyncVariant());
diff --git a/src/coreclr/tools/Common/Compiler/MethodExtensions.cs b/src/coreclr/tools/Common/Compiler/MethodExtensions.cs
index f1a9aa8755fc89..887f429da13977 100644
--- a/src/coreclr/tools/Common/Compiler/MethodExtensions.cs
+++ b/src/coreclr/tools/Common/Compiler/MethodExtensions.cs
@@ -201,5 +201,12 @@ public static bool IsCallEffectivelyDirect(this MethodDesc method)
         /// but use async calling convention.
         /// </summary>
         public static bool IsAsyncCall(this MethodDesc method) => method.IsAsyncVariant() || method.IsReturnDroppingAsyncThunk() || (method.IsAsync && !method.Signature.ReturnsTaskOrValueTask());
+
+        /// <summary>
+        /// Check if this is an async method whose codegen supports passing it the IL of the original non-async method.
+        /// </summary>
+        /// <param name="method"></param>
+        /// <returns></returns>
+        public static bool SupportsAsyncVersionCodegen(this MethodDesc method) => method.IsAsyncVariant() && method.IsAsyncThunk();
     }
 }
diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
index 1076cf8a9b0b16..e616fa9c9c92ff 100644
--- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
+++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
@@ -831,11 +831,19 @@ private bool Get_CORINFO_METHOD_INFO(MethodDesc method, MethodIL methodIL, CORIN
             // of async contexts. Regular user implemented runtime async methods
             // require this behavior, but thunks should be transparent and should not
             // come with this behavior.
-            if (method.IsAsyncVariant() && method.IsAsync)
+            if (method.RequiresSaveRestoreOfAsyncContexts())
             {
                 methodInfo->options |= CorInfoOptions.CORINFO_ASYNC_SAVE_CONTEXTS;
             }
 
+#if !READYTORUN
+            if (method.SupportsAsyncVersionCodegen())
+            {
+                // This is an async version and the IL belongs to the sync version.
+                methodInfo->options |= CorInfoOptions.CORINFO_ASYNC_VERSION;
+            }
+#endif
+
             methodInfo->regionKind = CorInfoRegionKind.CORINFO_REGION_NONE;
             Get_CORINFO_SIG_INFO(method, sig: &methodInfo->args, methodIL);
             Get_CORINFO_SIG_INFO(methodIL.GetLocals(), &methodInfo->locals);
@@ -1267,6 +1275,7 @@ private bool getMethodInfo(CORINFO_METHOD_STRUCT_* ftn, CORINFO_METHOD_INFO* inf
             if (!_compilation.CanInline(MethodBeingCompiled, method))
                 return false;
 
+
             MethodIL methodIL = method.IsUnboxingThunk() ? null : _compilation.GetMethodIL(method);
             return Get_CORINFO_METHOD_INFO(method, methodIL, info);
         }
@@ -1519,7 +1528,7 @@ static CORINFO_RESOLVED_TOKEN CreateResolvedTokenFromMethod(CorInfoImpl jitInter
 
                 CORINFO_RESOLVED_TOKEN result = default(CORINFO_RESOLVED_TOKEN);
                 MethodILScope scope = jitInterface._compilation.GetMethodIL(methodWithToken.Method);
-                scope ??= EcmaMethodILScope.Create((EcmaMethod)methodWithToken.Method.GetTypicalMethodDefinition());
+                scope ??= EcmaMethodILScope.Create(methodWithToken.Method.GetTypicalMethodDefinition());
                 result.tokenScope = jitInterface.ObjectToHandle(scope);
                 result.tokenContext = jitInterface.contextFromMethod(method);
 #if READYTORUN
@@ -3499,6 +3508,59 @@ private void getAsyncInfo(ref CORINFO_ASYNC_INFO pAsyncInfoOut)
             pAsyncInfoOut.finishSuspensionWithContinuationContextMethHnd = ObjectToHandle(asyncHelpers.GetKnownMethod("FinishSuspensionWithContinuationContext"u8, null));
         }
 
+        private CORINFO_METHOD_STRUCT_* getAwaitReturnCall(CORINFO_METHOD_STRUCT_* callerHandle, ref CORINFO_LOOKUP instArg)
+        {
+            instArg.lookupKind.needsRuntimeLookup = false;
+            instArg.constLookup.accessType = InfoAccessType.IAT_VALUE;
+            instArg.constLookup.addr = null;
+
+#if READYTORUN
+            return null;
+#else
+            MethodDesc caller = HandleToObject(callerHandle);
+            Debug.Assert(caller.IsAsyncVariant() && caller.IsAsyncThunk());
+
+            MethodDesc taskReturningMethod = caller.GetTargetOfAsyncVariant();
+            TypeDesc taskReturnType = taskReturningMethod.Signature.ReturnType;
+            bool isValueTask = taskReturnType.IsValueType;
+
+            CompilerTypeSystemContext context = _compilation.TypeSystemContext;
+            DefType asyncHelpers = context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8);
+
+            MethodDesc runtimeDeterminedCaller = caller.GetSharedRuntimeFormMethodTarget();
+
+            TypeDesc returnType = runtimeDeterminedCaller.Signature.ReturnType;
+            MethodDesc runtimeDeterminedResult;
+            if (returnType.IsVoid)
+            {
+                TypeDesc parameterType = isValueTask
+                    ? context.SystemModule.GetKnownType("System.Threading.Tasks"u8, "ValueTask"u8)
+                    : context.SystemModule.GetKnownType("System.Threading.Tasks"u8, "Task"u8);
+                MethodSignature signature = new MethodSignature(MethodSignatureFlags.Static, 0, context.GetWellKnownType(WellKnownType.Void), [parameterType]);
+                runtimeDeterminedResult = asyncHelpers.GetKnownMethod("TransparentAwaitWithResult"u8, signature);
+            }
+            else
+            {
+                TypeDesc signatureVariable = context.GetSignatureVariable(0, method: true);
+                TypeDesc parameterType = isValueTask
+                    ? context.SystemModule.GetKnownType("System.Threading.Tasks"u8, "ValueTask`1"u8).MakeInstantiatedType(signatureVariable)
+                    : context.SystemModule.GetKnownType("System.Threading.Tasks"u8, "Task`1"u8).MakeInstantiatedType(signatureVariable);
+                MethodSignature signature = new MethodSignature(MethodSignatureFlags.Static, 1, signatureVariable, [parameterType]);
+                runtimeDeterminedResult = asyncHelpers.GetKnownMethod("TransparentAwaitWithResult"u8, signature).MakeInstantiatedMethod(returnType);
+            }
+
+            MethodDesc result = runtimeDeterminedResult.GetCanonMethodTarget(CanonicalFormKind.Specific);
+
+            if (result.RequiresInstArg())
+            {
+                // Runtime lookup is needed
+                ComputeLookup(caller != MethodBeingCompiled, runtimeDeterminedResult, ReadyToRunHelperId.MethodDictionary, caller, ref instArg);
+            }
+
+            return ObjectToHandle(result);
+#endif
+        }
+
         private CORINFO_CLASS_STRUCT_* getContinuationType(nuint dataSize, ref bool objRefs, nuint objRefsSize)
         {
             Debug.Assert(objRefsSize == (dataSize + (nuint)(PointerSize - 1)) / (nuint)PointerSize);
diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs
index 2ec1fc175a8d91..2e634fd9c606b9 100644
--- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs
+++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs
@@ -142,6 +142,7 @@ static ICorJitInfoCallbacks()
                 s_callbacks.runWithSPMIErrorTrap = &_runWithSPMIErrorTrap;
                 s_callbacks.getEEInfo = &_getEEInfo;
                 s_callbacks.getAsyncInfo = &_getAsyncInfo;
+                s_callbacks.getAwaitReturnCall = &_getAwaitReturnCall;
                 s_callbacks.getMethodDefFromMethod = &_getMethodDefFromMethod;
                 s_callbacks.printMethodName = &_printMethodName;
                 s_callbacks.getMethodNameFromMetadata = &_getMethodNameFromMetadata;
@@ -325,6 +326,7 @@ static ICorJitInfoCallbacks()
             public delegate* unmanaged<IntPtr, IntPtr*, void*, void*, byte> runWithSPMIErrorTrap;
             public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_EE_INFO*, void> getEEInfo;
             public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_ASYNC_INFO*, void> getAsyncInfo;
+            public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_METHOD_STRUCT_*, CORINFO_LOOKUP*, CORINFO_METHOD_STRUCT_*> getAwaitReturnCall;
             public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_METHOD_STRUCT_*, mdToken> getMethodDefFromMethod;
             public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_METHOD_STRUCT_*, byte*, nuint, nuint*, nuint> printMethodName;
             public delegate* unmanaged<IntPtr, IntPtr*, CORINFO_METHOD_STRUCT_*, byte**, byte**, byte**, nuint, byte*> getMethodNameFromMetadata;
@@ -2191,6 +2193,21 @@ private static void _getAsyncInfo(IntPtr thisHandle, IntPtr* ppException, CORINF
             }
         }
 
+        [UnmanagedCallersOnly]
+        private static CORINFO_METHOD_STRUCT_* _getAwaitReturnCall(IntPtr thisHandle, IntPtr* ppException, CORINFO_METHOD_STRUCT_* callerHandle, CORINFO_LOOKUP* instArg)
+        {
+            var _this = GetThis(thisHandle);
+            try
+            {
+                return _this.getAwaitReturnCall(callerHandle, ref *instArg);
+            }
+            catch (Exception ex)
+            {
+                *ppException = _this.AllocException(ex);
+                return default;
+            }
+        }
+
         [UnmanagedCallersOnly]
         private static mdToken _getMethodDefFromMethod(IntPtr thisHandle, IntPtr* ppException, CORINFO_METHOD_STRUCT_* hMethod)
         {
diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs
index f0fe635a87e505..b70afe4fc91020 100644
--- a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs
+++ b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs
@@ -444,6 +444,7 @@ public enum CorInfoOptions
                                                    CORINFO_GENERICS_CTXT_FROM_METHODTABLE),
         CORINFO_GENERICS_CTXT_KEEP_ALIVE = 0x00000100, // Keep the generics context alive throughout the method even if there is no explicit use, and report its location to the CLR
         CORINFO_ASYNC_SAVE_CONTEXTS = 0x00000200, // Runtime async method must save and restore contexts
+        CORINFO_ASYNC_VERSION = 0x00000400, // This is an async version whose IL belongs to a non-async method
     }
 
     // These are used to detect array methods as NamedIntrinsic in JIT importer,
diff --git a/src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt b/src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt
index 384ca10f881504..2456fc811624ae 100644
--- a/src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt
+++ b/src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt
@@ -295,6 +295,7 @@ FUNCTIONS
     [ManualNativeWrapper] bool runWithSPMIErrorTrap(ICorJitInfo::errorTrapFunction function, void* parameter);
     void getEEInfo(CORINFO_EE_INFO* pEEInfoOut);
     void getAsyncInfo(CORINFO_ASYNC_INFO* pAsyncInfoOut);
+    CORINFO_METHOD_HANDLE getAwaitReturnCall(CORINFO_METHOD_HANDLE callerHandle, CORINFO_LOOKUP* instArg);
     mdMethodDef getMethodDefFromMethod(CORINFO_METHOD_HANDLE hMethod);
     size_t printMethodName(CORINFO_METHOD_HANDLE ftn, char* buffer, size_t bufferSize, size_t* pRequiredBufferSize)
     const char* getMethodNameFromMetadata(CORINFO_METHOD_HANDLE ftn, const char **className, const char **namespaceName, const char **enclosingClassNames, size_t maxEnclosingClassNames);
diff --git a/src/coreclr/tools/Common/TypeSystem/IL/EcmaMethodIL.cs b/src/coreclr/tools/Common/TypeSystem/IL/EcmaMethodIL.cs
index 3d8a22c0adccb3..bcaca7df89f1c4 100644
--- a/src/coreclr/tools/Common/TypeSystem/IL/EcmaMethodIL.cs
+++ b/src/coreclr/tools/Common/TypeSystem/IL/EcmaMethodIL.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Immutable;
+using System.Diagnostics;
 using System.Reflection.Metadata;
 using System.Reflection.Metadata.Ecma335;
 using System.Threading;
@@ -144,17 +145,20 @@ public override object GetObject(int token, NotFoundBehavior notFoundBehavior =
     public sealed partial class EcmaMethodILScope : MethodILScope
     {
         private readonly EcmaModule _module;
-        private readonly EcmaMethod _method;
+        private readonly MethodDesc _method;
 
-        public static EcmaMethodILScope Create(EcmaMethod method)
+        public static EcmaMethodILScope Create(MethodDesc method)
         {
             return new EcmaMethodILScope(method);
         }
 
-        private EcmaMethodILScope(EcmaMethod method)
+        private EcmaMethodILScope(MethodDesc method)
         {
             _method = method;
-            _module = method.Module;
+
+            // We allow MethodDesc so that this is usable with e.g. async variants too,
+            // but the owning module needs to be an EcmaModule.
+            _module = ((EcmaType)method.OwningType).Module;
         }
 
         public EcmaModule Module
diff --git a/src/coreclr/tools/Common/TypeSystem/IL/NativeAotILProvider.cs b/src/coreclr/tools/Common/TypeSystem/IL/NativeAotILProvider.cs
index 0a26773578045f..5bbde912ee987b 100644
--- a/src/coreclr/tools/Common/TypeSystem/IL/NativeAotILProvider.cs
+++ b/src/coreclr/tools/Common/TypeSystem/IL/NativeAotILProvider.cs
@@ -362,14 +362,14 @@ public override MethodIL GetMethodIL(MethodDesc method)
             else
             if (method is AsyncMethodVariant asyncVariantImpl)
             {
-                if (asyncVariantImpl.IsAsync)
-                {
-                    return new AsyncEcmaMethodIL(asyncVariantImpl, EcmaMethodIL.Create(asyncVariantImpl.Target));
-                }
-                else
-                {
-                    return AsyncThunkILEmitter.EmitAsyncMethodThunk(asyncVariantImpl, asyncVariantImpl.Target);
-                }
+                MethodIL wrappedIL = asyncVariantImpl.IsAsync
+                    ? EcmaMethodIL.Create(asyncVariantImpl.Target)
+                    : GetMethodIL(asyncVariantImpl.Target);
+
+                if (wrappedIL == null)
+                    return null;
+
+                return new AsyncMethodIL(asyncVariantImpl, wrappedIL);
             }
             else
             {
@@ -378,25 +378,25 @@ public override MethodIL GetMethodIL(MethodDesc method)
             }
         }
 
-        private sealed class AsyncEcmaMethodIL : MethodIL
+        private sealed class AsyncMethodIL : MethodIL
         {
             private readonly AsyncMethodVariant _variant;
-            private readonly EcmaMethodIL _ecmaIL;
+            private readonly MethodIL _wrappedIL;
 
-            public AsyncEcmaMethodIL(AsyncMethodVariant variant, EcmaMethodIL ecmaIL)
-                => (_variant, _ecmaIL) = (variant, ecmaIL);
+            public AsyncMethodIL(AsyncMethodVariant variant, MethodIL wrappedIL)
+                => (_variant, _wrappedIL) = (variant, wrappedIL);
 
             // This is the reason we need this class - the method that owns the IL is the variant.
             public override MethodDesc OwningMethod => _variant;
 
-            // Everything else dispatches to EcmaMethodIL
-            public override MethodDebugInformation GetDebugInfo() => _ecmaIL.GetDebugInfo();
-            public override ILExceptionRegion[] GetExceptionRegions() => _ecmaIL.GetExceptionRegions();
-            public override byte[] GetILBytes() => _ecmaIL.GetILBytes();
-            public override LocalVariableDefinition[] GetLocals() => _ecmaIL.GetLocals();
-            public override object GetObject(int token, NotFoundBehavior notFoundBehavior = NotFoundBehavior.Throw) => _ecmaIL.GetObject(token, notFoundBehavior);
-            public override bool IsInitLocals => _ecmaIL.IsInitLocals;
-            public override int MaxStack => _ecmaIL.MaxStack;
+            // Everything else dispatches to the wrapped IL
+            public override MethodDebugInformation GetDebugInfo() => _wrappedIL.GetDebugInfo();
+            public override ILExceptionRegion[] GetExceptionRegions() => _wrappedIL.GetExceptionRegions();
+            public override byte[] GetILBytes() => _wrappedIL.GetILBytes();
+            public override LocalVariableDefinition[] GetLocals() => _wrappedIL.GetLocals();
+            public override object GetObject(int token, NotFoundBehavior notFoundBehavior = NotFoundBehavior.Throw) => _wrappedIL.GetObject(token, notFoundBehavior);
+            public override bool IsInitLocals => _wrappedIL.IsInitLocals;
+            public override int MaxStack => _wrappedIL.MaxStack;
         }
     }
 }
diff --git a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncThunks.cs b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncThunks.cs
index 571bc758795ce2..dc6cc330b645c1 100644
--- a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncThunks.cs
+++ b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncThunks.cs
@@ -242,160 +242,6 @@ public static MethodIL EmitTaskReturningThunk(MethodDesc taskReturningMethod, Me
             return emitter.Link(taskReturningMethod);
         }
 
-        // The emitted code matches method EmitAsyncMethodThunk in CoreCLR VM.
-        public static MethodIL EmitAsyncMethodThunk(MethodDesc asyncMethod, MethodDesc taskReturningMethod)
-        {
-            TypeSystemContext context = asyncMethod.Context;
-
-            var emitter = new ILEmitter();
-            emitter.SetHasGeneratedTokens();
-
-            var codestream = emitter.NewCodeStream();
-
-            if (taskReturningMethod.OwningType.HasInstantiation)
-            {
-                var instantiatedType = (InstantiatedType)TypeSystemHelpers.InstantiateAsOpen(taskReturningMethod.OwningType);
-                taskReturningMethod = context.GetMethodForInstantiatedType(taskReturningMethod, instantiatedType);
-            }
-
-            if (taskReturningMethod.HasInstantiation)
-            {
-                var inst = new TypeDesc[taskReturningMethod.Instantiation.Length];
-                for (int i = 0; i < inst.Length; i++)
-                {
-                    inst[i] = context.GetSignatureVariable(i, true);
-                }
-                taskReturningMethod = taskReturningMethod.MakeInstantiatedMethod(new Instantiation(inst));
-            }
-
-            MethodSignature sig = asyncMethod.Signature;
-
-            int localArg = 0;
-            if (!sig.IsStatic)
-            {
-                codestream.EmitLdArg(localArg++);
-            }
-
-            for (int iArg = 0; iArg < sig.Length; iArg++)
-            {
-                codestream.EmitLdArg(localArg++);
-            }
-
-            codestream.Emit(ILOpcode.call, emitter.NewToken(taskReturningMethod));
-
-            TypeDesc taskReturningMethodReturnType = taskReturningMethod.Signature.ReturnType;
-
-            bool isValueTask = taskReturningMethodReturnType.IsValueType;
-
-            if (isValueTask)
-            {
-                TypeDesc valueTaskType = taskReturningMethodReturnType;
-                MethodDesc isCompletedMethod;
-                MethodDesc completionResultMethod;
-                MethodDesc transparentAwaitValueTaskMethod;
-
-                if (!taskReturningMethodReturnType.HasInstantiation)
-                {
-                    // ValueTask (non-generic)
-                    isCompletedMethod = valueTaskType.GetKnownMethod("get_IsCompleted"u8, null);
-                    completionResultMethod = valueTaskType.GetKnownMethod("ThrowIfCompletedUnsuccessfully"u8, null);
-                    transparentAwaitValueTaskMethod =
-                        context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8)
-                        .GetKnownMethod("TransparentAwaitValueTask"u8, null);
-                }
-                else
-                {
-                    // ValueTask<T> (generic)
-                    isCompletedMethod = valueTaskType.GetKnownMethod("get_IsCompleted"u8, null);
-                    completionResultMethod = valueTaskType.GetKnownMethod("get_Result"u8, null);
-                    transparentAwaitValueTaskMethod =
-                        context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8)
-                        .GetKnownMethod("TransparentAwaitValueTaskOfT"u8, null)
-                        .MakeInstantiatedMethod(valueTaskType.Instantiation[0]);
-                }
-
-                ILLocalVariable valueTaskLocal = emitter.NewLocal(valueTaskType);
-                ILCodeLabel valueTaskCompletedLabel = emitter.NewCodeLabel();
-
-                // Store value task returned by call to actual user func
-                codestream.EmitStLoc(valueTaskLocal);
-                codestream.EmitLdLoca(valueTaskLocal);
-
-                // Was it already completed?
-                codestream.Emit(ILOpcode.call, emitter.NewToken(isCompletedMethod));
-                codestream.Emit(ILOpcode.brtrue, valueTaskCompletedLabel);
-
-                // No, tail await to TransparentAwaitValueTask
-                codestream.EmitLdLoc(valueTaskLocal);
-                codestream.Emit(ILOpcode.call, emitter.NewToken(context.GetCoreLibEntryPoint("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8, "TailAwait"u8, null)));
-                codestream.Emit(ILOpcode.call, emitter.NewToken(transparentAwaitValueTaskMethod));
-                codestream.Emit(ILOpcode.ret);
-
-                // Yes, just get the result
-                codestream.EmitLabel(valueTaskCompletedLabel);
-                codestream.EmitLdLoca(valueTaskLocal);
-                codestream.Emit(ILOpcode.call, emitter.NewToken(completionResultMethod));
-                codestream.Emit(ILOpcode.ret);
-            }
-            else
-            {
-                // Task path
-                TypeDesc taskType = taskReturningMethodReturnType;
-                MethodDesc completedTaskResultMethod;
-                MethodDesc transparentAwaitMethod;
-
-                if (!taskReturningMethodReturnType.HasInstantiation)
-                {
-                    // Task (non-generic)
-                    completedTaskResultMethod = context.SystemModule
-                        .GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8)
-                        .GetKnownMethod("CompletedTask"u8, null);
-                    transparentAwaitMethod = context.SystemModule
-                        .GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8)
-                        .GetKnownMethod("TransparentAwait"u8, null);
-                }
-                else
-                {
-                    // Task<T> (generic)
-                    TypeDesc logicalReturnType = taskReturningMethodReturnType.Instantiation[0];
-
-                    MethodDesc completedTaskResultMethodOpen = context.SystemModule
-                        .GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8)
-                        .GetKnownMethod("CompletedTaskResult"u8, null);
-                    MethodDesc transparentAwaitMethodOpen = context.SystemModule
-                        .GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8)
-                        .GetKnownMethod("TransparentAwaitOfT"u8, null);
-
-                    completedTaskResultMethod = completedTaskResultMethodOpen.MakeInstantiatedMethod(new Instantiation(logicalReturnType));
-                    transparentAwaitMethod = transparentAwaitMethodOpen.MakeInstantiatedMethod(new Instantiation(logicalReturnType));
-                }
-
-                ILLocalVariable taskLocal = emitter.NewLocal(taskType);
-                ILCodeLabel getResultLabel = emitter.NewCodeLabel();
-
-                // Store task returned by actual user func or by ValueTask.AsTask
-                codestream.EmitStLoc(taskLocal);
-
-                codestream.EmitLdLoc(taskLocal);
-                codestream.Emit(ILOpcode.call, emitter.NewToken(
-                    context.SystemModule.GetKnownType("System.Threading.Tasks"u8, "Task"u8)
-                        .GetKnownMethod("get_IsCompleted"u8, null)));
-                codestream.Emit(ILOpcode.brtrue, getResultLabel);
-
-                codestream.EmitLdLoc(taskLocal);
-                codestream.Emit(ILOpcode.call, emitter.NewToken(context.GetCoreLibEntryPoint("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8, "TailAwait"u8, null)));
-                codestream.Emit(ILOpcode.call, emitter.NewToken(transparentAwaitMethod));
-                codestream.Emit(ILOpcode.ret);
-
-                codestream.EmitLabel(getResultLabel);
-                codestream.EmitLdLoc(taskLocal);
-                codestream.Emit(ILOpcode.call, emitter.NewToken(completedTaskResultMethod));
-                codestream.Emit(ILOpcode.ret);
-            }
-
-            return emitter.Link(asyncMethod);
-        }
-
         // Provided an async variant, emits an async wrapper that drops the returned value.
         // Used in the covariant return scenario.
         // The emitted code matches EmitReturnDroppingThunk in CoreCLR VM.
diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/AsyncMaskingILProvider.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/AsyncMaskingILProvider.cs
index a7cdaf478b4dca..d488ab433ad315 100644
--- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/AsyncMaskingILProvider.cs
+++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/AsyncMaskingILProvider.cs
@@ -43,7 +43,7 @@ public override MethodIL GetMethodIL(MethodDesc method)
         public static MethodIL WrapIL(MethodIL methodIL)
         {
             MethodDesc owningMethod = methodIL.OwningMethod;
-            if (owningMethod.IsAsync && owningMethod.IsAsyncVariant())
+            if (owningMethod.IsAsyncVariant())
             {
                 return new AsyncMaskedMethodIL(((CompilerTypeSystemContext)owningMethod.Context).GetTargetOfAsyncVariantMethod(owningMethod), methodIL);
             }
diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs
index c61590199c91f9..4c10dfe20a3d4c 100644
--- a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs
+++ b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs
@@ -70,7 +70,9 @@ public enum ImportState : byte
         private DependencyList _dependencies;
         private BasicBlock _lateBasicBlocks;
 
+        private int _prevMatchedAwaitTailCallRetPostOffset;
         private bool _asyncDependenciesReported;
+        private bool _wrappingAwaitReported;
 
         private sealed class ExceptionRegion
         {
@@ -80,6 +82,7 @@ private sealed class ExceptionRegion
 
         public ILImporter(ILScanner compilation, MethodDesc method, MethodIL methodIL = null)
         {
+
             if (methodIL == null)
             {
                 methodIL = compilation.GetMethodIL(method);
@@ -180,7 +183,7 @@ public ILImporter(ILScanner compilation, MethodDesc method, MethodIL methodIL =
                 }
             }
 
-            if (_canonMethod.IsAsyncCall())
+            if (_canonMethod.RequiresSaveRestoreOfAsyncContexts())
             {
                 const string reason = "Async state machine";
                 DefType asyncHelpers = _compilation.TypeSystemContext.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8);
@@ -342,12 +345,7 @@ private bool MatchTaskAwaitPattern()
             //    call       <Await>
 
             // Find where this basic block ends
-            int nextBBOffset = _currentOffset;
-            while (nextBBOffset < _basicBlocks.Length && _basicBlocks[nextBBOffset] == null)
-                nextBBOffset++;
-
-            // Create ILReader for what's left in the basic block
-            var reader = new ILReader(new ReadOnlySpan<byte>(_ilBytes, _currentOffset, nextBBOffset - _currentOffset));
+            ILReader reader = GetRemainingBlockIL();
 
             if (!reader.HasNext)
                 return false;
@@ -414,6 +412,34 @@ private bool MatchTaskAwaitPattern()
                 && IsAsyncHelpersAwait((MethodDesc)_methodIL.GetObject(reader.ReadILToken()));
         }
 
+        private ILReader GetRemainingBlockIL()
+        {
+            int nextBBOffset = _currentOffset;
+            while (nextBBOffset < _basicBlocks.Length && _basicBlocks[nextBBOffset] == null)
+                nextBBOffset++;
+
+            // Create ILReader for what's left in the basic block
+            return new ILReader(new ReadOnlySpan<byte>(_ilBytes, _currentOffset, nextBBOffset - _currentOffset));
+        }
+
+        private bool MatchTailCallAwait(MethodDesc method)
+        {
+            // Create ILReader for what's left in the basic block
+            var reader = GetRemainingBlockIL();
+
+            if (!reader.HasNext)
+                return false;
+
+            ILOpcode opcode = reader.ReadILOpcode();
+            if (opcode == ILOpcode.ret && method.Signature.ReturnsTaskOrValueTask())
+            {
+                _prevMatchedAwaitTailCallRetPostOffset = _currentOffset + reader.Offset;
+                return true;
+            }
+
+            return false;
+        }
+
         private void ImportCall(ILOpcode opcode, int token)
         {
             // We get both the canonical and runtime determined form - JitInterface mostly operates
@@ -434,31 +460,8 @@ private void ImportCall(ILOpcode opcode, int token)
             }
 
             // Are we scanning a call within a state machine?
-            if (opcode is ILOpcode.call or ILOpcode.callvirt
-                && _canonMethod.IsAsyncCall())
+            if (opcode is ILOpcode.call or ILOpcode.callvirt && _canonMethod.IsAsyncCall())
             {
-                // Add dependencies on infra to do suspend/resume. We only need to do this once per method scanned.
-                if (!_asyncDependenciesReported && method.IsAsync)
-                {
-                    _asyncDependenciesReported = true;
-
-                    const string asyncReason = "Async state machine";
-
-                    AsyncResumptionStub resumptionStub = _compilation.TypeSystemContext.GetAsyncResumptionStub(_canonMethod, _compilation.TypeSystemContext.GeneratedAssembly.GetGlobalModuleType());
-                    _dependencies.Add(_compilation.NodeFactory.MethodEntrypoint(resumptionStub), asyncReason);
-
-                    _dependencies.Add(_factory.ConstructedTypeSymbol(_compilation.TypeSystemContext.ContinuationType), asyncReason);
-
-                    DefType asyncHelpers = _compilation.TypeSystemContext.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8);
-
-                    _dependencies.Add(_compilation.GetHelperEntrypoint(ReadyToRunHelper.AllocContinuation), asyncReason);
-                    _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureExecutionContext"u8, null)), asyncReason);
-                    _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureContinuationContext"u8, null)), asyncReason);
-                    _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("RestoreContextsOnSuspension"u8, null)), asyncReason);
-                    _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("FinishSuspensionNoContinuationContext"u8, null)), asyncReason);
-                    _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("FinishSuspensionWithContinuationContext"u8, null)), asyncReason);
-                }
-
                 // If this is the task await pattern, the JIT will first resolve the call to the
                 // async variant, then may switch back to the original if the async variant is just
                 // a thunk (for non-runtime-async methods). Report both variants as dependencies such
@@ -471,7 +474,7 @@ private void ImportCall(ILOpcode opcode, int token)
                 // Don't get async variant of Delegate.Invoke method; the pointed to method is not an async variant either.
                 allowAsyncVariant = allowAsyncVariant && !method.OwningType.IsDelegate;
 
-                if (allowAsyncVariant && MatchTaskAwaitPattern())
+                if (allowAsyncVariant && (_canonMethod.SupportsAsyncVersionCodegen() ? MatchTailCallAwait(method) : MatchTaskAwaitPattern()))
                 {
                     MethodDesc asyncVariantMethod = _factory.TypeSystemContext.GetAsyncVariantMethod(method);
                     MethodDesc asyncVariantRuntimeDeterminedMethod = _factory.TypeSystemContext.GetAsyncVariantMethod(runtimeDeterminedMethod);
@@ -484,6 +487,12 @@ private void ImportCall(ILOpcode opcode, int token)
 
         private void ImportCall(ILOpcode opcode, MethodDesc method, MethodDesc runtimeDeterminedMethod)
         {
+            // Add dependencies on infra to do suspend/resume. We only need to do this once per method scanned.
+            if (!_asyncDependenciesReported && _canonMethod.IsAsyncCall() && method.IsAsyncCall())
+            {
+                RegisterDependenciesOnAsyncCall();
+            }
+
             string reason = null;
             switch (opcode)
             {
@@ -954,6 +963,28 @@ private void ImportCall(ILOpcode opcode, MethodDesc method, MethodDesc runtimeDe
             }
         }
 
+        private void RegisterDependenciesOnAsyncCall()
+        {
+            Debug.Assert(!_asyncDependenciesReported);
+            _asyncDependenciesReported = true;
+
+            const string asyncReason = "Async state machine";
+
+            AsyncResumptionStub resumptionStub = _compilation.TypeSystemContext.GetAsyncResumptionStub(_canonMethod, _compilation.TypeSystemContext.GeneratedAssembly.GetGlobalModuleType());
+            _dependencies.Add(_compilation.NodeFactory.MethodEntrypoint(resumptionStub), asyncReason);
+
+            _dependencies.Add(_factory.ConstructedTypeSymbol(_compilation.TypeSystemContext.ContinuationType), asyncReason);
+
+            DefType asyncHelpers = _compilation.TypeSystemContext.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8);
+
+            _dependencies.Add(_compilation.GetHelperEntrypoint(ReadyToRunHelper.AllocContinuation), asyncReason);
+            _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureExecutionContext"u8, null)), asyncReason);
+            _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureContinuationContext"u8, null)), asyncReason);
+            _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("RestoreContextsOnSuspension"u8, null)), asyncReason);
+            _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("FinishSuspensionNoContinuationContext"u8, null)), asyncReason);
+            _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("FinishSuspensionWithContinuationContext"u8, null)), asyncReason);
+        }
+
         private void ImportLdFtn(int token, ILOpcode opCode)
         {
             ImportCall(opCode, token);
@@ -1754,6 +1785,69 @@ private DefType GetWellKnownType(WellKnownType wellKnownType)
             return _compilation.TypeSystemContext.GetWellKnownType(wellKnownType);
         }
 
+        private void ImportReturn()
+        {
+            if (_wrappingAwaitReported || !_canonMethod.SupportsAsyncVersionCodegen() || _prevMatchedAwaitTailCallRetPostOffset == _currentOffset)
+            {
+                return;
+            }
+
+            _wrappingAwaitReported = true;
+
+            MethodDesc taskReturningMethod = _canonMethod.GetTargetOfAsyncVariant();
+            TypeDesc taskReturnType = taskReturningMethod.Signature.ReturnType;
+            bool isValueTask = taskReturnType.IsValueType;
+
+            CompilerTypeSystemContext context = _compilation.TypeSystemContext;
+            DefType asyncHelpers = context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8);
+
+            MethodDesc runtimeDeterminedCaller = _canonMethod.GetSharedRuntimeFormMethodTarget();
+
+            TypeDesc returnType = runtimeDeterminedCaller.Signature.ReturnType;
+            MethodDesc runtimeDeterminedResult;
+            if (returnType.IsVoid)
+            {
+                TypeDesc parameterType = isValueTask
+                    ? context.SystemModule.GetKnownType("System.Threading.Tasks"u8, "ValueTask"u8)
+                    : context.SystemModule.GetKnownType("System.Threading.Tasks"u8, "Task"u8);
+                MethodSignature signature = new MethodSignature(MethodSignatureFlags.Static, 0, context.GetWellKnownType(WellKnownType.Void), [parameterType]);
+                runtimeDeterminedResult = asyncHelpers.GetKnownMethod("TransparentAwaitWithResult"u8, signature);
+            }
+            else
+            {
+                TypeDesc signatureVariable = context.GetSignatureVariable(0, method: true);
+                TypeDesc parameterType = isValueTask
+                    ? context.SystemModule.GetKnownType("System.Threading.Tasks"u8, "ValueTask`1"u8).MakeInstantiatedType(signatureVariable)
+                    : context.SystemModule.GetKnownType("System.Threading.Tasks"u8, "Task`1"u8).MakeInstantiatedType(signatureVariable);
+                MethodSignature signature = new MethodSignature(MethodSignatureFlags.Static, 1, signatureVariable, [parameterType]);
+                runtimeDeterminedResult = asyncHelpers.GetKnownMethod("TransparentAwaitWithResult"u8, signature).MakeInstantiatedMethod(returnType);
+            }
+
+            MethodDesc targetMethod = runtimeDeterminedResult.GetCanonMethodTarget(CanonicalFormKind.Specific);
+
+            const string reason = "Wrapped Task return";
+
+            if (runtimeDeterminedResult.IsRuntimeDeterminedExactMethod)
+            {
+                _dependencies.Add(GetGenericLookupHelper(ReadyToRunHelperId.MethodDictionary, runtimeDeterminedResult), reason);
+                _dependencies.Add(_factory.CanonicalEntrypoint(targetMethod), reason);
+            }
+            else
+            {
+                if (targetMethod.RequiresInstArg())
+                {
+                    _dependencies.Add(_compilation.NodeFactory.MethodGenericDictionary(runtimeDeterminedResult), reason);
+                }
+
+                _dependencies.Add(GetMethodEntrypoint(targetMethod), reason);
+            }
+
+            if (!_asyncDependenciesReported)
+            {
+                RegisterDependenciesOnAsyncCall();
+            }
+        }
+
         private static void ImportNop() { }
         private static void ImportBreak() { }
         private static void ImportLoadVar(int index, bool argument) { }
@@ -1762,7 +1856,6 @@ private static void ImportAddressOfVar(int index, bool argument) { }
         private static void ImportDup() { }
         private static void ImportPop() { }
         private static void ImportLoadNull() { }
-        private static void ImportReturn() { }
         private static void ImportLoadInt(long value, StackValueKind kind) { }
         private static void ImportLoadFloat(double value) { }
         private static void ImportLoadIndirect(int token) { }
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs
index 1edea14b3f10a8..f046893f12f979 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ExternalReferenceTokenManager.cs
@@ -122,7 +122,7 @@ private void EnsureMethodDefTokensAreAvailableInVersionBubble(MethodDesc methodD
                 AddTokenToMutableModule(ecmaMethod);
                 return;
             }
-            if (methodDesc.HasInstantiation)
+            if (methodDesc.HasInstantiation || methodDesc.OwningType.HasInstantiation)
             {
                 EnsureTypeDefTokensAreAvailableInVersionBubble(methodDesc.GetMethodDefinition().OwningType);
                 foreach (TypeDesc instParam in methodDesc.Instantiation)
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs
index 126e44cebafb3f..08ed688106bc4f 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs
@@ -195,14 +195,6 @@ private static bool NeedsTaskReturningThunk(MethodDesc method)
             return false;
         }
 
-        private static bool NeedsAsyncThunk(MethodDesc method)
-        {
-            Debug.Assert(method.IsTypicalMethodDefinition);
-            if (method is not AsyncMethodVariant)
-                return false;
-            return !method.IsAsync;
-        }
-
         private MethodIL GetMethodILForAsyncMethod(MethodDesc method)
         {
             Debug.Assert(method.IsAsync && method is EcmaMethod);
@@ -245,9 +237,14 @@ public override MethodIL GetMethodIL(MethodDesc method)
                 if (_manifestModuleWrappedMethods.TryGetValue(amv, out var methodIL))
                     return methodIL;
 
-                return NeedsAsyncThunk(amv) ?
-                    AsyncThunkILEmitter.EmitAsyncMethodThunk(amv, amv.Target)
-                    : new AsyncEcmaMethodIL(amv, EcmaMethodIL.Create(amv.Target));
+                MethodIL asyncTargetMethodIL = amv.IsAsync
+                    ? EcmaMethodIL.Create(amv.Target)
+                    : GetMethodIL(amv.Target);
+
+                if (asyncTargetMethodIL == null)
+                    return null;
+
+                return new AsyncMethodIL(amv, asyncTargetMethodIL);
             }
             else if (method is MethodForInstantiatedType || method is InstantiatedMethod)
             {
@@ -380,27 +377,27 @@ public override object GetObject(int token, NotFoundBehavior notFoundBehavior =
             }
         }
 
-        public sealed class AsyncEcmaMethodIL : MethodIL, IEcmaMethodIL
+        public sealed class AsyncMethodIL : MethodIL, IEcmaMethodIL
         {
             private readonly AsyncMethodVariant _variant;
-            private readonly EcmaMethodIL _ecmaIL;
+            private readonly MethodIL _methodIL;
 
-            public AsyncEcmaMethodIL(AsyncMethodVariant variant, EcmaMethodIL ecmaIL)
-                => (_variant, _ecmaIL) = (variant, ecmaIL);
+            public AsyncMethodIL(AsyncMethodVariant variant, MethodIL methodIL)
+                => (_variant, _methodIL) = (variant, methodIL);
 
             // This is the reason we need this class - the method that owns the IL is the variant.
             public override MethodDesc OwningMethod => _variant;
 
-            // Everything else dispatches to EcmaMethodIL
-            public override MethodDebugInformation GetDebugInfo() => _ecmaIL.GetDebugInfo();
-            public override ILExceptionRegion[] GetExceptionRegions() => _ecmaIL.GetExceptionRegions();
-            public override byte[] GetILBytes() => _ecmaIL.GetILBytes();
-            public override LocalVariableDefinition[] GetLocals() => _ecmaIL.GetLocals();
-            public override object GetObject(int token, NotFoundBehavior notFoundBehavior = NotFoundBehavior.Throw) => _ecmaIL.GetObject(token, notFoundBehavior);
-            public override bool IsInitLocals => _ecmaIL.IsInitLocals;
-            public override int MaxStack => _ecmaIL.MaxStack;
+            // Everything else dispatches to the wrapped IL
+            public override MethodDebugInformation GetDebugInfo() => _methodIL.GetDebugInfo();
+            public override ILExceptionRegion[] GetExceptionRegions() => _methodIL.GetExceptionRegions();
+            public override byte[] GetILBytes() => _methodIL.GetILBytes();
+            public override LocalVariableDefinition[] GetLocals() => _methodIL.GetLocals();
+            public override object GetObject(int token, NotFoundBehavior notFoundBehavior = NotFoundBehavior.Throw) => _methodIL.GetObject(token, notFoundBehavior);
+            public override bool IsInitLocals => _methodIL.IsInitLocals;
+            public override int MaxStack => _methodIL.MaxStack;
 
-            public IEcmaModule Module => _ecmaIL.Module;
+            public IEcmaModule Module => ((EcmaType)(_variant.OwningType)).Module;
         }
     }
 }
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs
index 6537dd111808a6..33914e5cc1f62d 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs
@@ -582,6 +582,14 @@ public static bool ShouldSkipCompilation(InstructionSetSupport instructionSetSup
                 return true;
             }
 
+            // Currently crossgen2 does not support compiling async versions of synchronous Task-returning functions.
+            // We would compile a wrapper thunk but that comes with different perf characteristics and diagnostics
+            // that we do not want to deal with.
+            if (methodNeedingCode.SupportsAsyncVersionCodegen())
+            {
+                return true;
+            }
+
             if (ShouldCodeNotBeCompiledIntoFinalImage(instructionSetSupport, methodNeedingCode))
             {
                 return true;
@@ -3436,7 +3444,7 @@ private void reportInliningDecision(CORINFO_METHOD_STRUCT_* inlinerHnd, CORINFO_
                     //    of the build finishes, it will then compute the IL bodies for those methods, then run the compilation again.
 
                     if (needsTokenTranslation && !(methodIL is IMethodTokensAreUseableInCompilation)
-                        && (methodIL is EcmaMethodIL || methodIL is ReadyToRunILProvider.AsyncEcmaMethodIL))
+                        && (methodIL is EcmaMethodIL || methodIL is ReadyToRunILProvider.AsyncMethodIL))
                     {
                         // We may have already acquired the right type of MethodIL here, or be working with a method that is an IL Intrinsic.
                         // Add the typicalMethod (which may be an AsyncMethodVariant) so that
diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs
index 185f3863a3eb8f..efd1a88f79f939 100644
--- a/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs
+++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs
@@ -15,6 +15,7 @@
 
 using ILCompiler;
 using ILCompiler.DependencyAnalysis;
+using System.Runtime.CompilerServices;
 
 #if SUPPORT_JIT
 using MethodCodeNode = Internal.Runtime.JitSupport.JitMethodCodeNode;
@@ -251,6 +252,11 @@ private static CORINFO_RUNTIME_LOOKUP_KIND GetLookupKindFromContextSource(Generi
         }
 
         private void ComputeLookup(ref CORINFO_RESOLVED_TOKEN pResolvedToken, object entity, ReadyToRunHelperId helperId, MethodDesc callerHandle, ref CORINFO_LOOKUP lookup)
+        {
+            ComputeLookup(pResolvedToken.tokenContext != contextFromMethodBeingCompiled(), entity, helperId, callerHandle, ref lookup);
+        }
+
+        private void ComputeLookup(bool isInlining, object entity, ReadyToRunHelperId helperId, MethodDesc callerHandle, ref CORINFO_LOOKUP lookup)
         {
             Debug.Assert(callerHandle != null);
 
@@ -269,7 +275,7 @@ private void ComputeLookup(ref CORINFO_RESOLVED_TOKEN pResolvedToken, object ent
                     // currently do it for an inline. This is not a big issue because ReadyToRun helpers
                     // in optimized code only happen in special build configurations (such as
                     // `-O --noscan` or multimodule build).
-                    if (pResolvedToken.tokenContext != contextFromMethodBeingCompiled())
+                    if (isInlining)
                     {
                         lookup.lookupKind.runtimeLookupKind = CORINFO_RUNTIME_LOOKUP_KIND.CORINFO_LOOKUP_NOT_SUPPORTED;
                         return;
diff --git a/src/coreclr/tools/aot/jitinterface/jitinterface_generated.h b/src/coreclr/tools/aot/jitinterface/jitinterface_generated.h
index 90ec07a3b446bc..247b42cc0bc0b9 100644
--- a/src/coreclr/tools/aot/jitinterface/jitinterface_generated.h
+++ b/src/coreclr/tools/aot/jitinterface/jitinterface_generated.h
@@ -133,6 +133,7 @@ struct JitInterfaceCallbacks
     bool (* runWithSPMIErrorTrap)(void * thisHandle, CorInfoExceptionClass** ppException, ICorJitInfo::errorTrapFunction function, void* parameter);
     void (* getEEInfo)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_EE_INFO* pEEInfoOut);
     void (* getAsyncInfo)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_ASYNC_INFO* pAsyncInfoOut);
+    CORINFO_METHOD_HANDLE (* getAwaitReturnCall)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_METHOD_HANDLE callerHandle, CORINFO_LOOKUP* instArg);
     mdMethodDef (* getMethodDefFromMethod)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_METHOD_HANDLE hMethod);
     size_t (* printMethodName)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_METHOD_HANDLE ftn, char* buffer, size_t bufferSize, size_t* pRequiredBufferSize);
     const char* (* getMethodNameFromMetadata)(void * thisHandle, CorInfoExceptionClass** ppException, CORINFO_METHOD_HANDLE ftn, const char** className, const char** namespaceName, const char** enclosingClassNames, size_t maxEnclosingClassNames);
@@ -1383,6 +1384,16 @@ class JitInterfaceWrapper : public ICorJitInfo
     if (pException != nullptr) throw pException;
 }
 
+    virtual CORINFO_METHOD_HANDLE getAwaitReturnCall(
+          CORINFO_METHOD_HANDLE callerHandle,
+          CORINFO_LOOKUP* instArg)
+{
+    CorInfoExceptionClass* pException = nullptr;
+    CORINFO_METHOD_HANDLE temp = _callbacks->getAwaitReturnCall(_thisHandle, &pException, callerHandle, instArg);
+    if (pException != nullptr) throw pException;
+    return temp;
+}
+
     virtual mdMethodDef getMethodDefFromMethod(
           CORINFO_METHOD_HANDLE hMethod)
 {
diff --git a/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h b/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h
index 85a7a8bf540eed..0ec56054bb0d2f 100644
--- a/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h
+++ b/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h
@@ -30,6 +30,39 @@ struct Agnostic_CORINFO_SIG_INFO
     DWORD     token;
 };
 
+struct Agnostic_CORINFO_CONST_LOOKUP
+{
+    DWORD     accessType;
+    DWORDLONG handle; // actually a union of two pointer sized things
+};
+
+struct Agnostic_CORINFO_LOOKUP_KIND
+{
+    DWORD needsRuntimeLookup;
+    DWORD runtimeLookupKind;
+};
+
+struct Agnostic_CORINFO_RUNTIME_LOOKUP
+{
+    DWORDLONG                     signature;
+    DWORD                         helper;
+    DWORD                         indirections;
+    DWORD                         testForNull;
+    WORD                          sizeOffset;
+    DWORDLONG                     offsets[CORINFO_MAXINDIRECTIONS];
+    DWORD                         indirectFirstOffset;
+    DWORD                         indirectSecondOffset;
+    Agnostic_CORINFO_CONST_LOOKUP helperEntryPoint;
+};
+
+struct Agnostic_CORINFO_LOOKUP
+{
+    Agnostic_CORINFO_LOOKUP_KIND    lookupKind;
+    Agnostic_CORINFO_RUNTIME_LOOKUP runtimeLookup; // This and constLookup actually a union, but with different
+                                                   // layouts.. :-| copy the right one based on lookupKinds value
+    Agnostic_CORINFO_CONST_LOOKUP constLookup;
+};
+
 struct Agnostic_CORINFO_METHOD_INFO
 {
     DWORDLONG                 ftn;
@@ -209,6 +242,12 @@ struct Agnostic_CORINFO_ASYNC_INFO
     DWORDLONG finishSuspensionWithContinuationContextMethHnd;
 };
 
+struct Agnostic_GetAwaitReturnCallResult
+{
+    DWORDLONG methodHnd;
+    Agnostic_CORINFO_LOOKUP instArg;
+};
+
 struct Agnostic_GetOSRInfo
 {
     DWORD index;
@@ -266,45 +305,12 @@ struct Agnostic_CORINFO_HELPER_DESC
     Agnostic_CORINFO_HELPER_ARG args[CORINFO_ACCESS_ALLOWED_MAX_ARGS];
 };
 
-struct Agnostic_CORINFO_CONST_LOOKUP
-{
-    DWORD     accessType;
-    DWORDLONG handle; // actually a union of two pointer sized things
-};
-
 struct Agnostic_GetHelperFtn
 {
     Agnostic_CORINFO_CONST_LOOKUP helperLookup;
     DWORDLONG                     helperMethod;
 };
 
-struct Agnostic_CORINFO_LOOKUP_KIND
-{
-    DWORD needsRuntimeLookup;
-    DWORD runtimeLookupKind;
-};
-
-struct Agnostic_CORINFO_RUNTIME_LOOKUP
-{
-    DWORDLONG                     signature;
-    DWORD                         helper;
-    DWORD                         indirections;
-    DWORD                         testForNull;
-    WORD                          sizeOffset;
-    DWORDLONG                     offsets[CORINFO_MAXINDIRECTIONS];
-    DWORD                         indirectFirstOffset;
-    DWORD                         indirectSecondOffset;
-    Agnostic_CORINFO_CONST_LOOKUP helperEntryPoint;
-};
-
-struct Agnostic_CORINFO_LOOKUP
-{
-    Agnostic_CORINFO_LOOKUP_KIND    lookupKind;
-    Agnostic_CORINFO_RUNTIME_LOOKUP runtimeLookup; // This and constLookup actually a union, but with different
-                                                   // layouts.. :-| copy the right one based on lookupKinds value
-    Agnostic_CORINFO_CONST_LOOKUP constLookup;
-};
-
 struct Agnostic_CORINFO_FIELD_INFO
 {
     DWORD                         fieldAccessor;
diff --git a/src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h b/src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h
index b464feef60b3f6..63cdb605f575d0 100644
--- a/src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h
+++ b/src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h
@@ -79,6 +79,7 @@ LWM(GetSZArrayHelperEnumeratorClass, DWORDLONG, DWORDLONG)
 LWM(GetDelegateCtor, Agnostic_GetDelegateCtorIn, Agnostic_GetDelegateCtorOut)
 LWM(GetEEInfo, DWORD, Agnostic_CORINFO_EE_INFO)
 LWM(GetAsyncInfo, DWORD, Agnostic_CORINFO_ASYNC_INFO)
+LWM(GetAwaitReturnCall, DWORDLONG, Agnostic_GetAwaitReturnCallResult)
 LWM(GetEHinfo, DLD, Agnostic_CORINFO_EH_CLAUSE)
 LWM(GetStaticFieldContent, DLDDD, DD)
 LWM(GetObjectContent, DLDD, DD)
diff --git a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp
index ec6b3b9e1ef4ac..acc61ceeb04d75 100644
--- a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp
+++ b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp
@@ -4516,6 +4516,33 @@ void MethodContext::repGetAsyncInfo(CORINFO_ASYNC_INFO* pAsyncInfoOut)
     DEBUG_REP(dmpGetAsyncInfo(0, value));
 }
 
+void MethodContext::recGetAwaitReturnCall(CORINFO_METHOD_HANDLE callerHnd, CORINFO_LOOKUP* instArg, CORINFO_METHOD_HANDLE methHnd)
+{
+    if (GetAwaitReturnCall == nullptr)
+        GetAwaitReturnCall = new LightWeightMap<DWORDLONG, Agnostic_GetAwaitReturnCallResult>();
+
+    Agnostic_GetAwaitReturnCallResult value;
+    ZeroMemory(&value, sizeof(value));
+    value.methodHnd = CastHandle(methHnd);
+    value.instArg = SpmiRecordsHelper::StoreAgnostic_CORINFO_LOOKUP(instArg);
+
+    GetAwaitReturnCall->Add(CastHandle(callerHnd), value);
+    DEBUG_REC(dmpGetAwaitReturnCall(CastHandle(callerHnd), value));
+}
+void MethodContext::dmpGetAwaitReturnCall(DWORDLONG key, Agnostic_GetAwaitReturnCallResult& value)
+{
+    printf("GetAwaitReturnCall key %016" PRIX64 " value methodHnd-%016" PRIX64 " instArg %s",
+        key,
+        value.methodHnd,
+        SpmiDumpHelper::DumpAgnostic_CORINFO_LOOKUP(value.instArg).c_str());
+}
+CORINFO_METHOD_HANDLE MethodContext::repGetAwaitReturnCall(CORINFO_METHOD_HANDLE callerHnd, CORINFO_LOOKUP* instArg)
+{
+    const Agnostic_GetAwaitReturnCallResult& result = LookupByKeyOrMissNoMessage(GetAwaitReturnCall, CastHandle(callerHnd));
+    *instArg = SpmiRecordsHelper::RestoreCORINFO_LOOKUP(result.instArg);
+    return (CORINFO_METHOD_HANDLE)result.methodHnd;
+}
+
 void MethodContext::recGetGSCookie(GSCookie* pCookieVal, GSCookie** ppCookieVal)
 {
     if (GetGSCookie == nullptr)
diff --git a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h
index 449a18a91caeb5..8956394957e6a1 100644
--- a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h
+++ b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h
@@ -572,6 +572,10 @@ class MethodContext
     void dmpGetAsyncInfo(DWORD key, const Agnostic_CORINFO_ASYNC_INFO& value);
     void repGetAsyncInfo(CORINFO_ASYNC_INFO* pAsyncInfoOut);
 
+    void recGetAwaitReturnCall(CORINFO_METHOD_HANDLE callerHnd, CORINFO_LOOKUP* instArg, CORINFO_METHOD_HANDLE methHnd);
+    void dmpGetAwaitReturnCall(DWORDLONG key, Agnostic_GetAwaitReturnCallResult& value);
+    CORINFO_METHOD_HANDLE repGetAwaitReturnCall(CORINFO_METHOD_HANDLE callerHnd, CORINFO_LOOKUP* instArg);
+
     void recGetGSCookie(GSCookie* pCookieVal, GSCookie** ppCookieVal);
     void dmpGetGSCookie(DWORD key, DLDL value);
     void repGetGSCookie(GSCookie* pCookieVal, GSCookie** ppCookieVal);
@@ -1222,6 +1226,7 @@ enum mcPackets
     Packet_GetWasmTypeSymbol = 235,
     Packet_GetWasmLowering = 236,
     Packet_GetAsyncOtherVariant = 237,
+    Packet_GetAwaitReturnCall = 238,
 };
 
 void SetDebugDumpVariables();
diff --git a/src/coreclr/tools/superpmi/superpmi-shared/spmirecordhelper.h b/src/coreclr/tools/superpmi/superpmi-shared/spmirecordhelper.h
index 1a460b05b76e70..b2df4eded655e6 100644
--- a/src/coreclr/tools/superpmi/superpmi-shared/spmirecordhelper.h
+++ b/src/coreclr/tools/superpmi/superpmi-shared/spmirecordhelper.h
@@ -97,7 +97,7 @@ class SpmiRecordsHelper
     static Agnostic_CORINFO_LOOKUP_KIND CreateAgnostic_CORINFO_LOOKUP_KIND(
         const CORINFO_LOOKUP_KIND* pGenericLookupKind);
 
-    static CORINFO_LOOKUP_KIND RestoreCORINFO_LOOKUP_KIND(Agnostic_CORINFO_LOOKUP_KIND& lookupKind);
+    static CORINFO_LOOKUP_KIND RestoreCORINFO_LOOKUP_KIND(const Agnostic_CORINFO_LOOKUP_KIND& lookupKind);
 
     static Agnostic_CORINFO_CONST_LOOKUP StoreAgnostic_CORINFO_CONST_LOOKUP(
         const CORINFO_CONST_LOOKUP* pLookup);
@@ -107,11 +107,11 @@ class SpmiRecordsHelper
     static Agnostic_CORINFO_RUNTIME_LOOKUP StoreAgnostic_CORINFO_RUNTIME_LOOKUP(
         CORINFO_RUNTIME_LOOKUP* pLookup);
 
-    static CORINFO_RUNTIME_LOOKUP RestoreCORINFO_RUNTIME_LOOKUP(Agnostic_CORINFO_RUNTIME_LOOKUP& Lookup);
+    static CORINFO_RUNTIME_LOOKUP RestoreCORINFO_RUNTIME_LOOKUP(const Agnostic_CORINFO_RUNTIME_LOOKUP& lookup);
 
     static Agnostic_CORINFO_LOOKUP StoreAgnostic_CORINFO_LOOKUP(CORINFO_LOOKUP* pLookup);
 
-    static CORINFO_LOOKUP RestoreCORINFO_LOOKUP(Agnostic_CORINFO_LOOKUP& agnosticLookup);
+    static CORINFO_LOOKUP RestoreCORINFO_LOOKUP(const Agnostic_CORINFO_LOOKUP& agnosticLookup);
 
     static Agnostic_CORINFO_TYPE_LAYOUT_NODE StoreAgnostic_CORINFO_TYPE_LAYOUT_NODE(const CORINFO_TYPE_LAYOUT_NODE& node);
     static CORINFO_TYPE_LAYOUT_NODE RestoreCORINFO_TYPE_LAYOUT_NODE(const Agnostic_CORINFO_TYPE_LAYOUT_NODE& node);
@@ -454,7 +454,7 @@ inline Agnostic_CORINFO_LOOKUP_KIND SpmiRecordsHelper::CreateAgnostic_CORINFO_LO
 }
 
 inline CORINFO_LOOKUP_KIND SpmiRecordsHelper::RestoreCORINFO_LOOKUP_KIND(
-    Agnostic_CORINFO_LOOKUP_KIND& lookupKind)
+    const Agnostic_CORINFO_LOOKUP_KIND& lookupKind)
 {
     CORINFO_LOOKUP_KIND genericLookupKind;
     genericLookupKind.needsRuntimeLookup = lookupKind.needsRuntimeLookup != 0;
@@ -500,7 +500,7 @@ inline Agnostic_CORINFO_RUNTIME_LOOKUP SpmiRecordsHelper::StoreAgnostic_CORINFO_
 }
 
 inline CORINFO_RUNTIME_LOOKUP SpmiRecordsHelper::RestoreCORINFO_RUNTIME_LOOKUP(
-    Agnostic_CORINFO_RUNTIME_LOOKUP& lookup)
+    const Agnostic_CORINFO_RUNTIME_LOOKUP& lookup)
 {
     CORINFO_RUNTIME_LOOKUP runtimeLookup;
     runtimeLookup.signature            = (LPVOID)lookup.signature;
@@ -532,7 +532,7 @@ inline Agnostic_CORINFO_LOOKUP SpmiRecordsHelper::StoreAgnostic_CORINFO_LOOKUP(C
     return lookup;
 }
 
-inline CORINFO_LOOKUP SpmiRecordsHelper::RestoreCORINFO_LOOKUP(Agnostic_CORINFO_LOOKUP& agnosticLookup)
+inline CORINFO_LOOKUP SpmiRecordsHelper::RestoreCORINFO_LOOKUP(const Agnostic_CORINFO_LOOKUP& agnosticLookup)
 {
     CORINFO_LOOKUP lookup;
     ZeroMemory(&lookup, sizeof(lookup));
diff --git a/src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp b/src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp
index 6d0cadcd5616ae..9302dc77bc403f 100644
--- a/src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp
+++ b/src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp
@@ -1404,6 +1404,14 @@ void interceptor_ICJI::getAsyncInfo(CORINFO_ASYNC_INFO* pAsyncInfo)
     mc->recGetAsyncInfo(pAsyncInfo);
 }
 
+CORINFO_METHOD_HANDLE interceptor_ICJI::getAwaitReturnCall(CORINFO_METHOD_HANDLE callerHandle, CORINFO_LOOKUP* instArg)
+{
+    mc->cr->AddCall("getAwaitReturnCall");
+    CORINFO_METHOD_HANDLE result = original_ICorJitInfo->getAwaitReturnCall(callerHandle, instArg);
+    mc->recGetAwaitReturnCall(callerHandle, instArg, result);
+    return result;
+}
+
 /*********************************************************************************/
 //
 // Diagnostic methods
diff --git a/src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp b/src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp
index 874083bd15b940..70de01f8704eb1 100644
--- a/src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp
+++ b/src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp
@@ -986,6 +986,14 @@ void interceptor_ICJI::getAsyncInfo(
     original_ICorJitInfo->getAsyncInfo(pAsyncInfoOut);
 }
 
+CORINFO_METHOD_HANDLE interceptor_ICJI::getAwaitReturnCall(
+          CORINFO_METHOD_HANDLE callerHandle,
+          CORINFO_LOOKUP* instArg)
+{
+    mcs->AddCall("getAwaitReturnCall");
+    return original_ICorJitInfo->getAwaitReturnCall(callerHandle, instArg);
+}
+
 mdMethodDef interceptor_ICJI::getMethodDefFromMethod(
           CORINFO_METHOD_HANDLE hMethod)
 {
diff --git a/src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp b/src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp
index b870f0f947f2f6..f301b3d057987e 100644
--- a/src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp
+++ b/src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp
@@ -864,6 +864,13 @@ void interceptor_ICJI::getAsyncInfo(
     original_ICorJitInfo->getAsyncInfo(pAsyncInfoOut);
 }
 
+CORINFO_METHOD_HANDLE interceptor_ICJI::getAwaitReturnCall(
+          CORINFO_METHOD_HANDLE callerHandle,
+          CORINFO_LOOKUP* instArg)
+{
+    return original_ICorJitInfo->getAwaitReturnCall(callerHandle, instArg);
+}
+
 mdMethodDef interceptor_ICJI::getMethodDefFromMethod(
           CORINFO_METHOD_HANDLE hMethod)
 {
diff --git a/src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp b/src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp
index 93262fd2279b01..abe084f498f809 100644
--- a/src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp
+++ b/src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp
@@ -1212,6 +1212,12 @@ void MyICJI::getAsyncInfo(CORINFO_ASYNC_INFO* pAsyncInfo)
     jitInstance->mc->repGetAsyncInfo(pAsyncInfo);
 }
 
+CORINFO_METHOD_HANDLE MyICJI::getAwaitReturnCall(CORINFO_METHOD_HANDLE callerHandle, CORINFO_LOOKUP* instArg)
+{
+    jitInstance->mc->cr->AddCall("getAwaitReturnCall");
+    return jitInstance->mc->repGetAwaitReturnCall(callerHandle, instArg);
+}
+
 /*********************************************************************************/
 //
 // Diagnostic methods
diff --git a/src/coreclr/vm/asyncthunks.cpp b/src/coreclr/vm/asyncthunks.cpp
index 0bc85a906c7c71..4f976e561d873a 100644
--- a/src/coreclr/vm/asyncthunks.cpp
+++ b/src/coreclr/vm/asyncthunks.cpp
@@ -33,17 +33,10 @@ bool MethodDesc::TryGenerateAsyncThunk(DynamicResolver** resolver, COR_ILMETHOD_
     }
     else
     {
-        if (!IsReturnDroppingThunk())
-        {
-            // an async thunk is implemented in terms of non-async variant
-            pAsyncOtherVariant = this->GetOrdinaryVariant();
-        }
-        else
-        {
-            // this is a special void-returning async variant that calls
-            // the normal async variant and drops the result
-            pAsyncOtherVariant = this->GetAsyncVariant();
-        }
+        _ASSERTE(IsReturnDroppingThunk());
+        // this is a special void-returning async variant that calls
+        // the normal async variant and drops the result
+        pAsyncOtherVariant = this->GetAsyncVariant();
     }
 
     _ASSERTE(!IsWrapperStub() && !pAsyncOtherVariant->IsWrapperStub());
@@ -64,14 +57,8 @@ bool MethodDesc::TryGenerateAsyncThunk(DynamicResolver** resolver, COR_ILMETHOD_
     }
     else
     {
-        if (IsReturnDroppingThunk())
-        {
-            EmitReturnDroppingThunk(pAsyncOtherVariant, msig, &sl);
-        }
-        else
-        {
-            EmitAsyncMethodThunk(pAsyncOtherVariant, msig, &sl);
-        }
+        _ASSERTE(IsReturnDroppingThunk());
+        EmitReturnDroppingThunk(pAsyncOtherVariant, msig, &sl);
     }
 
     NewHolder<ILStubResolver> ilResolver = new ILStubResolver();
@@ -377,44 +364,6 @@ int MethodDesc::GetTokenForGenericMethodCallWithAsyncReturnType(ILCodeStream* pC
     return pCode->GetToken(md, mdTokenNil, methodSigToken);
 }
 
-// Given a method Bar<T>.Foo, return a MethodSpec token for Bar<T>.Foo
-// instantiated with the result type from the current async method's return
-// type. For example, if "this" represents Task<List<T>> Foo<T>(), and
-// "md" is TaskAwaiter<T>.GetResult(), this returns a MethodSpec representing
-// TaskAwaiter<List<T>>.GetResult().
-int MethodDesc::GetTokenForGenericTypeMethodCallWithAsyncReturnType(ILCodeStream* pCode, MethodDesc* md)
-{
-    if (!md->HasClassOrMethodInstantiation())
-    {
-        return pCode->GetToken(md);
-    }
-
-    // We never get here with a method instantiation currently.
-    _ASSERTE(!md->HasMethodInstantiation());
-
-    SigBuilder typeSigBuilder;
-    typeSigBuilder.AppendData(ELEMENT_TYPE_GENERICINST);
-    typeSigBuilder.AppendData(ELEMENT_TYPE_INTERNAL);
-    // TODO: (async) Encoding potentially shared method tables in
-    // signatures of tokens seems odd, but this hits assert
-    // with the typical method table.
-    typeSigBuilder.AppendPointer(md->GetMethodTable());
-    typeSigBuilder.AppendData(1);
-
-    SigPointer retTypeSig = GetAsyncThunkResultTypeSig();
-    PCCOR_SIGNATURE retTypeSigRaw;
-    uint32_t retTypeSigLen;
-    retTypeSig.GetSignature(&retTypeSigRaw, &retTypeSigLen);
-
-    typeSigBuilder.AppendBlob((const PVOID)retTypeSigRaw, retTypeSigLen);
-
-    DWORD typeSigLen;
-    PCCOR_SIGNATURE typeSig = (PCCOR_SIGNATURE)typeSigBuilder.GetSignature(&typeSigLen);
-    int typeSigToken = pCode->GetSigToken(typeSig, typeSigLen);
-
-    return pCode->GetToken(md, typeSigToken);
-}
-
 int MethodDesc::GetTokenForThunkTarget(ILCodeStream* pCode, MethodDesc* md)
 {
     int token;
@@ -475,180 +424,6 @@ int MethodDesc::GetTokenForThunkTarget(ILCodeStream* pCode, MethodDesc* md)
     return token;
 }
 
-// Provided a Task-returning method, emits an async wrapper.
-// The emitted code matches method EmitAsyncMethodThunk in the Managed Type System.
-void MethodDesc::EmitAsyncMethodThunk(MethodDesc* pTaskReturningVariant, MetaSig& msig, ILStubLinker* pSL)
-{
-    _ASSERTE(!pTaskReturningVariant->IsAsyncThunkMethod());
-    _ASSERTE(!pTaskReturningVariant->IsVoid());
-
-    // Implement IL that is effectively the following:
-    // {
-    //    Task task = other(arg);
-    //    if (!task.IsCompleted)
-    //    {
-    //        TailAwait();
-    //        return AsyncHelpers.TransparentAwait(task);
-    //    }
-    //    return AsyncHelpers.CompletedTaskResult(task);
-    // }
-
-    // For ValueTask:
-
-    // {
-    //    ValueTask vt = other(arg);
-    //    if (!vt.IsCompleted)
-    //    {
-    //        TailAwait();
-    //        return AsyncHelpers.TransparentAwaitValueTask(vt);
-    //    }
-
-    //    return vt.Result/vt.ThrowIfCompletedUnsuccessfully();
-    // }
-    ILCodeStream* pCode = pSL->NewCodeStream(ILStubLinker::kDispatch);
-
-    int token = GetTokenForThunkTarget(pCode, pTaskReturningVariant);
-
-    DWORD localArg = 0;
-    if (msig.HasThis())
-    {
-        pCode->EmitLDARG(localArg++);
-    }
-    for (UINT iArg = 0; iArg < msig.NumFixedArgs(); iArg++)
-    {
-        pCode->EmitLDARG(localArg++);
-    }
-
-    // other(arg)
-    if (pTaskReturningVariant->IsAbstract())
-    {
-        _ASSERTE(pTaskReturningVariant->IsCLRToCOMCall());
-        pCode->EmitCALLVIRT(token, localArg, 1);
-    }
-    else
-    {
-        pCode->EmitCALL(token, localArg, 1);
-    }
-
-    TypeHandle thLogicalRetType = msig.GetRetTypeHandleThrowing();
-    if (IsAsyncVariantForValueTaskReturningMethod())
-    {
-        MethodTable* pMTValueTask;
-        int isCompletedToken;
-        int completionResultToken;
-        int transparentAwaitValueTaskToken;
-        if (msig.IsReturnTypeVoid())
-        {
-            pMTValueTask = CoreLibBinder::GetClass(CLASS__VALUETASK);
-
-            MethodDesc* pMDValueTaskIsCompleted = CoreLibBinder::GetMethod(METHOD__VALUETASK__GET_ISCOMPLETED);
-            MethodDesc* pMDCompletionResult = CoreLibBinder::GetMethod(METHOD__VALUETASK__THROW_IF_COMPLETED_UNSUCCESSFULLY);
-            MethodDesc* pMDTransparentAwaitValueTask = CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__TRANSPARENT_AWAIT_VALUE_TASK);
-
-            isCompletedToken = pCode->GetToken(pMDValueTaskIsCompleted);
-            completionResultToken = pCode->GetToken(pMDCompletionResult);
-            transparentAwaitValueTaskToken = pCode->GetToken(pMDTransparentAwaitValueTask);
-        }
-        else
-        {
-            MethodTable* pMTValueTaskOpen = CoreLibBinder::GetClass(CLASS__VALUETASK_1);
-            pMTValueTask = ClassLoader::LoadGenericInstantiationThrowing(pMTValueTaskOpen->GetModule(), pMTValueTaskOpen->GetCl(), Instantiation(&thLogicalRetType, 1)).GetMethodTable();
-
-            MethodDesc* pMDValueTaskIsCompleted = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__GET_ISCOMPLETED);
-            MethodDesc* pMDCompletionResult = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__GET_RESULT);
-            MethodDesc* pMDTransparentAwaitValueTask = CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__TRANSPARENT_AWAIT_VALUE_TASK_OF_T);
-
-            pMDValueTaskIsCompleted = FindOrCreateAssociatedMethodDesc(pMDValueTaskIsCompleted, pMTValueTask, FALSE, Instantiation(), FALSE);
-            pMDCompletionResult = FindOrCreateAssociatedMethodDesc(pMDCompletionResult, pMTValueTask, FALSE, Instantiation(), FALSE);
-            pMDTransparentAwaitValueTask = FindOrCreateAssociatedMethodDesc(pMDTransparentAwaitValueTask, pMDTransparentAwaitValueTask->GetMethodTable(), FALSE, Instantiation(&thLogicalRetType, 1), FALSE);
-
-            isCompletedToken = GetTokenForGenericTypeMethodCallWithAsyncReturnType(pCode, pMDValueTaskIsCompleted);
-            completionResultToken = GetTokenForGenericTypeMethodCallWithAsyncReturnType(pCode, pMDCompletionResult);
-            transparentAwaitValueTaskToken = GetTokenForGenericMethodCallWithAsyncReturnType(pCode, pMDTransparentAwaitValueTask);
-        }
-
-        LocalDesc valueTaskLocalDesc(pMTValueTask);
-        DWORD valueTaskLocal = pCode->NewLocal(valueTaskLocalDesc);
-        ILCodeLabel* valueTaskCompletedLabel = pCode->NewCodeLabel();
-
-        // Store value task returned by call to actual user func
-        pCode->EmitSTLOC(valueTaskLocal);
-        pCode->EmitLDLOCA(valueTaskLocal);
-
-        // Was it already completed?
-        pCode->EmitCALL(isCompletedToken, 1, 1);
-        pCode->EmitBRTRUE(valueTaskCompletedLabel);
-
-        // No, tail await to TransparentAwaitValueTask
-        pCode->EmitLDLOC(valueTaskLocal);
-        pCode->EmitCALL(METHOD__ASYNC_HELPERS__TAIL_AWAIT, 0, 0);
-        pCode->EmitCALL(transparentAwaitValueTaskToken, 1, msig.IsReturnTypeVoid() ? 0 : 1);
-        pCode->EmitRET();
-
-        // Yes, just get the result
-        pCode->EmitLabel(valueTaskCompletedLabel);
-        pCode->EmitLDLOCA(valueTaskLocal);
-        pCode->EmitCALL(completionResultToken, 1, msig.IsReturnTypeVoid() ? 0 : 1);
-        pCode->EmitRET();
-    }
-    else
-    {
-        MethodTable* pMTTask;
-
-        int completedTaskResultToken;
-        int transparentAwaitToken;
-
-        if (msig.IsReturnTypeVoid())
-        {
-            pMTTask = CoreLibBinder::GetClass(CLASS__TASK);
-
-            MethodDesc* pMDCompletedTask = CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__COMPLETED_TASK);
-            MethodDesc* pMDTransparentAwait = CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__TRANSPARENT_AWAIT);
-
-            completedTaskResultToken = pCode->GetToken(pMDCompletedTask);
-            transparentAwaitToken = pCode->GetToken(pMDTransparentAwait);
-        }
-        else
-        {
-            MethodTable* pMTTaskOpen = CoreLibBinder::GetClass(CLASS__TASK_1);
-            pMTTask = ClassLoader::LoadGenericInstantiationThrowing(pMTTaskOpen->GetModule(), pMTTaskOpen->GetCl(), Instantiation(&thLogicalRetType, 1)).GetMethodTable();
-
-            MethodDesc* pMDCompletedTaskResult = CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__COMPLETED_TASK_RESULT);
-            MethodDesc* pMDTransparentAwait = CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__TRANSPARENT_AWAIT_OF_T);
-
-            pMDCompletedTaskResult = FindOrCreateAssociatedMethodDesc(pMDCompletedTaskResult, pMDCompletedTaskResult->GetMethodTable(), FALSE, Instantiation(&thLogicalRetType, 1), FALSE);
-            pMDTransparentAwait = FindOrCreateAssociatedMethodDesc(pMDTransparentAwait, pMDTransparentAwait->GetMethodTable(), FALSE, Instantiation(&thLogicalRetType, 1), FALSE);
-
-            completedTaskResultToken = GetTokenForGenericMethodCallWithAsyncReturnType(pCode, pMDCompletedTaskResult);
-            transparentAwaitToken = GetTokenForGenericMethodCallWithAsyncReturnType(pCode, pMDTransparentAwait);
-        }
-
-        LocalDesc taskLocalDesc(pMTTask);
-        DWORD taskLocal = pCode->NewLocal(taskLocalDesc);
-        ILCodeLabel* pGetResultLabel = pCode->NewCodeLabel();
-
-        // Store task returned by actual user func
-        pCode->EmitSTLOC(taskLocal);
-
-        // Did it already complete?
-        pCode->EmitLDLOC(taskLocal);
-        pCode->EmitCALL(METHOD__TASK__GET_ISCOMPLETED, 1, 1);
-        pCode->EmitBRTRUE(pGetResultLabel);
-
-        // No, so tail await to TransparentAwait
-        pCode->EmitLDLOC(taskLocal);
-        pCode->EmitCALL(METHOD__ASYNC_HELPERS__TAIL_AWAIT, 0, 0);
-        pCode->EmitCALL(transparentAwaitToken, 1, msig.IsReturnTypeVoid() ? 0 : 1);
-        pCode->EmitRET();
-
-        // Yes, so just get the result
-        pCode->EmitLabel(pGetResultLabel);
-        pCode->EmitLDLOC(taskLocal);
-        pCode->EmitCALL(completedTaskResultToken, 1, msig.IsReturnTypeVoid() ? 0 : 1);
-        pCode->EmitRET();
-    }
-}
-
 // Provided an async variant, emits an async wrapper that drops the returned value.
 // Used in the covariant return scenario.
 // The emitted code matches EmitReturnDroppingThunk in the Managed Type System.
diff --git a/src/coreclr/vm/corelib.h b/src/coreclr/vm/corelib.h
index 5d453720aa0ebb..46ecba5177bf69 100644
--- a/src/coreclr/vm/corelib.h
+++ b/src/coreclr/vm/corelib.h
@@ -721,6 +721,10 @@ DEFINE_METHOD(ASYNC_HELPERS,      TRANSPARENT_AWAIT,            TransparentAwait
 DEFINE_METHOD(ASYNC_HELPERS,      TRANSPARENT_AWAIT_OF_T,       TransparentAwaitOfT, NoSig)
 DEFINE_METHOD(ASYNC_HELPERS,      TRANSPARENT_AWAIT_VALUE_TASK,   TransparentAwaitValueTask, NoSig)
 DEFINE_METHOD(ASYNC_HELPERS,      TRANSPARENT_AWAIT_VALUE_TASK_OF_T, TransparentAwaitValueTaskOfT, NoSig)
+DEFINE_METHOD(ASYNC_HELPERS,      TRANSPARENT_AWAIT_TASK_WITH_RESULT,                TransparentAwaitWithResult, SM_Task_RetVoid)
+DEFINE_METHOD(ASYNC_HELPERS,      TRANSPARENT_AWAIT_VALUETASK_WITH_RESULT,           TransparentAwaitWithResult, SM_ValueTask_RetVoid)
+DEFINE_METHOD(ASYNC_HELPERS,      TRANSPARENT_AWAIT_TASK_OF_T_WITH_RESULT,           TransparentAwaitWithResult, GM_TaskOfT_RetT)
+DEFINE_METHOD(ASYNC_HELPERS,      TRANSPARENT_AWAIT_VALUETASK_OF_T_WITH_RESULT,      TransparentAwaitWithResult, GM_ValueTaskOfT_RetT)
 DEFINE_METHOD(ASYNC_HELPERS,      COMPLETED_TASK_RESULT,     CompletedTaskResult, NoSig)
 DEFINE_METHOD(ASYNC_HELPERS,      COMPLETED_TASK,            CompletedTask, NoSig)
 DEFINE_METHOD(ASYNC_HELPERS,      CAPTURE_EXECUTION_CONTEXT, CaptureExecutionContext, NoSig)
diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp
index c0ec22f3e75a8f..c31046d5b1432d 100644
--- a/src/coreclr/vm/jitinterface.cpp
+++ b/src/coreclr/vm/jitinterface.cpp
@@ -1085,7 +1085,9 @@ void CEEInfo::resolveToken(/* IN, OUT */ CORINFO_RESOLVED_TOKEN * pResolvedToken
                 // in rare cases a method that returns Task is not actually TaskReturning (i.e. returns T).
                 // we cannot resolve to an Async variant in such case.
                 // return NULL, so that caller would re-resolve as a regular method call
-                pMD = pMD->ReturnsTaskOrValueTask() ? pMD->GetAsyncVariant(/*allowInstParam*/FALSE) : NULL;
+                // For COM-import interface calls we do not have an async variant of the COM interop stub, and
+                // in any case there would be no benefit of creating one.
+                pMD = pMD->ReturnsTaskOrValueTask() && !pMD->GetClass()->IsComImport() ? pMD->GetAsyncVariant(/*allowInstParam*/FALSE) : NULL;
             }
             break;
 
@@ -1310,7 +1312,7 @@ uint32_t CEEInfo::getThreadLocalFieldInfo (CORINFO_FIELD_HANDLE  field, bool isG
         typeIndex = MethodTableAuxiliaryData::GetThreadStaticsInfo(pMT->GetAuxiliaryData())->NonGCTlsIndex.GetIndexOffset();
     }
 
-    assert(typeIndex != TypeIDProvider::INVALID_TYPE_ID);
+    _ASSERTE(typeIndex != TypeIDProvider::INVALID_TYPE_ID);
 
     EE_TO_JIT_TRANSITION();
     return typeIndex;
@@ -3324,10 +3326,22 @@ void CEEInfo::ComputeRuntimeLookupForSharedGenericToken(DictionaryEntryKind entr
         _ASSERTE(false);
     }
 
+    FinishComputeRuntimeLookup(sigBuilder, pCallerMD, pResultLookup);
+}
+
+void CEEInfo::FinishComputeRuntimeLookup(
+    SigBuilder& sigBuilder,
+    MethodDesc* pCallerMD,
+    CORINFO_LOOKUP* pResultLookup)
+{
+    CORINFO_RUNTIME_LOOKUP* pResult = &pResultLookup->runtimeLookup;
     DictionaryEntrySignatureSource signatureSource = FromJIT;
 
     WORD slot;
 
+    MethodDesc* pContextMD = pCallerMD;
+    MethodTable* pContextMT = pCallerMD->GetMethodTable();
+
     // It's a method dictionary lookup
     if (pResultLookup->lookupKind.runtimeLookupKind == CORINFO_LOOKUP_METHODPARAM)
     {
@@ -7527,10 +7541,14 @@ COR_ILMETHOD_DECODER* CEEInfo::getMethodInfoWorker(
     /* Grab information from the IL header */
     SigPointer localSig{};
 
+    // For async versions we get the IL of the ordinary variant and pass the
+    // JIT a flag indicating that.
+    MethodDesc* ilFtn = ftn->SupportsAsyncVersionCodegen() ? ftn->GetOrdinaryVariant() : ftn;
+
     MethodInfoWorkerContext cxt{ ftn, header };
 
     TransientMethodDetails* detailsMaybe = NULL;
-    if (FindTransientMethodDetails(ftn, &detailsMaybe))
+    if (FindTransientMethodDetails(ilFtn, &detailsMaybe))
     {
         cxt.UpdateWith(*detailsMaybe);
         scopeHnd = cxt.CreateScopeHandle();
@@ -7589,9 +7607,9 @@ COR_ILMETHOD_DECODER* CEEInfo::getMethodInfoWorker(
             localSig = SigPointer{ cxt.Header->LocalVarSig, cxt.Header->cbLocalVarSig };
         }
     }
-    else if (ftn->IsDynamicMethod())
+    else if (ilFtn->IsDynamicMethod())
     {
-        DynamicResolver* pResolver = ftn->AsDynamicMethodDesc()->GetResolver();
+        DynamicResolver* pResolver = ilFtn->AsDynamicMethodDesc()->GetResolver();
         scopeHnd = MakeDynamicScope(pResolver);
 
         unsigned int EHCount;
@@ -7602,7 +7620,7 @@ COR_ILMETHOD_DECODER* CEEInfo::getMethodInfoWorker(
         methInfo->EHcount = (unsigned short)EHCount;
         localSig = pResolver->GetLocalSig();
     }
-    else if (ftn->TryGenerateTransientILImplementation(&cxt.TransientResolver, &cxt.Header))
+    else if (ilFtn->TryGenerateTransientILImplementation(&cxt.TransientResolver, &cxt.Header))
     {
         scopeHnd = cxt.CreateScopeHandle();
 
@@ -7612,7 +7630,7 @@ COR_ILMETHOD_DECODER* CEEInfo::getMethodInfoWorker(
     }
     else
     {
-        _ASSERTE(!ftn->IsIntrinsic() && "Non-implementable intrinsic methods should have a throwing body");
+        _ASSERTE(!ilFtn->IsIntrinsic() && "Non-implementable intrinsic methods should have a throwing body");
         ThrowHR(COR_E_BADIMAGEFORMAT);
     }
 
@@ -7622,7 +7640,9 @@ COR_ILMETHOD_DECODER* CEEInfo::getMethodInfoWorker(
                     ((ftn->AcquiresInstMethodTableFromThis() ? CORINFO_GENERICS_CTXT_FROM_THIS : 0) |
                         (ftn->RequiresInstMethodTableArg() ? CORINFO_GENERICS_CTXT_FROM_METHODTABLE : 0) |
                         (ftn->RequiresInstMethodDescArg() ? CORINFO_GENERICS_CTXT_FROM_METHODDESC : 0) |
-                        (ftn->RequiresAsyncContextSaveAndRestore() ? CORINFO_ASYNC_SAVE_CONTEXTS : 0)));
+                        (ftn->RequiresAsyncContextSaveAndRestore() ? CORINFO_ASYNC_SAVE_CONTEXTS : 0) |
+                        (ilFtn != ftn ? CORINFO_ASYNC_VERSION : 0)));
+
 
     if (methInfo->options & CORINFO_GENERICS_CTXT_MASK)
     {
@@ -7693,7 +7713,7 @@ COR_ILMETHOD_DECODER* CEEInfo::getMethodInfoWorker(
         CONV_TO_JITSIG_FLAGS_LOCALSIG,
         &methInfo->locals);
 
-    COR_ILMETHOD_DECODER* ilHeader = cxt.Header;
+    COR_ILMETHOD_DECODER* pILHeader = cxt.Header;
 
     // If we have transient method details we need to handle
     // the lifetime of the details.
@@ -7709,7 +7729,7 @@ COR_ILMETHOD_DECODER* CEEInfo::getMethodInfoWorker(
         cxt.UpdateWith({});
     }
 
-    return ilHeader;
+    return pILHeader;
 } // getMethodInfoWorker
 
 //---------------------------------------------------------------------------------------
@@ -8421,7 +8441,6 @@ void CEEInfo::reportTailCallDecision (CORINFO_METHOD_HANDLE callerHnd,
 }
 
 static void getEHinfoHelper(
-    CORINFO_METHOD_HANDLE   ftnHnd,
     unsigned                EHnumber,
     CORINFO_EH_CLAUSE*      clause,
     COR_ILMETHOD_DECODER*   pILHeader)
@@ -8470,7 +8489,7 @@ void CEEInfo::getEHinfo(
     else
     {
         COR_ILMETHOD_DECODER header(ftn->GetILHeader(), ftn->GetMDImport(), NULL);
-        getEHinfoHelper(ftnHnd, EHnumber, clause, &header);
+        getEHinfoHelper(EHnumber, clause, &header);
     }
 
     EE_TO_JIT_TRANSITION();
@@ -8999,7 +9018,7 @@ CORINFO_METHOD_HANDLE CEEInfo::getAsyncOtherVariant(
     MethodDesc* pMD = GetMethod(ftn);
     MethodDesc* pAsyncOtherVariant = NULL;
 
-    if (pMD->ReturnsTaskOrValueTask())
+    if (pMD->ReturnsTaskOrValueTask() && !pMD->GetClass()->IsComImport())
     {
          pAsyncOtherVariant = pMD->GetAsyncVariant();
     }
@@ -10378,6 +10397,139 @@ void CEEInfo::getAsyncInfo(CORINFO_ASYNC_INFO* pAsyncInfoOut)
     EE_TO_JIT_TRANSITION();
 }
 
+CORINFO_METHOD_HANDLE CEEInfo::getAwaitReturnCall(CORINFO_METHOD_HANDLE callerHandle, CORINFO_LOOKUP* instArg)
+{
+    CONTRACTL {
+        THROWS;
+        GC_TRIGGERS;
+        MODE_PREEMPTIVE;
+    } CONTRACTL_END;
+
+    MethodDesc* pMD = NULL;
+
+    JIT_TO_EE_TRANSITION();
+
+    instArg->lookupKind.needsRuntimeLookup = false;
+    instArg->constLookup.accessType = IAT_VALUE;
+    instArg->constLookup.addr = NULL;
+
+    MethodDesc* pCallerMD = GetMethod(callerHandle);
+
+    _ASSERTE(pCallerMD->IsAsyncVariantMethod() && pCallerMD->IsAsyncThunkMethod());
+
+    MetaSig sig(pCallerMD);
+    TypeHandle retType = sig.GetRetTypeHandleThrowing();
+    MethodDesc* pTypicalAwaitMD;
+
+    if (pCallerMD->IsAsyncVariantForValueTaskReturningMethod())
+    {
+        if (sig.IsReturnTypeVoid())
+        {
+            pTypicalAwaitMD = pMD = CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__TRANSPARENT_AWAIT_VALUETASK_WITH_RESULT);
+        }
+        else
+        {
+            pTypicalAwaitMD = CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__TRANSPARENT_AWAIT_VALUETASK_OF_T_WITH_RESULT);
+            pMD = MethodDesc::FindOrCreateAssociatedMethodDesc(pTypicalAwaitMD, pTypicalAwaitMD->GetMethodTable(), FALSE, Instantiation(&retType, 1), TRUE);
+        }
+    }
+    else
+    {
+        if (sig.IsReturnTypeVoid())
+        {
+            pTypicalAwaitMD = pMD = CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__TRANSPARENT_AWAIT_TASK_WITH_RESULT);
+        }
+        else
+        {
+            pTypicalAwaitMD = CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__TRANSPARENT_AWAIT_TASK_OF_T_WITH_RESULT);
+            pMD = MethodDesc::FindOrCreateAssociatedMethodDesc(pTypicalAwaitMD, pTypicalAwaitMD->GetMethodTable(), FALSE, Instantiation(&retType, 1), TRUE);
+        }
+    }
+
+    if (pMD->RequiresInstArg())
+    {
+        if (retType.IsCanonicalSubtype())
+        {
+            ComputeRuntimeLookupForAwaitCall(pCallerMD, pTypicalAwaitMD, instArg);
+        }
+        else
+        {
+            MethodDesc* pContext = MethodDesc::FindOrCreateAssociatedMethodDesc(pTypicalAwaitMD, pTypicalAwaitMD->GetMethodTable(), FALSE, Instantiation(&retType, 1), FALSE);
+            instArg->lookupKind.needsRuntimeLookup = false;
+            instArg->constLookup.accessType = IAT_VALUE;
+            instArg->constLookup.addr = pContext;
+        }
+    }
+
+    EE_TO_JIT_TRANSITION();
+
+    return CORINFO_METHOD_HANDLE(pMD);
+}
+
+// Compute the runtime lookup for the instantiation argument for an
+// AsyncHelpers.TransparentAwait call, to be used for wrapping a return value
+// from pCallerMD for its runtime async version.
+// For example, if pCallerMD is ValueTask<List<T>> Foo<T>(), then we call this
+// for the runtime async version of Foo and it gives a runtime lookup that
+// computes the method desc for AsyncHelpers.TransparentAwait<List<T>>.
+void CEEInfo::ComputeRuntimeLookupForAwaitCall(MethodDesc* pCallerMD, MethodDesc* pTypicalAwaitMD, CORINFO_LOOKUP* lookup)
+{
+    lookup->lookupKind.needsRuntimeLookup = true;
+
+    CORINFO_RUNTIME_LOOKUP* rlookup = &lookup->runtimeLookup;
+
+    rlookup->signature = NULL;
+    rlookup->indirectFirstOffset = 0;
+    rlookup->indirectSecondOffset = 0;
+
+    rlookup->sizeOffset = CORINFO_NO_SIZE_CHECK;
+    rlookup->indirections = CORINFO_USEHELPER;
+
+    if (pCallerMD->RequiresInstMethodDescArg())
+    {
+        lookup->lookupKind.runtimeLookupKind = CORINFO_LOOKUP_METHODPARAM;
+        rlookup->helper = CORINFO_HELP_RUNTIMEHANDLE_METHOD;
+    }
+    else if (pCallerMD->RequiresInstMethodTableArg())
+    {
+        lookup->lookupKind.runtimeLookupKind = CORINFO_LOOKUP_CLASSPARAM;
+        rlookup->helper = CORINFO_HELP_RUNTIMEHANDLE_CLASS;
+    }
+    else
+    {
+        lookup->lookupKind.runtimeLookupKind = CORINFO_LOOKUP_THISOBJ;
+        rlookup->helper = CORINFO_HELP_RUNTIMEHANDLE_CLASS;
+    }
+
+    SigBuilder sigBuilder;
+    sigBuilder.AppendData(MethodDescSlot);
+
+    if (lookup->lookupKind.runtimeLookupKind != CORINFO_LOOKUP_METHODPARAM)
+    {
+        sigBuilder.AppendData(pCallerMD->GetMethodTable()->GetNumDicts() - 1);
+    }
+
+    // Containing type
+    sigBuilder.AppendElementType(ELEMENT_TYPE_INTERNAL);
+    sigBuilder.AppendPointer(pTypicalAwaitMD->GetMethodTable());
+
+    // Method flags
+    sigBuilder.AppendData(ENCODE_METHOD_SIG_MethodInstantiation | ENCODE_METHOD_SIG_InstantiatingStub);
+
+    // Method
+    sigBuilder.AppendElementType(ELEMENT_TYPE_INTERNAL);
+    sigBuilder.AppendPointer(pTypicalAwaitMD->GetMethodTable());
+    sigBuilder.AppendData(RidFromToken(pTypicalAwaitMD->GetMemberDef()));
+
+    // Finally the instantiation.
+    SigPointer resultSig = pCallerMD->GetAsyncThunkResultTypeSig();
+     // 1 argument
+    sigBuilder.AppendData(1);
+    resultSig.ConvertToInternalExactlyOne(pCallerMD->GetModule(), NULL, &sigBuilder);
+
+    FinishComputeRuntimeLookup(sigBuilder, pCallerMD, lookup);
+}
+
 static MethodTable* getContinuationType(
     size_t dataSize,
     bool* objRefs,
@@ -13076,19 +13228,27 @@ void CEECodeGenInfo::getEHinfo(
 
     MethodDesc* pMD = GetMethod(ftn);
 
+    bool isMethodBeingCompiled = pMD == m_pMethodBeingCompiled;
+
+    if (pMD->SupportsAsyncVersionCodegen())
+    {
+        // Get the EH info from the ordinary variant.
+        pMD = pMD->GetOrdinaryVariant();
+    }
+
     COR_ILMETHOD* pILHeader;
-    if (IsDynamicMethodHandle(ftn))
+    if (pMD->IsDynamicMethod())
     {
         pMD->AsDynamicMethodDesc()->GetResolver()->GetEHInfo(EHnumber, clause);
     }
-    else if (pMD == m_pMethodBeingCompiled)
+    else if (isMethodBeingCompiled)
     {
-        getEHinfoHelper(ftn, EHnumber, clause, m_ILHeader);
+        getEHinfoHelper(EHnumber, clause, m_ILHeader);
     }
     else if (pMD->MayHaveILHeader() && (pILHeader = pMD->GetILHeader()) != NULL)
     {
         COR_ILMETHOD_DECODER header(pILHeader, pMD->GetMDImport(), NULL);
-        getEHinfoHelper(ftn, EHnumber, clause, &header);
+        getEHinfoHelper(EHnumber, clause, &header);
     }
     else if (pMD->IsIL())
     {
@@ -13098,7 +13258,7 @@ void CEECodeGenInfo::getEHinfo(
             _ASSERTE(!"Expected to be able to find transient method details in getEHinfo");
         }
 
-        getEHinfoHelper(ftn, EHnumber, clause, details->Header);
+        getEHinfoHelper(EHnumber, clause, details->Header);
     }
     else
     {
diff --git a/src/coreclr/vm/jitinterface.h b/src/coreclr/vm/jitinterface.h
index c5c8445d957a45..aebb381a0bf37d 100644
--- a/src/coreclr/vm/jitinterface.h
+++ b/src/coreclr/vm/jitinterface.h
@@ -299,6 +299,7 @@ class CEEInfo : public ICorJitInfo
     void GetTypeContext(CORINFO_CONTEXT_HANDLE context, SigTypeContext* pTypeContext);
 
     void HandleException(struct _EXCEPTION_POINTERS* pExceptionPointers);
+
 public:
 #include "icorjitinfoimpl_generated.h"
     uint32_t getClassAttribsInternal (CORINFO_CLASS_HANDLE cls);
@@ -408,6 +409,12 @@ class CEEInfo : public ICorJitInfo
                                                    MethodDesc * pTemplateMD /* for method-based slots */,
                                                    MethodDesc * pCallerMD,
                                                    CORINFO_LOOKUP *pResultLookup);
+    void FinishComputeRuntimeLookup(
+        SigBuilder& sig,
+        MethodDesc* pCallerMD,
+        CORINFO_LOOKUP* pResultLookup);
+
+    void ComputeRuntimeLookupForAwaitCall(MethodDesc* pCallerMD, MethodDesc* pTypicalAwaitMD, CORINFO_LOOKUP* lookup);
 
 #if defined(FEATURE_GDBJIT)
     CalledMethod * GetCalledMethods() { return m_pCalledMethods; }
diff --git a/src/coreclr/vm/metasig.h b/src/coreclr/vm/metasig.h
index 88c986a9c6e4b7..fb3801964ee0cf 100644
--- a/src/coreclr/vm/metasig.h
+++ b/src/coreclr/vm/metasig.h
@@ -554,6 +554,11 @@ DEFINE_METASIG_T(SM(RefRuntimeAsyncAwaitState_RetValueTask, r(g(RUNTIME_ASYNC_AW
 DEFINE_METASIG_T(GM(RefRuntimeAsyncAwaitState_RetTaskOfT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, r(g(RUNTIME_ASYNC_AWAIT_STATE)), GI(C(TASK_1), 1, M(0))))
 DEFINE_METASIG_T(GM(RefRuntimeAsyncAwaitState_RetValueTaskOfT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, r(g(RUNTIME_ASYNC_AWAIT_STATE)), GI(g(VALUETASK_1), 1, M(0))))
 
+DEFINE_METASIG_T(SM(Task_RetVoid, C(TASK), v))
+DEFINE_METASIG_T(SM(ValueTask_RetVoid, g(VALUETASK), v))
+DEFINE_METASIG_T(GM(TaskOfT_RetT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, GI(C(TASK_1), 1, M(0)), M(0)))
+DEFINE_METASIG_T(GM(ValueTaskOfT_RetT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, GI(g(VALUETASK_1), 1, M(0)), M(0)))
+
 // Undefine macros in case we include the file again in the compilation unit
 
 #undef  DEFINE_METASIG
diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp
index dc0c6b3cef2b08..0ff2b314c2c53d 100644
--- a/src/coreclr/vm/method.hpp
+++ b/src/coreclr/vm/method.hpp
@@ -1546,7 +1546,7 @@ class MethodDesc
         LIMITED_METHOD_DAC_CONTRACT;
 
         // methods with transient IL bodies do not have IL headers
-        return IsIL() && !IsUnboxingStub() && !IsAsyncThunkMethod();
+        return IsIL() && !IsUnboxingStub() && (!IsAsyncThunkMethod() || SupportsAsyncVersionCodegen());
     }
 
     ULONG GetRVA();
@@ -2075,6 +2075,12 @@ class MethodDesc
         return hasAsyncFlags(asyncFlags, AsyncMethodFlags::ReturnDroppingThunk);
     }
 
+    inline bool SupportsAsyncVersionCodegen() const
+    {
+        LIMITED_METHOD_DAC_CONTRACT;
+        return IsAsyncThunkMethod() && IsAsyncVariantMethod() && !IsReturnDroppingThunk();
+    }
+
     inline bool MatchesAsyncVariantLookup(AsyncVariantLookup lookup) const
     {
         LIMITED_METHOD_DAC_CONTRACT;
@@ -2303,13 +2309,11 @@ class MethodDesc
     bool TryGenerateAsyncThunk(DynamicResolver** resolver, COR_ILMETHOD_DECODER** methodILDecoder);
     bool TryGenerateUnsafeAccessor(DynamicResolver** resolver, COR_ILMETHOD_DECODER** methodILDecoder);
     void EmitTaskReturningThunk(MethodDesc* pAsyncCallVariant, MetaSig& thunkMsig, ILStubLinker* pSL);
-    void EmitAsyncMethodThunk(MethodDesc* pTaskReturningVariant, MetaSig& msig, ILStubLinker* pSL);
     void EmitReturnDroppingThunk(MethodDesc* pAsyncOtherVariant, MetaSig& msig, ILStubLinker* pSL);
-    SigPointer GetAsyncThunkResultTypeSig();
     int GetTokenForThunkTarget(ILCodeStream* pCode, MethodDesc* md);
     int GetTokenForGenericMethodCallWithAsyncReturnType(ILCodeStream* pCode, MethodDesc* md);
-    int GetTokenForGenericTypeMethodCallWithAsyncReturnType(ILCodeStream* pCode, MethodDesc* md);
 public:
+    SigPointer GetAsyncThunkResultTypeSig();
     static void CreateDerivedTargetSig(MetaSig& msig, SigBuilder* stubSigBuilder);
     bool TryGenerateTransientILImplementation(DynamicResolver** resolver, COR_ILMETHOD_DECODER** methodILDecoder);
     void GenerateFunctionPointerCall(DynamicResolver** resolver, COR_ILMETHOD_DECODER** methodILDecoder);
diff --git a/src/coreclr/vm/method.inl b/src/coreclr/vm/method.inl
index 9331543274d7a5..84be3c9617b20f 100644
--- a/src/coreclr/vm/method.inl
+++ b/src/coreclr/vm/method.inl
@@ -178,7 +178,30 @@ inline bool MethodDesc::IsDiagnosticsHidden()
     //   tolerate if the runtime-implemented frame is missing because they can still see the managed target method.
 
     WRAPPER_NO_CONTRACT;
-    return IsILStub() || IsAsyncThunkMethod() || IsWrapperStub();
+    if (IsILStub())
+    {
+        return true;
+    }
+
+    if (IsAsyncThunkMethod())
+    {
+        if (IsReturnDroppingThunk())
+        {
+            return true;
+        }
+
+        if (!SupportsAsyncVersionCodegen())
+        {
+            return true;
+        }
+    }
+
+    if (IsWrapperStub())
+    {
+        return true;
+    }
+
+    return false;
 }
 
 inline BOOL MethodDesc::IsQCall()
diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp
index d4220ae0185414..3a1243257892f5 100644
--- a/src/coreclr/vm/prestub.cpp
+++ b/src/coreclr/vm/prestub.cpp
@@ -689,7 +689,6 @@ namespace
         // So, if config provides no header and we see an implementation method desc, then just ask the method desc itself.
         if (ilHeader == NULL && pMD->IsAsyncVariantMethod())
         {
-            _ASSERTE(!pMD->IsAsyncThunkMethod());
             ilHeader = pMD->GetILHeader();
         }
 
diff --git a/src/libraries/Directory.Build.targets b/src/libraries/Directory.Build.targets
index b6fee26af35e3d..13ecdb47cd03c5 100644
--- a/src/libraries/Directory.Build.targets
+++ b/src/libraries/Directory.Build.targets
@@ -142,7 +142,6 @@
     and $([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net11.0'))
     and '$(UseRuntimeAsync)' != 'false'
     and '$(RuntimeAsyncSupported)' == 'true'">
-    <Features>$(Features);runtime-async=on</Features>
   </PropertyGroup>
 
   <Import Project="$(RepositoryEngineeringDir)codeOptimization.targets" />
diff --git a/src/tests/async/reflection/reflection.cs b/src/tests/async/reflection/reflection.cs
index a5992542037f2e..db08b050071965 100644
--- a/src/tests/async/reflection/reflection.cs
+++ b/src/tests/async/reflection/reflection.cs
@@ -237,6 +237,9 @@ public class Accessors2
     [Fact]
     public static void UnsafeAccessors()
     {
+        PrivateAsync1<int>.s = 0;
+        PrivateAsync2.s = 0;
+
         Accessors2.accessor<int>(null, 7).GetAwaiter().GetResult();
         Assert.Equal(4, PrivateAsync1<int>.s);
         Assert.Equal(4, PrivateAsync2.s);
@@ -246,6 +249,26 @@ public static void UnsafeAccessors()
         Assert.Equal(8, PrivateAsync2.s);
     }
 
+    [Fact]
+    public static void UnsafeAccessorsAsync()
+    {
+        UnsafeAccessorsAsyncInner().GetAwaiter().GetResult();
+    }
+
+    private static async Task UnsafeAccessorsAsyncInner()
+    {
+        PrivateAsync1<int>.s = 0;
+        PrivateAsync2.s = 0;
+
+        await Accessors2.accessor<int>(null, 7);
+        Assert.Equal(4, PrivateAsync1<int>.s);
+        Assert.Equal(4, PrivateAsync2.s);
+
+        await Accessors1<int>.accessor(null, 7);
+        Assert.Equal(8, PrivateAsync1<int>.s);
+        Assert.Equal(8, PrivateAsync2.s);
+    }
+
     [Fact]
     public static void CurrentMethod()
     {
diff --git a/src/tests/nativeaot/SmokeTests/UnitTests/UnitTests.csproj b/src/tests/nativeaot/SmokeTests/UnitTests/UnitTests.csproj
index 4830ee3e936a3e..8a1b0afbb1ce8d 100644
--- a/src/tests/nativeaot/SmokeTests/UnitTests/UnitTests.csproj
+++ b/src/tests/nativeaot/SmokeTests/UnitTests/UnitTests.csproj
@@ -13,8 +13,6 @@
 
     <IlcOrderFile>order.txt</IlcOrderFile>
     <StackTraceLineNumberSupport>true</StackTraceLineNumberSupport>
-
-    <Features>$(Features);runtime-async=on</Features>
   </PropertyGroup>
 
   <ItemGroup>
