Stream: git-wasmtime

Topic: wasmtime / issue #12109 Zero-length reads do not unblock ...


view this post on Zulip Wasmtime GitHub notifications bot (Dec 02 2025 at 19:51):

alexcrichton assigned dicej to issue #12109.

view this post on Zulip Wasmtime GitHub notifications bot (Dec 02 2025 at 19:51):

alexcrichton opened issue #12109:

Given this input:

(component
  (type $s (stream u32))
  (core module $libc (memory (export "m") 1))

  (component $a
    (core instance $libc (instantiate $libc))
    (core func $stream.read (canon stream.read $s async (memory $libc "m")))

    (core module $m
      (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32)))

      (func (export "run") (param $r i32)
        (call $stream.read (local.get $r) (i32.const 0) (i32.const 0))
        if unreachable end
      )
    )

    (core instance $i (instantiate $m
      (with "" (instance
        (export "stream.read" (func $stream.read))
      ))
    ))

    (func (export "run") (param "s" $s) (canon lift (core func $i "run")))
  )

  (component $b
    (import "a" (func $a (param "s" $s)))
    (core instance $libc (instantiate $libc))

    (core func $stream.new (canon stream.new $s))
    (core func $stream.write (canon stream.write $s async (memory $libc "m")))
    (core func $a (canon lower (func $a)))

    (core func $waitable-set.new (canon waitable-set.new))
    (core func $waitable-set.poll (canon waitable-set.poll (memory $libc "m")))
    (core func $waitable.join (canon waitable.join))

    (core module $m
      (import "" "stream.new" (func $stream.new (result i64)))
      (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32)))
      (import "" "a" (func $a (param i32)))
      (import "" "waitable-set.new" (func $waitable-set.new (result i32)))
      (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32)))
      (import "" "waitable.join" (func $waitable.join (param i32 i32)))
      (import "" "m" (memory 1))

      (func (export "run")
        (local $tmp i64)
        (local $r i32)
        (local $w i32)
        (local $s i32)
        (local $rc 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))))

        (call $stream.write (local.get $w) (i32.const 0) (i32.const 1))
        i32.const -1
        i32.ne
        if unreachable end

        (call $a (local.get $r))

        (local.set $s (call $waitable-set.new))
        (call $waitable.join (local.get $w) (local.get $s))
        (local.set $rc (call $waitable-set.poll (local.get $s) (i32.const 100)))

        (i32.ne (local.get $rc) (i32.const 3)) ;; EVENT_STREAM_WRITE
        if unreachable end
      )
    )

    (core instance $i (instantiate $m
      (with "" (instance
        (export "stream.new" (func $stream.new))
        (export "stream.write" (func $stream.write))
        (export "a" (func $a))
        (export "waitable-set.new" (func $waitable-set.new))
        (export "waitable-set.poll" (func $waitable-set.poll))
        (export "waitable.join" (func $waitable.join))
        (export "m" (memory $libc "m"))
      ))
    ))

    (func (export "run") async (canon lift (core func $i "run")))
  )

  (instance $a (instantiate $a))
  (instance $b (instantiate $b (with "a" (func $a "run"))))
  (export "run" (func $b "run"))
)

(assert_return (invoke "run"))

I see:

$ WASMTIME_BACKTRACE_DETAILS=1 cargo run wast foo.wast -W component-model-async
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/x86_64-unknown-linux-gnu/debug/wasmtime wast foo.wast -W component-model-async`
Error: failed to run script file 'foo.wast'

Caused by:
    0: failed directive on foo.wast:95
    1: error while executing at wasm backtrace:
           0:    0x41e - wasm-function[6]
                           at ./foo.wast:71:24
    2: wasm trap: wasm `unreachable` instruction executed

Reading over this documentation, namely the definitions of read and write, I believe that a zero-length read should unblock a nonzero-length write (or, well, any write). The only special case is that a zero-length-read with a zero-length-write unblocks the write only (not the read).

cc @lukewagner on this too

view this post on Zulip Wasmtime GitHub notifications bot (Dec 02 2025 at 19:51):

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

view this post on Zulip Wasmtime GitHub notifications bot (Dec 02 2025 at 20:39):

lukewagner commented on issue #12109:

There may be some wording or code that needs to be improved, but I think (and re-reading it just now, CanonicalABI.md#stream-state seems to match): a zero-length {read or write} should never unblock a non-zero-length {read or write}. Specifically, if the writer arrives first and blocks, then the read will be guarded here by if dst_buffer.remain() > 0 (where dst_buffer.remain() is the length passed to stream.read) and thus pending_on_copy is not called (which would have woken the writer) while on_copy_done is called (which makes stream.read complete eagerly).

view this post on Zulip Wasmtime GitHub notifications bot (Dec 02 2025 at 21:46):

alexcrichton commented on issue #12109:

Ah ok I think I confused myself as to what I was reading. So it's expected that zero-length-things never wakeup non-zero-length-things, but two zero-length-things always have the writer wake up?

view this post on Zulip Wasmtime GitHub notifications bot (Dec 02 2025 at 22:54):

lukewagner commented on issue #12109:

Yep!

view this post on Zulip Wasmtime GitHub notifications bot (Dec 02 2025 at 23:33):

alexcrichton closed issue #12109:

Given this input:

(component
  (type $s (stream u32))
  (core module $libc (memory (export "m") 1))

  (component $a
    (core instance $libc (instantiate $libc))
    (core func $stream.read (canon stream.read $s async (memory $libc "m")))

    (core module $m
      (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32)))

      (func (export "run") (param $r i32)
        (call $stream.read (local.get $r) (i32.const 0) (i32.const 0))
        if unreachable end
      )
    )

    (core instance $i (instantiate $m
      (with "" (instance
        (export "stream.read" (func $stream.read))
      ))
    ))

    (func (export "run") (param "s" $s) (canon lift (core func $i "run")))
  )

  (component $b
    (import "a" (func $a (param "s" $s)))
    (core instance $libc (instantiate $libc))

    (core func $stream.new (canon stream.new $s))
    (core func $stream.write (canon stream.write $s async (memory $libc "m")))
    (core func $a (canon lower (func $a)))

    (core func $waitable-set.new (canon waitable-set.new))
    (core func $waitable-set.poll (canon waitable-set.poll (memory $libc "m")))
    (core func $waitable.join (canon waitable.join))

    (core module $m
      (import "" "stream.new" (func $stream.new (result i64)))
      (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32)))
      (import "" "a" (func $a (param i32)))
      (import "" "waitable-set.new" (func $waitable-set.new (result i32)))
      (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32)))
      (import "" "waitable.join" (func $waitable.join (param i32 i32)))
      (import "" "m" (memory 1))

      (func (export "run")
        (local $tmp i64)
        (local $r i32)
        (local $w i32)
        (local $s i32)
        (local $rc 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))))

        (call $stream.write (local.get $w) (i32.const 0) (i32.const 1))
        i32.const -1
        i32.ne
        if unreachable end

        (call $a (local.get $r))

        (local.set $s (call $waitable-set.new))
        (call $waitable.join (local.get $w) (local.get $s))
        (local.set $rc (call $waitable-set.poll (local.get $s) (i32.const 100)))

        (i32.ne (local.get $rc) (i32.const 3)) ;; EVENT_STREAM_WRITE
        if unreachable end
      )
    )

    (core instance $i (instantiate $m
      (with "" (instance
        (export "stream.new" (func $stream.new))
        (export "stream.write" (func $stream.write))
        (export "a" (func $a))
        (export "waitable-set.new" (func $waitable-set.new))
        (export "waitable-set.poll" (func $waitable-set.poll))
        (export "waitable.join" (func $waitable.join))
        (export "m" (memory $libc "m"))
      ))
    ))

    (func (export "run") async (canon lift (core func $i "run")))
  )

  (instance $a (instantiate $a))
  (instance $b (instantiate $b (with "a" (func $a "run"))))
  (export "run" (func $b "run"))
)

(assert_return (invoke "run"))

I see:

$ WASMTIME_BACKTRACE_DETAILS=1 cargo run wast foo.wast -W component-model-async
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/x86_64-unknown-linux-gnu/debug/wasmtime wast foo.wast -W component-model-async`
Error: failed to run script file 'foo.wast'

Caused by:
    0: failed directive on foo.wast:95
    1: error while executing at wasm backtrace:
           0:    0x41e - wasm-function[6]
                           at ./foo.wast:71:24
    2: wasm trap: wasm `unreachable` instruction executed

Reading over this documentation, namely the definitions of read and write, I believe that a zero-length read should unblock a nonzero-length write (or, well, any write). The only special case is that a zero-length-read with a zero-length-write unblocks the write only (not the read).

cc @lukewagner on this too

view this post on Zulip Wasmtime GitHub notifications bot (Dec 02 2025 at 23:33):

alexcrichton commented on issue #12109:

Ok cool sounds good! In that case I'll close this and work on adjusting my fuzzer-to-be...


Last updated: Dec 06 2025 at 06:05 UTC