Skip to content

[browser][coreCLR][R2R] By-value Int128/UInt128/Decimal128 argument corrupted through interface dispatch under wasm R2R #132137

Closed

Description

@pavelsavara

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 boxed object pointer) 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 adjusts this and forwards the arguments to the unboxed entrypoint).
  • decimal (four int fields) and Guid (heterogeneous layout) are passed indirectly and are fine. Int128/UInt128/Decimal128 share a homogeneous two-ulong layout 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=0 did not change the behavior when set as an env var, but the knob does not appear to be honored by browserhost, 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_GenericMath clusters (whole classes fail via Assert.Equal<T>),
    • TotalOrderIeee754ComparerTests.TotalOrderTestDecimal128 (Decimal128 compared via IComparable), and
    • System.Threading.Tasks.Tests.TaskRtTests.FromResult_KnownResults_Cached — this is the same bug, not a Task.FromResult caching problem: AssertCached<T>() runs Assert.Same(FromResult, FromResult) (caching, passes) then Assert.Equal(value, FromResult(value).Result) (→ EqualityComparer<T>.Default), and it first fails at AssertCached<Int128>()/<UInt128>() where default == 0 produces the reported Assert.Equal … Expected: 0 / Actual: 0.

Note

This issue was researched and drafted with the assistance of GitHub Copilot.

Activity

  1. added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Aug 11, 2026
  2. added
    arch-wasmWebAssembly architecture
    os-browserBrowser variant of arch-wasm
    and removed
    arch-wasmWebAssembly architecture
    os-browserBrowser variant of arch-wasm
    on Aug 11, 2026
  3. dotnet-policy-service commented on Aug 11, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
    See info in area-owners.md if you want to be subscribed.

  4. dotnet-policy-service commented on Aug 11, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
    See info in area-owners.md if you want to be subscribed.

  5. pavelsavara commented on Aug 11, 2026

    @pavelsavara
    Author
  6. changed the title [-][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[/+] on Aug 11, 2026
  7. added this to the 12.0.0 milestone on Aug 11, 2026
  8. removed
    untriagedNew issue has not been triaged by the area owner
    on Aug 11, 2026
  9. pavelsavara commented on Aug 11, 2026

    @pavelsavara
    MemberAuthor

    Probably related #131492

  10. AndyAyersMS commented on Aug 12, 2026

    @AndyAyersMS
    Member

    Fixed by #131492

  11. locked and limited conversation to collaborators on Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    arch-wasmWebAssembly architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIos-browserBrowser variant of arch-wasm

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions