[wasm][coreCLR] Switch to preemptive GC mode before DetachThread in RuntimeThreadShutdown#130497
[wasm][coreCLR] Switch to preemptive GC mode before DetachThread in RuntimeThreadShutdown#130497pavelsavara wants to merge 1 commit into
DetachThread in RuntimeThreadShutdown#130497Conversation
There was a problem hiding this comment.
Pull request overview
This PR adjusts CoreCLR’s thread shutdown path on TARGET_WASM so that a thread transitions to preemptive GC mode before calling Thread::DetachThread(TRUE) in RuntimeThreadShutdown. This aligns with DetachThread’s requirement that the caller is not in cooperative mode (it asserts !PreemptiveGCDisabled()).
Changes:
- Add a
#ifdef TARGET_WASMblock inRuntimeThreadShutdownto enter preemptive GC mode viaGCX_PREEMP_NO_DTOR()immediately beforepThread->DetachThread(TRUE). - Add a comment explaining why the wasm TLS-destructor-driven shutdown path can still be in cooperative mode and why switching is safe after resetting the frame to
FRAME_TOP.
|
Failure is known #130479 |
|
|
||
| #ifdef TARGET_WASM | ||
| // On wasm the thread can still be in cooperative GC mode at shutdown (for example when the | ||
| // process exits from managed code): unlike other OSes, the thread-local destructor that |
There was a problem hiding this comment.
for example when the process exits from managed code
What is the actual stacktrace how we end up here in cooperative mode?
There was a problem hiding this comment.
I think it's JS setTimeout() -> C# threadpool -> xunit last item -> Environment.Exit -> posix exit() -> emscripten thread destructor
Copilot thinks:
- Native/JS →
ExecuteInterpretedMethodFromUnmanaged(pMD, …)— prestub.cpp. These are the per-method reverse thunks generated in wasm/browser/callhelpers-reverse.cpp, e.g.CallEntryPoint(managedMain),CallJSExport,CallDelegate,CompleteTask(JS interop), class-ctor/default-ctor helpers, etc. - →
ExecuteInterpretedMethodWithArgs— prestub.cpp - →
ExecuteInterpretedMethod— prestub.cpp, which first asserts the caller is preemptive (if (thread->PreemptiveGCDisabled()) ReversePInvokeBadTransition();), then does theGCX_MAYBE_COOPtransition above. - →
InterpExecMethodruns the bytecode inMODE_COOPERATIVE(interpexec.cpp), toggling back to preemptive withGCX_PREEMP_NO_DTOR()only for outbound unmanaged/P-Invoke calls (interpexec.cpp).
I will try to catch the real native stack.
There was a problem hiding this comment.
This is one I can easily reproduce, but it's not the one which asserts.
RuntimeThreadShutdown(void*) (ceemain.cpp:1683)
TlsDestructionMonitor::~TlsDestructionMonitor() (ceemain.cpp:1812)
$__cxx_global_array_dtor
__cxxabiv1::(anonymous namespace)::run_dtors(void*) (cxa_thread_atexit.cpp:79)
__cxxabiv1::(anonymous namespace)::DtorsManager::~DtorsManager() (cxa_thread_atexit.cpp:102)
$__cxx_global_array_dtor
__funcs_on_exit (atexit.c:34)
(anonymous) (dotnet.native.93kurx3rql.js:565)
exitRuntime (dotnet.native.93kurx3rql.js:484)
exitJS (dotnet.native.93kurx3rql.js:6476)
abortPosix (dotnet.native.93kurx3rql.js:4135)
quitNow (dotnet.js:514)
exit (dotnet.js:502)
(anonymous) (main.js:33)
There was a problem hiding this comment.
Right, I would expect C runtime exit to be always called in preemptive mode. I think the problem should be fixed closer to where we are calling C runtime exit in cooperative mode.
There was a problem hiding this comment.
This is broken call with R2R into UCO which causes subsequent posix exit() but the GC mode is off-balance already.
RuntimeError: function signature mismatch
at System.Runtime.InteropServices.JavaScript.Tests.9e6h66ejnv.wasm:0x1f0fa4
at dotnet.native.wasm.(anonymous namespace)::CallFunc_This_RetI32_PE(unsigned long, signed char*, signed (anonymous namespace)::CallFunc_This_RetI32_PE(unsigned long, signed char*, signed char*) (callhelpers-interp-to-managed.cpp:180)
at dotnet.native.wasm.InvokeCalliStub(unsigned long, void (*)(unsigned long, signed char*, signed char*), signed char*, signed char*, InvokeCalliStub(unsigned long, void (*)(unsigned long, signed char*, signed char*), signed char*, signed char*, Object**) (helpers.cpp:891)
at dotnet.native.wasm.InvokeManagedMethod(MethodDesc*, signed char*, signed char*, unsigned long, InvokeManagedMethod(MethodDesc*, signed char*, signed char*, unsigned long, Object**) (helpers.cpp:1483)
at dotnet.native.wasm.InterpExecMethod(InterpreterFrame*, InterpMethodContextFrame*, InterpThreadContext*, InterpExecMethod(InterpreterFrame*, InterpMethodContextFrame*, InterpThreadContext*, ExceptionClauseArgs*) (interpexec.cpp:3518)
at ::ExecuteInterpretedMethod(TransitionBlock *, TADDR, void *) (prestub.cpp:2093)
at dotnet.native.wasm.ExecuteInterpretedMethodWithArgs(unsigned long, signed char*, unsigned long, void*, unsigned ExecuteInterpretedMethodWithArgs(unsigned long, signed char*, unsigned long, void*, unsigned long) (prestub.cpp:2153)
at ::ExecuteInterpretedMethodFromUnmanaged(MethodDesc *, int8_t *, size_t, int8_t *, PCODE) (prestub.cpp:2286)
at Call_System_Private_CoreLib_System_Threading_ThreadPool_BackgroundJobHandler_Void_RetVoid() (callhelpers-reverse.cpp:38)
at ::SystemJS_ExecuteBackgroundJobCallback() (callhelpers-reverse.cpp:43)
::DebugBreak() (debug.cpp:414)
Thread::DetachThread(int) (threads.cpp:877)
RuntimeThreadShutdown(void*) (ceemain.cpp:1683)
TlsDestructionMonitor::~TlsDestructionMonitor() (ceemain.cpp:1812)
$__cxx_global_array_dtor
__cxxabiv1::(anonymous namespace)::run_dtors(void*) (cxa_thread_atexit.cpp:79)
__cxxabiv1::(anonymous namespace)::DtorsManager::~DtorsManager() (cxa_thread_atexit.cpp:102)
$__cxx_global_array_dtor
__funcs_on_exit (atexit.c:34)
(anonymous) (dotnet.native.vp36jarcur.js:565)
exitRuntime (dotnet.native.vp36jarcur.js:484)
exitJS (dotnet.native.vp36jarcur.js:6534)
maybeExit (dotnet.native.vp36jarcur.js:6860)
callUserCallback (dotnet.native.vp36jarcur.js:6876)
(anonymous) (dotnet.native.vp36jarcur.js:6889)
setTimeout
safeSetTimeout (dotnet.native.vp36jarcur.js:6887)
_SystemJS_ScheduleBackgroundJob (dotnet.native.vp36jarcur.js:5391)
There was a problem hiding this comment.
Can InvokeCalliStub -> CallFunc_This_RetI32_PE catch that JS exception/trap ?
There was a problem hiding this comment.
RuntimeError: function signature mismatch
This should be fatal error that makes us abort immediately.
Summary
On wasm (CoreCLR interpreter), a thread can reach
RuntimeThreadShutdown→Thread::DetachThread(TRUE)while still in cooperative GC mode.
DetachThreadrequires preemptive mode and asserts it:This is a Debug/Checked-only
_ASSERTE, so it aborts the process on Debug and Checked wasm/CoreCLR runtimes.This change transitions the thread to preemptive before
DetachThreadon wasm, matching what the siblingDestroyThreadpath already does.Root cause
Managed code on wasm executes in the interpreter, which runs in cooperative GC mode. The thread transitions
into cooperative mode at the reverse-pinvoke /
UnmanagedCallersOnlyboundary inExecuteInterpretedMethod(
src/coreclr/vm/prestub.cpp):GCX_MAYBE_COOP(pInterpreterCode->Method->unmanagedCallersOnly);