Skip to content

Include requesting assembly chain in assembly load failure exceptions#125795

Open
Copilot wants to merge 33 commits intomainfrom
copilot/fix-issue-9185
Open

Include requesting assembly chain in assembly load failure exceptions#125795
Copilot wants to merge 33 commits intomainfrom
copilot/fix-issue-9185

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 19, 2026

This pull request enhances .NET exception diagnostics for assembly loading failures by tracking and surfacing the chain of requesting assemblies when a dependency is missing or invalid. This helps developers more easily identify which assemblies triggered a load failure, especially in complex dependency scenarios.

Exception information:

  • The constructors for BadImageFormatException, FileLoadException, and FileNotFoundException now accept an additional requestingAssemblyChain parameter, which records the chain of assemblies that led to the load failure. This chain is stored and included in the exception’s string representation.
  • The ToString() methods of these exceptions now append a "Requested by: ..." message listing the requesting assembly chain, improving error clarity for users.
  • A new resource string, IO_FileLoad_RequestedBy, is added for this message.

Runtime and binding cache:

  • The runtime now allows querying the parent (requesting) assembly for an assembly in the AssemblySpecBindingCache. New methods allow walking the chain of requesting assemblies up to a depth of 10, building a diagnostic chain for exceptions.
  • The EEFileLoadException and related logic are updated to capture and propagate the requesting assembly chain, including in exception cloning and when thrown from the runtime.
  • The unmanaged-to-managed exception creation path is updated to pass the requesting assembly chain to managed exception constructors.
  • The chain is a best-effort representation. An assembly can be requested by multiple different parents. The message uses the parent of the first matching cached assembly found during iteration.

Testing and validation:

  • Adds new test assemblies (MissingDependency.Leaf, MissingDependency.Mid) and a test in AssemblyLoadContextTest to verify that the requesting assembly chain appears in exception messages when a transitive dependency is missing.

These changes significantly improve the developer experience when diagnosing assembly loading failures by providing more actionable information in exception messages.

Example:

System.IO.FileNotFoundException: Could not load file or assembly 'LibC, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.
File name: 'LibC, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'
Requested by: LibB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
 --> LibA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
 --> helloworld, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
 --> System.Private.CoreLib, Version=11.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
   at LibB.LibBClass.GetMessage() in C:\repos\helloworld\LibB\Class1.cs:line 5
   at LibB.LibBClass.GetMessage() in C:\repos\helloworld\LibB\Class1.cs:line 5
   at LibA.LibAClass.GetMessage() in C:\repos\helloworld\LibA\Class1.cs:line 5
   at Program.Main(String[] args) in C:\repos\helloworld\Program.cs:line 329

@dotnet/appmodel @AaronRobinsonMSFT


💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.

Copilot AI and others added 2 commits March 19, 2026 19:02
When an assembly fails to load, the FileLoadException/FileNotFoundException
now includes the name of the requesting (parent) assembly in the FusionLog
property. This helps diagnose dependency chain issues by showing which
assembly triggered the failed load.

The information flows from the native AssemblySpec::GetParentAssembly()
through EEFileLoadException::CreateThrowable() to the managed exception's
FusionLog property, which is displayed in ToString() output.

Fixes #9185

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
BadImageFormatException is also created by EEFileLoadException::CreateThrowable()
when the HRESULT maps to kBadImageFormatException. Without the 3-arg constructor,
the requesting assembly info would be silently dropped for bad image format errors.

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
@elinor-fung
Copy link
Copy Markdown
Member

@copilot we should include the full requesting assembly chain instead of just the parent.

Walk the inner exception chain to build the full dependency path.
When A→B→C→D and D fails, D's FusionLog now shows:

  Requesting assembly: 'C'
  Requesting assembly: 'B'
  Requesting assembly: 'A'

At each Throw(AssemblySpec*) level, the current parent assembly name
is appended to every inner EEFileLoadException's chain. The managed
FormatRequestingAssemblyChain helper formats each newline-separated
name with the "Requesting assembly:" resource string.

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 19, 2026

@copilot we should include the full requesting assembly chain instead of just the parent.

Updated in 228155e. Now at each Throw(AssemblySpec*) level, the current parent assembly name is appended to every inner EEFileLoadException's chain. For a dependency chain A→B→C→D where D is missing, D's FusionLog will show:

Requesting assembly: 'C, Version=...'
Requesting assembly: 'B, Version=...'
Requesting assembly: 'A, Version=...'

