[mono][wasm] Type loader aborts (object.c:8000) / false "Recursive type definition detected" on a struct with a static ValueTask<Self> field
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Description
On the Mono/WebAssembly runtime, loading `ValueTask` aborts when `T` is a value type that declares a **`static` field of type `ValueTask`** (i.e. `T` statically references `ValueTask`, whose instance layout contains a `T`). The runtime dies with a native assertion:
```
[MONO] * Assertion at /__w/1/s/src/runtime/src/mono/mono/metadata/object.c:8000, condition `' not met
```
In some (larger / Release) builds the same root cause surfaces instead as a managed exception:
```
System.TypeLoadException: Recursive type definition detected System.Threading.Tasks.ValueTask`1
```
The type is **not** genuinely recursive: a `static` field does not participate in a type's instance layout, and `T` here is a `readonly record struct` of `Size = 1`, so `ValueTask` has a well-defined finite layout. **CoreCLR loads and runs the identical IL without any error.** This is the same class of false positive as #85821 ("Mono incorrectly throws a TypeLoadException for a recursive type definition"), which was fixed for the `T[]` array path by #115278 ("[mono][metadata] Defer class init for T when initializing `T[]`"). That fix does not cover this case: a **static field whose type is a generic struct that transitively references the declaring type**.
The offending type (a MediatR/MediatorLite-style `Unit`):
```csharp
[StructLayout(LayoutKind.Sequential, Size = 1)]
public readonly record struct Unit : IEquatable, IComparable
{
public static readonly Unit Value = default;
// Get-only auto-property with initializer => static ValueTask backing field inside Unit:
// static ValueTask k__BackingField
// -> Unit statically references ValueTask, whose _result field is a Unit.
public static ValueTask CompletedTask { get; } = ValueTask.FromResult(Value);
}
```
The abort is a **native runtime assertion, not a catchable managed exception** — a surrounding `try/catch` does not catch it, so a Blazor WebAssembly app that loads this type simply dies. This is not academic: `Unit`/`ValueTask` result types are used by MediatR-style mediators (this was found via MediatorLite v2, whose source-generated mediator returns `ValueTask`), so any request dispatched through such a mediator aborts a Blazor WASM client at startup.
**The failure is load-order dependent.** If `ValueTask` is first loaded via a benign path (e.g. touching `Unit.CompletedTask` directly) *before* the path that trips the detector, the type loads successfully and the crash is masked for the rest of the process. The minimal repro therefore loads `ValueTask` first via the mediator dispatch and does not touch `Unit.CompletedTask` beforehand.
### Reproduction Steps
Minimal, dependency-free repro (two projects sharing the same types): **https://github.com/SolemnDucc/mono-wasm-recursive-typeload-repro**
1. `dotnet run --project BlazorWasm` and open the URL in a Chromium/Chrome browser.
- Hand-written `ValueTask` probes (direct, `async`, and the `Unsafe.As,ValueTask>` reinterpret) render `OK`.
- The first dispatch that loads `ValueTask` aborts the runtime; the page freezes and the console shows the `object.c:8000` assertion.
2. `dotnet run --project CoreClr` runs the **same** types on CoreCLR and prints every dispatch (and a direct `Unit.CompletedTask` load) as `OK`, ending `ALL OK on CoreCLR (.NET 10.x)`.
Same IL, opposite outcomes.
Smallest form of the trigger:
```csharp
[StructLayout(LayoutKind.Sequential, Size = 1)]
public readonly struct Poison
{
public static readonly Poison Value = default;
public static ValueTask Completed { get; } = ValueTask.FromResult(Value);
}
// First instantiation of ValueTask on Mono/WASM aborts the runtime.
```
### Expected behavior
`ValueTask` loads and is usable, exactly as on CoreCLR (a `static` field is not part of the declaring type's instance layout, so there is no recursive instance layout). No `TypeLoadException` and, above all, no native assertion abort.
### Actual behavior
The Mono/WASM type loader aborts with `[MONO] * Assertion at .../mono/metadata/object.c:8000, condition `' not met` (interpreter, Debug and Release published), or throws `TypeLoadException: Recursive type definition detected System.Threading.Tasks.ValueTask`1` in some builds. The assertion is uncatchable and kills the app.
### Regression?
Not a regression — the `TypeLoadException` false positive for recursive type definitions on Mono is long-standing (#85821). The array (`T[]`) case was fixed in 8.0 by #115278; this static-field-of-generic-struct case was not covered and still reproduces on the latest stable runtime.
### Known Workarounds
Avoid declaring a `static` field whose type is a generic struct that references the declaring type. For the `Unit` case, make `CompletedTask` a computed (expression-bodied) property so no static backing field is emitted:
```csharp
public static ValueTask CompletedTask => ValueTask.FromResult(Value);
```
(There is no application-level workaround when the offending type lives in a third-party package; it must be fixed at the type's source.)
### Configuration
- **.NET SDK** 10.0.103
- **Mono/WASM runtime pack** `Microsoft.NETCore.App.Runtime.Mono.browser-wasm` — reproduced on **10.0.8** (SDK default) and **10.0.9** (latest stable, pinned via `RuntimeFrameworkVersion`).
- **Microsoft.AspNetCore.Components.WebAssembly** 10.0.9
- `PublishTrimmed=false`, `PublishAot=false` — plain Mono interpreter (not a linker/AOT artifact).
- Host OS: Linux x64; browser: Chromium/Chrome. The failing runtime is the browser-side Mono/WASM interpreter, so the host OS should not matter.
- CoreCLR control: .NET 10.0.8 — no error.
### Other information
- Likely related to the recursion detection in `mono/metadata/object.c` (the same area addressed by #85821 / #115278). The `T[]`-path fix deferred class init for `T` when initializing `T[]`; the analogous deferral seems to be missing when a `static` field's type is a generic struct instantiation that closes back over the declaring type.
- Beyond the false positive, a type loader aborting on a **native assertion** for valid managed IL (that CoreCLR accepts) is itself worth addressing — even the "correct" diagnosis should be a managed `TypeLoadException`, not an uncatchable runtime abort.
Contributor guide
Assessment
This issue has not been assessed yet.