Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 16, 2025

Fixes: #12735 #12645

Context

A race condition in the TypeLoader class caused System.ObjectDisposedException crashes when building projects with /mt /m (multi-threaded mode). The error occurred when multiple threads simultaneously loaded task types using MetadataLoadContext:

MSBUILD : error : System.ObjectDisposedException: This object is no longer valid because the MetadataLoadContext that created it has been disposed.
   at System.Reflection.TypeLoading.RoProperty.get_Name()
   at Microsoft.Build.Execution.ReflectableTaskPropertyInfo..ctor(PropertyInfo propertyInfo, ...)
   at Microsoft.Build.Shared.LoadedType..ctor(Type type, ...)

Root Cause

The MetadataLoadContext _context field in TypeLoader.cs (line 46) was declared as static, causing it to be shared across all threads:

  1. Thread A creates a MetadataLoadContext and stores it in the static _context field
  2. Thread B overwrites the static _context with its own MetadataLoadContext
  3. Thread A attempts to access properties on types loaded from its (now-replaced) context
  4. Thread B disposes the _context
  5. Thread A crashes with ObjectDisposedException when accessing propertyInfo.Name

Changes Made

Made the MetadataLoadContext instance-local instead of static to eliminate the shared state:

  1. Removed the static _context field - Eliminated the race condition source
  2. Renamed and refactored CreateMetadataLoadContext - Changed from LoadAssemblyUsingMetadataLoadContext to return only MetadataLoadContext to enable idiomatic C# resource management with using statement. The function name now accurately reflects that it only creates the context without loading assemblies.
  3. Updated GetLoadedTypeFromTypeNameUsingMetadataLoadContext - Uses using statement for automatic disposal instead of manual disposal in finally block. Assembly loading now happens separately after context creation.

Why This Works

The MetadataLoadContext is only used to extract metadata (property names, types, attributes) which is stored as strings/primitives during LoadedType construction. All metadata extraction completes before context disposal via the using statement. The actual task execution happens in the TaskHost process where assemblies are loaded normally, so no access to disposed contexts occurs during runtime.

Testing

  • ✅ Full build completes successfully
  • ✅ All TypeLoader unit tests pass
  • ✅ All 59 TaskRegistry unit tests pass
  • ✅ Sample projects build correctly

Notes

The use of C#'s using statement for resource disposal follows .NET best practices and makes the code cleaner and more maintainable compared to manual disposal in a finally block. The function naming has been updated to accurately reflect its purpose (creating a context rather than loading an assembly).


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@dotnet-policy-service
Copy link
Contributor

Hello @@copilot, I noticed that you’re changing an .swr file or any file under src/Package/MSBuild.VSSetup.. Please make sure to validate this change by an experimental VS insertion. This is accomplished by pushing to an exp/* branch, which requires write permissions to this repo.

The race condition occurred when multiple threads were loading types using MetadataLoadContext in /mt mode:
- Thread A would create a MetadataLoadContext and store it in the static _context field
- Thread B would overwrite the static _context with its own MetadataLoadContext
- Thread A would try to access properties on types loaded from the now-replaced context
- Thread B would dispose the _context, causing ObjectDisposedException in Thread A

The fix:
- Removed the static _context field that was being shared across threads
- Made LoadAssemblyUsingMetadataLoadContext return both the assembly and context as a tuple
- Changed GetLoadedTypeFromTypeNameUsingMetadataLoadContext to use a local context variable
- Wrapped all reflection operations in a try-finally to ensure proper disposal of the context
- The context is now disposed only after all reflection operations on the loaded types are complete

Co-authored-by: AR-May <[email protected]>
Copilot AI changed the title [WIP] Fix race condition in TaskRegistry in /mt mode Fix race condition in TaskRegistry/TypeLoader when building with /mt /m mode Oct 16, 2025
Copilot finished work on behalf of AR-May October 16, 2025 15:06
Copilot AI requested a review from AR-May October 16, 2025 15:06
@JanProvaznik JanProvaznik marked this pull request as ready for review November 19, 2025 16:35
Copilot AI review requested due to automatic review settings November 19, 2025 16:35
Copilot finished reviewing on behalf of JanProvaznik November 19, 2025 16:38
Copy link
Contributor

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 race condition in the TypeLoader class that caused System.ObjectDisposedException crashes when building with /mt /m (multi-threaded mode). The root cause was a static MetadataLoadContext _context field being shared across threads, leading to one thread disposing the context while another thread was still using types loaded from it.

Key Changes

  • Removed the static _context field to eliminate shared state between threads
  • Modified LoadAssemblyUsingMetadataLoadContext to return a tuple containing both the assembly and its associated context
  • Wrapped type-finding logic in a try-finally block to ensure proper disposal of each thread's local context after metadata extraction

Copy link
Member

@JanProvaznik JanProvaznik left a comment

Choose a reason for hiding this comment

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

I think this fixes the issue appropriately. There was no reason for the context to be in a field, and the disposal pattern is corrected

Changed LoadAssemblyUsingMetadataLoadContext to return only the MetadataLoadContext instead of a tuple,
allowing the calling code to use the C# 'using' statement for cleaner resource management.
This addresses the code review feedback to use a using statement instead of manual disposal in finally block.

Co-authored-by: AR-May <[email protected]>
Copilot finished work on behalf of AR-May November 20, 2025 12:32
The function now only creates and returns a MetadataLoadContext (it no longer loads the assembly),
so the name has been updated to accurately reflect its purpose.

Co-authored-by: AR-May <[email protected]>
Copilot finished work on behalf of AR-May November 20, 2025 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

task instantiation occassionaly fails in -mt mode

3 participants