From 7e896cd57b8969a8bda1bdba0bbc2ec42e11a5a0 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Thu, 9 Apr 2026 20:46:14 -0700 Subject: [PATCH 01/16] Improve FileNotFoundException diagnostics in assembly loading Thread diagnostic context through the assembly binding pipeline so that FileNotFoundException carries actionable information about what failed and where, instead of just a generic message. Key changes: - Populate FusionLog on FileNotFoundException with binding diagnostic info (file open errors, metadata read failures, cached failure replays) - Preserve original Win32 error through BinderAcquirePEImage instead of discarding it via EX_CATCH_HRESULT - Fix TryOpenFile to return ERROR_OPEN_FAILED (not ERROR_FILE_NOT_FOUND) when GetLastError() is zero, making the error distinguishable - Extend EEFileLoadException with m_diagnosticInfo field and thread it through CreateThrowable to the managed FileLoadException.Create bridge - Extend FailureCacheEntry with diagnostic context so cached failure replays include the original error info with a '(Cached)' prefix - Thread SString* pDiagnosticInfo through the bind chain: BindAssembly -> BindByName -> BindLocked -> BindByTpaList -> GetAssembly - All diagnostic message format strings are in mscorrc.rc resources All diagnostic string formatting occurs only in error paths. The happy path has zero additional allocations or string work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System/IO/FileLoadException.CoreCLR.cs | 5 +- .../IO/FileNotFoundException.CoreCLR.cs | 9 ++++ src/coreclr/binder/assemblybindercommon.cpp | 48 +++++++++++++------ src/coreclr/binder/failurecache.cpp | 17 ++++++- src/coreclr/binder/inc/applicationcontext.hpp | 3 +- src/coreclr/binder/inc/applicationcontext.inl | 5 +- .../binder/inc/assemblybindercommon.hpp | 15 ++++-- src/coreclr/binder/inc/failurecache.hpp | 6 ++- .../binder/inc/failurecachehashtraits.hpp | 9 ++++ src/coreclr/dlls/mscorrc/mscorrc.rc | 8 ++++ src/coreclr/dlls/mscorrc/resource.h | 5 ++ src/coreclr/vm/appdomain.cpp | 2 + src/coreclr/vm/clrex.cpp | 25 +++++++++- src/coreclr/vm/clrex.h | 4 +- src/coreclr/vm/coreassemblyspec.cpp | 28 ++++++++++- src/coreclr/vm/peimage.cpp | 7 +-- .../src/System/IO/FileNotFoundException.cs | 2 +- 17 files changed, 162 insertions(+), 36 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/IO/FileLoadException.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/IO/FileLoadException.CoreCLR.cs index 927e08905606eb..01770b4f509032 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/IO/FileLoadException.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/IO/FileLoadException.CoreCLR.cs @@ -47,16 +47,17 @@ internal enum FileLoadExceptionKind } [UnmanagedCallersOnly] - internal static unsafe void Create(FileLoadExceptionKind kind, char* pFileName, int hresult, object* pThrowable, Exception* pException) + internal static unsafe void Create(FileLoadExceptionKind kind, char* pFileName, int hresult, char* pDiagnosticInfo, object* pThrowable, Exception* pException) { try { string? fileName = pFileName is not null ? new string(pFileName) : null; + string? diagnosticInfo = pDiagnosticInfo is not null ? new string(pDiagnosticInfo) : null; Debug.Assert(Enum.IsDefined(kind)); *pThrowable = kind switch { FileLoadExceptionKind.BadImageFormat => new BadImageFormatException(fileName, hresult), - FileLoadExceptionKind.FileNotFound => new FileNotFoundException(fileName, hresult), + FileLoadExceptionKind.FileNotFound => new FileNotFoundException(fileName, hresult, diagnosticInfo), FileLoadExceptionKind.OutOfMemory => new OutOfMemoryException(), _ /* FileLoadExceptionKind.FileLoad */ => new FileLoadException(fileName, hresult), }; diff --git a/src/coreclr/System.Private.CoreLib/src/System/IO/FileNotFoundException.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/IO/FileNotFoundException.CoreCLR.cs index 15d54ec5b367c8..14fd366bc4bca0 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/IO/FileNotFoundException.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/IO/FileNotFoundException.CoreCLR.cs @@ -12,5 +12,14 @@ internal FileNotFoundException(string? fileName, int hResult) FileName = fileName; SetMessageField(); } + + internal FileNotFoundException(string? fileName, int hResult, string? diagnosticInfo) + : base(null) + { + HResult = hResult; + FileName = fileName; + FusionLog = diagnosticInfo; + SetMessageField(); + } } } diff --git a/src/coreclr/binder/assemblybindercommon.cpp b/src/coreclr/binder/assemblybindercommon.cpp index caa3525a029d43..09156b96fde280 100644 --- a/src/coreclr/binder/assemblybindercommon.cpp +++ b/src/coreclr/binder/assemblybindercommon.cpp @@ -37,7 +37,8 @@ extern HRESULT RuntimeInvokeHostAssemblyResolver(INT_PTR pAssemblyLoadContextToB STDAPI BinderAcquirePEImage(LPCTSTR szAssemblyPath, PEImage** ppPEImage, - ProbeExtensionResult probeExtensionResult); + ProbeExtensionResult probeExtensionResult, + SString *pDiagnosticInfo = NULL); namespace BINDER_SPACE { @@ -192,7 +193,8 @@ namespace BINDER_SPACE HRESULT AssemblyBinderCommon::BindAssembly(/* in */ AssemblyBinder *pBinder, /* in */ AssemblyName *pAssemblyName, /* in */ bool excludeAppPaths, - /* out */ Assembly **ppAssembly) + /* out */ Assembly **ppAssembly, + /* out */ SString *pDiagnosticInfo) { HRESULT hr = S_OK; LONG kContextVersion = 0; @@ -213,7 +215,8 @@ namespace BINDER_SPACE false, // skipFailureCaching false, // skipVersionCompatibilityCheck excludeAppPaths, - &bindResult)); + &bindResult, + pDiagnosticInfo)); // Remember the post-bind version kContextVersion = pApplicationContext->GetVersion(); @@ -393,7 +396,8 @@ namespace BINDER_SPACE bool skipFailureCaching, bool skipVersionCompatibilityCheck, bool excludeAppPaths, - BindResult *pBindResult) + BindResult *pBindResult, + SString *pDiagnosticInfo) { HRESULT hr = S_OK; PathString assemblyDisplayName; @@ -402,7 +406,7 @@ namespace BINDER_SPACE pAssemblyName->GetDisplayName(assemblyDisplayName, AssemblyName::INCLUDE_VERSION); - hr = pApplicationContext->GetFailureCache()->Lookup(assemblyDisplayName); + hr = pApplicationContext->GetFailureCache()->Lookup(assemblyDisplayName, pDiagnosticInfo); if (FAILED(hr)) { if ((hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) && skipFailureCaching) @@ -430,7 +434,8 @@ namespace BINDER_SPACE pAssemblyName, skipVersionCompatibilityCheck, excludeAppPaths, - pBindResult)); + pBindResult, + pDiagnosticInfo)); if (!pBindResult->HaveResult()) { @@ -455,7 +460,7 @@ namespace BINDER_SPACE } } - hr = pApplicationContext->AddToFailureCache(assemblyDisplayName, hr); + hr = pApplicationContext->AddToFailureCache(assemblyDisplayName, hr, pDiagnosticInfo); } LogExit: @@ -467,7 +472,8 @@ namespace BINDER_SPACE AssemblyName *pAssemblyName, bool skipVersionCompatibilityCheck, bool excludeAppPaths, - BindResult *pBindResult) + BindResult *pBindResult, + SString *pDiagnosticInfo) { HRESULT hr = S_OK; @@ -509,7 +515,8 @@ namespace BINDER_SPACE hr = BindByTpaList(pApplicationContext, pAssemblyName, excludeAppPaths, - pBindResult); + pBindResult, + pDiagnosticInfo); if (SUCCEEDED(hr) && pBindResult->HaveResult()) { bool isCompatible = IsCompatibleAssemblyVersion(pAssemblyName, pBindResult->GetAssemblyName()); @@ -825,7 +832,8 @@ namespace BINDER_SPACE HRESULT AssemblyBinderCommon::BindByTpaList(ApplicationContext *pApplicationContext, AssemblyName *pRequestedAssemblyName, bool excludeAppPaths, - BindResult *pBindResult) + BindResult *pBindResult, + SString *pDiagnosticInfo) { HRESULT hr = S_OK; @@ -861,7 +869,8 @@ namespace BINDER_SPACE hr = GetAssembly(assemblyFilePath, TRUE, // fIsInTPA &pTPAAssembly, - probeExtensionResult); + probeExtensionResult, + pDiagnosticInfo); BinderTracing::PathProbed(assemblyFilePath, BinderTracing::PathSource::Bundle, hr); @@ -891,7 +900,9 @@ namespace BINDER_SPACE hr = GetAssembly(fileName, TRUE, // fIsInTPA - &pTPAAssembly); + &pTPAAssembly, + ProbeExtensionResult::Invalid(), + pDiagnosticInfo); BinderTracing::PathProbed(fileName, BinderTracing::PathSource::ApplicationAssemblies, hr); pBindResult->SetAttemptResult(hr, pTPAAssembly); @@ -977,7 +988,8 @@ namespace BINDER_SPACE HRESULT AssemblyBinderCommon::GetAssembly(SString &assemblyPath, BOOL fIsInTPA, Assembly **ppAssembly, - ProbeExtensionResult probeExtensionResult) + ProbeExtensionResult probeExtensionResult, + SString *pDiagnosticInfo) { HRESULT hr = S_OK; @@ -993,7 +1005,7 @@ namespace BINDER_SPACE { LPCTSTR szAssemblyPath = const_cast(assemblyPath.GetUnicode()); - hr = BinderAcquirePEImage(szAssemblyPath, &pPEImage, probeExtensionResult); + hr = BinderAcquirePEImage(szAssemblyPath, &pPEImage, probeExtensionResult, pDiagnosticInfo); IF_FAIL_GO(hr); } @@ -1012,6 +1024,14 @@ namespace BINDER_SPACE { hr = HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); } + else if (FAILED(hr) && pDiagnosticInfo != NULL && pDiagnosticInfo->IsEmpty()) + { + StackSString format; + format.LoadResource(IDS_BINDING_FAILED_TO_INIT_ASSEMBLY); + StackSString hrString; + hrString.Printf("%08x", hr); + pDiagnosticInfo->FormatMessage(FORMAT_MESSAGE_FROM_STRING, format.GetUnicode(), 0, 0, assemblyPath, hrString); + } return hr; } diff --git a/src/coreclr/binder/failurecache.cpp b/src/coreclr/binder/failurecache.cpp index c6e1f286fbb7ed..15fac7692c4513 100644 --- a/src/coreclr/binder/failurecache.cpp +++ b/src/coreclr/binder/failurecache.cpp @@ -12,6 +12,7 @@ // ============================================================ #include "failurecache.hpp" +#include "common.h" namespace BINDER_SPACE { @@ -32,7 +33,8 @@ namespace BINDER_SPACE } HRESULT FailureCache::Add(SString &assemblyNameorPath, - HRESULT hrBindingResult) + HRESULT hrBindingResult, + SString *pDiagnosticInfo) { HRESULT hr = S_OK; @@ -44,6 +46,10 @@ namespace BINDER_SPACE pFailureCacheEntry->GetAssemblyNameOrPath().Set(assemblyNameorPath); pFailureCacheEntry->SetBindingResult(hrBindingResult); + if (pDiagnosticInfo != NULL && !pDiagnosticInfo->IsEmpty()) + { + pFailureCacheEntry->SetDiagnosticInfo(*pDiagnosticInfo); + } Hash::Add(pFailureCacheEntry); pFailureCacheEntry.SuppressRelease(); @@ -52,7 +58,8 @@ namespace BINDER_SPACE return hr; } - HRESULT FailureCache::Lookup(SString &assemblyNameorPath) + HRESULT FailureCache::Lookup(SString &assemblyNameorPath, + SString *pDiagnosticInfo) { HRESULT hr = S_OK; FailureCacheEntry *pFailureCachEntry = Hash::Lookup(assemblyNameorPath); @@ -60,6 +67,12 @@ namespace BINDER_SPACE if (pFailureCachEntry != NULL) { hr = pFailureCachEntry->GetBindingResult(); + if (pDiagnosticInfo != NULL && !pFailureCachEntry->GetDiagnosticInfo().IsEmpty()) + { + StackSString format; + format.LoadResource(IDS_BINDING_CACHED_FAILURE_PREFIX); + pDiagnosticInfo->FormatMessage(FORMAT_MESSAGE_FROM_STRING, format.GetUnicode(), 0, 0, pFailureCachEntry->GetDiagnosticInfo()); + } } return hr; diff --git a/src/coreclr/binder/inc/applicationcontext.hpp b/src/coreclr/binder/inc/applicationcontext.hpp index 53bcfbcac48012..fcd28d4391c516 100644 --- a/src/coreclr/binder/inc/applicationcontext.hpp +++ b/src/coreclr/binder/inc/applicationcontext.hpp @@ -95,7 +95,8 @@ namespace BINDER_SPACE inline ExecutionContext *GetExecutionContext(); inline FailureCache *GetFailureCache(); inline HRESULT AddToFailureCache(SString &assemblyNameOrPath, - HRESULT hrBindResult); + HRESULT hrBindResult, + SString *pDiagnosticInfo = NULL); inline StringArrayList *GetAppPaths(); inline SimpleNameToFileNameMap *GetTpaList(); inline StringArrayList *GetPlatformResourceRoots(); diff --git a/src/coreclr/binder/inc/applicationcontext.inl b/src/coreclr/binder/inc/applicationcontext.inl index 16ec0c6d87de2a..b5b04e0564cfaf 100644 --- a/src/coreclr/binder/inc/applicationcontext.inl +++ b/src/coreclr/binder/inc/applicationcontext.inl @@ -41,9 +41,10 @@ FailureCache *ApplicationContext::GetFailureCache() } HRESULT ApplicationContext::AddToFailureCache(SString &assemblyNameOrPath, - HRESULT hrBindResult) + HRESULT hrBindResult, + SString *pDiagnosticInfo) { - HRESULT hr = GetFailureCache()->Add(assemblyNameOrPath, hrBindResult); + HRESULT hr = GetFailureCache()->Add(assemblyNameOrPath, hrBindResult, pDiagnosticInfo); IncrementVersion(); return hr; } diff --git a/src/coreclr/binder/inc/assemblybindercommon.hpp b/src/coreclr/binder/inc/assemblybindercommon.hpp index 55049c894f3ba6..4c5cefd5cc3dfe 100644 --- a/src/coreclr/binder/inc/assemblybindercommon.hpp +++ b/src/coreclr/binder/inc/assemblybindercommon.hpp @@ -31,7 +31,8 @@ namespace BINDER_SPACE static HRESULT BindAssembly(/* in */ AssemblyBinder *pBinder, /* in */ AssemblyName *pAssemblyName, /* in */ bool excludeAppPaths, - /* out */ Assembly **ppAssembly); + /* out */ Assembly **ppAssembly, + /* out */ SString *pDiagnosticInfo = NULL); static HRESULT BindToSystem(/* in */ SString &systemDirectory, /* out */ Assembly **ppSystemAssembly); @@ -44,7 +45,8 @@ namespace BINDER_SPACE static HRESULT GetAssembly(/* in */ SString &assemblyPath, /* in */ BOOL fIsInTPA, /* out */ Assembly **ppAssembly, - /* in */ ProbeExtensionResult probeExtensionResult = ProbeExtensionResult::Invalid()); + /* in */ ProbeExtensionResult probeExtensionResult = ProbeExtensionResult::Invalid(), + /* out */ SString *pDiagnosticInfo = NULL); #if !defined(DACCESS_COMPILE) static HRESULT BindUsingHostAssemblyResolver (/* in */ INT_PTR pAssemblyLoadContextToBindWithin, @@ -72,13 +74,15 @@ namespace BINDER_SPACE /* in */ bool skipFailureCaching, /* in */ bool skipVersionCompatibilityCheck, /* in */ bool excludeAppPaths, - /* out */ BindResult *pBindResult); + /* out */ BindResult *pBindResult, + /* out */ SString *pDiagnosticInfo = NULL); static HRESULT BindLocked(/* in */ ApplicationContext *pApplicationContext, /* in */ AssemblyName *pAssemblyName, /* in */ bool skipVersionCompatibilityCheck, /* in */ bool excludeAppPaths, - /* out */ BindResult *pBindResult); + /* out */ BindResult *pBindResult, + /* out */ SString *pDiagnosticInfo = NULL); static HRESULT FindInExecutionContext(/* in */ ApplicationContext *pApplicationContext, /* in */ AssemblyName *pAssemblyName, @@ -87,7 +91,8 @@ namespace BINDER_SPACE static HRESULT BindByTpaList(/* in */ ApplicationContext *pApplicationContext, /* in */ AssemblyName *pRequestedAssemblyName, /* in */ bool excludeAppPaths, - /* out */ BindResult *pBindResult); + /* out */ BindResult *pBindResult, + /* out */ SString *pDiagnosticInfo = NULL); static HRESULT Register(/* in */ ApplicationContext *pApplicationContext, /* in */ BindResult *pBindResult); diff --git a/src/coreclr/binder/inc/failurecache.hpp b/src/coreclr/binder/inc/failurecache.hpp index bce67b33c50c6c..62ece00daed618 100644 --- a/src/coreclr/binder/inc/failurecache.hpp +++ b/src/coreclr/binder/inc/failurecache.hpp @@ -28,8 +28,10 @@ namespace BINDER_SPACE ~FailureCache(); HRESULT Add(/* in */ SString &assemblyNameorPath, - /* in */ HRESULT hrBindResult); - HRESULT Lookup(/* in */ SString &assemblyNameorPath); + /* in */ HRESULT hrBindResult, + /* in */ SString *pDiagnosticInfo = NULL); + HRESULT Lookup(/* in */ SString &assemblyNameorPath, + /* out */ SString *pDiagnosticInfo = NULL); void Remove(/* in */ SString &assemblyName); }; }; diff --git a/src/coreclr/binder/inc/failurecachehashtraits.hpp b/src/coreclr/binder/inc/failurecachehashtraits.hpp index 1ffa96f56b1985..46aa10c5694b2c 100644 --- a/src/coreclr/binder/inc/failurecachehashtraits.hpp +++ b/src/coreclr/binder/inc/failurecachehashtraits.hpp @@ -45,10 +45,19 @@ namespace BINDER_SPACE { m_hrBindingResult = hrBindingResult; } + inline SString &GetDiagnosticInfo() + { + return m_diagnosticInfo; + } + inline void SetDiagnosticInfo(const SString &diagnosticInfo) + { + m_diagnosticInfo.Set(diagnosticInfo); + } protected: SString m_assemblyNameOrPath; HRESULT m_hrBindingResult; + SString m_diagnosticInfo; }; class FailureCacheHashTraits : public DefaultSHashTraits diff --git a/src/coreclr/dlls/mscorrc/mscorrc.rc b/src/coreclr/dlls/mscorrc/mscorrc.rc index 724394d9911bb2..6e79f1ec4c4905 100644 --- a/src/coreclr/dlls/mscorrc/mscorrc.rc +++ b/src/coreclr/dlls/mscorrc/mscorrc.rc @@ -687,6 +687,14 @@ BEGIN IDS_NATIVE_IMAGE_CANNOT_BE_LOADED_MULTIPLE_TIMES "Native image cannot be loaded multiple times" END +STRINGTABLE DISCARDABLE +BEGIN + IDS_BINDING_FAILED_TO_OPEN_FILE "Failed to open file '%1'. HRESULT: 0x%2" + IDS_BINDING_EXCEPTION_OPENING_FILE "Exception opening '%1': %2. HRESULT: 0x%3" + IDS_BINDING_FAILED_TO_INIT_ASSEMBLY "Failed to initialize assembly from '%1'. HRESULT: 0x%2" + IDS_BINDING_CACHED_FAILURE_PREFIX "(Cached) %1" +END + // // Descriptions for FACILITY_URT hresults. None of these may be parameterized. // diff --git a/src/coreclr/dlls/mscorrc/resource.h b/src/coreclr/dlls/mscorrc/resource.h index 9906a03515c1f3..dda1ecc60f995a 100644 --- a/src/coreclr/dlls/mscorrc/resource.h +++ b/src/coreclr/dlls/mscorrc/resource.h @@ -527,3 +527,8 @@ #define IDS_EE_CANNOTCASTSAME_DETAIL_LOCATION 0x2652 #define IDS_EE_CANNOTCASTSAME_GENARG_BYTE_ARRAY 0x2653 #define IDS_EE_CANNOTCASTSAME_GENARG_LOCATION 0x2654 + +#define IDS_BINDING_FAILED_TO_OPEN_FILE 0x2655 +#define IDS_BINDING_EXCEPTION_OPENING_FILE 0x2656 +#define IDS_BINDING_FAILED_TO_INIT_ASSEMBLY 0x2657 +#define IDS_BINDING_CACHED_FAILURE_PREFIX 0x2658 diff --git a/src/coreclr/vm/appdomain.cpp b/src/coreclr/vm/appdomain.cpp index 63f78b3f757e86..40373b7eb1ace9 100644 --- a/src/coreclr/vm/appdomain.cpp +++ b/src/coreclr/vm/appdomain.cpp @@ -2420,7 +2420,9 @@ Assembly *AppDomain::LoadAssembly(AssemblySpec* pSpec, PAL_CPP_THROW(Exception *, pEx); } else + { AddExceptionToCache(pSpec, pEx); + } } } EX_END_HOOK; diff --git a/src/coreclr/vm/clrex.cpp b/src/coreclr/vm/clrex.cpp index 565f8ec633b68e..991a29ae33f3ae 100644 --- a/src/coreclr/vm/clrex.cpp +++ b/src/coreclr/vm/clrex.cpp @@ -1452,6 +1452,28 @@ EEFileLoadException::EEFileLoadException(const SString &name, HRESULT hr, Except } +EEFileLoadException::EEFileLoadException(const SString &name, HRESULT hr, const SString &diagnosticInfo, Exception *pInnerException/* = NULL*/) + : EEException(GetFileLoadKind(hr)), + m_name(name), + m_hr(hr), + m_diagnosticInfo(diagnosticInfo) +{ + CONTRACTL + { + GC_NOTRIGGER; + THROWS; + MODE_ANY; + } + CONTRACTL_END; + + _ASSERTE(pInnerException == NULL || !(pInnerException->IsTransient())); + m_innerException = pInnerException ? pInnerException->DomainBoundClone() : NULL; + + if (m_name.IsEmpty()) + m_name.Set(W("")); +} + + EEFileLoadException::~EEFileLoadException() { STATIC_CONTRACT_NOTHROW; @@ -1587,10 +1609,11 @@ OBJECTREF EEFileLoadException::CreateThrowable() GCPROTECT_BEGIN(gc); LPCWSTR pFileName = m_name.GetUnicode(); + LPCWSTR pDiagnosticInfo = m_diagnosticInfo.IsEmpty() ? NULL : m_diagnosticInfo.GetUnicode(); UnmanagedCallersOnlyCaller createFileLoadEx(METHOD__FILE_LOAD_EXCEPTION__CREATE); FileLoadExceptionKind kind = GetFileLoadExceptionKind(m_hr); - createFileLoadEx.InvokeThrowing(kind, pFileName, (int)m_hr, &gc.pNewException); + createFileLoadEx.InvokeThrowing(kind, pFileName, (int)m_hr, pDiagnosticInfo, &gc.pNewException); _ASSERTE(gc.pNewException->GetMethodTable() == CoreLibBinder::GetException(m_kind)); GCPROTECT_END(); diff --git a/src/coreclr/vm/clrex.h b/src/coreclr/vm/clrex.h index 01fbce5df1a1c7..8f77d1e9b97486 100644 --- a/src/coreclr/vm/clrex.h +++ b/src/coreclr/vm/clrex.h @@ -665,10 +665,12 @@ class EEFileLoadException : public EEException private: SString m_name; HRESULT m_hr; + SString m_diagnosticInfo; public: EEFileLoadException(const SString &name, HRESULT hr, Exception *pInnerException = NULL); + EEFileLoadException(const SString &name, HRESULT hr, const SString &diagnosticInfo, Exception *pInnerException = NULL); ~EEFileLoadException(); // virtual overrides @@ -692,7 +694,7 @@ class EEFileLoadException : public EEException virtual Exception *CloneHelper() { WRAPPER_NO_CONTRACT; - return new EEFileLoadException(m_name, m_hr); + return new EEFileLoadException(m_name, m_hr, m_diagnosticInfo); } private: diff --git a/src/coreclr/vm/coreassemblyspec.cpp b/src/coreclr/vm/coreassemblyspec.cpp index 8e98f6729088e3..e6149b940e6ccb 100644 --- a/src/coreclr/vm/coreassemblyspec.cpp +++ b/src/coreclr/vm/coreassemblyspec.cpp @@ -78,7 +78,8 @@ HRESULT AssemblySpec::Bind(AppDomain *pAppDomain, BINDER_SPACE::Assembly** ppAs STDAPI BinderAcquirePEImage(LPCWSTR wszAssemblyPath, PEImage **ppPEImage, - ProbeExtensionResult probeExtensionResult) + ProbeExtensionResult probeExtensionResult, + SString *pDiagnosticInfo) { HRESULT hr = S_OK; @@ -94,6 +95,14 @@ STDAPI BinderAcquirePEImage(LPCWSTR wszAssemblyPath, hr = pImage->TryOpenFile(); if (FAILED(hr)) { + if (pDiagnosticInfo != NULL) + { + StackSString format; + format.LoadResource(IDS_BINDING_FAILED_TO_OPEN_FILE); + StackSString hrString; + hrString.Printf("%08x", hr); + pDiagnosticInfo->FormatMessage(FORMAT_MESSAGE_FROM_STRING, format.GetUnicode(), 0, 0, SString(wszAssemblyPath), hrString); + } goto Exit; } } @@ -101,7 +110,22 @@ STDAPI BinderAcquirePEImage(LPCWSTR wszAssemblyPath, if (pImage) *ppPEImage = pImage.Extract(); } - EX_CATCH_HRESULT(hr); + EX_CATCH + { + hr = GET_EXCEPTION()->GetHR(); + _ASSERTE(FAILED(hr)); + if (pDiagnosticInfo != NULL) + { + StackSString format; + format.LoadResource(IDS_BINDING_EXCEPTION_OPENING_FILE); + StackSString exMessage; + GET_EXCEPTION()->GetMessage(exMessage); + StackSString hrString; + hrString.Printf("%08x", hr); + pDiagnosticInfo->FormatMessage(FORMAT_MESSAGE_FROM_STRING, format.GetUnicode(), 0, 0, SString(wszAssemblyPath), exMessage, hrString); + } + } + EX_END_CATCH Exit: return hr; diff --git a/src/coreclr/vm/peimage.cpp b/src/coreclr/vm/peimage.cpp index aee07068633a65..2c038fabefd695 100644 --- a/src/coreclr/vm/peimage.cpp +++ b/src/coreclr/vm/peimage.cpp @@ -809,10 +809,11 @@ HRESULT PEImage::TryOpenFile(bool takeLock) if (m_hFile != INVALID_HANDLE_VALUE) return S_OK; - if (GetLastError()) - return HRESULT_FROM_WIN32(GetLastError()); + DWORD dwLastError = GetLastError(); + if (dwLastError != 0) + return HRESULT_FROM_WIN32(dwLastError); - return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + return HRESULT_FROM_WIN32(ERROR_OPEN_FAILED); } #endif // !DACCESS_COMPILE diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs index 1175b9c9fcda0b..5e5c87536f36bd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs @@ -68,7 +68,7 @@ private void SetMessageField() } public string? FileName { get; } - public string? FusionLog { get; } + public string? FusionLog { get; internal set; } public override string ToString() { From 65030665b99f390fb040f27060e2e5356a7b4b4a Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Thu, 9 Apr 2026 21:33:07 -0700 Subject: [PATCH 02/16] Wire diagnostic info through full bind chain to exception throw sites Thread SString* pDiagnosticInfo through: AssemblyBinder::BindAssemblyByName -> BindUsingAssemblyName (virtual, Default + Custom overrides) -> BindAssemblyByNameWorker -> AssemblyBinderCommon::BindAssembly -> AssemblySpec::Bind -> AppDomain::BindAssemblySpec Add EEFileLoadException::Throw overload accepting diagnostic SString. In AppDomain::BindAssemblySpec, pass StackSString to Bind() and use it at the EEFileLoadException::Throw site so the diagnostic info reaches FileNotFoundException.FusionLog on the managed side. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/binder/customassemblybinder.cpp | 11 ++++++---- src/coreclr/binder/defaultassemblybinder.cpp | 11 ++++++---- src/coreclr/binder/inc/customassemblybinder.h | 4 ++-- .../binder/inc/defaultassemblybinder.h | 5 +++-- src/coreclr/vm/appdomain.cpp | 7 ++++++- src/coreclr/vm/assemblybinder.cpp | 5 +++-- src/coreclr/vm/assemblybinder.h | 4 ++-- src/coreclr/vm/assemblyspec.hpp | 3 ++- src/coreclr/vm/clrex.cpp | 21 +++++++++++++++++++ src/coreclr/vm/clrex.h | 1 + src/coreclr/vm/coreassemblyspec.cpp | 4 ++-- 11 files changed, 56 insertions(+), 20 deletions(-) diff --git a/src/coreclr/binder/customassemblybinder.cpp b/src/coreclr/binder/customassemblybinder.cpp index 950e462d512ae5..9ebb1ede95ff7a 100644 --- a/src/coreclr/binder/customassemblybinder.cpp +++ b/src/coreclr/binder/customassemblybinder.cpp @@ -14,7 +14,8 @@ using namespace BINDER_SPACE; // CustomAssemblyBinder implementation // ============================================================================ HRESULT CustomAssemblyBinder::BindAssemblyByNameWorker(BINDER_SPACE::AssemblyName *pAssemblyName, - BINDER_SPACE::Assembly **ppCoreCLRFoundAssembly) + BINDER_SPACE::Assembly **ppCoreCLRFoundAssembly, + SString *pDiagnosticInfo) { VALIDATE_ARG_RET(pAssemblyName != nullptr && ppCoreCLRFoundAssembly != nullptr); HRESULT hr = S_OK; @@ -28,7 +29,8 @@ HRESULT CustomAssemblyBinder::BindAssemblyByNameWorker(BINDER_SPACE::AssemblyNam hr = AssemblyBinderCommon::BindAssembly(this, pAssemblyName, false, //excludeAppPaths, - ppCoreCLRFoundAssembly); + ppCoreCLRFoundAssembly, + pDiagnosticInfo); if (!FAILED(hr)) { _ASSERTE(*ppCoreCLRFoundAssembly != NULL); @@ -39,7 +41,8 @@ HRESULT CustomAssemblyBinder::BindAssemblyByNameWorker(BINDER_SPACE::AssemblyNam } HRESULT CustomAssemblyBinder::BindUsingAssemblyName(BINDER_SPACE::AssemblyName* pAssemblyName, - BINDER_SPACE::Assembly** ppAssembly) + BINDER_SPACE::Assembly** ppAssembly, + SString* pDiagnosticInfo) { // When LoadContext needs to resolve an assembly reference, it will go through the following lookup order: // @@ -58,7 +61,7 @@ HRESULT CustomAssemblyBinder::BindUsingAssemblyName(BINDER_SPACE::AssemblyName* { // Step 1 - Try to find the assembly within the LoadContext. - hr = BindAssemblyByNameWorker(pAssemblyName, &pCoreCLRFoundAssembly); + hr = BindAssemblyByNameWorker(pAssemblyName, &pCoreCLRFoundAssembly, pDiagnosticInfo); if ((hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) || (hr == FUSION_E_APP_DOMAIN_LOCKED) || (hr == FUSION_E_REF_DEF_MISMATCH)) { diff --git a/src/coreclr/binder/defaultassemblybinder.cpp b/src/coreclr/binder/defaultassemblybinder.cpp index 730618d0a43dfa..a3a6ab0a4f3b25 100644 --- a/src/coreclr/binder/defaultassemblybinder.cpp +++ b/src/coreclr/binder/defaultassemblybinder.cpp @@ -13,7 +13,8 @@ using namespace BINDER_SPACE; HRESULT DefaultAssemblyBinder::BindAssemblyByNameWorker(BINDER_SPACE::AssemblyName *pAssemblyName, BINDER_SPACE::Assembly **ppCoreCLRFoundAssembly, - bool excludeAppPaths) + bool excludeAppPaths, + SString *pDiagnosticInfo) { VALIDATE_ARG_RET(pAssemblyName != nullptr && ppCoreCLRFoundAssembly != nullptr); HRESULT hr = S_OK; @@ -26,7 +27,8 @@ HRESULT DefaultAssemblyBinder::BindAssemblyByNameWorker(BINDER_SPACE::AssemblyNa hr = AssemblyBinderCommon::BindAssembly(this, pAssemblyName, excludeAppPaths, - ppCoreCLRFoundAssembly); + ppCoreCLRFoundAssembly, + pDiagnosticInfo); if (!FAILED(hr)) { (*ppCoreCLRFoundAssembly)->SetBinder(this); @@ -39,7 +41,8 @@ HRESULT DefaultAssemblyBinder::BindAssemblyByNameWorker(BINDER_SPACE::AssemblyNa // DefaultAssemblyBinder implementation // ============================================================================ HRESULT DefaultAssemblyBinder::BindUsingAssemblyName(BINDER_SPACE::AssemblyName *pAssemblyName, - BINDER_SPACE::Assembly **ppAssembly) + BINDER_SPACE::Assembly **ppAssembly, + SString *pDiagnosticInfo) { HRESULT hr = S_OK; VALIDATE_ARG_RET(pAssemblyName != nullptr && ppAssembly != nullptr); @@ -48,7 +51,7 @@ HRESULT DefaultAssemblyBinder::BindUsingAssemblyName(BINDER_SPACE::AssemblyName ReleaseHolder pCoreCLRFoundAssembly; - hr = BindAssemblyByNameWorker(pAssemblyName, &pCoreCLRFoundAssembly, false /* excludeAppPaths */); + hr = BindAssemblyByNameWorker(pAssemblyName, &pCoreCLRFoundAssembly, false /* excludeAppPaths */, pDiagnosticInfo); #if !defined(DACCESS_COMPILE) if ((hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) || diff --git a/src/coreclr/binder/inc/customassemblybinder.h b/src/coreclr/binder/inc/customassemblybinder.h index 7a89c5033b9e50..c62e087d49127e 100644 --- a/src/coreclr/binder/inc/customassemblybinder.h +++ b/src/coreclr/binder/inc/customassemblybinder.h @@ -22,7 +22,7 @@ class CustomAssemblyBinder final : public AssemblyBinder BINDER_SPACE::Assembly** ppAssembly) override; HRESULT BindUsingAssemblyName(BINDER_SPACE::AssemblyName* pAssemblyName, - BINDER_SPACE::Assembly** ppAssembly) override; + BINDER_SPACE::Assembly** ppAssembly, SString* pDiagnosticInfo = NULL) override; AssemblyLoaderAllocator* GetLoaderAllocator() override; @@ -48,7 +48,7 @@ class CustomAssemblyBinder final : public AssemblyBinder void ReleaseLoadContext(); private: - HRESULT BindAssemblyByNameWorker(BINDER_SPACE::AssemblyName *pAssemblyName, BINDER_SPACE::Assembly **ppCoreCLRFoundAssembly); + HRESULT BindAssemblyByNameWorker(BINDER_SPACE::AssemblyName *pAssemblyName, BINDER_SPACE::Assembly **ppCoreCLRFoundAssembly, SString *pDiagnosticInfo = NULL); DefaultAssemblyBinder *m_pDefaultBinder; diff --git a/src/coreclr/binder/inc/defaultassemblybinder.h b/src/coreclr/binder/inc/defaultassemblybinder.h index 3d35854e09f3ff..4c9ffc952faab2 100644 --- a/src/coreclr/binder/inc/defaultassemblybinder.h +++ b/src/coreclr/binder/inc/defaultassemblybinder.h @@ -20,7 +20,7 @@ class DefaultAssemblyBinder final : public AssemblyBinder BINDER_SPACE::Assembly** ppAssembly) override; HRESULT BindUsingAssemblyName(BINDER_SPACE::AssemblyName* pAssemblyName, - BINDER_SPACE::Assembly** ppAssembly) override; + BINDER_SPACE::Assembly** ppAssembly, SString* pDiagnosticInfo = NULL) override; AssemblyLoaderAllocator* GetLoaderAllocator() override { @@ -46,7 +46,8 @@ class DefaultAssemblyBinder final : public AssemblyBinder HRESULT BindAssemblyByNameWorker( BINDER_SPACE::AssemblyName *pAssemblyName, BINDER_SPACE::Assembly **ppCoreCLRFoundAssembly, - bool excludeAppPaths); + bool excludeAppPaths, + SString *pDiagnosticInfo = NULL); }; #endif // __DEFAULT_ASSEMBLY_BINDER_H__ diff --git a/src/coreclr/vm/appdomain.cpp b/src/coreclr/vm/appdomain.cpp index 40373b7eb1ace9..b63e53f7ad8235 100644 --- a/src/coreclr/vm/appdomain.cpp +++ b/src/coreclr/vm/appdomain.cpp @@ -3138,6 +3138,7 @@ PEAssembly * AppDomain::BindAssemblySpec( HRESULT hrBindResult = S_OK; PEAssemblyHolder result; + StackSString bindDiagnosticInfo; bool isCached = false; EX_TRY @@ -3148,7 +3149,7 @@ PEAssembly * AppDomain::BindAssemblySpec( { ReleaseHolder boundAssembly; - hrBindResult = pSpec->Bind(this, &boundAssembly); + hrBindResult = pSpec->Bind(this, &boundAssembly, &bindDiagnosticInfo); if (boundAssembly) { @@ -3192,6 +3193,10 @@ PEAssembly * AppDomain::BindAssemblySpec( if (fFailure && fThrowOnFileNotFound) { + if (!bindDiagnosticInfo.IsEmpty()) + { + EEFileLoadException::Throw(pFailedSpec, COR_E_FILENOTFOUND, bindDiagnosticInfo, NULL); + } EEFileLoadException::Throw(pFailedSpec, COR_E_FILENOTFOUND, NULL); } } diff --git a/src/coreclr/vm/assemblybinder.cpp b/src/coreclr/vm/assemblybinder.cpp index 9fab20523cd92b..dc0d6acd8ea93b 100644 --- a/src/coreclr/vm/assemblybinder.cpp +++ b/src/coreclr/vm/assemblybinder.cpp @@ -9,7 +9,8 @@ #ifndef DACCESS_COMPILE HRESULT AssemblyBinder::BindAssemblyByName(AssemblyNameData* pAssemblyNameData, - BINDER_SPACE::Assembly** ppAssembly) + BINDER_SPACE::Assembly** ppAssembly, + SString* pDiagnosticInfo) { _ASSERTE(pAssemblyNameData != nullptr && ppAssembly != nullptr); @@ -20,7 +21,7 @@ HRESULT AssemblyBinder::BindAssemblyByName(AssemblyNameData* pAssemblyNameData, SAFE_NEW(pAssemblyName, BINDER_SPACE::AssemblyName); IF_FAIL_GO(pAssemblyName->Init(*pAssemblyNameData)); - hr = BindUsingAssemblyName(pAssemblyName, ppAssembly); + hr = BindUsingAssemblyName(pAssemblyName, ppAssembly, pDiagnosticInfo); Exit: return hr; diff --git a/src/coreclr/vm/assemblybinder.h b/src/coreclr/vm/assemblybinder.h index 89b501a6759a23..158acfcaacf5b6 100644 --- a/src/coreclr/vm/assemblybinder.h +++ b/src/coreclr/vm/assemblybinder.h @@ -18,9 +18,9 @@ class AssemblyBinder { public: - HRESULT BindAssemblyByName(AssemblyNameData* pAssemblyNameData, BINDER_SPACE::Assembly** ppAssembly); + HRESULT BindAssemblyByName(AssemblyNameData* pAssemblyNameData, BINDER_SPACE::Assembly** ppAssembly, SString* pDiagnosticInfo = NULL); virtual HRESULT BindUsingPEImage(PEImage* pPEImage, bool excludeAppPaths, BINDER_SPACE::Assembly** ppAssembly) = 0; - virtual HRESULT BindUsingAssemblyName(BINDER_SPACE::AssemblyName* pAssemblyName, BINDER_SPACE::Assembly** ppAssembly) = 0; + virtual HRESULT BindUsingAssemblyName(BINDER_SPACE::AssemblyName* pAssemblyName, BINDER_SPACE::Assembly** ppAssembly, SString* pDiagnosticInfo = NULL) = 0; /// /// Get LoaderAllocator for binders that contain it. For other binders, return NULL. diff --git a/src/coreclr/vm/assemblyspec.hpp b/src/coreclr/vm/assemblyspec.hpp index 977366b05242d9..f338d35b3637cd 100644 --- a/src/coreclr/vm/assemblyspec.hpp +++ b/src/coreclr/vm/assemblyspec.hpp @@ -195,7 +195,8 @@ class AssemblySpec : public BaseAssemblySpec HRESULT Bind( AppDomain* pAppDomain, - BINDER_SPACE::Assembly** ppAssembly); + BINDER_SPACE::Assembly** ppAssembly, + SString* pDiagnosticInfo = NULL); Assembly *LoadAssembly(FileLoadLevel targetLevel, BOOL fThrowOnFileNotFound = TRUE); diff --git a/src/coreclr/vm/clrex.cpp b/src/coreclr/vm/clrex.cpp index 991a29ae33f3ae..7cfa5d4c110fcd 100644 --- a/src/coreclr/vm/clrex.cpp +++ b/src/coreclr/vm/clrex.cpp @@ -1670,6 +1670,27 @@ void DECLSPEC_NORETURN EEFileLoadException::Throw(AssemblySpec *pSpec, HRESULT EX_THROW_WITH_INNER(EEFileLoadException, (name, hr), pInnerException); } +/* static */ +void DECLSPEC_NORETURN EEFileLoadException::Throw(AssemblySpec *pSpec, HRESULT hr, const SString &diagnosticInfo, Exception *pInnerException/* = NULL*/) +{ + CONTRACTL + { + GC_TRIGGERS; + THROWS; + MODE_ANY; + } + CONTRACTL_END; + + if (hr == COR_E_THREADABORTED) + COMPlusThrow(kThreadAbortException); + if (hr == E_OUTOFMEMORY) + COMPlusThrowOM(); + + StackSString name; + pSpec->GetDisplayName(0, name); + EX_THROW_WITH_INNER(EEFileLoadException, (name, hr, diagnosticInfo), pInnerException); +} + /* static */ void DECLSPEC_NORETURN EEFileLoadException::Throw(PEAssembly *pPEAssembly, HRESULT hr, Exception *pInnerException /* = NULL*/) { diff --git a/src/coreclr/vm/clrex.h b/src/coreclr/vm/clrex.h index 8f77d1e9b97486..9277d0da478d5d 100644 --- a/src/coreclr/vm/clrex.h +++ b/src/coreclr/vm/clrex.h @@ -685,6 +685,7 @@ class EEFileLoadException : public EEException static RuntimeExceptionKind GetFileLoadKind(HRESULT hr); static void DECLSPEC_NORETURN Throw(AssemblySpec *pSpec, HRESULT hr, Exception *pInnerException = NULL); + static void DECLSPEC_NORETURN Throw(AssemblySpec *pSpec, HRESULT hr, const SString &diagnosticInfo, Exception *pInnerException = NULL); static void DECLSPEC_NORETURN Throw(PEAssembly *pPEAssembly, HRESULT hr, Exception *pInnerException = NULL); static void DECLSPEC_NORETURN Throw(LPCWSTR path, HRESULT hr, Exception *pInnerException = NULL); static void DECLSPEC_NORETURN Throw(PEAssembly *parent, const void *memory, COUNT_T size, HRESULT hr, Exception *pInnerException = NULL); diff --git a/src/coreclr/vm/coreassemblyspec.cpp b/src/coreclr/vm/coreassemblyspec.cpp index e6149b940e6ccb..e992acc6073554 100644 --- a/src/coreclr/vm/coreassemblyspec.cpp +++ b/src/coreclr/vm/coreassemblyspec.cpp @@ -27,7 +27,7 @@ #include "../binder/inc/assemblybindercommon.hpp" #include "../binder/inc/applicationcontext.hpp" -HRESULT AssemblySpec::Bind(AppDomain *pAppDomain, BINDER_SPACE::Assembly** ppAssembly) +HRESULT AssemblySpec::Bind(AppDomain *pAppDomain, BINDER_SPACE::Assembly** ppAssembly, SString* pDiagnosticInfo) { CONTRACTL { @@ -63,7 +63,7 @@ HRESULT AssemblySpec::Bind(AppDomain *pAppDomain, BINDER_SPACE::Assembly** ppAs { AssemblyNameData assemblyNameData = { 0 }; PopulateAssemblyNameData(assemblyNameData); - hr = pBinder->BindAssemblyByName(&assemblyNameData, &pPrivAsm); + hr = pBinder->BindAssemblyByName(&assemblyNameData, &pPrivAsm, pDiagnosticInfo); } if (SUCCEEDED(hr)) From 1be58e3d25fbc137d4edde370a4f4021fc9e250f Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 10 Apr 2026 11:47:11 -0700 Subject: [PATCH 03/16] Revert TryOpenFile ERROR_OPEN_FAILED change Restore the original ERROR_FILE_NOT_FOUND fallback when GetLastError() is zero. The GetLastError() caching fix (using a local variable) is kept. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/peimage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/vm/peimage.cpp b/src/coreclr/vm/peimage.cpp index 2c038fabefd695..829a482910ca16 100644 --- a/src/coreclr/vm/peimage.cpp +++ b/src/coreclr/vm/peimage.cpp @@ -813,7 +813,7 @@ HRESULT PEImage::TryOpenFile(bool takeLock) if (dwLastError != 0) return HRESULT_FROM_WIN32(dwLastError); - return HRESULT_FROM_WIN32(ERROR_OPEN_FAILED); + return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); } #endif // !DACCESS_COMPILE From cc0c5beb7b0713b189d0c4cb8b4215024a68cab6 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 10 Apr 2026 12:04:32 -0700 Subject: [PATCH 04/16] Switch diagnostic strings to Printf format and append mode Replace FormatMessage (%1, %2) with Printf-style (%s, %08x) format strings in mscorrc.rc resources. Use AppendPrintf instead of FormatMessage/Set so diagnostic info accumulates across multiple failure points rather than overwriting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/binder/assemblybindercommon.cpp | 6 ++---- src/coreclr/binder/failurecache.cpp | 2 +- src/coreclr/dlls/mscorrc/mscorrc.rc | 8 ++++---- src/coreclr/vm/coreassemblyspec.cpp | 10 ++++------ 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/coreclr/binder/assemblybindercommon.cpp b/src/coreclr/binder/assemblybindercommon.cpp index 09156b96fde280..431974a99655fa 100644 --- a/src/coreclr/binder/assemblybindercommon.cpp +++ b/src/coreclr/binder/assemblybindercommon.cpp @@ -1024,13 +1024,11 @@ namespace BINDER_SPACE { hr = HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); } - else if (FAILED(hr) && pDiagnosticInfo != NULL && pDiagnosticInfo->IsEmpty()) + else if (FAILED(hr) && pDiagnosticInfo != NULL) { StackSString format; format.LoadResource(IDS_BINDING_FAILED_TO_INIT_ASSEMBLY); - StackSString hrString; - hrString.Printf("%08x", hr); - pDiagnosticInfo->FormatMessage(FORMAT_MESSAGE_FROM_STRING, format.GetUnicode(), 0, 0, assemblyPath, hrString); + pDiagnosticInfo->AppendPrintf(format.GetUTF8(), assemblyPath.GetUTF8(), hr); } return hr; diff --git a/src/coreclr/binder/failurecache.cpp b/src/coreclr/binder/failurecache.cpp index 15fac7692c4513..d342a176290655 100644 --- a/src/coreclr/binder/failurecache.cpp +++ b/src/coreclr/binder/failurecache.cpp @@ -71,7 +71,7 @@ namespace BINDER_SPACE { StackSString format; format.LoadResource(IDS_BINDING_CACHED_FAILURE_PREFIX); - pDiagnosticInfo->FormatMessage(FORMAT_MESSAGE_FROM_STRING, format.GetUnicode(), 0, 0, pFailureCachEntry->GetDiagnosticInfo()); + pDiagnosticInfo->AppendPrintf(format.GetUTF8(), pFailureCachEntry->GetDiagnosticInfo().GetUTF8()); } } diff --git a/src/coreclr/dlls/mscorrc/mscorrc.rc b/src/coreclr/dlls/mscorrc/mscorrc.rc index 6e79f1ec4c4905..677e0930783bef 100644 --- a/src/coreclr/dlls/mscorrc/mscorrc.rc +++ b/src/coreclr/dlls/mscorrc/mscorrc.rc @@ -689,10 +689,10 @@ END STRINGTABLE DISCARDABLE BEGIN - IDS_BINDING_FAILED_TO_OPEN_FILE "Failed to open file '%1'. HRESULT: 0x%2" - IDS_BINDING_EXCEPTION_OPENING_FILE "Exception opening '%1': %2. HRESULT: 0x%3" - IDS_BINDING_FAILED_TO_INIT_ASSEMBLY "Failed to initialize assembly from '%1'. HRESULT: 0x%2" - IDS_BINDING_CACHED_FAILURE_PREFIX "(Cached) %1" + IDS_BINDING_FAILED_TO_OPEN_FILE "Failed to open file '%s'. HRESULT: 0x%08x" + IDS_BINDING_EXCEPTION_OPENING_FILE "Exception opening '%s': %s. HRESULT: 0x%08x" + IDS_BINDING_FAILED_TO_INIT_ASSEMBLY "Failed to initialize assembly from '%s'. HRESULT: 0x%08x" + IDS_BINDING_CACHED_FAILURE_PREFIX "[cached failure] %s" END // diff --git a/src/coreclr/vm/coreassemblyspec.cpp b/src/coreclr/vm/coreassemblyspec.cpp index e992acc6073554..b9b634d024629c 100644 --- a/src/coreclr/vm/coreassemblyspec.cpp +++ b/src/coreclr/vm/coreassemblyspec.cpp @@ -99,9 +99,8 @@ STDAPI BinderAcquirePEImage(LPCWSTR wszAssemblyPath, { StackSString format; format.LoadResource(IDS_BINDING_FAILED_TO_OPEN_FILE); - StackSString hrString; - hrString.Printf("%08x", hr); - pDiagnosticInfo->FormatMessage(FORMAT_MESSAGE_FROM_STRING, format.GetUnicode(), 0, 0, SString(wszAssemblyPath), hrString); + SString pathStr(wszAssemblyPath); + pDiagnosticInfo->AppendPrintf(format.GetUTF8(), pathStr.GetUTF8(), hr); } goto Exit; } @@ -118,11 +117,10 @@ STDAPI BinderAcquirePEImage(LPCWSTR wszAssemblyPath, { StackSString format; format.LoadResource(IDS_BINDING_EXCEPTION_OPENING_FILE); + SString pathStr(wszAssemblyPath); StackSString exMessage; GET_EXCEPTION()->GetMessage(exMessage); - StackSString hrString; - hrString.Printf("%08x", hr); - pDiagnosticInfo->FormatMessage(FORMAT_MESSAGE_FROM_STRING, format.GetUnicode(), 0, 0, SString(wszAssemblyPath), exMessage, hrString); + pDiagnosticInfo->AppendPrintf(format.GetUTF8(), pathStr.GetUTF8(), exMessage.GetUTF8(), hr); } } EX_END_CATCH From ec7a60cc4bbbc16b7a0240ee730852961feb5303 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 10 Apr 2026 15:01:14 -0700 Subject: [PATCH 05/16] Include HRESULT message in diagnostic info and update cached prefix Use GetHRMsg to convert HRESULTs to human-readable messages (e.g. 'The process cannot access the file because it is being used by another process') instead of raw hex codes. Update cached failure prefix to '[cached failure]'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/binder/assemblybindercommon.cpp | 4 +++- src/coreclr/dlls/mscorrc/mscorrc.rc | 6 +++--- src/coreclr/vm/coreassemblyspec.cpp | 6 ++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/coreclr/binder/assemblybindercommon.cpp b/src/coreclr/binder/assemblybindercommon.cpp index 431974a99655fa..2fa2b6133382ec 100644 --- a/src/coreclr/binder/assemblybindercommon.cpp +++ b/src/coreclr/binder/assemblybindercommon.cpp @@ -1028,7 +1028,9 @@ namespace BINDER_SPACE { StackSString format; format.LoadResource(IDS_BINDING_FAILED_TO_INIT_ASSEMBLY); - pDiagnosticInfo->AppendPrintf(format.GetUTF8(), assemblyPath.GetUTF8(), hr); + StackSString hrMsg; + GetHRMsg(hr, hrMsg); + pDiagnosticInfo->AppendPrintf(format.GetUTF8(), assemblyPath.GetUTF8(), hrMsg.GetUTF8()); } return hr; diff --git a/src/coreclr/dlls/mscorrc/mscorrc.rc b/src/coreclr/dlls/mscorrc/mscorrc.rc index 677e0930783bef..b1517e177f28cc 100644 --- a/src/coreclr/dlls/mscorrc/mscorrc.rc +++ b/src/coreclr/dlls/mscorrc/mscorrc.rc @@ -689,9 +689,9 @@ END STRINGTABLE DISCARDABLE BEGIN - IDS_BINDING_FAILED_TO_OPEN_FILE "Failed to open file '%s'. HRESULT: 0x%08x" - IDS_BINDING_EXCEPTION_OPENING_FILE "Exception opening '%s': %s. HRESULT: 0x%08x" - IDS_BINDING_FAILED_TO_INIT_ASSEMBLY "Failed to initialize assembly from '%s'. HRESULT: 0x%08x" + IDS_BINDING_FAILED_TO_OPEN_FILE "Failed to open file '%s'. %s" + IDS_BINDING_EXCEPTION_OPENING_FILE "Exception opening '%s': %s" + IDS_BINDING_FAILED_TO_INIT_ASSEMBLY "Failed to initialize assembly from '%s'. %s" IDS_BINDING_CACHED_FAILURE_PREFIX "[cached failure] %s" END diff --git a/src/coreclr/vm/coreassemblyspec.cpp b/src/coreclr/vm/coreassemblyspec.cpp index b9b634d024629c..a7936995aa23c3 100644 --- a/src/coreclr/vm/coreassemblyspec.cpp +++ b/src/coreclr/vm/coreassemblyspec.cpp @@ -100,7 +100,9 @@ STDAPI BinderAcquirePEImage(LPCWSTR wszAssemblyPath, StackSString format; format.LoadResource(IDS_BINDING_FAILED_TO_OPEN_FILE); SString pathStr(wszAssemblyPath); - pDiagnosticInfo->AppendPrintf(format.GetUTF8(), pathStr.GetUTF8(), hr); + StackSString hrMsg; + GetHRMsg(hr, hrMsg); + pDiagnosticInfo->AppendPrintf(format.GetUTF8(), pathStr.GetUTF8(), hrMsg.GetUTF8()); } goto Exit; } @@ -120,7 +122,7 @@ STDAPI BinderAcquirePEImage(LPCWSTR wszAssemblyPath, SString pathStr(wszAssemblyPath); StackSString exMessage; GET_EXCEPTION()->GetMessage(exMessage); - pDiagnosticInfo->AppendPrintf(format.GetUTF8(), pathStr.GetUTF8(), exMessage.GetUTF8(), hr); + pDiagnosticInfo->AppendPrintf(format.GetUTF8(), pathStr.GetUTF8(), exMessage.GetUTF8()); } } EX_END_CATCH From c6dc939c15a2ef6c8d53499a2ebb2e203b02e626 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 10 Apr 2026 15:18:10 -0700 Subject: [PATCH 06/16] Add tests for TPA assembly load failures Add TpaBindFailureTest with three scenarios, each using a dedicated TPA assembly referenced as a normal ProjectReference: - TpaAssembly_NotFound: delete the DLL, use the type via [NoInlining] wrapper, verify exception string contains the assembly path - TpaAssembly_SharingViolation: lock the DLL exclusively, use the type, verify exception string contains the file path and HRESULT 80070020 - TpaAssembly_Corrupt: overwrite with garbage bytes, use the type, verify exception string contains the file path Each scenario triggers the load via direct type usage (not explicit Assembly.Load) to exercise the realistic JIT/type-system load path through BindByTpaList. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ime.Loader.Test.BindFailure.Corrupt.csproj | 10 +++ .../TestClass.cs | 10 +++ ...time.Loader.Test.BindFailure.Locked.csproj | 10 +++ .../TestClass.cs | 10 +++ ...ime.Loader.Test.BindFailure.Missing.csproj | 10 +++ .../TestClass.cs | 10 +++ .../tests/System.Runtime.Loader.Tests.csproj | 13 +++ .../tests/TpaLoadFailureTest.cs | 87 +++++++++++++++++++ 8 files changed, 160 insertions(+) create mode 100644 src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Corrupt/System.Runtime.Loader.Test.BindFailure.Corrupt.csproj create mode 100644 src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Corrupt/TestClass.cs create mode 100644 src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Locked/System.Runtime.Loader.Test.BindFailure.Locked.csproj create mode 100644 src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Locked/TestClass.cs create mode 100644 src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Missing/System.Runtime.Loader.Test.BindFailure.Missing.csproj create mode 100644 src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Missing/TestClass.cs create mode 100644 src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs diff --git a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Corrupt/System.Runtime.Loader.Test.BindFailure.Corrupt.csproj b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Corrupt/System.Runtime.Loader.Test.BindFailure.Corrupt.csproj new file mode 100644 index 00000000000000..e2e8167e2a6699 --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Corrupt/System.Runtime.Loader.Test.BindFailure.Corrupt.csproj @@ -0,0 +1,10 @@ + + + false + $(NetCoreAppCurrent) + true + + + + + diff --git a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Corrupt/TestClass.cs b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Corrupt/TestClass.cs new file mode 100644 index 00000000000000..b6150252a5631b --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Corrupt/TestClass.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace BindFailureTest.Corrupt +{ + public class TestClass + { + public static string GetMessage() => "Hello from Corrupt test assembly"; + } +} diff --git a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Locked/System.Runtime.Loader.Test.BindFailure.Locked.csproj b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Locked/System.Runtime.Loader.Test.BindFailure.Locked.csproj new file mode 100644 index 00000000000000..e2e8167e2a6699 --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Locked/System.Runtime.Loader.Test.BindFailure.Locked.csproj @@ -0,0 +1,10 @@ + + + false + $(NetCoreAppCurrent) + true + + + + + diff --git a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Locked/TestClass.cs b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Locked/TestClass.cs new file mode 100644 index 00000000000000..51c0d2a0b59b57 --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Locked/TestClass.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace BindFailureTest.Locked +{ + public class TestClass + { + public static string GetMessage() => "Hello from Locked test assembly"; + } +} diff --git a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Missing/System.Runtime.Loader.Test.BindFailure.Missing.csproj b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Missing/System.Runtime.Loader.Test.BindFailure.Missing.csproj new file mode 100644 index 00000000000000..e2e8167e2a6699 --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Missing/System.Runtime.Loader.Test.BindFailure.Missing.csproj @@ -0,0 +1,10 @@ + + + false + $(NetCoreAppCurrent) + true + + + + + diff --git a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Missing/TestClass.cs b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Missing/TestClass.cs new file mode 100644 index 00000000000000..290afcaaddc603 --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Test.BindFailure.Missing/TestClass.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace BindFailureTest.Missing +{ + public class TestClass + { + public static string GetMessage() => "Hello from Missing test assembly"; + } +} diff --git a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj index 6fa7eb235138d8..c1a9ae5e510215 100644 --- a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj +++ b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj @@ -31,6 +31,8 @@ + + @@ -54,6 +56,9 @@ + + + @@ -107,4 +112,12 @@ + + + + + + diff --git a/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs b/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs new file mode 100644 index 00000000000000..297da6ae1e2763 --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.IO; +using System.Runtime.CompilerServices; + +namespace System.Runtime.Loader.Tests +{ + public class TpaLoadFailureTest + { + private static string GetAssemblyPath(string assemblyFileName) + { + string appDir = Path.GetDirectoryName(AssemblyPathHelper.GetAssemblyLocation(typeof(TpaLoadFailureTest).Assembly)); + return Path.Combine(appDir, assemblyFileName); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static string UseMissingAssembly() => global::BindFailureTest.Missing.TestClass.GetMessage(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static string UseLockedAssembly() => global::BindFailureTest.Locked.TestClass.GetMessage(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static string UseCorruptAssembly() => global::BindFailureTest.Corrupt.TestClass.GetMessage(); + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.HasAssemblyFiles))] + public void NotFound_ExceptionContainsAssemblyPath() + { + // The Missing assembly is referenced at compile time but not copied to the output + // directory (Private=false), so it will not be found at runtime. + const string assemblyFileName = "System.Runtime.Loader.Test.BindFailure.Missing.dll"; + string dllPath = GetAssemblyPath(assemblyFileName); + Assert.False(File.Exists(dllPath), $"Test assembly should not be present at {dllPath}"); + + var ex = Assert.Throws(() => UseMissingAssembly()); + string exString = ex.ToString(); + Assert.Contains("System.Runtime.Loader.Test.BindFailure.Missing", exString); + Assert.Contains(dllPath, exString); + Assert.Contains(HResults.COR_E_FILENOTFOUND.ToString("X8"), exString); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.IsWindows), nameof(PlatformDetection.HasAssemblyFiles))] + public void SharingViolation_ExceptionContainsPathAndHResult() + { + // The Locked assembly is copied to the output directory so we can lock it. + const string assemblyFileName = "System.Runtime.Loader.Test.BindFailure.Locked.dll"; + string dllPath = GetAssemblyPath(assemblyFileName); + Assert.True(File.Exists(dllPath), $"Test assembly not found at {dllPath}"); + + using FileStream lockStream = new FileStream(dllPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + + var ex = Assert.Throws(() => UseLockedAssembly()); + + string exString = ex.ToString(); + Assert.Contains(dllPath, exString); + Assert.Contains(HResults.ERROR_SHARING_VIOLATION.ToString("X8"), exString); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.HasAssemblyFiles))] + public void Corrupt_ExceptionContainsPathAndHResult() + { + const int COR_E_ASSEMBLYEXPECTED = unchecked((int)0x80131018); + + // The Corrupt assembly is referenced at compile time but not copied to the output + // directory (Private=false). We write a corrupt file in its place. + const string assemblyFileName = "System.Runtime.Loader.Test.BindFailure.Corrupt.dll"; + string dllPath = GetAssemblyPath(assemblyFileName); + Assert.False(File.Exists(dllPath), $"Test assembly should not be present at {dllPath}"); + + try + { + File.WriteAllBytes(dllPath, new byte[] { 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00 }); + + var ex = Assert.Throws(() => UseCorruptAssembly()); + + string exString = ex.ToString(); + Assert.Contains(dllPath, exString); + Assert.Contains(COR_E_ASSEMBLYEXPECTED.ToString("X8"), exString); + } + finally + { + try { File.Delete(dllPath); } catch { } + } + } + } +} From 7976c5632a236c04006dfcd79aa2dc45feaa4367 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 10 Apr 2026 19:36:54 -0700 Subject: [PATCH 07/16] Change FusionLog back to get-only auto-property Get-only auto-properties can be set from constructors even across partial class files, so no setter is needed. This preserves the readonly backing field semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/IO/FileNotFoundException.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs index 5e5c87536f36bd..1175b9c9fcda0b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs @@ -68,7 +68,7 @@ private void SetMessageField() } public string? FileName { get; } - public string? FusionLog { get; internal set; } + public string? FusionLog { get; } public override string ToString() { From 96228a06a7d853b43b631f4cc41c720fa60d7375 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 10 Apr 2026 21:43:27 -0700 Subject: [PATCH 08/16] Simplify EEFileLoadException: delegating ctor and throw site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make the diagnosticInfo constructor delegate to the base constructor instead of duplicating its body. - Remove the unnecessary IsEmpty check at the throw site in AppDomain::BindAssemblySpec — always pass bindDiagnosticInfo since CreateThrowable already gates on m_diagnosticInfo.IsEmpty(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/appdomain.cpp | 6 +----- src/coreclr/vm/clrex.cpp | 11 ++--------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/coreclr/vm/appdomain.cpp b/src/coreclr/vm/appdomain.cpp index b63e53f7ad8235..c31545505dfd6d 100644 --- a/src/coreclr/vm/appdomain.cpp +++ b/src/coreclr/vm/appdomain.cpp @@ -3193,11 +3193,7 @@ PEAssembly * AppDomain::BindAssemblySpec( if (fFailure && fThrowOnFileNotFound) { - if (!bindDiagnosticInfo.IsEmpty()) - { - EEFileLoadException::Throw(pFailedSpec, COR_E_FILENOTFOUND, bindDiagnosticInfo, NULL); - } - EEFileLoadException::Throw(pFailedSpec, COR_E_FILENOTFOUND, NULL); + EEFileLoadException::Throw(pFailedSpec, COR_E_FILENOTFOUND, bindDiagnosticInfo, NULL); } } } diff --git a/src/coreclr/vm/clrex.cpp b/src/coreclr/vm/clrex.cpp index 7cfa5d4c110fcd..b4df072384be77 100644 --- a/src/coreclr/vm/clrex.cpp +++ b/src/coreclr/vm/clrex.cpp @@ -1453,10 +1453,7 @@ EEFileLoadException::EEFileLoadException(const SString &name, HRESULT hr, Except EEFileLoadException::EEFileLoadException(const SString &name, HRESULT hr, const SString &diagnosticInfo, Exception *pInnerException/* = NULL*/) - : EEException(GetFileLoadKind(hr)), - m_name(name), - m_hr(hr), - m_diagnosticInfo(diagnosticInfo) + : EEFileLoadException(name, hr, pInnerException) { CONTRACTL { @@ -1466,11 +1463,7 @@ EEFileLoadException::EEFileLoadException(const SString &name, HRESULT hr, const } CONTRACTL_END; - _ASSERTE(pInnerException == NULL || !(pInnerException->IsTransient())); - m_innerException = pInnerException ? pInnerException->DomainBoundClone() : NULL; - - if (m_name.IsEmpty()) - m_name.Set(W("")); + m_diagnosticInfo.Set(diagnosticInfo); } From f4ce4b93c3e7dc3ae489f04c3e395bab838ad7e1 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 10 Apr 2026 21:49:44 -0700 Subject: [PATCH 09/16] Add newline separators between diagnostic messages When multiple probing attempts contribute diagnostics (e.g., bundle probe then TPA probe, or cached failure replay), insert a newline before appending each new message so FusionLog is readable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/binder/assemblybindercommon.cpp | 3 +++ src/coreclr/binder/failurecache.cpp | 3 +++ src/coreclr/vm/coreassemblyspec.cpp | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/src/coreclr/binder/assemblybindercommon.cpp b/src/coreclr/binder/assemblybindercommon.cpp index 2fa2b6133382ec..f72e17846c7c3d 100644 --- a/src/coreclr/binder/assemblybindercommon.cpp +++ b/src/coreclr/binder/assemblybindercommon.cpp @@ -1026,6 +1026,9 @@ namespace BINDER_SPACE } else if (FAILED(hr) && pDiagnosticInfo != NULL) { + if (!pDiagnosticInfo->IsEmpty()) + pDiagnosticInfo->AppendUTF8("\n"); + StackSString format; format.LoadResource(IDS_BINDING_FAILED_TO_INIT_ASSEMBLY); StackSString hrMsg; diff --git a/src/coreclr/binder/failurecache.cpp b/src/coreclr/binder/failurecache.cpp index d342a176290655..1be8b8d4a0d967 100644 --- a/src/coreclr/binder/failurecache.cpp +++ b/src/coreclr/binder/failurecache.cpp @@ -69,6 +69,9 @@ namespace BINDER_SPACE hr = pFailureCachEntry->GetBindingResult(); if (pDiagnosticInfo != NULL && !pFailureCachEntry->GetDiagnosticInfo().IsEmpty()) { + if (!pDiagnosticInfo->IsEmpty()) + pDiagnosticInfo->AppendUTF8("\n"); + StackSString format; format.LoadResource(IDS_BINDING_CACHED_FAILURE_PREFIX); pDiagnosticInfo->AppendPrintf(format.GetUTF8(), pFailureCachEntry->GetDiagnosticInfo().GetUTF8()); diff --git a/src/coreclr/vm/coreassemblyspec.cpp b/src/coreclr/vm/coreassemblyspec.cpp index a7936995aa23c3..950308c2e3493d 100644 --- a/src/coreclr/vm/coreassemblyspec.cpp +++ b/src/coreclr/vm/coreassemblyspec.cpp @@ -97,6 +97,9 @@ STDAPI BinderAcquirePEImage(LPCWSTR wszAssemblyPath, { if (pDiagnosticInfo != NULL) { + if (!pDiagnosticInfo->IsEmpty()) + pDiagnosticInfo->AppendUTF8("\n"); + StackSString format; format.LoadResource(IDS_BINDING_FAILED_TO_OPEN_FILE); SString pathStr(wszAssemblyPath); @@ -117,6 +120,9 @@ STDAPI BinderAcquirePEImage(LPCWSTR wszAssemblyPath, _ASSERTE(FAILED(hr)); if (pDiagnosticInfo != NULL) { + if (!pDiagnosticInfo->IsEmpty()) + pDiagnosticInfo->AppendUTF8("\n"); + StackSString format; format.LoadResource(IDS_BINDING_EXCEPTION_OPENING_FILE); SString pathStr(wszAssemblyPath); From bdc3496916edcf596d1eefc7c65458c2f6dd0fff Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 10 Apr 2026 21:56:01 -0700 Subject: [PATCH 10/16] Move Init failure diagnostic next to the Init call Capture the diagnostic info right after pAssembly->Init fails instead of in the shared Exit path. This makes the error attribution clearer and avoids the else-if coupling with the IsFileNotFound normalization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/binder/assemblybindercommon.cpp | 28 ++++++++++++--------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/coreclr/binder/assemblybindercommon.cpp b/src/coreclr/binder/assemblybindercommon.cpp index f72e17846c7c3d..393ecf36daed55 100644 --- a/src/coreclr/binder/assemblybindercommon.cpp +++ b/src/coreclr/binder/assemblybindercommon.cpp @@ -1010,7 +1010,22 @@ namespace BINDER_SPACE } // Initialize assembly object - IF_FAIL_GO(pAssembly->Init(pPEImage, fIsInTPA)); + hr = pAssembly->Init(pPEImage, fIsInTPA); + if (FAILED(hr)) + { + if (pDiagnosticInfo != NULL) + { + if (!pDiagnosticInfo->IsEmpty()) + pDiagnosticInfo->AppendUTF8("\n"); + + StackSString format; + format.LoadResource(IDS_BINDING_FAILED_TO_INIT_ASSEMBLY); + StackSString hrMsg; + GetHRMsg(hr, hrMsg); + pDiagnosticInfo->AppendPrintf(format.GetUTF8(), assemblyPath.GetUTF8(), hrMsg.GetUTF8()); + } + goto Exit; + } // We're done *ppAssembly = pAssembly.Extract(); @@ -1024,17 +1039,6 @@ namespace BINDER_SPACE { hr = HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); } - else if (FAILED(hr) && pDiagnosticInfo != NULL) - { - if (!pDiagnosticInfo->IsEmpty()) - pDiagnosticInfo->AppendUTF8("\n"); - - StackSString format; - format.LoadResource(IDS_BINDING_FAILED_TO_INIT_ASSEMBLY); - StackSString hrMsg; - GetHRMsg(hr, hrMsg); - pDiagnosticInfo->AppendPrintf(format.GetUTF8(), assemblyPath.GetUTF8(), hrMsg.GetUTF8()); - } return hr; } From 42d636db45b185733c9014cb59a4b6057de5c5a4 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 10 Apr 2026 22:08:50 -0700 Subject: [PATCH 11/16] Fix test comments to match actual build mechanism The Missing and Corrupt assemblies are deleted by the RemoveBindFailureTestAssemblies MSBuild target after build, not by Private=false. Update comments to reflect this. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Runtime.Loader/tests/TpaLoadFailureTest.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs b/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs index 297da6ae1e2763..c5d137de685511 100644 --- a/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs +++ b/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs @@ -27,8 +27,9 @@ private static string GetAssemblyPath(string assemblyFileName) [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.HasAssemblyFiles))] public void NotFound_ExceptionContainsAssemblyPath() { - // The Missing assembly is referenced at compile time but not copied to the output - // directory (Private=false), so it will not be found at runtime. + // The Missing assembly is referenced at compile time and listed in deps.json + // (so the host adds it to the TPA list), but the DLL is deleted from the output + // directory by the RemoveBindFailureTestAssemblies MSBuild target after build. const string assemblyFileName = "System.Runtime.Loader.Test.BindFailure.Missing.dll"; string dllPath = GetAssemblyPath(assemblyFileName); Assert.False(File.Exists(dllPath), $"Test assembly should not be present at {dllPath}"); @@ -62,8 +63,10 @@ public void Corrupt_ExceptionContainsPathAndHResult() { const int COR_E_ASSEMBLYEXPECTED = unchecked((int)0x80131018); - // The Corrupt assembly is referenced at compile time but not copied to the output - // directory (Private=false). We write a corrupt file in its place. + // The Corrupt assembly is referenced at compile time and listed in deps.json + // (so the host adds it to the TPA list), but the DLL is deleted from the output + // directory by the RemoveBindFailureTestAssemblies MSBuild target after build. + // We write a corrupt file in its place. const string assemblyFileName = "System.Runtime.Loader.Test.BindFailure.Corrupt.dll"; string dllPath = GetAssemblyPath(assemblyFileName); Assert.False(File.Exists(dllPath), $"Test assembly should not be present at {dllPath}"); From db4fdb2b2a267b8362938d2fd7e2fd7bfa91c5ce Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 15 Apr 2026 13:42:12 -0700 Subject: [PATCH 12/16] Fix include ordering and use delegating constructor - Move #include common.h before failurecache.hpp (PCH trigger must be first include) - Make FileNotFoundException 3-arg ctor delegate to the 2-arg ctor via : this(fileName, hResult) instead of duplicating its body Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/IO/FileNotFoundException.CoreCLR.cs | 5 +---- src/coreclr/binder/failurecache.cpp | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/IO/FileNotFoundException.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/IO/FileNotFoundException.CoreCLR.cs index 14fd366bc4bca0..69120f6ee92a71 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/IO/FileNotFoundException.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/IO/FileNotFoundException.CoreCLR.cs @@ -14,12 +14,9 @@ internal FileNotFoundException(string? fileName, int hResult) } internal FileNotFoundException(string? fileName, int hResult, string? diagnosticInfo) - : base(null) + : this(fileName, hResult) { - HResult = hResult; - FileName = fileName; FusionLog = diagnosticInfo; - SetMessageField(); } } } diff --git a/src/coreclr/binder/failurecache.cpp b/src/coreclr/binder/failurecache.cpp index 1be8b8d4a0d967..7f488867ef58ce 100644 --- a/src/coreclr/binder/failurecache.cpp +++ b/src/coreclr/binder/failurecache.cpp @@ -11,8 +11,8 @@ // // ============================================================ -#include "failurecache.hpp" #include "common.h" +#include "failurecache.hpp" namespace BINDER_SPACE { From a298c54aa5232a8c25420ff1a20f1487cc9c5430 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 15 Apr 2026 15:34:46 -0700 Subject: [PATCH 13/16] Address PR review feedback for tests - Assert on ex.FusionLog directly instead of ex.ToString() for less brittle assertions - Use discard for unused FileStream lock variable - Move assembly deletion from MSBuild target into test setup via EnsureAssemblyRemoved helper, making tests self-contained - Remove empty catch in Corrupt test cleanup (File.Delete doesn't throw for missing files) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/System.Runtime.Loader.Tests.csproj | 8 ---- .../tests/TpaLoadFailureTest.cs | 43 ++++++++----------- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj index c1a9ae5e510215..8d9664bc9be831 100644 --- a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj +++ b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj @@ -112,12 +112,4 @@ - - - - - - diff --git a/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs b/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs index c5d137de685511..d5343dbfd1ce40 100644 --- a/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs +++ b/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs @@ -15,6 +15,14 @@ private static string GetAssemblyPath(string assemblyFileName) return Path.Combine(appDir, assemblyFileName); } + // Ensure the DLL is not present on disk. The assembly is still listed in deps.json + // so the host adds it to the TPA list, but the physical file is absent. + private static void EnsureAssemblyRemoved(string dllPath) + { + if (File.Exists(dllPath)) + File.Delete(dllPath); + } + [MethodImpl(MethodImplOptions.NoInlining)] private static string UseMissingAssembly() => global::BindFailureTest.Missing.TestClass.GetMessage(); @@ -27,35 +35,28 @@ private static string GetAssemblyPath(string assemblyFileName) [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.HasAssemblyFiles))] public void NotFound_ExceptionContainsAssemblyPath() { - // The Missing assembly is referenced at compile time and listed in deps.json - // (so the host adds it to the TPA list), but the DLL is deleted from the output - // directory by the RemoveBindFailureTestAssemblies MSBuild target after build. const string assemblyFileName = "System.Runtime.Loader.Test.BindFailure.Missing.dll"; string dllPath = GetAssemblyPath(assemblyFileName); - Assert.False(File.Exists(dllPath), $"Test assembly should not be present at {dllPath}"); + EnsureAssemblyRemoved(dllPath); var ex = Assert.Throws(() => UseMissingAssembly()); - string exString = ex.ToString(); - Assert.Contains("System.Runtime.Loader.Test.BindFailure.Missing", exString); - Assert.Contains(dllPath, exString); - Assert.Contains(HResults.COR_E_FILENOTFOUND.ToString("X8"), exString); + Assert.Contains("System.Runtime.Loader.Test.BindFailure.Missing", ex.FusionLog); + Assert.Contains(dllPath, ex.FusionLog); + Assert.Contains(HResults.COR_E_FILENOTFOUND.ToString("X8"), ex.FusionLog); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.IsWindows), nameof(PlatformDetection.HasAssemblyFiles))] public void SharingViolation_ExceptionContainsPathAndHResult() { - // The Locked assembly is copied to the output directory so we can lock it. const string assemblyFileName = "System.Runtime.Loader.Test.BindFailure.Locked.dll"; string dllPath = GetAssemblyPath(assemblyFileName); Assert.True(File.Exists(dllPath), $"Test assembly not found at {dllPath}"); - using FileStream lockStream = new FileStream(dllPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + using FileStream _ = new FileStream(dllPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); var ex = Assert.Throws(() => UseLockedAssembly()); - - string exString = ex.ToString(); - Assert.Contains(dllPath, exString); - Assert.Contains(HResults.ERROR_SHARING_VIOLATION.ToString("X8"), exString); + Assert.Contains(dllPath, ex.FusionLog); + Assert.Contains(HResults.ERROR_SHARING_VIOLATION.ToString("X8"), ex.FusionLog); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.HasAssemblyFiles))] @@ -63,27 +64,21 @@ public void Corrupt_ExceptionContainsPathAndHResult() { const int COR_E_ASSEMBLYEXPECTED = unchecked((int)0x80131018); - // The Corrupt assembly is referenced at compile time and listed in deps.json - // (so the host adds it to the TPA list), but the DLL is deleted from the output - // directory by the RemoveBindFailureTestAssemblies MSBuild target after build. - // We write a corrupt file in its place. const string assemblyFileName = "System.Runtime.Loader.Test.BindFailure.Corrupt.dll"; string dllPath = GetAssemblyPath(assemblyFileName); - Assert.False(File.Exists(dllPath), $"Test assembly should not be present at {dllPath}"); + EnsureAssemblyRemoved(dllPath); try { File.WriteAllBytes(dllPath, new byte[] { 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00 }); var ex = Assert.Throws(() => UseCorruptAssembly()); - - string exString = ex.ToString(); - Assert.Contains(dllPath, exString); - Assert.Contains(COR_E_ASSEMBLYEXPECTED.ToString("X8"), exString); + Assert.Contains(dllPath, ex.FusionLog); + Assert.Contains(COR_E_ASSEMBLYEXPECTED.ToString("X8"), ex.FusionLog); } finally { - try { File.Delete(dllPath); } catch { } + File.Delete(dllPath); } } } From 275d271761f8fa7a3ab897b5292c0073c642c6a6 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 15 Apr 2026 19:57:18 -0700 Subject: [PATCH 14/16] Fix test failures: ensure DLLs removed before test payload packaging The MSBuild delete target with only AfterTargets=Build ran too early - the Helix payload packaging step re-copies outputs afterward, so the deleted DLLs reappear in the test payload. Add BeforeTargets=PrepareForRun so the deletion also happens right before the test archive is created. Use \ instead of \ to match the ZipTestArchive source. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/System.Runtime.Loader.Tests.csproj | 10 +++ .../tests/TpaLoadFailureTest.cs | 72 ++++++++++++------- 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj index 8d9664bc9be831..6f2729821042c9 100644 --- a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj +++ b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj @@ -112,4 +112,14 @@ + + + + + + + diff --git a/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs b/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs index d5343dbfd1ce40..dc6ca9b84e7904 100644 --- a/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs +++ b/src/libraries/System.Runtime.Loader/tests/TpaLoadFailureTest.cs @@ -9,38 +9,35 @@ namespace System.Runtime.Loader.Tests { public class TpaLoadFailureTest { - private static string GetAssemblyPath(string assemblyFileName) + private static string GetAssemblyPath(string assemblyName) { string appDir = Path.GetDirectoryName(AssemblyPathHelper.GetAssemblyLocation(typeof(TpaLoadFailureTest).Assembly)); - return Path.Combine(appDir, assemblyFileName); - } - - // Ensure the DLL is not present on disk. The assembly is still listed in deps.json - // so the host adds it to the TPA list, but the physical file is absent. - private static void EnsureAssemblyRemoved(string dllPath) - { - if (File.Exists(dllPath)) - File.Delete(dllPath); + return Path.Combine(appDir, assemblyName + ".dll"); } [MethodImpl(MethodImplOptions.NoInlining)] - private static string UseMissingAssembly() => global::BindFailureTest.Missing.TestClass.GetMessage(); + private static string UseMissingAssembly() => BindFailureTest.Missing.TestClass.GetMessage(); [MethodImpl(MethodImplOptions.NoInlining)] - private static string UseLockedAssembly() => global::BindFailureTest.Locked.TestClass.GetMessage(); + private static string UseLockedAssembly() => BindFailureTest.Locked.TestClass.GetMessage(); [MethodImpl(MethodImplOptions.NoInlining)] - private static string UseCorruptAssembly() => global::BindFailureTest.Corrupt.TestClass.GetMessage(); + private static string UseCorruptAssembly() => BindFailureTest.Corrupt.TestClass.GetMessage(); [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.HasAssemblyFiles))] public void NotFound_ExceptionContainsAssemblyPath() { - const string assemblyFileName = "System.Runtime.Loader.Test.BindFailure.Missing.dll"; - string dllPath = GetAssemblyPath(assemblyFileName); - EnsureAssemblyRemoved(dllPath); + // The Missing assembly is listed in deps.json (so the host adds it to the TPA + // list) but deleted from the output directory by the RemoveBindFailureTestAssemblies + // MSBuild target, so it will not be found at runtime. + const string assemblyName = "System.Runtime.Loader.Test.BindFailure.Missing"; + string dllPath = GetAssemblyPath(assemblyName); + Assert.False(File.Exists(dllPath), $"Test assembly should not be present at {dllPath}"); var ex = Assert.Throws(() => UseMissingAssembly()); - Assert.Contains("System.Runtime.Loader.Test.BindFailure.Missing", ex.FusionLog); + Assert.NotNull(ex.FileName); + Assert.Contains(assemblyName, ex.FileName); + Assert.NotNull(ex.FusionLog); Assert.Contains(dllPath, ex.FusionLog); Assert.Contains(HResults.COR_E_FILENOTFOUND.ToString("X8"), ex.FusionLog); } @@ -48,15 +45,30 @@ public void NotFound_ExceptionContainsAssemblyPath() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.IsWindows), nameof(PlatformDetection.HasAssemblyFiles))] public void SharingViolation_ExceptionContainsPathAndHResult() { - const string assemblyFileName = "System.Runtime.Loader.Test.BindFailure.Locked.dll"; - string dllPath = GetAssemblyPath(assemblyFileName); - Assert.True(File.Exists(dllPath), $"Test assembly not found at {dllPath}"); + // The Locked assembly is listed in deps.json but deleted from the output + // directory by the RemoveBindFailureTestAssemblies MSBuild target. + // We write a file and lock it before the load attempt. + const string assemblyName = "System.Runtime.Loader.Test.BindFailure.Locked"; + string dllPath = GetAssemblyPath(assemblyName); + Assert.False(File.Exists(dllPath), $"Test assembly should not be present at {dllPath}"); - using FileStream _ = new FileStream(dllPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + try + { + File.WriteAllBytes(dllPath, new byte[] { 0x4D, 0x5A, 0x90, 0x00 }); - var ex = Assert.Throws(() => UseLockedAssembly()); - Assert.Contains(dllPath, ex.FusionLog); - Assert.Contains(HResults.ERROR_SHARING_VIOLATION.ToString("X8"), ex.FusionLog); + using FileStream _ = new FileStream(dllPath, FileMode.Open, FileAccess.Read, FileShare.None); + + var ex = Assert.Throws(() => UseLockedAssembly()); + Assert.NotNull(ex.FileName); + Assert.Contains(assemblyName, ex.FileName); + Assert.NotNull(ex.FusionLog); + Assert.Contains(dllPath, ex.FusionLog); + Assert.Contains(HResults.ERROR_SHARING_VIOLATION.ToString("X8"), ex.FusionLog); + } + finally + { + File.Delete(dllPath); + } } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.HasAssemblyFiles))] @@ -64,15 +76,21 @@ public void Corrupt_ExceptionContainsPathAndHResult() { const int COR_E_ASSEMBLYEXPECTED = unchecked((int)0x80131018); - const string assemblyFileName = "System.Runtime.Loader.Test.BindFailure.Corrupt.dll"; - string dllPath = GetAssemblyPath(assemblyFileName); - EnsureAssemblyRemoved(dllPath); + // The Corrupt assembly is listed in deps.json but deleted from the output + // directory by the RemoveBindFailureTestAssemblies MSBuild target. + // We write a corrupt file in its place before the load attempt. + const string assemblyName = "System.Runtime.Loader.Test.BindFailure.Corrupt"; + string dllPath = GetAssemblyPath(assemblyName); + Assert.False(File.Exists(dllPath), $"Test assembly should not be present at {dllPath}"); try { File.WriteAllBytes(dllPath, new byte[] { 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00 }); var ex = Assert.Throws(() => UseCorruptAssembly()); + Assert.NotNull(ex.FileName); + Assert.Contains(assemblyName, ex.FileName); + Assert.NotNull(ex.FusionLog); Assert.Contains(dllPath, ex.FusionLog); Assert.Contains(COR_E_ASSEMBLYEXPECTED.ToString("X8"), ex.FusionLog); } From f8981faca43cc9d7364f6493d63300a19f20e75a Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Tue, 21 Apr 2026 21:20:42 -0700 Subject: [PATCH 15/16] Move diagnostic info into BindResult struct Address review feedback: collapse the separate SString* pDiagnosticInfo output parameter into BindResult, which already carries the binding outcome. The internal methods (BindByName, BindLocked, BindByTpaList) now use pBindResult->GetDiagnosticInfo() instead of a separate pointer. BindAssembly copies the diagnostic info from BindResult to the caller's SString* after binding completes. External methods (GetAssembly) that don't take BindResult keep their SString* parameter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/binder/assemblybindercommon.cpp | 31 +++++++++---------- .../binder/inc/assemblybindercommon.hpp | 9 ++---- src/coreclr/binder/inc/bindresult.hpp | 4 +++ 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/coreclr/binder/assemblybindercommon.cpp b/src/coreclr/binder/assemblybindercommon.cpp index 393ecf36daed55..a88d3cfa50b859 100644 --- a/src/coreclr/binder/assemblybindercommon.cpp +++ b/src/coreclr/binder/assemblybindercommon.cpp @@ -215,8 +215,7 @@ namespace BINDER_SPACE false, // skipFailureCaching false, // skipVersionCompatibilityCheck excludeAppPaths, - &bindResult, - pDiagnosticInfo)); + &bindResult)); // Remember the post-bind version kContextVersion = pApplicationContext->GetVersion(); @@ -226,6 +225,11 @@ namespace BINDER_SPACE Exit: tracer.TraceBindResult(bindResult); + if (pDiagnosticInfo != NULL && !bindResult.GetDiagnosticInfo().IsEmpty()) + { + pDiagnosticInfo->Append(bindResult.GetDiagnosticInfo()); + } + if (bindResult.HaveResult()) { BindResult hostBindResult; @@ -396,8 +400,7 @@ namespace BINDER_SPACE bool skipFailureCaching, bool skipVersionCompatibilityCheck, bool excludeAppPaths, - BindResult *pBindResult, - SString *pDiagnosticInfo) + BindResult *pBindResult) { HRESULT hr = S_OK; PathString assemblyDisplayName; @@ -406,7 +409,7 @@ namespace BINDER_SPACE pAssemblyName->GetDisplayName(assemblyDisplayName, AssemblyName::INCLUDE_VERSION); - hr = pApplicationContext->GetFailureCache()->Lookup(assemblyDisplayName, pDiagnosticInfo); + hr = pApplicationContext->GetFailureCache()->Lookup(assemblyDisplayName, &pBindResult->GetDiagnosticInfo()); if (FAILED(hr)) { if ((hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) && skipFailureCaching) @@ -434,8 +437,7 @@ namespace BINDER_SPACE pAssemblyName, skipVersionCompatibilityCheck, excludeAppPaths, - pBindResult, - pDiagnosticInfo)); + pBindResult)); if (!pBindResult->HaveResult()) { @@ -460,7 +462,7 @@ namespace BINDER_SPACE } } - hr = pApplicationContext->AddToFailureCache(assemblyDisplayName, hr, pDiagnosticInfo); + hr = pApplicationContext->AddToFailureCache(assemblyDisplayName, hr, &pBindResult->GetDiagnosticInfo()); } LogExit: @@ -472,8 +474,7 @@ namespace BINDER_SPACE AssemblyName *pAssemblyName, bool skipVersionCompatibilityCheck, bool excludeAppPaths, - BindResult *pBindResult, - SString *pDiagnosticInfo) + BindResult *pBindResult) { HRESULT hr = S_OK; @@ -515,8 +516,7 @@ namespace BINDER_SPACE hr = BindByTpaList(pApplicationContext, pAssemblyName, excludeAppPaths, - pBindResult, - pDiagnosticInfo); + pBindResult); if (SUCCEEDED(hr) && pBindResult->HaveResult()) { bool isCompatible = IsCompatibleAssemblyVersion(pAssemblyName, pBindResult->GetAssemblyName()); @@ -832,8 +832,7 @@ namespace BINDER_SPACE HRESULT AssemblyBinderCommon::BindByTpaList(ApplicationContext *pApplicationContext, AssemblyName *pRequestedAssemblyName, bool excludeAppPaths, - BindResult *pBindResult, - SString *pDiagnosticInfo) + BindResult *pBindResult) { HRESULT hr = S_OK; @@ -870,7 +869,7 @@ namespace BINDER_SPACE TRUE, // fIsInTPA &pTPAAssembly, probeExtensionResult, - pDiagnosticInfo); + &pBindResult->GetDiagnosticInfo()); BinderTracing::PathProbed(assemblyFilePath, BinderTracing::PathSource::Bundle, hr); @@ -902,7 +901,7 @@ namespace BINDER_SPACE TRUE, // fIsInTPA &pTPAAssembly, ProbeExtensionResult::Invalid(), - pDiagnosticInfo); + &pBindResult->GetDiagnosticInfo()); BinderTracing::PathProbed(fileName, BinderTracing::PathSource::ApplicationAssemblies, hr); pBindResult->SetAttemptResult(hr, pTPAAssembly); diff --git a/src/coreclr/binder/inc/assemblybindercommon.hpp b/src/coreclr/binder/inc/assemblybindercommon.hpp index 4c5cefd5cc3dfe..df37a572e9d029 100644 --- a/src/coreclr/binder/inc/assemblybindercommon.hpp +++ b/src/coreclr/binder/inc/assemblybindercommon.hpp @@ -74,15 +74,13 @@ namespace BINDER_SPACE /* in */ bool skipFailureCaching, /* in */ bool skipVersionCompatibilityCheck, /* in */ bool excludeAppPaths, - /* out */ BindResult *pBindResult, - /* out */ SString *pDiagnosticInfo = NULL); + /* out */ BindResult *pBindResult); static HRESULT BindLocked(/* in */ ApplicationContext *pApplicationContext, /* in */ AssemblyName *pAssemblyName, /* in */ bool skipVersionCompatibilityCheck, /* in */ bool excludeAppPaths, - /* out */ BindResult *pBindResult, - /* out */ SString *pDiagnosticInfo = NULL); + /* out */ BindResult *pBindResult); static HRESULT FindInExecutionContext(/* in */ ApplicationContext *pApplicationContext, /* in */ AssemblyName *pAssemblyName, @@ -91,8 +89,7 @@ namespace BINDER_SPACE static HRESULT BindByTpaList(/* in */ ApplicationContext *pApplicationContext, /* in */ AssemblyName *pRequestedAssemblyName, /* in */ bool excludeAppPaths, - /* out */ BindResult *pBindResult, - /* out */ SString *pDiagnosticInfo = NULL); + /* out */ BindResult *pBindResult); static HRESULT Register(/* in */ ApplicationContext *pApplicationContext, /* in */ BindResult *pBindResult); diff --git a/src/coreclr/binder/inc/bindresult.hpp b/src/coreclr/binder/inc/bindresult.hpp index 5309a3d5cffaed..8b24d3d0b9aa25 100644 --- a/src/coreclr/binder/inc/bindresult.hpp +++ b/src/coreclr/binder/inc/bindresult.hpp @@ -56,12 +56,16 @@ namespace BINDER_SPACE const AttemptResult* GetAttempt(bool foundInContext) const; + SString& GetDiagnosticInfo() { return m_diagnosticInfo; } + protected: bool m_isContextBound; ReleaseHolder m_pAssembly; AttemptResult m_inContextAttempt; AttemptResult m_applicationAssembliesAttempt; + + SString m_diagnosticInfo; }; }; From c6eecd37eceb09da8799f3c57211fb15173f2a55 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Thu, 23 Apr 2026 21:08:56 -0700 Subject: [PATCH 16/16] Take diagnostic info by const SString& in failure cache Add The diagnostic info is only read when adding an entry to the failure cache, so take it by const reference instead of a pointer. This eliminates the null check and makes the caller's intent clearer. Lookup still takes a pointer since it writes to the out parameter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/binder/assemblybindercommon.cpp | 2 +- src/coreclr/binder/failurecache.cpp | 6 +++--- src/coreclr/binder/inc/applicationcontext.hpp | 2 +- src/coreclr/binder/inc/applicationcontext.inl | 4 ++-- src/coreclr/binder/inc/failurecache.hpp | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/coreclr/binder/assemblybindercommon.cpp b/src/coreclr/binder/assemblybindercommon.cpp index bf61e640da2f9a..1acae66f1e395f 100644 --- a/src/coreclr/binder/assemblybindercommon.cpp +++ b/src/coreclr/binder/assemblybindercommon.cpp @@ -466,7 +466,7 @@ namespace BINDER_SPACE } } - hr = pApplicationContext->AddToFailureCache(assemblyDisplayName, hr, &pBindResult->GetDiagnosticInfo()); + hr = pApplicationContext->AddToFailureCache(assemblyDisplayName, hr, pBindResult->GetDiagnosticInfo()); } LogExit: diff --git a/src/coreclr/binder/failurecache.cpp b/src/coreclr/binder/failurecache.cpp index 7f488867ef58ce..cd7b721beecff7 100644 --- a/src/coreclr/binder/failurecache.cpp +++ b/src/coreclr/binder/failurecache.cpp @@ -34,7 +34,7 @@ namespace BINDER_SPACE HRESULT FailureCache::Add(SString &assemblyNameorPath, HRESULT hrBindingResult, - SString *pDiagnosticInfo) + const SString &diagnosticInfo) { HRESULT hr = S_OK; @@ -46,9 +46,9 @@ namespace BINDER_SPACE pFailureCacheEntry->GetAssemblyNameOrPath().Set(assemblyNameorPath); pFailureCacheEntry->SetBindingResult(hrBindingResult); - if (pDiagnosticInfo != NULL && !pDiagnosticInfo->IsEmpty()) + if (!diagnosticInfo.IsEmpty()) { - pFailureCacheEntry->SetDiagnosticInfo(*pDiagnosticInfo); + pFailureCacheEntry->SetDiagnosticInfo(diagnosticInfo); } Hash::Add(pFailureCacheEntry); diff --git a/src/coreclr/binder/inc/applicationcontext.hpp b/src/coreclr/binder/inc/applicationcontext.hpp index fcd28d4391c516..b383141011f01d 100644 --- a/src/coreclr/binder/inc/applicationcontext.hpp +++ b/src/coreclr/binder/inc/applicationcontext.hpp @@ -96,7 +96,7 @@ namespace BINDER_SPACE inline FailureCache *GetFailureCache(); inline HRESULT AddToFailureCache(SString &assemblyNameOrPath, HRESULT hrBindResult, - SString *pDiagnosticInfo = NULL); + const SString &diagnosticInfo); inline StringArrayList *GetAppPaths(); inline SimpleNameToFileNameMap *GetTpaList(); inline StringArrayList *GetPlatformResourceRoots(); diff --git a/src/coreclr/binder/inc/applicationcontext.inl b/src/coreclr/binder/inc/applicationcontext.inl index b5b04e0564cfaf..be31b37129fa2d 100644 --- a/src/coreclr/binder/inc/applicationcontext.inl +++ b/src/coreclr/binder/inc/applicationcontext.inl @@ -42,9 +42,9 @@ FailureCache *ApplicationContext::GetFailureCache() HRESULT ApplicationContext::AddToFailureCache(SString &assemblyNameOrPath, HRESULT hrBindResult, - SString *pDiagnosticInfo) + const SString &diagnosticInfo) { - HRESULT hr = GetFailureCache()->Add(assemblyNameOrPath, hrBindResult, pDiagnosticInfo); + HRESULT hr = GetFailureCache()->Add(assemblyNameOrPath, hrBindResult, diagnosticInfo); IncrementVersion(); return hr; } diff --git a/src/coreclr/binder/inc/failurecache.hpp b/src/coreclr/binder/inc/failurecache.hpp index 62ece00daed618..931c9ee45e4635 100644 --- a/src/coreclr/binder/inc/failurecache.hpp +++ b/src/coreclr/binder/inc/failurecache.hpp @@ -29,7 +29,7 @@ namespace BINDER_SPACE HRESULT Add(/* in */ SString &assemblyNameorPath, /* in */ HRESULT hrBindResult, - /* in */ SString *pDiagnosticInfo = NULL); + /* in */ const SString &diagnosticInfo); HRESULT Lookup(/* in */ SString &assemblyNameorPath, /* out */ SString *pDiagnosticInfo = NULL); void Remove(/* in */ SString &assemblyName);