dotnet/runtime · error · Error

External roots are not supported in multithreaded mode

Error message

External roots are not supported in multithreaded mode

What it means

Thrown by mono_wasm_new_external_root when Wasm threads are enabled and disableManagedTransition is on. External roots wrap an externally-controlled native address (typically on the managed stack); that pattern is incompatible with the threaded runtime's GC coordination, so the API refuses to create one. The message variant 'in multithreaded mode' distinguishes it from the root-buffer guard.

Source

Thrown at src/mono/browser/runtime/roots.ts:48

    capacity = capacity | 0;

    const capacityBytes = capacity * 4;
    const offset = malloc(capacityBytes);
    if ((<any>offset % 4) !== 0)
        throw new Error("Malloc returned an unaligned offset");

    _zero_region(offset, capacityBytes);

    return new WasmRootBufferImpl(offset, capacity, true, name);
}

/**
 * Allocates a WasmRoot pointing to a root provided and controlled by external code. Typicaly on managed stack.
 * Releasing this root will not de-allocate the root space. You still need to call .release().
 */
export function mono_wasm_new_external_root<T extends MonoObject> (address: VoidPtr | MonoObjectRef): WasmRoot<T> {
    if (WasmEnableThreads && runtimeHelpers.disableManagedTransition) throw new Error("External roots are not supported in multithreaded mode");
    let result: WasmExternalRoot<T>;

    if (!address)
        throw new Error("address must be a location in the native heap");

    if (_external_root_free_instances.length > 0) {
        result = _external_root_free_instances.pop()!;
        result._set_address(address);
    } else {
        result = new WasmExternalRoot<T>(address);
    }

    return result;
}

/**
 * Allocates temporary storage for a pointer into the managed heap.
 * Pointers stored here will be visible to the GC, ensuring that the object they point to aren't moved or collected.

View on GitHub (pinned to 60108ba66e)

Solutions

  1. Avoid mono_wasm_new_external_root in threaded builds; use managed-transition-safe handle APIs.
  2. Run with the single-threaded build if external roots are mandatory for the interop scenario.
  3. Refactor the caller to obtain GC roots through mono_wasm_new_root / scratch roots that are compatible with the runtime mode.
Defensive patterns

Strategy: validation

Validate before calling

import WasmEnableThreads from 'consts:wasmEnableThreads';
import { runtimeHelpers } from './globals';
function canUseExternalRoot(): boolean {
  return !(WasmEnableThreads && runtimeHelpers.disableManagedTransition);
}

Prevention

When it happens

Trigger: Calling mono_wasm_new_external_root(address) in a threaded dotnet Wasm build with disableManagedTransition active. The threaded-mode guard at line 48 fires before the address is even inspected.

Common situations: Code that wraps stack/native addresses as roots, ported to the threaded runtime; an interop layer that previously used external roots now running with pthreads enabled; tests targeting the threaded configuration hitting legacy binding paths.

Related errors


AI-assisted analysis of dotnet/runtime@60108ba66e (2026-08-10). Data as JSON: /api/errors/8d8308b47f9fe252. Report an issue: GitHub.