Repository navigation
Description
Tested on top of #134092
On browser-wasm CoreCLR with ReadyToRun (full/eager R2R), the runtime aborts at startup (before Main) with:
RuntimeError: null function or function signature mismatch
The trap is an interpreter→R2R call_indirect type mismatch when interpreted code calls a shared-generic method whose R2R body was not emitted by crossgen2. The method's portable entry point (PEP) reports native code, and the interpreter takes the interp→R2R path, but the target function's wasm type does not match the call_indirect type the interp→R2R thunk uses.
Trap stack (named frames, built with WasmNativeStrip=false)
at WasmInterpreterToR2RThunk(vTiip) wasm-function[3857] <- TRAPS (call_indirect to the target)
at <interpreted> wasm-function[1410]/[8698]
at WasmR2RToInterpreterThunk(vTiip) wasm-function[3661]
at WasmDelayLoadHelper ... DelayLoad_MethodCall(Sig:vTiip) wasm-function[3500]
at S_P_CoreLib_System_AppContext__Setup wasm-function[70]
AppContext.Setup (R2R) → an interpreted method → an interpreter→R2R call that traps.
Failing method
Instrumenting InvokeManagedMethod (src/coreclr/vm/wasm/helpers.cpp) right before InvokeCalliStub at the trapping call:
pep=0x60d494 hasNative=1 code=0x21b7 key=MvTiip a=2
reqInst=0 shared=1 codeReqInst=0 codeShared=1 codeSame=1
cls=System.Collections.Generic.Dictionary`2 n=.ctor
The target is the canonical System.Collections.Generic.Dictionary<__Canon,__Canon>.ctor(int capacity, IEqualityComparer<TKey> comparer):
reqInst=0— the method needs no generic-context / inst arg (context comes fromthis). The signature key isMvTiip=void,this, two args, PEP — no genctx.shared=1,codeSame=1— it is the canonical shared method and the resolved code maps back to the sameMethodDesc.hasNative=1— the PEP reports a native entry, so the interpreter takesInvokeManagedMethod(there is no interpreter code for it), but thecall_indirect(vTiip)tocode=0x21b7traps.
This rules out a generic-context / exact-vs-canonical signature disagreement — every managed encoder (crossgen method-side thunk, runtime GetSignatureKey) computes vTiip with no genctx.
crossgen2 evidence: the method has no R2R body
Instrumenting the interp→R2R thunk emission in CorInfoImpl.ReadyToRun.cs and re-running the whole-CoreLib crossgen shows the 2-arg (int, IEqualityComparer) Dictionary ctor is emitted only for value-type-keyed instantiations (all reqInst=False, sig=vTiip), e.g.:
sig=vTiip reqInst=False ... Dictionary`2<System.Guid,System.__Canon>..ctor(int32,IEqualityComparer`1<Guid>)
sig=vTiip reqInst=False ... Dictionary`2<native int,System.__Canon>..ctor(int32,IEqualityComparer`1<native int>)
The fully-shared Dictionary<__Canon,__Canon> form is compiled only for the 0/1/4-arg ctors — not the 2-arg (int, IEqualityComparer) overload:
sig=vTp ... Dictionary`2<__Canon,__Canon>..ctor()
sig=vTip ... (various 1-arg forms)
sig=vTiiiip ... ConcurrentDictionary`2<__Canon,__Canon>..ctor(int32,int32,bool,IEqualityComparer`1<__Canon>)
So at runtime Dictionary<__Canon,__Canon>.ctor(int, IEqualityComparer) has no R2R body, yet its PEP reports native code and the interp→R2R thunk (vTiip) call_indirects to a target whose wasm type doesn't match → trap.
Relationship to existing issues
This appears to be the same failure family as #130634 (function signature mismatch on a cold R2R call to a method whose native code isn't wired onto its PEP), but observed from the interpreter→R2R direction and for a method with no R2R body at all (a crossgen2 compilation gap), rather than a cold-ordering race on a method that does have R2R code. Likely also related: #129622, #133307, #134144, #133466. Tracking: #130524.
Repro
Build/run a browser-wasm CoreCLR console app with PublishReadyToRun=true on a configuration with full/eager R2R coverage. It aborts at startup, before Main, in AppContext.Setup's call chain — the app's own code is irrelevant; it is a startup-path trap. (Investigated on a branch that enables eager/full R2R for browser-wasm CoreCLR; the fuller R2R coverage is what surfaces the interp→R2R transition into Dictionary.ctor.)
Fix proposals
- Robust interp→R2R dispatch (primary). When a method's R2R body is absent, or its PEP is not a matching-signature native function, dispatch it through the interpreter (or an IL adapter) instead of a blind
call_indirectthat traps. [wasm][R2R] Fix closed static delegate struct returns #134108 added exactly this "target-readiness check + IL-adapter fallback" for closed static delegates; extend the same machinery to the direct interp→R2R path (InvokeManagedMethod/GetCookieForManagedMethodinsrc/coreclr/vm/wasm/helpers.cpp; theCALL_INTERP_METHODgate insrc/coreclr/vm/interpexec.cppalready does aHasNativeEntryPointcheck for the CALLI path). - Don't report a non-callable native entry. Ensure
PortableEntryPoint::HasNativeEntryPoint/MethodDesc::GetMultiCallableAddrOfCodedo not report a callable native entry for a method that lacks a real, signature-matching R2R body, so the interpreter falls back correctly. (Overlaps theEnsurePortableEntryPointIsCallableFromR2Ranalysis in [wasm][R2R]function signature mismatchon the first (cold) R2R call to a method whose native code is not yet on its portable entry point #130634.) - Close the crossgen2 compilation gap. Have crossgen2 emit the fully-shared
Dictionary<__Canon,__Canon>.ctor(int, IEqualityComparer)(and, more generally, startup-reachable shared-generic methods) so that a realvTiipbody exists.
Note
This issue was investigated and drafted with GitHub Copilot (AI-generated) and reviewed by the author.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
dotnet-policy-service commentedon Sep 18, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
dotnet-policy-service commentedon Sep 18, 2026
Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.
pavelsavara commentedon Sep 18, 2026
Repro test case
I've added a minimal, self-contained repro to the existing
readytorun/wasm/WasmInterpreterTransitionstest on a branch offmain:pavelsavara/runtime@wasm-r2r-interp-dict-reproThe new interpreted method builds a shared reference-type generic via the
(int, IEqualityComparer)ctor — the exact instantiation that has no R2R body:This test project uses
AlwaysUseCrossGen2(full R2R, not--partial).--partialis not the triggerI confirmed the trap is independent of partial R2R. Rebuilding the console-node sample with full R2R (
PublishReadyToRun=truewith an emptyWasmReadyToRunProfile) produces the identical startup trap:So the root cause is a crossgen2 CoreLib instantiation-coverage gap: the canonical
Dictionary<__Canon,__Canon>.ctor(int, IEqualityComparer)has no R2R body, and the interpreter→R2R thunk'scall_indirecttraps. The trap fires at startup (AppContext.Setup) beforeMain, so any R2R wasm app hits it regardless of--partial.Note
This comment was generated with AI (GitHub Copilot) assistance.
pavelsavara commentedon Sep 21, 2026
Another reproduction of this same interp→R2R trap, this time from the native-export reverse-thunk direction (partial R2R driven by a
.mibcPGO profile), not just the startupAppContext.Setupchain.The browser event loop calls the generated native C export
SystemJS_ExecuteBackgroundJobCallback(fromsetTimeout), which forwards to the reverse thunk forSystem.Threading.ThreadPool.BackgroundJobHandler— an[UnmanagedCallersOnly]void()callback:Under
--partialR2R,BackgroundJobHandleris compiled to R2R (hasNativeCode=1) but has no interpreter body, soExecuteInterpretedMethodFromUnmanaged→InvokeManagedMethodtakes the interp→R2R path and traps identically:Notable: this is a
void()method (sig=vp— void return, PEP, nothis, no args, no genctx), so it rules out arg marshaling and any generic-context / exact-vs-canonical signature disagreement entirely. The interp→R2Rcall_indirecttraps even for the simplest possible signature when the target has no matching R2R body wired to its PEP — reinforcing fix proposals #1/#2 (target-readiness check + interpreter/IL-adapter fallback, and/or not reporting a non-callable native entry). This native-export path is covered by those as long as the reverse thunk (ExecuteInterpretedMethodFromUnmanagedinsrc/coreclr/vm/prestub.cpp) routes a no-interp-body method into that robust dispatch instead of asserting.Repro (branch enabling eager R2R for browser-wasm CoreCLR):
It prints
Hello World!and the args, then traps when the ThreadPool background-job callback fires afterMainreturns.Note
AI-generated (GitHub Copilot), reviewed by the author.
pavelsavara commentedon Sep 22, 2026
Another repro of this trap, from a reflection
MethodBase.Invokepath (not the startupAppContext.Setupchain) — a cleaner, non-startup reproduction while runningMicrosoft.CSharp.Testson browser-wasm CoreCLR R2R:(The parenthetical
icu::…/EE*Exception::…names in the raw trace are bogus nearest-symbol guesses from the stripped build; the reliable frames are the demangled C++ runtime frames and theS_P_CoreLib_*/ managed method names.)xunit invokes the test method via reflection; the invoke stub runs interpreted;
InvokeManagedMethodresolves the target withGetMultiCallableAddrOfCodeand dispatches throughWasmInterpreterToR2RThunk(vTip), whosecall_indirecttarget forWithTypedNullNullableInt64is a null/mismatched wasm function →null function.Same root cause as the
Dictionary.ctorcase above (PEP reports native code but there is no matching-signature R2R body), just reached throughSystem.Reflection.MethodBaseInvoker.InvokeWithOneArgrather than a delay-load thunk during startup — so it's a convenient standalone repro for validating fix proposal #1 (target-readiness check + interpreter fallback inInvokeManagedMethod/GetCookieForManagedMethod).Note
AI-generated (GitHub Copilot), reviewed by the author.
[wasm][coreclr][R2R] Publish existing method bodies before interprete…
lewing commentedon Sep 27, 2026
@pavelsavara I can't reproduce any of the three manifestations of this on current
main(d8057ba49dd). Could you retest with your eager-R2R configuration?Locally, on osx-arm64:
InterpretedBuildsSharedGenericDictionarytest inreadytorun/wasm/WasmInterpreterTransitionsconsole-nodewithPublishReadyToRun=true(full R2R, no profile)Hello World!and args, exit 0Microsoft.CSharp.TestswithTestWasmReadyToRun=true EnableAggressiveTrimming=trueon ChromeWithTypedNullNullableInt64caseI checked that the published images were ReadyToRun. The trimmed CoreLib now contains several
Dictionary<__Canon,__Canon>constructor bodies. I didn't check whether one of them is the(int, IEqualityComparer)overload, and I didn't bisect the fix. #134071 and #134137 (portable entrypoint and interpreter code publication ordering) are plausible candidates.The remaining manifestation is the native-export reverse thunk (
SystemJS_ExecuteBackgroundJobCallback→ThreadPool.BackgroundJobHandlerunder MIBC partial R2R), which #134355 addresses. I didn't run that configuration.If your retest is clean too, I suggest closing this issue once #134355 merges. If it still traps for you, please post the
InvokeManagedMethodinstrumentation output again (pep/code/key), so we can see what the published entry point resolves to on currentmain.Note
This comment was drafted with GitHub Copilot (AI-generated) from local test runs.