Skip to content

Commit 9670a30

Browse files
committed
Auto merge of rust-lang#2609 - RalfJung:is_terminal, r=RalfJung
Use is_terminal to implement isatty This means that Linux targets on Windows hosts should give a correct reply. We still don't have an implementation for Windows targets though, that requires some tracking of handles. Fixes rust-lang/miri#2419 Also restructure our `fs` tests a bit to test the parts that don't need libc separately. `as_unix_host_fd` is now not used any more, but it could become useful again in the future so I kept it.
2 parents befc94e + 1c9f368 commit 9670a30

13 files changed

+230
-151
lines changed

Diff for: src/tools/miri/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#![feature(is_some_and)]
1010
#![feature(nonzero_ops)]
1111
#![feature(local_key_cell_methods)]
12+
#![feature(is_terminal)]
1213
// Configure clippy and other lints
1314
#![allow(
1415
clippy::collapsible_else_if,

Diff for: src/tools/miri/src/shims/unix/foreign_items.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
452452
"isatty" => {
453453
let [fd] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
454454
let result = this.isatty(fd)?;
455-
this.write_scalar(Scalar::from_i32(result), dest)?;
455+
this.write_scalar(result, dest)?;
456456
}
457457
"pthread_atfork" => {
458458
let [prepare, parent, child] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;

Diff for: src/tools/miri/src/shims/unix/fs.rs

+39-29
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::convert::TryInto;
44
use std::fs::{
55
read_dir, remove_dir, remove_file, rename, DirBuilder, File, FileType, OpenOptions, ReadDir,
66
};
7-
use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write};
7+
use std::io::{self, ErrorKind, IsTerminal, Read, Seek, SeekFrom, Write};
88
use std::path::{Path, PathBuf};
99
use std::time::SystemTime;
1010

