alexcrichton opened issue #13023:
The following tests all fail in Wasmtime with debug assertions enabled, and they shouldn't. These should probably return a first-class trap of some kind or have some more validation earlier on. Note that these test cases are all generated and likely want edits before committing.
<details>
<summary>test 1</summary>
;;! component_model_async = true ;;! multi_memory = true (component (core module $libc (memory (export "m") 1) ) (core instance $libc (instantiate $libc)) (type $s (stream)) (core func $stream.new (canon stream.new $s)) (core func $stream.read (canon stream.read $s async (memory $libc "m"))) (core func $stream.write (canon stream.write $s async (memory $libc "m"))) (core module $m (import "" "m" (memory 1)) (import "" "stream.new" (func $stream.new (result i64))) (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) (func (export "run") (local $tmp i64) (local $r i32) (local $w i32) (local.set $tmp (call $stream.new)) (local.set $r (i32.wrap_i64 (local.get $tmp))) (local.set $w (i32.wrap_i64 (i64.shr_u (local.get $tmp) (i64.const 32)))) ;; reader requests a large number of zero-sized items (call $stream.read (local.get $r) (i32.const 0) (i32.const 0x20000000)) i32.const -1 ;; BLOCKED i32.ne if unreachable end ;; writer writes the same large number - triggers encode overflow (call $stream.write (local.get $w) (i32.const 0) (i32.const 0x20000000)) drop ) ) (core instance $i (instantiate $m (with "" (instance (export "m" (memory $libc "m")) (export "stream.new" (func $stream.new)) (export "stream.read" (func $stream.read)) (export "stream.write" (func $stream.write)) )) )) (func (export "run") (canon lift (core func $i "run"))) ) (assert_return (invoke "run"))</details>
<details>
<summary>test 2</summary>
;;! component_model_async = true ;;! reference_types = true ;;! multi_memory = true ;;! gc_types = true ;; Vulnerability: ReturnCode::encode overflow via event delivery path. ;; ;; This demonstrates the same root cause as vuln1 but through the waitable_set_wait ;; event delivery code path. A zero-payload stream read with count >= 2^28 causes ;; the event's ReturnCode to overflow when encoded in Event::parts() during ;; waitable_set_wait, crashing the host. ;; ;; In this test: ;; - Component $C exports an async function that reads from a zero-payload stream ;; with count = 0x10000000 (exactly 2^28), then waits for the result event ;; - Component $D calls $C and writes 0x10000000 items to the stream ;; - When $C receives the event through waitable_set_wait, Event::parts() calls ;; ReturnCode::encode() with n=0x10000000, triggering the debug_assert panic ;; ;; In debug builds: host process crashes with "assertion failed: *n < (1 << 28)" ;; In release builds: the count is silently truncated, corrupting the event payload (component (component $C (core module $Memory (memory (export "mem") 1)) (core instance $memory (instantiate $Memory)) (core module $CM (import "" "mem" (memory 1)) (import "" "task.return" (func $task.return)) (import "" "waitable.join" (func $waitable.join (param i32 i32))) (import "" "waitable-set.new" (func $waitable-set.new (result i32))) (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) (global $ws (mut i32) (i32.const 0)) (global $insr (mut i32) (i32.const 0)) (func $start (global.set $ws (call $waitable-set.new))) (start $start) (func $transform (export "transform") (param $readable i32) (result i32) (local $ret i32) (global.set $insr (local.get $readable)) ;; Read 0x10000000 (2^28) items from a zero-payload stream — should BLOCK (local.set $ret (call $stream.read (global.get $insr) (i32.const 0) (i32.const 0x10000000))) (if (i32.ne (local.get $ret) (i32.const -1)) (then unreachable)) ;; Return nothing, then wait for event via callback (call $task.return) (call $waitable.join (global.get $insr) (global.get $ws)) (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))) ) (func $transform_cb (export "transform_cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) ;; If we get here, the event was delivered without crashing. ;; In debug builds, the host crashes before reaching this point. ;; $event_code should be 2 (STREAM_READ) ;; $payload should contain the encoded ReturnCode, but with 2^28 count ;; it overflows: (0x10000000 << 4) | 0 = 0 (truncated) (call $stream.drop-readable (global.get $insr)) (i32.const 0 (; EXIT ;)) ) ) (type $ST (stream)) (canon task.return (memory $memory "mem") (core func $task.return)) (canon waitable.join (core func $waitable.join)) (canon waitable-set.new (core func $waitable-set.new)) (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) (canon stream.read $ST async (memory $memory "mem") (core func $stream.read)) (canon stream.drop-readable $ST (core func $stream.drop-readable)) (core instance $cm (instantiate $CM (with "" (instance (export "mem" (memory $memory "mem")) (export "task.return" (func $task.return)) (export "waitable.join" (func $waitable.join)) (export "waitable-set.new" (func $waitable-set.new)) (export "waitable-set.wait" (func $waitable-set.wait)) (export "stream.read" (func $stream.read)) (export "stream.drop-readable" (func $stream.drop-readable)) )))) (func (export "transform") (param "in" (stream)) (canon lift (core func $cm "transform") async (memory $memory "mem") (callback (func $cm "transform_cb")) )) ) (component $D (import "transform" (func $transform (param "in" (stream)))) (core module $Memory (memory (export "mem") 1)) (core instance $memory (instantiate $Memory)) (core module $DM (import "" "mem" (memory 1)) (import "" "stream.new" (func $stream.new (result i64))) (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) (import "" "transform" (func $transform (param i32) (result i32))) (func $run (export "run") (local $ret i32) (local $ret64 i64) (local $sr i32) (local $sw i32) ;; Create a zero-payload stream (local.set $ret64 (call $stream.new)) (local.set $sr (i32.wrap_i64 (local.get $ret64))) (local.set $sw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) ;; Call transform, passing the readable end ;; transform returns RETURNED status immediately (local.set $ret (call $transform (local.get $sr))) ;; Write 0x10000000 items — this rendezvous with the reader ;; This causes the reader's event to contain count = 0x10000000 ;; When waitable_set_wait delivers the event, Event::parts() will ;; call ReturnCode::encode() and hit the overflow (local.set $ret (call $stream.write (local.get $sw) (i32.const 0) (i32.const 0x10000000))) ;; Clean up (call $stream.drop-writable (local.get $sw)) ) ) (type $ST (stream)) (canon stream.new $ST (core func $stream.new)) (canon stream.write $ST async (memory $memory "mem") (core func $stream.write)) (canon stream.drop-writable $ST (core func $stream.drop-writable)) (canon lower (func $transform) async (memory $memory "mem") (core func $transform')) (core instance $dm (instantiate $DM (with "" (instance (export "mem" (memory $memory "mem")) (export "stream.new" (func $stream.new)) (export "stream.write" (func $stream.write)) (export "stream.drop-writable" (func $stream.drop-writable)) (export "transform" (func $transform')) )))) (func (export "run") (canon lift (core func $dm "run"))) ) (instance $c (instantiate $C)) (instance $d (instantiate $D (with "transform" (func $c "transform")))) (func (export "run") (alias export $d "run")) ) (assert_return (invoke "run"))</details>
<details>
<summary>test 3</summary>
;;! component_model_async = true ;; Vulnerability: ReturnCode::encode overflow via stream.cancel-write ;; ;; Attack: Create a zero-payload intra-component stream. Start a large write ;; that blocks. Read small chunks to accumulate the writer's completion event ;; past 2^28 items. Then cancel the write — the cancel path takes the ;; accumulated Completed(n) event and converts it to Cancelled(n). ;; When n >= 2^28, encode() triggers debug_assert panic, crashing the host. ;; ;; The distinct trigger path is: guest_cancel_write → cancel_write → encode() ;; at futures_and_streams.rs line 4273. (component definition $C (core module $libc (memory (export "mem") 1)) (core instance $libc (instantiate $libc)) (core module $m (import "" "mem" (memory 1)) (import "" "stream.new" (func $stream_new (result i64))) (import "" "stream.read" (func $stream_read (param i32 i32 i32) (result i32))) (import "" "stream.write" (func $stream_write (param i32 i32 i32) (result i32))) (import "" "stream.cancel-write" (func $stream_cancel_write (param i32) (result i32))) (import "" "stream.drop-readable" (func $stream_drop_readable (param i32))) (import " [message truncated]
alexcrichton added the wasm-proposal:component-model-async label to Issue #13023.
Last updated: Apr 12 2026 at 23:10 UTC