Stream: git-wasmtime

Topic: wasmtime / issue #14360 Three dropped-stream traps are un...


view this post on Zulip Wasmtime GitHub notifications bot (Sep 19 2026 at 15:29):

fitzgen opened issue #14360:

These traps, added in cc546eee26, are (apparently; I haven't investigate)d unreachable:

They are only returned from code paths that come after guest_read and guest_write that perform their own generic checks and return generic bail! errors instead of these typed trap errors.

Reproduction

$ wasmtime wast -W component-model-async=y dropped-trap-codes.wast
Error: failed to run script file 'dropped-trap-codes.wast'

Caused by:
    0: failed directive on dropped-trap-codes.wast:128
    1: expected 'to stream after being notified that the readable end dropped', got 'error while executing at wasm backtrace:
        0:    0x21e - wasm-function[10]
    ...
Caused by:
    cannot write after being notified that the readable end dropped

And also some unit tests that incorrectly pass today, but will fail when the bug is fixed:

$ cd repro && cargo test
running 6 tests
test new_trap_code_messages ... ok
test write_after_inline_dropped_write ... ok
test read_after_inline_dropped_read ... ok
test control_trap_code_is_observable ... ok
test write_after_polled_dropped_event ... ok
test read_after_polled_dropped_event ... ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

repro.tar.gz

dropped-trap-codes.wast

<details>

;; Wasmtime bug report 007: the trap codes added in cc546eee26 for "the guest
;; kept using a stream handle after being notified that the peer end dropped"
;; are shadowed by older, more generic checks, so three of the four new
;; `wasmtime::Trap` variants (and the matching C API trap codes 50, 51 and 52)
;; can never be produced.
;;
;; This file asserts the messages the new trap codes carry, i.e. the behavior
;; the upstream component-model test suite specifies.  All four directives fail
;; on Wasmtime 7ad2e732ab; the runner stops at the first one, so uncomment them
;; one at a time (see `report.md` for the output of each).
;;
;; The expectations below are deliberately written as *suffixes* of the real
;; `Trap::{Write,Read}ToDroppedStream` messages, with the leading "cannot
;; write"/"cannot read" removed.  `Wast::assert_trap` in `crates/wast/src/wast.rs`
;; treats any expectation containing "cannot write" (or "cannot read") as
;; matching any actual message containing the same phrase, so the full messages
;; would be accepted despite being wrong.  That leniency is why the in-tree
;; `tests/component-model/test/async/idle-drop.wast` passes today.
;;
;; The authoritative reproduction is `repro/`, a standalone Cargo project that
;; observes the missing trap codes through `wasmtime::Error::downcast_ref`.
;;
;;   wasmtime wast -W component-model-async=y dropped-trap-codes.wast

(component definition $T
  (core module $Memory (memory (export "mem") 1))
  (core instance $memory (instantiate $Memory))
  (core module $M
    (import "" "mem" (memory 1))
    (import "" "waitable.join" (func $waitable.join (param i32 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 "" "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.drop-readable" (func $stream.drop-readable (param i32)))
    (import "" "stream.drop-writable" (func $stream.drop-writable (param i32)))

    (global $ws (mut i32) (i32.const 0))
    (global $rx (mut i32) (i32.const 0))
    (global $tx (mut i32) (i32.const 0))

    (func $start (global.set $ws (call $waitable-set.new)))
    (start $start)

    (func $new-stream
      (local $ret64 i64)
      (local.set $ret64 (call $stream.new))
      (global.set $rx (i32.wrap_i64 (local.get $ret64)))
      (global.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32))))
    )

    ;; (1) Drop the readable end while the writable end is idle, consume the
    ;; DROPPED notification inline with a `stream.write` (which returns
    ;; DROPPED), then write again.  Expected: WriteToDroppedStream.
    (func (export "write-write")
      (call $new-stream)
      (call $stream.drop-readable (global.get $rx))
      (if (i32.ne (i32.const 0x01 (; DROPPED ;))
                  (call $stream.write (global.get $tx) (i32.const 16) (i32.const 4)))
        (then unreachable))
      (drop (call $stream.write (global.get $tx) (i32.const 16) (i32.const 4)))
      unreachable
    )

    ;; (2) Same, but consume the notification via the waitable set and then
    ;; write.  Expected: WriteToDroppedStream.
    (func (export "poll-write")
      (call $new-stream)
      (call $waitable.join (global.get $tx) (global.get $ws))
      (call $stream.drop-readable (global.get $rx))
      (drop (call $waitable-set.poll (global.get $ws) (i32.const 0)))
      (drop (call $stream.write (global.get $tx) (i32.const 16) (i32.const 4)))
      unreachable
    )

    ;; (3) Reader side: consume the notification via the waitable set and then
    ;; read.  Expected: ReadFromDroppedStream.
    (func (export "poll-read")
      (call $new-stream)
      (call $waitable.join (global.get $rx) (global.get $ws))
      (call $stream.drop-writable (global.get $tx))
      (drop (call $waitable-set.poll (global.get $ws) (i32.const 0)))
      (drop (call $stream.read (global.get $rx) (i32.const 16) (i32.const 4)))
      unreachable
    )

    ;; (4) Reader side: consume the notification inline with a `stream.read`
    ;; (which returns DROPPED) and then read again.  Expected:
    ;; ReadFromDroppedStream.
    (func (export "read-read")
      (call $new-stream)
      (call $stream.drop-writable (global.get $tx))
      (if (i32.ne (i32.const 0x01 (; DROPPED ;))
                  (call $stream.read (global.get $rx) (i32.const 16) (i32.const 4)))
        (then unreachable))
      (drop (call $stream.read (global.get $rx) (i32.const 16) (i32.const 4)))
      unreachable
    )
  )
  (type $ST (stream u8))
  (canon waitable.join (core func $waitable.join))
  (canon waitable-set.new (core func $waitable-set.new))
  (canon waitable-set.poll (memory (core memory $memory "mem")) (core func $waitable-set.poll))
  (canon stream.new $ST (core func $stream.new))
  (canon stream.read $ST async (memory (core memory $memory "mem")) (core func $stream.read))
  (canon stream.write $ST async (memory (core memory $memory "mem")) (core func $stream.write))
  (canon stream.drop-readable $ST (core func $stream.drop-readable))
  (canon stream.drop-writable $ST (core func $stream.drop-writable))
  (core instance $m (instantiate $M (with "" (instance
    (export "mem" (memory $memory "mem"))
    (export "waitable.join" (func $waitable.join))
    (export "waitable-set.new" (func $waitable-set.new))
    (export "waitable-set.poll" (func $waitable-set.poll))
    (export "stream.new" (func $stream.new))
    (export "stream.read" (func $stream.read))
    (export "stream.write" (func $stream.write))
    (export "stream.drop-readable" (func $stream.drop-readable))
    (export "stream.drop-writable" (func $stream.drop-writable))
  ))))
  (func (export "write-write") (canon lift (core func $m "write-write")))
  (func (export "poll-write") (canon lift (core func $m "poll-write")))
  (func (export "poll-read") (canon lift (core func $m "poll-read")))
  (func (export "read-read") (canon lift (core func $m "read-read")))
)

(component instance $i $T)
(assert_trap (invoke "write-write") "to stream after being notified that the readable end dropped")
(component instance $i $T)
;; (assert_trap (invoke "poll-write") "to stream after being notified that the readable end dropped")
(component instance $i $T)
;; (assert_trap (invoke "poll-read") "from stream after being notified that the writable end dropped")
(component instance $i $T)
;; (assert_trap (invoke "read-read") "from stream after being notified that the writable end dropped")

</details>

Full LLM Report

<details>

Date 2026-09-18
Wasmtime commit 7ad2e732ab9ca8665d3cdd91f9c395315eeafc81
Offending commit cc546eee26 — "Fix stream/future drop notification for idle ends" (#14342)
Host OS macOS 15.7.9 (Darwin 24.6.0)
Host arch arm64 (aarch64)
Model Claude Opus 5 (claude-opus-5)
Feature component model async (-W component-model-async=y) — Tier 2, off by default

Summary

cc546eee26 added four new public error codes for "the guest kept using a
stream/future handle after it was notified that the peer end dropped":

Only the last one is reachable. The code that raises the other three sits
after two older, more generic checks at the top of guest_write and
guest_read, and those generic checks always fire first. They bail! with a
plain string, so the resulting error carries no trap code at all:
wasmtime::Error::downcast_ref::<Trap>() returns None and the C API's
wasmtime_trap_code() returns false. The three new C enum values 50, 51 and
52 are therefore unobservable in any embedding.

Two of the four messages that are produced instead are also wrong on their
face: they
[message truncated]

view this post on Zulip Wasmtime GitHub notifications bot (Sep 19 2026 at 15:29):

fitzgen commented on issue #14360:

cc @dicej


Last updated: Sep 20 2026 at 18:08 UTC