Skip to content

Fix CLR_E_SHIM_RUNTIMELOAD in RAR's IMetaDataDispenser activation - #13899

Merged
rainersigwald merged 2 commits into
dotnet:mainfrom
JeremyKuhne:fix/rar-cor-metadata-dispenser-activation
May 29, 2026
Merged

Fix CLR_E_SHIM_RUNTIMELOAD in RAR's IMetaDataDispenser activation#13899
rainersigwald merged 2 commits into
dotnet:mainfrom
JeremyKuhne:fix/rar-cor-metadata-dispenser-activation

Conversation

@JeremyKuhne

Copy link
Copy Markdown
Member

Context

Fixes a regression in RAR introduced by #13853 (CsWin32 CLR metadata + TypeLib interop migration), reported by the VC team as VC.Tests.MsBuild.VC.MsBuild.P2PReferences.08 (CLR+CLR+CLR P2P chain): Referenced_C_IJW.dll is missing from Referencing_A_IJW\Debug\ after build.

Root cause

The migration switched AssemblyInformation / MetadataReader from RCW-based COM to struct-based COM through CsWin32 and AgileComPointer, and along the way activates CLSID_CorMetaDataDispenser via raw CoCreateInstance.

CLSID_CorMetaDataDispenser is registered with mscoree.dll (the .NET Framework shim) as its InprocServer32. CoCreateInstance therefore loads the shim, which then has to call LoadLibraryShim to bind a runtime before it can delegate to MetaDataDllGetClassObject.

In hosts that did not enter the CLR via mscoree's startup path — notably the VC cppxplatdev test harness, which embeds MSBuild in-process via BuildManager from a non-mscoree-rooted process — the shim's bound-runtime state is uninitialized and LoadLibraryShim fails with CLR_E_SHIM_RUNTIMELOAD (0x80131700, "Failed to load the runtime"). RAR catches that as a DependencyResolutionException and silently drops every transitive dependency for the failing reference. End-to-end symptom is the missing Referenced_C_IJW.dll copy.

Standalone msbuild.exe cannot reproduce because the msbuild.exe process enters the CLR via the standard mscoree startup path, so the shim is bound.

Fix

Activate the dispenser by calling clr.dll's exported DllGetClassObjectInternal directly via a new ComClassFactory.TryCreateFromModule overload, bypassing the shim entirely. This is exactly what the CLR's own managed-COM activation does internally for CLSIDs on its IsClrHostedLegacyComObject list. The approach is AOT-friendly (no Type.GetTypeFromCLSID, no Activator, no RCW), so .NET 10+ source-generated COM can use it unchanged.

ComClassFactory

  • Two public TryCreateFromModule overloads (standard DllGetClassObject and caller-specified export name).
  • Public ClrDllGetClassObjectInternalExportName constant with documentation of the shim mechanism.
  • Single delegate* unmanaged[Stdcall]<...> function-pointer path serves both net472 and .NET Core.

Callers

  • AssemblyInformation and MetadataReader now use TryCreateFromModule("clr.dll", CLSID, ClrDllGetClassObjectInternalExportName, ...).
  • Both retain the previous [PreserveSig] tolerance for GetAssemblyFromScope and GetPEKind failures (the legacy RCW code silently ignored those HRs).
  • Pointer release moved to DisposeUnmanagedResources so the finalizer path still revokes GIT cookies if the user forgets to dispose.

Other

  • NativeMethods.txt: add GetModuleHandle (kept for completeness — LoadLibrary, FreeLibrary, GetProcAddress were already present).
  • New net472-only AssemblyInformation_Tests covering both file-mapping lifetime (delete/overwrite after Dispose, including a repeated open-close stress loop) and dispenser activation.

