Skip to content

[wasm][coreCLR] Dispatch R2R-compiled methods from reflection and UnmanagedCallersOnly paths#129766

Open
pavelsavara wants to merge 13 commits into
dotnet:mainfrom
pavelsavara:fix/wasm-interp-r2r-null-bytecode
Open

[wasm][coreCLR] Dispatch R2R-compiled methods from reflection and UnmanagedCallersOnly paths#129766
pavelsavara wants to merge 13 commits into
dotnet:mainfrom
pavelsavara:fix/wasm-interp-r2r-null-bytecode

Conversation

@pavelsavara

@pavelsavara pavelsavara commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

On wasm (CoreCLR interpreter, FEATURE_PORTABLE_ENTRYPOINTS), invoking a method that was compiled to native R2R code crashed at xUnit discovery with:

Unimplemented or invalid interpreter opcode

followed by a secondary m_crawl.GetCodeInfo()->IsValid() stackwalk assert.

Root cause

The wasm invoke paths resolve a method's interpreter byte code via MethodDesc::GetInterpreterCode() and pass the pointer to the interpreter. When the target method has no interpreter byte code because it was compiled to native (R2R) code, GetInterpreterCode() returns NULL. On wasm a NULL pointer reads zeroed low linear memory instead of faulting, so the interpreter set startIp = NULL, fetched ip[0] == 0, and dispatched it as INTOP_INVALID — aborting the process.

This only reproduced with R2R builds (native methods have no interpreter byte code). Pure-IL builds give every method interpreter byte code, so the byte code pointer was never NULL.

Two distinct entry paths reach a native (R2R) method without interpreter byte code:

  1. Reflection / CallDescr (e.g. xUnit discovery and invoke) → CallDescrWorkerInternal.
  2. UnmanagedCallersOnly reverse calls from native code → GetUnmanagedCallersOnlyThunk / ExecuteInterpretedMethodFromUnmanaged.

Fix

Reflection / CallDescr path (calldescrworkerwasm.cpp): when the resolved interpreter byte code is NULL, fall back to InvokeManagedMethod (the interpreter → R2R thunk), mirroring the established pattern in ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex and the CALL_INTERP_METHOD path in InterpExecMethod.

UnmanagedCallersOnly path (wasm/helpers.cpp): resolve the method's code first and, if it has native (R2R) code, return that R2R entrypoint directly. An UnmanagedCallersOnly method compiled by crossgen2 is emitted with the native calling convention, so its R2R code is itself the directly-callable unmanaged entrypoint. The g_ReverseThunks interpreter fallback is only needed for interpreted methods, so R2R methods are now dispatched directly and never go through the interpreter reverse thunk.

Because the UnmanagedCallersOnly path is fixed at the source, the downstream interpreter helpers now assert instead of silently dispatching a NULL:

  • ExecuteInterpretedMethodFromUnmanaged asserts a non-NULL byte code pointer (R2R methods no longer reach it).
  • ExecuteInterpretedMethodWithArgs asserts a non-NULL targetIp, so any future misuse fails loudly at the actual point of error instead of as a cryptic INTOP_INVALID.

Contract widening: resolving an UnmanagedCallersOnly entrypoint may now run the prestub to publish R2R native code, which can throw and trigger a GC. Contracts are widened accordingly:

  • MethodDesc::GetSingleCallableAddrOfCodeForUnmanagedCallersOnly is GC_TRIGGERS under FEATURE_PORTABLE_ENTRYPOINTS.
  • PortableEntryPoint::EnsureCodeForUnmanagedCallersOnly is THROWS; GC_TRIGGERS; MODE_PREEMPTIVE.

Validation

System.Drawing.Primitives tests built with R2R, run under V8:

Tests run: 2441  Passed: 2439  Failed: 0  Skipped: 2
WASM EXIT 0

Identical to the IL baseline.

Note

This pull request was authored with the assistance of GitHub Copilot.

…thods

CallDescrWorkerInternal (the wasm reflection/CallDescr invoke path) resolved the
interpreter byte code via MethodDesc::GetInterpreterCode() and, after a DoPrestub
retry, passed the pointer straight to ExecuteInterpretedMethodWithArgs. When the
target method has no interpreter byte code because it was compiled to native (R2R)
code, GetInterpreterCode() returns NULL. On wasm a NULL pointer reads zeroed low
linear memory instead of faulting, so the interpreter dispatched the NULL byte code
as INTOP_INVALID and aborted with Unimplemented or invalid interpreter opcode
plus a secondary stackwalk assert.

Add the InvokeManagedMethod fallback (the interpreter->R2R thunk) when there is no
interpreter code, mirroring the existing pattern in
ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex and the
CALL_INTERP_METHOD path in InterpExecMethod.

Also harden the sibling entry points: ExecuteInterpretedMethodFromUnmanaged now
takes the same fallback, and ExecuteInterpretedMethodWithArgs asserts a non-NULL
targetIp so future misuse fails loudly at the point of error.
Copilot AI review requested due to automatic review settings June 23, 2026 19:53
@pavelsavara pavelsavara added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Jun 23, 2026
@pavelsavara pavelsavara changed the title [wasm] Fix interpreter dispatch of NULL byte code for R2R-compiled methods [wasm][coreCLR] Fix interpreter dispatch of NULL byte code for R2R-compiled methods Jun 23, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

This comment was marked as outdated.

