Skip to content

Wasm / R2R: codegen bringup test failures #128234

Closed

Description

@AndyAyersMS

11 tests fail in debug codegen bringup tests, when prejitted for Wasm:

.\build clr+libs -rc debug -c release -os browser
.\src\tests\build skipmanaged browser

;; put repo dotnet, nodejs in path

cd src\tests\Jit\CodeGenBringUpTests

dotnet msbuild /p:TargetArchitecture=wasm /p:TargetOS=browser /p:RuntimeFlavor=coreclr /p:BuildAsStandalone=true JIT.CodeGenBringUpTests_d.csproj

set CORE_ROOT=<REPO_ROOT>\artifacts\tests\coreclr\browser.wasm.debug\Tests\Core_Root
set RunCrossGen2=1

set DOTNET_ReadyToRunExcludeList=JIT.CodeGenBringUpTests_d;XUnitWrapperLibrary;ArrayExc_d;ArrayJagged_d;ArrayObj_d;div2_d;DivConst_d;Localloc_d;ModConst_d;RecursiveTailCall_d;Rotate_d;struct16args_d;StructFldAddr_d

<REPO_ROOT>\artifacts\tests\coreclr\browser.wasm.Debug\JIT\CodeGenBringUpTests\JIT.CodeGenBringUpTests_d\JIT.CodeGenBringUpTests_d.cmd

To repro, remove one of the trailing entries from the DOTNET_ReadyToRunExcludeList above...

