Fix CLR_E_SHIM_RUNTIMELOAD in RAR's IMetaDataDispenser activation#13899
Conversation
There was a problem hiding this comment.
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
AssemblyInformationandManifestUtil.MetadataReader(net472 path) to activateIMetaDataDispenserviaComClassFactory.TryCreateFromModule("clr.dll", ..., "DllGetClassObjectInternal", ...)instead of rawCoCreateInstance. - Extend
ComClassFactorywith module-export-based activation helpers and document the mscoree-shim failure mechanism. - Add new net472-only
AssemblyInformation_Testscovering 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. |
There was a problem hiding this comment.
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.
- Correctness —
DisposeUnmanagedResourcescomment should explain why callingAgileComPointer.Dispose()from the finalizer path is safe (CLR field-reachability guarantee +Interlocked.Exchangeguard in AgileComPointer) - Security/Correctness —
GetModuleHandlewas added toNativeMethods.txtbutLoadLibrary(with DLL search order) is used instead;GetModuleHandleis the right call for a module that must already be loaded - Performance —
LoadLibrary+GetProcAddress+DllGetClassObjectInternalon everyAssemblyInformationconstruction; cache at least theFARPROC
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 | noneGenerated by Expert Code Review (on open) for issue #13899 · ● 3.7M
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.
88fbf5f to
5b3733c
Compare
|
Bypassing macOS since the queues are long and this is a Windows-only fix. |
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.dllis missing fromReferencing_A_IJW\Debug\after build.Root cause
The migration switched
AssemblyInformation/MetadataReaderfrom RCW-based COM to struct-based COM through CsWin32 andAgileComPointer, and along the way activatesCLSID_CorMetaDataDispenservia rawCoCreateInstance.CLSID_CorMetaDataDispenseris registered withmscoree.dll(the .NET Framework shim) as itsInprocServer32.CoCreateInstancetherefore loads the shim, which then has to callLoadLibraryShimto bind a runtime before it can delegate toMetaDataDllGetClassObject.In hosts that did not enter the CLR via mscoree's startup path — notably the VC
cppxplatdevtest harness, which embeds MSBuild in-process viaBuildManagerfrom a non-mscoree-rooted process — the shim's bound-runtime state is uninitialized andLoadLibraryShimfails withCLR_E_SHIM_RUNTIMELOAD(0x80131700, "Failed to load the runtime"). RAR catches that as aDependencyResolutionExceptionand silently drops every transitive dependency for the failing reference. End-to-end symptom is the missingReferenced_C_IJW.dllcopy.Standalone
msbuild.execannot 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 exportedDllGetClassObjectInternaldirectly via a newComClassFactory.TryCreateFromModuleoverload, bypassing the shim entirely. This is exactly what the CLR's own managed-COM activation does internally for CLSIDs on itsIsClrHostedLegacyComObjectlist. The approach is AOT-friendly (noType.GetTypeFromCLSID, noActivator, no RCW), so .NET 10+ source-generated COM can use it unchanged.ComClassFactoryTryCreateFromModuleoverloads (standardDllGetClassObjectand caller-specified export name).ClrDllGetClassObjectInternalExportNameconstant with documentation of the shim mechanism.delegate* unmanaged[Stdcall]<...>function-pointer path serves both net472 and .NET Core.Callers
AssemblyInformationandMetadataReadernow useTryCreateFromModule("clr.dll", CLSID, ClrDllGetClassObjectInternalExportName, ...).[PreserveSig]tolerance forGetAssemblyFromScopeandGetPEKindfailures (the legacy RCW code silently ignored those HRs).DisposeUnmanagedResourcesso the finalizer path still revokes GIT cookies if the user forgets to dispose.Other
NativeMethods.txt: addGetModuleHandle(kept for completeness —LoadLibrary,FreeLibrary,GetProcAddresswere already present).AssemblyInformation_Testscovering both file-mapping lifetime (delete/overwrite after Dispose, including a repeated open-close stress loop) and dispenser activation.Validation
MetadataReader_Testspass on net472 x86.Microsoft.Build.Framework.dll+Microsoft.Build.Tasks.Core.dlldeployed into a local VS18 install, ran the failingProject.vcxprojdirectly withmsbuild.exeagainst the actual VC P2PReferences asset — bothReferenced_B_IJW.dllandReferenced_C_IJW.dllnow land inReferencing_A_IJW\Debug\.Cc / fyi @rainersigwald @JaynieBai @YuliiaKovalova — needs the VC team to re-run
P2PReferences.08with these binaries to confirm in their CI.