Validation

  • Local unit tests: 8/8 new + 12/12 existing MetadataReader_Tests pass on net472 x86.
  • End-to-end: built Microsoft.Build.Framework.dll + Microsoft.Build.Tasks.Core.dll deployed into a local VS18 install, ran the failing Project.vcxproj directly with msbuild.exe against the actual VC P2PReferences asset — both Referenced_B_IJW.dll and Referenced_C_IJW.dll now land in Referencing_A_IJW\Debug\.
  • Build clean, 0 warnings/0 errors.

Cc / fyi @rainersigwald @JaynieBai @YuliiaKovalova — needs the VC team to re-run P2PReferences.08 with these binaries to confirm in their CI.

Copilot AI review requested due to automatic review settings May 29, 2026 01:31
@JeremyKuhne
JeremyKuhne requested a review from a team as a code owner May 29, 2026 01:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 .NET Framework RAR regression when activating CLSID_CorMetaDataDispenser in hosts that embed MSBuild without going through the mscoree.dll shim startup path (triggering CLR_E_SHIM_RUNTIMELOAD). It does so by bypassing the shim and activating the metadata dispenser via clr.dll’s internal class-object export, then adds targeted net472-only regression tests around AssemblyInformation lifetime/activation.

Changes:

  • Update AssemblyInformation and ManifestUtil.MetadataReader (net472 path) to activate IMetaDataDispenser via ComClassFactory.TryCreateFromModule("clr.dll", ..., "DllGetClassObjectInternal", ...) instead of raw CoCreateInstance.
  • Extend ComClassFactory with module-export-based activation helpers and document the mscoree-shim failure mechanism.
  • Add new net472-only AssemblyInformation_Tests covering mapping lifetime and activation sanity.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Tasks/ManifestUtil/MetadataReader.cs Switch net472 COM activation to module-export path and preserve legacy HR-tolerance behavior.
src/Tasks/AssemblyDependency/AssemblyInformation.cs Same activation change; move COM pointer cleanup to unmanaged dispose path to cover finalizer scenarios.
src/Tasks.UnitTests/AssemblyDependency/AssemblyInformation_Tests.cs New net472-only regression tests for mapping lifetime and activation sanity.
src/Framework/Windows/Win32/System/Com/ComClassFactory.cs Add TryCreateFromModule overloads and CLR-specific export constant/docs.
src/Framework/NativeMethods.txt Add GetModuleHandle to the Win32 import list.

Comment thread src/Framework/Windows/Win32/System/Com/ComClassFactory.cs
Comment thread src/Framework/Windows/Win32/System/Com/ComClassFactory.cs
Comment thread src/Tasks.UnitTests/AssemblyDependency/AssemblyInformation_Tests.cs Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review of PR #13899 — Fix CLR_E_SHIM_RUNTIMELOAD regression in embedded-host scenarios

The fix correctly identifies the root cause: raw CoCreateInstance on CLSID_CorMetaDataDispenser delegates to mscoree.dll, whose LoadLibraryShim fails (0x80131700) in native-embedded hosts where the shim's startup state isn't initialized. Calling DllGetClassObjectInternal directly on the already-loaded clr.dll bypasses the shim entirely and reproduces what the CLR's own managed-COM activation path does internally. The approach is sound.

The HRESULT-tolerance changes for GetAssemblyFromScope and GetPEKind correctly restore the legacy [PreserveSig] contract that was inadvertently tightened by #13853.

No BLOCKING or MAJOR issues found.

# Dimension Verdict
22 Correctness 🟡 1 MODERATE — DisposeUnmanagedResources + managed Dispose is safe but comment is incomplete
24 Security 🟡 1 MODERATE — LoadLibrary DLL search order; GetModuleHandle was added to NativeMethods.txt but unused
3 Performance 🟡 1 MODERATE — LoadLibrary+GetProcAddress+DllGetClassObject per construction; no caching
14 Naming/NIT 🔵 3 NITs — GetModuleHandle dead entry, machine variable, public const on internal class