The managed FormatRequestingAssemblyChain helper (shared by all three exception types) formats each newline-separated name from the native chain string.

Copilot AI changed the title Include requesting assembly in assembly load failure exceptions Include full requesting assembly chain in assembly load failure exceptions Mar 19, 2026
…dd tests

The requesting assembly chain is now appended to the exception's Message
property instead of FusionLog. This means users see the chain directly
in the exception message when it is thrown, without needing to inspect
FusionLog separately.

Added tests for all three exception types (FileLoadException,
FileNotFoundException, BadImageFormatException) that verify:
- Single requesting assembly appears in Message
- Multiple requesting assemblies (chain) appear in Message
- FusionLog remains null

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
…missing transitive dependency

Remove the 6 reflection-based tests that invoked private constructors via
reflection from BadImageFormatExceptionTests, FileLoadExceptionTests, and
FileNotFoundExceptionTests.

Add an integration test in System.Runtime.Loader.Tests that creates a real
dependency chain (Root -> Mid -> Leaf) where the transitive dependency
(Leaf) is missing at runtime. The test verifies that a FileNotFoundException
is thrown with the correct missing assembly name when the chain is exercised.

Three helper assembly projects are added:
- MissingDependency.Leaf (not deployed via PrivateAssets=all)
- MissingDependency.Mid (depends on Leaf)
- MissingDependency.Root (depends on Mid)

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
The test now asserts that the FileNotFoundException message contains
"MissingDependency.Mid" (the requesting assembly), verifying that the
entire dependency chain is reported in the exception message, not just
the missing assembly file name.

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
The test now asserts that both MissingDependency.Mid and
MissingDependency.Root appear in the exception message, verifying
the entire dependency chain (Root → Mid → Leaf) is reported.

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
@elinor-fung elinor-fung changed the title Include full requesting assembly chain in assembly load failure exceptions Include requesting assembly chain in assembly load failure exceptions Apr 1, 2026
@github-actions

This comment has been minimized.

… cache

The binding cache stores raw Assembly* parent pointers via CopyFrom when
assemblies are bound. If a collectible AssemblyLoadContext is unloaded,
the parent assembly may be freed while the cache entry persists — the
cache cleanup only removes entries where m_pAssembly is the collectible
assembly, not entries where m_pParentAssembly points to it.

LookupParentAssemblyForAssembly iterates all cache entries and could
return a stale parent pointer, causing a SIGSEGV when the chain-walking
code in EEFileLoadException::Throw dereferences it via GetDisplayName.

Add a ContainsAssembly validation before returning a parent, so stale
entries are skipped and iteration continues to find a valid parent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 3, 2026

O(bindingCacheSize × loadedAssemblyCount)

This sounds like a problem for workloads with thousands of assemblies (not unusual).

When a collectible ALC is unloaded, RemoveAssembly removes entries
where m_pAssembly matches the unloaded assembly, but entries that
reference it as m_pParentAssembly were left with stale pointers.
LookupParentAssemblyForAssembly could then return a freed pointer
during the chain walk in EEFileLoadException::Throw.

Fix the root cause by also clearing m_pParentAssembly on any cache
entry that references the unloaded assembly as its parent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Build the chain string under DomainCacheCrst so all Assembly*
dereferences are safe from concurrent collectible ALC unload.
GetParentAssemblyChain on AppDomain acquires the lock, scans the
binding cache once into a MapSHash<Assembly*, Assembly*>, then
walks the map to build the chain string — all under the lock.

Remove now-unused FindCachedParentAssembly and
LookupParentAssemblyForAssembly.

Also rename test to MissingTransitiveDependency_ShowsRequestingAssemblyChain.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 6, 2026 17:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Comment on lines 70 to +83
@@ -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());

Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

The new _requestingAssemblyChain field is appended in ToString(), but it isn’t preserved across exception serialization/deserialization (the SerializationInfo ctor and GetObjectData only round-trip FileName/FusionLog). Since this type is still [Serializable] and already maintains custom serialization fields, please include the requesting assembly chain in the serialization payload and restore it in the serialization ctor so diagnostics survive remoting/logging scenarios that serialize exceptions.

Copilot uses AI. Check for mistakes.
@github-actions

This comment has been minimized.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 7, 2026

