Skip to content

Commit 58e709a

Browse files
Rollup merge of rust-lang#159961 - RalfJung:miri-sanitize_standard_fds, r=clarfonthey
sanitize_standard_fds: Miri supports poll now However, so far it doesn't support it on the standard FDs (0..=2). So let's support those in poll and then remove a Miri special case in std.
2 parents 411c320 + a4caad8 commit 58e709a

6 files changed

Lines changed: 160 additions & 40 deletions

File tree

library/std/src/sys/pal/unix/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ pub unsafe fn init(argc: isize, argv: *const *const u8, sigpipe: u8) {
7676

7777
// fast path with a single syscall for systems with poll()
7878
#[cfg(not(any(
79-
miri, // no `poll`
8079
target_os = "emscripten",
8180
target_os = "fuchsia",
8281
target_os = "vxworks",

src/tools/miri/src/shims/files.rs

Lines changed: 127 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,19 @@ pub trait FileDescription: std::fmt::Debug + FileDescriptionExt {
227227
}
228228
}
229229

230-
impl FileDescription for io::Stdin {
230+
#[derive(Debug)]
231+
struct Stdin {
232+
stdin: io::Stdin,
233+
watched: ReadinessWatched,
234+
}
235+
236+
impl Stdin {
237+
fn new() -> Self {
238+
Self { stdin: io::stdin(), watched: ReadinessWatched::default() }
239+
}
240+
}
241+
242+
impl FileDescription for Stdin {
231243
fn name(&self) -> &'static str {
232244
"stdin"
233245
}
@@ -245,17 +257,42 @@ impl FileDescription for io::Stdin {
245257
helpers::isolation_abort_error("`read` from stdin")?;
246258
}
247259

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

253265
fn is_tty(&self, communicate_allowed: bool) -> bool {
254-
communicate_allowed && self.is_terminal()
266+
communicate_allowed && self.stdin.is_terminal()
267+
}
268+
269+
fn readiness_watched(&self) -> Option<&ReadinessWatched> {
270+
Some(&self.watched)
255271
}
272+
273+
fn readiness(&self) -> Readiness {
274+
// Stdin is readable (we never return EWOULDBLOCK above) and also writable (since that never
275+
// blocks either). This matches what we see on Linux.
276+
let mut readiness = Readiness::EMPTY;
277+
readiness.readable = true;
278+
readiness.writable = true;
279+
readiness
280+
}
281+
}
282+
283+
#[derive(Debug)]
284+
struct Stdout {
285+
stdout: io::Stdout,
286+
watched: ReadinessWatched,
256287
}
257288

258-
impl FileDescription for io::Stdout {
289+
impl Stdout {
290+
fn new() -> Self {
291+
Self { stdout: io::stdout(), watched: ReadinessWatched::default() }
292+
}
293+
}
294+
295+
impl FileDescription for Stdout {
259296
fn name(&self) -> &'static str {
260297
"stdout"
261298
}
@@ -269,7 +306,7 @@ impl FileDescription for io::Stdout {
269306
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
270307
) -> InterpResult<'tcx> {
271308
// We allow writing to stdout even with isolation enabled.
272-
let result = ecx.write_to_host(&*self, len, ptr)?;
309+
let result = ecx.write_to_host(&self.stdout, len, ptr)?;
273310
// Stdout is buffered, flush to make sure it appears on the
274311
// screen. This is the write() syscall of the interpreted
275312
// program, we want it to correspond to a write() syscall on
@@ -281,11 +318,34 @@ impl FileDescription for io::Stdout {
281318
}
282319

283320
fn is_tty(&self, communicate_allowed: bool) -> bool {
284-
communicate_allowed && self.is_terminal()
321+
communicate_allowed && self.stdout.is_terminal()
322+
}
323+
324+
fn readiness_watched(&self) -> Option<&ReadinessWatched> {
325+
Some(&self.watched)
285326
}
327+
328+
fn readiness(&self) -> Readiness {
329+
// stdout can always be written (we never return EWOULDBLOCK there) and never be read.
330+
let mut readiness = Readiness::EMPTY;
331+
readiness.writable = true;
332+
readiness
333+
}
334+
}
335+
336+
#[derive(Debug)]
337+
struct Stderr {
338+
stderr: io::Stderr,
339+
watched: ReadinessWatched,
286340
}
287341

288-
impl FileDescription for io::Stderr {
342+
impl Stderr {
343+
fn new() -> Self {
344+
Self { stderr: io::stderr(), watched: ReadinessWatched::default() }
345+
}
346+
}
347+
348+
impl FileDescription for Stderr {
289349
fn name(&self) -> &'static str {
290350
"stderr"
291351
}
@@ -299,13 +359,65 @@ impl FileDescription for io::Stderr {
299359
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
300360
) -> InterpResult<'tcx> {
301361
// We allow writing to stderr even with isolation enabled.
302-
let result = ecx.write_to_host(&*self, len, ptr)?;
362+
let result = ecx.write_to_host(&self.stderr, len, ptr)?;
303363
// No need to flush, stderr is not buffered.
304364
finish.call(ecx, result)
305365
}
306366

307367
fn is_tty(&self, communicate_allowed: bool) -> bool {
308-
communicate_allowed && self.is_terminal()
368+
communicate_allowed && self.stderr.is_terminal()
369+
}
370+
371+
fn readiness_watched(&self) -> Option<&ReadinessWatched> {
372+
Some(&self.watched)
373+
}
374+
375+
fn readiness(&self) -> Readiness {
376+
// stderr can always be written (we never return EWOULDBLOCK there) and never be read.
377+
let mut readiness = Readiness::EMPTY;
378+
readiness.writable = true;
379+
readiness
380+
}
381+
}
382+
383+
/// Like /dev/null
384+
#[derive(Debug)]
385+
pub struct NullOutput {
386+
watched: ReadinessWatched,
387+
}
388+
389+
impl NullOutput {
390+
fn new() -> Self {
391+
Self { watched: ReadinessWatched::default() }
392+
}
393+
}
394+
395+
impl FileDescription for NullOutput {
396+
fn name(&self) -> &'static str {
397+
"null output"
398+
}
399+
400+
fn write<'tcx>(
401+
self: FileDescriptionRef<Self>,
402+
_communicate_allowed: bool,
403+
_ptr: Pointer,
404+
len: usize,
405+
ecx: &mut MiriInterpCx<'tcx>,
406+
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
407+
) -> InterpResult<'tcx> {
408+
// We just don't write anything, but report to the user that we did.
409+
finish.call(ecx, Ok(len))
410+
}
411+
412+
fn readiness_watched(&self) -> Option<&ReadinessWatched> {
413+
Some(&self.watched)
414+
}
415+
416+
fn readiness(&self) -> Readiness {
417+
// null output can always be written (we never return EWOULDBLOCK there) and never be read.
418+
let mut readiness = Readiness::EMPTY;
419+
readiness.writable = true;
420+
readiness
309421
}
310422
}
311423

@@ -416,28 +528,6 @@ impl FileDescription for DirHandle {
416528
}
417529
}
418530

419-
/// Like /dev/null
420-
#[derive(Debug)]
421-
pub struct NullOutput;
422-
423-
impl FileDescription for NullOutput {
424-
fn name(&self) -> &'static str {
425-
"stderr and stdout"
426-
}
427-
428-
fn write<'tcx>(
429-
self: FileDescriptionRef<Self>,
430-
_communicate_allowed: bool,
431-
_ptr: Pointer,
432-
len: usize,
433-
ecx: &mut MiriInterpCx<'tcx>,
434-
finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
435-
) -> InterpResult<'tcx> {
436-
// We just don't write anything, but report to the user that we did.
437-
finish.call(ecx, Ok(len))
438-
}
439-
}
440-
441531
/// Internal type of a file-descriptor - this is what [`FdTable`] expects
442532
pub type FdNum = i32;
443533

@@ -461,13 +551,13 @@ impl FdTable {
461551
}
462552
pub(crate) fn init(mute_stdout_stderr: bool) -> FdTable {
463553
let mut fds = FdTable::new();
464-
fds.insert_new(io::stdin());
554+
fds.insert_new(Stdin::new());
465555
if mute_stdout_stderr {
466-
assert_eq!(fds.insert_new(NullOutput), 1);
467-
assert_eq!(fds.insert_new(NullOutput), 2);
556+
assert_eq!(fds.insert_new(NullOutput::new()), 1);
557+
assert_eq!(fds.insert_new(NullOutput::new()), 2);
468558
} else {
469-
assert_eq!(fds.insert_new(io::stdout()), 1);
470-
assert_eq!(fds.insert_new(io::stderr()), 2);
559+
assert_eq!(fds.insert_new(Stdout::new()), 1);
560+
assert_eq!(fds.insert_new(Stderr::new()), 2);
471561
}
472562
fds
473563
}

src/tools/miri/tests/pass-dep/libc/libc-fs.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use libc_utils::{errno_check, errno_result};
2121
fn main() {
2222
test_dup();
2323
test_dup_stdout_stderr();
24+
test_fcntl_getfd();
2425
test_canonicalize_too_long();
2526
test_rename();
2627
test_ftruncate::<libc::off_t>(libc::ftruncate);
@@ -310,6 +311,13 @@ fn test_dup() {
310311
}
311312
}
312313

314+
fn test_fcntl_getfd() {
315+
// This should succeed for FDs that exist and fail for those that do not.
316+
let _success = errno_result(unsafe { libc::fcntl(0, libc::F_GETFD) }).unwrap();
317+
let err = errno_result(unsafe { libc::fcntl(1337, libc::F_GETFD) }).unwrap_err();
318+
assert_eq!(err.raw_os_error().unwrap(), libc::EBADF);
319+
}
320+
313321
fn test_canonicalize_too_long() {
314322
// Make sure we get an error for long paths.
315323
let too_long = "x/".repeat(libc::PATH_MAX.try_into().unwrap());
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//! Ensure we can poll the std handles, both the normal ones and the "null" ones.
2+
//@ignore-target: windows # no libc
3+
//@revisions: normal null
4+
//@[null]compile-flags: -Zmiri-mute-stdout-stderr
5+
//@run-native
6+
7+
#[path = "../../utils/libc.rs"]
8+
mod libc_utils;
9+
use libc_utils::*;
10+
11+
fn main() {
12+
let pfds: &mut [_] = &mut [
13+
libc::pollfd { fd: 0, events: libc::POLLOUT | libc::POLLIN, revents: 0 },
14+
libc::pollfd { fd: 1, events: libc::POLLOUT | libc::POLLIN, revents: 0 },
15+
libc::pollfd { fd: 2, events: libc::POLLOUT | libc::POLLIN, revents: 0 },
16+
];
17+
let num = errno_result(unsafe { libc::poll(pfds.as_mut_ptr(), 3, 0) }).unwrap();
18+
assert_eq!(num, 3);
19+
20+
assert_eq!(pfds[0].revents, libc::POLLIN | libc::POLLOUT);
21+
assert_eq!(pfds[1].revents, libc::POLLOUT);
22+
assert_eq!(pfds[2].revents, libc::POLLOUT);
23+
}

src/tools/miri/tests/pass-dep/libc/libc-socket.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,7 @@ fn test_sockopt_rcvtimeo() {
908908
/// the operation is finished, even when the socket file _descriptor_ gets
909909
/// closed in the mean time.
910910
fn test_unblock_after_socket_close() {
911-
// MacOS behaves different (`read` errors with EBADFD when the file description is closed)
911+
// MacOS behaves different (`read` errors with EBADF when the file description is closed)
912912
// so we skip the test when we are run on a native macOS target.
913913
if cfg!(not(miri)) && cfg!(target_os = "macos") {
914914
return;

src/tools/miri/tests/pass-dep/libc/libc-socketpair.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ fn test_blocking_write() {
185185
/// the operation is finished, even when the socket file _descriptor_ gets
186186
/// closed in the mean time.
187187
fn test_unblock_after_socket_close() {
188-
// MacOS behaves different (`read` errors with EBADFD when the file description is closed)
188+
// MacOS behaves different (`read` errors with EBADF when the file description is closed)
189189
// so we skip the test when we are run on a native macOS target.
190190
if cfg!(not(miri)) && cfg!(target_os = "macos") {
191191
return;

0 commit comments

Comments
 (0)