Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion library/std/src/sys/pal/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ pub unsafe fn init(argc: isize, argv: *const *const u8, sigpipe: u8) {

// fast path with a single syscall for systems with poll()
#[cfg(not(any(
miri, // no `poll`
target_os = "emscripten",
target_os = "fuchsia",
target_os = "vxworks",
Expand Down
164 changes: 127 additions & 37 deletions src/tools/miri/src/shims/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,19 @@ pub trait FileDescription: std::fmt::Debug + FileDescriptionExt {
}
}

impl FileDescription for io::Stdin {
#[derive(Debug)]
struct Stdin {
stdin: io::Stdin,
watched: ReadinessWatched,
}

impl Stdin {
fn new() -> Self {
Self { stdin: io::stdin(), watched: ReadinessWatched::default() }
}
}

impl FileDescription for Stdin {
fn name(&self) -> &'static str {
"stdin"
}
Expand All @@ -245,17 +257,42 @@ impl FileDescription for io::Stdin {
helpers::isolation_abort_error("`read` from stdin")?;
}

let mut stdin = &*self;
let result = ecx.read_from_host(|buf| stdin.read(buf), len, ptr)?;
// FIXME: this can block on the host, halting the entire interpreter.
let result = ecx.read_from_host(|buf| (&mut &self.stdin).read(buf), len, ptr)?;
finish.call(ecx, result)
}

fn is_tty(&self, communicate_allowed: bool) -> bool {
communicate_allowed && self.is_terminal()
communicate_allowed && self.stdin.is_terminal()
}

fn readiness_watched(&self) -> Option<&ReadinessWatched> {
Some(&self.watched)
}

fn readiness(&self) -> Readiness {
// Stdin is readable (we never return EWOULDBLOCK above) and also writable (since that never
// blocks either). This matches what we see on Linux.
let mut readiness = Readiness::EMPTY;
readiness.readable = true;
readiness.writable = true;
readiness
}
}

#[derive(Debug)]
struct Stdout {
stdout: io::Stdout,
watched: ReadinessWatched,
}

impl FileDescription for io::Stdout {
impl Stdout {
fn new() -> Self {
Self { stdout: io::stdout(), watched: ReadinessWatched::default() }
}
}

impl FileDescription for Stdout {
fn name(&self) -> &'static str {
"stdout"
}
Expand All @@ -269,7 +306,7 @@ impl FileDescription for io::Stdout {
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
) -> InterpResult<'tcx> {
// We allow writing to stdout even with isolation enabled.
let result = ecx.write_to_host(&*self, len, ptr)?;
let result = ecx.write_to_host(&self.stdout, len, ptr)?;
// Stdout is buffered, flush to make sure it appears on the
// screen. This is the write() syscall of the interpreted
// program, we want it to correspond to a write() syscall on
Expand All @@ -281,11 +318,34 @@ impl FileDescription for io::Stdout {
}

fn is_tty(&self, communicate_allowed: bool) -> bool {
communicate_allowed && self.is_terminal()
communicate_allowed && self.stdout.is_terminal()
}

fn readiness_watched(&self) -> Option<&ReadinessWatched> {
Some(&self.watched)
}

fn readiness(&self) -> Readiness {
// stdout can always be written (we never return EWOULDBLOCK there) and never be read.
let mut readiness = Readiness::EMPTY;
readiness.writable = true;
readiness
}
}

#[derive(Debug)]
struct Stderr {
stderr: io::Stderr,
watched: ReadinessWatched,
}

impl FileDescription for io::Stderr {
impl Stderr {
fn new() -> Self {
Self { stderr: io::stderr(), watched: ReadinessWatched::default() }
}
}

impl FileDescription for Stderr {
fn name(&self) -> &'static str {
"stderr"
}
Expand All @@ -299,13 +359,65 @@ impl FileDescription for io::Stderr {
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
) -> InterpResult<'tcx> {
// We allow writing to stderr even with isolation enabled.
let result = ecx.write_to_host(&*self, len, ptr)?;
let result = ecx.write_to_host(&self.stderr, len, ptr)?;
// No need to flush, stderr is not buffered.
finish.call(ecx, result)
}

fn is_tty(&self, communicate_allowed: bool) -> bool {
communicate_allowed && self.is_terminal()
communicate_allowed && self.stderr.is_terminal()
}

fn readiness_watched(&self) -> Option<&ReadinessWatched> {
Some(&self.watched)
}

fn readiness(&self) -> Readiness {
// stderr can always be written (we never return EWOULDBLOCK there) and never be read.
let mut readiness = Readiness::EMPTY;
readiness.writable = true;
readiness
}
}

/// Like /dev/null
#[derive(Debug)]
pub struct NullOutput {
watched: ReadinessWatched,
}

impl NullOutput {
fn new() -> Self {
Self { watched: ReadinessWatched::default() }
}
}

impl FileDescription for NullOutput {
fn name(&self) -> &'static str {
"null output"
}

fn write<'tcx>(
self: FileDescriptionRef<Self>,
_communicate_allowed: bool,
_ptr: Pointer,
len: usize,
ecx: &mut MiriInterpCx<'tcx>,
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
) -> InterpResult<'tcx> {
// We just don't write anything, but report to the user that we did.
finish.call(ecx, Ok(len))
}

fn readiness_watched(&self) -> Option<&ReadinessWatched> {
Some(&self.watched)
}

fn readiness(&self) -> Readiness {
// null output can always be written (we never return EWOULDBLOCK there) and never be read.
let mut readiness = Readiness::EMPTY;
readiness.writable = true;
readiness
}
}

Expand Down Expand Up @@ -416,28 +528,6 @@ impl FileDescription for DirHandle {
}
}

/// Like /dev/null
#[derive(Debug)]
pub struct NullOutput;

impl FileDescription for NullOutput {
fn name(&self) -> &'static str {
"stderr and stdout"
}

fn write<'tcx>(
self: FileDescriptionRef<Self>,
_communicate_allowed: bool,
_ptr: Pointer,
len: usize,
ecx: &mut MiriInterpCx<'tcx>,
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
) -> InterpResult<'tcx> {
// We just don't write anything, but report to the user that we did.
finish.call(ecx, Ok(len))
}
}

/// Internal type of a file-descriptor - this is what [`FdTable`] expects
pub type FdNum = i32;

Expand All @@ -461,13 +551,13 @@ impl FdTable {
}
pub(crate) fn init(mute_stdout_stderr: bool) -> FdTable {
let mut fds = FdTable::new();
fds.insert_new(io::stdin());
fds.insert_new(Stdin::new());
if mute_stdout_stderr {
assert_eq!(fds.insert_new(NullOutput), 1);
assert_eq!(fds.insert_new(NullOutput), 2);
assert_eq!(fds.insert_new(NullOutput::new()), 1);
assert_eq!(fds.insert_new(NullOutput::new()), 2);
} else {
assert_eq!(fds.insert_new(io::stdout()), 1);
assert_eq!(fds.insert_new(io::stderr()), 2);
assert_eq!(fds.insert_new(Stdout::new()), 1);
assert_eq!(fds.insert_new(Stderr::new()), 2);
}
fds
}
Expand Down
8 changes: 8 additions & 0 deletions src/tools/miri/tests/pass-dep/libc/libc-fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use libc_utils::{errno_check, errno_result};
fn main() {
test_dup();
test_dup_stdout_stderr();
test_fcntl_getfd();
test_canonicalize_too_long();
test_rename();
test_ftruncate::<libc::off_t>(libc::ftruncate);
Expand Down Expand Up @@ -310,6 +311,13 @@ fn test_dup() {
}
}

fn test_fcntl_getfd() {
// This should succeed for FDs that exist and fail for those that do not.
let _success = errno_result(unsafe { libc::fcntl(0, libc::F_GETFD) }).unwrap();
let err = errno_result(unsafe { libc::fcntl(1337, libc::F_GETFD) }).unwrap_err();
assert_eq!(err.raw_os_error().unwrap(), libc::EBADF);
}

fn test_canonicalize_too_long() {
// Make sure we get an error for long paths.
let too_long = "x/".repeat(libc::PATH_MAX.try_into().unwrap());
Expand Down
23 changes: 23 additions & 0 deletions src/tools/miri/tests/pass-dep/libc/libc-poll-std-handles.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! Ensure we can poll the std handles, both the normal ones and the "null" ones.
//@ignore-target: windows # no libc
//@revisions: normal null
//@[null]compile-flags: -Zmiri-mute-stdout-stderr
//@run-native

#[path = "../../utils/libc.rs"]
mod libc_utils;
use libc_utils::*;

fn main() {
let pfds: &mut [_] = &mut [
libc::pollfd { fd: 0, events: libc::POLLOUT | libc::POLLIN, revents: 0 },
libc::pollfd { fd: 1, events: libc::POLLOUT | libc::POLLIN, revents: 0 },
libc::pollfd { fd: 2, events: libc::POLLOUT | libc::POLLIN, revents: 0 },
];
let num = errno_result(unsafe { libc::poll(pfds.as_mut_ptr(), 3, 0) }).unwrap();
assert_eq!(num, 3);

assert_eq!(pfds[0].revents, libc::POLLIN | libc::POLLOUT);
assert_eq!(pfds[1].revents, libc::POLLOUT);
assert_eq!(pfds[2].revents, libc::POLLOUT);
}
2 changes: 1 addition & 1 deletion src/tools/miri/tests/pass-dep/libc/libc-socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ fn test_sockopt_rcvtimeo() {
/// the operation is finished, even when the socket file _descriptor_ gets
/// closed in the mean time.
fn test_unblock_after_socket_close() {
// MacOS behaves different (`read` errors with EBADFD when the file description is closed)
// MacOS behaves different (`read` errors with EBADF when the file description is closed)

@clarfonthey clarfonthey Jul 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frustrated that these both are very similar errors that exist and mean slightly different things, but want to verify this is actually correct here. Based upon the Linux manual:

EBADF Bad file descriptor (POSIX.1-2001).
EBADFD File descriptor in bad state.

It feels like EBADFD is correct, but from what it seems, MacOS only has EBADF? And just want to verify this is correct since I'm not 100% sure.

View changes since the review

@RalfJung RalfJung Jul 26, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only just realized that those two errors both exist and are different. I am fairly sure that we saw EBADF on macOS, not EBADFD, as EBADF is usually returned when calling read on a closed FD so that would make sense. Also as you say macOS doesn't even seem to have EBADFD. I don't have macOS machine so verifying this isn't entirely trivial and doesn't seem worth it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair, then. My verification after poking around was the macos-errno crate which does indeed list EBADF and not EBADFD, so, I'll say that's good enough.

// so we skip the test when we are run on a native macOS target.
if cfg!(not(miri)) && cfg!(target_os = "macos") {
return;
Expand Down
2 changes: 1 addition & 1 deletion src/tools/miri/tests/pass-dep/libc/libc-socketpair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ fn test_blocking_write() {
/// the operation is finished, even when the socket file _descriptor_ gets
/// closed in the mean time.
fn test_unblock_after_socket_close() {
// MacOS behaves different (`read` errors with EBADFD when the file description is closed)
// MacOS behaves different (`read` errors with EBADF when the file description is closed)
// so we skip the test when we are run on a native macOS target.
if cfg!(not(miri)) && cfg!(target_os = "macos") {
return;
Expand Down
Loading