Serialization — Intentionally not updated
The [Obsolete] GetObjectData methods do not serialize _requestingAssemblyChain. This is the correct decision — BinaryFormatter serialization is deprecated and will be removed. Adding new fields to deprecated serialization paths would be inappropriate. (Previous Copilot reviews incorrectly flagged this as a bug.)

This contradicts what we have just done in #126428 . We should be consistent - either keep updating the binary serializers or not at all.

@github-actions

This comment has been minimized.

Restore the requesting assembly chain across legacy exception
serialization round-trips for FileLoadException,
FileNotFoundException, and BadImageFormatException, keeping the
behavior consistent with other updated binary serializer paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 7, 2026 17:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.

{
FileName = info.GetString("FileNotFound_FileName");
FusionLog = info.GetString("FileNotFound_FusionLog");
_requestingAssemblyChain = info.GetString("FileNotFound_RequestingAssemblyChain");
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The serialization constructor now unconditionally reads "FileNotFound_RequestingAssemblyChain" via SerializationInfo.GetString. If this exception is deserialized from older serialized data that doesn't contain this entry, GetString will throw a SerializationException. Consider treating this value as optional (e.g., catch SerializationException / check for the entry) and default to null when missing for better forward/backward compatibility.

Suggested change
_requestingAssemblyChain = info.GetString("FileNotFound_RequestingAssemblyChain");
try
{
_requestingAssemblyChain = info.GetString("FileNotFound_RequestingAssemblyChain");
}
catch (SerializationException)
{
_requestingAssemblyChain = null;
}

Copilot uses AI. Check for mistakes.
Comment on lines 78 to 84
protected FileLoadException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
FileName = info.GetString("FileLoad_FileName");
FusionLog = info.GetString("FileLoad_FusionLog");
_requestingAssemblyChain = info.GetString("FileLoad_RequestingAssemblyChain");
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The serialization constructor now unconditionally reads "FileLoad_RequestingAssemblyChain" via SerializationInfo.GetString. If FileLoadException is deserialized from older serialized data that lacks this entry, GetString will throw. Consider making this field optional during deserialization (default to null when missing).

Copilot uses AI. Check for mistakes.
@github-actions

This comment has been minimized.

Use SerializationInfo.GetValueNoThrow for the new
*_RequestingAssemblyChain fields so older payloads that do not
contain the data still deserialize successfully.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

…est.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 7, 2026 18:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Comment on lines +1658 to +1675
// Build the requesting assembly chain: start with the immediate parent,
// then walk up the binding cache to find transitive requesting assemblies.
StackSString requestingChain;
pParentAssembly->GetDisplayName(requestingChain);

EX_TRY
{
const int MaxChainDepth = 10;
AppDomain::GetCurrentDomain()->GetParentAssemblyChain(
pParentAssembly, requestingChain, MaxChainDepth);
}
EX_CATCH
{
// If the chain walk fails for any reason, just use what we have so far
}
EX_END_CATCH

pException->SetRequestingAssemblyChain(requestingChain);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

pParentAssembly->GetDisplayName(requestingChain) (and the initial construction of requestingChain) is outside the EX_TRY/EX_CATCH best-effort block. If GetDisplayName throws (e.g., OOM while formatting the display name), this Throw path can end up throwing that secondary exception instead of the intended EEFileLoadException. Consider wrapping the entire requesting-chain construction (including the initial GetDisplayName and SetRequestingAssemblyChain) in an EX_TRY with EX_SWALLOW_NONTERMINAL (or similar) so diagnostics failures never change the primary failure being thrown.

Suggested change
// Build the requesting assembly chain: start with the immediate parent,
// then walk up the binding cache to find transitive requesting assemblies.
StackSString requestingChain;
pParentAssembly->GetDisplayName(requestingChain);
EX_TRY
{
const int MaxChainDepth = 10;
AppDomain::GetCurrentDomain()->GetParentAssemblyChain(
pParentAssembly, requestingChain, MaxChainDepth);
}
EX_CATCH
{
// If the chain walk fails for any reason, just use what we have so far
}
EX_END_CATCH
pException->SetRequestingAssemblyChain(requestingChain);
EX_TRY
{
// Build the requesting assembly chain: start with the immediate parent,
// then walk up the binding cache to find transitive requesting assemblies.
StackSString requestingChain;
pParentAssembly->GetDisplayName(requestingChain);
const int MaxChainDepth = 10;
AppDomain::GetCurrentDomain()->GetParentAssemblyChain(
pParentAssembly, requestingChain, MaxChainDepth);
pException->SetRequestingAssemblyChain(requestingChain);
}
EX_CATCH
{
// If building the requesting assembly chain fails for any reason,
// ignore the diagnostic data and preserve the primary exception.
}
EX_END_CATCH

Copilot uses AI. Check for mistakes.
Comment on lines +294 to +296
Assert.Contains("MissingDependency.Mid", exString);
Assert.Contains(" --> ", exString);
Assert.Contains("MissingDependency.Root", exString);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The assertions for MissingDependency.Mid / MissingDependency.Root are currently satisfied by the stack trace as well as by the new requesting-assembly-chain text, so this test may still pass even if the new "Requested by" block is missing or incomplete. Consider asserting that these assembly names appear before the stack trace section (e.g., before the first "\n at ") to ensure they’re coming from the requesting chain rather than from stack frames.

Suggested change
Assert.Contains("MissingDependency.Mid", exString);
Assert.Contains(" --> ", exString);
Assert.Contains("MissingDependency.Root", exString);
int stackTraceIndex = exString.IndexOf(Environment.NewLine + " at ", StringComparison.Ordinal);
string messagePortion = stackTraceIndex >= 0 ? exString[..stackTraceIndex] : exString;
Assert.Contains("MissingDependency.Mid", messagePortion);
Assert.Contains(" --> ", exString);
Assert.Contains("MissingDependency.Root", messagePortion);

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

🤖 Copilot Code Review — PR #125795

Note

This review was generated by GitHub Copilot.

Holistic Assessment

Motivation: Well-justified. Issue #9185 (open since 2018) captures a genuine, widespread developer pain point: transitive assembly load failures give no indication of which assembly triggered the load, making FileNotFoundException/FileLoadException messages hard to act on. The improvement to ToString() is clearly net-positive.

Approach: Reasonable overall. Walking the binding cache at throw time is pragmatic and low-overhead. The "best-effort" framing (exceptions swallowed) is appropriate for diagnostic metadata. However there are correctness gaps around how the immediate parent's display name is retrieved (not inside the best-effort guard) and a formatting bug in BadImageFormatException.ToString().

Summary: ⚠️ Needs Changes. Two concrete bugs need fixing before merge: the GetDisplayName call for the immediate parent sits outside the EX_TRY/EX_CATCH guard (defeating the best-effort intent and causing a memory leak of pException on failure), and BadImageFormatException.ToString() is missing a newline separator before InnerExceptionPrefix while the sibling exceptions correctly include it.


Detailed Findings

GetDisplayName for immediate parent is outside the best-effort guard — clrex.cpp ~line 1655

In Throw(AssemblySpec*), the chain-building is described as best-effort (exceptions swallowed), but pParentAssembly->GetDisplayName(requestingChain) is called before the EX_TRY:

Assembly *pParentAssembly = pSpec->GetParentAssembly();
if (pParentAssembly != NULL)
{
    StackSString requestingChain;
    pParentAssembly->GetDisplayName(requestingChain);  // ← OUTSIDE EX_TRY — can throw OOM

    EX_TRY
    {
        AppDomain::GetCurrentDomain()->GetParentAssemblyChain(...);
    }
    EX_CATCH { /* swallowed */ }
    EX_END_CATCH

    pException->SetRequestingAssemblyChain(requestingChain);
}

Two problems:

  1. Lost exception: If GetDisplayName throws (e.g. OOM), the throw propagates up and the caller sees an OOM instead of the intended EEFileLoadException. The diagnostic gathering — which is stated to be best-effort — is not actually best-effort for the first step.

  2. Memory leak: pException has already been allocated by this point (new EEFileLoadException(name, hr)). If GetDisplayName throws, pException is leaked because PAL_CPP_THROW is never reached.

Fix: wrap the entire chain-building block (including the initial GetDisplayName) in the EX_TRY/EX_CATCH:

if (pParentAssembly != NULL)
{
    StackSString requestingChain;
    EX_TRY
    {
        pParentAssembly->GetDisplayName(requestingChain);
        AppDomain::GetCurrentDomain()->GetParentAssemblyChain(
            pParentAssembly, requestingChain, MaxChainDepth);
    }
    EX_CATCH { /* swallowed — diagnostic is best-effort */ }
    EX_END_CATCH

    if (!requestingChain.IsEmpty())
        pException->SetRequestingAssemblyChain(requestingChain);
}

BadImageFormatException.ToString() — missing newline before InnerExceptionPrefix

FileLoadException.cs and FileNotFoundException.cs both correctly use Environment.NewLineConst + InnerExceptionPrefix when appending the inner exception. BadImageFormatException.cs does not:

// BadImageFormatException.cs — missing Environment.NewLineConst:
if (InnerException != null)
    s += InnerExceptionPrefix + InnerException.ToString();  // ← no newline

// FileLoadException.cs — correct:
if (InnerException != null)
    s += Environment.NewLineConst + InnerExceptionPrefix + InnerException.ToString();
```

`InnerExceptionPrefix` is `" ---> "` (no leading newline). With the chain section now added before it, when both `_requestingAssemblyChain` and `InnerException` are present, the output becomes:

```
Requested by: Mid, ...
 --> Root, ... ---> System.IO.FileNotFoundException: ...

The ---> inner runs on the same line as the last chain entry. The pre-existing bug was always there but was less visible when only the file name appeared before InnerException. Now that the chain is added, this formatting error is consistently triggered in the exact scenario this PR aims to improve.

Fix:

if (InnerException != null)
    s += Environment.NewLineConst + InnerExceptionPrefix + InnerException.ToString();

⚠️ Other Throw overloads receive no chain information

Three overloads still use the original EX_THROW_WITH_INNER and never call GetParentAssemblyChain:

  • Throw(PEAssembly*, HRESULT, Exception*) — load-by-path scenarios
  • Throw(LPCWSTR, HRESULT, Exception*) — raw path
  • Throw(PEAssembly*, memory, size, HRESULT, Exception*) — load-from-memory

These cases may not have parent assembly information from an AssemblySpec, but they are still called from contexts where a parent assembly exists. The gap means the chain only appears for AssemblySpec-based loads. This should be at minimum noted in a comment, and ideally tracked as a follow-up issue.

⚠️ Test ConditionalFact is missing IsAssemblyLoadingSupported

The new test uses:

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR))]

