blob: 062623a7f6a60e8a9fe97af3c8c92f97e8032fbe [file] [log] [blame]
use std::any::Any;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::convert::TryInto;
use std::fs::{
read_dir, remove_dir, remove_file, rename, DirBuilder, File, FileType, OpenOptions, ReadDir,
};
use std::io::{self, ErrorKind, IsTerminal, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use log::trace;
use rustc_data_structures::fx::FxHashMap;
use rustc_middle::ty::TyCtxt;
use rustc_target::abi::Size;
use crate::shims::os_str::bytes_to_os_str;
use crate::*;
use shims::time::system_time_to_duration;
#[derive(Debug)]
pub struct FileHandle {
file: File,
writable: bool,
}
pub trait FileDescriptor: std::fmt::Debug + Any {
fn name(&self) -> &'static str;
fn read<'tcx>(
&mut self,
_communicate_allowed: bool,
_bytes: &mut [u8],
_tcx: TyCtxt<'tcx>,
) -> InterpResult<'tcx, io::Result<usize>> {
throw_unsup_format!("cannot read from {}", self.name());
}
fn write<'tcx>(
&self,
_communicate_allowed: bool,
_bytes: &[u8],
_tcx: TyCtxt<'tcx>,
) -> InterpResult<'tcx, io::Result<usize>> {
throw_unsup_format!("cannot write to {}", self.name());
}
fn seek<'tcx>(
&mut self,
_communicate_allowed: bool,
_offset: SeekFrom,
) -> InterpResult<'tcx, io::Result<u64>> {
throw_unsup_format!("cannot seek on {}", self.name());
}
fn close<'tcx>(
self: Box<Self>,
_communicate_allowed: bool,
) -> InterpResult<'tcx, io::Result<i32>> {
throw_unsup_format!("cannot close {}", self.name());
}
fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>>;
fn is_tty(&self, _communicate_allowed: bool) -> bool {
false
}
#[cfg(unix)]
fn as_unix_host_fd(&self) -> Option<i32> {
None
}
}
impl dyn FileDescriptor {
#[inline(always)]
pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
(self as &dyn Any).downcast_ref()
}
#[inline(always)]
pub fn downcast_mut<T: Any>(&mut self) -> Option<&mut T> {
(self as &mut dyn Any).downcast_mut()
}
}
impl FileDescriptor for FileHandle {
fn name(&self) -> &'static str {
"FILE"
}
fn read<'tcx>(
&mut self,
communicate_allowed: bool,
bytes: &mut [u8],
_tcx: TyCtxt<'tcx>,
) -> InterpResult<'tcx, io::Result<usize>> {
assert!(communicate_allowed, "isolation should have prevented even opening a file");
Ok(self.file.read(bytes))
}
fn write<'tcx>(
&self,
communicate_allowed: bool,
bytes: &[u8],
_tcx: TyCtxt<'tcx>,
) -> InterpResult<'tcx, io::Result<usize>> {
assert!(communicate_allowed, "isolation should have prevented even opening a file");
Ok((&mut &self.file).write(bytes))
}
fn seek<'tcx>(
&mut self,
communicate_allowed: bool,
offset: SeekFrom,
) -> InterpResult<'tcx, io::Result<u64>> {
assert!(communicate_allowed, "isolation should have prevented even opening a file");
Ok(self.file.seek(offset))
}
fn close<'tcx>(
self: Box<Self>,
communicate_allowed: bool,
) -> InterpResult<'tcx, io::Result<i32>> {
assert!(communicate_allowed, "isolation should have prevented even opening a file");
// We sync the file if it was opened in a mode different than read-only.
if self.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 = self.file.sync_all().map(|_| 0i32);
// Now we actually close the file.
drop(self);
// And return the result.
Ok(result)
} else {
// We drop the file, this closes it but ignores any errors
// produced when closing 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(self);
Ok(Ok(0))
}
}
fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
let duplicated = self.file.try_clone()?;
Ok(Box::new(FileHandle { file: duplicated, writable: self.writable }))
}
#[cfg(unix)]
fn as_unix_host_fd(&self) -> Option<i32> {
use std::os::unix::io::AsRawFd;
Some(self.file.as_raw_fd())
}
fn is_tty(&self, communicate_allowed: bool) -> bool {
communicate_allowed && self.file.is_terminal()
}
}
impl FileDescriptor for io::Stdin {
fn name(&self) -> &'static str {
"stdin"
}
fn read<'tcx>(
&mut self,
communicate_allowed: bool,
bytes: &mut [u8],
_tcx: TyCtxt<'tcx>,
) -> InterpResult<'tcx, io::Result<usize>> {
if !communicate_allowed {
// We want isolation mode to be deterministic, so we have to disallow all reads, even stdin.
helpers::isolation_abort_error("`read` from stdin")?;
}
Ok(Read::read(self, bytes))
}
fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
Ok(Box::new(io::stdin()))
}
#[cfg(unix)]
fn as_unix_host_fd(&self) -> Option<i32> {
Some(libc::STDIN_FILENO)
}
fn is_tty(&self, communicate_allowed: bool) -> bool {
communicate_allowed && self.is_terminal()
}
}
impl FileDescriptor for io::Stdout {
fn name(&self) -> &'static str {
"stdout"
}
fn write<'tcx>(
&self,
_communicate_allowed: bool,
bytes: &[u8],
_tcx: TyCtxt<'tcx>,
) -> InterpResult<'tcx, io::Result<usize>> {
// We allow writing to stderr even with isolation enabled.
let result = Write::write(&mut { self }, bytes);
// Stdout is buffered, flush to make sure it appears on the
// screen. This is the write() syscall of the interpreted
// program, we want it to correspond to a write() syscall on
// the host -- there is no good in adding extra buffering
// here.
io::stdout().flush().unwrap();
Ok(result)
}
fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
Ok(Box::new(io::stdout()))
}
#[cfg(unix)]
fn as_unix_host_fd(&self) -> Option<i32> {
Some(libc::STDOUT_FILENO)
}
fn is_tty(&self, communicate_allowed: bool) -> bool {
communicate_allowed && self.is_terminal()
}
}
impl FileDescriptor for io::Stderr {
fn name(&self) -> &'static str {
"stderr"
}
fn write<'tcx>(
&self,
_communicate_allowed: bool,
bytes: &[u8],
_tcx: TyCtxt<'tcx>,
) -> InterpResult<'tcx, io::Result<usize>> {
// We allow writing to stderr even with isolation enabled.
// No need to flush, stderr is not buffered.
Ok(Write::write(&mut { self }, bytes))
}
fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
Ok(Box::new(io::stderr()))
}
#[cfg(unix)]
fn as_unix_host_fd(&self) -> Option<i32> {
Some(libc::STDERR_FILENO)
}
fn is_tty(&self, communicate_allowed: bool) -> bool {
communicate_allowed && self.is_terminal()
}
}
#[derive(Debug)]
struct NullOutput;
impl FileDescriptor for NullOutput {
fn name(&self) -> &'static str {
"stderr and stdout"
}
fn write<'tcx>(
&self,
_communicate_allowed: bool,
bytes: &[u8],
_tcx: TyCtxt<'tcx>,
) -> InterpResult<'tcx, io::Result<usize>> {
// We just don't write anything, but report to the user that we did.
Ok(Ok(bytes.len()))
}
fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
Ok(Box::new(NullOutput))
}
}
#[derive(Debug)]
pub struct FileHandler {
pub handles: BTreeMap<i32, Box<dyn FileDescriptor>>,
}
impl VisitTags for FileHandler {
fn visit_tags(&self, _visit: &mut dyn FnMut(BorTag)) {
// All our FileDescriptor do not have any tags.
}
}
impl FileHandler {
pub(crate) fn new(mute_stdout_stderr: bool) -> FileHandler {
let mut handles: BTreeMap<_, Box<dyn FileDescriptor>> = BTreeMap::new();
handles.insert(0i32, Box::new(io::stdin()));
if mute_stdout_stderr {
handles.insert(1i32, Box::new(NullOutput));
handles.insert(2i32, Box::new(NullOutput));
} else {
handles.insert(1i32, Box::new(io::stdout()));
handles.insert(2i32, Box::new(io::stderr()));
}
FileHandler { handles }
}
pub fn insert_fd(&mut self, file_handle: Box<dyn FileDescriptor>) -> i32 {
self.insert_fd_with_min_fd(file_handle, 0)
}
fn insert_fd_with_min_fd(&mut self, file_handle: Box<dyn FileDescriptor>, min_fd: i32) -> i32 {
// 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_key_value()
.map(|(fd, _)| fd.checked_add(1).unwrap())
.unwrap_or(min_fd)
});
self.handles.try_insert(new_fd, file_handle).unwrap();
new_fd
}
}
impl<'mir, 'tcx: 'mir> EvalContextExtPrivate<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
trait EvalContextExtPrivate<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
fn macos_stat_write_buf(
&mut self,
metadata: FileMetadata,
buf_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, i32> {
let this = self.eval_context_mut();
let mode: u16 = metadata.mode.to_u16()?;
let (access_sec, access_nsec) = metadata.accessed.unwrap_or((0, 0));
let (created_sec, created_nsec) = metadata.created.unwrap_or((0, 0));
let (modified_sec, modified_nsec) = metadata.modified.unwrap_or((0, 0));
let buf = this.deref_pointer_as(buf_op, this.libc_ty_layout("stat"))?;
this.write_int_fields_named(
&[
("st_dev", 0),
("st_mode", mode.into()),
("st_nlink", 0),
("st_ino", 0),
("st_uid", 0),
("st_gid", 0),
("st_rdev", 0),
("st_atime", access_sec.into()),
("st_atime_nsec", access_nsec.into()),
("st_mtime", modified_sec.into()),
("st_mtime_nsec", modified_nsec.into()),
("st_ctime", 0),
("st_ctime_nsec", 0),
("st_birthtime", created_sec.into()),
("st_birthtime_nsec", created_nsec.into()),
("st_size", metadata.size.into()),
("st_blocks", 0),
("st_blksize", 0),
("st_flags", 0),
("st_gen", 0),
],
&buf,
)?;
Ok(0)
}
fn file_type_to_d_type(
&mut self,
file_type: std::io::Result<FileType>,
) -> InterpResult<'tcx, i32> {
let this = self.eval_context_mut();
match file_type {
Ok(file_type) => {
if file_type.is_dir() {
Ok(this.eval_libc("DT_DIR").to_u8()?.into())
} else if file_type.is_file() {
Ok(this.eval_libc("DT_REG").to_u8()?.into())
} else if file_type.is_symlink() {
Ok(this.eval_libc("DT_LNK").to_u8()?.into())
} else {
// Certain file types are only supported when the host is a Unix system.
// (i.e. devices and sockets) If it is, check those cases, if not, fall back to
// DT_UNKNOWN sooner.
#[cfg(unix)]
{
use std::os::unix::fs::FileTypeExt;
if file_type.is_block_device() {
Ok(this.eval_libc("DT_BLK").to_u8()?.into())
} else if file_type.is_char_device() {
Ok(this.eval_libc("DT_CHR").to_u8()?.into())
} else if file_type.is_fifo() {
Ok(this.eval_libc("DT_FIFO").to_u8()?.into())
} else if file_type.is_socket() {
Ok(this.eval_libc("DT_SOCK").to_u8()?.into())
} else {
Ok(this.eval_libc("DT_UNKNOWN").to_u8()?.into())
}
}
#[cfg(not(unix))]
Ok(this.eval_libc("DT_UNKNOWN").to_u8()?.into())
}
}
Err(e) =>
match e.raw_os_error() {
Some(error) => Ok(error),
None =>
throw_unsup_format!(
"the error {} couldn't be converted to a return value",
e
),
},
}
}
}
/// An open directory, tracked by DirHandler.
#[derive(Debug)]
pub struct OpenDir {
/// The directory reader on the host.
read_dir: ReadDir,
/// The most recent entry returned by readdir()
entry: Pointer<Option<Provenance>>,
}
impl OpenDir {
fn new(read_dir: ReadDir) -> Self {
// We rely on `free` being a NOP on null pointers.
Self { read_dir, entry: Pointer::null() }
}
}
#[derive(Debug)]
pub struct DirHandler {
/// Directory iterators used to emulate libc "directory streams", as used in opendir, readdir,
/// and closedir.
///
/// When opendir is called, a directory iterator is created on the host for the target
/// directory, and an entry is stored in this hash map, indexed by an ID which represents
/// the directory stream. When readdir is called, the directory stream ID is used to look up
/// the corresponding ReadDir iterator from this map, and information from the next
/// directory entry is returned. When closedir is called, the ReadDir iterator is removed from
/// the map.
streams: FxHashMap<u64, OpenDir>,
/// ID number to be used by the next call to opendir
next_id: u64,
}
impl DirHandler {
#[allow(clippy::arithmetic_side_effects)]
fn insert_new(&mut self, read_dir: ReadDir) -> u64 {
let id = self.next_id;
self.next_id += 1;
self.streams.try_insert(id, OpenDir::new(read_dir)).unwrap();
id
}
}
impl Default for DirHandler {
fn default() -> DirHandler {
DirHandler {
streams: FxHashMap::default(),
// Skip 0 as an ID, because it looks like a null pointer to libc
next_id: 1,
}
}
}
impl VisitTags for DirHandler {
fn visit_tags(&self, visit: &mut dyn FnMut(BorTag)) {
let DirHandler { streams, next_id: _ } = self;
for dir in streams.values() {
dir.entry.visit_tags(visit);
}
}
}
fn maybe_sync_file(
file: &File,
writable: bool,
operation: fn(&File) -> std::io::Result<()>,
) -> std::io::Result<i32> {
if !writable && cfg!(windows) {
// sync_all() and sync_data() will return an error on Windows hosts if the file is not opened
// for writing. (FlushFileBuffers requires that the file handle have the
// GENERIC_WRITE right)
Ok(0i32)
} else {
let result = operation(file);
result.map(|_| 0i32)
}
}
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
fn open(&mut self, args: &[OpTy<'tcx, Provenance>]) -> InterpResult<'tcx, i32> {
if args.len() < 2 {
throw_ub_format!(
"incorrect number of arguments for `open`: got {}, expected at least 2",
args.len()
);
}
let this = self.eval_context_mut();
let path = this.read_pointer(&args[0])?;
let flag = this.read_scalar(&args[1])?.to_i32()?;
let mut options = OpenOptions::new();
let o_rdonly = this.eval_libc_i32("O_RDONLY");
let o_wronly = this.eval_libc_i32("O_WRONLY");
let o_rdwr = this.eval_libc_i32("O_RDWR");
// The first two bits of the flag correspond to the access mode in linux, macOS and
// windows. We need to check that in fact the access mode flags for the current target
// only use these two bits, otherwise we are in an unsupported target and should error.
if (o_rdonly | o_wronly | o_rdwr) & !0b11 != 0 {
throw_unsup_format!("access mode flags on this target are unsupported");
}
let mut writable = true;
// Now we check the access mode
let access_mode = flag & 0b11;
if access_mode == o_rdonly {
writable = false;
options.read(true);
} else if access_mode == o_wronly {
options.write(true);
} else if access_mode == o_rdwr {
options.read(true).write(true);
} else {
throw_unsup_format!("unsupported access mode {:#x}", access_mode);
}
// We need to check that there aren't unsupported options in `flag`. For this we try to
// reproduce the content of `flag` in the `mirror` variable using only the supported
// options.
let mut mirror = access_mode;
let o_append = this.eval_libc_i32("O_APPEND");
if flag & o_append == o_append {
options.append(true);
mirror |= o_append;
}
let o_trunc = this.eval_libc_i32("O_TRUNC");
if flag & o_trunc == o_trunc {
options.truncate(true);
mirror |= o_trunc;
}
let o_creat = this.eval_libc_i32("O_CREAT");
if flag & o_creat == o_creat {
// Get the mode. On macOS, the argument type `mode_t` is actually `u16`, but
// C integer promotion rules mean that on the ABI level, it gets passed as `u32`
// (see https://github.com/rust-lang/rust/issues/71915).
let mode = if let Some(arg) = args.get(2) {
this.read_scalar(arg)?.to_u32()?
} else {
throw_ub_format!(
"incorrect number of arguments for `open` with `O_CREAT`: got {}, expected at least 3",
args.len()
);
};
if mode != 0o666 {
throw_unsup_format!("non-default mode 0o{:o} is not supported", mode);
}
mirror |= o_creat;
let o_excl = this.eval_libc_i32("O_EXCL");
if flag & o_excl == o_excl {
mirror |= o_excl;
options.create_new(true);
} else {
options.create(true);
}
}
let o_cloexec = this.eval_libc_i32("O_CLOEXEC");
if flag & o_cloexec == o_cloexec {
// We do not need to do anything for this flag because `std` already sets it.
// (Technically we do not support *not* setting this flag, but we ignore that.)
mirror |= o_cloexec;
}
if this.tcx.sess.target.os == "linux" {
let o_tmpfile = this.eval_libc_i32("O_TMPFILE");
if flag & o_tmpfile == o_tmpfile {
// if the flag contains `O_TMPFILE` then we return a graceful error
let eopnotsupp = this.eval_libc("EOPNOTSUPP");
this.set_last_error(eopnotsupp)?;
return Ok(-1);
}
}
// If `flag` is not equal to `mirror`, there is an unsupported option enabled in `flag`,
// then we throw an error.
if flag != mirror {
throw_unsup_format!("unsupported flags {:#x}", flag & !mirror);
}
let path = this.read_path_from_c_str(path)?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`open`", reject_with)?;
this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
return Ok(-1);
}
let fd = options.open(path).map(|file| {
let fh = &mut this.machine.file_handler;
fh.insert_fd(Box::new(FileHandle { file, writable }))
});
this.try_unwrap_io_result(fd)
}
fn fcntl(&mut self, args: &[OpTy<'tcx, Provenance>]) -> InterpResult<'tcx, i32> {
let this = self.eval_context_mut();
if args.len() < 2 {
throw_ub_format!(
"incorrect number of arguments for fcntl: got {}, expected at least 2",
args.len()
);
}
let fd = this.read_scalar(&args[0])?.to_i32()?;
let cmd = this.read_scalar(&args[1])?.to_i32()?;
// We only support getting the flags for a descriptor.
if cmd == this.eval_libc_i32("F_GETFD") {
// Currently this is the only flag that `F_GETFD` returns. It is OK to just return the
// `FD_CLOEXEC` value without checking if the flag is set for the file because `std`
// always sets this flag when opening a file. However we still need to check that the
// file itself is open.
if this.machine.file_handler.handles.contains_key(&fd) {
Ok(this.eval_libc_i32("FD_CLOEXEC"))
} 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 args.len() < 3 {
throw_ub_format!(
"incorrect number of arguments for fcntl with cmd=`F_DUPFD`/`F_DUPFD_CLOEXEC`: got {}, expected at least 3",
args.len()
);
}
let start = this.read_scalar(&args[2])?.to_i32()?;
let fh = &mut this.machine.file_handler;
match fh.handles.get_mut(&fd) {
Some(file_descriptor) => {
let dup_result = file_descriptor.dup();
match dup_result {
Ok(dup_fd) => Ok(fh.insert_fd_with_min_fd(dup_fd, start)),
Err(e) => {
this.set_last_error_from_io_error(e.kind())?;
Ok(-1)
}
}
}
None => this.handle_not_found(),
}
} else if this.tcx.sess.target.os == "macos" && cmd == this.eval_libc_i32("F_FULLFSYNC") {
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`fcntl`", reject_with)?;
this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
return Ok(-1);
}
if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
// FIXME: Support fullfsync for all FDs
let FileHandle { file, writable } =
file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
err_unsup_format!(
"`F_FULLFSYNC` is only supported on file-backed file descriptors"
)
})?;
let io_result = maybe_sync_file(file, *writable, File::sync_all);
this.try_unwrap_io_result(io_result)
} else {
this.handle_not_found()
}
} else {
throw_unsup_format!("the {:#x} command is not supported for `fcntl`)", cmd);
}
}
fn close(&mut self, fd_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, Scalar<Provenance>> {
let this = self.eval_context_mut();
let fd = this.read_scalar(fd_op)?.to_i32()?;
Ok(Scalar::from_i32(
if let Some(file_descriptor) = this.machine.file_handler.handles.remove(&fd) {
let result = file_descriptor.close(this.machine.communicate())?;
this.try_unwrap_io_result(result)?
} else {
this.handle_not_found()?
},
))
}
/// Function used when a handle is not found inside `FileHandler`. It returns `Ok(-1)`and sets
/// the last OS error to `libc::EBADF` (invalid file descriptor). This function uses
/// `T: From<i32>` instead of `i32` directly because some fs functions return different integer
/// types (like `read`, that returns an `i64`).
fn handle_not_found<T: From<i32>>(&mut self) -> InterpResult<'tcx, T> {
let this = self.eval_context_mut();
let ebadf = this.eval_libc("EBADF");
this.set_last_error(ebadf)?;
Ok((-1).into())
}
fn read(
&mut self,
fd: i32,
buf: Pointer<Option<Provenance>>,
count: u64,
) -> InterpResult<'tcx, i64> {
let this = self.eval_context_mut();
// Isolation check is done via `FileDescriptor` trait.
trace!("Reading from FD {}, size {}", fd, count);
// Check that the *entire* buffer is actually valid memory.
this.check_ptr_access(buf, Size::from_bytes(count), CheckInAllocMsg::MemoryAccessTest)?;
// We cap the number of read bytes to the largest value that we are able to fit in both the
// host's and target's `isize`. This saves us from having to handle overflows later.
let count = count
.min(u64::try_from(this.target_isize_max()).unwrap())
.min(u64::try_from(isize::MAX).unwrap());
let communicate = this.machine.communicate();
if let Some(file_descriptor) = this.machine.file_handler.handles.get_mut(&fd) {
trace!("read: FD mapped to {:?}", file_descriptor);
// We want to read at most `count` bytes. We are sure that `count` is not negative
// because it was a target's `usize`. Also we are sure that its smaller than
// `usize::MAX` because it is bounded by the host's `isize`.
let mut bytes = vec![0; usize::try_from(count).unwrap()];
// `File::read` never returns a value larger than `count`,
// so this cannot fail.
let result = file_descriptor
.read(communicate, &mut bytes, *this.tcx)?
.map(|c| i64::try_from(c).unwrap());
match result {
Ok(read_bytes) => {
// If reading to `bytes` did not fail, we write those bytes to the buffer.
this.write_bytes_ptr(buf, bytes)?;
Ok(read_bytes)
}
Err(e) => {
this.set_last_error_from_io_error(e.kind())?;
Ok(-1)
}
}
} else {
trace!("read: FD not found");
this.handle_not_found()
}
}
fn write(
&mut self,
fd: i32,
buf: Pointer<Option<Provenance>>,
count: u64,
) -> InterpResult<'tcx, i64> {
let this = self.eval_context_mut();
// Isolation check is done via `FileDescriptor` trait.
// Check that the *entire* buffer is actually valid memory.
this.check_ptr_access(buf, Size::from_bytes(count), CheckInAllocMsg::MemoryAccessTest)?;
// We cap the number of written bytes to the largest value that we are able to fit in both the
// host's and target's `isize`. This saves us from having to handle overflows later.
let count = count
.min(u64::try_from(this.target_isize_max()).unwrap())
.min(u64::try_from(isize::MAX).unwrap());
let communicate = this.machine.communicate();
if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
let bytes = this.read_bytes_ptr_strip_provenance(buf, Size::from_bytes(count))?;
let result = file_descriptor
.write(communicate, bytes, *this.tcx)?
.map(|c| i64::try_from(c).unwrap());
this.try_unwrap_io_result(result)
} else {
this.handle_not_found()
}
}
fn lseek64(
&mut self,
fd_op: &OpTy<'tcx, Provenance>,
offset_op: &OpTy<'tcx, Provenance>,
whence_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Scalar<Provenance>> {
let this = self.eval_context_mut();
// Isolation check is done via `FileDescriptor` trait.
let fd = this.read_scalar(fd_op)?.to_i32()?;
let offset = this.read_scalar(offset_op)?.to_i64()?;
let whence = this.read_scalar(whence_op)?.to_i32()?;
let seek_from = if whence == this.eval_libc_i32("SEEK_SET") {
SeekFrom::Start(u64::try_from(offset).unwrap())
} else if whence == this.eval_libc_i32("SEEK_CUR") {
SeekFrom::Current(offset)
} else if whence == this.eval_libc_i32("SEEK_END") {
SeekFrom::End(offset)
} else {
let einval = this.eval_libc("EINVAL");
this.set_last_error(einval)?;
return Ok(Scalar::from_i64(-1));
};
let communicate = this.machine.communicate();
Ok(Scalar::from_i64(
if let Some(file_descriptor) = this.machine.file_handler.handles.get_mut(&fd) {
let result = file_descriptor
.seek(communicate, seek_from)?
.map(|offset| i64::try_from(offset).unwrap());
this.try_unwrap_io_result(result)?
} else {
this.handle_not_found()?
},
))
}
fn unlink(&mut self, path_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
let this = self.eval_context_mut();
let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`unlink`", reject_with)?;
this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
return Ok(-1);
}
let result = remove_file(path).map(|_| 0);
this.try_unwrap_io_result(result)
}
fn symlink(
&mut self,
target_op: &OpTy<'tcx, Provenance>,
linkpath_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, i32> {
#[cfg(unix)]
fn create_link(src: &Path, dst: &Path) -> std::io::Result<()> {
std::os::unix::fs::symlink(src, dst)
}
#[cfg(windows)]
fn create_link(src: &Path, dst: &Path) -> std::io::Result<()> {
use std::os::windows::fs;
if src.is_dir() { fs::symlink_dir(src, dst) } else { fs::symlink_file(src, dst) }
}
let this = self.eval_context_mut();
let target = this.read_path_from_c_str(this.read_pointer(target_op)?)?;
let linkpath = this.read_path_from_c_str(this.read_pointer(linkpath_op)?)?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`symlink`", reject_with)?;
this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
return Ok(-1);
}
let result = create_link(&target, &linkpath).map(|_| 0);
this.try_unwrap_io_result(result)
}
fn macos_stat(
&mut self,
path_op: &OpTy<'tcx, Provenance>,
buf_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Scalar<Provenance>> {
let this = self.eval_context_mut();
this.assert_target_os("macos", "stat");
let path_scalar = this.read_pointer(path_op)?;
let path = this.read_path_from_c_str(path_scalar)?.into_owned();
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`stat`", reject_with)?;
let eacc = this.eval_libc("EACCES");
this.set_last_error(eacc)?;
return Ok(Scalar::from_i32(-1));
}
// `stat` always follows symlinks.
let metadata = match FileMetadata::from_path(this, &path, true)? {
Some(metadata) => metadata,
None => return Ok(Scalar::from_i32(-1)), // `FileMetadata` has set errno
};
Ok(Scalar::from_i32(this.macos_stat_write_buf(metadata, buf_op)?))
}
// `lstat` is used to get symlink metadata.
fn macos_lstat(
&mut self,
path_op: &OpTy<'tcx, Provenance>,
buf_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Scalar<Provenance>> {
let this = self.eval_context_mut();
this.assert_target_os("macos", "lstat");
let path_scalar = this.read_pointer(path_op)?;
let path = this.read_path_from_c_str(path_scalar)?.into_owned();
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`lstat`", reject_with)?;
let eacc = this.eval_libc("EACCES");
this.set_last_error(eacc)?;
return Ok(Scalar::from_i32(-1));
}
let metadata = match FileMetadata::from_path(this, &path, false)? {
Some(metadata) => metadata,
None => return Ok(Scalar::from_i32(-1)), // `FileMetadata` has set errno
};
Ok(Scalar::from_i32(this.macos_stat_write_buf(metadata, buf_op)?))
}
fn macos_fstat(
&mut self,
fd_op: &OpTy<'tcx, Provenance>,
buf_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Scalar<Provenance>> {
let this = self.eval_context_mut();
this.assert_target_os("macos", "fstat");
let fd = this.read_scalar(fd_op)?.to_i32()?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`fstat`", reject_with)?;
// Set error code as "EBADF" (bad fd)
return Ok(Scalar::from_i32(this.handle_not_found()?));
}
let metadata = match FileMetadata::from_fd(this, fd)? {
Some(metadata) => metadata,
None => return Ok(Scalar::from_i32(-1)),
};
Ok(Scalar::from_i32(this.macos_stat_write_buf(metadata, buf_op)?))
}
fn linux_statx(
&mut self,
dirfd_op: &OpTy<'tcx, Provenance>, // Should be an `int`
pathname_op: &OpTy<'tcx, Provenance>, // Should be a `const char *`
flags_op: &OpTy<'tcx, Provenance>, // Should be an `int`
mask_op: &OpTy<'tcx, Provenance>, // Should be an `unsigned int`
statxbuf_op: &OpTy<'tcx, Provenance>, // Should be a `struct statx *`
) -> InterpResult<'tcx, i32> {
let this = self.eval_context_mut();
this.assert_target_os("linux", "statx");
let dirfd = this.read_scalar(dirfd_op)?.to_i32()?;
let pathname_ptr = this.read_pointer(pathname_op)?;
let flags = this.read_scalar(flags_op)?.to_i32()?;
let _mask = this.read_scalar(mask_op)?.to_u32()?;
let statxbuf_ptr = this.read_pointer(statxbuf_op)?;
// If the statxbuf or pathname pointers are null, the function fails with `EFAULT`.
if this.ptr_is_null(statxbuf_ptr)? || this.ptr_is_null(pathname_ptr)? {
let efault = this.eval_libc("EFAULT");
this.set_last_error(efault)?;
return Ok(-1);
}
let statxbuf = this.deref_pointer_as(statxbuf_op, this.libc_ty_layout("statx"))?;
let path = this.read_path_from_c_str(pathname_ptr)?.into_owned();
// See <https://github.com/rust-lang/rust/pull/79196> for a discussion of argument sizes.
let at_empty_path = this.eval_libc_i32("AT_EMPTY_PATH");
let empty_path_flag = flags & at_empty_path == at_empty_path;
// We only support:
// * interpreting `path` as an absolute directory,
// * interpreting `path` as a path relative to `dirfd` when the latter is `AT_FDCWD`, or
// * interpreting `dirfd` as any file descriptor when `path` is empty and AT_EMPTY_PATH is
// set.
// Other behaviors cannot be tested from `libstd` and thus are not implemented. If you
// found this error, please open an issue reporting it.
if !(path.is_absolute()
|| dirfd == this.eval_libc_i32("AT_FDCWD")
|| (path.as_os_str().is_empty() && empty_path_flag))
{
throw_unsup_format!(
"using statx is only supported with absolute paths, relative paths with the file \
descriptor `AT_FDCWD`, and empty paths with the `AT_EMPTY_PATH` flag set and any \
file descriptor"
)
}
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`statx`", reject_with)?;
let ecode = if path.is_absolute() || dirfd == this.eval_libc_i32("AT_FDCWD") {
// since `path` is provided, either absolute or
// relative to CWD, `EACCES` is the most relevant.
this.eval_libc("EACCES")
} else {
// `dirfd` is set to target file, and `path` is empty
// (or we would have hit the `throw_unsup_format`
// above). `EACCES` would violate the spec.
assert!(empty_path_flag);
this.eval_libc("EBADF")
};
this.set_last_error(ecode)?;
return Ok(-1);
}
// the `_mask_op` parameter specifies the file information that the caller requested.
// However `statx` is allowed to return information that was not requested or to not
// return information that was requested. This `mask` represents the information we can
// actually provide for any target.
let mut mask = this.eval_libc_u32("STATX_TYPE") | this.eval_libc_u32("STATX_SIZE");
// If the `AT_SYMLINK_NOFOLLOW` flag is set, we query the file's metadata without following
// symbolic links.
let follow_symlink = flags & this.eval_libc_i32("AT_SYMLINK_NOFOLLOW") == 0;
// If the path is empty, and the AT_EMPTY_PATH flag is set, we query the open file
// represented by dirfd, whether it's a directory or otherwise.
let metadata = if path.as_os_str().is_empty() && empty_path_flag {
FileMetadata::from_fd(this, dirfd)?
} else {
FileMetadata::from_path(this, &path, follow_symlink)?
};
let metadata = match metadata {
Some(metadata) => metadata,
None => return Ok(-1),
};
// The `mode` field specifies the type of the file and the permissions over the file for
// the owner, its group and other users. Given that we can only provide the file type
// without using platform specific methods, we only set the bits corresponding to the file
// type. This should be an `__u16` but `libc` provides its values as `u32`.
let mode: u16 = metadata
.mode
.to_u32()?
.try_into()
.unwrap_or_else(|_| bug!("libc contains bad value for constant"));
// We need to set the corresponding bits of `mask` if the access, creation and modification
// times were available. Otherwise we let them be zero.
let (access_sec, access_nsec) = metadata
.accessed
.map(|tup| {
mask |= this.eval_libc_u32("STATX_ATIME");
InterpResult::Ok(tup)
})
.unwrap_or_else(|| Ok((0, 0)))?;
let (created_sec, created_nsec) = metadata
.created
.map(|tup| {
mask |= this.eval_libc_u32("STATX_BTIME");
InterpResult::Ok(tup)
})
.unwrap_or_else(|| Ok((0, 0)))?;
let (modified_sec, modified_nsec) = metadata
.modified
.map(|tup| {
mask |= this.eval_libc_u32("STATX_MTIME");
InterpResult::Ok(tup)
})
.unwrap_or_else(|| Ok((0, 0)))?;
// Now we write everything to `statxbuf`. We write a zero for the unavailable fields.
this.write_int_fields_named(
&[
("stx_mask", mask.into()),
("stx_blksize", 0),
("stx_attributes", 0),
("stx_nlink", 0),
("stx_uid", 0),
("stx_gid", 0),
("stx_mode", mode.into()),
("stx_ino", 0),
("stx_size", metadata.size.into()),
("stx_blocks", 0),
("stx_attributes_mask", 0),
("stx_rdev_major", 0),
("stx_rdev_minor", 0),
("stx_dev_major", 0),
("stx_dev_minor", 0),
],
&statxbuf,
)?;
#[rustfmt::skip]
this.write_int_fields_named(
&[
("tv_sec", access_sec.into()),
("tv_nsec", access_nsec.into()),
],
&this.project_field_named(&statxbuf, "stx_atime")?,
)?;
#[rustfmt::skip]
this.write_int_fields_named(
&[
("tv_sec", created_sec.into()),
("tv_nsec", created_nsec.into()),
],
&this.project_field_named(&statxbuf, "stx_btime")?,
)?;
#[rustfmt::skip]
this.write_int_fields_named(
&[
("tv_sec", 0.into()),
("tv_nsec", 0.into()),
],
&this.project_field_named(&statxbuf, "stx_ctime")?,
)?;
#[rustfmt::skip]
this.write_int_fields_named(
&[
("tv_sec", modified_sec.into()),
("tv_nsec", modified_nsec.into()),
],
&this.project_field_named(&statxbuf, "stx_mtime")?,
)?;
Ok(0)
}
fn rename(
&mut self,
oldpath_op: &OpTy<'tcx, Provenance>,
newpath_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, i32> {
let this = self.eval_context_mut();
let oldpath_ptr = this.read_pointer(oldpath_op)?;
let newpath_ptr = this.read_pointer(newpath_op)?;
if this.ptr_is_null(oldpath_ptr)? || this.ptr_is_null(newpath_ptr)? {
let efault = this.eval_libc("EFAULT");
this.set_last_error(efault)?;
return Ok(-1);
}
let oldpath = this.read_path_from_c_str(oldpath_ptr)?;
let newpath = this.read_path_from_c_str(newpath_ptr)?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`rename`", reject_with)?;
this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
return Ok(-1);
}
let result = rename(oldpath, newpath).map(|_| 0);
this.try_unwrap_io_result(result)
}
fn mkdir(
&mut self,
path_op: &OpTy<'tcx, Provenance>,
mode_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, i32> {
let this = self.eval_context_mut();
#[cfg_attr(not(unix), allow(unused_variables))]
let mode = if this.tcx.sess.target.os == "macos" {
u32::from(this.read_scalar(mode_op)?.to_u16()?)
} else {
this.read_scalar(mode_op)?.to_u32()?
};
let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`mkdir`", reject_with)?;
this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
return Ok(-1);
}
#[cfg_attr(not(unix), allow(unused_mut))]
let mut builder = DirBuilder::new();
// If the host supports it, forward on the mode of the directory
// (i.e. permission bits and the sticky bit)
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
builder.mode(mode);
}
let result = builder.create(path).map(|_| 0i32);
this.try_unwrap_io_result(result)
}
fn rmdir(&mut self, path_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
let this = self.eval_context_mut();
let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`rmdir`", reject_with)?;
this.set_last_error_from_io_error(ErrorKind::PermissionDenied)?;
return Ok(-1);
}
let result = remove_dir(path).map(|_| 0i32);
this.try_unwrap_io_result(result)
}
fn opendir(
&mut self,
name_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Scalar<Provenance>> {
let this = self.eval_context_mut();
let name = this.read_path_from_c_str(this.read_pointer(name_op)?)?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`opendir`", reject_with)?;
let eacc = this.eval_libc("EACCES");
this.set_last_error(eacc)?;
return Ok(Scalar::null_ptr(this));
}
let result = read_dir(name);
match result {
Ok(dir_iter) => {
let id = this.machine.dir_handler.insert_new(dir_iter);
// The libc API for opendir says that this method returns a pointer to an opaque
// structure, but we are returning an ID number. Thus, pass it as a scalar of
// pointer width.
Ok(Scalar::from_target_usize(id, this))
}
Err(e) => {
this.set_last_error_from_io_error(e.kind())?;
Ok(Scalar::null_ptr(this))
}
}
}
fn linux_readdir64(
&mut self,
dirp_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Scalar<Provenance>> {
let this = self.eval_context_mut();
this.assert_target_os("linux", "readdir64");
let dirp = this.read_target_usize(dirp_op)?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`readdir`", reject_with)?;
let eacc = this.eval_libc("EBADF");
this.set_last_error(eacc)?;
return Ok(Scalar::null_ptr(this));
}
let open_dir = this.machine.dir_handler.streams.get_mut(&dirp).ok_or_else(|| {
err_unsup_format!("the DIR pointer passed to readdir64 did not come from opendir")
})?;
let entry = match open_dir.read_dir.next() {
Some(Ok(dir_entry)) => {
// Write the directory entry into a newly allocated buffer.
// The name is written with write_bytes, while the rest of the
// dirent64 struct is written using write_int_fields.
// For reference:
// pub struct dirent64 {
// pub d_ino: ino64_t,
// pub d_off: off64_t,
// pub d_reclen: c_ushort,
// pub d_type: c_uchar,
// pub d_name: [c_char; 256],
// }
let mut name = dir_entry.file_name(); // not a Path as there are no separators!
name.push("\0"); // Add a NUL terminator
let name_bytes = name.as_encoded_bytes();
let name_len = u64::try_from(name_bytes.len()).unwrap();
let dirent64_layout = this.libc_ty_layout("dirent64");
let d_name_offset = dirent64_layout.fields.offset(4 /* d_name */).bytes();
let size = d_name_offset.checked_add(name_len).unwrap();
let entry =
this.malloc(size, /*zero_init:*/ false, MiriMemoryKind::Runtime)?;
// If the host is a Unix system, fill in the inode number with its real value.
// If not, use 0 as a fallback value.
#[cfg(unix)]
let ino = std::os::unix::fs::DirEntryExt::ino(&dir_entry);
#[cfg(not(unix))]
let ino = 0u64;
let file_type = this.file_type_to_d_type(dir_entry.file_type())?;
this.write_int_fields_named(
&[
("d_ino", ino.into()),
("d_off", 0),
("d_reclen", size.into()),
("d_type", file_type.into()),
],
&this.ptr_to_mplace(entry, dirent64_layout),
)?;
let name_ptr = entry.offset(Size::from_bytes(d_name_offset), this)?;
this.write_bytes_ptr(name_ptr, name_bytes.iter().copied())?;
entry
}
None => {
// end of stream: return NULL
Pointer::null()
}
Some(Err(e)) => {
this.set_last_error_from_io_error(e.kind())?;
Pointer::null()
}
};
let open_dir = this.machine.dir_handler.streams.get_mut(&dirp).unwrap();
let old_entry = std::mem::replace(&mut open_dir.entry, entry);
this.free(old_entry, MiriMemoryKind::Runtime)?;
Ok(Scalar::from_maybe_pointer(entry, this))
}
fn macos_readdir_r(
&mut self,
dirp_op: &OpTy<'tcx, Provenance>,
entry_op: &OpTy<'tcx, Provenance>,
result_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Scalar<Provenance>> {
let this = self.eval_context_mut();
this.assert_target_os("macos", "readdir_r");
let dirp = this.read_target_usize(dirp_op)?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`readdir_r`", reject_with)?;
// Set error code as "EBADF" (bad fd)
return Ok(Scalar::from_i32(this.handle_not_found()?));
}
let open_dir = this.machine.dir_handler.streams.get_mut(&dirp).ok_or_else(|| {
err_unsup_format!("the DIR pointer passed to readdir_r did not come from opendir")
})?;
Ok(Scalar::from_i32(match open_dir.read_dir.next() {
Some(Ok(dir_entry)) => {
// Write into entry, write pointer to result, return 0 on success.
// The name is written with write_os_str_to_c_str, while the rest of the
// dirent struct is written using write_int_fields.
// For reference:
// pub struct dirent {
// pub d_ino: u64,
// pub d_seekoff: u64,
// pub d_reclen: u16,
// pub d_namlen: u16,
// pub d_type: u8,
// pub d_name: [c_char; 1024],
// }
let entry_place = this.deref_pointer_as(entry_op, this.libc_ty_layout("dirent"))?;
let name_place = this.project_field(&entry_place, 5)?;
let file_name = dir_entry.file_name(); // not a Path as there are no separators!
let (name_fits, file_name_buf_len) = this.write_os_str_to_c_str(
&file_name,
name_place.ptr(),
name_place.layout.size.bytes(),
)?;
let file_name_len = file_name_buf_len.checked_sub(1).unwrap();
if !name_fits {
throw_unsup_format!(
"a directory entry had a name too large to fit in libc::dirent"
);
}
// If the host is a Unix system, fill in the inode number with its real value.
// If not, use 0 as a fallback value.
#[cfg(unix)]
let ino = std::os::unix::fs::DirEntryExt::ino(&dir_entry);
#[cfg(not(unix))]
let ino = 0u64;
let file_type = this.file_type_to_d_type(dir_entry.file_type())?;
this.write_int_fields_named(
&[
("d_ino", ino.into()),
("d_seekoff", 0),
("d_reclen", 0),
("d_namlen", file_name_len.into()),
("d_type", file_type.into()),
],
&entry_place,
)?;
let result_place = this.deref_pointer(result_op)?;
this.write_scalar(this.read_scalar(entry_op)?, &result_place)?;
0
}
None => {
// end of stream: return 0, assign *result=NULL
this.write_null(&this.deref_pointer(result_op)?)?;
0
}
Some(Err(e)) =>
match e.raw_os_error() {
// return positive error number on error
Some(error) => error,
None => {
throw_unsup_format!(
"the error {} couldn't be converted to a return value",
e
)
}
},
}))
}
fn closedir(&mut self, dirp_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
let this = self.eval_context_mut();
let dirp = this.read_target_usize(dirp_op)?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`closedir`", reject_with)?;
// Set error code as "EBADF" (bad fd)
return this.handle_not_found();
}
if let Some(open_dir) = this.machine.dir_handler.streams.remove(&dirp) {
this.free(open_dir.entry, MiriMemoryKind::Runtime)?;
drop(open_dir);
Ok(0)
} else {
this.handle_not_found()
}
}
fn ftruncate64(
&mut self,
fd_op: &OpTy<'tcx, Provenance>,
length_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Scalar<Provenance>> {
let this = self.eval_context_mut();
let fd = this.read_scalar(fd_op)?.to_i32()?;
let length = this.read_scalar(length_op)?.to_i64()?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`ftruncate64`", reject_with)?;
// Set error code as "EBADF" (bad fd)
return Ok(Scalar::from_i32(this.handle_not_found()?));
}
Ok(Scalar::from_i32(
if let Some(file_descriptor) = this.machine.file_handler.handles.get_mut(&fd) {
// FIXME: Support ftruncate64 for all FDs
let FileHandle { file, writable } =
file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
err_unsup_format!(
"`ftruncate64` is only supported on file-backed file descriptors"
)
})?;
if *writable {
if let Ok(length) = length.try_into() {
let result = file.set_len(length);
this.try_unwrap_io_result(result.map(|_| 0i32))?
} else {
let einval = this.eval_libc("EINVAL");
this.set_last_error(einval)?;
-1
}
} else {
// The file is not writable
let einval = this.eval_libc("EINVAL");
this.set_last_error(einval)?;
-1
}
} else {
this.handle_not_found()?
},
))
}
fn fsync(&mut self, fd_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
// On macOS, `fsync` (unlike `fcntl(F_FULLFSYNC)`) does not wait for the
// underlying disk to finish writing. In the interest of host compatibility,
// we conservatively implement this with `sync_all`, which
// *does* wait for the disk.
let this = self.eval_context_mut();
let fd = this.read_scalar(fd_op)?.to_i32()?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`fsync`", reject_with)?;
// Set error code as "EBADF" (bad fd)
return this.handle_not_found();
}
if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
// FIXME: Support fsync for all FDs
let FileHandle { file, writable } =
file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
err_unsup_format!("`fsync` is only supported on file-backed file descriptors")
})?;
let io_result = maybe_sync_file(file, *writable, File::sync_all);
this.try_unwrap_io_result(io_result)
} else {
this.handle_not_found()
}
}
fn fdatasync(&mut self, fd_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
let this = self.eval_context_mut();
let fd = this.read_scalar(fd_op)?.to_i32()?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`fdatasync`", reject_with)?;
// Set error code as "EBADF" (bad fd)
return this.handle_not_found();
}
if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
// FIXME: Support fdatasync for all FDs
let FileHandle { file, writable } =
file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
err_unsup_format!(
"`fdatasync` is only supported on file-backed file descriptors"
)
})?;
let io_result = maybe_sync_file(file, *writable, File::sync_data);
this.try_unwrap_io_result(io_result)
} else {
this.handle_not_found()
}
}
fn sync_file_range(
&mut self,
fd_op: &OpTy<'tcx, Provenance>,
offset_op: &OpTy<'tcx, Provenance>,
nbytes_op: &OpTy<'tcx, Provenance>,
flags_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Scalar<Provenance>> {
let this = self.eval_context_mut();
let fd = this.read_scalar(fd_op)?.to_i32()?;
let offset = this.read_scalar(offset_op)?.to_i64()?;
let nbytes = this.read_scalar(nbytes_op)?.to_i64()?;
let flags = this.read_scalar(flags_op)?.to_i32()?;
if offset < 0 || nbytes < 0 {
let einval = this.eval_libc("EINVAL");
this.set_last_error(einval)?;
return Ok(Scalar::from_i32(-1));
}
let allowed_flags = this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_BEFORE")
| this.eval_libc_i32("SYNC_FILE_RANGE_WRITE")
| this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_AFTER");
if flags & allowed_flags != flags {
let einval = this.eval_libc("EINVAL");
this.set_last_error(einval)?;
return Ok(Scalar::from_i32(-1));
}
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`sync_file_range`", reject_with)?;
// Set error code as "EBADF" (bad fd)
return Ok(Scalar::from_i32(this.handle_not_found()?));
}
if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
// FIXME: Support sync_data_range for all FDs
let FileHandle { file, writable } =
file_descriptor.downcast_ref::<FileHandle>().ok_or_else(|| {
err_unsup_format!(
"`sync_data_range` is only supported on file-backed file descriptors"
)
})?;
let io_result = maybe_sync_file(file, *writable, File::sync_data);
Ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
} else {
Ok(Scalar::from_i32(this.handle_not_found()?))
}
}
fn readlink(
&mut self,
pathname_op: &OpTy<'tcx, Provenance>,
buf_op: &OpTy<'tcx, Provenance>,
bufsize_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, i64> {
let this = self.eval_context_mut();
let pathname = this.read_path_from_c_str(this.read_pointer(pathname_op)?)?;
let buf = this.read_pointer(buf_op)?;
let bufsize = this.read_target_usize(bufsize_op)?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`readlink`", reject_with)?;
let eacc = this.eval_libc("EACCES");
this.set_last_error(eacc)?;
return Ok(-1);
}
let result = std::fs::read_link(pathname);
match result {
Ok(resolved) => {
// 'readlink' truncates the resolved path if the provided buffer is not large
// enough, and does *not* add a null terminator. That means we cannot use the usual
// `write_path_to_c_str` and have to re-implement parts of it ourselves.
let resolved = this.convert_path(
Cow::Borrowed(resolved.as_ref()),
crate::shims::os_str::PathConversion::HostToTarget,
);
let mut path_bytes = resolved.as_encoded_bytes();
let bufsize: usize = bufsize.try_into().unwrap();
if path_bytes.len() > bufsize {
path_bytes = &path_bytes[..bufsize]
}
this.write_bytes_ptr(buf, path_bytes.iter().copied())?;
Ok(path_bytes.len().try_into().unwrap())
}
Err(e) => {
this.set_last_error_from_io_error(e.kind())?;
Ok(-1)
}
}
}
#[cfg_attr(not(unix), allow(unused))]
fn isatty(
&mut self,
miri_fd: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Scalar<Provenance>> {
let this = self.eval_context_mut();
// "returns 1 if fd is an open file descriptor referring to a terminal;
// otherwise 0 is returned, and errno is set to indicate the error"
let fd = this.read_scalar(miri_fd)?.to_i32()?;
let error = if let Some(fd) = this.machine.file_handler.handles.get(&fd) {
if fd.is_tty(this.machine.communicate()) {
return Ok(Scalar::from_i32(1));
} else {
this.eval_libc("ENOTTY")
}
} else {
// FD does not exist
this.eval_libc("EBADF")
};
this.set_last_error(error)?;
Ok(Scalar::from_i32(0))
}
fn realpath(
&mut self,
path_op: &OpTy<'tcx, Provenance>,
processed_path_op: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Scalar<Provenance>> {
let this = self.eval_context_mut();
this.assert_target_os_is_unix("realpath");
let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
let processed_ptr = this.read_pointer(processed_path_op)?;
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`realpath`", reject_with)?;
let eacc = this.eval_libc("EACCES");
this.set_last_error(eacc)?;
return Ok(Scalar::from_target_usize(0, this));
}
let result = std::fs::canonicalize(pathname);
match result {
Ok(resolved) => {
let path_max = this
.eval_libc_i32("PATH_MAX")
.try_into()
.expect("PATH_MAX does not fit in u64");
let dest = if this.ptr_is_null(processed_ptr)? {
// POSIX says behavior when passing a null pointer is implementation-defined,
// but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer
// similarly to:
//
// "If resolved_path is specified as NULL, then realpath() uses
// malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold
// the resolved pathname, and returns a pointer to this buffer. The
// caller should deallocate this buffer using free(3)."
// <https://man7.org/linux/man-pages/man3/realpath.3.html>
this.alloc_path_as_c_str(&resolved, MiriMemoryKind::C.into())?
} else {
let (wrote_path, _) =
this.write_path_to_c_str(&resolved, processed_ptr, path_max)?;
if !wrote_path {
// Note that we do not explicitly handle `FILENAME_MAX`
// (different from `PATH_MAX` above) as it is Linux-specific and
// seems like a bit of a mess anyway: <https://eklitzke.org/path-max-is-tricky>.
let enametoolong = this.eval_libc("ENAMETOOLONG");
this.set_last_error(enametoolong)?;
return Ok(Scalar::from_target_usize(0, this));
}
processed_ptr
};
Ok(Scalar::from_maybe_pointer(dest, this))
}
Err(e) => {
this.set_last_error_from_io_error(e.kind())?;
Ok(Scalar::from_target_usize(0, this))
}
}
}
fn mkstemp(&mut self, template_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> {
use rand::seq::SliceRandom;
// POSIX defines the template string.
const TEMPFILE_TEMPLATE_STR: &str = "XXXXXX";
let this = self.eval_context_mut();
this.assert_target_os_is_unix("mkstemp");
// POSIX defines the maximum number of attempts before failure.
//
// `mkstemp()` relies on `tmpnam()` which in turn relies on `TMP_MAX`.
// POSIX says this about `TMP_MAX`:
// * Minimum number of unique filenames generated by `tmpnam()`.
// * Maximum number of times an application can call `tmpnam()` reliably.
// * The value of `TMP_MAX` is at least 25.
// * On XSI-conformant systems, the value of `TMP_MAX` is at least 10000.
// See <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdio.h.html>.
let max_attempts = this.eval_libc_u32("TMP_MAX");
// Get the raw bytes from the template -- as a byte slice, this is a string in the target
// (and the target is unix, so a byte slice is the right representation).
let template_ptr = this.read_pointer(template_op)?;
let mut template = this.eval_context_ref().read_c_str(template_ptr)?.to_owned();
let template_bytes = template.as_mut_slice();
// Reject if isolation is enabled.
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
this.reject_in_isolation("`mkstemp`", reject_with)?;
let eacc = this.eval_libc("EACCES");
this.set_last_error(eacc)?;
return Ok(-1);
}
// Get the bytes of the suffix we expect in _target_ encoding.
let suffix_bytes = TEMPFILE_TEMPLATE_STR.as_bytes();
// At this point we have one `&[u8]` that represents the template and one `&[u8]`
// that represents the expected suffix.
// Now we figure out the index of the slice we expect to contain the suffix.
let start_pos = template_bytes.len().saturating_sub(suffix_bytes.len());
let end_pos = template_bytes.len();
let last_six_char_bytes = &template_bytes[start_pos..end_pos];
// If we don't find the suffix, it is an error.
if last_six_char_bytes != suffix_bytes {
let einval = this.eval_libc("EINVAL");
this.set_last_error(einval)?;
return Ok(-1);
}
// At this point we know we have 6 ASCII 'X' characters as a suffix.
// From <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/posix/tempname.c#L175>
const SUBSTITUTIONS: &[char; 62] = &[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
];
// The file is opened with specific options, which Rust does not expose in a portable way.
// So we use specific APIs depending on the host OS.
let mut fopts = OpenOptions::new();
fopts.read(true).write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
fopts.mode(0o600);
// Do not allow others to read or modify this file.
fopts.custom_flags(libc::O_EXCL);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
// Do not allow others to read or modify this file.
fopts.share_mode(0);
}
// If the generated file already exists, we will try again `max_attempts` many times.
for _ in 0..max_attempts {
let rng = this.machine.rng.get_mut();
// Generate a random unique suffix.
let unique_suffix = SUBSTITUTIONS.choose_multiple(rng, 6).collect::<String>();
// Replace the template string with the random string.
template_bytes[start_pos..end_pos].copy_from_slice(unique_suffix.as_bytes());
// Write the modified template back to the passed in pointer to maintain POSIX semantics.
this.write_bytes_ptr(template_ptr, template_bytes.iter().copied())?;
// To actually open the file, turn this into a host OsString.
let p = bytes_to_os_str(template_bytes)?.to_os_string();
let possibly_unique = std::env::temp_dir().join::<PathBuf>(p.into());
let file = fopts.open(possibly_unique);
match file {
Ok(f) => {
let fh = &mut this.machine.file_handler;
let fd = fh.insert_fd(Box::new(FileHandle { file: f, writable: true }));
return Ok(fd);
}
Err(e) =>
match e.kind() {
// If the random file already exists, keep trying.
ErrorKind::AlreadyExists => continue,
// Any other errors are returned to the caller.
_ => {
// "On error, -1 is returned, and errno is set to
// indicate the error"
this.set_last_error_from_io_error(e.kind())?;
return Ok(-1);
}
},
}
}
// We ran out of attempts to create the file, return an error.
let eexist = this.eval_libc("EEXIST");
this.set_last_error(eexist)?;
Ok(-1)
}
}
/// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when
/// `time` is Ok. Returns `None` if `time` is an error. Fails if `time` happens before the unix
/// epoch.
fn extract_sec_and_nsec<'tcx>(
time: std::io::Result<SystemTime>,
) -> InterpResult<'tcx, Option<(u64, u32)>> {
time.ok()
.map(|time| {
let duration = system_time_to_duration(&time)?;
Ok((duration.as_secs(), duration.subsec_nanos()))
})
.transpose()
}
/// Stores a file's metadata in order to avoid code duplication in the different metadata related
/// shims.
struct FileMetadata {
mode: Scalar<Provenance>,
size: u64,
created: Option<(u64, u32)>,
accessed: Option<(u64, u32)>,
modified: Option<(u64, u32)>,
}
impl FileMetadata {
fn from_path<'tcx>(
ecx: &mut MiriInterpCx<'_, 'tcx>,
path: &Path,
follow_symlink: bool,
) -> InterpResult<'tcx, Option<FileMetadata>> {
let metadata =
if follow_symlink { std::fs::metadata(path) } else { std::fs::symlink_metadata(path) };
FileMetadata::from_meta(ecx, metadata)
}
fn from_fd<'tcx>(
ecx: &mut MiriInterpCx<'_, 'tcx>,
fd: i32,
) -> InterpResult<'tcx, Option<FileMetadata>> {
let option = ecx.machine.file_handler.handles.get(&fd);
let file = match option {
Some(file_descriptor) =>
&file_descriptor
.downcast_ref::<FileHandle>()
.ok_or_else(|| {
err_unsup_format!(
"obtaining metadata is only supported on file-backed file descriptors"
)
})?
.file,
None => return ecx.handle_not_found().map(|_: i32| None),
};
let metadata = file.metadata();
FileMetadata::from_meta(ecx, metadata)
}
fn from_meta<'tcx>(
ecx: &mut MiriInterpCx<'_, 'tcx>,
metadata: Result<std::fs::Metadata, std::io::Error>,
) -> InterpResult<'tcx, Option<FileMetadata>> {
let metadata = match metadata {
Ok(metadata) => metadata,
Err(e) => {
ecx.set_last_error_from_io_error(e.kind())?;
return Ok(None);
}
};
let file_type = metadata.file_type();
let mode_name = if file_type.is_file() {
"S_IFREG"
} else if file_type.is_dir() {
"S_IFDIR"
} else {
"S_IFLNK"
};
let mode = ecx.eval_libc(mode_name);
let size = metadata.len();
let created = extract_sec_and_nsec(metadata.created())?;
let accessed = extract_sec_and_nsec(metadata.accessed())?;
let modified = extract_sec_and_nsec(metadata.modified())?;
// FIXME: Provide more fields using platform specific methods.
Ok(Some(FileMetadata { mode, size, created, accessed, modified }))
}
}