dotnet / dotnet/runtime

[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

Open
#130,634 5 comments 0 reactions 0 assignees View on GitHub
arch-wasm area-VM-coreclr
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.