The immediately adjacent test (added in the same file) uses:

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsAssemblyLoadingSupported), nameof(PlatformDetection.IsCoreCLR))]

Assembly loading may not be supported on all CoreCLR configurations. The new test should match the pattern of its sibling and add nameof(PlatformDetection.IsAssemblyLoadingSupported).

⚠️ ToString() behavioral change should be documented as a breaking change

Changing ToString() output on FileLoadException, FileNotFoundException, and BadImageFormatException is a user-visible behavioral breaking change. Any test or tooling that asserts on the exact string representation of these exceptions (common in NUnit/xUnit test suites) will break. Per the dotnet/runtime process, this warrants a breaking change issue in dotnet/docs.

✅ Thread safety — DomainCacheCrst protects the chain walk

GetParentAssemblyChain holds DomainCacheCrstHolderForGCPreemp for the entire operation. RemoveAssemblyFromCache (the only path that calls RemoveAssembly) acquires DomainCacheCrstHolderForGCCoop before modifying the cache. Both use the same underlying m_DomainCacheCrst. The ClearParentAssembly write in RemoveAssembly and the GetParentAssemblyMap read in the chain walk are therefore mutually exclusive. ✅

✅ Manual macro expansion is correct

The manual expansion of EX_THROW_WITH_INNER in Throw(AssemblySpec*) is faithful to the macro definition: FAULT_NOT_FATAL()ExThrowWithInnerHelpernew EEFileLoadExceptionSetInnerException → chain logic → STRESS_LOG3EX_THROW_DEBUG_TRAPPAL_CPP_THROW. ✅

✅ Serialization backward compatibility

The deserialization path uses GetValueNoThrow, which returns null when the key is absent (reading data serialized by older runtimes). ToString() only appends the chain when _requestingAssemblyChain is non-null/non-empty. No compatibility issue. ✅

✅ Collectible ALC stale pointer safety

RemoveAssembly now calls ClearParentAssembly on any cache entry that references the removed assembly as its parent. Because RemoveAssemblyFromCache holds m_DomainCacheCrst during both this clear and the subsequent iteration in GetParentAssemblyMap, there is no window where a stale Assembly* could be returned under the lock. ✅

✅ Exception swallowing in chain walk is appropriate

The EX_TRY/EX_CATCH around GetParentAssemblyChain silently drops failures during the chain walk. This is explicitly documented as best-effort diagnostic information, and losing it should not affect the load failure exception itself. ✅

Generated by Code Review for issue #125795 ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants