Skip to content

Commit d365ff2

Browse files
Implement ControlledExecution API (dotnet#71661)
1 parent 9390088 commit d365ff2

File tree

21 files changed

+516
-13
lines changed

21 files changed

+516
-13
lines changed

Diff for: docs/project/list-of-diagnostics.md

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ The PR that reveals the implementation of the `<IncludeInternalObsoleteAttribute
100100
| __`SYSLIB0043`__ | ECDiffieHellmanPublicKey.ToByteArray() and the associated constructor do not have a consistent and interoperable implementation on all platforms. Use ECDiffieHellmanPublicKey.ExportSubjectPublicKeyInfo() instead. |
101101
| __`SYSLIB0044`__ | AssemblyName.CodeBase and AssemblyName.EscapedCodeBase are obsolete. Using them for loading an assembly is not supported. |
102102
| __`SYSLIB0045`__ | Cryptographic factory methods accepting an algorithm name are obsolete. Use the parameterless Create factory method on the algorithm type instead. |
103+
| __`SYSLIB0046`__ | ControlledExecution.Run method may corrupt the process and should not be used in production code. |
103104

104105
## Analyzer Warnings
105106

Diff for: src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
<Compile Include="$(BclSourcesRoot)\System\Runtime\CompilerServices\RuntimeFeature.CoreCLR.cs" />
209209
<Compile Include="$(BclSourcesRoot)\System\Runtime\CompilerServices\RuntimeHelpers.CoreCLR.cs" />
210210
<Compile Include="$(BclSourcesRoot)\System\Runtime\CompilerServices\TypeDependencyAttribute.cs" />
211+
<Compile Include="$(BclSourcesRoot)\System\Runtime\ControlledExecution.CoreCLR.cs" />
211212
<Compile Include="$(BclSourcesRoot)\System\Runtime\DependentHandle.cs" />
212213
<Compile Include="$(BclSourcesRoot)\System\Runtime\GCSettings.CoreCLR.cs" />
213214
<Compile Include="$(BclSourcesRoot)\System\Runtime\JitInfo.CoreCLR.cs" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Runtime.CompilerServices;
5+
using System.Runtime.ExceptionServices;
6+
using System.Runtime.InteropServices;
7+
using System.Threading;
8+
9+
namespace System.Runtime
10+
{
11+
/// <summary>
12+
/// Allows to run code and abort it asynchronously.
13+
/// </summary>
14+
public static partial class ControlledExecution
15+
{
16+
[ThreadStatic]
17+
private static bool t_executing;
18+
19+
/// <summary>
20+
/// Runs code that may be aborted asynchronously.
21+
/// </summary>
22+
/// <param name="action">The delegate that represents the code to execute.</param>
23+
/// <param name="cancellationToken">The cancellation token that may be used to abort execution.</param>
24+
/// <exception cref="System.PlatformNotSupportedException">The method is not supported on this platform.</exception>
25+
/// <exception cref="System.ArgumentNullException">The <paramref name="action"/> argument is null.</exception>
26+
/// <exception cref="System.InvalidOperationException">
27+
/// The current thread is already running the <see cref="ControlledExecution.Run"/> method.
28+
/// </exception>
29+
/// <exception cref="System.OperationCanceledException">The execution was aborted.</exception>
30+
/// <remarks>
31+
/// <para>This method enables aborting arbitrary managed code in a non-cooperative manner by throwing an exception
32+
/// in the thread executing that code. While the exception may be caught by the code, it is re-thrown at the end
33+
/// of `catch` blocks until the execution flow returns to the `ControlledExecution.Run` method.</para>
34+
/// <para>Execution of the code is not guaranteed to abort immediately, or at all. This situation can occur, for
35+
/// example, if a thread is stuck executing unmanaged code or the `catch` and `finally` blocks that are called as
36+
/// part of the abort procedure, thereby indefinitely delaying the abort. Furthermore, execution may not be
37+
/// aborted immediately if the thread is currently executing a `catch` or `finally` block.</para>
38+
/// <para>Aborting code at an unexpected location may corrupt the state of data structures in the process and lead
39+
/// to unpredictable results. For that reason, this method should not be used in production code and calling it
40+
/// produces a compile-time warning.</para>
41+
/// </remarks>
42+
[Obsolete(Obsoletions.ControlledExecutionRunMessage, DiagnosticId = Obsoletions.ControlledExecutionRunDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
43+
public static void Run(Action action, CancellationToken cancellationToken)
44+
{
45+
if (!OperatingSystem.IsWindows())
46+
{
47+
throw new PlatformNotSupportedException();
48+
}
49+
50+
ArgumentNullException.ThrowIfNull(action);
51+
52+
// ControlledExecution.Run does not support nested invocations. If there's one already in flight
53+
// on this thread, fail.
54+
if (t_executing)
55+
{
56+
throw new InvalidOperationException(SR.InvalidOperation_NestedControlledExecutionRun);
57+
}
58+
59+
// Store the current thread so that it may be referenced by the Canceler.Cancel callback if one occurs.
60+
Canceler canceler = new(Thread.CurrentThread);
61+
62+
try
63+
{
64+
// Mark this thread as now running a ControlledExecution.Run to prevent recursive usage.
65+
t_executing = true;
66+
67+
// Register for aborting. From this moment until ctr.Unregister is called, this thread is subject to being
68+
// interrupted at any moment. This could happen during the call to UnsafeRegister if cancellation has
69+
// already been requested at the time of the registration.
70+
CancellationTokenRegistration ctr = cancellationToken.UnsafeRegister(e => ((Canceler)e!).Cancel(), canceler);
71+
try
72+
{
73+
// Invoke the caller's code.
74+
action();
75+
}
76+
finally
77+
{
78+
// This finally block may be cloned by JIT for the non-exceptional code flow. In that case the code
79+
// below is not guarded against aborting. That is OK as the outer try block will catch the
80+
// ThreadAbortException and call ResetAbortThread.
81+
82+
// Unregister the callback. Unlike Dispose, Unregister will not block waiting for an callback in flight
83+
// to complete, and will instead return false if the callback has already been invoked or is currently
84+
// in flight.
85+
if (!ctr.Unregister())
86+
{
87+
// Wait until the callback has completed. Either the callback is already invoked and completed
88+
// (in which case IsCancelCompleted will be true), or it may still be in flight. If it's in flight,
89+
// the AbortThread call may be waiting for this thread to exit this finally block to exit, so while
90+
// spinning waiting for the callback to complete, we also need to call ResetAbortThread in order to
91+
// reset the flag the AbortThread call is polling in its waiting loop.
92+
SpinWait sw = default;
93+
while (!canceler.IsCancelCompleted)
94+
{
95+
ResetAbortThread();
96+
sw.SpinOnce();
97+
}
98+
}
99+
}
100+
}
101+
catch (ThreadAbortException tae)
102+
{
103+
// We don't want to leak ThreadAbortExceptions to user code. Instead, translate the exception into
104+
// an OperationCanceledException, preserving stack trace details from the ThreadAbortException in
105+
// order to aid in diagnostics and debugging.
106+
OperationCanceledException e = cancellationToken.IsCancellationRequested ? new(cancellationToken) : new();
107+
if (tae.StackTrace is string stackTrace)
108+
{
109+
ExceptionDispatchInfo.SetRemoteStackTrace(e, stackTrace);
110+
}
111+
throw e;
112+
}
113+
finally
114+
{
115+
// Unmark this thread for recursion detection.
116+
t_executing = false;
117+
118+
if (cancellationToken.IsCancellationRequested)
119+
{
120+
// Reset an abort request that may still be pending on this thread.
121+
ResetAbortThread();
122+
}
123+
}
124+
}
125+
126+
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_Abort")]
127+
private static partial void AbortThread(ThreadHandle thread);
128+
129+
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_ResetAbort")]
130+
[SuppressGCTransition]
131+
private static partial void ResetAbortThread();
132+
133+
private sealed class Canceler
134+
{
135+
private readonly Thread _thread;
136+
private volatile bool _cancelCompleted;
137+
138+
public Canceler(Thread thread)
139+
{
140+
_thread = thread;
141+
}
142+
143+
public bool IsCancelCompleted => _cancelCompleted;
144+
145+
public void Cancel()
146+
{
147+
try
148+
{
149+
// Abort the thread executing the action (which may be the current thread).
150+
AbortThread(_thread.GetNativeHandle());
151+
}
152+
finally
153+
{
154+
_cancelCompleted = true;
155+
}
156+
}
157+
}
158+
}
159+
}

Diff for: src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@
190190
<Compile Include="System\Resources\ManifestBasedResourceGroveler.NativeAot.cs" />
191191
<Compile Include="System\RuntimeArgumentHandle.cs" />
192192
<Compile Include="System\RuntimeType.cs" />
193+
<Compile Include="System\Runtime\ControlledExecution.NativeAot.cs" />
193194
<Compile Include="System\Runtime\DependentHandle.cs" />
194195
<Compile Include="System\Runtime\CompilerServices\ForceLazyDictionaryAttribute.cs" />
195196
<Compile Include="System\Runtime\CompilerServices\EagerStaticClassConstructionAttribute.cs" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Threading;
5+
6+
namespace System.Runtime
7+
{
8+
public static class ControlledExecution
9+
{
10+
[Obsolete(Obsoletions.ControlledExecutionRunMessage, DiagnosticId = Obsoletions.ControlledExecutionRunDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
11+
public static void Run(Action action, CancellationToken cancellationToken)
12+
{
13+
throw new PlatformNotSupportedException();
14+
}
15+
}
16+
}

Diff for: src/coreclr/vm/arm64/asmhelpers.asm

+27
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#ifdef FEATURE_READYTORUN
2121
IMPORT DynamicHelperWorker
2222
#endif
23+
IMPORT HijackHandler
24+
IMPORT ThrowControlForThread
2325

2426
#ifdef FEATURE_USE_SOFTWARE_WRITE_WATCH_FOR_GC_HEAP
2527
IMPORT g_sw_ww_table
@@ -1028,6 +1030,31 @@ FaultingExceptionFrame_FrameOffset SETA SIZEOF__GSCookie
10281030
MEND
10291031

10301032

1033+
; ------------------------------------------------------------------
1034+
;
1035+
; Helpers for ThreadAbort exceptions
1036+
;
1037+
1038+
NESTED_ENTRY RedirectForThreadAbort2,,HijackHandler
1039+
PROLOG_SAVE_REG_PAIR fp,lr, #-16!
1040+
1041+
; stack must be 16 byte aligned
1042+
CHECK_STACK_ALIGNMENT
1043+
1044+
; On entry:
1045+
;
1046+
; x0 = address of FaultingExceptionFrame
1047+
;
1048+
; Invoke the helper to setup the FaultingExceptionFrame and raise the exception
1049+
bl ThrowControlForThread
1050+
1051+
; ThrowControlForThread doesn't return.
1052+
EMIT_BREAKPOINT
1053+
1054+
NESTED_END RedirectForThreadAbort2
1055+
1056+
GenerateRedirectedStubWithFrame RedirectForThreadAbort, RedirectForThreadAbort2
1057+
10311058
; ------------------------------------------------------------------
10321059
; ResolveWorkerChainLookupAsmStub
10331060
;

Diff for: src/coreclr/vm/arm64/asmmacros.h

+18
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,24 @@ __EndLabelName SETS "$FuncName":CC:"_End"
154154

155155
MEND
156156

157+
;-----------------------------------------------------------------------------
158+
; Macro used to check (in debug builds only) whether the stack is 16-bytes aligned (a requirement before calling
159+
; out into C++/OS code). Invoke this directly after your prolog (if the stack frame size is fixed) or directly
160+
; before a call (if you have a frame pointer and a dynamic stack). A breakpoint will be invoked if the stack
161+
; is misaligned.
162+
;
163+
MACRO
164+
CHECK_STACK_ALIGNMENT
165+
166+
#ifdef _DEBUG
167+
add x9, sp, xzr
168+
tst x9, #15
169+
beq %F0
170+
EMIT_BREAKPOINT
171+
0
172+
#endif
173+
MEND
174+
157175
;-----------------------------------------------------------------------------
158176
; The Following sets of SAVE_*_REGISTERS expect the memory to be reserved and
159177
; base address to be passed in $reg

Diff for: src/coreclr/vm/arm64/stubs.cpp

-6
Original file line numberDiff line numberDiff line change
@@ -918,12 +918,6 @@ PTR_CONTEXT GetCONTEXTFromRedirectedStubStackFrame(T_CONTEXT * pContext)
918918
return *ppContext;
919919
}
920920

921-
void RedirectForThreadAbort()
922-
{
923-
// ThreadAbort is not supported in .net core
924-
throw "NYI";
925-
}
926-
927921
#if !defined(DACCESS_COMPILE)
928922
FaultingExceptionFrame *GetFrameFromRedirectedStubStackFrame (DISPATCHER_CONTEXT *pDispatcherContext)
929923
{

Diff for: src/coreclr/vm/arm64/unixstubs.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ extern "C"
99
{
1010
PORTABILITY_ASSERT("Implement for PAL");
1111
}
12+
13+
void RedirectForThreadAbort()
14+
{
15+
PORTABILITY_ASSERT("Implement for PAL");
16+
}
1217
};

Diff for: src/coreclr/vm/comsynchronizable.cpp

+25-2
Original file line numberDiff line numberDiff line change
@@ -1096,15 +1096,38 @@ extern "C" BOOL QCALLTYPE ThreadNative_YieldThread()
10961096

10971097
BOOL ret = FALSE;
10981098

1099-
BEGIN_QCALL
1099+
BEGIN_QCALL;
11001100

11011101
ret = __SwitchToThread(0, CALLER_LIMITS_SPINNING);
11021102

1103-
END_QCALL
1103+
END_QCALL;
11041104

11051105
return ret;
11061106
}
11071107

1108+
extern "C" void QCALLTYPE ThreadNative_Abort(QCall::ThreadHandle thread)
1109+
{
1110+
QCALL_CONTRACT;
1111+
1112+
BEGIN_QCALL;
1113+
1114+
thread->UserAbort(EEPolicy::TA_Safe, INFINITE);
1115+
1116+
END_QCALL;
1117+
}
1118+
1119+
// Unmark the current thread for a safe abort.
1120+
extern "C" void QCALLTYPE ThreadNative_ResetAbort()
1121+
{
1122+
QCALL_CONTRACT_NO_GC_TRANSITION;
1123+
1124+
Thread *pThread = GetThread();
1125+
if (pThread->IsAbortRequested())
1126+
{
1127+
pThread->UnmarkThreadForAbort(EEPolicy::TA_Safe);
1128+
}
1129+
}
1130+
11081131
FCIMPL0(INT32, ThreadNative::GetCurrentProcessorNumber)
11091132
{
11101133
FCALL_CONTRACT;

Diff for: src/coreclr/vm/comsynchronizable.h

+2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ extern "C" void QCALLTYPE ThreadNative_InformThreadNameChange(QCall::ThreadHandl
103103
extern "C" UINT64 QCALLTYPE ThreadNative_GetProcessDefaultStackSize();
104104
extern "C" BOOL QCALLTYPE ThreadNative_YieldThread();
105105
extern "C" UINT64 QCALLTYPE ThreadNative_GetCurrentOSThreadId();
106+
extern "C" void QCALLTYPE ThreadNative_Abort(QCall::ThreadHandle thread);
107+
extern "C" void QCALLTYPE ThreadNative_ResetAbort();
106108

107109
#endif // _COMSYNCHRONIZABLE_H
108110

Diff for: src/coreclr/vm/qcallentrypoints.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ static const Entry s_QCall[] =
209209
DllImportEntry(ThreadNative_InformThreadNameChange)
210210
DllImportEntry(ThreadNative_YieldThread)
211211
DllImportEntry(ThreadNative_GetCurrentOSThreadId)
212+
DllImportEntry(ThreadNative_Abort)
213+
DllImportEntry(ThreadNative_ResetAbort)
212214
DllImportEntry(ThreadPool_GetCompletedWorkItemCount)
213215
DllImportEntry(ThreadPool_RequestWorkerThread)
214216
DllImportEntry(ThreadPool_PerformGateActivities)

Diff for: src/coreclr/vm/threads.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -2496,7 +2496,7 @@ class Thread
24962496

24972497
public:
24982498
void MarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType);
2499-
void UnmarkThreadForAbort();
2499+
void UnmarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType = EEPolicy::TA_Rude);
25002500

25012501
static ULONGLONG GetNextSelfAbortEndTime()
25022502
{

Diff for: src/coreclr/vm/threadsuspend.cpp

+7-4
Original file line numberDiff line numberDiff line change
@@ -1785,7 +1785,7 @@ void Thread::RemoveAbortRequestBit()
17851785
}
17861786

17871787
// Make sure that when AbortRequest bit is cleared, we also dec TrapReturningThreads count.
1788-
void Thread::UnmarkThreadForAbort()
1788+
void Thread::UnmarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType /* = EEPolicy::TA_Rude */)
17891789
{
17901790
CONTRACTL
17911791
{
@@ -1794,11 +1794,14 @@ void Thread::UnmarkThreadForAbort()
17941794
}
17951795
CONTRACTL_END;
17961796

1797-
// Switch to COOP (for ClearAbortReason) before acquiring AbortRequestLock
1798-
GCX_COOP();
1799-
18001797
AbortRequestLockHolder lh(this);
18011798

1799+
if (m_AbortType > (DWORD)abortType)
1800+
{
1801+
// Aborting at a higher level
1802+
return;
1803+
}
1804+
18021805
m_AbortType = EEPolicy::TA_None;
18031806
m_AbortEndTime = MAXULONGLONG;
18041807
m_RudeAbortEndTime = MAXULONGLONG;

Diff for: src/libraries/Common/src/System/Obsoletions.cs

+3
Original file line numberDiff line numberDiff line change
@@ -147,5 +147,8 @@ internal static class Obsoletions
147147

148148
internal const string CryptoStringFactoryMessage = "Cryptographic factory methods accepting an algorithm name are obsolete. Use the parameterless Create factory method on the algorithm type instead.";
149149
internal const string CryptoStringFactoryDiagId = "SYSLIB0045";
150+
151+
internal const string ControlledExecutionRunMessage = "ControlledExecution.Run method may corrupt the process and should not be used in production code.";
152+
internal const string ControlledExecutionRunDiagId = "SYSLIB0046";
150153
}
151154
}

0 commit comments

Comments
 (0)