Auto merge of #1156 - divergentdave:fcntl-F_DUPFD_CLOEXEC, r=RalfJung
Add F_DUPFD/F_DUPFD_CLOEXEC to fcntl shim This adds support for `F_DUPFD` and `F_DUPFD_CLOEXEC` to the shim for `fcntl`. (The `FileHandler` does not track the `FD_CLOEXEC` flag for open files, so these commands are effectively the same.) These changes enable using `File::try_clone`. I also changed the initial value of the `low` field in `FileHandler`, so that it matches the intent of the preceding comment. The `open` shim was pre-incrementing it when choosing new file descriptor numbers, so FD 3 was being skipped.
This commit is contained in:
commit
2d02e105d1
@ -1,5 +1,6 @@
|
||||
#![feature(rustc_private)]
|
||||
#![feature(option_expect_none, option_unwrap_none)]
|
||||
#![feature(map_first_last)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
#![allow(clippy::cast_lossless)]
|
||||
|
||||
|
114
src/shims/fs.rs
114
src/shims/fs.rs
@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fs::{remove_file, rename, File, OpenOptions};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
@ -18,18 +18,48 @@ pub struct FileHandle {
|
||||
writable: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FileHandler {
|
||||
handles: HashMap<i32, FileHandle>,
|
||||
low: i32,
|
||||
handles: BTreeMap<i32, FileHandle>,
|
||||
}
|
||||
|
||||
impl Default for FileHandler {
|
||||
fn default() -> Self {
|
||||
FileHandler {
|
||||
handles: Default::default(),
|
||||
// 0, 1 and 2 are reserved for stdin, stdout and stderr.
|
||||
low: 3,
|
||||
}
|
||||
// fd numbers 0, 1, and 2 are reserved for stdin, stdout, and stderr
|
||||
const MIN_NORMAL_FILE_FD: i32 = 3;
|
||||
|
||||
impl FileHandler {
|
||||
fn insert_fd(&mut self, file_handle: FileHandle) -> i32 {
|
||||
self.insert_fd_with_min_fd(file_handle, 0)
|
||||
}
|
||||
|
||||
fn insert_fd_with_min_fd(&mut self, file_handle: FileHandle, min_fd: i32) -> i32 {
|
||||
let min_fd = std::cmp::max(min_fd, MIN_NORMAL_FILE_FD);
|
||||
|
||||
// Find the lowest unused FD, starting from min_fd. If the first such unused FD is in
|
||||
// between used FDs, the find_map combinator will return it. If the first such unused FD
|
||||
// is after all other used FDs, the find_map combinator will return None, and we will use
|
||||
// the FD following the greatest FD thus far.
|
||||
let candidate_new_fd = self
|
||||
.handles
|
||||
.range(min_fd..)
|
||||
.zip(min_fd..)
|
||||
.find_map(|((fd, _fh), counter)| {
|
||||
if *fd != counter {
|
||||
// There was a gap in the fds stored, return the first unused one
|
||||
// (note that this relies on BTreeMap iterating in key order)
|
||||
Some(counter)
|
||||
} else {
|
||||
// This fd is used, keep going
|
||||
None
|
||||
}
|
||||
});
|
||||
let new_fd = candidate_new_fd.unwrap_or_else(|| {
|
||||
// find_map ran out of BTreeMap entries before finding a free fd, use one plus the
|
||||
// maximum fd in the map
|
||||
self.handles.last_entry().map(|entry| entry.key() + 1).unwrap_or(min_fd)
|
||||
});
|
||||
|
||||
self.handles.insert(new_fd, file_handle).unwrap_none();
|
||||
new_fd
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,10 +137,8 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
|
||||
let path = this.read_os_str_from_c_str(this.read_scalar(path_op)?.not_undef()?)?;
|
||||
|
||||
let fd = options.open(&path).map(|file| {
|
||||
let mut fh = &mut this.machine.file_handler;
|
||||
fh.low += 1;
|
||||
fh.handles.insert(fh.low, FileHandle { file, writable }).unwrap_none();
|
||||
fh.low
|
||||
let fh = &mut this.machine.file_handler;
|
||||
fh.insert_fd(FileHandle { file, writable })
|
||||
});
|
||||
|
||||
this.try_unwrap_io_result(fd)
|
||||
@ -120,7 +148,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
|
||||
&mut self,
|
||||
fd_op: OpTy<'tcx, Tag>,
|
||||
cmd_op: OpTy<'tcx, Tag>,
|
||||
_arg1_op: Option<OpTy<'tcx, Tag>>,
|
||||
start_op: Option<OpTy<'tcx, Tag>>,
|
||||
) -> InterpResult<'tcx, i32> {
|
||||
let this = self.eval_context_mut();
|
||||
|
||||
@ -139,6 +167,31 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
|
||||
} else {
|
||||
this.handle_not_found()
|
||||
}
|
||||
} else if cmd == this.eval_libc_i32("F_DUPFD")?
|
||||
|| cmd == this.eval_libc_i32("F_DUPFD_CLOEXEC")?
|
||||
{
|
||||
// Note that we always assume the FD_CLOEXEC flag is set for every open file, in part
|
||||
// because exec() isn't supported. The F_DUPFD and F_DUPFD_CLOEXEC commands only
|
||||
// differ in whether the FD_CLOEXEC flag is pre-set on the new file descriptor,
|
||||
// thus they can share the same implementation here.
|
||||
if fd < MIN_NORMAL_FILE_FD {
|
||||
throw_unsup_format!("Duplicating file descriptors for stdin, stdout, or stderr is not supported")
|
||||
}
|
||||
let start_op = start_op.ok_or_else(|| {
|
||||
err_unsup_format!(
|
||||
"fcntl with command F_DUPFD or F_DUPFD_CLOEXEC requires a third argument"
|
||||
)
|
||||
})?;
|
||||
let start = this.read_scalar(start_op)?.to_i32()?;
|
||||
let fh = &mut this.machine.file_handler;
|
||||
let (file_result, writable) = match fh.handles.get(&fd) {
|
||||
Some(FileHandle { file, writable }) => (file.try_clone(), *writable),
|
||||
None => return this.handle_not_found(),
|
||||
};
|
||||
let fd_result = file_result.map(|duplicated| {
|
||||
fh.insert_fd_with_min_fd(FileHandle { file: duplicated, writable }, start)
|
||||
});
|
||||
this.try_unwrap_io_result(fd_result)
|
||||
} else {
|
||||
throw_unsup_format!("The {:#x} command is not supported for `fcntl`)", cmd);
|
||||
}
|
||||
@ -151,23 +204,23 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
|
||||
|
||||
let fd = this.read_scalar(fd_op)?.to_i32()?;
|
||||
|
||||
if let Some(handle) = this.machine.file_handler.handles.remove(&fd) {
|
||||
if let Some(FileHandle { file, writable }) = this.machine.file_handler.handles.remove(&fd) {
|
||||
// We sync the file if it was opened in a mode different than read-only.
|
||||
if handle.writable {
|
||||
if writable {
|
||||
// `File::sync_all` does the checks that are done when closing a file. We do this to
|
||||
// to handle possible errors correctly.
|
||||
let result = this.try_unwrap_io_result(handle.file.sync_all().map(|_| 0i32));
|
||||
let result = this.try_unwrap_io_result(file.sync_all().map(|_| 0i32));
|
||||
// Now we actually close the file.
|
||||
drop(handle);
|
||||
drop(file);
|
||||
// And return the result.
|
||||
result
|
||||
} else {
|
||||
// We drop the file, this closes it but ignores any errors produced when closing
|
||||
// it. This is done because `File::sync_call` cannot be done over files like
|
||||
// it. This is done because `File::sync_all` cannot be done over files like
|
||||
// `/dev/urandom` which are read-only. Check
|
||||
// https://github.com/rust-lang/miri/issues/999#issuecomment-568920439 for a deeper
|
||||
// discussion.
|
||||
drop(handle);
|
||||
drop(file);
|
||||
Ok(0)
|
||||
}
|
||||
} else {
|
||||
@ -200,7 +253,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
|
||||
// host's and target's `isize`. This saves us from having to handle overflows later.
|
||||
let count = count.min(this.isize_max() as u64).min(isize::max_value() as u64);
|
||||
|
||||
if let Some(handle) = this.machine.file_handler.handles.get_mut(&fd) {
|
||||
if let Some(FileHandle { file, writable: _ }) = this.machine.file_handler.handles.get_mut(&fd) {
|
||||
// This can never fail because `count` was capped to be smaller than
|
||||
// `isize::max_value()`.
|
||||
let count = isize::try_from(count).unwrap();
|
||||
@ -208,8 +261,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
|
||||
// because it was a target's `usize`. Also we are sure that its smaller than
|
||||
// `usize::max_value()` because it is a host's `isize`.
|
||||
let mut bytes = vec![0; count as usize];
|
||||
let result = handle
|
||||
.file
|
||||
let result = file
|
||||
.read(&mut bytes)
|
||||
// `File::read` never returns a value larger than `count`, so this cannot fail.
|
||||
.map(|c| i64::try_from(c).unwrap());
|
||||
@ -255,9 +307,9 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
|
||||
// host's and target's `isize`. This saves us from having to handle overflows later.
|
||||
let count = count.min(this.isize_max() as u64).min(isize::max_value() as u64);
|
||||
|
||||
if let Some(handle) = this.machine.file_handler.handles.get_mut(&fd) {
|
||||
if let Some(FileHandle { file, writable: _ }) = this.machine.file_handler.handles.get_mut(&fd) {
|
||||
let bytes = this.memory.read_bytes(buf, Size::from_bytes(count))?;
|
||||
let result = handle.file.write(&bytes).map(|c| i64::try_from(c).unwrap());
|
||||
let result = file.write(&bytes).map(|c| i64::try_from(c).unwrap());
|
||||
this.try_unwrap_io_result(result)
|
||||
} else {
|
||||
this.handle_not_found()
|
||||
@ -290,8 +342,8 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
|
||||
return Ok(-1);
|
||||
};
|
||||
|
||||
if let Some(handle) = this.machine.file_handler.handles.get_mut(&fd) {
|
||||
let result = handle.file.seek(seek_from).map(|offset| offset as i64);
|
||||
if let Some(FileHandle { file, writable: _ }) = this.machine.file_handler.handles.get_mut(&fd) {
|
||||
let result = file.seek(seek_from).map(|offset| offset as i64);
|
||||
this.try_unwrap_io_result(result)
|
||||
} else {
|
||||
this.handle_not_found()
|
||||
@ -652,11 +704,11 @@ impl FileMetadata {
|
||||
fd: i32,
|
||||
) -> InterpResult<'tcx, Option<FileMetadata>> {
|
||||
let option = ecx.machine.file_handler.handles.get(&fd);
|
||||
let handle = match option {
|
||||
Some(handle) => handle,
|
||||
let file = match option {
|
||||
Some(FileHandle { file, writable: _ }) => file,
|
||||
None => return ecx.handle_not_found().map(|_: i32| None),
|
||||
};
|
||||
let metadata = handle.file.metadata();
|
||||
let metadata = file.metadata();
|
||||
|
||||
FileMetadata::from_meta(ecx, metadata)
|
||||
}
|
||||
|
@ -5,25 +5,23 @@ use std::fs::{File, remove_file, rename};
|
||||
use std::io::{Read, Write, ErrorKind, Result, Seek, SeekFrom};
|
||||
use std::path::{PathBuf, Path};
|
||||
|
||||
fn test_metadata(bytes: &[u8], path: &Path) -> Result<()> {
|
||||
// Test that the file metadata is correct.
|
||||
let metadata = path.metadata()?;
|
||||
// `path` should point to a file.
|
||||
assert!(metadata.is_file());
|
||||
// The size of the file must be equal to the number of written bytes.
|
||||
assert_eq!(bytes.len() as u64, metadata.len());
|
||||
Ok(())
|
||||
fn main() {
|
||||
test_file();
|
||||
test_file_clone();
|
||||
test_seek();
|
||||
test_metadata();
|
||||
test_symlink();
|
||||
test_errors();
|
||||
test_rename();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn test_file() {
|
||||
let tmp = std::env::temp_dir();
|
||||
let filename = PathBuf::from("miri_test_fs.txt");
|
||||
let filename = PathBuf::from("miri_test_fs_file.txt");
|
||||
let path = tmp.join(&filename);
|
||||
let symlink_path = tmp.join("miri_test_fs_symlink.txt");
|
||||
let bytes = b"Hello, World!\n";
|
||||
// Clean the paths for robustness.
|
||||
remove_file(&path).ok();
|
||||
remove_file(&symlink_path).ok();
|
||||
|
||||
// Test creating, writing and closing a file (closing is tested when `file` is dropped).
|
||||
let mut file = File::create(&path).unwrap();
|
||||
@ -42,6 +40,47 @@ fn main() {
|
||||
file.read_to_end(&mut contents).unwrap();
|
||||
assert_eq!(bytes, contents.as_slice());
|
||||
|
||||
// Removing file should succeed.
|
||||
remove_file(&path).unwrap();
|
||||
}
|
||||
|
||||
fn test_file_clone() {
|
||||
let tmp = std::env::temp_dir();
|
||||
let filename = PathBuf::from("miri_test_fs_file_clone.txt");
|
||||
let path = tmp.join(&filename);
|
||||
let bytes = b"Hello, World!\n";
|
||||
// Clean the paths for robustness.
|
||||
remove_file(&path).ok();
|
||||
|
||||
let mut file = File::create(&path).unwrap();
|
||||
file.write(bytes).unwrap();
|
||||
|
||||
// Cloning a file should be successful.
|
||||
let file = File::open(&path).unwrap();
|
||||
let mut cloned = file.try_clone().unwrap();
|
||||
// Reading from a cloned file should get the same text.
|
||||
let mut contents = Vec::new();
|
||||
cloned.read_to_end(&mut contents).unwrap();
|
||||
assert_eq!(bytes, contents.as_slice());
|
||||
|
||||
// Removing file should succeed.
|
||||
remove_file(&path).unwrap();
|
||||
}
|
||||
|
||||
fn test_seek() {
|
||||
let tmp = std::env::temp_dir();
|
||||
let filename = PathBuf::from("miri_test_fs_seek.txt");
|
||||
let path = tmp.join(&filename);
|
||||
let bytes = b"Hello, World!\n";
|
||||
// Clean the paths for robustness.
|
||||
remove_file(&path).ok();
|
||||
|
||||
let mut file = File::create(&path).unwrap();
|
||||
file.write(bytes).unwrap();
|
||||
|
||||
let mut file = File::open(&path).unwrap();
|
||||
let mut contents = Vec::new();
|
||||
file.read_to_end(&mut contents).unwrap();
|
||||
// Test that seeking to the beginning and reading until EOF gets the text again.
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut contents = Vec::new();
|
||||
@ -59,11 +98,53 @@ fn main() {
|
||||
file.read_to_end(&mut contents).unwrap();
|
||||
assert_eq!(&bytes[2..], contents.as_slice());
|
||||
|
||||
// Removing file should succeed.
|
||||
remove_file(&path).unwrap();
|
||||
}
|
||||
|
||||
fn check_metadata(bytes: &[u8], path: &Path) -> Result<()> {
|
||||
// Test that the file metadata is correct.
|
||||
let metadata = path.metadata()?;
|
||||
// `path` should point to a file.
|
||||
assert!(metadata.is_file());
|
||||
// The size of the file must be equal to the number of written bytes.
|
||||
assert_eq!(bytes.len() as u64, metadata.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_metadata() {
|
||||
let tmp = std::env::temp_dir();
|
||||
let filename = PathBuf::from("miri_test_fs_metadata.txt");
|
||||
let path = tmp.join(&filename);
|
||||
let bytes = b"Hello, World!\n";
|
||||
// Clean the paths for robustness.
|
||||
remove_file(&path).ok();
|
||||
|
||||
let mut file = File::create(&path).unwrap();
|
||||
file.write(bytes).unwrap();
|
||||
|
||||
// Test that metadata of an absolute path is correct.
|
||||
test_metadata(bytes, &path).unwrap();
|
||||
check_metadata(bytes, &path).unwrap();
|
||||
// Test that metadata of a relative path is correct.
|
||||
std::env::set_current_dir(&tmp).unwrap();
|
||||
test_metadata(bytes, &filename).unwrap();
|
||||
check_metadata(bytes, &filename).unwrap();
|
||||
|
||||
// Removing file should succeed.
|
||||
remove_file(&path).unwrap();
|
||||
}
|
||||
|
||||
fn test_symlink() {
|
||||
let tmp = std::env::temp_dir();
|
||||
let filename = PathBuf::from("miri_test_fs_link_target.txt");
|
||||
let path = tmp.join(&filename);
|
||||
let symlink_path = tmp.join("miri_test_fs_symlink.txt");
|
||||
let bytes = b"Hello, World!\n";
|
||||
// Clean the paths for robustness.
|
||||
remove_file(&path).ok();
|
||||
remove_file(&symlink_path).ok();
|
||||
|
||||
let mut file = File::create(&path).unwrap();
|
||||
file.write(bytes).unwrap();
|
||||
|
||||
// Creating a symbolic link should succeed.
|
||||
std::os::unix::fs::symlink(&path, &symlink_path).unwrap();
|
||||
@ -73,7 +154,7 @@ fn main() {
|
||||
symlink_file.read_to_end(&mut contents).unwrap();
|
||||
assert_eq!(bytes, contents.as_slice());
|
||||
// Test that metadata of a symbolic link is correct.
|
||||
test_metadata(bytes, &symlink_path).unwrap();
|
||||
check_metadata(bytes, &symlink_path).unwrap();
|
||||
// Test that the metadata of a symbolic link is correct when not following it.
|
||||
assert!(symlink_path.symlink_metadata().unwrap().file_type().is_symlink());
|
||||
// Removing symbolic link should succeed.
|
||||
@ -81,10 +162,30 @@ fn main() {
|
||||
|
||||
// Removing file should succeed.
|
||||
remove_file(&path).unwrap();
|
||||
}
|
||||
|
||||
fn test_errors() {
|
||||
let tmp = std::env::temp_dir();
|
||||
let filename = PathBuf::from("miri_test_fs_errors.txt");
|
||||
let path = tmp.join(&filename);
|
||||
let bytes = b"Hello, World!\n";
|
||||
// Clean the paths for robustness.
|
||||
remove_file(&path).ok();
|
||||
|
||||
// The following tests also check that the `__errno_location()` shim is working properly.
|
||||
// Opening a non-existing file should fail with a "not found" error.
|
||||
assert_eq!(ErrorKind::NotFound, File::open(&path).unwrap_err().kind());
|
||||
// Removing a non-existing file should fail with a "not found" error.
|
||||
assert_eq!(ErrorKind::NotFound, remove_file(&path).unwrap_err().kind());
|
||||
// Reading the metadata of a non-existing file should fail with a "not found" error.
|
||||
assert_eq!(ErrorKind::NotFound, check_metadata(bytes, &path).unwrap_err().kind());
|
||||
}
|
||||
|
||||
fn test_rename() {
|
||||
let tmp = std::env::temp_dir();
|
||||
// Renaming a file should succeed.
|
||||
let path1 = tmp.join("rename_source.txt");
|
||||
let path2 = tmp.join("rename_destination.txt");
|
||||
let path1 = tmp.join("miri_test_fs_rename_source.txt");
|
||||
let path2 = tmp.join("miri_test_fs_rename_destination.txt");
|
||||
// Clean files for robustness.
|
||||
remove_file(&path1).ok();
|
||||
remove_file(&path2).ok();
|
||||
@ -94,12 +195,4 @@ fn main() {
|
||||
assert_eq!(ErrorKind::NotFound, path1.metadata().unwrap_err().kind());
|
||||
assert!(path2.metadata().unwrap().is_file());
|
||||
remove_file(&path2).unwrap();
|
||||
|
||||
// The two following tests also check that the `__errno_location()` shim is working properly.
|
||||
// Opening a non-existing file should fail with a "not found" error.
|
||||
assert_eq!(ErrorKind::NotFound, File::open(&path).unwrap_err().kind());
|
||||
// Removing a non-existing file should fail with a "not found" error.
|
||||
assert_eq!(ErrorKind::NotFound, remove_file(&path).unwrap_err().kind());
|
||||
// Reading the metadata of a non-existing file should fail with a "not found" error.
|
||||
assert_eq!(ErrorKind::NotFound, test_metadata(bytes, &path).unwrap_err().kind());
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user