Skip to content

wasmtime's stdin worker thread over-reads from a host fd backed by a regular file #14065

Description

@eduardomourar

Test Case

use std::io::Read;
fn main() {
    let mut buf = [0u8; 1];
    let n = std::io::stdin().read(&mut buf).unwrap();
    println!("read {n} bytes: {:?}", &buf[..n]);
}

Steps to Reproduce

  • Compile the repro for wasm32-wasip1 (also reproduces on wasm32-wasip2 with wasmtime run -S cli-exit-with-code=y).
  • On the host, open a regular file containing 5 bytes (e.g. abcde) with a fresh file descriptor, positioned at offset 0.
  • Run wasmtime run repro.wasm with that host fd passed as the child's stdin (e.g. via Python's subprocess.run(..., stdin=fd), or any launcher that hands wasmtime an already-open fd rather than a fresh pipe).
  • After the guest process exits, check the host fd's position with lseek(fd, 0, SEEK_CUR).

Expected Results

The host fd's position should advance by exactly 1 (the number of bytes the guest actually requested and read), matching what happens when a native (non-wasm) program does the equivalent read() on the same fd. This matters for any caller that shares one host file descriptor across multiple separate WASI guest invocations, expecting each one to pick up where the last left off — the same pattern POSIX dd/cat/etc. rely on when chained via shell pipelines against a seekable input.

Actual Results

The host fd's position ends up at 5 (end of file) — the entire file was consumed, not just the 1 byte the guest asked for and printed. Confirmed on wasmtime 47.0.3 (latest release as of 2026-08-02), on both wasm32-wasip1 and wasm32-wasip2.

Root cause (traced through wasmtime source): crates/wasi/src/cli/worker_thread_stdin.rs's dedicated stdin-reading thread calls std::io::stdin().read(&mut bytes) (see the create() function's read loop) — but std::io::stdin() returns a handle to a process-global BufReader::with_capacity(STDIN_BUF_SIZE, ...) (8KB by default) that lives inside the wasmtime host process. A single read() call against that BufReader can pull far more than size_hint bytes from the underlying OS file descriptor into the BufReader's internal buffer whenever the fd is backed by a regular (seekable) file — unlike a pipe or terminal, where a read() typically returns only what's immediately available. The excess bytes sitting in that internal buffer are never handed back to the guest and are simply lost when the wasmtime process exits, having already been consumed from the underlying fd.

This is user-visible for any real program: in uutils/coreutils, dd's Source::stdin_as_file() already takes the raw fd directly instead of going through std::io::Stdin specifically to avoid coreutils' own internal buffering — but that has no effect here, since the over-read happens one layer down, inside wasmtime's own host-side stdin handling, before the guest's fd_read call is even serviced. No guest-side code change can work around this.

Versions and Environment

Wasmtime version or commit: 47.0.3 (latest release as of 2026-08-02)

Operating system: macOS (Darwin); the bug is in generic std::io::Stdin buffering behavior, not an OS-specific code path, so likely reproduces on Linux too

Architecture: aarch64 host, wasm32-wasip1 and wasm32-wasip2 guests (both affected identically, since both route through the same worker_thread_stdin.rs module)

Extra Info

Suggested fix: read from the raw file descriptor directly (e.g. via std::os::fd::AsRawFd + a raw read(2)/rustix::io::read) instead of going through std::io::stdin()'s buffered handle, so the amount actually read from the OS never exceeds size_hint. This is exactly the same fix shape as coreutils' own uucore::io::RawReader (a zero-buffering wrapper around rustix::io::read), which exists for the identical reason.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIncorrect behavior in the current implementation that needs fixingwasi:implIssues pertaining to WASI implementation in Wasmtime

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions