Repository navigation
Description
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;
}
#endifSo 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); // unguardedEECodeInfo::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
-
Missing validity guard (latent on all platforms).
SendMethodEventandSendMethodILToNativeMapEventuseEECodeInfowithout checkingIsValid(). Addressed by adding the guard; the affected method is then skipped rather than killing the runtime. -
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/MethodDCEndevents for all framework code. Two candidates, not yet distinguished:- R2R code ranges are not registered with
ExecutionManageron wasm (ReadyToRunJitManagerrange 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.
- R2R code ranges are not registered with
Impact
Two conditions must both hold:
- the session requests method rundown (
NgenMethodDCEndetc.), and - the module is R2R.
Observed in Wasm.Build.Tests.Blazor.EventPipeDiagnosticsTests:
BlazorEventPipeTestWithHeapDump— traps. In CI the promise returned bycollectGcDumpnever 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.TestsOr 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=falsedoes not re-stage the assemblies and still serves the R2R images, giving a false negative. Deleteobj/andbin/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.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
pavelsavara commentedon Aug 17, 2026
Adding the
EECodeInfo::IsValid()guard (item 1) is enough to stop the trap, and with it:Wasm.Build.Tests.Blazor.EventPipeDiagnosticsTestsBlazorEventPipeTestWithMetricsBlazorEventPipeTestWithHeapDumpBlazorEventPipeTestWithCpuSamples(Debug/Release, aot: False)So the heap-dump test does not need disabling once the guard is in. Only
BlazorEventPipeTestWithCpuSamplesis skipped against this issue, and it fails differently — not a trap, but a missing-data assertion: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.IncrementCountis 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.
dotnet-policy-service commentedon Aug 17, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
[browser][CoreCLR] Resolve ReadyToRun addresses for EventPipe (#134026)
[browser][CoreCLR] Resolve ReadyToRun addresses for EventPipe (dotnet…