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.
Test Case
Steps to Reproduce
wasm32-wasip1(also reproduces onwasm32-wasip2withwasmtime run -S cli-exit-with-code=y).abcde) with a fresh file descriptor, positioned at offset 0.wasmtime run repro.wasmwith that host fd passed as the child's stdin (e.g. via Python'ssubprocess.run(..., stdin=fd), or any launcher that hands wasmtime an already-open fd rather than a fresh pipe).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 POSIXdd/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
wasmtime47.0.3 (latest release as of 2026-08-02), on bothwasm32-wasip1andwasm32-wasip2.Root cause (traced through wasmtime source):
crates/wasi/src/cli/worker_thread_stdin.rs's dedicated stdin-reading thread callsstd::io::stdin().read(&mut bytes)(see thecreate()function's read loop) — butstd::io::stdin()returns a handle to a process-globalBufReader::with_capacity(STDIN_BUF_SIZE, ...)(8KB by default) that lives inside the wasmtime host process. A singleread()call against thatBufReadercan pull far more thansize_hintbytes from the underlying OS file descriptor into theBufReader's internal buffer whenever the fd is backed by a regular (seekable) file — unlike a pipe or terminal, where aread()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'sSource::stdin_as_file()already takes the raw fd directly instead of going throughstd::io::Stdinspecifically 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'sfd_readcall 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::Stdinbuffering behavior, not an OS-specific code path, so likely reproduces on Linux tooArchitecture: aarch64 host, wasm32-wasip1 and wasm32-wasip2 guests (both affected identically, since both route through the same
worker_thread_stdin.rsmodule)Extra Info
Suggested fix: read from the raw file descriptor directly (e.g. via
std::os::fd::AsRawFd+ a rawread(2)/rustix::io::read) instead of going throughstd::io::stdin()'s buffered handle, so the amount actually read from the OS never exceedssize_hint. This is exactly the same fix shape as coreutils' ownuucore::io::RawReader(a zero-buffering wrapper aroundrustix::io::read), which exists for the identical reason.