@@ -65,6 +65,8 @@ trait FileDescriptor: std::fmt::Debug {
6565

6666
fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>>;
6767

68+
fn is_tty(&self) -> bool;
69+
6870
#[cfg(unix)]
6971
fn as_unix_host_fd(&self) -> Option<i32> {
7072
None
@@ -143,6 +145,10 @@ impl FileDescriptor for FileHandle {
143145
use std::os::unix::io::AsRawFd;
144146
Some(self.file.as_raw_fd())
145147
}
148+
149+
fn is_tty(&self) -> bool {
150+
self.file.is_terminal()
151+
}
146152
}
147153

148154
impl FileDescriptor for io::Stdin {
@@ -170,6 +176,10 @@ impl FileDescriptor for io::Stdin {
170176
fn as_unix_host_fd(&self) -> Option<i32> {
171177
Some(libc::STDIN_FILENO)
172178
}
179+
180+
fn is_tty(&self) -> bool {
181+
self.is_terminal()
182+
}
173183
}
174184

175185
impl FileDescriptor for io::Stdout {
@@ -202,6 +212,10 @@ impl FileDescriptor for io::Stdout {
202212
fn as_unix_host_fd(&self) -> Option<i32> {
203213
Some(libc::STDOUT_FILENO)
204214
}
215+
216+
fn is_tty(&self) -> bool {
217+
self.is_terminal()
218+
}
205219
}
206220

207221
impl FileDescriptor for io::Stderr {
@@ -227,12 +241,16 @@ impl FileDescriptor for io::Stderr {
227241
fn as_unix_host_fd(&self) -> Option<i32> {
228242
Some(libc::STDERR_FILENO)
229243
}
244+
245+
fn is_tty(&self) -> bool {
246+
self.is_terminal()
247+
}
230248
}
231249

232250
#[derive(Debug)]
233-
struct DummyOutput;
251+
struct NullOutput;
234252

235-
impl FileDescriptor for DummyOutput {
253+
impl FileDescriptor for NullOutput {
236254
fn name(&self) -> &'static str {
237255
"stderr and stdout"
238256
}
@@ -247,7 +265,11 @@ impl FileDescriptor for DummyOutput {
247265
}
248266

249267
fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
250-
Ok(Box::new(DummyOutput))
268+
Ok(Box::new(NullOutput))
269+
}
270+
271+
fn is_tty(&self) -> bool {
272+
false
251273
}
252274
}
253275

@@ -267,8 +289,8 @@ impl FileHandler {
267289
let mut handles: BTreeMap<_, Box<dyn FileDescriptor>> = BTreeMap::new();
268290
handles.insert(0i32, Box::new(io::stdin()));
269291
if mute_stdout_stderr {
270-
handles.insert(1i32, Box::new(DummyOutput));
271-
handles.insert(2i32, Box::new(DummyOutput));
292+
handles.insert(1i32, Box::new(NullOutput));
293+
handles.insert(2i32, Box::new(NullOutput));
272294
} else {
273295
handles.insert(1i32, Box::new(io::stdout()));
274296
handles.insert(2i32, Box::new(io::stderr()));
@@ -1662,35 +1684,23 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
16621684
}
16631685

16641686
#[cfg_attr(not(unix), allow(unused))]
1665-
fn isatty(&mut self, miri_fd: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
1687+
fn isatty(
1688+
&mut self,
1689+
miri_fd: &OpTy<'tcx, Provenance>,
1690+
) -> InterpResult<'tcx, Scalar<Provenance>> {
16661691
let this = self.eval_context_mut();
1667-
#[cfg(unix)]
1692+
// "returns 1 if fd is an open file descriptor referring to a terminal;
1693+
// otherwise 0 is returned, and errno is set to indicate the error"
16681694
if matches!(this.machine.isolated_op, IsolatedOp::Allow) {
1669-
let miri_fd = this.read_scalar(miri_fd)?.to_i32()?;
1670-
if let Some(host_fd) =
1671-
this.machine.file_handler.handles.get(&miri_fd).and_then(|fd| fd.as_unix_host_fd())
1672-
{
1673-
// "returns 1 if fd is an open file descriptor referring to a terminal;
1674-
// otherwise 0 is returned, and errno is set to indicate the error"
1675-
// SAFETY: isatty has no preconditions
1676-
let is_tty = unsafe { libc::isatty(host_fd) };
1677-
if is_tty == 0 {
1678-
let errno = std::io::Error::last_os_error()
1679-
.raw_os_error()
1680-
.map(Scalar::from_i32)
1681-
.unwrap();
1682-
this.set_last_error(errno)?;
1683-
}
1684-
return Ok(is_tty);
1695+
let fd = this.read_scalar(miri_fd)?.to_i32()?;
1696+
if this.machine.file_handler.handles.get(&fd).map(|fd| fd.is_tty()) == Some(true) {
1697+
return Ok(Scalar::from_i32(1));
16851698
}
16861699
}
1687-
// We are attemping to use a Unix interface on a non-Unix platform, or we are on a Unix
1688-
// platform and the passed file descriptor is not open, or isolation is enabled
1689-
// FIXME: It should be possible to emulate this at least on Windows by using
1690-
// GetConsoleMode.
1700+
// Fallback when the FD was not found or isolation is enabled.
16911701
let enotty = this.eval_libc("ENOTTY")?;
16921702
this.set_last_error(enotty)?;
1693-
Ok(0)
1703+
Ok(Scalar::from_i32(0))
16941704
}
16951705

16961706
fn realpath(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//@ignore-target-windows: no libc on Windows
2+
//@compile-flags: -Zmiri-isolation-error=warn-nobacktrace
3+
//@normalize-stderr-test: "(stat(x)?)" -> "$$STAT"
4+
5+
use std::ffi::CString;
6+
use std::fs;
7+
use std::io::{Error, ErrorKind};
8+
9+
fn main() {
10+
// test `fcntl`
11+
unsafe {
12+
assert_eq!(libc::fcntl(1, libc::F_DUPFD, 0), -1);
13+
assert_eq!(Error::last_os_error().raw_os_error(), Some(libc::EPERM));
14+
}
15+
16+
// test `readlink`
17+
let symlink_c_str = CString::new("foo.txt").unwrap();
18+
let mut buf = vec![0; "foo_link.txt".len() + 1];
19+
unsafe {
20+
assert_eq!(libc::readlink(symlink_c_str.as_ptr(), buf.as_mut_ptr(), buf.len()), -1);
21+
assert_eq!(Error::last_os_error().raw_os_error(), Some(libc::EACCES));
22+
}
23+
24+
// test `stat`
25+
assert_eq!(fs::metadata("foo.txt").unwrap_err().kind(), ErrorKind::PermissionDenied);
26+
// check that it is the right kind of `PermissionDenied`
27+
assert_eq!(Error::last_os_error().raw_os_error(), Some(libc::EACCES));
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
warning: `fcntl` was made to return an error due to isolation
2+
3+
warning: `readlink` was made to return an error due to isolation
4+
5+
warning: `$STAT` was made to return an error due to isolation
6+

Diff for: src/tools/miri/tests/pass-dep/shims/libc-fs.rs

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//@ignore-target-windows: no libc on Windows
2+
//@compile-flags: -Zmiri-disable-isolation
3+
4+
#![feature(io_error_more)]
5+
#![feature(io_error_uncategorized)]
6+
7+
use std::convert::TryInto;
8+
use std::ffi::CString;
9+
use std::fs::{canonicalize, remove_file, File};
10+
use std::io::{Error, ErrorKind, Write};
11+
use std::os::unix::ffi::OsStrExt;
12+
use std::path::PathBuf;
13+
14+
fn main() {
15+
test_dup_stdout_stderr();
16+
test_canonicalize_too_long();
17+
test_readlink();
18+
test_file_open_unix_allow_two_args();
19+
test_file_open_unix_needs_three_args();
20+
test_file_open_unix_extra_third_arg();
21+
}
22+
23+
fn tmp() -> PathBuf {
24+
std::env::var("MIRI_TEMP")
25+
.map(|tmp| {
26+
// MIRI_TEMP is set outside of our emulated
27+
// program, so it may have path separators that don't
28+
// correspond to our target platform. We normalize them here
29+
// before constructing a `PathBuf`
30+
31+
#[cfg(windows)]
32+
return PathBuf::from(tmp.replace("/", "\\"));
33+
34+
#[cfg(not(windows))]
35+
return PathBuf::from(tmp.replace("\\", "/"));
36+
})
37+
.unwrap_or_else(|_| std::env::temp_dir())
38+
}
39+
40+
/// Prepare: compute filename and make sure the file does not exist.
41+
fn prepare(filename: &str) -> PathBuf {
42+
let path = tmp().join(filename);
43+
// Clean the paths for robustness.
44+
remove_file(&path).ok();
45+
path
46+
}
47+
48+
/// Prepare like above, and also write some initial content to the file.
49+
fn prepare_with_content(filename: &str, content: &[u8]) -> PathBuf {
50+
let path = prepare(filename);
51+
let mut file = File::create(&path).unwrap();
52+
file.write(content).unwrap();
53+
path
54+
}
55+
56+
fn test_file_open_unix_allow_two_args() {
57+
let path = prepare_with_content("test_file_open_unix_allow_two_args.txt", &[]);
58+
59+
let mut name = path.into_os_string();
60+
name.push("\0");
61+
let name_ptr = name.as_bytes().as_ptr().cast::<libc::c_char>();
62+
let _fd = unsafe { libc::open(name_ptr, libc::O_RDONLY) };
63+
}
64+
65+
fn test_file_open_unix_needs_three_args() {
66+
let path = prepare_with_content("test_file_open_unix_needs_three_args.txt", &[]);
67+
68+
let mut name = path.into_os_string();
69+
name.push("\0");
70+
let name_ptr = name.as_bytes().as_ptr().cast::<libc::c_char>();
71+
let _fd = unsafe { libc::open(name_ptr, libc::O_CREAT, 0o666) };
72+
}
73+
74+
fn test_file_open_unix_extra_third_arg() {
75+
let path = prepare_with_content("test_file_open_unix_extra_third_arg.txt", &[]);
76+
77+
let mut name = path.into_os_string();
78+
name.push("\0");
79+
let name_ptr = name.as_bytes().as_ptr().cast::<libc::c_char>();
80+
let _fd = unsafe { libc::open(name_ptr, libc::O_RDONLY, 42) };
81+
}
82+
83+
fn test_dup_stdout_stderr() {
84+
let bytes = b"hello dup fd\n";
85+
unsafe {
86+
let new_stdout = libc::fcntl(1, libc::F_DUPFD, 0);
87+
let new_stderr = libc::fcntl(2, libc::F_DUPFD, 0);
88+
libc::write(new_stdout, bytes.as_ptr() as *const libc::c_void, bytes.len());
89+
libc::write(new_stderr, bytes.as_ptr() as *const libc::c_void, bytes.len());
90+
}
91+
}
92+
93+
fn test_canonicalize_too_long() {
94+
// Make sure we get an error for long paths.
95+
let too_long = "x/".repeat(libc::PATH_MAX.try_into().unwrap());
96+
assert!(canonicalize(too_long).is_err());
97+
}
98+
99+
fn test_readlink() {
100+
let bytes = b"Hello, World!\n";
101+
let path = prepare_with_content("miri_test_fs_link_target.txt", bytes);
102+
let expected_path = path.as_os_str().as_bytes();
103+
104+
let symlink_path = prepare("miri_test_fs_symlink.txt");
105+
std::os::unix::fs::symlink(&path, &symlink_path).unwrap();
106+
107+
// Test that the expected string gets written to a buffer of proper
108+
// length, and that a trailing null byte is not written.
109+
let symlink_c_str = CString::new(symlink_path.as_os_str().as_bytes()).unwrap();
110+
let symlink_c_ptr = symlink_c_str.as_ptr();
111+
112+
// Make the buf one byte larger than it needs to be,
113+
// and check that the last byte is not overwritten.
114+
let mut large_buf = vec![0xFF; expected_path.len() + 1];
115+
let res =
116+
unsafe { libc::readlink(symlink_c_ptr, large_buf.as_mut_ptr().cast(), large_buf.len()) };
117+
// Check that the resovled path was properly written into the buf.
118+
assert_eq!(&large_buf[..(large_buf.len() - 1)], expected_path);
119+
assert_eq!(large_buf.last(), Some(&0xFF));
120+
assert_eq!(res, large_buf.len() as isize - 1);
121+
122+
// Test that the resolved path is truncated if the provided buffer
123+
// is too small.
124+
let mut small_buf = [0u8; 2];
125+
let res =
126+
unsafe { libc::readlink(symlink_c_ptr, small_buf.as_mut_ptr().cast(), small_buf.len()) };
127+
assert_eq!(small_buf, &expected_path[..small_buf.len()]);
128+
assert_eq!(res, small_buf.len() as isize);
129+
130+
// Test that we report a proper error for a missing path.
131+
let bad_path = CString::new("MIRI_MISSING_FILE_NAME").unwrap();
132+
let res = unsafe {
133+
libc::readlink(bad_path.as_ptr(), small_buf.as_mut_ptr().cast(), small_buf.len())
134+
};
135+
assert_eq!(res, -1);
136+
assert_eq!(Error::last_os_error().kind(), ErrorKind::NotFound);
137+
}

Diff for: src/tools/miri/tests/pass-dep/shims/libc-rsfs.stdout

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello dup fd

Diff for: src/tools/miri/tests/pass-dep/shims/fs_with_isolation.rs renamed to src/tools/miri/tests/pass/shims/fs-with-isolation.rs

+1-18
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,14 @@
22
//@compile-flags: -Zmiri-isolation-error=warn-nobacktrace
33
//@normalize-stderr-test: "(stat(x)?)" -> "$$STAT"
44

5-
use std::ffi::CString;
65
use std::fs::{self, File};
7-
use std::io::{Error, ErrorKind};
6+
use std::io::ErrorKind;
87
use std::os::unix;
98

109
fn main() {
1110
// test `open`
1211
assert_eq!(File::create("foo.txt").unwrap_err().kind(), ErrorKind::PermissionDenied);
1312

14-
// test `fcntl`
15-
unsafe {
16-
assert_eq!(libc::fcntl(1, libc::F_DUPFD, 0), -1);
17-
assert_eq!(Error::last_os_error().raw_os_error(), Some(libc::EPERM));
18-
}
19-
2013
// test `unlink`
2114
assert_eq!(fs::remove_file("foo.txt").unwrap_err().kind(), ErrorKind::PermissionDenied);
2215

@@ -26,17 +19,8 @@ fn main() {
2619
ErrorKind::PermissionDenied
2720
);
2821

29-
// test `readlink`
30-
let symlink_c_str = CString::new("foo.txt").unwrap();
31-
let mut buf = vec![0; "foo_link.txt".len() + 1];
32-
unsafe {
33-
assert_eq!(libc::readlink(symlink_c_str.as_ptr(), buf.as_mut_ptr(), buf.len()), -1);
34-
assert_eq!(Error::last_os_error().raw_os_error(), Some(libc::EACCES));
35-
}
36-
3722
// test `stat`
3823
assert_eq!(fs::metadata("foo.txt").unwrap_err().kind(), ErrorKind::PermissionDenied);
39-
assert_eq!(Error::last_os_error().raw_os_error(), Some(libc::EACCES));
4024

4125
// test `rename`
4226
assert_eq!(fs::rename("a.txt", "b.txt").unwrap_err().kind(), ErrorKind::PermissionDenied);
@@ -49,5 +33,4 @@ fn main() {
4933

5034
// test `opendir`
5135
assert_eq!(fs::read_dir("foo/bar").unwrap_err().kind(), ErrorKind::PermissionDenied);
52-
assert_eq!(Error::last_os_error().raw_os_error(), Some(libc::EACCES));
5336
}

Diff for: src/tools/miri/tests/pass-dep/shims/fs_with_isolation.stderr renamed to src/tools/miri/tests/pass/shims/fs-with-isolation.stderr

-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
warning: `open` was made to return an error due to isolation
22

3-
warning: `fcntl` was made to return an error due to isolation
4-
53
warning: `unlink` was made to return an error due to isolation
64

75
warning: `symlink` was made to return an error due to isolation
86

9-
warning: `readlink` was made to return an error due to isolation
10-
117
warning: `$STAT` was made to return an error due to isolation
128

139
warning: `rename` was made to return an error due to isolation

0 commit comments

Comments
 (0)