Activity

  1. added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on May 14, 2026
  2. dotnet-policy-service commented on May 14, 2026

    @dotnet-policy-service
    Contributor

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

  3. AndyAyersMS commented on May 14, 2026

    @AndyAyersMS
    MemberAuthor

    Many of these require EH, so won't work for a while.

  4. dotnet-policy-service commented on May 14, 2026

    @dotnet-policy-service
    Contributor

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

  5. AndyAyersMS commented on May 14, 2026

    @AndyAyersMS
    MemberAuthor

    With the _r variants I also get failures with three more tests. These all seem to require EH as well.

    • CastThenBinop
    • UDivConst_r
    • UModConst_r
  6. removed
    untriagedNew issue has not been triaged by the area owner
    on May 15, 2026
  7. added this to the 11.0.0 milestone on May 15, 2026
  8. AndyAyersMS commented on Jun 3, 2026

    @AndyAyersMS
    MemberAuthor

    Need to retry this now that we've got more bits implemented.

  9. AndyAyersMS commented on Jun 4, 2026

    @AndyAyersMS
    MemberAuthor

    Note

    This comment was generated with Copilot CLI based on a rerun of the exact repro from the issue.

    Retested at main @ 3b0dd88d6cb (HEAD as of 2026-06-04). Rebuilt with the exact recipe from the issue (build.cmd clr+libs -rc debug -c release -os browser plus the test build) and reran JIT.CodeGenBringUpTests_{d,r}.cmd with RunCrossGen2=1. Because the test driver aborts on first failure, I enumerated failures by iteratively expanding DOTNET_ReadyToRunExcludeList.

    Now passing

    Test Likely fix
    ArrayJagged_{d,r}, ArrayObj_{d,r} #128225 (write barriers)
    Rotate_{d,r}, struct16args_{d,r}, StructFldAddr_{d,r} #128275 (R2R layout; tracked separately in #128231)

    Still failing

    Cluster 1 — same VM assert on first-pass EH dispatch (8 tests, 15 failures across _d/_r):

    ASSERT FAILED
        Expression: coldOffset < methodRegionInfo.coldSize
        Location:   src/coreclr/vm/codeman.cpp:6978
        Function:   GetCodeAddressForRelOffset
    …
       0) System.Runtime.ExceptionServices.InternalCalls::<RhpEHEnumNext>g____PInvoke|5_0
       1) System.Runtime.ExceptionServices.InternalCalls::RhpEHEnumNext
       2) System.Runtime.EH::FindFirstPassHandler
       3) System.Runtime.EH::DispatchEx
       4) System.Runtime.EH::RhThrowEx
    

    Hit by every test that throws out of R2R'd code:

    • ArrayExc_{d,r} (IndexOutOfRange)
    • div2_{d,r}, CastThenBinop (OverflowException)
    • DivConst_{d,r}, ModConst_{d,r} (DivideByZero)
    • Localloc_{d,r} (NullReference)
    • UDivConst_{d,r}, UModConst_{d,r} (OverflowException / DivideByZero)

    Note: UDivConst_d and UModConst_d were not listed in the original report — they were masked because Rotate_d aborted the runner before them. Now that #128275 fixed the Rotate/struct16args/StructFldAddr asserts, the runner gets further alphabetically and these surface. Not a regression.

    This looks like one underlying bug — the funclet/cold-region offset bookkeeping that feeds GetCodeAddressForRelOffset during EH dispatch. Worth filing as its own issue.

    Cluster 2 — wrong output (codegen):

    • RecursiveTailCall_{d,r}: prints each recursion level 4× instead of 1× (output shows 4 4 4 4 3 3 3 3 …); Expected 100, Actual 0. Looks like broken tail-call codegen, not EH. (see below)

    Score

    • Of the 11 originally-failing _d tests: 5 pass, 6 fail (with _d variants of UDivConst/UModConst newly exposed).
    • Of the 3 originally-failing _r tests (CastThenBinop, UDivConst_r, UModConst_r): all 3 still fail, same EH cluster.

    Suggest closing this issue and filing two new ones for clusters 1 and 2.

  10. AndyAyersMS commented on Jun 4, 2026

    @AndyAyersMS
    MemberAuthor

    RecursiveTailCall failure is in R2R-compiled shared‑generic code accessing typeof(T). Still digging in.

    Looks like the issue is that there are a few NYI helpers:

     extern "C" void STDCALL DelayLoad_Helper()      { PORTABILITY_ASSERT(...); }
     extern "C" void STDCALL DelayLoad_Helper_Obj()  { PORTABILITY_ASSERT(...); }
     extern "C" void STDCALL DelayLoad_Helper_ObjObj() { PORTABILITY_ASSERT(...); }
  11. AndyAyersMS commented on Jun 4, 2026

    @AndyAyersMS
    MemberAuthor

    Note

    This comment was AI-generated with GitHub Copilot under @AndyAyersMS's direction.

    Found a third, distinct failure while running the full pri0 wasm suite under R2R: a wasm-only crossgen2 JIT assert on async-in-try state machines. It surfaces in the tracing/eventpipe/debuginfo/tester test because the referenced Microsoft.Diagnostics.NETCore.Client.dll contains three such methods.

    Repro (same shape as the original):

    .\build clr+libs -rc debug -c release -os browser
    .\src\tests\build skipmanaged browser
    
    ;; put repo dotnet in path
    
    cd src\tests\JIT\Directed\debugging\debuginfo\tester
    
    dotnet msbuild /p:TargetArchitecture=wasm /p:TargetOS=browser /p:RuntimeFlavor=coreclr /p:BuildAsStandalone=true tester.csproj
    
    set CORE_ROOT=<REPO_ROOT>\artifacts\tests\coreclr\browser.wasm.debug\Tests\Core_Root
    set TESTDIR=<REPO_ROOT>\artifacts\tests\coreclr\browser.wasm.Debug\JIT\Directed\debugging\debuginfo\tester
    
    "%CORE_ROOT%\crossgen2\crossgen2.exe" ^
      --targetarch:wasm --targetos:browser ^
      -O -f:wasm --verify-type-and-field-layout --method-layout:random ^
      -o:"%TEMP%\Microsoft.Diagnostics.NETCore.Client.wasm" ^
      "%TESTDIR%\IL-CG2\Microsoft.Diagnostics.NETCore.Client.dll" ^
      -r:"%CORE_ROOT%\System.*.dll" ^
      -r:"%CORE_ROOT%\Microsoft.*.dll" ^
      -r:"%CORE_ROOT%\xunit.*.dll" ^
      -r:"%CORE_ROOT%\mscorlib.dll" ^
      -r:"%CORE_ROOT%\netstandard.dll" ^
      -r:"%TESTDIR%\IL-CG2\*.dll"
    

    Expected output (crossgen2 exits with 0x80000003, asserting three times):

    src\coreclr\jit\flowgraph.cpp:7811
    Assertion failed '!"Unexpected try region entry edge"' in
      'Microsoft.Diagnostics.NETCore.Client.DiagnosticsServerRouterFactory+<IsStreamConnectedAsync>d__18:MoveNext():this'
      during 'Wasm transform sccs' (IL size 267; hash 0xf9348f7c; FullOpts)
    
    ...same assert in TcpClientRouterFactory+<ConnectTcpStreamAsyncInternal>d__20:MoveNext() (IL 1061; 0xd13c6e23)
    ...same assert in IpcClientRouterFactory+<ConnectIpcStreamAsync>d__13:MoveNext()           (IL 1270; 0x674f2b84)
    

    All three are async state machines with try { await ... } catch { }. Minimal C# pattern (from IsStreamConnectedAsync):

    protected async Task IsStreamConnectedAsync(Stream stream, CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            if (!IsStreamConnected(stream, token))
                throw new EndOfStreamException();
            try
            {
                await Task.Delay(IsStreamConnectedTimeoutMs, token).ConfigureAwait(false);
            }
            catch { }
        }
    }

    The compiler-generated MoveNext has the async-resume path enter the inner try from the state-machine dispatcher (the resume switch jumps directly into the try body). FlowGraphTryRegions::Build (src/coreclr/jit/flowgraph.cpp:7787-7811) only accepts try-entry edges from (a) the ebdTryBeg block or (b) a predecessor flagged BBF_ASYNC_RESUMPTION / BBF_CATCH_RESUMPTION. The dispatcher → try edge doesn't have either flag at this point in the wasm pipeline, so it asserts. The phase is wasm-only (#ifdef TARGET_WASM in compiler.cpp:5016-5028), so the assert doesn't reproduce on other targets.

    This pattern is common enough in real code that it's likely behind a substantial fraction of the broader pri0 R2R regressions; out of 638 standalone pri0 wrappers retried under RunCrossGen2=1, 58 (~9%) failed before the test ran at all because crossgen2 itself aborted, and most of those traced back to this assert (the rest were environment-variable leakage, not JIT bugs).


    try the above with

    --singlemethodtypename "Microsoft.Diagnostics.NETCore.Client.DiagnosticsServerRouterFactory+<IsStreamConnectedAsync>d__18,Microsoft.Diagnostics.NETCore.Client" --singlemethodname "MoveNext" --singlemethodindex 1
    
  12. AndyAyersMS commented on Jun 5, 2026

    @AndyAyersMS
    MemberAuthor

    Cluster 1 — same VM assert on first-pass EH dispatch (8 tests, 15 failures across _d/_r):

    This is a known todo -- @davidwrighton this should be fixable now, right?

     UINT32 codeLength = gcInfoDecoder.GetCodeLength();
     #ifdef TARGET_WASM
         codeLength = 1; // WASM-TODO, something isn't working in the GCEncoder/Decoder for WASM,
                         // and we're getting back 0 for code length. We'll need to fix this at some point.
     #endif
    
  13. 3 remaining items

  14. AndyAyersMS commented on Jun 5, 2026

    @AndyAyersMS
    MemberAuthor

    Note

    AI/Copilot-assisted investigation.

    Directed_1 wrapper: a new R2R wasm GCInfo bug (silent bad codegen)

    Followup to my prior comment on the cg2 asserts. The Directed_1 wrapper failure listed there ("0 tests ran, immediate exit 1, no captured output") is not a wrapper-script issue, not broken on Directed_1.wasm, and not caught by crossgen2 — it's silent bad codegen in XUnitWrapperLibrary.wasm that explodes at runtime.

    Reproducible isolation

    Running Directed_1.cmd after manipulating which .wasm sidecars are present (everything else identical):

    Wasm files present in JIT/Directed/Directed_1/ Result
    All 76 (default) exit 1, 0 tests run, no output
    Only Directed_1.wasm (move XUnitWrapperLibrary.wasm aside) exit 0, 72/72 pass
    Only XUnitWrapperLibrary.wasm (move Directed_1.wasm aside) exit 1, 0 tests run
    All 76 + DOTNET_ReadyToRun=0 exit 0, 72/72 pass
    None (interpreter fallback) secondary interpreter assert in TestSummary.ReportStartingTest

    So Directed_1.wasm is fine; XUnitWrapperLibrary.wasm is the broken R2R image.

    Failure signature

    With DOTNET_LogToConsole=1 DOTNET_LogLevel=10 DOTNET_LogFacility=0xffffffff, the relevant tail:

    ZAP: Using R2R precompiled code for XUnitWrapperLibrary.TestFilter..ctor sig="(string[])" (token 6000003)
    ... ML::FM Considering System.Environment::GetEnvironmentVariable ...
       Argument at 004fd5cc causes promotion of object 00000000
       Relocating from 00000000 to 00000001
    In PreStubWorker for System.Environment::GetEnvironmentVariable
    DoPrestub: method 0x47d64c8
    { Jitting method (0x47d64c8) System.Environment::GetEnvironmentVariable string *(string)
       System.Environment::GetEnvironmentVariable string *(string) EE implemented
    ASSERT: src/coreclr/vm/threads.h:4982
    FAILED ASSERT ... Early return from JIT/EE interface method
    RUNNING EXE: /
    Aborted(native code called abort())
    

    Interpretation:

    • R2R code for XUnitWrapperLibrary.TestFilter..ctor(string[]) (token 0x06000003) is entered.
    • It calls Environment.GetEnvironmentVariable("TEST_HARNESS_STRIPE_TO_EXECUTE") (TestFilter.cs:145).
    • During the prestub/JIT of the callee, the GC scans the caller's frame. A stack slot at 0x004fd5cc containing 0 is misreported as a managed pointer (Argument ... causes promotion of object 00000000); the GC dutifully "relocates" 0 → 1, then bails out and the runtime asserts.

    This is a GCInfo emission bug in R2R wasm codegen for this method (or for its call site to GetEnvironmentVariable). Crossgen2 emits the image cleanly (exit 0, no warnings); the bug only manifests when the runtime tries to GC-scan the frame.

    Why only Directed_1 is affected in our matrix

    Of the wrappers I’ve been re-verifying, only Directed_1 has a standalone XUnitWrapperLibrary.wasm artifact alongside its other per-test *.wasm sidecars. The merged-style wrappers (Methodical_d2, Regression_2, Regression_3, Generics_1, JIT.SIMD) embed XUnitWrapperLibrary into the big single test DLL and the build did not emit a separate XUnitWrapperLibrary.wasm for them. So they avoid this code path entirely.

    (Cross-checked with Test-Path XUnitWrapperLibrary.wasm in each wrapper dir; only Directed_1 has it.)

    Status / scope

    This is distinct from the 4 cg2 compile-time asserts in my previous comment — those are crossgen2 crashes; this one is runtime silent bad codegen that survives cg2 with no warnings. It's specific to the thin-shell wrapper model (Directed_1, and likely any other wrapper that emits a standalone XUnitWrapperLibrary.wasm).

    cc @AndyAyersMS @kg

  15. AndyAyersMS commented on Jun 6, 2026

    @AndyAyersMS
    MemberAuthor

    RecursiveTailCall failure is in R2R-compiled shared‑generic code accessing typeof(T). Still digging in.

    Looks like the issue is that there are a few NYI helpers:

    extern "C" void STDCALL DelayLoad_Helper() { PORTABILITY_ASSERT(...); }
    extern "C" void STDCALL DelayLoad_Helper_Obj() { PORTABILITY_ASSERT(...); }
    extern "C" void STDCALL DelayLoad_Helper_ObjObj() { PORTABILITY_ASSERT(...); }

    Suspect the jit is still asking for dynamic helpers in some cases. Digging in deeper.

  16. AndyAyersMS commented on Jun 6, 2026

    @AndyAyersMS
    MemberAuthor

    Note

    This comment was generated with AI assistance.

    TL;DR

    When wasm-R2R'd code calls a non-virtual instance method (via IL callvirt) on a type whose defining assembly is outside the version bubble (e.g. passed only as -r: and not R2R-compiled), the JIT correctly falls back to the CORINFO_HELP_VIRTUAL_FUNC_PTR helper sequence. On other targets the helper returns the target method's real entry point, which can be called directly. On wasm, when the target lives in the interpreter (not R2R), the helper hands back a portable-entrypoint pseudo-handle that is not a valid wasm function-table index, so the subsequent call_indirect corrupts state and trips MethodTable::SanityCheck shortly after.

    The accumulator commit "[Wasm R2R] Bypass dynamic LDVIRTFTN helper" is a necessary precursor (it routes wasm down CORINFO_HELP_VIRTUAL_FUNC_PTR instead of the dynamic-helper variant), but it does not fix the underlying interp-target problem on the resolved-funcptr call.

    Update: seems like this may also be EH related, so will focus on that for now.


    Minimal reproduction

    Crashes:

    crossgen2.exe -r:XUnitWrapperLibrary.dll -r:ArrayExc_d.dll \
      --targetos:browser --targetarch:wasm \
      -o JIT.CodeGenBringUpTests_d.wasm JIT.CodeGenBringUpTests_d.dll

    Does not crash (same merged xunit harness, no per-test ref):

    crossgen2.exe -r:XUnitWrapperLibrary.dll \
      --targetos:browser --targetarch:wasm \
      -o JIT.CodeGenBringUpTests_d.wasm JIT.CodeGenBringUpTests_d.dll

    r2rdump diff of the two outputs shows the only added R2R method is Program.<<Main>$>g__TestExecutor11|0_12 (the per-test TestExecutor for ArrayExc_d). With -r:ArrayExc_d.dll present, the call graph rooted at TestExecutor11 becomes fully resolvable, so crossgen2 R2R-compiles it; without that ref, the unresolved BringUpTest_ArrayExc.TestEntryPoint call causes the method to be skipped.

    Once R2R-compiled, TestExecutor11's very first cross-module call into XW (TestFilter.ShouldRunTest) crashes — regardless of which --filter the harness is run with, because all TestExecutors are dispatched from Main in sequence and the filter check is inside each TestExecutor.

    The CI failure is reproduced by the existing CI command lines because CLRTest.CrossGen.targets passes -r:$PWD/IL-CG2/*.dll when R2R-compiling merged xunit test assemblies, which pulls every per-test dll into the reference set and therefore R2R-compiles every TestExecutor.

    Disassembly evidence

    TestExecutor11's TestFilter.ShouldRunTest call site (r2rdump excerpt):

    i32.const 113572 // XUnitWrapperLibrary.TestFilter (TYPE_HANDLE)
    i32.const 113508 // TestFilter.ShouldRunTest (METHOD_HANDLE)
    i32.const 112752 // VIRTUAL_FUNC_PTR (HELPER)
    ... call_indirect (type 9)   // resolve
    local.set 5
    ...
    local.get 41
    i32.const 1023
    i32.le_u
    br_if 2                       // bounds check: skip slow path for funcidx <= 1023
    call_indirect (type 10)       // invoke

    TestFilter.ShouldRunTest is a non-virtual public instance method on a non-sealed class. The C# compiler emits callvirt; the JIT cannot devirtualize because of the cross-version-bubble servicing rule in CorInfoImpl.ReadyToRun.cs:2204-2220 (the non-virtual could become virtual in a serviced XW). The wasm branch at line 2381 then forces CORINFO_VIRTUALCALL_LDVIRTFTN. None of this is incorrect on its own; the failure is in what the runtime helper hands back when the target is interpreter-only.

    Suggested fix direction

    Either:

    1. Runtime helper: CORINFO_HELP_VIRTUAL_FUNC_PTR on wasm must always return something directly call_indirect-able for the wasm signature in use — wrapping interpreter targets in the same R2R→interp thunk machinery already used for direct cross-module calls (WasmR2RToInterpreterThunkNode / READYTORUN_FIXUP_InjectStringThunks).
    2. Codegen: detect the portable-entrypoint return value from the helper and dispatch via the interpreter trampoline instead of a raw call_indirect.

    Option 1 keeps the JIT contract intact and is probably the cleaner fix.

    Other accumulated wasm-R2R findings

    Branched at AndyAyersMS/runtime:wasm-fixes-accumulator:

    • 5ce568c3e4c [Wasm RyuJit] Match how other targets consume initblk fill values
    • 4988fc41b13 JIT/wasm: implement CKFINITE
    • 14418220f27 [Wasm RyuJit] Zero init gc shadow stack slots in the prolog
    • a5a9eb3eacd [Wasm R2R] Allow LDVIRTFTN call sigs to carry a type arg (MD-array methods)
    • 662c45522c4 [Wasm R2R] Bypass dynamic LDVIRTFTN helper

    Cc @davidwrighton @kg @lambdageek

  17. AndyAyersMS commented on Jun 7, 2026

    @AndyAyersMS
    MemberAuthor

    Note

    AI-generated content. Reviewed by @AndyAyersMS.

    New wasm R2R bug surfaced during JIT/opt sweep — JIT/opt/Compares/compares

    EqualsOrGreaterThan_MaxValue_LHSConst_SideEffects(null) (test source: src/tests/JIT/opt/Compares/compares.cs:75) does not throw NullReferenceException under wasm R2R.

    Minimal repro:

    public class C { public byte B; }
    
    [MethodImpl(MethodImplOptions.NoInlining)]
    static bool Cmp(C c) => c.B <= 255;   // also reproduces with `c.B == 0`
    
    // Cmp(null) under wasm R2R returns without throwing.

    Root cause: genCodeForIndir in src/coreclr/jit/codegenwasm.cpp (line 2501) does not emit an explicit null check for GT_IND. Compare with genCodeForStoreInd (line 2523), which does check GTF_IND_NONFAULTING and call genEmitNullCheck. The wasm JIT relies on the load itself trapping (x86-style implicit null check), but loads from low offsets in wasm linear memory succeed silently.

    The JIT IR is otherwise correct — IND retains GTF_EXCEPT, value-numbering tracks NullPtrExc, and a throw helper block is allocated (Adding throw helper BB04 for ACD0 SCK_NULL_CHECK) — but no branch to it is ever emitted.

    Generated wasm (excerpt):

    i32.load 0 8        ;; load arg c
    i32.const 4
    i32.add             ;; c + 4 (field B offset)
    i32.load8_u 0 0     ;; <-- load with no null check
    i32.const 255
    i32.le_s
    return
    

    x64 JIT/R2R correctly throws NRE because cmp byte ptr [rcx+0x08], 255 faults on the unmapped null page.

    Fix direction: either extend genCodeForIndir to mirror genCodeForStoreInd's null-check emission, or have the wasm runtime protect the first maxUncheckedOffsetForNullObject bytes of linear memory.

    Workaround: added compares to DOTNET_ReadyToRunExcludeList in the test driver.

  18. AndyAyersMS commented on Jun 7, 2026

    @AndyAyersMS
    MemberAuthor

    Note

    AI-generated content. Reviewed by @AndyAyersMS.

    New wasm R2R bug surfaced — JIT/Methodical/int64/unsigned/addsub_il_r (release-opt only, debug build passes)

    add.ovf.un / sub.ovf.un (unsigned overflow add/sub) use the signed overflow detection algorithm in the wasm JIT codegen. This misses unsigned overflow when the two operands have different high bits.

    Root cause: genCodeForBinaryOverflow in src/coreclr/jit/codegenwasm.cpp (line 1474). For GT_ADD and GT_SUB, the emitted sequence is the signed XOR-sign check:

    i64.add
    local.set <c>
    local.get <a>; local.get <b>; i64.xor; i64.const 0; i64.ge_s   ;; same sign?
    if
      local.get <c>; local.get <a>; i64.xor; i64.const 0; i64.lt_s ;; sign changed?
      br_if throwOvf
    end
    

    The outer i64.ge_s test passes only when operands have the same sign. When signs differ, the inner overflow check is skipped — but unsigned overflow can still occur in that case.

    Note that GT_MUL in the same function correctly checks treeNode->IsUnsigned() (line 1568) and chooses signed vs unsigned codegen accordingly. GT_ADD and GT_SUB do not.

    Minimal repro:

    [MethodImpl(MethodImplOptions.NoInlining)]
    static ulong CheckedAdd(ulong a, ulong b) => checked(a + b);
    
    // Both operands together overflow uint64, but have different high bits.
    CheckedAdd(0xf34b743eeff8e5c6, 0x4f4067f1a0e404ad);
    // Expected: OverflowException
    // Actual (wasm R2R): returns 0x428bdc3090dcea73 (wrapped, no exception)

    Test impact: Only 2 of 256 testNumbers(a, b) pairs in addsub.cs trigger this (#254 and #256). Both have one operand with the high bit set and the other clear, and their unsigned sum overflows 2^64.

    Fix direction: For GT_ADD/GT_SUB with IsUnsigned(), emit the simple unsigned-overflow check: if (c < op1) throw. (For sub, the underflow check is if (op1 < op2) throw, evaluated before the subtract.)

    Workaround: addsub_il_r is in DOTNET_ReadyToRunExcludeList in the test driver.

  19. AndyAyersMS commented on Jun 7, 2026

    @AndyAyersMS
    MemberAuthor

    Note

    AI-generated content by the GitHub Copilot CLI

    Continued sweep of JIT/Regression/JitBlue standalone tests under wasm R2R turned up two NEW R2R-specific bug clusters distinct from the EH and tail-call issues already on the master list. Both reproduce only with R2R enabled; flipping the assembly off in DOTNET_ReadyToRunExcludeList makes them pass.

    1. CEA cloning miscompile of collection expressions

    Test: JIT/Regression/JitBlue/Runtime_127075 — uses Roslyn-lowered [.. head, .. s_tail] (collection expression with two enumerator scopes for IReadOnlyCollection<I>).

    Asserts in the loop body don't match head[i] because the two enumerators share a stack-allocated slot and the second loop iterates the first enumerator's _array/_endIndex. This is the exact repro that b3db78f4423 ("JIT: Fix CEA cloning when enumerator local is reassigned from an untracked source", #127079) was added to cover — the test was committed alongside the fix. The fix in ObjectAllocator::CheckForGuardedAllocationOrCopy is in the JIT and is in tree; it just doesn't fire on the wasm R2R compilation path, so we get the unfixed behavior here. Plausibly a tracked-vs-untracked-locals interaction (per the recent "[Wasm R2R] Report tracked GC vars as untracked in GC info" change) or a slight CEA pipeline divergence in the wasm cross-JIT.

    2. R2R hands a corrupt MethodTable to the interpreter

    Tests: Runtime_53549, Runtime_65694_2 — both abort in stderr with:

    ASSERT FAILED
            Expression: SanityCheck()
            Location:   src/coreclr/vm/methodtable.cpp:8659
            Function:   Validate
    

    MethodTable::SanityCheck() (methodtable.cpp:6331) fails the "non-generic non-array implies pCanonMT == this" invariant. Stacks differ:

    • Runtime_53549 — R2R'd call site → VirtualDispatchHelpers::VirtualFunctionPointerSlow → ResolveVirtualFunctionPointer → assert. Source is a[i].ToString() on A[] { B, B, ... } where A.ToString is virtual.
    • Runtime_65694_2 — R2R'd call site → prestubbed GC.Collect → _Collect PInvoke marshaling → assert. Both have InlinedCallFrame (R2R origin) above the InterpreterFrame (callee), so the bad MT is being marshalled from R2R'd code into the helper / PInvoke transition.

    This looks like an R2R fixup producing a bogus type handle for the virtual-call / PInvoke transition, not the EH or tail-call clusters. Worth a JIT/runtime owner taking a look.

    Excluded a few more in the sweep workaround

    Added to DOTNET_ReadyToRunExcludeList: Runtime_127075, Runtime_53549, Runtime_65694_2, b441487, DevDiv_362706, DevDiv_736188, Runtime_101046 (silent R2R failures — to triage), plus a batch of try/catch-style tests that fall under the existing EH cluster, plus three Thread.Start tests that hit the pre-existing wasm no-multithreading gap (b113493, b143840, b426654).

  20. AndyAyersMS commented on Jun 7, 2026

    @AndyAyersMS
    MemberAuthor

    Note

    This comment was generated by GitHub Copilot.

    Triage of the 4 silent R2R failures (exit 1, no stderr)

    I verified each by running with DOTNET_ReadyToRunExcludeList=<name> to confirm whether the failure is R2R-specific, then inspected the test source.

    Test R2R-specific? Source has try/catch? Classification
    b441487 ✅ yes ✅ yes (4× .try/catch NRE) EH cluster
    DevDiv_362706 ✅ yes ✅ yes (.try/catch Exception wrapping a Fuzzlyn fragment) EH cluster
    DevDiv_736188 ✅ yes ❌ no NEW: R2R + JitStressRegs=0x204 silent abort
    Runtime_101046 ❌ no ❌ no Pre-existing wasm/mono interop bug #106393 ([ActiveIssue] for TestRuntimes.Mono); test runner does not honor [ActiveIssue]

    Notes:

    • b441487 and DevDiv_362706 fold into the existing EH-under-R2R cluster ("Wasm EH with catches/finallys/filters in R2R methods does not work yet").
    • DevDiv_736188 is the only genuinely new finding here: the test sets DOTNET_JitStressRegs=0x204 via CLRTestEnvironmentVariable and the cross-JIT silently produces broken wasm under that stress combination. Low priority (stress-only).
    • Runtime_101046 already exits 101 without R2R because the C P/Invoke target apparently doesn't sign-extend short -> int correctly on wasm; with R2R the failure escalates to a silent abort.

    Exclude-list entries updated accordingly. With these triaged, the only R2R bugs still outstanding without a PR/fix are:

    • compares (root-caused, JIT codegen — genCodeForIndir null check)
    • addsub_il_r (root-caused, JIT codegen — genCodeForBinaryOverflow signed vs. unsigned)
    • Runtime_127075 (CEA cloning miscompile, R2R-only path)
    • Runtime_53549 / Runtime_65694_2 (corrupt MethodTable marshalled out of R2R'd code)
    • DevDiv_736188 (R2R + JitStressRegs stress, this comment)
  21. AndyAyersMS commented on Jun 7, 2026

    @AndyAyersMS
    MemberAuthor

    Note

    This comment was generated by GitHub Copilot.

    Correction on DevDiv_736188: the DOTNET_JitStressRegs=0x204 env var set by the test's csproj is a red herring on wasm — it only affects the in-process JIT, and (as you noted) wasm doesn't do meaningful register allocation. Re-running with the stress flag stripped still reproduces the silent abort, so the trigger is just R2R.

    What the test actually does: CallWith2Args<string[]>(ref args, null) -> shared-generic CallWith3Args<__Canon>(ref field, ref InternalSyncObject, initializer) returning default(T). The .r2rdump shows crossgen2 emits both the string[] instantiation (with METHOD_DICTIONARY) and the __Canon form (with MethodDictionaryLookup helper). So the new R2R bug is in the shared-generic dictionary lookup / byref ref-type path under wasm, not in stress codegen.

    Updated the entry in my exclude list accordingly. Still counts as a NEW R2R bug but with a different (and more likely real-world relevant) shape than I first wrote.

  22. AndyAyersMS commented on Jun 10, 2026

    @AndyAyersMS
    MemberAuthor

    Codegen bringup is now looking reasonably solid, so I am going to close this and track the remaining problems with individual issues.

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

Metadata

Metadata

Assignees

Labels

arch-wasmWebAssembly architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions