Fix circular init in AOT equality comparer causing NRE on Mono#16615
Merged
akoeplinger merged 1 commit intodotnet:mainfrom Mar 20, 2026
Merged
Fix circular init in AOT equality comparer causing NRE on Mono#16615akoeplinger merged 1 commit intodotnet:mainfrom
akoeplinger merged 1 commit intodotnet:mainfrom
Conversation
…/WASM The defaultComparer field in AssertEqualityComparer (AOT path) eagerly creates AssertEqualityComparerAdapter<object>(AssertEqualityComparer<object>()). This triggers AssertEqualityComparer<object>'s static initializer, which reads DefaultInnerComparer = GetDefaultInnerComparer(typeof(object)), which reads back defaultComparer — before it has been assigned. On Mono/WASM, this causes DefaultInnerComparer to be permanently null. When comparing ValueTuple elements via IStructuralEquatable.Equals, the null comparer is passed through, causing NullReferenceException inside ValueTuple's implementation. Fix by removing the cached static field and creating a new instance per call, matching the non-AOT (reflection) path's behavior. The re-entrant type initialization resolves correctly because the second access to AssertEqualityComparer<object>'s cctor is a no-op (already in progress on the same thread), so the instance constructor runs without re-triggering the static initializer. By the time the outer call returns, DefaultInnerComparer has been assigned. Verified on both CoreCLR and Mono runtimes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
akoeplinger
approved these changes
Mar 20, 2026
agocke
added a commit
to agocke/assert.xunit
that referenced
this pull request
Mar 20, 2026
The defaultComparer field in AssertEqualityComparer (AOT path) eagerly creates AssertEqualityComparerAdapter<object>, which triggers AssertEqualityComparer<object>'s static initializer. That initializer reads defaultComparer back via GetDefaultInnerComparer before the field has been assigned. On Mono/WASM this results in DefaultInnerComparer being permanently null, leading to NullReferenceException when comparing value types via IStructuralEquatable. Fix by creating a new instance on each call, matching the non-AOT reflection path behavior. Port of dotnet/arcade#16615 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bradwilson
pushed a commit
to xunit/assert.xunit
that referenced
this pull request
Mar 20, 2026
The defaultComparer field in AssertEqualityComparer (AOT path) eagerly creates AssertEqualityComparerAdapter<object>, which triggers AssertEqualityComparer<object>'s static initializer. That initializer reads defaultComparer back via GetDefaultInnerComparer before the field has been assigned. On Mono/WASM this results in DefaultInnerComparer being permanently null, leading to NullReferenceException when comparing value types via IStructuralEquatable. Fix by creating a new instance on each call, matching the non-AOT reflection path behavior. Port of dotnet/arcade#16615 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defaultComparer field in AssertEqualityComparer (AOT path) eagerly
creates AssertEqualityComparerAdapter(AssertEqualityComparer()).
This triggers AssertEqualityComparer's static initializer, which reads
DefaultInnerComparer = GetDefaultInnerComparer(typeof(object)), which reads
back defaultComparer — before it has been assigned. On Mono/WASM, this causes
DefaultInnerComparer to be permanently null. When comparing ValueTuple
elements via IStructuralEquatable.Equals, the null comparer is passed through,
causing NullReferenceException inside ValueTuple's implementation.
Fix by removing the cached static field and creating a new instance per call,
matching the non-AOT (reflection) path's behavior. The re-entrant type
initialization resolves correctly because the second access to
AssertEqualityComparer's cctor is a no-op (already in progress on the
same thread), so the instance constructor runs without re-triggering the
static initializer. By the time the outer call returns, DefaultInnerComparer
has been assigned.