Skip to content
Closed
Changes from all 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
10 changes: 10 additions & 0 deletions src/coreclr/vm/ceemain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1680,6 +1680,16 @@ static void RuntimeThreadShutdown(void* thread)
GCX_COOP_NO_DTOR_END();
}

#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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for example when the process exits from managed code

What is the actual stacktrace how we end up here in cooperative mode?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's JS setTimeout() -> C# threadpool -> xunit last item -> Environment.Exit -> posix exit() -> emscripten thread destructor

Copilot thinks:

  1. Native/JS → ExecuteInterpretedMethodFromUnmanaged(pMD, …) — prestub.cpp. These are the per-method reverse thunks generated in wasm/browser/callhelpers-reverse.cpp, e.g. CallEntryPoint (managed Main), CallJSExport, CallDelegate, CompleteTask (JS interop), class-ctor/default-ctor helpers, etc.
  2. ExecuteInterpretedMethodWithArgs — prestub.cpp
  3. ExecuteInterpretedMethod — prestub.cpp, which first asserts the caller is preemptive (if (thread->PreemptiveGCDisabled()) ReversePInvokeBadTransition();), then does the GCX_MAYBE_COOP transition above.
  4. InterpExecMethod runs the bytecode in MODE_COOPERATIVE (interpexec.cpp), toggling back to preemptive with GCX_PREEMP_NO_DTOR() only for outbound unmanaged/P-Invoke calls (interpexec.cpp).

I will try to catch the real native stack.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can InvokeCalliStub -> CallFunc_This_RetI32_PE catch that JS exception/trap ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RuntimeError: function signature mismatch

This should be fatal error that makes us abort immediately.

@pavelsavara pavelsavara Jul 11, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a way, this is abort. It's on emscripten/js level. Perhaps abort needs to uninstall __funcs_on_exit callbacks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// drives this path runs without the thread first returning to native/preemptive code.
// DetachThread requires preemptive mode (it asserts !PreemptiveGCDisabled()). The frame was
// reset to FRAME_TOP above, so the dying thread has no GC-scannable frames and it is safe to
// switch to preemptive here (no restore needed - the thread is being detached).
GCX_PREEMP_NO_DTOR();
#endif // TARGET_WASM

pThread->DetachThread(TRUE);
}
else
Expand Down
Loading