[wasm][R2R] `function signature mismatch` on the first (cold) R2R call to a method whose native code is not yet on its portable entry point
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
On Wasm ReadyToRun, the **first** R2R call to a method whose R2R native code has not yet been wired onto its portable entry point (PEP) traps with `RuntimeError: function signature mismatch`. The external-method fixup resolves that first call to a portable-entry-point → interpreter transition thunk whose wasm function type does not match the `call_indirect` type declared at the R2R call site.
The failure is order-dependent: it only occurs when the target is reached "cold" (before its native code is wired). The same call succeeds if the target's native code was already wired by an earlier call.
### Repro
Build a Wasm R2R `System.Private.CoreLib` with crossgen2 and run an app under corerun with `DOTNET_ReadyToRun=1`.
Any of these trap:
- a `throw` + `catch` whose stack trace gets **formatted** (e.g. `e.StackTrace` is read, or a second exception is thrown),
- `new System.Diagnostics.StackTrace(true)`,
- `System.Globalization.CultureInfo.GetCultureInfo("en-US")`.
A single same-frame `throw` + `catch` that never formats a stack trace **succeeds**.
Exceptions reach this path because stack-trace formatting loads localized resources, which runs globalization code (`ResourceManager` → `CultureInfo.GetCultureInfo` → `CultureData.GetCultureData`) on a cold path.
```
RuntimeError: function signature mismatch
at wasm-function[7959] (System.Globalization.CultureData.GetCultureData)
at wasm-function[43386] (WasmDelayLoadHelper)
at wasm-function[8118] (System.Globalization.CultureInfo.GetCultureInfo)
...
```
The trapping instruction (module byte offset `0x68e7cf` in `GetCultureData`) is:
```
call_indirect (type 19) ;; type 19 = (i32, i32, i32) -> ()
```
which is the call to `System.Threading.Monitor.Exit(object)` (the `finally` of `GetCultureData`'s `lock`).
The correctly-typed R2R stub for `Monitor.Exit` is present in the module as `func[10088]`, and it is also `type#19` — i.e. the declared call type and the intended target type match. The resolved target at the moment of the trap does not.
### Root cause
Instrumenting `MethodDesc::EnsurePortableEntryPointIsCallableFromR2R` (`src/coreclr/vm/method.cpp`) at the trapping call:
```
[first call] Monitor.Exit hasNativeCode=0 -> wires interpreter transition thunk
[later call] Monitor.Exit hasNativeCode=1
```
- `MethodDesc::GetPortableEntryPoint()` returns the temporary entry point (a prestub). On the first call the target's PEP has no native code (`hasNativeCode == 0`).
- With no native code, `EnsurePortableEntryPointIsCallableFromR2R` resolves the entry to the portable-entry-point → interpreter transition thunk (`GetPortableEntryPointToInterpreterThunk`, looked up via the `'I'`-prefixed signature key in `src/coreclr/vm/wasm/helpers.cpp`).
- That thunk's wasm function type does not match the R2R call site's declared `call_indirect` type (`type#19`), so the call traps.
- On a later call the same PEP reports `hasNativeCode == 1`, so the direct R2R entry (`func[10088]`, `type#19`) would be used. The mismatch only occurs on the cold first call.
The in-source comment on `EnsurePortableEntryPointIsCallableFromR2R` states it "must be called before any R2R code may call the target method"; the observed cold call violates that ordering.
### Notes
- EH dispatch is not itself affected: `throw` / `catch` / unwind works (a single same-frame throw+catch returns normally). The failure is in method-handle/resource resolution during stack-trace formatting.
- `Monitor.Exit` is an `InternalCall` (FCall) — it has no IL.
- Reproduces on a clean baseline runtime and SPC
> [!NOTE]
> This issue description was drafted with GitHub Copilot (AI-generated) and reviewed by the author.
Contributor guide
Research direction
Start with MethodDesc::EnsurePortableEntryPointIsCallableFromR2R in src/coreclr/vm/method.cpp and the 'I'-prefixed signature lookup in src/coreclr/vm/wasm/helpers.cpp. Build the Wasm R2R System.Private.CoreLib with crossgen2, run the listed corerun repro, and trace the cold Monitor.Exit resolution. Done means the first cold R2R call uses a target compatible with the call_indirect type and no longer traps.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, csharp, wasm
- Domain
- backend, compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 43/100