Skip to content

[wasm][coreCLR] R2R generic dictionary lookup traps with null function (zero import cell for GenericLookupSignature) #129821

Closed

Description

@pavelsavara

Summary

On wasm (CoreCLR, R2R), executing shared-generic R2R code that performs a generic-dictionary lookup (e.g. typeof(T) inside a __Canon instantiation) traps with:

Uncaught RuntimeError: null function or function signature mismatch

This is a call_indirect to wasm function-table index 0. The generic-lookup R2R import cell is emitted as a zero pointer and the runtime-lookup slow path calls through it.

Repro

Branch with the wasm R2R bring-up work (depends on #129766 / UCO→R2R dispatch to reach the R2R-native export). Reproduced deterministically on V8:

.\dotnet.cmd build -bl /p:TargetOS=browser /p:TargetArchitecture=wasm /p:Configuration=Debug /p:RuntimeFlavor=CoreCLR /t:Test .\src\libraries\System.Runtime.InteropServices.JavaScript\tests\System.Runtime.InteropServices.JavaScript.UnitTests\System.Runtime.InteropServices.JavaScript.Tests.csproj /p:Scenario="WasmTestOnV8" /p:WasmTestAppArgs="-method System.Runtime.InteropServices.JavaScript.Tests.JSExportTest.JsExportIJSObject"

Call chain: JsExportIJSObject (R2R) → UCO/interp→R2R thunk → JsExportTest<System.__Canon> (R2R shared-generic) → traps at the first generic-dictionary access.

Isolation (confirmed)

Staged diagnostics inside JsExportTest<T> (3 V8 runs) pinpoint the trap precisely:

step operation result
enter JsExportTest<__Canon> (no dictionary) OK
invoke == null check (no dictionary) OK
res = invoke(value, echoName) shared-generic delegate invoke OK, res non-null
typeof(T) generic-dictionary lookup TRAPS "null function"

The shared-generic delegate invoke works; only the generic-dictionary lookup traps. (This refutes an earlier delegate _methodPtr hypothesis.)

Root cause

  • getReadyToRunHelper returns false on wasm (CorInfoImpl.ReadyToRun.cs, "WebAssembly doesn't use the compact ReadyToRun helpers"), so generic-handle lookups route through the runtime-lookup path (embedGenericHandle → ComputeRuntimeLookupForSharedGenericToken).
  • ProcessDynamicDictionaryLookup (src/coreclr/vm/prestub.cpp) resolves the general (non-MVAR/VAR-fast-path) case as CORINFO_USEHELPER with CORINFO_HELP_RUNTIMEHANDLE_METHOD / _CLASS — a helper call made through the generic-lookup import cell.
  • That import cell is a DelayLoadHelperImport. On wasm its delay-load helper is set to null for GenericLookupSignature, and EncodeData then emits a zero pointer:

https://github.com/dotnet/runtime/blob/main/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/DelayLoadHelperImport.cs

if (factory.Target.Architecture == TargetArchitecture.Wasm32)
{
    if (instanceSignature is GenericLookupSignature)
    {
        // Generic lookups are resolved via eager fixups and don't need import thunks
        _delayLoadHelper = null;
    }
    else
    {
        _delayLoadHelper = factory.WasmImportThunkPortableEntrypoint(this);
    }
}

The comment's assumption is incorrect for method/this-dependent dictionary lookups: they are inherently per-call dynamic (they need the runtime generic-context argument) and cannot be resolved by Module::RunEagerFixups (which only processes import sections flagged Eager and asserts *fixupCell != 0). So the cell stays 0, and the USEHELPER slow path does call_indirect to index 0 → "null function".

Introduced by #127483 (commit 2ac8dc55ec2, "[WASM] Add READYTORUN_FIXUP_InjectStringThunks and R2R thunk infrastructure"), which changed wasm from always using WasmImportThunkPortableEntrypoint to nulling the helper for GenericLookupSignature.

Why it isn't a one-line revert

Simply restoring WasmImportThunkPortableEntrypoint(this) for generic-lookup cells makes crossgen2 itself crash during R2R compilation of any assembly with a generic-dictionary lookup:

System.IndexOutOfRangeException
   at ILCompiler.DependencyAnalysis.ReadyToRun.WasmImportThunk.EmitCode(...)

WasmImportThunk.EmitCode raises the wasm signature to a managed MethodSignature and walks it with an ArgIterator; the generic-lookup helper's hidden generic-context argument (HasGenericContextArg is hardcoded false) makes the offsets[] / wasm-locals arrays mismatch. This is the real reason the cell was nulled — emitting a generic-lookup delay-load thunk is deferred (WASM-TODO) work.

(Verified locally: applied the revert, rebuilt crossgen2-wasm, reproduced the IndexOutOfRangeException, then reverted again.)

Proposed fix

Implement generic-lookup (dynamic dictionary lookup) delay-load thunk support on wasm:

  1. Compiler — WasmImportThunk.EmitCode to handle the generic-context argument in its arg spill/restore loops (and surface HasGenericContextArg for generic-lookup signatures).
  2. VM — a matching delay-load helper that invokes ProcessDynamicDictionaryLookup with the generic context + signature and patches the cell, then wire the appropriate ReadyToRunHelper id for generic lookups.

Alternative: emit the dictionary-lookup slow path inline on wasm without a call through a delay-load cell (larger codegen change).

Relevant files

  • src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/DelayLoadHelperImport.cs
  • src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs
  • src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs (getReadyToRunHelper, embedGenericHandle)
  • src/coreclr/vm/prestub.cpp (ProcessDynamicDictionaryLookup)
  • src/coreclr/vm/ceeload.cpp (Module::RunEagerFixups)

cc @davidwrighton @radekdoulik

Note

This issue was authored with the assistance of GitHub Copilot.

Activity

  1. dotnet-policy-service commented on Jun 24, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
    See info in area-owners.md if you want to be subscribed.

  2. davidwrighton commented on Jun 24, 2026

    @davidwrighton
    Member

    I'm working on this right now.

  3. added this to the Future milestone on Jun 25, 2026
  4. removed
    untriagedNew issue has not been triaged by the area owner
    on Jun 25, 2026
  5. changed the title [-][wasm][coreCLR] R2R generic dictionary lookup traps with ull function (zero import cell for GenericLookupSignature)[/-] [+][mono][amd64] Fix calling-convention mismatch for small straddling vtypes in shared generics[/+] on Jun 25, 2026
  6. changed the title [-][mono][amd64] Fix calling-convention mismatch for small straddling vtypes in shared generics[/-] [+][wasm][coreCLR] R2R generic dictionary lookup traps with ull function (zero import cell for GenericLookupSignature)[/+] on Jun 25, 2026
  7. AndyAyersMS commented on Jun 25, 2026

    @AndyAyersMS
    Member

    With this and other fixes I'm able to get part way into running something where SPC is R2R'd. See #129850.

    Also re-checking JIT subtree pri-1 tests where just the test assemblies are R2R'd. This should fix the 7 or so remaining failures (and there is still one test I need to exclude with > 1000 parameters).

  8. changed the title [-][wasm][coreCLR] R2R generic dictionary lookup traps with ull function (zero import cell for GenericLookupSignature)[/-] [+][wasm][coreCLR] R2R generic dictionary lookup traps with null function (zero import cell for GenericLookupSignature)[/+] on Jun 25, 2026
  9. added a commit that references this issue on Jun 26, 2026
    0bc7eda
  10. added a commit that references this issue on Jul 15, 2026
    fdf6789
  11. locked and limited conversation to collaborators on Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

arch-wasmWebAssembly architectureos-browserBrowser variant of arch-wasm

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions