Rollup merge of #150668 - stdio-swap, r=Mark-Simulacrum,RalfJung

Unix implementation for stdio set/take/replace

Tracking issue: https://github.com/rust-lang/rust/issues/150667
ACP: https://github.com/rust-lang/libs-team/issues/500
This commit is contained in:
Matthias Krüger 2026-01-11 09:56:38 +01:00 committed by GitHub
commit 7a9ef99a56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 174 additions and 22 deletions

View file

@ -92,9 +92,138 @@
#![stable(feature = "rust1", since = "1.0.0")]
use crate::io::{self, Stderr, StderrLock, Stdin, StdinLock, Stdout, StdoutLock, Write};
#[stable(feature = "rust1", since = "1.0.0")]
pub use crate::os::fd::*;
#[allow(unused_imports)] // not used on all targets
use crate::sys::cvt;
// Tests for this module
#[cfg(test)]
mod tests;
#[unstable(feature = "stdio_swap", issue = "150667", reason = "recently added")]
pub trait StdioExt: crate::sealed::Sealed {
/// Redirects the stdio file descriptor to point to the file description underpinning `fd`.
///
/// Rust std::io write buffers (if any) are flushed, but other runtimes
/// (e.g. C stdio) or libraries that acquire a clone of the file descriptor
/// will not be aware of this change.
///
/// # Platform-specific behavior
///
/// This is [currently] implemented using
///
/// - `fd_renumber` on wasip1
/// - `dup2` on most unixes
///
/// [currently]: crate::io#platform-specific-behavior
///
/// ```
/// #![feature(stdio_swap)]
/// use std::io::{self, Read, Write};
/// use std::os::unix::io::StdioExt;
///
/// fn main() -> io::Result<()> {
/// let (reader, mut writer) = io::pipe()?;
/// let mut stdin = io::stdin();
/// stdin.set_fd(reader)?;
/// writer.write_all(b"Hello, world!")?;
/// let mut buffer = vec![0; 13];
/// assert_eq!(stdin.read(&mut buffer)?, 13);
/// assert_eq!(&buffer, b"Hello, world!");
/// Ok(())
/// }
/// ```
fn set_fd<T: Into<OwnedFd>>(&mut self, fd: T) -> io::Result<()>;
/// Redirects the stdio file descriptor and returns a new `OwnedFd`
/// backed by the previous file description.
///
/// See [`set_fd()`] for details.
///
/// [`set_fd()`]: StdioExt::set_fd
fn replace_fd<T: Into<OwnedFd>>(&mut self, replace_with: T) -> io::Result<OwnedFd>;
/// Redirects the stdio file descriptor to the null device (`/dev/null`)
/// and returns a new `OwnedFd` backed by the previous file description.
///
/// Programs that communicate structured data via stdio can use this early in `main()` to
/// extract the fds, treat them as other IO types (`File`, `UnixStream`, etc),
/// apply custom buffering or avoid interference from stdio use later in the program.
///
/// See [`set_fd()`] for additional details.
///
/// [`set_fd()`]: StdioExt::set_fd
fn take_fd(&mut self) -> io::Result<OwnedFd>;
}
macro io_ext_impl($stdio_ty:ty, $stdio_lock_ty:ty, $writer:literal) {
#[unstable(feature = "stdio_swap", issue = "150667", reason = "recently added")]
impl StdioExt for $stdio_ty {
fn set_fd<T: Into<OwnedFd>>(&mut self, fd: T) -> io::Result<()> {
self.lock().set_fd(fd)
}
fn take_fd(&mut self) -> io::Result<OwnedFd> {
self.lock().take_fd()
}
fn replace_fd<T: Into<OwnedFd>>(&mut self, replace_with: T) -> io::Result<OwnedFd> {
self.lock().replace_fd(replace_with)
}
}
#[unstable(feature = "stdio_swap", issue = "150667", reason = "recently added")]
impl StdioExt for $stdio_lock_ty {
fn set_fd<T: Into<OwnedFd>>(&mut self, fd: T) -> io::Result<()> {
#[cfg($writer)]
self.flush()?;
replace_stdio_fd(self.as_fd(), fd.into())
}
fn take_fd(&mut self) -> io::Result<OwnedFd> {
let null = null_fd()?;
let cloned = self.as_fd().try_clone_to_owned()?;
self.set_fd(null)?;
Ok(cloned)
}
fn replace_fd<T: Into<OwnedFd>>(&mut self, replace_with: T) -> io::Result<OwnedFd> {
let cloned = self.as_fd().try_clone_to_owned()?;
self.set_fd(replace_with)?;
Ok(cloned)
}
}
}
io_ext_impl!(Stdout, StdoutLock<'_>, true);
io_ext_impl!(Stdin, StdinLock<'_>, false);
io_ext_impl!(Stderr, StderrLock<'_>, true);
fn null_fd() -> io::Result<OwnedFd> {
let null_dev = crate::fs::OpenOptions::new().read(true).write(true).open("/dev/null")?;
Ok(null_dev.into())
}
/// Replaces the underlying file descriptor with the one from `other`.
/// Does not set CLOEXEC.
fn replace_stdio_fd(this: BorrowedFd<'_>, other: OwnedFd) -> io::Result<()> {
cfg_select! {
all(target_os = "wasi", target_env = "p1") => {
cvt(unsafe { libc::__wasilibc_fd_renumber(other.as_raw_fd(), this.as_raw_fd()) }).map(|_| ())
}
not(any(
target_arch = "wasm32",
target_os = "hermit",
target_os = "trusty",
target_os = "motor"
)) => {
cvt(unsafe {libc::dup2(other.as_raw_fd(), this.as_raw_fd())}).map(|_| ())
}
_ => {
let _ = (this, other);
Err(io::Error::UNSUPPORTED_PLATFORM)
}
}
}

View file

@ -249,6 +249,15 @@ impl FileDescription for io::Stdin {
finish.call(ecx, result)
}
fn destroy<'tcx>(
self,
_self_id: FdId,
_communicate_allowed: bool,
_ecx: &mut MiriInterpCx<'tcx>,
) -> InterpResult<'tcx, io::Result<()>> {
interp_ok(Ok(()))
}
fn is_tty(&self, communicate_allowed: bool) -> bool {
communicate_allowed && self.is_terminal()
}
@ -279,6 +288,15 @@ impl FileDescription for io::Stdout {
finish.call(ecx, result)
}
fn destroy<'tcx>(
self,
_self_id: FdId,
_communicate_allowed: bool,
_ecx: &mut MiriInterpCx<'tcx>,
) -> InterpResult<'tcx, io::Result<()>> {
interp_ok(Ok(()))
}
fn is_tty(&self, communicate_allowed: bool) -> bool {
communicate_allowed && self.is_terminal()
}
@ -289,6 +307,15 @@ impl FileDescription for io::Stderr {
"stderr"
}
fn destroy<'tcx>(
self,
_self_id: FdId,
_communicate_allowed: bool,
_ecx: &mut MiriInterpCx<'tcx>,
) -> InterpResult<'tcx, io::Result<()>> {
interp_ok(Ok(()))
}
fn write<'tcx>(
self: FileDescriptionRef<Self>,
_communicate_allowed: bool,
@ -436,6 +463,15 @@ impl FileDescription for NullOutput {
// We just don't write anything, but report to the user that we did.
finish.call(ecx, Ok(len))
}
fn destroy<'tcx>(
self,
_self_id: FdId,
_communicate_allowed: bool,
_ecx: &mut MiriInterpCx<'tcx>,
) -> InterpResult<'tcx, io::Result<()>> {
interp_ok(Ok(()))
}
}
/// Internal type of a file-descriptor - this is what [`FdTable`] expects

View file

@ -1,10 +0,0 @@
//@ignore-target: windows # No libc IO on Windows
//@compile-flags: -Zmiri-disable-isolation
// FIXME: standard handles cannot be closed (https://github.com/rust-lang/rust/issues/40032)
fn main() {
unsafe {
libc::close(1); //~ ERROR: cannot close stdout
}
}

View file

@ -1,12 +0,0 @@
error: unsupported operation: cannot close stdout
--> tests/fail-dep/libc/fs/close_stdout.rs:LL:CC
|
LL | libc::close(1);
| ^^^^^^^^^^^^^^ unsupported operation occurred here
|
= help: this is likely not a bug in the program; it indicates that the program performed an operation that Miri does not support
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error

View file

@ -48,6 +48,7 @@ fn main() {
test_nofollow_not_symlink();
#[cfg(target_os = "macos")]
test_ioctl();
test_close_stdout();
}
fn test_file_open_unix_allow_two_args() {
@ -579,3 +580,11 @@ fn test_ioctl() {
assert_eq!(libc::ioctl(fd, libc::FIOCLEX), 0);
}
}
fn test_close_stdout() {
// This is std library UB, but that's not relevant since we're
// only interacting with libc here.
unsafe {
libc::close(1);
}
}