fitzgen opened issue #14360:
These traps, added in
cc546eee26, are (apparently; I haven't investigate)d unreachable:
wasmtime::Trap::ReadFromDroppedStream/WASMTIME_TRAP_READ_FROM_DROPPED_STREAMwasmtime::Trap::WriteToDroppedStream/WASMTIME_TRAP_WRITE_TO_DROPPED_STREAMwasmtime::Trap::WriteToDroppedFuture/WASMTIME_TRAP_WRITE_TO_DROPPED_FUTUREThey are only returned from code paths that come after
guest_readandguest_writethat perform their own generic checks and return genericbail!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 droppedAnd 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
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 7ad2e732ab9ca8665d3cdd91f9c395315eeafc81Offending 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 defaultSummary
cc546eee26added four new public error codes for "the guest kept using a
stream/future handle after it was notified that the peer end dropped":
wasmtime::Trap::ReadFromDroppedStream/WASMTIME_TRAP_READ_FROM_DROPPED_STREAM(50)wasmtime::Trap::WriteToDroppedStream/WASMTIME_TRAP_WRITE_TO_DROPPED_STREAM(51)wasmtime::Trap::WriteToDroppedFuture/WASMTIME_TRAP_WRITE_TO_DROPPED_FUTURE(52)wasmtime::Trap::LiftDroppedStream/WASMTIME_TRAP_LIFT_DROPPED_STREAM(53)Only the last one is reachable. The code that raises the other three sits
after two older, more generic checks at the top ofguest_writeand
guest_read, and those generic checks always fire first. Theybail!with a
plain string, so the resulting error carries no trap code at all:
wasmtime::Error::downcast_ref::<Trap>()returnsNoneand the C API's
wasmtime_trap_code()returnsfalse. 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]
fitzgen commented on issue #14360:
cc @dicej
Last updated: Sep 20 2026 at 18:08 UTC