dotnet/runtime · error · Error

External roots are not supported when threads are enabled

Error message

External roots are not supported when threads are enabled

What it means

Thrown by mono_wasm_new_root_buffer when both WasmEnableThreads is true and runtimeHelpers.disableManagedTransition is set. External/root buffer allocations are unsafe in the threaded runtime because GC coordination across threads differs, so the API is blocked in that configuration. The message variant here ('when threads are enabled') is specific to the root-buffer factory.

Source

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

import { MonoObjectRef, MonoObjectRefNull, MonoObject, is_nullish, WasmRoot, WasmRootBuffer } from "./types/internal";
import { _zero_region, free, localHeapViewU32, malloc } from "./memory";
import { gc_locked } from "./gc-lock";

const maxScratchRoots = 8192;
let _scratch_root_buffer: WasmRootBuffer | null = null;
let _scratch_root_free_indices: Int32Array | null = null;
let _scratch_root_free_indices_count = 0;
const _scratch_root_free_instances: WasmRoot<any>[] = [];
const _external_root_free_instances: WasmExternalRoot<any>[] = [];

/**
 * Allocates a block of memory that can safely contain pointers into the managed heap.
 * The result object has get(index) and set(index, value) methods that can be used to retrieve and store managed pointers.
 * Once you are done using the root buffer, you must call its release() method.
 * For small numbers of roots, it is preferable to use the mono_wasm_new_root and mono_wasm_new_roots APIs instead.
 */
export function mono_wasm_new_root_buffer (capacity: number, name?: string): WasmRootBuffer {
    if (WasmEnableThreads && runtimeHelpers.disableManagedTransition) throw new Error("External roots are not supported when threads are enabled");
    if (capacity <= 0)
        throw new Error("capacity >= 1");

    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().

View on GitHub (pinned to 60108ba66e)

Solutions

  1. Switch to APIs that are valid in threaded mode - avoid external/root-buffer allocation; use the managed interop handle APIs instead.
  2. Run with the single-threaded Wasm build (WasmEnableThreads=false) if external root buffers are required.
  3. Ensure you are not constructing root buffers from raw JS interop code; let the generated bindings/manage handles do it.

Example fix

// before (throws in threaded build)
const buf = mono_wasm_new_root_buffer(16);
// after - use handle-based interop in threaded mode, or run single-threaded build
Defensive patterns

Strategy: validation

Validate before calling

import WasmEnableThreads from 'consts:wasmEnableThreads';
import { runtimeHelpers } from './globals';
function canUseRootBuffer(): boolean {
  return !(WasmEnableThreads && runtimeHelpers.disableManagedTransition);
}
if (canUseRootBuffer()) { const buf = mono_wasm_new_root_buffer(capacity); }

Prevention

When it happens

Trigger: Calling mono_wasm_new_root_buffer (the API that allocates a GC-visible root block) while running a multithreaded dotnet Wasm build with disableManagedTransition active. The combined flag check fails on line 27.

Common situations: Migrating code from the single-threaded runtime to the threaded (pthreads) build; running tests under the threaded configuration that previously called the root-buffer API; a dependency that allocates root buffers being used with threads enabled.

Related errors


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