fitzgen commented on issue #14565:
<details> <summary>Full LLM report</summary>
DRC permanently leaks the array when
ArrayRef::new/new_fixedfails its element type check
Date 2026-10-05 Wasmtime commit 73b04cff3317d1e308866eb24359483ac6116669(main)Host macOS 15.8.1 (Darwin 24.6.0), aarch64-apple-darwinModel Claude Opus 5.5 ( claude-opus-5-5)Features Wasm GC, DeferredReferenceCountingcollector (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 backsArrayRef::new,new_async,new_fixedandnew_fixed_async
(called throughretry_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_arrayerror path at lines 431-434. The uninitialized array is
never rooted, initialized, or freed:
DRC: the block is unreachable, but DRC only frees objects whose
refcount drops to zero, and this object was never on the over-approximated
roots list. SoStore::gcnever reclaims it, and the space is lost for the
life of the store.Copying collector: unaffected, because the unreachable object is simply
not copied.Null collector: never frees anything anyway.
StructRef::new(structref.rs:295before the:346allocation) and
ExnRef::new(exnref.rs:280before:343) type-check before allocating.
Only the array path has the wrong order.Each failing call leaks
len * elem_sizeplus the header. A host that builds
arrays from guest- or user-suppliedVals, 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.rssets up:
a 1 MiB fixed GC heap (
gc_heap_initial_size=gc_heap_reservation=
1 MiB,gc_heap_may_move(false));an
(array (mut i32))type.Each iteration then does three things:
Calls
ArrayRef::new_fixed/ArrayRef::newwithVal::I64elements and
asserts the "type mismatch" error.Runs a full
store.gc(None).- 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 okExpected: every iteration succeeds, as it does for the copying collector.
Suggested fix
Hoist the element type-check loop above
alloc_uninit_array, matching
StructRef::newandExnRef::new. Alternatively, move it inside the closure
whose error arm callsdealloc_uninit_array.</details>
fitzgen opened issue #14565:
ArrayRef::new_from_iterallocates 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 afterStore::gc.ArrayRef::new,new_async,
new_fixedandnew_fixed_asyncare all affected.StructRef::newand
ExnRef::newtype-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_leakExpected Results
The test passes.
Actual Results
Error: iteration 259 Caused by: GC heap out of memory: no capacity for allocation of 4028 bytesVersions and Environment
Wasmtime version or commit:
73b04cff33Operating system: macOS 15.8.1
Architecture: aarch64
fitzgen added the wasm-proposal:gc label to Issue #14565.
alexcrichton closed issue #14565:
ArrayRef::new_from_iterallocates 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 afterStore::gc.ArrayRef::new,new_async,
new_fixedandnew_fixed_asyncare all affected.StructRef::newand
ExnRef::newtype-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_leakExpected Results
The test passes.
Actual Results
Error: iteration 259 Caused by: GC heap out of memory: no capacity for allocation of 4028 bytesVersions and Environment
Wasmtime version or commit:
73b04cff33Operating system: macOS 15.8.1
Architecture: aarch64
Last updated: Oct 11 2026 at 02:20 UTC