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.
test 1
;;! 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"))
test 2
;;! 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"))
test 3
;;! 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 "" "stream.drop-writable" (func $stream_drop_writable (param i32)))
(func (export "run")
(local $handles i64)
(local $reader i32)
(local $writer i32)
(local $ret i32)
;; Create a zero-payload stream (type $s is (stream) with no element type).
;; This means check_bounds skips the memory bounds check entirely,
;; allowing arbitrary count values.
(local.set $handles (call $stream_new))
(local.set $reader (i32.wrap_i64 (local.get $handles)))
(local.set $writer (i32.wrap_i64 (i64.shr_u (local.get $handles) (i64.const 32))))
;; Step 1: Write 0x20000000 (2^29) items. No reader is ready, so this
;; returns BLOCKED (-1). The writer handle enters Busy state and
;; WriteState becomes GuestReady { count: 0x20000000 }.
(local.set $ret
(call $stream_write (local.get $writer) (i32.const 0) (i32.const 0x20000000)))
(if (i32.ne (local.get $ret) (i32.const -1))
(then unreachable)) ;; must be BLOCKED
;; Step 2: Read 0x08000000 (2^27) items from the readable end.
;; guest_read finds the pending writer (GuestReady), does the rendezvous:
;; count = min(0x08000000, 0x20000000) = 0x08000000
;; write_complete = true (0x20000000 != 0 || 0x08000000 > 0)
;; → sends StreamWrite event with Completed(0x08000000) for writer
;; write_buffer_remaining = true (0x08000000 < 0x20000000)
;; → writer stays GuestReady { count: 0x18000000 }
;; Read returns Completed(0x08000000) → encode → (0x08000000 << 4) | 0
;; = 0x80000000 — fits in u32, passes debug_assert (0x08000000 < 2^28).
(local.set $ret
(call $stream_read (local.get $reader) (i32.const 0) (i32.const 0x08000000)))
;; Verify read returned Completed(0x08000000): (0x08000000 << 4) | 0 = 0x80000000
(if (i32.ne (local.get $ret) (i32.const 0x80000000))
(then unreachable))
;; Step 3: Read another 0x08000000 items.
;; guest_read finds the writer still in GuestReady { count: 0x18000000 }:
;; count = min(0x08000000, 0x18000000) = 0x08000000
;; write_complete = true
;; → takes existing StreamWrite event Completed(0x08000000)
;; → accumulates total = 0x08000000 + 0x08000000 = 0x10000000 (= 2^28)
;; → sends StreamWrite event with Completed(0x10000000) for writer
;; Read returns Completed(0x08000000) → encode passes (0x08000000 < 2^28).
(local.set $ret
(call $stream_read (local.get $reader) (i32.const 0) (i32.const 0x08000000)))
(if (i32.ne (local.get $ret) (i32.const 0x80000000))
(then unreachable))
;; Step 4: Cancel the write.
;; The writer handle is still Busy (from step 1's blocked write).
;; cancel_write (line 3898) calls take_event and finds:
;; StreamWrite { code: Completed(0x10000000) }
;; It converts to Cancelled(0x10000000) (line 3904-3905).
;; Back in stream_cancel_write (line 4273), .encode() is called.
;; encode() hits debug_assert!(0x10000000 < (1 << 28)) → FALSE → PANIC!
;; The panic propagates through catch_unwind → resume_unwind → host crash.
(call $stream_cancel_write (local.get $writer))
drop
)
)
(type $s (stream))
(core func $stream_new (canon stream.new $s))
(core func $stream_read (canon stream.read $s async (memory $libc "mem")))
(core func $stream_write (canon stream.write $s async (memory $libc "mem")))
(core func $stream_cancel_write (canon stream.cancel-write $s))
(core func $stream_drop_readable (canon stream.drop-readable $s))
(core func $stream_drop_writable (canon stream.drop-writable $s))
(core instance $i (instantiate $m (with "" (instance
(export "mem" (memory $libc "mem"))
(export "stream.new" (func $stream_new))
(export "stream.read" (func $stream_read))
(export "stream.write" (func $stream_write))
(export "stream.cancel-write" (func $stream_cancel_write))
(export "stream.drop-readable" (func $stream_drop_readable))
(export "stream.drop-writable" (func $stream_drop_writable))
))))
(func (export "trigger") async (canon lift (core func $i "run")))
)
(component instance $C $C)
(assert_trap (invoke "trigger") "unreachable")
cc @dicej
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.
test 1
test 2
test 3
cc @dicej