✅ 20/24 dimensions clean.

  • CorrectnessDisposeUnmanagedResources comment should explain why calling AgileComPointer.Dispose() from the finalizer path is safe (CLR field-reachability guarantee + Interlocked.Exchange guard in AgileComPointer)
  • Security/CorrectnessGetModuleHandle was added to NativeMethods.txt but LoadLibrary (with DLL search order) is used instead; GetModuleHandle is the right call for a module that must already be loaded
  • PerformanceLoadLibrary + GetProcAddress + DllGetClassObjectInternal on every AssemblyInformation construction; cache at least the FARPROC

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

  • #13899 pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
  • #13899 pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by Expert Code Review (on open) for issue #13899 · ● 3.7M

Comment thread src/Tasks/AssemblyDependency/AssemblyInformation.cs
Comment thread src/Tasks/AssemblyDependency/AssemblyInformation.cs Outdated
Comment thread src/Framework/NativeMethods.txt
Comment thread src/Framework/Windows/Win32/System/Com/ComClassFactory.cs
Comment thread src/Framework/Windows/Win32/System/Com/ComClassFactory.cs Outdated
Comment thread src/Framework/Windows/Win32/System/Com/ComClassFactory.cs Outdated
Comment thread src/Framework/Windows/Win32/System/Com/ComClassFactory.cs Outdated
Comment thread src/Framework/NativeMethods.txt
Comment thread src/Framework/Windows/Win32/System/Com/ComClassFactory.cs
Comment thread src/Tasks/AssemblyDependency/AssemblyInformation.cs Outdated
Commit 5682672 (dotnet#13853) migrated AssemblyInformation / MetadataReader
from RCW-based COM to struct-based COM through CsWin32 and AgileComPointer.
The new code activates CLSID_CorMetaDataDispenser via raw CoCreateInstance.
That loads mscoree.dll (the .NET Framework shim), which in turn calls
LoadLibraryShim to bind a runtime before delegating to MetaDataDllGetClassObject.

In hosts that did not enter the CLR via mscoree (notably the VC cppxplatdev
test harness, which embeds MSBuild in-process via BuildManager) the shim's
bound-runtime state is uninitialized and LoadLibraryShim returns
CLR_E_SHIM_RUNTIMELOAD (0x80131700) "Failed to load the runtime". RAR catches
that as a DependencyResolutionException and silently drops every transitive
dependency for the failing reference, manifesting as VC's P2PReferences.08
regression (Referenced_C_IJW.dll missing from Referencing_A_IJW\Debug).

The fix activates the dispenser by calling clr.dll's exported
DllGetClassObjectInternal directly via a new
ComClassFactory.TryCreateFromModule overload, bypassing the shim entirely.
This is what the CLR's own managed-COM activation does internally for CLSIDs
in IsClrHostedLegacyComObject. The approach is also AOT-friendly (no
Type.GetTypeFromCLSID, no Activator, no RCW), so .NET 10+ source-generated
COM can use it unchanged.

* ComClassFactory: add two public TryCreateFromModule overloads (standard
  DllGetClassObject and caller-specified export name) plus
  ClrDllGetClassObjectInternalExportName constant. Single Stdcall function-
  pointer path serves both TFMs.
* AssemblyInformation, MetadataReader: switch dispenser activation to
  TryCreateFromModule("clr.dll", ..., ClrDllGetClassObjectInternalExportName).
  Tolerate GetAssemblyFromScope / GetPEKind failures (preserve [PreserveSig]
  semantics from the previous RCW path).
* NativeMethods.txt: add GetModuleHandle (kept for completeness; LoadLibrary,
  FreeLibrary, GetProcAddress were already present).
* AssemblyInformation_Tests: new net472-only tests covering both file-mapping
  lifetime (delete/overwrite after Dispose) and dispenser activation.
@JeremyKuhne
JeremyKuhne force-pushed the fix/rar-cor-metadata-dispenser-activation branch from 88fbf5f to 5b3733c Compare May 29, 2026 02:58
Comment thread src/Tasks/AssemblyDependency/AssemblyInformation.cs Outdated
Comment thread src/Framework/Windows/Win32/System/Com/ComClassFactory.cs Outdated
@JeremyKuhne
JeremyKuhne enabled auto-merge (squash) May 29, 2026 19:43
@rainersigwald
rainersigwald disabled auto-merge May 29, 2026 21:50
@rainersigwald

Copy link
Copy Markdown
Member

Bypassing macOS since the queues are long and this is a Windows-only fix.

@rainersigwald
rainersigwald merged commit ad1f4a6 into dotnet:main May 29, 2026
6 of 9 checks passed
@JeremyKuhne
JeremyKuhne deleted the fix/rar-cor-metadata-dispenser-activation branch May 30, 2026 16:52
This was referenced Aug 12, 2026
WarperSan pushed a commit to WarperSan/ThunderPipe that referenced this pull request Aug 12, 2026
Updated
[Microsoft.Build.Utilities.Core](https://github.com/dotnet/msbuild) from
18.8.2 to 18.9.6.

<details>
<summary>Release notes</summary>

_Sourced from [Microsoft.Build.Utilities.Core's
releases](https://github.com/dotnet/msbuild/releases)._

## 18.9.6

## What's Changed
* [vs18.6] Update dependencies from dotnet/arcade by
@​dotnet-maestro[bot] in dotnet/msbuild#13793
* [vs18.0] Update dependencies from dotnet/arcade by
@​dotnet-maestro[bot] in dotnet/msbuild#13859
* [vs18.6] Update dependencies from dotnet/arcade by
@​dotnet-maestro[bot] in dotnet/msbuild#13858
* [vs18.7] Update dependencies from dotnet/arcade by
@​dotnet-maestro[bot] in dotnet/msbuild#13863
* CsWin32 follow-up: CLR metadata + TypeLib interop migration by
@​JeremyKuhne in dotnet/msbuild#13853
* Update vmr-sb-validation.yml for Azure Pipelines by @​meghnave in
dotnet/msbuild#13871
* Test: keep shell alive 15s in ToolTaskCanChangeCanonicalErrorFormat
(#​13734) by @​jankratochvilcz in
dotnet/msbuild#13878
* Add vs18.8 to merge-flow config by @​OvesN in
dotnet/msbuild#13877
* Stable branding for 18.8 release by @​OvesN in
dotnet/msbuild#13883
* Bump main to 18.9.0 after vs18.8 snap by @​OvesN in
dotnet/msbuild#13880
* Avoid checkout in insertion pipeline by @​rainersigwald in
dotnet/msbuild#13887
* Report actual launch path in MSB4216 for Runtime="NET" task host by
@​ViktorHofer in dotnet/msbuild#13889
* Migrate Tlblmp and AxImp to Multithreaded Execution by @​AlesProkop in
dotnet/msbuild#13708
* Replace ErrorUtilities assertion methods with Assumed API and BCL
throw helpers by @​DustinCampbell in
dotnet/msbuild#13790
* Fix CLR_E_SHIM_RUNTIMELOAD in RAR's IMetaDataDispenser activation by
@​JeremyKuhne in dotnet/msbuild#13899
* [main] Update dependencies from nuget/nuget.client by
@​dotnet-maestro[bot] in dotnet/msbuild#13905
* [main] Update dependencies from dotnet/arcade by @​dotnet-maestro[bot]
in dotnet/msbuild#13907
* [main] Update dependencies from dotnet/roslyn by @​dotnet-maestro[bot]
in dotnet/msbuild#13910
* [vs17.14] Update dependencies from dotnet/arcade by
@​dotnet-maestro[bot] in dotnet/msbuild#13908
* [vs18.0] Update dependencies from dotnet/arcade by
@​dotnet-maestro[bot] in dotnet/msbuild#13906
* Fix ToolTask output loss: increase EOF pipe timeout from 2s to 30s by
@​huulinhnguyen-dev in dotnet/msbuild#13767
* Improve symlink cycle condition by @​GangWang01 in
dotnet/msbuild#13901
* [vs18.6] Update dependencies from dotnet/arcade by
@​dotnet-maestro[bot] in dotnet/msbuild#13904
* Add flaky-test detection and auto-fix agentic workflows by
@​ViktorHofer in dotnet/msbuild#13915
* Quote --ignore-exit-code values so the quarantine pipeline does not
shell-split on Unix by @​ViktorHofer in
dotnet/msbuild#13918
* Fix flaky-test detector PR-evidence loss and raise scan limits by
@​ViktorHofer in dotnet/msbuild#13919
* Tighten the pr review agent by @​JanKrivanek in
dotnet/msbuild#13921
* Add environment variables for governance detection by @​ViktorHofer in
dotnet/msbuild#13920
* Change IsPackable to true and add IsShipping flag by @​ViktorHofer in
dotnet/msbuild#13924
* [vs18.7] Update dependencies from dotnet/arcade by
@​dotnet-maestro[bot] in dotnet/msbuild#13911
* Make flaky detector verify recurrence postdates the fix before
commenting by @​ViktorHofer in
dotnet/msbuild#13930
* Fix AbsolutePath.GetCanonicalForm process state leak on Windows by
@​OvesN in dotnet/msbuild#13788
* Fix flaky detector: unblock dnceng feed, fail fast, and defer
quarantine to a second run by @​ViktorHofer in
dotnet/msbuild#13936
* Update documentation for ImplicitUsings element by @​drewnoakes in
dotnet/msbuild#13900
* [vs18.6] Point OptProf bootstrapper at rel/stable instead of int.main
by @​AlesProkop in dotnet/msbuild#13923
* Add CS8618 suppressor for required MSBuild task properties by
@​AArnott in dotnet/msbuild#13926
* Tighten NodeLaunchData.EnvironmentOverrides nullability to
IDictionary<string, string?>? by @​OvesN with @​Copilot in
dotnet/msbuild#13815
* Fix ToolTask EOF wait to be STA-safe via CountdownEvent (MSB4018 in
AspNetCompiler) by @​YuliiaKovalova in
dotnet/msbuild#13917
* [automated] Merge branch 'vs18.6' => 'vs18.7' by @​github-actions[bot]
in dotnet/msbuild#13941
* Localized file check-in by OneLocBuild Task: Build definition ID 9434:
Build ID 14192258 by @​dotnet-bot in
dotnet/msbuild#13849
* Bumping to 10.0.8 runtime packages by @​OvesN in
dotnet/msbuild#13898
* Flaky-test workflow: reassure on empty PR list + drop local
reproduction (quarantine-first) by @​ViktorHofer in
dotnet/msbuild#13938
* [Flaky Test] Un-quarantine 5 consistently-green tests by
@​github-actions[bot] in dotnet/msbuild#13952
* Flaky-test detector: open PRs ready-for-review; drop
newly-filed-issues section from PR body by @​ViktorHofer in
dotnet/msbuild#13958
* [Flaky Test] Quarantine 4 flaky tests by @​github-actions[bot] in
dotnet/msbuild#13937
* Flaky-test: fix duplicate-issue bug by switching dedup key to a
visible code-block key by @​ViktorHofer in
dotnet/msbuild#13963
* CsWin32 follow-up: WindowsNative + VS Setup Configuration + remaining
hand-rolled interop by @​JeremyKuhne in
dotnet/msbuild#13872
* Add the reviewer release skill checking if the Learn article Change
waves is updated by @​GangWang01 in
dotnet/msbuild#13840
* [main] Source code updates from dotnet/dotnet by @​dotnet-maestro[bot]
in dotnet/msbuild#13977
 ... (truncated)

Commits viewable in [compare
view](dotnet/msbuild@v18.8.2...v18.9.6).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=Microsoft.Build.Utilities.Core&package-manager=nuget&previous-version=18.8.2&new-version=18.9.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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.

3 participants