[browser][coreCLR] Assembly resolution has no VFS fallback — private/copy-local assemblies fail to load (FileNotFoundException) where Mono succeeds
- 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
Assessment
This issue has not been assessed yet.