Stream: git-wasmtime

Topic: wasmtime / issue #14061 wasip2 CLI stdio discards host `i...


view this post on Zulip Wasmtime GitHub notifications bot (Aug 02 2026 at 00:14):

eduardomourar opened issue #14061:

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

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.

view this post on Zulip Wasmtime GitHub notifications bot (Aug 02 2026 at 00:14):

eduardomourar added the bug label to Issue #14061.

view this post on Zulip Wasmtime GitHub notifications bot (Aug 03 2026 at 14:03):

alexcrichton closed issue #14061:

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

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.

view this post on Zulip Wasmtime GitHub notifications bot (Aug 03 2026 at 14:03):

alexcrichton commented on issue #14061:

The behavior you're describing here is an artifact of WASI's interfaces as opposed to Wasmtime's implementation. If you're interested in having this resolved I'd recommend filing an issue in the upstream WASI specification repository.

view this post on Zulip Wasmtime GitHub notifications bot (Aug 06 2026 at 15:55):

eduardomourar commented on issue #14061:

@alexcrichton, thanks for looking at this. I don't think this is a spec limitation, though. The stream-error type's two variants (last-operation-failed(error)/closed) are the full interface, sure, but wasmtime's own p1 path already handles this correctly: From<StreamError> for types::Error in p1.rs downcasts the inner error back to std::io::Error and maps it through filesystem::ErrorCode::from (which has real PIPE/ISDIR arms). p2's CLI-stdio path just doesn't do any equivalent recovery. It wraps everything as opaque LastOperationFailed in cli/stdout.rs and cli/worker_thread_stdin.rs, and wasi-libc maps that to EIO since it can only distinguish Closed from LastOperationFailed.

So the spec already accommodates the fix; it's just that the p2 implementation doesn't use the same trick p1 does. A minimal fix could be as simple as returning StreamError::Closed when e.kind() == BrokenPipe (which is what wasi-libc already expects on that variant), or giving p2 the same ErrorCode recovery p1 has.

Happy to put a PR together if either approach sounds reasonable.

view this post on Zulip Wasmtime GitHub notifications bot (Aug 10 2026 at 14:34):

alexcrichton reopened issue #14061:

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

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.

view this post on Zulip Wasmtime GitHub notifications bot (Aug 10 2026 at 14:34):

alexcrichton commented on issue #14061:

Ah sorry yeah I can expand on my previous message a bit. You're correct that in wasip1 and wasip2 there are vectors by which these precise errors can be communicated. My comment is that for wasip3, the latest iteration of WASI, there is no means by which to communicate precise errors via stdio right now. You can see the current specification here and notably what I'm referring to is this error type definition which can represent EPIPE but cannot represent EISDIR. I've tested locally for wasip3 and was able to get EPIPE correctly with the steps you outlined, but EISDIR showed up as EIO.

For wasip2, however, if you'd like then yeah having a PR to make the error type more precise I think would be fine to add to Wasmtime. I'll reopen for further discussion on that if needed, too.

view this post on Zulip Wasmtime GitHub notifications bot (Aug 11 2026 at 14:08):

dicej closed issue #14061:

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

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.


Last updated: Aug 30 2026 at 09:07 UTC