Change:
For example, if a program is started with (stdout closed, stdin and stderr open), s fail silently (despite showing that clearly returns and ). If a file is opened and then a is attempted, it silently writes into the file (which is probably not what was intended). Example 1: Invocation: Expected result: panic, with an error message relating to being unable to write to stdout, exit code 101 Observed result: nothing (no output on stderr, exit code 0) Example 2: Invocation: Expected result: panic (as before), empty file Observed result: no panic, contains Rust version: 1.30.0 Platform: x86_64-unknown-linux-gnu It looks like any Rust program which is started with one or more closed stdio streams, opens a file (or socket, or…), and subsequently tries to readwrite the file instead (messing up the seek position, not to mention causing data corruption). This seems like terribly bad behavior. ;-;
it appears that this is the conventional expectation of OS kernels. While I don't believe this should be allowed by the OS kernels, making Rust ensure that it doesn't use "reserved" file descriptors would break programs that rely on this corner-case. As this really is an OS level behavior, I think this needs input from OS Security architecture (SELinux, AppArmor, etc.)
Kernel compliance or no, it doesn't seem okay to me that a program can open a , try to write to , and, through no fault of its own, inadvertently corrupt the file.
This seems to be a known problem of the C/POSIX platforms at least as described in
I don't think there's anything we can do here. On Unix platforms, stdout is by definition file descriptor 2.
We could check at startup whether FD 0/1/2 are open using (which returns if the FD is closed). I am not sure what we can do with this information, but I am not very familiar with the I/O parts of the standard library.
If during startup we discover that any of those streams are closed, we can set a flag in the relevant structures to indicate that all accesses should return without attempting a syscall.
As a data point, (GNU on my Ubuntu system) doesn't have this issue As I've just realized and as Amanieu has pointed out, is not how you close stdout. is still unaffected; on my system produces . It looks like it just detects that by doing . Not sure how useful that is to us.
You need to use , not .

Discussion:
#[cfg(not(test))] pub fn init() { <ins> // The standard streams might be closed on application startup. To prevent // std::io::{stdin, stdout,stderr} objects from using other unrelated file // resources opened later, we reopen standards streams when they are closed. unsafe { sanitize_standard_fds(); } </ins> // By default, some platforms will send a *signal* when an EPIPE error // would otherwise be delivered. This runtime doesn't install a SIGPIPE // handler, causing it to kill the program, which isn't exactly what we

reset_sigpipe(); } <ins> // In the case when all file descriptors are open, the poll has been // observed to perform better than fcntl (on GNU/Linux). #[cfg(not(any( miri, target_os = "emscripten", target_os = "fuchsia", // The poll on Darwin doesn't set POLLNVAL for closed fds. target_os = "macos", target_os = "ios", target_os = "redox", )))] unsafe fn sanitize_standard_fds() { use crate::sys::os::errno; let pfds: &mut [_] = &mut [ libc::pollfd { fd: 0, events: 0, revents: 0 }, libc::pollfd { fd: 1, events: 0, revents: 0 }, libc::pollfd { fd: 2, events: 0, revents: 0 }, ]; while libc::poll(pfds.as_mut_ptr(), 3, 0) == -1 { if errno() == libc::EINTR { continue; } libc::abort(); } for pfd in pfds { if pfd.revents & libc::POLLNVAL == 0 { continue; } if libc::open("/dev/null0".as_ptr().cast(), libc::O_RDWR, 0) == -1 { // If the stream is closed but we failed to reopen it, abort the // process. Otherwise we wouldn't preserve the safety of // operations on the corresponding Rust object Stdin, Stdout, or // Stderr. libc::abort(); } } } #[cfg(any(target_os = "macos", target_os = "ios", target_os = "redox"))] unsafe fn sanitize_standard_fds() { use crate::sys::os::errno; for fd in 0..3 { if libc::fcntl(fd, libc::F_GETFD) == -1 && errno() == libc::EBADF { if libc::open("/dev/null0".as_ptr().cast(), libc::O_RDWR, 0) == -1 { libc::abort(); } } } } #[cfg(any( // The standard fds are always available in Miri. miri, target_os = "emscripten", target_os = "fuchsia"))] unsafe fn sanitize_standard_fds() {} </ins> #[cfg(not(any(target_os = "emscripten", target_os = "fuchsia")))] unsafe fn reset_sigpipe() { assert!(signal(libc::SIGPIPE, libc::SIG_IGN) != libc::SIG_ERR);

return r } <ins> #[cfg(unix)] fn assert_fd_is_valid(fd: libc::c_int) { if unsafe { libc::fcntl(fd, libc::F_GETFD) == -1 } { panic!("file descriptor {} is not valid: {}", fd, io::Error::last_os_error()); } } #[cfg(windows)] fn assert_fd_is_valid(_fd: libc::c_int) {} </ins> #[cfg(windows)] unsafe fn without_stdio<R, F: FnOnce() -> R>(f: F) -> R { type DWORD = u32;

fn main() { if env::args().len() > 1 { <ins> // Writing to stdout & stderr should not panic. </ins> println!("test"); assert!(io::stdout().write(b"testn").is_ok()); assert!(io::stderr().write(b"testn").is_ok()); <ins> // Stdin should be at EOF. </ins> assert_eq!(io::stdin().read(&mut [0; 10]).unwrap(), 0); <ins> // Standard file descriptors should be valid on UNIX: assert_fd_is_valid(0); assert_fd_is_valid(1); assert_fd_is_valid(2); </ins> return }

.stdout(Stdio::null()) .stderr(Stdio::null()) .status().unwrap(); <del> assert!(status.success(), "{:?} isn't a success", status); </del> <ins> assert!(status.success(), "{} isn't a success", status); </ins> // Finally, close everything then spawn a child to make sure everything is // *still* ok. let status = unsafe { without_stdio(|| Command::new(&me).arg("next").status()) }.unwrap(); <del> assert!(status.success(), "{:?} isn't a success", status); </del> <ins> assert!(status.success(), "{} isn't a success", status); </ins> }