Stream: git-wasmtime

Topic: wasmtime / issue #13023 Futures/streams don't fully valid...


view this post on Zulip Wasmtime GitHub notifications bot (Apr 09 2026 at 20:30):

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]

view this post on Zulip Wasmtime GitHub notifications bot (Apr 09 2026 at 20:30):

alexcrichton added the wasm-proposal:component-model-async label to Issue #13023.


Last updated: Apr 12 2026 at 23:10 UTC