@pavelsavara pavelsavara requested a review from jkotas June 23, 2026 20:12
@lewing lewing requested a review from a team June 23, 2026 21:35
Comment thread src/coreclr/vm/prestub.cpp Outdated
Address PR dotnet#129766 review feedback: resolve R2R-compiled native code for

UnmanagedCallersOnly methods directly instead of going through the interpreter

reverse-thunk fallback.

- GetUnmanagedCallersOnlyThunk (wasm/helpers.cpp): run DoPrestub under GCX_PREEMP

  to publish code, then return the native (R2R) entrypoint via PortableEntryPoint

  when present; the g_ReverseThunks fallback is only for interpreted methods.

- ExecuteInterpretedMethodFromUnmanaged (prestub.cpp): R2R methods are now

  dispatched directly and never reach this path, so replace the InvokeManagedMethod

  fallback with an assert that the interpreter byte code exists.

- Widen contracts for the prestub-on-resolve path: EnsureCodeForUnmanagedCallersOnly

  (precode_portable.cpp) is THROWS/GC_TRIGGERS/MODE_PREEMPTIVE;

  GetSingleCallableAddrOfCodeForUnmanagedCallersOnly (method.cpp) is GC_TRIGGERS

  under FEATURE_PORTABLE_ENTRYPOINTS.
pavelsavara added a commit to pavelsavara/runtime that referenced this pull request Jun 24, 2026
Address PR dotnet#129766 review feedback: resolve R2R-compiled native code for

UnmanagedCallersOnly methods directly instead of going through the interpreter

reverse-thunk fallback.

- GetUnmanagedCallersOnlyThunk (wasm/helpers.cpp): run DoPrestub under GCX_PREEMP

  to publish code, then return the native (R2R) entrypoint via PortableEntryPoint

  when present; the g_ReverseThunks fallback is only for interpreted methods.

- ExecuteInterpretedMethodFromUnmanaged (prestub.cpp): R2R methods are now

  dispatched directly and never reach this path, so replace the InvokeManagedMethod

  fallback with an assert that the interpreter byte code exists.

- Widen contracts for the prestub-on-resolve path: EnsureCodeForUnmanagedCallersOnly

  (precode_portable.cpp) is THROWS/GC_TRIGGERS/MODE_PREEMPTIVE;

  GetSingleCallableAddrOfCodeForUnmanagedCallersOnly (method.cpp) is GC_TRIGGERS

  under FEATURE_PORTABLE_ENTRYPOINTS.
@pavelsavara pavelsavara changed the title [wasm][coreCLR] Fix interpreter dispatch of NULL byte code for R2R-compiled methods [wasm][coreCLR] Dispatch R2R-compiled methods from reflection and UnmanagedCallersOnly paths Jun 24, 2026
@AndyAyersMS

Copy link
Copy Markdown
Member

UCO -> R2R seems to be needed to run against an R2R'd SPC, something in the shutdown path calls back into the runtime this way.

Comment thread src/coreclr/vm/wasm/helpers.cpp Outdated
Comment thread src/coreclr/vm/wasm/helpers.cpp Outdated
Comment thread src/coreclr/vm/wasm/helpers.cpp Outdated
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Copilot AI review requested due to automatic review settings June 25, 2026 17:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread src/coreclr/vm/method.cpp Outdated
Comment thread src/coreclr/vm/wasm/helpers.cpp Outdated
Copilot AI review requested due to automatic review settings June 25, 2026 17:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread src/coreclr/vm/wasm/helpers.cpp Outdated
Comment thread src/coreclr/vm/wasm/helpers.cpp Outdated
@pavelsavara

Copy link
Copy Markdown
Member Author

I'm OOO till Wed, whoever is blocked by this, feel free to take over.

Comment thread src/coreclr/vm/wasm/helpers.cpp Outdated
Copilot AI review requested due to automatic review settings June 26, 2026 03:48
Comment thread src/coreclr/vm/prestub.cpp Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment on lines +41 to +51
if (targetIp == NULL)
{
// The target method has no interpreter code because it was compiled to native (R2R) code.
// Invoke it as a compiled managed method through the interpreter->R2R thunk, mirroring the
// fallback already present in ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex and
// the CALL_INTERP_METHOD path in InterpExecMethod. Without this, the NULL bytecode pointer
// would be handed to the interpreter and dispatched as INTOP_INVALID.
ManagedMethodParam param = { pMethod, args, (int8_t*)retBuff, (PCODE)NULL, nullptr };
InvokeManagedMethod(&param);
return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the whole CallDescrWorker is not setup correctly wrt GC on wasm. Our plan is to delete it.

Copilot AI review requested due to automatic review settings June 26, 2026 03:57
Comment thread src/coreclr/vm/prestub.cpp Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment on lines +41 to +50
if (targetIp == NULL)
{
// The target method has no interpreter code because it was compiled to native (R2R) code.
// Invoke it as a compiled managed method through the interpreter->R2R thunk, mirroring the
// fallback already present in ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex and
// the CALL_INTERP_METHOD path in InterpExecMethod. Without this, the NULL bytecode pointer
// would be handed to the interpreter and dispatched as INTOP_INVALID.
ManagedMethodParam param = { pMethod, args, (int8_t*)retBuff, (PCODE)NULL, nullptr };
InvokeManagedMethod(&param);
return;
Copilot AI review requested due to automatic review settings June 26, 2026 04:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment on lines +48 to +50
ManagedMethodParam param = { pMethod, args, (int8_t*)retBuff, (PCODE)NULL, nullptr };
InvokeManagedMethod(&param);
return;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-CodeGen-Interpreter-coreclr os-browser Browser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants