From 5fc01bafc7185b797f71c37352cbe66aa740893c Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 24 Jan 2022 11:45:16 -0800 Subject: [PATCH] Fix `isatty` in WASI. (#3696) WASI doesn't have an `isatty` ioctl or syscall, so wasi-libc's `isatty` implementation uses the file descriptor type and rights to determine if the file descriptor is likely to be a tty. The real fix here will be to add an `isatty` call to WASI. But for now, have Wasmtime set the filetype and rights for file descriptors so that wasi-libc's `isatty` works as expected. --- Cargo.lock | 1 + crates/wasi-common/cap-std-sync/Cargo.toml | 1 + crates/wasi-common/cap-std-sync/src/file.rs | 10 ++++++ crates/wasi-common/cap-std-sync/src/stdio.rs | 38 +++++++++++++++++--- crates/wasi-common/src/ctx.rs | 22 ++++++++++-- crates/wasi-common/src/file.rs | 1 + crates/wasi-common/src/pipe.rs | 6 ++++ crates/wasi-common/tokio/src/file.rs | 3 ++ 8 files changed, 74 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6362f3b41389..488e4f7b3aa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3129,6 +3129,7 @@ version = "0.33.0" dependencies = [ "anyhow", "async-trait", + "atty", "cap-fs-ext", "cap-rand", "cap-std", diff --git a/crates/wasi-common/cap-std-sync/Cargo.toml b/crates/wasi-common/cap-std-sync/Cargo.toml index 5733c3d0c318..ac85ee8a7962 100644 --- a/crates/wasi-common/cap-std-sync/Cargo.toml +++ b/crates/wasi-common/cap-std-sync/Cargo.toml @@ -30,6 +30,7 @@ rustix = "0.31.0" [target.'cfg(windows)'.dependencies] winapi = "0.3" lazy_static = "1.4" +atty = "0.2.14" [dev-dependencies] tempfile = "3.1.0" diff --git a/crates/wasi-common/cap-std-sync/src/file.rs b/crates/wasi-common/cap-std-sync/src/file.rs index a3aaf4ed0b0f..b789c94e2b69 100644 --- a/crates/wasi-common/cap-std-sync/src/file.rs +++ b/crates/wasi-common/cap-std-sync/src/file.rs @@ -121,6 +121,16 @@ impl WasiFile for File { async fn num_ready_bytes(&self) -> Result { Ok(self.0.num_ready_bytes()?) } + fn isatty(&self) -> bool { + #[cfg(unix)] + { + rustix::io::isatty(&self.0) + } + #[cfg(windows)] + { + false + } + } async fn readable(&self) -> Result<(), Error> { Err(Error::badf()) } diff --git a/crates/wasi-common/cap-std-sync/src/stdio.rs b/crates/wasi-common/cap-std-sync/src/stdio.rs index fbb0ed3df5f5..db0f5f6ea338 100644 --- a/crates/wasi-common/cap-std-sync/src/stdio.rs +++ b/crates/wasi-common/cap-std-sync/src/stdio.rs @@ -35,7 +35,11 @@ impl WasiFile for Stdin { Ok(()) } async fn get_filetype(&self) -> Result { - Ok(FileType::Unknown) + if self.isatty() { + Ok(FileType::CharacterDevice) + } else { + Ok(FileType::Unknown) + } } async fn get_fdflags(&self) -> Result { Ok(FdFlags::empty()) @@ -104,6 +108,16 @@ impl WasiFile for Stdin { async fn num_ready_bytes(&self) -> Result { Ok(self.0.num_ready_bytes()?) } + fn isatty(&self) -> bool { + #[cfg(unix)] + { + rustix::io::isatty(&self.0) + } + #[cfg(not(unix))] + { + atty::is(atty::Stream::Stdin) + } + } async fn readable(&self) -> Result<(), Error> { Err(Error::badf()) } @@ -125,7 +139,7 @@ impl AsFd for Stdin { } macro_rules! wasi_file_write_impl { - ($ty:ty) => { + ($ty:ty, $ident:ident) => { #[async_trait::async_trait] impl WasiFile for $ty { fn as_any(&self) -> &dyn Any { @@ -138,7 +152,11 @@ macro_rules! wasi_file_write_impl { Ok(()) } async fn get_filetype(&self) -> Result { - Ok(FileType::Unknown) + if self.isatty() { + Ok(FileType::CharacterDevice) + } else { + Ok(FileType::Unknown) + } } async fn get_fdflags(&self) -> Result { Ok(FdFlags::APPEND) @@ -210,6 +228,16 @@ macro_rules! wasi_file_write_impl { async fn num_ready_bytes(&self) -> Result { Ok(0) } + fn isatty(&self) -> bool { + #[cfg(unix)] + { + rustix::io::isatty(&self.0) + } + #[cfg(not(unix))] + { + atty::is(atty::Stream::$ident) + } + } async fn readable(&self) -> Result<(), Error> { Err(Error::badf()) } @@ -237,11 +265,11 @@ pub struct Stdout(std::io::Stdout); pub fn stdout() -> Stdout { Stdout(std::io::stdout()) } -wasi_file_write_impl!(Stdout); +wasi_file_write_impl!(Stdout, Stdout); pub struct Stderr(std::io::Stderr); pub fn stderr() -> Stderr { Stderr(std::io::stderr()) } -wasi_file_write_impl!(Stderr); +wasi_file_write_impl!(Stderr, Stderr); diff --git a/crates/wasi-common/src/ctx.rs b/crates/wasi-common/src/ctx.rs index 538ed95cf584..334f60474adb 100644 --- a/crates/wasi-common/src/ctx.rs +++ b/crates/wasi-common/src/ctx.rs @@ -71,15 +71,31 @@ impl WasiCtx { } pub fn set_stdin(&mut self, f: Box) { - self.insert_file(0, f, FileCaps::all()); + let rights = Self::stdio_rights(&*f); + self.insert_file(0, f, rights); } pub fn set_stdout(&mut self, f: Box) { - self.insert_file(1, f, FileCaps::all()); + let rights = Self::stdio_rights(&*f); + self.insert_file(1, f, rights); } pub fn set_stderr(&mut self, f: Box) { - self.insert_file(2, f, FileCaps::all()); + let rights = Self::stdio_rights(&*f); + self.insert_file(2, f, rights); + } + + fn stdio_rights(f: &dyn WasiFile) -> FileCaps { + let mut rights = FileCaps::all(); + + // If `f` is a tty, restrict the `tell` and `seek` capabilities, so + // that wasi-libc's `isatty` correctly detects the file descriptor + // as a tty. + if f.isatty() { + rights &= !(FileCaps::TELL | FileCaps::SEEK); + } + + rights } pub fn push_preopened_dir( diff --git a/crates/wasi-common/src/file.rs b/crates/wasi-common/src/file.rs index a5516248cf26..f43d212ff833 100644 --- a/crates/wasi-common/src/file.rs +++ b/crates/wasi-common/src/file.rs @@ -34,6 +34,7 @@ pub trait WasiFile: Send + Sync { async fn seek(&self, pos: std::io::SeekFrom) -> Result; // file op that generates a new stream from a file will supercede this async fn peek(&self, buf: &mut [u8]) -> Result; // read op async fn num_ready_bytes(&self) -> Result; // read op + fn isatty(&self) -> bool; async fn readable(&self) -> Result<(), Error>; async fn writable(&self) -> Result<(), Error>; diff --git a/crates/wasi-common/src/pipe.rs b/crates/wasi-common/src/pipe.rs index f1e088c4b411..6e8c5d9f7169 100644 --- a/crates/wasi-common/src/pipe.rs +++ b/crates/wasi-common/src/pipe.rs @@ -180,6 +180,9 @@ impl WasiFile for ReadPipe { async fn num_ready_bytes(&self) -> Result { Ok(0) } + fn isatty(&self) -> bool { + false + } async fn readable(&self) -> Result<(), Error> { Err(Error::badf()) } @@ -336,6 +339,9 @@ impl WasiFile for WritePipe { async fn num_ready_bytes(&self) -> Result { Ok(0) } + fn isatty(&self) -> bool { + false + } async fn readable(&self) -> Result<(), Error> { Err(Error::badf()) } diff --git a/crates/wasi-common/tokio/src/file.rs b/crates/wasi-common/tokio/src/file.rs index 3a8699e1e903..812ec2a5529a 100644 --- a/crates/wasi-common/tokio/src/file.rs +++ b/crates/wasi-common/tokio/src/file.rs @@ -112,6 +112,9 @@ macro_rules! wasi_file_impl { async fn num_ready_bytes(&self) -> Result { block_on_dummy_executor(|| self.0.num_ready_bytes()) } + fn isatty(&self) -> bool { + self.0.isatty() + } #[cfg(not(windows))] async fn readable(&self) -> Result<(), Error> {