diff --git a/src/coreclr/System.Private.CoreLib/src/System/BadImageFormatException.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/BadImageFormatException.CoreCLR.cs index 88a951412d6a05..920efedf8b1e54 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/BadImageFormatException.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/BadImageFormatException.CoreCLR.cs @@ -5,11 +5,12 @@ namespace System { public partial class BadImageFormatException { - internal BadImageFormatException(string? fileName, int hResult) + internal BadImageFormatException(string? fileName, string? requestingAssemblyChain, int hResult) : base(null) { HResult = hResult; _fileName = fileName; + _requestingAssemblyChain = requestingAssemblyChain; SetMessageField(); } } 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..15a0fd60c01852 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 @@ -9,11 +9,12 @@ namespace System.IO { public partial class FileLoadException { - private FileLoadException(string? fileName, int hResult) + private FileLoadException(string? fileName, string? requestingAssemblyChain, int hResult) : base(null) { HResult = hResult; FileName = fileName; + _requestingAssemblyChain = requestingAssemblyChain; _message = FormatFileLoadExceptionMessage(FileName, HResult); } @@ -47,18 +48,19 @@ 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, char* pRequestingAssemblyChain, int hresult, object* pThrowable, Exception* pException) { try { string? fileName = pFileName is not null ? new string(pFileName) : null; + string? requestingAssemblyChain = pRequestingAssemblyChain is not null ? new string(pRequestingAssemblyChain) : null; Debug.Assert(Enum.IsDefined(kind)); *pThrowable = kind switch { - FileLoadExceptionKind.BadImageFormat => new BadImageFormatException(fileName, hresult), - FileLoadExceptionKind.FileNotFound => new FileNotFoundException(fileName, hresult), + FileLoadExceptionKind.BadImageFormat => new BadImageFormatException(fileName, requestingAssemblyChain, hresult), + FileLoadExceptionKind.FileNotFound => new FileNotFoundException(fileName, requestingAssemblyChain, hresult), FileLoadExceptionKind.OutOfMemory => new OutOfMemoryException(), - _ /* FileLoadExceptionKind.FileLoad */ => new FileLoadException(fileName, hresult), + _ /* FileLoadExceptionKind.FileLoad */ => new FileLoadException(fileName, requestingAssemblyChain, hresult), }; } catch (Exception ex) 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..2ce6d85af1e22b 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 @@ -5,11 +5,12 @@ namespace System.IO { public partial class FileNotFoundException { - internal FileNotFoundException(string? fileName, int hResult) + internal FileNotFoundException(string? fileName, string? requestingAssemblyChain, int hResult) : base(null) { HResult = hResult; FileName = fileName; + _requestingAssemblyChain = requestingAssemblyChain; SetMessageField(); } } diff --git a/src/coreclr/vm/appdomain.cpp b/src/coreclr/vm/appdomain.cpp index 488e407303fd85..5a902c22ebafd3 100644 --- a/src/coreclr/vm/appdomain.cpp +++ b/src/coreclr/vm/appdomain.cpp @@ -3029,6 +3029,40 @@ BOOL AppDomain::IsCached(AssemblySpec *pSpec) return m_AssemblyCache.Contains(pSpec); } +void AppDomain::GetParentAssemblyChain(Assembly *pStartAssembly, SString &chain, int maxDepth) +{ + STANDARD_VM_CONTRACT; + + // Hold the lock for the entire chain build so that all Assembly* + // from the cache are safe from collectible ALC unload. + GCX_PREEMP(); + DomainCacheCrstHolderForGCCoop lock(this); + + MapSHash parentMap; + m_AssemblyCache.GetParentAssemblyMap(parentMap); + + Assembly *pWalkAssembly = pStartAssembly; + for (int depth = 0; depth < maxDepth && pWalkAssembly != NULL; depth++) + { + Assembly *pParent; + if (!parentMap.Lookup(pWalkAssembly, &pParent)) + break; + + if (pParent == pWalkAssembly) + break; + + StackSString parentName; + pParent->GetDisplayName(parentName); + chain.Append(W("\n --> ")); + chain.Append(parentName); + + if (pParent->IsSystem()) + break; + + pWalkAssembly = pParent; + } +} + PEAssembly* AppDomain::FindCachedFile(AssemblySpec* pSpec, BOOL fThrow /*=TRUE*/) { CONTRACTL diff --git a/src/coreclr/vm/appdomain.hpp b/src/coreclr/vm/appdomain.hpp index e5126e40accd96..08138af6a315ae 100644 --- a/src/coreclr/vm/appdomain.hpp +++ b/src/coreclr/vm/appdomain.hpp @@ -1115,6 +1115,8 @@ class AppDomain final return m_AssemblyCache.LookupAssembly(pSpec, fThrow); } + void GetParentAssemblyChain(Assembly *pStartAssembly, SString &chain, int maxDepth); + private: PEAssembly* FindCachedFile(AssemblySpec* pSpec, BOOL fThrow = TRUE); BOOL IsCached(AssemblySpec *pSpec); diff --git a/src/coreclr/vm/assemblyspec.cpp b/src/coreclr/vm/assemblyspec.cpp index 638ff52d5e458a..2d8f08c524f339 100644 --- a/src/coreclr/vm/assemblyspec.cpp +++ b/src/coreclr/vm/assemblyspec.cpp @@ -703,6 +703,38 @@ PEAssembly *AssemblySpecBindingCache::LookupFile(AssemblySpec *pSpec, BOOL fThro } } +// Caller must hold DomainCacheCrst. +// The binding cache may contain multiple entries for the same assembly +// (bound under different AssemblySpecs), possibly with a different parent. +// The first match found during iteration wins, so the map is best-effort. +void AssemblySpecBindingCache::GetParentAssemblyMap(MapSHash &parentMap) +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_ANY; + } + CONTRACTL_END; + + PtrHashMap::PtrIterator i = m_map.begin(); + while (!i.end()) + { + AssemblyBinding *b = (AssemblyBinding*) i.GetValue(); + if (!b->IsError()) + { + Assembly *pAssembly = b->GetAssembly(); + Assembly *pParent = b->GetParentAssembly(); + if (pAssembly != NULL && pParent != NULL) + { + if (parentMap.LookupPtr(pAssembly) == NULL) + parentMap.Add(pAssembly, pParent); + } + } + ++i; + } +} + class AssemblyBindingHolder { @@ -1042,6 +1074,10 @@ BOOL AssemblySpecBindingCache::RemoveAssembly(Assembly* pAssembly) result = TRUE; } + else if (entry->GetParentAssembly() == pAssembly) + { + entry->ClearParentAssembly(); + } ++i; } diff --git a/src/coreclr/vm/assemblyspec.hpp b/src/coreclr/vm/assemblyspec.hpp index d4e2a57a5ac395..bbef4db42ea724 100644 --- a/src/coreclr/vm/assemblyspec.hpp +++ b/src/coreclr/vm/assemblyspec.hpp @@ -263,6 +263,8 @@ class AssemblySpecBindingCache inline Assembly* GetAssembly(){ LIMITED_METHOD_CONTRACT; return m_pAssembly; }; inline void SetAssembly(Assembly* pAssembly){ LIMITED_METHOD_CONTRACT; m_pAssembly = pAssembly; }; inline PEAssembly* GetFile(){ LIMITED_METHOD_CONTRACT; return m_pPEAssembly;}; + inline Assembly* GetParentAssembly(){ LIMITED_METHOD_CONTRACT; return m_spec.GetParentAssembly(); }; + inline void ClearParentAssembly(){ LIMITED_METHOD_CONTRACT; m_spec.SetParentAssembly(NULL); }; inline BOOL IsError(){ LIMITED_METHOD_CONTRACT; return (m_exceptionType!=EXTYPE_NONE);}; // bound to the file, but failed later @@ -409,6 +411,7 @@ class AssemblySpecBindingCache Assembly *LookupAssembly(AssemblySpec *pSpec, BOOL fThrow=TRUE); PEAssembly *LookupFile(AssemblySpec *pSpec, BOOL fThrow = TRUE); + void GetParentAssemblyMap(MapSHash &parentMap); BOOL StoreAssembly(AssemblySpec *pSpec, Assembly *pAssembly); BOOL StorePEAssembly(AssemblySpec *pSpec, PEAssembly *pPEAssembly); diff --git a/src/coreclr/vm/clrex.cpp b/src/coreclr/vm/clrex.cpp index 565f8ec633b68e..5ba65aaae28977 100644 --- a/src/coreclr/vm/clrex.cpp +++ b/src/coreclr/vm/clrex.cpp @@ -1587,10 +1587,11 @@ OBJECTREF EEFileLoadException::CreateThrowable() GCPROTECT_BEGIN(gc); LPCWSTR pFileName = m_name.GetUnicode(); + LPCWSTR pRequestingChain = m_requestingAssemblyChain.IsEmpty() ? NULL : m_requestingAssemblyChain.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, pRequestingChain, (int)m_hr, &gc.pNewException); _ASSERTE(gc.pNewException->GetMethodTable() == CoreLibBinder::GetException(m_kind)); GCPROTECT_END(); @@ -1624,8 +1625,6 @@ BOOL EEFileLoadException::CheckType(Exception* ex) // @todo: ideally we would use inner exceptions with these routines -/* static */ - /* static */ void DECLSPEC_NORETURN EEFileLoadException::Throw(AssemblySpec *pSpec, HRESULT hr, Exception *pInnerException/* = NULL*/) { @@ -1644,7 +1643,46 @@ void DECLSPEC_NORETURN EEFileLoadException::Throw(AssemblySpec *pSpec, HRESULT StackSString name; pSpec->GetDisplayName(0, name); - EX_THROW_WITH_INNER(EEFileLoadException, (name, hr), pInnerException); + + // Extract the requesting assembly chain for diagnostic purposes + { + FAULT_NOT_FATAL(); + + Exception *inner2 = ExThrowWithInnerHelper(pInnerException); + EEFileLoadException *pException = new EEFileLoadException(name, hr); + pException->SetInnerException(inner2); + + Assembly *pParentAssembly = pSpec->GetParentAssembly(); + if (pParentAssembly != NULL) + { + StackSString requestingChain; + + EX_TRY + { + // Build the requesting assembly chain: start with the immediate parent, + // then walk up the binding cache to find transitive requesting assemblies. + pParentAssembly->GetDisplayName(requestingChain); + + const int MaxChainDepth = 10; + AppDomain::GetCurrentDomain()->GetParentAssemblyChain( + pParentAssembly, requestingChain, MaxChainDepth); + + pException->SetRequestingAssemblyChain(requestingChain); + } + EX_CATCH + { + // Ignore failures while building best-effort diagnostic data and preserve + // the primary file load exception. + } + EX_END_CATCH + } + + STRESS_LOG3(LF_EH, LL_INFO100, "EX_THROW_WITH_INNER Type = 0x%x HR = 0x%x, " + INDEBUG(__FILE__) " line %d\n", EEFileLoadException::GetType(), + pException->GetHR(), __LINE__); + EX_THROW_DEBUG_TRAP(__FUNCTION__, __FILE__, __LINE__, "EEFileLoadException", pException->GetHR(), "(name, hr)"); + PAL_CPP_THROW(EEFileLoadException *, pException); + } } /* static */ diff --git a/src/coreclr/vm/clrex.h b/src/coreclr/vm/clrex.h index 01fbce5df1a1c7..0f8f316b76f3aa 100644 --- a/src/coreclr/vm/clrex.h +++ b/src/coreclr/vm/clrex.h @@ -665,12 +665,19 @@ class EEFileLoadException : public EEException private: SString m_name; HRESULT m_hr; + SString m_requestingAssemblyChain; public: EEFileLoadException(const SString &name, HRESULT hr, Exception *pInnerException = NULL); ~EEFileLoadException(); + void SetRequestingAssemblyChain(const SString &requestingAssemblyChain) + { + WRAPPER_NO_CONTRACT; + m_requestingAssemblyChain = requestingAssemblyChain; + } + // virtual overrides HRESULT GetHR() { @@ -692,7 +699,9 @@ class EEFileLoadException : public EEException virtual Exception *CloneHelper() { WRAPPER_NO_CONTRACT; - return new EEFileLoadException(m_name, m_hr); + EEFileLoadException *pClone = new EEFileLoadException(m_name, m_hr); + pClone->SetRequestingAssemblyChain(m_requestingAssemblyChain); + return pClone; } private: diff --git a/src/coreclr/vm/wasm/callhelpers-reverse.cpp b/src/coreclr/vm/wasm/callhelpers-reverse.cpp index 25ccba61ded632..b61aeb0d7e13fe 100644 --- a/src/coreclr/vm/wasm/callhelpers-reverse.cpp +++ b/src/coreclr/vm/wasm/callhelpers-reverse.cpp @@ -287,17 +287,17 @@ static void Call_System_Private_CoreLib_System_Reflection_LoaderAllocator_Create ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_Reflection_LoaderAllocator_Create_I32_I32_RetVoid, (int8_t*)args, sizeof(args), nullptr, (PCODE)&Call_System_Private_CoreLib_System_Reflection_LoaderAllocator_Create_I32_I32_RetVoid); } -static MethodDesc* MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid = nullptr; -static void Call_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid(int32_t arg0, void * arg1, int32_t arg2, void * arg3, void * arg4) +static MethodDesc* MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid = nullptr; +static void Call_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid(int32_t arg0, void * arg1, void * arg2, int32_t arg3, void * arg4, void * arg5) { - int64_t args[5] = { (int64_t)arg0, (int64_t)arg1, (int64_t)arg2, (int64_t)arg3, (int64_t)arg4 }; + int64_t args[6] = { (int64_t)arg0, (int64_t)arg1, (int64_t)arg2, (int64_t)arg3, (int64_t)arg4, (int64_t)arg5 }; // Lazy lookup of MethodDesc for the function export scenario. - if (!MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid) + if (!MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid) { - LookupUnmanagedCallersOnlyMethodByName("System.IO.FileLoadException, System.Private.CoreLib", "Create", &MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid); + LookupUnmanagedCallersOnlyMethodByName("System.IO.FileLoadException, System.Private.CoreLib", "Create", &MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid); } - ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid, (int8_t*)args, sizeof(args), nullptr, (PCODE)&Call_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid); + ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid, (int8_t*)args, sizeof(args), nullptr, (PCODE)&Call_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid); } static MethodDesc* MD_System_Private_CoreLib_System_TypeLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid = nullptr; @@ -1216,7 +1216,7 @@ const ReverseThunkMapEntry g_ReverseThunks[] = { 4090197812, "ConvertToManaged#3:System.Private.CoreLib:System.StubHelpers:BSTRMarshaler", { &MD_System_Private_CoreLib_System_StubHelpers_BSTRMarshaler_ConvertToManaged_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_StubHelpers_BSTRMarshaler_ConvertToManaged_I32_I32_I32_RetVoid } }, { 1901425681, "ConvertToNative#2:System.Private.CoreLib:System.StubHelpers:BSTRMarshaler", { &MD_System_Private_CoreLib_System_StubHelpers_BSTRMarshaler_ConvertToNative_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_System_StubHelpers_BSTRMarshaler_ConvertToNative_I32_I32_RetI32 } }, { 1243134822, "Create#2:System.Private.CoreLib:System.Reflection:LoaderAllocator", { &MD_System_Private_CoreLib_System_Reflection_LoaderAllocator_Create_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Reflection_LoaderAllocator_Create_I32_I32_RetVoid } }, - { 1899576323, "Create#5:System.Private.CoreLib:System.IO:FileLoadException", { &MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid } }, + { 1447412576, "Create#6:System.Private.CoreLib:System.IO:FileLoadException", { &MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid } }, { 1263271190, "Create#6:System.Private.CoreLib:System:TypeLoadException", { &MD_System_Private_CoreLib_System_TypeLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_TypeLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid } }, { 509807279, "CreateArgumentException#5:System.Private.CoreLib:System:Exception", { &MD_System_Private_CoreLib_System_Exception_CreateArgumentException_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Exception_CreateArgumentException_I32_I32_I32_I32_I32_RetVoid } }, { 1570902419, "CreateAssemblyName#3:System.Private.CoreLib:System.Reflection:AssemblyName", { &MD_System_Private_CoreLib_System_Reflection_AssemblyName_CreateAssemblyName_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Reflection_AssemblyName_CreateAssemblyName_I32_I32_I32_RetVoid } }, diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index cf37982fbe434e..ac51112ce4dbd1 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -2840,6 +2840,9 @@ Could not load the file '{0}'. + + Requested by: {0} + Directory path: '{0}' diff --git a/src/libraries/System.Private.CoreLib/src/System/BadImageFormatException.cs b/src/libraries/System.Private.CoreLib/src/System/BadImageFormatException.cs index 437c20b9c9ab11..75d26cd5070eed 100644 --- a/src/libraries/System.Private.CoreLib/src/System/BadImageFormatException.cs +++ b/src/libraries/System.Private.CoreLib/src/System/BadImageFormatException.cs @@ -17,6 +17,7 @@ public partial class BadImageFormatException : SystemException { private readonly string? _fileName; // The name of the corrupt PE file. private readonly string? _fusionLog; // fusion log (when applicable) + private readonly string? _requestingAssemblyChain; public BadImageFormatException() : base(SR.Arg_BadImageFormatException) @@ -56,6 +57,7 @@ protected BadImageFormatException(SerializationInfo info, StreamingContext conte { _fileName = info.GetString("BadImageFormat_FileName"); _fusionLog = info.GetString("BadImageFormat_FusionLog"); + _requestingAssemblyChain = (string?)info.GetValueNoThrow("BadImageFormat_RequestingAssemblyChain", typeof(string)); } [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] @@ -65,6 +67,7 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont base.GetObjectData(info, context); info.AddValue("BadImageFormat_FileName", _fileName, typeof(string)); info.AddValue("BadImageFormat_FusionLog", _fusionLog, typeof(string)); + info.AddValue("BadImageFormat_RequestingAssemblyChain", _requestingAssemblyChain, typeof(string)); } public override string Message @@ -97,6 +100,9 @@ public override string ToString() if (!string.IsNullOrEmpty(_fileName)) s += Environment.NewLineConst + SR.Format(SR.IO_FileName_Name, _fileName); + if (!string.IsNullOrEmpty(_requestingAssemblyChain)) + s += Environment.NewLineConst + SR.Format(SR.IO_FileLoad_RequestedBy, _requestingAssemblyChain.ReplaceLineEndings()); + if (InnerException != null) s += InnerExceptionPrefix + InnerException.ToString(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileLoadException.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileLoadException.cs index 2413ce80c2b0a3..040a4bcbc11ad3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileLoadException.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileLoadException.cs @@ -46,6 +46,7 @@ public FileLoadException(string? message, string? fileName, Exception? inner) public string? FileName { get; } public string? FusionLog { get; } + private readonly string? _requestingAssemblyChain; public override string ToString() { @@ -54,6 +55,9 @@ public override string ToString() if (!string.IsNullOrEmpty(FileName)) s += Environment.NewLineConst + SR.Format(SR.IO_FileName_Name, FileName); + if (!string.IsNullOrEmpty(_requestingAssemblyChain)) + s += Environment.NewLineConst + SR.Format(SR.IO_FileLoad_RequestedBy, _requestingAssemblyChain.ReplaceLineEndings()); + if (InnerException != null) s += Environment.NewLineConst + InnerExceptionPrefix + InnerException.ToString(); @@ -76,6 +80,7 @@ protected FileLoadException(SerializationInfo info, StreamingContext context) { FileName = info.GetString("FileLoad_FileName"); FusionLog = info.GetString("FileLoad_FusionLog"); + _requestingAssemblyChain = (string?)info.GetValueNoThrow("FileLoad_RequestingAssemblyChain", typeof(string)); } [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] @@ -85,6 +90,7 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont base.GetObjectData(info, context); info.AddValue("FileLoad_FileName", FileName, typeof(string)); info.AddValue("FileLoad_FusionLog", FusionLog, typeof(string)); + info.AddValue("FileLoad_RequestingAssemblyChain", _requestingAssemblyChain, typeof(string)); } } } 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..0cb4bc3b1241c3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs @@ -69,6 +69,7 @@ private void SetMessageField() public string? FileName { get; } public string? FusionLog { get; } + private readonly string? _requestingAssemblyChain; public override string ToString() { @@ -77,6 +78,9 @@ public override string ToString() if (!string.IsNullOrEmpty(FileName)) s += Environment.NewLineConst + SR.Format(SR.IO_FileName_Name, FileName); + if (!string.IsNullOrEmpty(_requestingAssemblyChain)) + s += Environment.NewLineConst + SR.Format(SR.IO_FileLoad_RequestedBy, _requestingAssemblyChain.ReplaceLineEndings()); + if (InnerException != null) s += Environment.NewLineConst + InnerExceptionPrefix + InnerException.ToString(); @@ -98,6 +102,7 @@ protected FileNotFoundException(SerializationInfo info, StreamingContext context { FileName = info.GetString("FileNotFound_FileName"); FusionLog = info.GetString("FileNotFound_FusionLog"); + _requestingAssemblyChain = (string?)info.GetValueNoThrow("FileNotFound_RequestingAssemblyChain", typeof(string)); } [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] @@ -107,6 +112,7 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont base.GetObjectData(info, context); info.AddValue("FileNotFound_FileName", FileName, typeof(string)); info.AddValue("FileNotFound_FusionLog", FusionLog, typeof(string)); + info.AddValue("FileNotFound_RequestingAssemblyChain", _requestingAssemblyChain, typeof(string)); } } } diff --git a/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs b/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs index 323a9706dc351e..19604e0455aa74 100644 --- a/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs +++ b/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs @@ -278,6 +278,25 @@ public static void LoadNonRuntimeAssembly() Assert.IsType(error.InnerException); } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR))] + public static void MissingTransitiveDependency_ShowsRequestingAssemblyChain() + { + // MissingDependency.Root depends on MissingDependency.Mid which depends on MissingDependency.Leaf. + // MissingDependency.Leaf.dll is not deployed (via PrivateAssets=all in Mid's project reference). + // When Root calls Mid's method that uses Leaf types, the runtime throws FileNotFoundException + // for the missing Leaf assembly. The exception message should include the full requesting + // assembly chain (Mid and Root) so users can diagnose dependency loading issues. + FileNotFoundException ex = Assert.Throws( + () => MissingDependency.Root.RootClass.UseMiddle()); + + Assert.NotNull(ex.FileName); + Assert.Contains("MissingDependency.Leaf", ex.FileName); + string exString = ex.ToString(); + Assert.Contains("MissingDependency.Mid", exString); + Assert.Contains(" --> ", exString); + Assert.Contains("MissingDependency.Root", exString); + } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsAssemblyLoadingSupported), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.HasAssemblyFiles))] public static void InvalidCastException_DifferentALC_ShowsAssemblyInfo() { diff --git a/src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/LeafClass.cs b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/LeafClass.cs new file mode 100644 index 00000000000000..f6da4365e871ae --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/LeafClass.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 MissingDependency.Leaf +{ + public class LeafClass + { + public string GetValue() => "Leaf"; + } +} diff --git a/src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/MissingDependency.Leaf.csproj b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/MissingDependency.Leaf.csproj new file mode 100644 index 00000000000000..95057b96d5d206 --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/MissingDependency.Leaf.csproj @@ -0,0 +1,8 @@ + + + $(NetCoreAppCurrent) + + + + + diff --git a/src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MidClass.cs b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MidClass.cs new file mode 100644 index 00000000000000..59766f3d60fc4c --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MidClass.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using MissingDependency.Leaf; + +namespace MissingDependency.Mid +{ + public class MidClass + { + [MethodImpl(MethodImplOptions.NoInlining)] + public static string UseLeaf() + { + return new LeafClass().GetValue(); + } + } +} diff --git a/src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MissingDependency.Mid.csproj b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MissingDependency.Mid.csproj new file mode 100644 index 00000000000000..dfb002634d4be4 --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MissingDependency.Mid.csproj @@ -0,0 +1,13 @@ + + + $(NetCoreAppCurrent) + + + + + + + all + + + diff --git a/src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/MissingDependency.Root.csproj b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/MissingDependency.Root.csproj new file mode 100644 index 00000000000000..01dcb3ef46c3ba --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/MissingDependency.Root.csproj @@ -0,0 +1,11 @@ + + + $(NetCoreAppCurrent) + + + + + + + + diff --git a/src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/RootClass.cs b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/RootClass.cs new file mode 100644 index 00000000000000..7a665a8874249f --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/RootClass.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using MissingDependency.Mid; + +namespace MissingDependency.Root +{ + public class RootClass + { + [MethodImpl(MethodImplOptions.NoInlining)] + public static string UseMiddle() + { + return MidClass.UseLeaf(); + } + } +} 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 073972916ff2cd..bfbbc14c89f004 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 @@ -60,6 +60,7 @@ +