Test Case
No wasm file needed — reproducible with any wasip2 guest that writes past a closed pipe, or reads stdin redirected from a directory. Minimal repro source: [gist-equivalent below].
// write side
use std::io::Write;
fn main() {
loop {
if let Err(e) = std::io::stdout().write_all(b"0123456789\n") {
eprintln!("write error: kind={:?} raw_os_error={:?}", e.kind(), e.raw_os_error());
return;
}
}
}
// read side
use std::io::Read;
fn main() {
let mut buf = [0u8; 16];
match std::io::stdin().read(&mut buf) {
Err(e) => eprintln!("stdin read error: kind={:?} raw_os_error={:?}", e.kind(), e.raw_os_error()),
_ => {}
}
}
Steps to Reproduce
- Compile the write-side repro for
wasm32-wasip2, run under wasmtime run pipe.wasm | head -c 30, so the reader closes early.
- Compile the read-side repro for
wasm32-wasip2, run under wasmtime run --dir <dir> stdin.wasm < <dir>, redirecting stdin from a directory.
- Compare against the same repros compiled for
wasm32-wasip1.
Expected Results
wasip1 correctly reports BrokenPipe/EPIPE(64) for the write case and IsADirectory/EISDIR(31) for the read case — matching native Unix.
Actual Results
wasip2 reports InputOutputError/EIO(29) for both cases. Confirmed on wasmtime 45.0.0 and 47.0.3 (latest release, 2026-07-31) — unchanged between versions.
Root cause (traced through wasi-libc + wasmtime source):
This affects every wasip2 guest, not just Rust's — e.g. uutils/coreutils#13625 had to add a userland wasi_is_broken_pipe helper checking for raw errno 29 as a workaround.
Versions and Environment
Wasmtime version or commit: 45.0.0 and 47.0.3 (both exhibit the bug)
Operating system: macOS (Darwin), confirmed reproducible; likely platform-independent since the bug is in wasmtime's stdio glue rather than any OS-specific path
Architecture: aarch64 host, wasm32-wasip2 guest
Extra Info
The wasi:io/streams spec's stream-error type is designed for exactly this: it carries an opaque error resource that other interfaces can "downcast" into more specific info (e.g. wasi:filesystem/types/filesystem-error-code). A fix could either (a) have StdioOutputStream::write/WasiStdin::read check e.kind() and return StreamError::Closed for BrokenPipe (matching what wasi-libc already expects), or (b) give p2's CLI stdio the same io::Error-recovery path p1 already has via filesystem::ErrorCode::from.
Test Case
No wasm file needed — reproducible with any wasip2 guest that writes past a closed pipe, or reads stdin redirected from a directory. Minimal repro source: [gist-equivalent below].
Steps to Reproduce
wasm32-wasip2, run underwasmtime run pipe.wasm | head -c 30, so the reader closes early.wasm32-wasip2, run underwasmtime run --dir <dir> stdin.wasm < <dir>, redirecting stdin from a directory.wasm32-wasip1.Expected Results
wasip1 correctly reports
BrokenPipe/EPIPE(64) for the write case andIsADirectory/EISDIR(31) for the read case — matching native Unix.Actual Results
wasip2 reports
InputOutputError/EIO(29) for both cases. Confirmed onwasmtime45.0.0 and 47.0.3 (latest release, 2026-07-31) — unchanged between versions.Root cause (traced through wasi-libc + wasmtime source):
crates/wasi/src/cli/stdout.rs#StdioOutputStream::write/flushwraps any error fromstd::io::stdout().write_all()asStreamError::LastOperationFailed(anyhow::Error)viawasmtime::format_err!(e), even whene.kind() == BrokenPipe. It never returnsStreamError::Closed.crates/wasi/src/cli/worker_thread_stdin.rs#WasiStdin::readdoes the same forStdinState::Error(e) => Err(StreamError::LastOperationFailed(e.into())), discardinge.kind() == IsADirectory.wasip2_handle_write_error/wasip2_handle_read_erroronly distinguishStreamError::ClosedfromLastOperationFailed, mapping the latter to a genericEIO— so once the specific error kind is erased above, it can't be recovered here either.From<StreamError> for types::ErrordowncastsLastOperationFailed's inner error back tostd::io::Errorand runs it throughfilesystem::ErrorCode::from, which has realRustixErrno::PIPE/ISDIRarms. The p2 CLI-stdio path has no equivalent recovery step.This affects every wasip2 guest, not just Rust's — e.g. uutils/coreutils#13625 had to add a userland
wasi_is_broken_pipehelper checking for raw errno 29 as a workaround.Versions and Environment
Wasmtime version or commit: 45.0.0 and 47.0.3 (both exhibit the bug)
Operating system: macOS (Darwin), confirmed reproducible; likely platform-independent since the bug is in
wasmtime's stdio glue rather than any OS-specific pathArchitecture: aarch64 host, wasm32-wasip2 guest
Extra Info
The
wasi:io/streamsspec'sstream-errortype is designed for exactly this: it carries an opaqueerrorresource that other interfaces can "downcast" into more specific info (e.g.wasi:filesystem/types/filesystem-error-code). A fix could either (a) haveStdioOutputStream::write/WasiStdin::readchecke.kind()and returnStreamError::ClosedforBrokenPipe(matching what wasi-libc already expects), or (b) give p2's CLI stdio the same io::Error-recovery path p1 already has viafilesystem::ErrorCode::from.