Stream: git-wasmtime

Topic: wasmtime / issue #14565 DRC leaks the array when `ArrayRe...


view this post on Zulip Wasmtime GitHub notifications bot (Oct 06 2026 at 14:57):

fitzgen commented on issue #14565:

<details> <summary>Full LLM report</summary>

DRC permanently leaks the array when ArrayRef::new / new_fixed fails its element type check

Date 2026-10-05
Wasmtime commit 73b04cff3317d1e308866eb24359483ac6116669 (main)
Host macOS 15.8.1 (Darwin 24.6.0), aarch64-apple-darwin
Model Claude Opus 5.5 (claude-opus-5-5)
Features Wasm GC, DeferredReferenceCounting collector (gc-drc)
Class GC-heap leak through the public host API (DoS of a bounded heap)

Summary

ArrayRef::new_from_iter (crates/wasmtime/src/runtime/gc/enabled/arrayref.rs:390-438),
which backs ArrayRef::new, new_async, new_fixed and new_fixed_async
(called through retry_after_gc_async), allocates the
array before it type-checks the elements:

let arrayref = store.require_gc_store_mut()?
    .alloc_uninit_array(allocator.type_index(), len, allocator.layout())  // :404-408
    ...;
for elem in elems.clone() {
    elem.ensure_matches_ty(store, allocator.ty.element_type().unpack())
        .context("element type mismatch")?;                              // :411-414
}
// "From this point on, if we get any errors ... eagerly deallocate it"

A type mismatch returns through the ? at line 413. That is before the
dealloc_uninit_array error path at lines 431-434. The uninitialized array is
never rooted, initialized, or freed:

StructRef::new (structref.rs:295 before the :346 allocation) and
ExnRef::new (exnref.rs:280 before :343) type-check before allocating.
Only the array path has the wrong order.

Each failing call leaks len * elem_size plus the header. A host that builds
arrays from guest- or user-supplied Vals, where a wrong type is an ordinary
recoverable error, exhausts a bounded GC heap after a few hundred failures.

Reproduction

crate/src/bin/array_typecheck_leak.rs sets up:

Each iteration then does three things:

  1. Calls ArrayRef::new_fixed/ArrayRef::new with Val::I64 elements and
    asserts the "type mismatch" error.

  2. Runs a full store.gc(None).

  3. Allocates a correctly typed 1000-element array inside a RootScope.
$ cd reports/015-arrayref-typecheck-leak/crate
$ cargo run --bin array_typecheck_leak            # same result with --release
  DeferredReferenceCounting: good allocation failed after 259 failed ones: GC heap out of memory: no capacity for allocation of 4028 bytes
  Copying: 10000 iterations ok

Expected: every iteration succeeds, as it does for the copying collector.

Suggested fix

Hoist the element type-check loop above alloc_uninit_array, matching
StructRef::new and ExnRef::new. Alternatively, move it inside the closure
whose error arm calls dealloc_uninit_array.

</details>

view this post on Zulip Wasmtime GitHub notifications bot (Oct 06 2026 at 14:59):

fitzgen opened issue #14565:

ArrayRef::new_from_iter allocates the array before it type-checks the
elements. If the check fails, the function returns before reaching the code
that deallocates the uninitialized array. The DRC collector never reclaims
that block, not even after Store::gc. ArrayRef::new, new_async,
new_fixed and new_fixed_async are all affected. StructRef::new and
ExnRef::new type-check before allocating, so they don't leak.

Test Case

#[test]
#[cfg_attr(miri, ignore)]
fn drc_array_new_type_mismatch_does_not_leak() -> Result<()> {
    let mut config = Config::new();
    config.collector(Collector::DeferredReferenceCounting);
    config.gc_heap_initial_size(1 << 20);
    config.gc_heap_reservation(1 << 20);
    config.gc_heap_may_move(false);
    let engine = Engine::new(&config)?;
    let mut store = Store::new(&engine, ());

    let ty = ArrayType::new(
        &engine,
        FieldType::new(Mutability::Var, StorageType::ValType(ValType::I32)),
    );
    let pre = ArrayRefPre::new(&mut store, ty);

    for i in 0..1000 {
        // Wrong element type: fails with "element type mismatch", as expected.
        assert!(ArrayRef::new(&mut store, &pre, &Val::I64(0), 1000).is_err());
        store.gc(None)?;

        // A well-typed, immediately-dropped allocation should always fit.
        let mut scope = RootScope::new(&mut store);
        ArrayRef::new(&mut scope, &pre, &Val::I32(0), 1000)
            .map_err(|e| e.context(format!("iteration {i}")))?;
    }
    Ok(())
}

Steps to Reproduce

Add the test above to tests/all/gc.rs, then run:

cargo test --test all -- drc_array_new_type_mismatch_does_not_leak

Expected Results

The test passes.

Actual Results

Error: iteration 259

Caused by:
    GC heap out of memory: no capacity for allocation of 4028 bytes

Versions and Environment

Wasmtime version or commit: 73b04cff33

Operating system: macOS 15.8.1

Architecture: aarch64

view this post on Zulip Wasmtime GitHub notifications bot (Oct 06 2026 at 14:59):

fitzgen added the wasm-proposal:gc label to Issue #14565.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 06 2026 at 23:19):

alexcrichton closed issue #14565:

ArrayRef::new_from_iter allocates the array before it type-checks the
elements. If the check fails, the function returns before reaching the code
that deallocates the uninitialized array. The DRC collector never reclaims
that block, not even after Store::gc. ArrayRef::new, new_async,
new_fixed and new_fixed_async are all affected. StructRef::new and
ExnRef::new type-check before allocating, so they don't leak.

Test Case

#[test]
#[cfg_attr(miri, ignore)]
fn drc_array_new_type_mismatch_does_not_leak() -> Result<()> {
    let mut config = Config::new();
    config.collector(Collector::DeferredReferenceCounting);
    config.gc_heap_initial_size(1 << 20);
    config.gc_heap_reservation(1 << 20);
    config.gc_heap_may_move(false);
    let engine = Engine::new(&config)?;
    let mut store = Store::new(&engine, ());

    let ty = ArrayType::new(
        &engine,
        FieldType::new(Mutability::Var, StorageType::ValType(ValType::I32)),
    );
    let pre = ArrayRefPre::new(&mut store, ty);

    for i in 0..1000 {
        // Wrong element type: fails with "element type mismatch", as expected.
        assert!(ArrayRef::new(&mut store, &pre, &Val::I64(0), 1000).is_err());
        store.gc(None)?;

        // A well-typed, immediately-dropped allocation should always fit.
        let mut scope = RootScope::new(&mut store);
        ArrayRef::new(&mut scope, &pre, &Val::I32(0), 1000)
            .map_err(|e| e.context(format!("iteration {i}")))?;
    }
    Ok(())
}

Steps to Reproduce

Add the test above to tests/all/gc.rs, then run:

cargo test --test all -- drc_array_new_type_mismatch_does_not_leak

Expected Results

The test passes.

Actual Results

Error: iteration 259

Caused by:
    GC heap out of memory: no capacity for allocation of 4028 bytes

Versions and Environment

Wasmtime version or commit: 73b04cff33

Operating system: macOS 15.8.1

Architecture: aarch64


Last updated: Oct 11 2026 at 02:20 UTC