Stream: git-wasmtime

Topic: wasmtime / issue #13417 `trace_info` stale cache can lead...


view this post on Zulip Wasmtime GitHub notifications bot (May 20 2026 at 05:28):

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 gc is marked tier 2, we are directly reporting this bug on GitHub.

Overview

DrcHeap caches GC tracing metadata in trace_infos, keyed by VMSharedTypeIndex. When a pooled DRC heap is detached, it intentionally preserves that cache because the heap will only be reused with the same Engine (described in code comment). However, the same-engine assumption is not sufficient: when a module's TypeCollection is dropped, Wasmtime unregisters its rec groups and returns their shared type-index slab entries to the registry. A later module on the same Engine can then assign the same VMSharedTypeIndex to a different GC type. If that happens, DrcHeap::ensure_trace_info sees 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_pod reads beyond the new object's data and panics with out 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 thread directive is a PoC mechanism, not a requirement for triggering the bug. It was used for creating a child Store on the same Engine, 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 its trace_infos map 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 via VMGcObjectData::read_u32, which panics with out 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=y

Environment

$ 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_infoDrcHeap::detach

Bug path: the type registry can recycle the cache key

TypeCollection::dropTypeRegistryInner::unregister_type_collectionTypeRegistryInner::remove_entry_implTypeRegistryInner::remove_entry_types / Slab::deallocTypeRegistryInner::assign_shared_type_indices / Slab::alloc

Recommendation

Possible mitigations for this specific pattern include:

  1. Clear or invalidate DRC trace_infos when a pooled heap is detached or reassigned.
  2. Key cached trace metadata by a non-recycled type identity, not only VMSharedTypeIndex.
  3. Revalidate cached entries in ensure_trace_info against the current TypeRegistry layout 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.

view this post on Zulip Wasmtime GitHub notifications bot (May 20 2026 at 05:28):

qwaz added the bug label to Issue #13417.

view this post on Zulip Wasmtime GitHub notifications bot (May 20 2026 at 05:29):

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 gc is marked tier 2, we are directly reporting this bug on GitHub.

Overview

DrcHeap caches GC tracing metadata in trace_infos, keyed by VMSharedTypeIndex. When a pooled DRC heap is detached, it intentionally preserves that cache because the heap will only be reused with the same Engine (described in code comment). However, the same-engine assumption is not sufficient: when a module's TypeCollection is dropped, Wasmtime unregisters its rec groups and returns their shared type-index slab entries to the registry. A later module on the same Engine can then assign the same VMSharedTypeIndex to a different GC type. If that happens, DrcHeap::ensure_trace_info sees 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_pod reads beyond the new object's data and panics with out 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 thread directive is a PoC mechanism, not a requirement for triggering the bug. It was used for creating a child Store on the same Engine, 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 its trace_infos map 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 via VMGcObjectData::read_u32, which panics with out 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=y

Environment

$ 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_infoDrcHeap::detach

Bug path: the type registry can recycle the cache key

TypeCollection::dropTypeRegistryInner::unregister_type_collectionTypeRegistryInner::remove_entry_implTypeRegistryInner::remove_entry_types / Slab::deallocTypeRegistryInner::assign_shared_type_indices / Slab::alloc

Recommendation

Possible mitigations for this specific pattern include:

  1. Clear or invalidate DRC trace_infos when a pooled heap is detached or reassigned.
  2. Key cached trace metadata by a non-recycled type identity, not only VMSharedTypeIndex.
  3. Revalidate cached entries in ensure_trace_info against the current TypeRegistry layout 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.

view this post on Zulip Wasmtime GitHub notifications bot (May 20 2026 at 14:49):

fitzgen assigned fitzgen to issue #13417.

view this post on Zulip Wasmtime GitHub notifications bot (May 20 2026 at 14:49):

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

view this post on Zulip Wasmtime GitHub notifications bot (May 20 2026 at 15:12):

fitzgen commented on issue #13417:

Thanks for the bug report and thanks for double checking the security policy before reporting publicly!

view this post on Zulip Wasmtime GitHub notifications bot (May 20 2026 at 22:29):

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 gc is marked tier 2, we are directly reporting this bug on GitHub.

Overview

DrcHeap caches GC tracing metadata in trace_infos, keyed by VMSharedTypeIndex. When a pooled DRC heap is detached, it intentionally preserves that cache because the heap will only be reused with the same Engine (described in code comment). However, the same-engine assumption is not sufficient: when a module's TypeCollection is dropped, Wasmtime unregisters its rec groups and returns their shared type-index slab entries to the registry. A later module on the same Engine can then assign the same VMSharedTypeIndex to a different GC type. If that happens, DrcHeap::ensure_trace_info sees 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_pod reads beyond the new object's data and panics with out 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 thread directive is a PoC mechanism, not a requirement for triggering the bug. It was used for creating a child Store on the same Engine, 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 its trace_infos map 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 via VMGcObjectData::read_u32, which panics with out 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=y

Environment

$ 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_infoDrcHeap::detach

Bug path: the type registry can recycle the cache key

TypeCollection::dropTypeRegistryInner::unregister_type_collectionTypeRegistryInner::remove_entry_implTypeRegistryInner::remove_entry_types / Slab::deallocTypeRegistryInner::assign_shared_type_indices / Slab::alloc

Recommendation

Possible mitigations for this specific pattern include:

  1. Clear or invalidate DRC trace_infos when a pooled heap is detached or reassigned.
  2. Key cached trace metadata by a non-recycled type identity, not only VMSharedTypeIndex.
  3. Revalidate cached entries in ensure_trace_info against the current TypeRegistry layout 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