You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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.
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):
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).
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
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).
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.
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):
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:
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).
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.
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:
publicclassC{publicbyteB;}[MethodImpl(MethodImplOptions.NoInlining)]staticboolCmp(Cc)=>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.
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)]staticulongCheckedAdd(ulonga,ulongb)=>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.
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:
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).
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:
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.
Activity
dotnet-policy-service commentedon May 14, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.
AndyAyersMS commentedon May 14, 2026
Many of these require EH, so won't work for a while.
ArrayExcrequires handling a bounds check exceptionArrayJaggedwrite barrier?ArrayObjwrite barrier?div2divide by zeroDivConstdivide by zeroLocallocnull reference exceptionModConstdivide by zeroRecursiveTailCallRotate-- vm assert (see Wasm / R2R: Type System.DateTime: expected alignment 0x00000004, actual 0x00000008 #128231)struct16args-- vm assert (see Wasm / R2R: Type System.DateTime: expected alignment 0x00000004, actual 0x00000008 #128231)StructFldAddr-- vm assert (see Wasm / R2R: Type System.DateTime: expected alignment 0x00000004, actual 0x00000008 #128231)dotnet-policy-service commentedon May 14, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
AndyAyersMS commentedon May 14, 2026
With the
_rvariants I also get failures with three more tests. These all seem to require EH as well.CastThenBinopUDivConst_rUModConst_rAndyAyersMS commentedon Jun 3, 2026
Need to retry this now that we've got more bits implemented.
AndyAyersMS commentedon Jun 4, 2026
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 browserplus the test build) and reranJIT.CodeGenBringUpTests_{d,r}.cmdwithRunCrossGen2=1. Because the test driver aborts on first failure, I enumerated failures by iteratively expandingDOTNET_ReadyToRunExcludeList.Now passing
Still failing
Cluster 1 — same VM assert on first-pass EH dispatch (8 tests, 15 failures across
_d/_r):Hit by every test that throws out of R2R'd code:
Note:
UDivConst_dandUModConst_dwere not listed in the original report — they were masked becauseRotate_daborted 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
GetCodeAddressForRelOffsetduring EH dispatch. Worth filing as its own issue.Cluster 2 — wrong output (codegen):
4 4 4 4 3 3 3 3 …);Expected 100, Actual 0.Looks like broken tail-call codegen, not EH.(see below)Score
_dtests: 5 pass, 6 fail (with_dvariants of UDivConst/UModConst newly exposed)._rtests (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.
AndyAyersMS commentedon Jun 4, 2026
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:
AndyAyersMS commentedon Jun 4, 2026
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/testertest because the referencedMicrosoft.Diagnostics.NETCore.Client.dllcontains three such methods.Repro (same shape as the original):
Expected output (crossgen2 exits with
0x80000003, asserting three times):All three are async state machines with
try { await ... } catch { }. Minimal C# pattern (fromIsStreamConnectedAsync):The compiler-generated
MoveNexthas the async-resume path enter the innertryfrom 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) theebdTryBegblock or (b) a predecessor flaggedBBF_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_WASMincompiler.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
AndyAyersMS commentedon Jun 5, 2026
This is a known todo -- @davidwrighton this should be fixable now, right?
3 remaining items
AndyAyersMS commentedon Jun 5, 2026
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 inXUnitWrapperLibrary.wasmthat explodes at runtime.Reproducible isolation
Running
Directed_1.cmdafter manipulating which.wasmsidecars are present (everything else identical):JIT/Directed/Directed_1/Directed_1.wasm(moveXUnitWrapperLibrary.wasmaside)XUnitWrapperLibrary.wasm(moveDirected_1.wasmaside)DOTNET_ReadyToRun=0TestSummary.ReportStartingTestSo
Directed_1.wasmis fine;XUnitWrapperLibrary.wasmis the broken R2R image.Failure signature
With
DOTNET_LogToConsole=1 DOTNET_LogLevel=10 DOTNET_LogFacility=0xffffffff, the relevant tail:Interpretation:
XUnitWrapperLibrary.TestFilter..ctor(string[])(token0x06000003) is entered.Environment.GetEnvironmentVariable("TEST_HARNESS_STRIPE_TO_EXECUTE")(TestFilter.cs:145).0x004fd5cccontaining0is 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_1is affected in our matrixOf the wrappers I’ve been re-verifying, only
Directed_1has a standaloneXUnitWrapperLibrary.wasmartifact alongside its other per-test*.wasmsidecars. The merged-style wrappers (Methodical_d2,Regression_2,Regression_3,Generics_1,JIT.SIMD) embedXUnitWrapperLibraryinto the big single test DLL and the build did not emit a separateXUnitWrapperLibrary.wasmfor them. So they avoid this code path entirely.(Cross-checked with
Test-Path XUnitWrapperLibrary.wasmin each wrapper dir; onlyDirected_1has 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
[Wasm / RyuJit]: don't mark CORINFO_HELP_GETREFANY pure on wasm (#129060
JIT/wasm: place SCC dispatcher inside Wasm try region when needed (#1…
AndyAyersMS commentedon Jun 6, 2026
Suspect the jit is still asking for dynamic helpers in some cases. Digging in deeper.
AndyAyersMS commentedon Jun 6, 2026
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 theCORINFO_HELP_VIRTUAL_FUNC_PTRhelper 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 subsequentcall_indirectcorrupts state and tripsMethodTable::SanityCheckshortly after.The accumulator commit "[Wasm R2R] Bypass dynamic LDVIRTFTN helper" is a necessary precursor (it routes wasm down
CORINFO_HELP_VIRTUAL_FUNC_PTRinstead 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:
Does not crash (same merged xunit harness, no per-test ref):
r2rdumpdiff of the two outputs shows the only added R2R method isProgram.<<Main>$>g__TestExecutor11|0_12(the per-test TestExecutor forArrayExc_d). With-r:ArrayExc_d.dllpresent, the call graph rooted at TestExecutor11 becomes fully resolvable, so crossgen2 R2R-compiles it; without that ref, the unresolvedBringUpTest_ArrayExc.TestEntryPointcall causes the method to be skipped.Once R2R-compiled, TestExecutor11's very first cross-module call into XW (
TestFilter.ShouldRunTest) crashes — regardless of which--filterthe harness is run with, because all TestExecutors are dispatched fromMainin sequence and the filter check is inside each TestExecutor.The CI failure is reproduced by the existing CI command lines because
CLRTest.CrossGen.targetspasses-r:$PWD/IL-CG2/*.dllwhen 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.ShouldRunTestcall site (r2rdumpexcerpt):TestFilter.ShouldRunTestis a non-virtual public instance method on a non-sealed class. The C# compiler emitscallvirt; the JIT cannot devirtualize because of the cross-version-bubble servicing rule inCorInfoImpl.ReadyToRun.cs:2204-2220(the non-virtual could become virtual in a serviced XW). The wasm branch at line 2381 then forcesCORINFO_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:
CORINFO_HELP_VIRTUAL_FUNC_PTRon wasm must always return something directlycall_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).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 values4988fc41b13JIT/wasm: implement CKFINITE14418220f27[Wasm RyuJit] Zero init gc shadow stack slots in the prologa5a9eb3eacd[Wasm R2R] Allow LDVIRTFTN call sigs to carry a type arg (MD-array methods)662c45522c4[Wasm R2R] Bypass dynamic LDVIRTFTN helperCc @davidwrighton @kg @lambdageek
AndyAyersMS commentedon Jun 7, 2026
Note
AI-generated content. Reviewed by @AndyAyersMS.
New wasm R2R bug surfaced during JIT/opt sweep —
JIT/opt/Compares/comparesEqualsOrGreaterThan_MaxValue_LHSConst_SideEffects(null)(test source:src/tests/JIT/opt/Compares/compares.cs:75) does not throwNullReferenceExceptionunder wasm R2R.Minimal repro:
Root cause:
genCodeForIndirinsrc/coreclr/jit/codegenwasm.cpp(line 2501) does not emit an explicit null check forGT_IND. Compare withgenCodeForStoreInd(line 2523), which does checkGTF_IND_NONFAULTINGand callgenEmitNullCheck. 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 —
INDretainsGTF_EXCEPT, value-numbering tracksNullPtrExc, 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):
x64 JIT/R2R correctly throws NRE because
cmp byte ptr [rcx+0x08], 255faults on the unmapped null page.Fix direction: either extend
genCodeForIndirto mirrorgenCodeForStoreInd's null-check emission, or have the wasm runtime protect the firstmaxUncheckedOffsetForNullObjectbytes of linear memory.Workaround: added
comparestoDOTNET_ReadyToRunExcludeListin the test driver.AndyAyersMS commentedon Jun 7, 2026
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:
genCodeForBinaryOverflowinsrc/coreclr/jit/codegenwasm.cpp(line 1474). ForGT_ADDandGT_SUB, the emitted sequence is the signed XOR-sign check:The outer
i64.ge_stest 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_MULin the same function correctly checkstreeNode->IsUnsigned()(line 1568) and chooses signed vs unsigned codegen accordingly.GT_ADDandGT_SUBdo not.Minimal repro:
Test impact: Only 2 of 256
testNumbers(a, b)pairs inaddsub.cstrigger 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_SUBwithIsUnsigned(), emit the simple unsigned-overflow check:if (c < op1) throw. (For sub, the underflow check isif (op1 < op2) throw, evaluated before the subtract.)Workaround:
addsub_il_ris inDOTNET_ReadyToRunExcludeListin the test driver.[Wasm R2R] Report tracked GC vars as untracked in GC info (#129100)
AndyAyersMS commentedon Jun 7, 2026
Note
AI-generated content by the GitHub Copilot CLI
Continued sweep of
JIT/Regression/JitBluestandalone 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 inDOTNET_ReadyToRunExcludeListmakes 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 forIReadOnlyCollection<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 thatb3db78f4423("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 inObjectAllocator::CheckForGuardedAllocationOrCopyis 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:MethodTable::SanityCheck()(methodtable.cpp:6331) fails the "non-generic non-array impliespCanonMT == this" invariant. Stacks differ:Runtime_53549— R2R'd call site →VirtualDispatchHelpers::VirtualFunctionPointerSlow→ResolveVirtualFunctionPointer→ assert. Source isa[i].ToString()onA[] { B, B, ... }whereA.ToStringis virtual.Runtime_65694_2— R2R'd call site → prestubbedGC.Collect→_CollectPInvoke marshaling → assert. Both haveInlinedCallFrame(R2R origin) above theInterpreterFrame(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 threeThread.Starttests that hit the pre-existing wasm no-multithreading gap (b113493,b143840,b426654).AndyAyersMS commentedon Jun 7, 2026
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.b441487.try/catch NRE)DevDiv_362706.try/catch Exceptionwrapping a Fuzzlyn fragment)DevDiv_736188JitStressRegs=0x204silent abortRuntime_101046[ActiveIssue]forTestRuntimes.Mono); test runner does not honor[ActiveIssue]Notes:
b441487andDevDiv_362706fold into the existing EH-under-R2R cluster ("Wasm EH with catches/finallys/filters in R2R methods does not work yet").DevDiv_736188is the only genuinely new finding here: the test setsDOTNET_JitStressRegs=0x204viaCLRTestEnvironmentVariableand the cross-JIT silently produces broken wasm under that stress combination. Low priority (stress-only).Runtime_101046already exits 101 without R2R because the C P/Invoke target apparently doesn't sign-extendshort->intcorrectly 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 —genCodeForIndirnull check)addsub_il_r(root-caused, JIT codegen —genCodeForBinaryOverflowsigned vs. unsigned)Runtime_127075(CEA cloning miscompile, R2R-only path)Runtime_53549/Runtime_65694_2(corruptMethodTablemarshalled out of R2R'd code)DevDiv_736188(R2R + JitStressRegs stress, this comment)AndyAyersMS commentedon Jun 7, 2026
Note
This comment was generated by GitHub Copilot.
Correction on
DevDiv_736188: theDOTNET_JitStressRegs=0x204env 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-genericCallWith3Args<__Canon>(ref field, ref InternalSyncObject, initializer)returningdefault(T). The.r2rdumpshows crossgen2 emits both thestring[]instantiation (withMETHOD_DICTIONARY) and the__Canonform (withMethodDictionaryLookuphelper). 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.
[Wasm R2R] Fix unsigned add/sub overflow codegen (#129106)
AndyAyersMS commentedon Jun 10, 2026
Codegen bringup is now looking reasonably solid, so I am going to close this and track the remaining problems with individual issues.