Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ControlledExecution API #71661

Merged
merged 10 commits into from
Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,151 @@
// 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.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>
AntonLapounov marked this conversation as resolved.
Show resolved Hide resolved
/// <remarks>
/// <see cref="ControlledExecution"/> enables aborting arbitrary code in a non-cooperative manner.
/// Doing so may corrupt the process. This method is not recommended for use in production code
AntonLapounov marked this conversation as resolved.
Show resolved Hide resolved
/// in which reliability is important.
AntonLapounov marked this conversation as resolved.
Show resolved Hide resolved
/// </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 @@ -189,6 +189,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("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(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 @@ -1140,15 +1140,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 @@ -110,6 +110,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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2500,6 +2500,10 @@
<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>
Expand Down
Loading