Skip to content

Conversation

MichalStrehovsky
Copy link
Member

@MichalStrehovsky MichalStrehovsky commented Aug 20, 2025

The problem is that the first time we reverse p/invoke into an API in the shared library, we need to do two things: initialize runtime and run module initializers. Running module initializers is done as part of runtime initialization in the shared library case because it was convenient to do it there (in the EXE case, we run them after runtime initialization, right before Main).

The problem with running it as part of the runtime initialization is that the arbitrary user code can deadlock us (see the issues for stack).

This fixes the problem by attaching the newly created thread to the runtime before we do the reverse p/invoke. Since thread is already attached, we don't wait for runtime initialization. Waiting is unnecessary because the runtime needs to be really far along the initialization path if we got to a managed Thread.Start call.

Fixes #118773. Fixes #107699.

Cc @dotnet/ilc-contrib

… library

The problem is that the first time we reverse p/invoke into an API in the shared library, we need to do two things: initialize runtime and run module initializers. Running module initializers is done as part of runtime initialization in the shared library case because it was convenient to do it there (in the EXE case, we run them after runtime initialization, right before Main).

The problem with running it as part of the runtime initialization is that the arbitrary user code can deadlock us (see the issues for stack).

This separates runtime initialization and running module initializers. Doing just that doesn't fix the problem though because the deadlock we saw is part of thread creation. So we'd just deadlock on the module initializer runner. This introduces a new kind of reverse p/invoke enter routine that doesn't wait for module initializers. Instead we wait for module initializers before we are ready to run user code on the thread (and the thread is initialized just enough that ThreadStart can make progress).

This also fixes a potential issue in the EXE case where threads created from module initializers could start running before we're finished running module initializers. Instead this introduces a new opportunity for deadlocking :(.

I don't actually like this fix. But we need to do something about it because EventSource does exactly this (dotnet#118773).

Fixes dotnet#118773. Fixes dotnet#107699.
@Copilot Copilot AI review requested due to automatic review settings August 20, 2025 14:27
Copy link
Contributor

Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas
See info in area-owners.md if you want to be subscribed.

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes a deadlock issue that occurs when threads are created from ModuleInitializer methods in shared libraries. The problem was that runtime initialization and module initializer execution were tightly coupled, causing deadlocks when module initializers created threads that needed reverse p/invoke transitions.

Key changes:

  • Separates runtime initialization from module initializer execution
  • Introduces a "lite" reverse p/invoke transition that doesn't wait for module initializers
  • Adds manual synchronization to ensure module initializers complete before user code runs on new threads

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
SharedLibrary.cs Adds regression test that creates a thread in ModuleInitializer
NativeLibraryStartupMethod.cs Removes module initializer execution from runtime initialization
CorInfoImpl.cs Special-cases ThreadEntryPoint to use lite reverse p/invoke transition
Thread.NativeAot.cs Adds manual wait for module initializers before running user code
Thread.NativeAot.Windows.cs Documents special compiler handling of ThreadEntryPoint
Thread.NativeAot.Unix.cs Documents special compiler handling of ThreadEntryPoint
thread.h Adds EnsureModuleInitializersExecuted method and parameter to existing method
thread.cpp Implements separated runtime and module initializer logic
StartupCodeHelpers.cs Adds volatile flag and C-callable wrapper for module initializers
main.cpp Updates callback registration to handle both runtime and module initialization

@VSadov
Copy link
Member

VSadov commented Aug 20, 2025

This also fixes a potential issue in the EXE case where threads created from module initializers could start running before we're finished running module initializers. Instead, this introduces a new opportunity for deadlocking :(.

I think also if a module initializer drops a finalizable object, finalizer thread may run without waiting. That is because we pre-attach the finalizer thread. Perhaps we should not. I do not think it is necessary to pre-attach.

@VSadov
Copy link
Member

VSadov commented Aug 20, 2025

I don't actually like this fix. But we need to do something about it because EventSource does exactly this (#118773).

I kind of feel the same. We may yet find some other ways the scenario can get us in trouble. The fix does seem to solve the issue at hand.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

LGTM otherwise. Thanks!

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky MichalStrehovsky enabled auto-merge (squash) August 23, 2025 11:14
@MichalStrehovsky MichalStrehovsky merged commit bf795cb into dotnet:main Aug 23, 2025
100 of 102 checks passed
@MichalStrehovsky MichalStrehovsky deleted the fix107699 branch August 25, 2025 10:44
@MichalStrehovsky
Copy link
Member Author

@jkotas @VSadov what do you think about the risk with taking this to .NET 10? From what I can judge, this is not doing anything too special/new, but I'm no GC expert. Maybe we could take this to 10?

@jkotas
Copy link
Member

jkotas commented Aug 25, 2025

It is fine with me to take it for .NET 10.

@MichalStrehovsky
Copy link
Member Author

/backport to release/10.0

Copy link
Contributor

Started backporting to release/10.0: https://github.com/dotnet/runtime/actions/runs/17228890766

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants