dotnet / dotnet/runtime

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

Closed
#132,410 2 comments 0 reactions 1 assignee Claimed by @pavelsavara View on GitHub
arch-wasm area-ReadyToRun os-browser
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

Depends on https://github.com/dotnet/runtime/issues/130521

## Description

With ReadyToRun enabled for CoreCLR `browser-wasm` (dotnet/runtime#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:

```cpp
#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:

```cpp
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):

```powershell
$env:EMSDK_PATH="\.dotnet\wasm-tools\emscripten\-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:

```js
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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.