Skip to content

[wasm] Fix interpreter crash with MethodImpl .override on PortableEntryPoints#126124

Merged
radekdoulik merged 17 commits into
mainfrom
wasm-fix-methodimpl-interpreter-crash
Jun 29, 2026
Merged

[wasm] Fix interpreter crash with MethodImpl .override on PortableEntryPoints#126124
radekdoulik merged 17 commits into
mainfrom
wasm-fix-methodimpl-interpreter-crash

Conversation

@radekdoulik

@radekdoulik radekdoulik commented Mar 25, 2026

Copy link
Copy Markdown
Member

Fix an interpreter crash (and a related RuntimeHelpers.PrepareMethod issue) when a .override (MethodImpl) directive remaps a virtual method's vtable slot on PortableEntryPoints (e.g. WASM).

Problem

When MethodB declares .override MethodA, the overriding method's PortableEntryPoint is placed into the overridden method's (MethodA's) vtable slot, so that slot no longer belongs to MethodA:

  • The interpreter's SetNativeCodeInterlocked CAS fails for MethodA, so SetInterpreterCode is never reached and GetInterpreterCode returns NULL → crash when the method is invoked (delegates, reflection, virtual dispatch).
  • RuntimeHelpers.PrepareMethod / PrepareDelegate on MethodA operate on the decl, which no longer has the live code. On portable entrypoints this even compiles the decl's dead body and then fails to publish it; on other targets it silently prepares nothing. Either way the impl body that actually runs on invoke is never prepared.

Fix

Resolve the .override decl→impl redirect (MethodTable::MapMethodDeclToMethodImpl) at the points that need the method actually owning the slot's code:

  • interpexec.cpp (PrepareInterpreterCode) — under FEATURE_PORTABLE_ENTRYPOINTS, when the target is a remapped vtable slot, compile the overriding method and cache the result on the original MethodDesc (or poison it on failure). This fixes the runtime execution paths (delegates, reflection) where the target isn't known at compile time.
  • reflectioninvocation.cpp (PrepareMethodHelper) — resolve decl→impl up front so PrepareMethod / PrepareDelegate prepare the impl. This mirrors getFunctionEntryPoint, which resolves direct calls the same way, and corrects the PrepareMethod behavior on all platforms.

An earlier iteration special-cased ShouldCallPrestub in method.cpp; that approach was reverted in favor of the caller-side resolution above, so method.cpp is unchanged vs. main.

Tests

Validated on desktop (macOS/arm64) and browser-wasm/CoreCLR: both the interpreter execution path and the PrepareMethod path no longer crash and correctly dispatch to the overriding method.

Note

Parts of this PR description were drafted with the help of GitHub Copilot.

Copilot AI review requested due to automatic review settings March 25, 2026 20:54
@radekdoulik radekdoulik added this to the 11.0.0 milestone Mar 25, 2026
@radekdoulik radekdoulik added arch-wasm WebAssembly architecture area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI area-VM-coreclr and removed area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI labels Mar 25, 2026

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

Fixes a CoreCLR interpreter crash on WASM when a MethodImpl .override causes a virtual method’s vtable slot to be remapped to a different method’s PortableEntryPoint, which can prevent interpreter code from being established during prestub execution.

Changes:

  • Adjusts MethodDesc::ShouldCallPrestub() (PortableEntryPoints builds) to consult the method’s own PortableEntryPoint when the vtable slot has been remapped to a different method’s PE.
  • Enhances PrepareInterpreterCode() to detect vtable-slot PE redirection to a different MethodDesc (MethodImpl scenario), prepare interpreter code for the redirected method, and cache it on the original target method.

Reviewed changes

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

File Description
src/coreclr/vm/method.cpp Updates prestub decision logic to handle MethodImpl-induced vtable PE remapping correctly under PortableEntryPoints.
src/coreclr/vm/interpexec.cpp Adds MethodImpl redirect detection when interpreter code isn’t established, preparing interpreter code via the redirected slot method instead.

Comment thread src/coreclr/vm/method.cpp Outdated
Comment thread src/coreclr/vm/interpexec.cpp Outdated
Comment thread src/coreclr/vm/interpexec.cpp Outdated
@github-actions

This comment has been minimized.

Copilot AI review requested due to automatic review settings April 7, 2026 15:03

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 2 out of 2 changed files in this pull request and generated no new comments.

@github-actions

This comment has been minimized.

…ryPoints

Resolve MethodImpl overrides in getCallInfo for direct calls when
FEATURE_PORTABLE_ENTRYPOINTS is enabled. On non-WASM, this resolution
happens in getFunctionEntryPoint via MapMethodDeclToMethodImpl, but
that function is not available with portable entry points. Adding
the resolution to getCallInfo ensures the interpreter compiler
receives the correct target MethodDesc at compile time.

This fixes a crash where a non-virtual call to a MethodImpl-overridden
method (e.g. call instance MyBar::DoBar() with .override pointing to
DoBarOverride) would target the wrong method, leading to uninitialized
interpreter code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@radekdoulik radekdoulik force-pushed the wasm-fix-methodimpl-interpreter-crash branch from 05dd3a6 to 7dbe917 Compare April 7, 2026 19:02
@radekdoulik radekdoulik changed the title [wasm][coreclr] Fix interpreter crash with MethodImpl .override on PortableEntryPoints [wasm] Fix interpreter crash with MethodImpl .override on PortableEntryPoints Apr 7, 2026
@github-actions

This comment has been minimized.

Comment thread src/coreclr/vm/jitinterface.cpp Outdated
Fix crash when a .override directive replaces a virtual method's vtable slot
on WASM (PortableEntryPoints). The .override directive causes the overriding
method's entry point to be placed in the overridden method's vtable slot.
This makes SetNativeCode CAS fail for the overridden method (the slot no
longer belongs to it), so SetInterpreterCode is never reached and
GetInterpreterCode returns NULL, leading to a crash.

Two fixes:
- jitinterface.cpp: Resolve the .override at compile time in getCallInfo so
  the interpreter compiler targets the overriding method directly for
  non-virtual calls.
- interpexec.cpp: In PrepareInterpreterCode, when GetInterpreterCode returns
  NULL and the vtable slot points to a different method due to .override,
  follow the redirect to prepare and cache the overriding method's interpreter
  code. This handles runtime paths (delegates, reflection) where the target
  is not known at compile time.

Add delegate test coverage to self_override5.il using Delegate.CreateDelegate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
radekdoulik and others added 3 commits June 24, 2026 18:28
Clarify that PrepareMethod must prepare the impl (the method owning the
slot's code), not the decl. On portable entrypoints the no-fix path compiles
the decl's dead body and fails to publish it; on other targets it is a no-op
on the decl. Either way the impl body that runs on invoke is never prepared.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ixed

Remove the ActiveIssue gating (#120708, PlatformDetection.IsBrowser)
that disabled the test on browser-wasm while the interpreter .override crash was
unfixed. This PR fixes that crash, so the test can run on wasm again.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 24, 2026 16:40

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.

@radekdoulik

Copy link
Copy Markdown
Member Author

/azp run runtime-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@radekdoulik

Copy link
Copy Markdown
Member Author

/azp run runtime-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

lewing added a commit to lewing/runtime that referenced this pull request Jun 25, 2026
The same MethodImpl + PortableEntryPoint vtable-slot remapping bug we
hit on WASI is the one already ActiveIssue'd for Browser (the test
source carries ActiveIssue(120708, IsBrowser)). Radek Doulik has the
fix in flight as dotnet#126124 — exact same diagnosis as our
own analysis: when a .override remaps a virtual method's vtable slot,
SetNativeCodeInterlocked CAS fails for the overridden method, so
SetInterpreterCode is never reached and GetInterpreterCode returns
NULL; the indirect call through PortableEntryPoint._pActualCode then
traps with 'wasm trap: uninitialized element'.

Two changes:
* src/tests/Loader/classloader/MethodImpl/Desktop/self_override5.il:
  ActiveIssue(120708, IsBrowser) -> ActiveIssue(120708, IsWasm) so
  Browser AND WASI both skip via xunit once dotnet#126124 merges.
* src/coreclr/wasi/tests/known-failures.txt: refresh the comment to
  reference dotnet#126124 and Radek's diagnosis; keep the entry because .il
  tests don't go through XUnitWrapperGenerator in standalone-build
  mode, so the ActiveIssue attribute is ignored by our sweep runner.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/coreclr/vm/reflectioninvocation.cpp Outdated
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Copilot AI review requested due to automatic review settings June 28, 2026 17:07
@radekdoulik

Copy link
Copy Markdown
Member Author

/azp run runtime-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@radekdoulik radekdoulik enabled auto-merge (squash) June 28, 2026 17:10
@radekdoulik

Copy link
Copy Markdown
Member Author

/azp run runtime-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

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/reflectioninvocation.cpp
@github-actions

Copy link
Copy Markdown
Contributor

Copilot Code Review

Holistic Assessment

Motivation: The PR addresses a real interpreter crash on WASM (FEATURE_PORTABLE_ENTRYPOINTS) caused by .override (MethodImpl) remapping a virtual method's vtable slot to the overriding method's PortableEntryPoint. The fix also corrects a cross-platform PrepareMethod bug where the decl's dead body is prepared instead of the impl's live code. Both problems are well-understood and clearly demonstrated by the test additions.

Approach: Resolving decl→impl via MapMethodDeclToMethodImpl at the caller sites (PrepareInterpreterCode, PrepareMethodHelper) is consistent with established patterns in the codebase (fptrstubs.cpp:108, jitinterface.cpp:9180). The IsVtableSlot() guard is an efficient filter that avoids unnecessary method table walks. The FEATURE_PORTABLE_ENTRYPOINTS scoping in interpexec.cpp is correct — non-WASM platforms use precodes with per-MethodDesc native code slots (so the CAS doesn't fail), while the reflectioninvocation.cpp fix is correctly unguarded since PrepareMethod has the same semantic bug on all platforms.

Summary: ⚠️ Needs Human Review. The code is correct by my analysis: thread safety is sound (concurrent threads resolve to the same targetMethod and produce the same interpreter code pointer, making SetInterpreterCode/PoisonInterpreterCode idempotent), the override resolution follows existing patterns, and the tests cover the key crash scenarios (delegate, generic, virtual/non-virtual). However, this touches critical VM infrastructure (interpreter execution, reflection invocation) and two review threads from @jkotas remain formally unresolved — a human reviewer should verify the current state satisfies that feedback and confirm the fix scope is sufficient.


Detailed Findings

Detailed Findings

✅ Correctness — interpexec.cpp override resolution

The PrepareInterpreterCode fix is correct:

  1. MapMethodDeclToMethodImpl is called before DoPrestub, so the correct method body is compiled (not the dead decl body).
  2. The result is cached on pOriginalMethod via SetInterpreterCode, so subsequent calls to IsInterpreterCodeInitialized on the decl return the cached impl code without re-resolution.
  3. Both the poison path (targetIp == NULL) and the success path correctly propagate to pOriginalMethod.
  4. MapMethodDeclToMethodImpl handles the identity case (no .override) by returning the input unchanged, so pOriginalMethod == targetMethod and the caching code is correctly skipped.

Verified: MapMethodDeclToMethodImpl contracts (STATIC_CONTRACT_THROWS, STATIC_CONTRACT_GC_TRIGGERS) are compatible with the COOP mode context at this call site.

✅ Correctness — reflectioninvocation.cpp PrepareMethod fix

The PrepareMethodHelper fix resolves decl→impl before any preparation logic (EnsureActive, ShouldCallPrestub, DoPrestub). This mirrors the getFunctionEntryPoint pattern (jitinterface.cpp:9180) and fixes the cross-platform bug jkotas identified: without this, PrepareMethod on a decl compiles the decl's body (which is dead code after .override) instead of the impl's live body.

The fix is correctly unguarded by FEATURE_PORTABLE_ENTRYPOINTS since the semantic issue exists on all platforms — confirmed by jkotas's repro example in review thread PRRT_kwDODI9FZc58a8uz.

✅ Thread safety — SetInterpreterCode / PoisonInterpreterCode on pOriginalMethod

Both SetInterpreterCode (uses VolatileStore) and PoisonInterpreterCode (uses VolatileStore) are thread-safe stores. Concurrent threads entering PrepareInterpreterCode for the same pOriginalMethod will:

  • Resolve to the same targetMethod (deterministic)
  • Produce the same targetIp (compilation result is cached)
  • Write the same value to pOriginalMethod->m_interpreterCode (idempotent)

The assert _ASSERTE(m_interpreterCode != INTERPRETER_CODE_POISON) in SetInterpreterCode cannot fire in this context because poison and set derive from the same compilation outcome — a method cannot both fail and succeed compilation.

✅ Test coverage — delegate and generic scenarios

  • self_override5.il: Adding TestDelegateDoBar via Delegate.CreateDelegate is a well-targeted test — this exercises the runtime path where the interpreter must resolve the override at invocation time (not known at compile time). Testing both MyBar and MyFoo instances covers the MethodImpl body substitution and virtual dispatch through subclass overrides respectively.
  • self_override_generic.il: Tests generic .override with both reference (string) and value (int32) type instantiations, and both virtual and non-virtual calls. The .override method instance int32 Program::A<[1]>() syntax is valid ECMA-335 ILAsm for referencing the first generic method parameter of the containing method in the .override context.
  • The ActiveIssue removal for browser-wasm (issue [wasm][coreclr] fix and run more runtime tests, run them on CI #120708) is appropriate since the crash is now fixed.

💡 Suggestion — generic test could add delegate coverage

The new self_override_generic.il tests virtual and non-virtual call dispatch but does not test the delegate path for generic .override methods. A Delegate.CreateDelegate test for A<string>() and A<int32>() would validate that the interpreter correctly resolves the override through delegate invocation for generic methods. This could be a follow-up.

✅ Test project — self_override_generic.ilproj

The .ilproj structure matches sibling test projects (CLRTestPriority 1, Compile Include pointing to Desktop\ subfolder). The absence of <ProjectReference Include="$(TestLibraryProjectPath)" /> is correct — this test doesn't use TestLibrary utilities.

Note

This review was generated by GitHub Copilot.

Generated by Code Review for issue #126124 · ● 30.9M ·

@radekdoulik

Copy link
Copy Markdown
Member Author

/ba-g the failing outerloop jobs fail on main too

@radekdoulik radekdoulik merged commit c366a53 into main Jun 29, 2026
167 of 171 checks passed
@radekdoulik radekdoulik deleted the wasm-fix-methodimpl-interpreter-crash branch June 29, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-VM-coreclr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants