Repository navigation
[browser][coreCLR][R2R] By-value Int128/UInt128/Decimal128 argument corrupted through interface dispatch under wasm R2R #132137
Description
Summary
On browser CoreCLR with framework ReadyToRun (the model added in #129634), a value type with a homogeneous two-ulong (128-bit) layout — Int128, UInt128, Decimal128 — passed by value as an argument through an interface / constrained-generic dispatch stub is corrupted: the callee receives garbage in that argument. Direct (non-interface) instance calls, the static == operator, and calls whose argument is a boxed object are all correct. Only the by-value struct argument going through an interface stub is wrong.
This is not a generic "16-byte struct" problem — decimal and Guid (also 16 bytes) work correctly; only the three 128-bit-layout types are affected (see the sweep below).
Observed on Release; reproduces on both V8 and Firefox (engine-independent → codegen, not JS).
Affected vs. unaffected types (Release, node/V8)
Interface-dispatched EqualityComparer<T>.Default.Equals / IComparable<T>.CompareTo:
| Type | Size | Result |
|---|---|---|
int / long / double / TimeSpan / DateTime |
≤8 | ✅ correct |
decimal |
16 | ✅ correct |
Guid |
16 | ✅ correct |
Int128 |
16 | ❌ wrong |
UInt128 |
16 | ❌ wrong |
Decimal128 |
16 | ❌ wrong |
Repro
Standalone — a value-type equality/comparison that should trivially be true/0:
using System;
using System.Collections.Generic;
Int128 a = new Int128(0, 5);
Int128 b = new Int128(0, 5); // identical bits
// arg passed by value, direct -> CORRECT
Console.WriteLine($"a == b : {a == b}"); // True
Console.WriteLine($"a.Equals(b) : {a.Equals(b)}"); // True
Console.WriteLine($"a.CompareTo(b) : {a.CompareTo(b)}"); // 0
// arg is a boxed object pointer -> CORRECT
Console.WriteLine($"((object)a).Equals(b) : {((object)a).Equals(b)}"); // True
// arg passed by value THROUGH the interface stub -> WRONG
IEquatable<Int128> ia = a;
IComparable<Int128> ca = a;
Console.WriteLine($"((IEquatable<Int128>)a).Equals(b) : {ia.Equals(b)}"); // False (BUG)
Console.WriteLine($"EqualityComparer<Int128>.Default.Equals(a,b) : {EqualityComparer<Int128>.Default.Equals(a, b)}"); // False (BUG)
Console.WriteLine($"((IComparable<Int128>)a).CompareTo(b) : {ca.CompareTo(b)}"); // non-zero (BUG, should be 0)
Console.WriteLine($"((IComparable<Int128>)a).CompareTo(0) : {ca.CompareTo(default)}"); // wrong sign (BUG, should be > 0)CompareTo returns a constant value regardless of the argument (e.g. CompareTo(0) does not return a positive result), which shows the other parameter is arriving as garbage rather than the comparison logic being wrong. The corruption is also call-site dependent: in one run EqualityComparer<Int128>.Default.Equals(v, v) returned False while Equals(v, task.Result) returned True for the same value 0.
I reproduced this in src/mono/sample/wasm/console-node with <PublishReadyToRun>true</PublishReadyToRun>:
.\dotnet.cmd build /p:TargetOS=browser /p:TargetArchitecture=wasm /p:Configuration=Release /p:RuntimeFlavor=coreCLR /t:RunSample .\src\mono\sample\wasm\console-node\
and by running the real test classes directly against the Release runtime:
.\dotnet.cmd build /p:TargetOS=browser /p:TargetArchitecture=wasm /p:Configuration=Release /p:RuntimeFlavor=coreCLR /t:Test /p:Scenario=WasmTestOnV8 /p:XUnitClassName=System.Tests.Int128Tests_GenericMath .\src\libraries\System.Runtime\tests\System.Runtime.Tests\
→ ~every test in the class fails, including trivial ones like OneTest / AllBitsSetTest that only do Assert.Equal(constant, INumberBase<Int128>.One), because xUnit's Assert.Equal<T> goes through EqualityComparer<Int128>.Default → IEquatable<Int128>.Equals(Int128).
Root-cause hints
- The receiver (
this) unboxes fine —object.Equals(arg is a boxedobjectpointer) is correct. It is specifically the by-value 128-bit-layout argument that is corrupted when the call goes through an interface / constrained-generic dispatch stub (the unboxing thunk that adjuststhisand forwards the arguments to the unboxed entrypoint). decimal(fourintfields) andGuid(heterogeneous layout) are passed indirectly and are fine.Int128/UInt128/Decimal128share a homogeneous two-ulonglayout that RyuJIT/crossgen2 gives a special ABI class (register-pair / v128-like); the interface/unboxing stub appears to marshal that argument with the generic/indirect convention instead, so the argument slot is mismatched and corrupted. The fix likely lives in how the crossgen2 wasm unboxing/interface stub passes a value-type argument whose ABI class is the special 128-bit one.- Not JS-engine related (V8 + Firefox identical). Only observed with the Release framework R2R images; a Debug build that exercised only direct calls did not surface it.
DOTNET_ReadyToRun=0did not change the behavior when set as an env var, but the knob does not appear to be honored bybrowserhost, so this was not a clean A/B of R2R-vs-JIT. Worth confirming whether the corruption is in the crossgen2-emitted stub or the runtime wasm JIT stub.
Impact / provenance
- Discovered while triaging CI for [browser][coreCLR] browserhost to load R2R #129634 (browserhost to load R2R). Failing gate build: 1546573.
- Affected legs:
browser-wasm linux Release LibraryTestsCoreCLR_SmokeFirefoxV8(WasmTestOn{V8,Firefox}-CLR-ST-System.Runtime.Tests, exit code 71). - One defect covers multiple failing tests. Fixing the by-value 128-bit argument passing through interface stubs should clear:
- the
Int128Tests_GenericMath/UInt128Tests_GenericMathclusters (whole classes fail viaAssert.Equal<T>), TotalOrderIeee754ComparerTests.TotalOrderTestDecimal128(Decimal128compared viaIComparable), andSystem.Threading.Tasks.Tests.TaskRtTests.FromResult_KnownResults_Cached— this is the same bug, not aTask.FromResultcaching problem:AssertCached<T>()runsAssert.Same(FromResult, FromResult)(caching, passes) thenAssert.Equal(value, FromResult(value).Result)(→EqualityComparer<T>.Default), and it first fails atAssertCached<Int128>()/<UInt128>()wheredefault == 0produces the reportedAssert.Equal … Expected: 0 / Actual: 0.
- the
Note
This issue was researched and drafted with the assistance of GitHub Copilot.
Activity
dotnet-policy-service commentedon Aug 11, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
dotnet-policy-service commentedon Aug 11, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.
pavelsavara commentedon Aug 11, 2026
[-][browser][coreCLR][R2R] By-value 16-byte struct argument (Int128/UInt128/Decimal128) corrupted through interface dispatch under wasm R2R[/-][+][browser][coreCLR][R2R] By-value Int128/UInt128/Decimal128 argument corrupted through interface dispatch under wasm R2R[/+]pavelsavara commentedon Aug 11, 2026
Probably related #131492
AndyAyersMS commentedon Aug 12, 2026
Fixed by #131492