Implement ControlledExecution API (#71661)
authorAnton Lapounov <antonl@microsoft.com>
Thu, 28 Jul 2022 06:38:38 +0000 (23:38 -0700)
committerGitHub <noreply@github.com>
Thu, 28 Jul 2022 06:38:38 +0000 (23:38 -0700)
21 files changed:
docs/project/list-of-diagnostics.md
src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj
src/coreclr/System.Private.CoreLib/src/System/Runtime/ControlledExecution.CoreCLR.cs [new file with mode: 0644]
src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/ControlledExecution.NativeAot.cs [new file with mode: 0644]
src/coreclr/vm/arm64/asmhelpers.asm
src/coreclr/vm/arm64/asmmacros.h
src/coreclr/vm/arm64/stubs.cpp
src/coreclr/vm/arm64/unixstubs.cpp
src/coreclr/vm/comsynchronizable.cpp
src/coreclr/vm/comsynchronizable.h
src/coreclr/vm/qcallentrypoints.cpp
src/coreclr/vm/threads.h
src/coreclr/vm/threadsuspend.cpp
src/libraries/Common/src/System/Obsoletions.cs
src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
src/libraries/System.Runtime/ref/System.Runtime.cs
src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj
src/libraries/System.Runtime/tests/System/Runtime/ControlledExecutionTests.cs [new file with mode: 0644]
src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj
src/mono/System.Private.CoreLib/src/System/Runtime/ControlledExecution.Mono.cs [new file with mode: 0644]

index 888f93d..2110060 100644 (file)
@@ -100,6 +100,7 @@ The PR that reveals the implementation of the `<IncludeInternalObsoleteAttribute
 |  __`SYSLIB0043`__ | ECDiffieHellmanPublicKey.ToByteArray() and the associated constructor do not have a consistent and interoperable implementation on all platforms. Use ECDiffieHellmanPublicKey.ExportSubjectPublicKeyInfo() instead. |
 |  __`SYSLIB0044`__ | AssemblyName.CodeBase and AssemblyName.EscapedCodeBase are obsolete. Using them for loading an assembly is not supported. |
 |  __`SYSLIB0045`__ | Cryptographic factory methods accepting an algorithm name are obsolete. Use the parameterless Create factory method on the algorithm type instead. |
+|  __`SYSLIB0046`__ | ControlledExecution.Run method may corrupt the process and should not be used in production code. |
 
 ## Analyzer Warnings
 
index a440561..78568b2 100644 (file)
     <Compile Include="$(BclSourcesRoot)\System\Runtime\CompilerServices\RuntimeFeature.CoreCLR.cs" />
     <Compile Include="$(BclSourcesRoot)\System\Runtime\CompilerServices\RuntimeHelpers.CoreCLR.cs" />
     <Compile Include="$(BclSourcesRoot)\System\Runtime\CompilerServices\TypeDependencyAttribute.cs" />
+    <Compile Include="$(BclSourcesRoot)\System\Runtime\ControlledExecution.CoreCLR.cs" />
     <Compile Include="$(BclSourcesRoot)\System\Runtime\DependentHandle.cs" />
     <Compile Include="$(BclSourcesRoot)\System\Runtime\GCSettings.CoreCLR.cs" />
     <Compile Include="$(BclSourcesRoot)\System\Runtime\JitInfo.CoreCLR.cs" />
diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/ControlledExecution.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/ControlledExecution.CoreCLR.cs
new file mode 100644 (file)
index 0000000..3b419f8
--- /dev/null
@@ -0,0 +1,159 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.ExceptionServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace System.Runtime
+{
+    /// <summary>
+    /// Allows to run code and abort it asynchronously.
+    /// </summary>
+    public static partial class ControlledExecution
+    {
+        [ThreadStatic]
+        private static bool t_executing;
+
+        /// <summary>
+        /// Runs code that may be aborted asynchronously.
+        /// </summary>
+        /// <param name="action">The delegate that represents the code to execute.</param>
+        /// <param name="cancellationToken">The cancellation token that may be used to abort execution.</param>
+        /// <exception cref="System.PlatformNotSupportedException">The method is not supported on this platform.</exception>
+        /// <exception cref="System.ArgumentNullException">The <paramref name="action"/> argument is null.</exception>
+        /// <exception cref="System.InvalidOperationException">
+        /// The current thread is already running the <see cref="ControlledExecution.Run"/> method.
+        /// </exception>
+        /// <exception cref="System.OperationCanceledException">The execution was aborted.</exception>
+        /// <remarks>
+        /// <para>This method enables aborting arbitrary managed code in a non-cooperative manner by throwing an exception
+        /// in the thread executing that code.  While the exception may be caught by the code, it is re-thrown at the end
+        /// of `catch` blocks until the execution flow returns to the `ControlledExecution.Run` method.</para>
+        /// <para>Execution of the code is not guaranteed to abort immediately, or at all.  This situation can occur, for
+        /// example, if a thread is stuck executing unmanaged code or the `catch` and `finally` blocks that are called as
+        /// part of the abort procedure, thereby indefinitely delaying the abort.  Furthermore, execution may not be
+        /// aborted immediately if the thread is currently executing a `catch` or `finally` block.</para>
+        /// <para>Aborting code at an unexpected location may corrupt the state of data structures in the process and lead
+        /// to unpredictable results.  For that reason, this method should not be used in production code and calling it
+        /// produces a compile-time warning.</para>
+        /// </remarks>
+        [Obsolete(Obsoletions.ControlledExecutionRunMessage, DiagnosticId = Obsoletions.ControlledExecutionRunDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
+        public static void Run(Action action, CancellationToken cancellationToken)
+        {
+            if (!OperatingSystem.IsWindows())
+            {
+                throw new PlatformNotSupportedException();
+            }
+
+            ArgumentNullException.ThrowIfNull(action);
+
+            // ControlledExecution.Run does not support nested invocations.  If there's one already in flight
+            // on this thread, fail.
+            if (t_executing)
+            {
+                throw new InvalidOperationException(SR.InvalidOperation_NestedControlledExecutionRun);
+            }
+
+            // Store the current thread so that it may be referenced by the Canceler.Cancel callback if one occurs.
+            Canceler canceler = new(Thread.CurrentThread);
+
+            try
+            {
+                // Mark this thread as now running a ControlledExecution.Run to prevent recursive usage.
+                t_executing = true;
+
+                // Register for aborting.  From this moment until ctr.Unregister is called, this thread is subject to being
+                // interrupted at any moment.  This could happen during the call to UnsafeRegister if cancellation has
+                // already been requested at the time of the registration.
+                CancellationTokenRegistration ctr = cancellationToken.UnsafeRegister(e => ((Canceler)e!).Cancel(), canceler);
+                try
+                {
+                    // Invoke the caller's code.
+                    action();
+                }
+                finally
+                {
+                    // This finally block may be cloned by JIT for the non-exceptional code flow.  In that case the code
+                    // below is not guarded against aborting.  That is OK as the outer try block will catch the
+                    // ThreadAbortException and call ResetAbortThread.
+
+                    // Unregister the callback.  Unlike Dispose, Unregister will not block waiting for an callback in flight
+                    // to complete, and will instead return false if the callback has already been invoked or is currently
+                    // in flight.
+                    if (!ctr.Unregister())
+                    {
+                        // Wait until the callback has completed.  Either the callback is already invoked and completed
+                        // (in which case IsCancelCompleted will be true), or it may still be in flight.  If it's in flight,
+                        // the AbortThread call may be waiting for this thread to exit this finally block to exit, so while
+                        // spinning waiting for the callback to complete, we also need to call ResetAbortThread in order to
+                        // reset the flag the AbortThread call is polling in its waiting loop.
+                        SpinWait sw = default;
+                        while (!canceler.IsCancelCompleted)
+                        {
+                            ResetAbortThread();
+                            sw.SpinOnce();
+                        }
+                    }
+                }
+            }
+            catch (ThreadAbortException tae)
+            {
+                // We don't want to leak ThreadAbortExceptions to user code.  Instead, translate the exception into
+                // an OperationCanceledException, preserving stack trace details from the ThreadAbortException in
+                // order to aid in diagnostics and debugging.
+                OperationCanceledException e = cancellationToken.IsCancellationRequested ? new(cancellationToken) : new();
+                if (tae.StackTrace is string stackTrace)
+                {
+                    ExceptionDispatchInfo.SetRemoteStackTrace(e, stackTrace);
+                }
+                throw e;
+            }
+            finally
+            {
+                // Unmark this thread for recursion detection.
+                t_executing = false;
+
+                if (cancellationToken.IsCancellationRequested)
+                {
+                    // Reset an abort request that may still be pending on this thread.
+                    ResetAbortThread();
+                }
+            }
+        }
+
+        [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_Abort")]
+        private static partial void AbortThread(ThreadHandle thread);
+
+        [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_ResetAbort")]
+        [SuppressGCTransition]
+        private static partial void ResetAbortThread();
+
+        private sealed class Canceler
+        {
+            private readonly Thread _thread;
+            private volatile bool _cancelCompleted;
+
+            public Canceler(Thread thread)
+            {
+                _thread = thread;
+            }
+
+            public bool IsCancelCompleted => _cancelCompleted;
+
+            public void Cancel()
+            {
+                try
+                {
+                    // Abort the thread executing the action (which may be the current thread).
+                    AbortThread(_thread.GetNativeHandle());
+                }
+                finally
+                {
+                    _cancelCompleted = true;
+                }
+            }
+        }
+    }
+}
index 941bb14..3a5a4df 100644 (file)
     <Compile Include="System\Resources\ManifestBasedResourceGroveler.NativeAot.cs" />
     <Compile Include="System\RuntimeArgumentHandle.cs" />
     <Compile Include="System\RuntimeType.cs" />
+    <Compile Include="System\Runtime\ControlledExecution.NativeAot.cs" />
     <Compile Include="System\Runtime\DependentHandle.cs" />
     <Compile Include="System\Runtime\CompilerServices\ForceLazyDictionaryAttribute.cs" />
     <Compile Include="System\Runtime\CompilerServices\EagerStaticClassConstructionAttribute.cs" />
diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/ControlledExecution.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/ControlledExecution.NativeAot.cs
new file mode 100644 (file)
index 0000000..b2031db
--- /dev/null
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading;
+
+namespace System.Runtime
+{
+    public static class ControlledExecution
+    {
+        [Obsolete(Obsoletions.ControlledExecutionRunMessage, DiagnosticId = Obsoletions.ControlledExecutionRunDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
+        public static void Run(Action action, CancellationToken cancellationToken)
+        {
+            throw new PlatformNotSupportedException();
+        }
+    }
+}
index aed9a9c..6b2042d 100644 (file)
@@ -20,6 +20,8 @@
 #ifdef FEATURE_READYTORUN
     IMPORT DynamicHelperWorker
 #endif
+    IMPORT HijackHandler
+    IMPORT ThrowControlForThread
 
 #ifdef FEATURE_USE_SOFTWARE_WRITE_WATCH_FOR_GC_HEAP
     IMPORT  g_sw_ww_table
@@ -1029,6 +1031,31 @@ FaultingExceptionFrame_FrameOffset        SETA  SIZEOF__GSCookie
 
 
 ; ------------------------------------------------------------------
+;
+; Helpers for ThreadAbort exceptions
+;
+
+        NESTED_ENTRY RedirectForThreadAbort2,,HijackHandler
+        PROLOG_SAVE_REG_PAIR fp,lr, #-16!
+
+        ; stack must be 16 byte aligned
+        CHECK_STACK_ALIGNMENT
+
+        ; On entry:
+        ;
+        ; x0 = address of FaultingExceptionFrame
+        ;
+        ; Invoke the helper to setup the FaultingExceptionFrame and raise the exception
+        bl              ThrowControlForThread
+
+        ; ThrowControlForThread doesn't return.
+        EMIT_BREAKPOINT
+
+        NESTED_END RedirectForThreadAbort2
+
+        GenerateRedirectedStubWithFrame RedirectForThreadAbort, RedirectForThreadAbort2
+
+; ------------------------------------------------------------------
 ; ResolveWorkerChainLookupAsmStub
 ;
 ; This method will perform a quick chained lookup of the entry if the
index 8bfa79d..4a0cb5d 100644 (file)
@@ -155,6 +155,24 @@ $FuncName
     MEND
 
 ;-----------------------------------------------------------------------------
+; Macro used to check (in debug builds only) whether the stack is 16-bytes aligned (a requirement before calling
+; out into C++/OS code). Invoke this directly after your prolog (if the stack frame size is fixed) or directly
+; before a call (if you have a frame pointer and a dynamic stack). A breakpoint will be invoked if the stack
+; is misaligned.
+;
+    MACRO
+        CHECK_STACK_ALIGNMENT
+
+#ifdef _DEBUG
+        add     x9, sp, xzr
+        tst     x9, #15
+        beq     %F0
+        EMIT_BREAKPOINT
+0
+#endif
+    MEND
+
+;-----------------------------------------------------------------------------
 ; The Following sets of SAVE_*_REGISTERS expect the memory to be reserved and
 ; base address to be passed in $reg
 ;
index ba99ae5..4f680a0 100644 (file)
@@ -918,12 +918,6 @@ PTR_CONTEXT GetCONTEXTFromRedirectedStubStackFrame(T_CONTEXT * pContext)
     return *ppContext;
 }
 
-void RedirectForThreadAbort()
-{
-    // ThreadAbort is not supported in .net core
-    throw "NYI";
-}
-
 #if !defined(DACCESS_COMPILE)
 FaultingExceptionFrame *GetFrameFromRedirectedStubStackFrame (DISPATCHER_CONTEXT *pDispatcherContext)
 {
index d51902a..9b313f8 100644 (file)
@@ -9,4 +9,9 @@ extern "C"
     {
         PORTABILITY_ASSERT("Implement for PAL");
     }
+
+    void RedirectForThreadAbort()
+    {
+        PORTABILITY_ASSERT("Implement for PAL");
+    }
 };
index 839b957..1a2c963 100644 (file)
@@ -1096,15 +1096,38 @@ extern "C" BOOL QCALLTYPE ThreadNative_YieldThread()
 
     BOOL ret = FALSE;
 
-    BEGIN_QCALL
+    BEGIN_QCALL;
 
     ret = __SwitchToThread(0, CALLER_LIMITS_SPINNING);
 
-    END_QCALL
+    END_QCALL;
 
     return ret;
 }
 
+extern "C" void QCALLTYPE ThreadNative_Abort(QCall::ThreadHandle thread)
+{
+    QCALL_CONTRACT;
+
+    BEGIN_QCALL;
+
+    thread->UserAbort(EEPolicy::TA_Safe, INFINITE);
+
+    END_QCALL;
+}
+
+// Unmark the current thread for a safe abort.
+extern "C" void QCALLTYPE ThreadNative_ResetAbort()
+{
+    QCALL_CONTRACT_NO_GC_TRANSITION;
+
+    Thread *pThread = GetThread();
+    if (pThread->IsAbortRequested())
+    {
+        pThread->UnmarkThreadForAbort(EEPolicy::TA_Safe);
+    }
+}
+
 FCIMPL0(INT32, ThreadNative::GetCurrentProcessorNumber)
 {
     FCALL_CONTRACT;
index 889f99c..98a2eba 100644 (file)
@@ -103,6 +103,8 @@ extern "C" void QCALLTYPE ThreadNative_InformThreadNameChange(QCall::ThreadHandl
 extern "C" UINT64 QCALLTYPE ThreadNative_GetProcessDefaultStackSize();
 extern "C" BOOL QCALLTYPE ThreadNative_YieldThread();
 extern "C" UINT64 QCALLTYPE ThreadNative_GetCurrentOSThreadId();
+extern "C" void QCALLTYPE ThreadNative_Abort(QCall::ThreadHandle thread);
+extern "C" void QCALLTYPE ThreadNative_ResetAbort();
 
 #endif // _COMSYNCHRONIZABLE_H
 
index 5b5f4c8..ddaa9a3 100644 (file)
@@ -209,6 +209,8 @@ static const Entry s_QCall[] =
     DllImportEntry(ThreadNative_InformThreadNameChange)
     DllImportEntry(ThreadNative_YieldThread)
     DllImportEntry(ThreadNative_GetCurrentOSThreadId)
+    DllImportEntry(ThreadNative_Abort)
+    DllImportEntry(ThreadNative_ResetAbort)
     DllImportEntry(ThreadPool_GetCompletedWorkItemCount)
     DllImportEntry(ThreadPool_RequestWorkerThread)
     DllImportEntry(ThreadPool_PerformGateActivities)
index f50c53f..59588b5 100644 (file)
@@ -2496,7 +2496,7 @@ private:
 
 public:
     void MarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType);
-    void UnmarkThreadForAbort();
+    void UnmarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType = EEPolicy::TA_Rude);
 
     static ULONGLONG GetNextSelfAbortEndTime()
     {
index e1b4a66..558d177 100644 (file)
@@ -1785,7 +1785,7 @@ void Thread::RemoveAbortRequestBit()
 }
 
 // Make sure that when AbortRequest bit is cleared, we also dec TrapReturningThreads count.
-void Thread::UnmarkThreadForAbort()
+void Thread::UnmarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType /* = EEPolicy::TA_Rude */)
 {
     CONTRACTL
     {
@@ -1794,11 +1794,14 @@ void Thread::UnmarkThreadForAbort()
     }
     CONTRACTL_END;
 
-    // Switch to COOP (for ClearAbortReason) before acquiring AbortRequestLock
-    GCX_COOP();
-
     AbortRequestLockHolder lh(this);
 
+    if (m_AbortType > (DWORD)abortType)
+    {
+        // Aborting at a higher level
+        return;
+    }
+
     m_AbortType = EEPolicy::TA_None;
     m_AbortEndTime = MAXULONGLONG;
     m_RudeAbortEndTime = MAXULONGLONG;
index f15282d..7c193dc 100644 (file)
@@ -147,5 +147,8 @@ namespace System
 
         internal const string CryptoStringFactoryMessage = "Cryptographic factory methods accepting an algorithm name are obsolete. Use the parameterless Create factory method on the algorithm type instead.";
         internal const string CryptoStringFactoryDiagId = "SYSLIB0045";
+
+        internal const string ControlledExecutionRunMessage = "ControlledExecution.Run method may corrupt the process and should not be used in production code.";
+        internal const string ControlledExecutionRunDiagId = "SYSLIB0046";
     }
 }
index a86cb88..1fe307f 100644 (file)
   <data name="InvalidOperation_NativeOverlappedReused" xml:space="preserve">
     <value>NativeOverlapped cannot be reused for multiple operations.</value>
   </data>
+  <data name="InvalidOperation_NestedControlledExecutionRun" xml:space="preserve">
+    <value>The thread is already executing the ControlledExecution.Run method.</value>
+    <comment>{Locked="ControlledExecution.Run"}</comment>
+  </data>
   <data name="InvalidOperation_NoMultiModuleAssembly" xml:space="preserve">
     <value>You cannot have more than one dynamic module in each dynamic assembly in this version of the runtime.</value>
   </data>
index 4449a3f..1c4378e 100644 (file)
@@ -12136,6 +12136,11 @@ namespace System.Runtime
         public AssemblyTargetedPatchBandAttribute(string targetedPatchBand) { }
         public string TargetedPatchBand { get { throw null; } }
     }
+    public static partial class ControlledExecution
+    {
+        [System.ObsoleteAttribute("ControlledExecution.Run method may corrupt the process and should not be used in production code.", DiagnosticId = "SYSLIB0046", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")]
+        public static void Run(System.Action action, System.Threading.CancellationToken cancellationToken) { throw null; }
+    }
     public partial struct DependentHandle : System.IDisposable
     {
         private object _dummy;
index 036977e..8ff7eb9 100644 (file)
     <Compile Include="System\Reflection\TypeDelegatorTests.cs" />
     <Compile Include="System\Reflection\TypeTests.Get.CornerCases.cs" />
     <Compile Include="System\Reflection\TypeTests.GetMember.cs" />
+    <Compile Include="System\Runtime\ControlledExecutionTests.cs" />
     <Compile Include="System\Runtime\DependentHandleTests.cs" />
     <Compile Include="System\Runtime\JitInfoTests.cs" />
     <Compile Include="System\Runtime\MemoryFailPointTests.cs" />
diff --git a/src/libraries/System.Runtime/tests/System/Runtime/ControlledExecutionTests.cs b/src/libraries/System.Runtime/tests/System/Runtime/ControlledExecutionTests.cs
new file mode 100644 (file)
index 0000000..2d615b3
--- /dev/null
@@ -0,0 +1,221 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading;
+using Xunit;
+
+// Disable warnings for ControlledExecution.Run
+#pragma warning disable SYSLIB0046
+
+namespace System.Runtime.Tests
+{
+    public class ControlledExecutionTests
+    {
+        private bool _startedExecution, _caughtException, _finishedExecution;
+        private Exception _exception;
+        private CancellationTokenSource _cts;
+        private volatile int _counter;
+
+        // Tests cancellation on timeout. The ThreadAbortException must be mapped to OperationCanceledException.
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)]
+        public void CancelOnTimeout()
+        {
+            var cts = new CancellationTokenSource();
+            cts.CancelAfter(200);
+            RunTest(LengthyAction, cts.Token);
+
+            Assert.True(_startedExecution);
+            Assert.True(_caughtException);
+            Assert.False(_finishedExecution);
+            Assert.IsType<OperationCanceledException>(_exception);
+        }
+
+        // Tests that catch blocks are not aborted. The action catches the ThreadAbortException and throws an exception of a different type.
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)]
+        public void CancelOnTimeout_ThrowFromCatch()
+        {
+            var cts = new CancellationTokenSource();
+            cts.CancelAfter(200);
+            RunTest(LengthyAction_ThrowFromCatch, cts.Token);
+
+            Assert.True(_startedExecution);
+            Assert.True(_caughtException);
+            Assert.False(_finishedExecution);
+            Assert.IsType<TimeoutException>(_exception);
+        }
+
+        // Tests that finally blocks are not aborted. The action throws an exception from a finally block.
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)]
+        public void CancelOnTimeout_ThrowFromFinally()
+        {
+            var cts = new CancellationTokenSource();
+            cts.CancelAfter(200);
+            RunTest(LengthyAction_ThrowFromFinally, cts.Token);
+
+            Assert.True(_startedExecution);
+            Assert.IsType<TimeoutException>(_exception);
+        }
+
+        // Tests that finally blocks are not aborted. The action throws an exception from a try block.
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)]
+        public void CancelOnTimeout_Finally()
+        {
+            var cts = new CancellationTokenSource();
+            cts.CancelAfter(200);
+            RunTest(LengthyAction_Finally, cts.Token);
+
+            Assert.True(_startedExecution);
+            Assert.True(_finishedExecution);
+            Assert.IsType<TimeoutException>(_exception);
+        }
+
+        // Tests cancellation before calling the Run method
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)]
+        public void CancelBeforeRun()
+        {
+            var cts = new CancellationTokenSource();
+            cts.Cancel();
+            Thread.Sleep(100);
+            RunTest(LengthyAction, cts.Token);
+
+            Assert.IsType<OperationCanceledException>(_exception);
+        }
+
+        // Tests cancellation by the action itself
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)]
+        public void CancelItself()
+        {
+            _cts = new CancellationTokenSource();
+            RunTest(Action_CancelItself, _cts.Token);
+
+            Assert.True(_startedExecution);
+            Assert.False(_finishedExecution);
+            Assert.IsType<AggregateException>(_exception);
+            Assert.IsType<ThreadAbortException>(_exception.InnerException);
+        }
+
+        private void RunTest(Action action, CancellationToken cancellationToken)
+        {
+            _startedExecution = _caughtException = _finishedExecution = false;
+            _exception = null;
+
+            try
+            {
+                ControlledExecution.Run(action, cancellationToken);
+            }
+            catch (Exception e)
+            {
+                _exception = e;
+            }
+        }
+
+        private void LengthyAction()
+        {
+            _startedExecution = true;
+            // Redirection via thread suspension is supported on Windows only.
+            // Make a call in the loop to allow redirection on other platforms.
+            bool sleep = !PlatformDetection.IsWindows;
+
+            try
+            {
+                for (_counter = 0; _counter < int.MaxValue; _counter++)
+                {
+                    if ((_counter & 0xfffff) == 0 && sleep)
+                    {
+                        Thread.Sleep(0);
+                    }
+                }
+            }
+            catch
+            {
+                // Swallow all exceptions to verify that the exception is automatically rethrown
+                _caughtException = true;
+            }
+
+            _finishedExecution = true;
+        }
+
+        private void LengthyAction_ThrowFromCatch()
+        {
+            _startedExecution = true;
+            bool sleep = !PlatformDetection.IsWindows;
+
+            try
+            {
+                for (_counter = 0; _counter < int.MaxValue; _counter++)
+                {
+                    if ((_counter & 0xfffff) == 0 && sleep)
+                    {
+                        Thread.Sleep(0);
+                    }
+                }
+            }
+            catch
+            {
+                _caughtException = true;
+                // The catch block must not be aborted
+                Thread.Sleep(100);
+                throw new TimeoutException();
+            }
+
+            _finishedExecution = true;
+        }
+
+        private void LengthyAction_ThrowFromFinally()
+        {
+            _startedExecution = true;
+
+            try
+            {
+                // Make sure to run the non-inlined finally
+                throw new Exception();
+            }
+            finally
+            {
+                // The finally block must not be aborted
+                Thread.Sleep(400);
+                throw new TimeoutException();
+            }
+        }
+
+        private void LengthyAction_Finally()
+        {
+            _startedExecution = true;
+
+            try
+            {
+                // Make sure to run the non-inlined finally
+                throw new TimeoutException();
+            }
+            finally
+            {
+                // The finally block must not be aborted
+                Thread.Sleep(400);
+                _finishedExecution = true;
+            }
+        }
+
+        private void Action_CancelItself()
+        {
+            _startedExecution = true;
+
+            try
+            {
+                // Make sure to run the non-inlined finally
+                throw new TimeoutException();
+            }
+            finally
+            {
+                // The finally block must be aborted
+                _cts.Cancel();
+                _finishedExecution = true;
+            }
+        }
+    }
+}
index 4945fcc..e9ffd3d 100644 (file)
       <Compile Include="$(BclSourcesRoot)\System\Reflection\Metadata\AssemblyExtensions.cs" />
       <Compile Include="$(BclSourcesRoot)\System\Reflection\Metadata\MetadataUpdater.cs" />
       <Compile Include="$(BclSourcesRoot)\System\Resources\ManifestBasedResourceGroveler.Mono.cs" />
+      <Compile Include="$(BclSourcesRoot)\System\Runtime\ControlledExecution.Mono.cs" />
       <Compile Include="$(BclSourcesRoot)\System\Runtime\DependentHandle.cs" />
       <Compile Include="$(BclSourcesRoot)\System\Runtime\GCSettings.Mono.cs" />
       <Compile Include="$(BclSourcesRoot)\System\Runtime\JitInfo.Mono.cs" />
diff --git a/src/mono/System.Private.CoreLib/src/System/Runtime/ControlledExecution.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Runtime/ControlledExecution.Mono.cs
new file mode 100644 (file)
index 0000000..b2031db
--- /dev/null
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading;
+
+namespace System.Runtime
+{
+    public static class ControlledExecution
+    {
+        [Obsolete(Obsoletions.ControlledExecutionRunMessage, DiagnosticId = Obsoletions.ControlledExecutionRunDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
+        public static void Run(Action action, CancellationToken cancellationToken)
+        {
+            throw new PlatformNotSupportedException();
+        }
+    }
+}