Skip to content

Commit

Permalink
Implement ControlledExecution API (#71661)
Browse files Browse the repository at this point in the history
  • Loading branch information
AntonLapounov authored Jul 28, 2022
1 parent 9390088 commit d365ff2
Show file tree
Hide file tree
Showing 21 changed files with 516 additions and 13 deletions.
1 change: 1 addition & 0 deletions docs/project/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@
<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" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
<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" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
27 changes: 27 additions & 0 deletions src/coreclr/vm/arm64/asmhelpers.asm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1028,6 +1030,31 @@ FaultingExceptionFrame_FrameOffset SETA SIZEOF__GSCookie
MEND


; ------------------------------------------------------------------
;
; 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
;
Expand Down
18 changes: 18 additions & 0 deletions src/coreclr/vm/arm64/asmmacros.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ __EndLabelName SETS "$FuncName":CC:"_End"

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
Expand Down
6 changes: 0 additions & 6 deletions src/coreclr/vm/arm64/stubs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
5 changes: 5 additions & 0 deletions src/coreclr/vm/arm64/unixstubs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ extern "C"
{
PORTABILITY_ASSERT("Implement for PAL");
}

void RedirectForThreadAbort()
{
PORTABILITY_ASSERT("Implement for PAL");
}
};
27 changes: 25 additions & 2 deletions src/coreclr/vm/comsynchronizable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/coreclr/vm/comsynchronizable.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 2 additions & 0 deletions src/coreclr/vm/qcallentrypoints.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/vm/threads.h
Original file line number Diff line number Diff line change
Expand Up @@ -2496,7 +2496,7 @@ class Thread

public:
void MarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType);
void UnmarkThreadForAbort();
void UnmarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType = EEPolicy::TA_Rude);

static ULONGLONG GetNextSelfAbortEndTime()
{
Expand Down
11 changes: 7 additions & 4 deletions src/coreclr/vm/threadsuspend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/Common/src/System/Obsoletions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,8 @@ internal static class Obsoletions

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";
}
}
Loading

0 comments on commit d365ff2

Please sign in to comment.