dotnet / dotnet/runtime

[browser][coreCLR] Assembly resolution has no VFS fallback — private/copy-local assemblies fail to load (FileNotFoundException) where Mono succeeds

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

Description

### Description

On `browser-wasm` **CoreCLR** (browserhost), loading a managed assembly that is present on the emscripten VFS but not eagerly registered from the boot config fails with `FileNotFoundException`. The same code works on **Mono** WASM, because Mono's assembly loader can probe the VFS filesystem while the CoreCLR browserhost probe is an in-memory-only lookup.

Surfaced by `System.Runtime.Serialization.Formatters` in `System.Runtime.Tests` (and others) on CoreCLR:

```
System.IO.FileNotFoundException : Could not load file or assembly
'System.Runtime.Serialization.Formatters, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.
The system cannot find the file specified.
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
```

This assembly is a **private, copy-local** reference (a `NetCoreAppMinimum` build with a functional `BinaryFormatter`) added by the test project, not part of the shared framework:

```xml

```

### Affected tests (browser CoreCLR, Release)

Observed on the `LibraryTestsCoreCLR` legs of the R2R bring-up PR #129634 (build [1551759](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1551759), head `d3f76ed6`):

- `System.Tests.DateTimeTests.GetObjectData_Invoke_ReturnsExpected`
- `System.Formats.Nrbf.Tests.EdgeCaseTests.*` (fails during xUnit **discovery** — `CustomAttributeFormatException` -> `FileNotFoundException`)
- `System.Collections.Tests.InternalHashCodeTests_*.ComparerImplementations_Dictionary_WithWellKnownStringComparers` (×16 fixtures)
- `System.Collections.Tests.HashSet_NonGeneric_Tests.HashSet_CopyConstructor_ShouldWorkWithRandomizedEffectiveComparer`

### Root cause

**CoreCLR browserhost resolves *every* managed assembly through `external_assembly_probe`, which is an in-memory `Map` with no filesystem/VFS fallback.**

The host wires `BrowserHost_ExternalAssemblyProbe` as `host_contract.external_assembly_probe` (`src/native/corehost/browserhost/browserhost.cpp`). Its implementation (`src/native/libs/Common/JavaScript/host/assets.ts`) only consults `loadedAssemblies`, which is populated exclusively from boot-config assets (`finishWebcilInstance` — "this loader receives them from boot config"):

```ts
export function BrowserHost_ExternalAssemblyProbe(pathPtr, outDataStartPtr, outSize): boolean {
const path = _ems_.UTF8ArrayToString(_ems_.dotnetApi.localHeapViewU8(), pathPtr);
const assembly = loadedAssemblies.get(path); // in-memory map only
if (assembly) { /* return ptr + length */ return true; }
_ems_.dotnetLogger.debug(`Assembly not found: '${path}'`);
return false; // -> FileNotFoundException
}
```

There is no `FS`/VFS read anywhere in the CoreCLR probe path. If an assembly is not in the eagerly-registered boot-config set, CoreCLR cannot load it even when the file physically exists on the emscripten VFS.

### Why Mono succeeds (comparison)

Mono's asset loader (`src/mono/browser/runtime/assets.ts`) writes assets to the **emscripten VFS** as real files in addition to any in-memory registration:

```ts
case "vfs": {
...
Module.FS_createPath("/", parentDirectory, true, true);
Module.FS_createDataFile(parentDirectory, fileName, bytes, /*canRead*/ true, /*canWrite*/ true, /*canOwn*/ true);
break;
}
// and, for behavior === "assembly":
cwraps.mono_wasm_add_assembly(virtualName, offset, bytes.length);
```

The app base is a VFS directory (`FS.chdir(virtualWorkingDirectory)` in `src/mono/browser/runtime/startup.ts`), so Mono's native assembly loader resolves an assembly that isn't in the eager in-memory set by **probing the VFS filesystem** under the app base. That is what makes `System.Runtime.Serialization.Formatters` load on Mono but not on CoreCLR.

### Candidate fixes

1. **Bundling parity** — ensure the CoreCLR WASM publish registers private/copy-local assemblies as eager `"assembly"` boot-config assets (so they land in `loadedAssemblies`), matching what Mono's in-memory path gets. This keeps the runtime probe in-memory-only.
2. **Runtime fallback** — on a `loadedAssemblies` miss, have `BrowserHost_ExternalAssemblyProbe` read the file from the emscripten FS at the probed path, mirroring Mono's filesystem probing.

Open question to confirm before picking a fix: whether the private copy is present on the VFS-but-not-in-boot-config (favor fix 1 or 2), or missing from the publish bundle entirely (bundling gap). This can be checked from the `_framework` boot manifest of a CoreCLR test bundle.

### Configuration

- `-os browser -a wasm`, CoreCLR (browserhost), Release; reproduces in V8, Firefox, and Chrome.
- Not R2R-specific — it's an assembly-resolution/probe limitation of the CoreCLR browser host.

### Regression?

No — this is a gap in the new CoreCLR-on-browser host rather than a regression from a previously working CoreCLR configuration. Tracking so the CoreCLR wasm library test legs can match Mono coverage.

> [!NOTE]
> This issue was researched and drafted with the assistance of GitHub Copilot.

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.