eduardomourar opened issue #14065:
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 onwasm32-wasip2withwasmtime 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.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).- 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 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.
eduardomourar added the bug label to Issue #14065.
alexcrichton added the wasi:impl label to Issue #14065.
pchickey closed issue #14065:
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 onwasm32-wasip2withwasmtime 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.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).- 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 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.
eduardomourar commented on issue #14065:
I really appreciate the fix done so quickly. Thanks!
Last updated: Aug 30 2026 at 09:07 UTC