Skip to content

Commit 44e083e

Browse files
committed
Reopen standard streams when they are closed on Unix
The syscalls returning a new file descriptors generally use lowest-numbered file descriptor not currently opened, without any exceptions for those corresponding to the standard streams. Previously when any of standard streams has been closed before starting the application, operations on std::io::{stderr,stdin,stdout} objects were likely to operate on other logically unrelated file resources opened afterwards. Avoid the issue by reopening the standard streams when they are closed.
1 parent 87d262a commit 44e083e

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ pub use crate::sys_common::os_str_bytes as os_str;
7575

7676
#[cfg(not(test))]
7777
pub fn init() {
78+
// The standard streams might be closed on application startup. To prevent
79+
// std::io::{stdin, stdout,stderr} objects from using other unrelated file
80+
// resources opened later, we reopen standards streams when they are closed.
81+
unsafe {
82+
sanitize_standard_fds();
83+
}
84+
7885
// By default, some platforms will send a *signal* when an EPIPE error
7986
// would otherwise be delivered. This runtime doesn't install a SIGPIPE
8087
// handler, causing it to kill the program, which isn't exactly what we
@@ -86,6 +93,56 @@ pub fn init() {
8693
reset_sigpipe();
8794
}
8895

96+
// In the case when all file descriptors are open, the poll has been
97+
// observed to perform better than fcntl (on GNU/Linux).
98+
#[cfg(not(any(
99+
target_os = "emscripten",
100+
target_os = "fuchsia",
101+
// The poll on Darwin doesn't set POLLNVAL for closed fds.
102+
target_os = "macos",
103+
target_os = "ios",
104+
target_os = "redox",
105+
)))]
106+
unsafe fn sanitize_standard_fds() {
107+
use crate::sys::os::errno;
108+
let pfds: &mut [_] = &mut [
109+
libc::pollfd { fd: 0, events: 0, revents: 0 },
110+
libc::pollfd { fd: 1, events: 0, revents: 0 },
111+
libc::pollfd { fd: 2, events: 0, revents: 0 },
112+
];
113+
while libc::poll(pfds.as_mut_ptr(), 3, 0) == -1 {
114+
if errno() == libc::EINTR {
115+
continue;
116+
}
117+
libc::abort();
118+
}
119+
for pfd in pfds {
120+
if pfd.revents & libc::POLLNVAL == 0 {
121+
continue;
122+
}
123+
if libc::open("/dev/null\0".as_ptr().cast(), libc::O_RDWR, 0) == -1 {
124+
// If the stream is closed but we failed to reopen it, abort the
125+
// process. Otherwise we wouldn't preserve the safety of
126+
// operations on the corresponding Rust object Stdin, Stdout, or
127+
// Stderr.
128+
libc::abort();
129+
}
130+
}
131+
}
132+
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "redox"))]
133+
unsafe fn sanitize_standard_fds() {
134+
use crate::sys::os::errno;
135+
for fd in 0..3 {
136+
if libc::fcntl(fd, libc::F_GETFD) == -1 && errno() == libc::EBADF {
137+
if libc::open("/dev/null\0".as_ptr().cast(), libc::O_RDWR, 0) == -1 {
138+
libc::abort();
139+
}
140+
}
141+
}
142+
}
143+
#[cfg(any(target_os = "emscripten", target_os = "fuchsia"))]
144+
unsafe fn sanitize_standard_fds() {}
145+
89146
#[cfg(not(any(target_os = "emscripten", target_os = "fuchsia")))]
90147
unsafe fn reset_sigpipe() {
91148
assert!(signal(libc::SIGPIPE, libc::SIG_IGN) != libc::SIG_ERR);

src/test/ui/closed-std-fds.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Verifies that std provides replacement for the standard file descriptors when they are missing.
2+
//
3+
// run-pass
4+
// ignore-windows unix specific test
5+
// ignore-cloudabi no processes
6+
// ignore-emscripten no processes
7+
// ignore-sgx no processes
8+
9+
#![feature(rustc_private)]
10+
extern crate libc;
11+
12+
use std::io::{self, Read};
13+
use std::os::unix::process::CommandExt;
14+
use std::process::Command;
15+
16+
fn main() {
17+
let mut args = std::env::args();
18+
let argv0 = args.next().expect("argv0");
19+
match args.next().as_deref() {
20+
Some("child") => child(),
21+
None => parent(&argv0),
22+
_ => unreachable!(),
23+
}
24+
}
25+
26+
fn parent(argv0: &str) {
27+
let status = unsafe { Command::new(argv0)
28+
.arg("child")
29+
.pre_exec(close_std_fds_on_exec)
30+
.status()
31+
.expect("failed to execute child process")
32+
};
33+
if !status.success() {
34+
panic!("child failed with {}", status);
35+
}
36+
}
37+
38+
fn close_std_fds_on_exec() -> io::Result<()> {
39+
for fd in 0..3 {
40+
if unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) == -1 } {
41+
return Err(io::Error::last_os_error())
42+
}
43+
}
44+
Ok(())
45+
}
46+
47+
fn child() {
48+
// Standard file descriptors should be valid.
49+
assert_fd_is_valid(0);
50+
assert_fd_is_valid(1);
51+
assert_fd_is_valid(2);
52+
53+
// Writing to stdout & stderr should not panic.
54+
println!("a");
55+
println!("b");
56+
eprintln!("c");
57+
eprintln!("d");
58+
59+
// Stdin should be at EOF.
60+
let mut buffer = Vec::new();
61+
let n = io::stdin().read_to_end(&mut buffer).unwrap();
62+
assert_eq!(n, 0);
63+
}
64+
65+
fn assert_fd_is_valid(fd: libc::c_int) {
66+
if unsafe { libc::fcntl(fd, libc::F_GETFD) == -1 } {
67+
panic!("file descriptor {} is not valid {}", fd, io::Error::last_os_error());
68+
}
69+
}

0 commit comments

Comments
 (0)