Skip to content

[browser][coreCLR] R2R: EventPipe rundown traps with null function and omits precompiled method events #132410

Closed

Description

@pavelsavara

Depends on #130521

Description

With ReadyToRun enabled for CoreCLR browser-wasm (#132339), stopping an EventPipe session that requests method rundown traps the runtime:

DOTNET: Unhandled error:  null function
RuntimeError: null function
    at dotnet.native.wasm:wasm-function[1470]:0x7f04d
    at dotnet.native.wasm:wasm-function[3180]:0x14108f
    at dotnet.native.wasm:wasm-function[3182]:0x1418d4
    at dotnet.native.wasm:wasm-function[1519]:0x880af
    at dotnet.native.wasm:wasm-function[5522]:0x26c703
    at dotnet.native.wasm:wasm-function[7782]:0x2d01f4
    at dotnet.native.js:1:69544
    at callUserCallback (dotnet.native.js:1:109091)

Symbolicated via dotnet.native.js.symbols (innermost first):

index symbol
1470 ETW::MethodLog::SendMethodEvent(MethodDesc*, ...)
3180 ETW::EnumerationLog::IterateModule(Module*, unsigned int)
3182 ETW::EnumerationLog::IterateAppDomain(unsigned int)
1519 stop_session(unsigned long long)
5522 server_loop_tick(void*)
7782 SystemJS_ExecuteDiagnosticServerCallback

After the trap the runtime is dead — every subsequent call reports Assert failed: The runtime is not running.

Root cause

IterateModule calls ETW::MethodLog::SendEventsForNgenMethods (inlined in the stack above), whose entire body is gated on the module being R2R:

#ifdef FEATURE_READYTORUN
    if (pModule->IsReadyToRun())
    {
        ReadyToRunInfo::MethodIterator mi(pModule->GetReadyToRunInfo());
        while (mi.Next())
        {
            // Call GetMethodDesc_NoRestore instead of GetMethodDesc to avoid restoring methods at shutdown.
            MethodDesc *hotDesc = (MethodDesc *)mi.GetMethodDesc_NoRestore();
            if (hotDesc != NULL)
                ETW::MethodLog::SendMethodEvent(hotDesc, dwEventOptions, FALSE);
        }
        return;
    }
#endif

So this path is unreachable without R2R, which is why the failure appeared only once R2R was turned on.

SendMethodEvent is called with pNativeCodeStartAddress == NULL, so it uses pMethodDesc->GetNativeCode(), then:

TADDR start = MethodAndStartAddressToEECodeInfoPointer(pMethodDesc, pNativeCodeStartAddress);
if (start == 0) return;          // only guards a null address
EECodeInfo codeInfo(start);
codeInfo.GetMethodRegionInfo(&methodRegionInfo);   // unguarded

EECodeInfo::Init takes its Invalid: path when ExecutionManager::FindCodeRange (or JitCodeToMethodInfo) fails, leaving m_pJM = NULL. GetMethodRegionInfo then does GetJitManager()->JitTokenToMethodRegionInfo(...) — a virtual dispatch through a null pointer. On x64 that is a null-deref AV; on wasm a call_indirect through an empty function-table slot is reported as RuntimeError: null function.

EECodeInfo::IsValid() is the established guard for exactly this and is used in ~37 places across the VM, including neighbours making the identical call (perfmap.cpp, gccover.cpp). eventtrace.cpp had zero uses of it.

Two separable problems

  1. Missing validity guard (latent on all platforms). SendMethodEvent and SendMethodILToNativeMapEvent use EECodeInfo without checking IsValid(). Addressed by adding the guard; the affected method is then skipped rather than killing the runtime.

  2. R2R code addresses do not resolve on wasm (the actual gap). With the guard in place the crash disappears, but every R2R-precompiled method is silently omitted from the rundown, so traces are missing MethodLoad/MethodDCEnd events for all framework code. Two candidates, not yet distinguished:

    • R2R code ranges are not registered with ExecutionManager on wasm (ReadyToRunJitManager range registration missing/incomplete), or
    • GetNativeCode() for an unrestored R2R method on wasm returns something that is not a code address at all (plausibly a function-table index), which can never resolve through a range lookup.

Impact

Two conditions must both hold:

  1. the session requests method rundown (NgenMethodDCEnd etc.), and
  2. the module is R2R.

Observed in Wasm.Build.Tests.Blazor.EventPipeDiagnosticsTests:

  • BlazorEventPipeTestWithHeapDump — traps. In CI the promise returned by collectGcDump never settles, so the work item ran until the 90-minute Helix executor kill.
  • BlazorEventPipeTestWithMetrics — passes on the same R2R app; its session does not request method rundown, confirming condition (1) is required.

Reproduction

Local, with R2R on (the default for CoreCLR browser-wasm):

$env:EMSDK_PATH="<repo>\.dotnet\wasm-tools\emscripten\<ver>-windows-x64"
.\dotnet.cmd build /p:TargetOS=browser /p:TargetArchitecture=wasm /p:Configuration=Release `
  /p:RuntimeFlavor=CoreCLR /t:Test `
  /p:XUnitMethodName=Wasm.Build.Tests.Blazor.EventPipeDiagnosticsTests.BlazorEventPipeTestWithHeapDump `
  src/mono/wasm/Wasm.Build.Tests

Or directly in the browser against a built app:

await globalThis.getDotnetRuntime(0).collectGcDump({ durationSeconds: 5.0, skipDownload: true });

Bisected by rebuilding the same app with R2R off — same app, same dotnet.native.wasm:

build System.Private.CoreLib served collectGcDump
PublishReadyToRun=true 29,098,176 B (R2R image) traps, runtime dead, promise never settles
PublishReadyToRun=false (clean rebuild) 5,622,045 B (IL/webcil) resolves with a valid Nettrace payload

An incremental rebuild with -p:PublishReadyToRun=false does not re-stage the assemblies and still serves the R2R images, giving a false negative. Delete obj/ and bin/ first.

Ask

Item 2 above: make R2R method code addresses resolvable on wasm so rundown emits method events for precompiled code, and re-enable the tests skipped against this issue.

Note

This issue body was generated by GitHub Copilot from a local investigation (symbolication, source analysis and an R2R on/off bisect) and reviewed before posting.

Activity

  1. pavelsavara commented on Aug 17, 2026

    @pavelsavara
    MemberAuthor

    Adding the EECodeInfo::IsValid() guard (item 1) is enough to stop the trap, and with it:

    Wasm.Build.Tests.Blazor.EventPipeDiagnosticsTests before guard after guard
    BlazorEventPipeTestWithMetrics pass pass
    BlazorEventPipeTestWithHeapDump traps / hangs until the Helix timeout pass
    BlazorEventPipeTestWithCpuSamples(Debug/Release, aot: False) never reached in CI fail

    So the heap-dump test does not need disabling once the guard is in. Only BlazorEventPipeTestWithCpuSamples is skipped against this issue, and it fails differently — not a trap, but a missing-data assertion:

    The cpuprofile.nettrace should contain stack frames for the 'Counter.IncrementCount' method
    

    That is item 2 surfacing: with the guard, methods whose code address doesn't resolve are silently omitted from the rundown instead of killing the runtime, so the trace lacks the frames the test asserts on.

    One caveat on scope: unlike the heap-dump failure, this one has not been bisected R2R-on/off. Counter.IncrementCount is an app method and should be JIT-compiled rather than R2R, so it is possible this is a broader CoreCLR-wasm EventPipe symbolication gap rather than strictly an R2R one. Worth confirming before treating the issue as fully scoped.

    The skip is keyed on the runtime flavor (BuildTestBase.IsCoreClrRuntime) because R2R is on by default for CoreCLR browser-wasm and Wasm.Build.Tests has no separate R2R switch.

    Note

    This comment was generated by GitHub Copilot from a local investigation and reviewed before posting.

  2. dotnet-policy-service commented on Aug 17, 2026

    @dotnet-policy-service
    Contributor

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

  3. self-assigned this
    on Aug 17, 2026
  4. added this to the 12.0.0 milestone on Aug 17, 2026
  5. added a commit that references this issue on Sep 16, 2026
    6a06ead
  6. added a commit that references this issue on Sep 18, 2026
    e7a38de
  7. locked and limited conversation to collaborators on Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

  • Status
    No status

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions