diff --git a/src/tools/miri/src/machine.rs b/src/tools/miri/src/machine.rs index 2137de6a29b..14b7afcc971 100644 --- a/src/tools/miri/src/machine.rs +++ b/src/tools/miri/src/machine.rs @@ -31,7 +31,7 @@ use rustc_target::spec::abi::Abi; use crate::{ concurrency::{data_race, weak_memory}, - shims::unix::FileHandler, + shims::unix::FdTable, *, }; @@ -463,9 +463,9 @@ pub struct MiriMachine<'mir, 'tcx> { pub(crate) validate: bool, /// The table of file descriptors. - pub(crate) file_handler: shims::unix::FileHandler, + pub(crate) fds: shims::unix::FdTable, /// The table of directory descriptors. - pub(crate) dir_handler: shims::unix::DirHandler, + pub(crate) dirs: shims::unix::DirTable, /// This machine's monotone clock. pub(crate) clock: Clock, @@ -640,8 +640,8 @@ impl<'mir, 'tcx> MiriMachine<'mir, 'tcx> { tls: TlsData::default(), isolated_op: config.isolated_op, validate: config.validate, - file_handler: FileHandler::new(config.mute_stdout_stderr), - dir_handler: Default::default(), + fds: FdTable::new(config.mute_stdout_stderr), + dirs: Default::default(), layouts, threads: ThreadManager::default(), static_roots: Vec::new(), @@ -774,11 +774,11 @@ impl VisitProvenance for MiriMachine<'_, '_> { argv, cmd_line, extern_statics, - dir_handler, + dirs, borrow_tracker, data_race, alloc_addresses, - file_handler, + fds, tcx: _, isolated_op: _, validate: _, @@ -817,8 +817,8 @@ impl VisitProvenance for MiriMachine<'_, '_> { threads.visit_provenance(visit); tls.visit_provenance(visit); env_vars.visit_provenance(visit); - dir_handler.visit_provenance(visit); - file_handler.visit_provenance(visit); + dirs.visit_provenance(visit); + fds.visit_provenance(visit); data_race.visit_provenance(visit); borrow_tracker.visit_provenance(visit); alloc_addresses.visit_provenance(visit); diff --git a/src/tools/miri/src/shims/unix/fd.rs b/src/tools/miri/src/shims/unix/fd.rs new file mode 100644 index 00000000000..bc9348ee0e8 --- /dev/null +++ b/src/tools/miri/src/shims/unix/fd.rs @@ -0,0 +1,429 @@ +//! General management of file descriptors, and support for +//! standard file descriptors (stdin/stdout/stderr). + +use std::any::Any; +use std::collections::BTreeMap; +use std::io::{self, ErrorKind, IsTerminal, Read, SeekFrom, Write}; + +use rustc_middle::ty::TyCtxt; +use rustc_target::abi::Size; + +use crate::shims::unix::*; +use crate::*; + +/// Represents an open file descriptor. +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> { + throw_unsup_format!("cannot read from {}", self.name()); + } + + fn write<'tcx>( + &self, + _communicate_allowed: bool, + _bytes: &[u8], + _tcx: TyCtxt<'tcx>, + ) -> InterpResult<'tcx, io::Result> { + throw_unsup_format!("cannot write to {}", self.name()); + } + + fn seek<'tcx>( + &mut self, + _communicate_allowed: bool, + _offset: SeekFrom, + ) -> InterpResult<'tcx, io::Result> { + throw_unsup_format!("cannot seek on {}", self.name()); + } + + fn close<'tcx>( + self: Box, + _communicate_allowed: bool, + ) -> InterpResult<'tcx, io::Result> { + throw_unsup_format!("cannot close {}", self.name()); + } + + /// Return a new file descriptor *that refers to the same underlying object*. + fn dup(&mut self) -> io::Result>; + + fn is_tty(&self, _communicate_allowed: bool) -> bool { + false + } +} + +impl dyn FileDescriptor { + #[inline(always)] + pub fn downcast_ref(&self) -> Option<&T> { + (self as &dyn Any).downcast_ref() + } + + #[inline(always)] + pub fn downcast_mut(&mut self) -> Option<&mut T> { + (self as &mut dyn Any).downcast_mut() + } +} + +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> { + 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> { + Ok(Box::new(io::stdin())) + } + + 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> { + // 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> { + Ok(Box::new(io::stdout())) + } + + 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> { + // 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> { + Ok(Box::new(io::stderr())) + } + + fn is_tty(&self, communicate_allowed: bool) -> bool { + communicate_allowed && self.is_terminal() + } +} + +/// Like /dev/null +#[derive(Debug)] +pub 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> { + // We just don't write anything, but report to the user that we did. + Ok(Ok(bytes.len())) + } + + fn dup(&mut self) -> io::Result> { + Ok(Box::new(NullOutput)) + } +} + +/// The file descriptor table +#[derive(Debug)] +pub struct FdTable { + pub fds: BTreeMap>, +} + +impl VisitProvenance for FdTable { + fn visit_provenance(&self, _visit: &mut VisitWith<'_>) { + // All our FileDescriptor do not have any tags. + } +} + +impl FdTable { + pub(crate) fn new(mute_stdout_stderr: bool) -> FdTable { + let mut fds: BTreeMap<_, Box> = BTreeMap::new(); + fds.insert(0i32, Box::new(io::stdin())); + if mute_stdout_stderr { + fds.insert(1i32, Box::new(NullOutput)); + fds.insert(2i32, Box::new(NullOutput)); + } else { + fds.insert(1i32, Box::new(io::stdout())); + fds.insert(2i32, Box::new(io::stderr())); + } + FdTable { fds } + } + + pub fn insert_fd(&mut self, file_handle: Box) -> i32 { + self.insert_fd_with_min_fd(file_handle, 0) + } + + /// Insert a new FD that is at least `min_fd`. + pub fn insert_fd_with_min_fd( + &mut self, + file_handle: Box, + 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.fds.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.fds.last_key_value().map(|(fd, _)| fd.checked_add(1).unwrap()).unwrap_or(min_fd) + }); + + self.fds.try_insert(new_fd, file_handle).unwrap(); + new_fd + } + + pub fn get(&self, fd: i32) -> Option<&dyn FileDescriptor> { + Some(&**self.fds.get(&fd)?) + } + + pub fn get_mut(&mut self, fd: i32) -> Option<&mut dyn FileDescriptor> { + Some(&mut **self.fds.get_mut(&fd)?) + } + + pub fn remove(&mut self, fd: i32) -> Option> { + self.fds.remove(&fd) + } + + pub fn is_fd(&self, fd: i32) -> bool { + self.fds.contains_key(&fd) + } +} + +impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {} +pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { + 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.fds.is_fd(fd) { + Ok(this.eval_libc_i32("FD_CLOEXEC")) + } else { + this.fd_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()?; + + match this.machine.fds.get_mut(fd) { + Some(file_descriptor) => { + let dup_result = file_descriptor.dup(); + match dup_result { + Ok(dup_fd) => Ok(this.machine.fds.insert_fd_with_min_fd(dup_fd, start)), + Err(e) => { + this.set_last_error_from_io_error(e.kind())?; + Ok(-1) + } + } + } + None => this.fd_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); + } + + this.ffullsync_fd(fd) + } 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> { + 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.fds.remove(fd) { + let result = file_descriptor.close(this.machine.communicate())?; + this.try_unwrap_io_result(result)? + } else { + this.fd_not_found()? + })) + } + + /// Function used when a file descriptor does not exist. It returns `Ok(-1)`and sets + /// the last OS error to `libc::EBADF` (invalid file descriptor). This function uses + /// `T: From` instead of `i32` directly because some fs functions return different integer + /// types (like `read`, that returns an `i64`). + fn fd_not_found>(&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>, + 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.fds.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.fd_not_found() + } + } + + fn write( + &mut self, + fd: i32, + buf: Pointer>, + 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.fds.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.fd_not_found() + } + } +} diff --git a/src/tools/miri/src/shims/unix/foreign_items.rs b/src/tools/miri/src/shims/unix/foreign_items.rs index 4ceda809350..b1e1aec5880 100644 --- a/src/tools/miri/src/shims/unix/foreign_items.rs +++ b/src/tools/miri/src/shims/unix/foreign_items.rs @@ -8,8 +8,10 @@ use rustc_target::spec::abi::Abi; use crate::*; use shims::foreign_items::EmulateForeignItemResult; +use shims::unix::fd::EvalContextExt as _; use shims::unix::fs::EvalContextExt as _; use shims::unix::mem::EvalContextExt as _; +use shims::unix::socket::EvalContextExt as _; use shims::unix::sync::EvalContextExt as _; use shims::unix::thread::EvalContextExt as _; @@ -51,7 +53,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { // See `fn emulate_foreign_item_inner` in `shims/foreign_items.rs` for the general pattern. #[rustfmt::skip] match link_name.as_str() { - // Environment related shims + // Environment variables "getenv" => { let [name] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?; let result = this.getenv(name)?; @@ -79,25 +81,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { this.write_scalar(Scalar::from_i32(result), dest)?; } - // File related shims - "open" | "open64" => { - // `open` is variadic, the third argument is only present when the second argument has O_CREAT (or on linux O_TMPFILE, but miri doesn't support that) set - this.check_abi_and_shim_symbol_clash(abi, Abi::C { unwind: false }, link_name)?; - let result = this.open(args)?; - this.write_scalar(Scalar::from_i32(result), dest)?; - } - "close" => { - let [fd] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?; - let result = this.close(fd)?; - this.write_scalar(result, dest)?; - } - "fcntl" => { - // `fcntl` is variadic. The argument count is checked based on the first argument - // in `this.fcntl()`, so we do not use `check_shim` here. - this.check_abi_and_shim_symbol_clash(abi, Abi::C { unwind: false }, link_name)?; - let result = this.fcntl(args)?; - this.write_scalar(Scalar::from_i32(result), dest)?; - } + // File descriptors "read" => { let [fd, buf, count] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?; let fd = this.read_scalar(fd)?.to_i32()?; @@ -116,6 +100,26 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { // Now, `result` is the value we return back to the program. this.write_scalar(Scalar::from_target_isize(result, this), dest)?; } + "close" => { + let [fd] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?; + let result = this.close(fd)?; + this.write_scalar(result, dest)?; + } + "fcntl" => { + // `fcntl` is variadic. The argument count is checked based on the first argument + // in `this.fcntl()`, so we do not use `check_shim` here. + this.check_abi_and_shim_symbol_clash(abi, Abi::C { unwind: false }, link_name)?; + let result = this.fcntl(args)?; + this.write_scalar(Scalar::from_i32(result), dest)?; + } + + // File and file system access + "open" | "open64" => { + // `open` is variadic, the third argument is only present when the second argument has O_CREAT (or on linux O_TMPFILE, but miri doesn't support that) set + this.check_abi_and_shim_symbol_clash(abi, Abi::C { unwind: false }, link_name)?; + let result = this.open(args)?; + this.write_scalar(Scalar::from_i32(result), dest)?; + } "unlink" => { let [path] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?; let result = this.unlink(path)?; @@ -219,7 +223,16 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { this.write_scalar(Scalar::from_i32(result), dest)?; } - // Time related shims + // Sockets + "socketpair" => { + let [domain, type_, protocol, sv] = + this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?; + + let result = this.socketpair(domain, type_, protocol, sv)?; + this.write_scalar(result, dest)?; + } + + // Time "gettimeofday" => { let [tv, tz] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?; let result = this.gettimeofday(tv, tz)?; @@ -598,7 +611,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { if bufsize > 256 { let err = this.eval_libc("EIO"); this.set_last_error(err)?; - this.write_scalar(Scalar::from_i32(-1), dest)? + this.write_scalar(Scalar::from_i32(-1), dest)?; } else { this.gen_random(buf, bufsize)?; this.write_scalar(Scalar::from_i32(0), dest)?; diff --git a/src/tools/miri/src/shims/unix/fs.rs b/src/tools/miri/src/shims/unix/fs.rs index b141ca4a019..31076fdfaf6 100644 --- a/src/tools/miri/src/shims/unix/fs.rs +++ b/src/tools/miri/src/shims/unix/fs.rs @@ -1,6 +1,6 @@ -use std::any::Any; +//! File and file system access + use std::borrow::Cow; -use std::collections::BTreeMap; use std::fs::{ read_dir, remove_dir, remove_file, rename, DirBuilder, File, FileType, OpenOptions, ReadDir, }; @@ -13,70 +13,16 @@ use rustc_middle::ty::TyCtxt; use rustc_target::abi::Size; use crate::shims::os_str::bytes_to_os_str; +use crate::shims::unix::*; use crate::*; use shims::time::system_time_to_duration; #[derive(Debug)] -pub struct FileHandle { +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> { - throw_unsup_format!("cannot read from {}", self.name()); - } - - fn write<'tcx>( - &self, - _communicate_allowed: bool, - _bytes: &[u8], - _tcx: TyCtxt<'tcx>, - ) -> InterpResult<'tcx, io::Result> { - throw_unsup_format!("cannot write to {}", self.name()); - } - - fn seek<'tcx>( - &mut self, - _communicate_allowed: bool, - _offset: SeekFrom, - ) -> InterpResult<'tcx, io::Result> { - throw_unsup_format!("cannot seek on {}", self.name()); - } - - fn close<'tcx>( - self: Box, - _communicate_allowed: bool, - ) -> InterpResult<'tcx, io::Result> { - throw_unsup_format!("cannot close {}", self.name()); - } - - fn dup(&mut self) -> io::Result>; - - fn is_tty(&self, _communicate_allowed: bool) -> bool { - false - } -} - -impl dyn FileDescriptor { - #[inline(always)] - pub fn downcast_ref(&self) -> Option<&T> { - (self as &dyn Any).downcast_ref() - } - - #[inline(always)] - pub fn downcast_mut(&mut self) -> Option<&mut T> { - (self as &mut dyn Any).downcast_mut() - } -} - impl FileDescriptor for FileHandle { fn name(&self) -> &'static str { "FILE" @@ -147,172 +93,6 @@ impl FileDescriptor for FileHandle { } } -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> { - 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> { - Ok(Box::new(io::stdin())) - } - - 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> { - // 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> { - Ok(Box::new(io::stdout())) - } - - 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> { - // 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> { - Ok(Box::new(io::stderr())) - } - - 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> { - // We just don't write anything, but report to the user that we did. - Ok(Ok(bytes.len())) - } - - fn dup(&mut self) -> io::Result> { - Ok(Box::new(NullOutput)) - } -} - -#[derive(Debug)] -pub struct FileHandler { - pub handles: BTreeMap>, -} - -impl VisitProvenance for FileHandler { - fn visit_provenance(&self, _visit: &mut VisitWith<'_>) { - // All our FileDescriptor do not have any tags. - } -} - -impl FileHandler { - pub(crate) fn new(mute_stdout_stderr: bool) -> FileHandler { - let mut handles: BTreeMap<_, Box> = 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) -> i32 { - self.insert_fd_with_min_fd(file_handle, 0) - } - - fn insert_fd_with_min_fd(&mut self, file_handle: Box, 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( @@ -411,10 +191,11 @@ trait EvalContextExtPrivate<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx /// An open directory, tracked by DirHandler. #[derive(Debug)] -pub struct OpenDir { +struct OpenDir { /// The directory reader on the host. read_dir: ReadDir, - /// The most recent entry returned by readdir() + /// The most recent entry returned by readdir(). + /// Will be freed by the next call. entry: Pointer>, } @@ -425,8 +206,11 @@ impl OpenDir { } } +/// The table of open directories. +/// Curiously, Unix/POSIX does not unify this into the "file descriptor" concept... everything +/// is a file, except a directory is not? #[derive(Debug)] -pub struct DirHandler { +pub struct DirTable { /// Directory iterators used to emulate libc "directory streams", as used in opendir, readdir, /// and closedir. /// @@ -441,7 +225,7 @@ pub struct DirHandler { next_id: u64, } -impl DirHandler { +impl DirTable { #[allow(clippy::arithmetic_side_effects)] fn insert_new(&mut self, read_dir: ReadDir) -> u64 { let id = self.next_id; @@ -451,9 +235,9 @@ impl DirHandler { } } -impl Default for DirHandler { - fn default() -> DirHandler { - DirHandler { +impl Default for DirTable { + fn default() -> DirTable { + DirTable { streams: FxHashMap::default(), // Skip 0 as an ID, because it looks like a null pointer to libc next_id: 1, @@ -461,9 +245,9 @@ impl Default for DirHandler { } } -impl VisitProvenance for DirHandler { +impl VisitProvenance for DirTable { fn visit_provenance(&self, visit: &mut VisitWith<'_>) { - let DirHandler { streams, next_id: _ } = self; + let DirTable { streams, next_id: _ } = self; for dir in streams.values() { dir.entry.visit_provenance(visit); @@ -615,200 +399,13 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { } let fd = options.open(path).map(|file| { - let fh = &mut this.machine.file_handler; + let fh = &mut this.machine.fds; 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::().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> { - 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` instead of `i32` directly because some fs functions return different integer - /// types (like `read`, that returns an `i64`). - fn handle_not_found>(&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>, - 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>, - 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: i32, @@ -832,16 +429,14 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { }; 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()? - }, - )) + Ok(Scalar::from_i64(if let Some(file_descriptor) = this.machine.fds.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.fd_not_found()? + })) } fn unlink(&mut self, path_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> { @@ -970,7 +565,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { 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()?)); + return Ok(Scalar::from_i32(this.fd_not_found()?)); } let metadata = match FileMetadata::from_fd(this, fd)? { @@ -1269,7 +864,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { match result { Ok(dir_iter) => { - let id = this.machine.dir_handler.insert_new(dir_iter); + let id = this.machine.dirs.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 @@ -1301,7 +896,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { return Ok(Scalar::null_ptr(this)); } - let open_dir = this.machine.dir_handler.streams.get_mut(&dirp).ok_or_else(|| { + let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| { err_unsup_format!("the DIR pointer passed to readdir64 did not come from opendir") })?; @@ -1366,7 +961,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { } }; - let open_dir = this.machine.dir_handler.streams.get_mut(&dirp).unwrap(); + let open_dir = this.machine.dirs.streams.get_mut(&dirp).unwrap(); let old_entry = std::mem::replace(&mut open_dir.entry, entry); this.free(old_entry, MiriMemoryKind::Runtime)?; @@ -1391,10 +986,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { 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()?)); + return Ok(Scalar::from_i32(this.fd_not_found()?)); } - let open_dir = this.machine.dir_handler.streams.get_mut(&dirp).ok_or_else(|| { + let open_dir = this.machine.dirs.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() { @@ -1507,15 +1102,15 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { 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(); + return this.fd_not_found(); } - if let Some(open_dir) = this.machine.dir_handler.streams.remove(&dirp) { + if let Some(open_dir) = this.machine.dirs.streams.remove(&dirp) { this.free(open_dir.entry, MiriMemoryKind::Runtime)?; drop(open_dir); Ok(0) } else { - this.handle_not_found() + this.fd_not_found() } } @@ -1526,37 +1121,35 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { 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()?)); + return Ok(Scalar::from_i32(this.fd_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::().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 - } + Ok(Scalar::from_i32(if let Some(file_descriptor) = this.machine.fds.get_mut(fd) { + // FIXME: Support ftruncate64 for all FDs + let FileHandle { file, writable } = + file_descriptor.downcast_ref::().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 { - // The file is not writable let einval = this.eval_libc("EINVAL"); this.set_last_error(einval)?; -1 } } else { - this.handle_not_found()? - }, - )) + // The file is not writable + let einval = this.eval_libc("EINVAL"); + this.set_last_error(einval)?; + -1 + } + } else { + this.fd_not_found()? + })) } fn fsync(&mut self, fd_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> { @@ -1573,20 +1166,24 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { 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(); + return this.fd_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::().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() - } + return self.ffullsync_fd(fd); + } + + fn ffullsync_fd(&mut self, fd: i32) -> InterpResult<'tcx, i32> { + let this = self.eval_context_mut(); + let Some(file_descriptor) = this.machine.fds.get(fd) else { + return Ok(this.fd_not_found()?); + }; + // Only regular files support synchronization. + let FileHandle { file, writable } = + file_descriptor.downcast_ref::().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) } fn fdatasync(&mut self, fd_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> { @@ -1598,22 +1195,19 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { 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(); + return this.fd_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::().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() - } + let Some(file_descriptor) = this.machine.fds.get(fd) else { + return Ok(this.fd_not_found()?); + }; + // Only regular files support synchronization. + let FileHandle { file, writable } = + file_descriptor.downcast_ref::().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) } fn sync_file_range( @@ -1648,22 +1242,21 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { 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()?)); + return Ok(Scalar::from_i32(this.fd_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::().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()?)) - } + let Some(file_descriptor) = this.machine.fds.get(fd) else { + return Ok(Scalar::from_i32(this.fd_not_found()?)); + }; + // Only regular files support synchronization. + let FileHandle { file, writable } = + file_descriptor.downcast_ref::().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)?)) } fn readlink( @@ -1720,7 +1313,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { // "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) { + let error = if let Some(fd) = this.machine.fds.get(fd) { if fd.is_tty(this.machine.communicate()) { return Ok(Scalar::from_i32(1)); } else { @@ -1897,7 +1490,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { match file { Ok(f) => { - let fh = &mut this.machine.file_handler; + let fh = &mut this.machine.fds; let fd = fh.insert_fd(Box::new(FileHandle { file: f, writable: true })); return Ok(fd); } @@ -1963,7 +1556,7 @@ impl FileMetadata { ecx: &mut MiriInterpCx<'_, 'tcx>, fd: i32, ) -> InterpResult<'tcx, Option> { - let option = ecx.machine.file_handler.handles.get(&fd); + let option = ecx.machine.fds.get(fd); let file = match option { Some(file_descriptor) => &file_descriptor @@ -1974,7 +1567,7 @@ impl FileMetadata { ) })? .file, - None => return ecx.handle_not_found().map(|_: i32| None), + None => return ecx.fd_not_found().map(|_: i32| None), }; let metadata = file.metadata(); diff --git a/src/tools/miri/src/shims/unix/linux/fd.rs b/src/tools/miri/src/shims/unix/linux/fd.rs index 22fbb6da95a..7d5177e5c42 100644 --- a/src/tools/miri/src/shims/unix/linux/fd.rs +++ b/src/tools/miri/src/shims/unix/linux/fd.rs @@ -1,17 +1,12 @@ use std::cell::Cell; -use rustc_middle::ty::ScalarInt; - +use crate::shims::unix::*; use crate::*; use epoll::{Epoll, EpollEvent}; use event::Event; -use socketpair::SocketPair; - -use shims::unix::fs::EvalContextExt as _; pub mod epoll; pub mod event; -pub mod socketpair; impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {} pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { @@ -35,7 +30,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { throw_unsup_format!("epoll_create1 flags {flags} are not implemented"); } - let fd = this.machine.file_handler.insert_fd(Box::new(Epoll::default())); + let fd = this.machine.fds.insert_fd(Box::new(Epoll::default())); Ok(Scalar::from_i32(fd)) } @@ -79,7 +74,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { let data = this.read_scalar(&data)?; let event = EpollEvent { events, data }; - if let Some(epfd) = this.machine.file_handler.handles.get_mut(&epfd) { + if let Some(epfd) = this.machine.fds.get_mut(epfd) { let epfd = epfd .downcast_mut::() .ok_or_else(|| err_unsup_format!("non-epoll FD passed to `epoll_ctl`"))?; @@ -87,10 +82,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { epfd.file_descriptors.insert(fd, event); Ok(Scalar::from_i32(0)) } else { - Ok(Scalar::from_i32(this.handle_not_found()?)) + Ok(Scalar::from_i32(this.fd_not_found()?)) } } else if op == epoll_ctl_del { - if let Some(epfd) = this.machine.file_handler.handles.get_mut(&epfd) { + if let Some(epfd) = this.machine.fds.get_mut(epfd) { let epfd = epfd .downcast_mut::() .ok_or_else(|| err_unsup_format!("non-epoll FD passed to `epoll_ctl`"))?; @@ -98,7 +93,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { epfd.file_descriptors.remove(&fd); Ok(Scalar::from_i32(0)) } else { - Ok(Scalar::from_i32(this.handle_not_found()?)) + Ok(Scalar::from_i32(this.fd_not_found()?)) } } else { let einval = this.eval_libc("EINVAL"); @@ -150,7 +145,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { let _maxevents = this.read_scalar(maxevents)?.to_i32()?; let _timeout = this.read_scalar(timeout)?.to_i32()?; - if let Some(epfd) = this.machine.file_handler.handles.get_mut(&epfd) { + if let Some(epfd) = this.machine.fds.get_mut(epfd) { let _epfd = epfd .downcast_mut::() .ok_or_else(|| err_unsup_format!("non-epoll FD passed to `epoll_wait`"))?; @@ -158,7 +153,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { // FIXME return number of events ready when scheme for marking events ready exists throw_unsup_format!("returning ready events from epoll_wait is not yet implemented"); } else { - Ok(Scalar::from_i32(this.handle_not_found()?)) + Ok(Scalar::from_i32(this.fd_not_found()?)) } } @@ -203,51 +198,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { throw_unsup_format!("EFD_SEMAPHORE is unsupported"); } - let fh = &mut this.machine.file_handler; - let fd = fh.insert_fd(Box::new(Event { val: Cell::new(val.into()) })); + let fd = this.machine.fds.insert_fd(Box::new(Event { val: Cell::new(val.into()) })); Ok(Scalar::from_i32(fd)) } - - /// Currently this function creates new `SocketPair`s without specifying the domain, type, or - /// protocol of the new socket and these are stored in the socket values `sv` argument. - /// - /// This function creates an unnamed pair of connected sockets in the specified domain, of the - /// specified type, and using the optionally specified protocol. - /// - /// The `domain` argument specified a communication domain; this selects the protocol family - /// used for communication. The socket `type` specifies the communication semantics. - /// The `protocol` specifies a particular protocol to use with the socket. Normally there's - /// only a single protocol supported for a particular socket type within a given protocol - /// family, in which case `protocol` can be specified as 0. It is possible that many protocols - /// exist and in that case, a particular protocol must be specified. - /// - /// For more information on the arguments see the socket manpage: - /// - /// - /// - fn socketpair( - &mut self, - domain: &OpTy<'tcx, Provenance>, - type_: &OpTy<'tcx, Provenance>, - protocol: &OpTy<'tcx, Provenance>, - sv: &OpTy<'tcx, Provenance>, - ) -> InterpResult<'tcx, Scalar> { - let this = self.eval_context_mut(); - - let _domain = this.read_scalar(domain)?.to_i32()?; - let _type_ = this.read_scalar(type_)?.to_i32()?; - let _protocol = this.read_scalar(protocol)?.to_i32()?; - let sv = this.deref_pointer(sv)?; - - let fh = &mut this.machine.file_handler; - let sv0 = fh.insert_fd(Box::new(SocketPair)); - let sv0 = ScalarInt::try_from_int(sv0, sv.layout.size).unwrap(); - let sv1 = fh.insert_fd(Box::new(SocketPair)); - let sv1 = ScalarInt::try_from_int(sv1, sv.layout.size).unwrap(); - - this.write_scalar(sv0, &sv)?; - this.write_scalar(sv1, &sv.offset(sv.layout.size, sv.layout, this)?)?; - - Ok(Scalar::from_i32(0)) - } } diff --git a/src/tools/miri/src/shims/unix/linux/fd/epoll.rs b/src/tools/miri/src/shims/unix/linux/fd/epoll.rs index 8c5aed6def6..f2da76ca98d 100644 --- a/src/tools/miri/src/shims/unix/linux/fd/epoll.rs +++ b/src/tools/miri/src/shims/unix/linux/fd/epoll.rs @@ -1,6 +1,6 @@ use crate::*; -use crate::shims::unix::fs::FileDescriptor; +use crate::shims::unix::FileDescriptor; use rustc_data_structures::fx::FxHashMap; use std::io; diff --git a/src/tools/miri/src/shims/unix/linux/fd/event.rs b/src/tools/miri/src/shims/unix/linux/fd/event.rs index 49408fda3ae..0eb4befd52f 100644 --- a/src/tools/miri/src/shims/unix/linux/fd/event.rs +++ b/src/tools/miri/src/shims/unix/linux/fd/event.rs @@ -1,4 +1,4 @@ -use crate::shims::unix::fs::FileDescriptor; +use crate::shims::unix::FileDescriptor; use rustc_const_eval::interpret::InterpResult; use rustc_middle::ty::TyCtxt; diff --git a/src/tools/miri/src/shims/unix/linux/fd/socketpair.rs b/src/tools/miri/src/shims/unix/linux/fd/socketpair.rs deleted file mode 100644 index 6adae88235f..00000000000 --- a/src/tools/miri/src/shims/unix/linux/fd/socketpair.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::*; - -use crate::shims::unix::fs::FileDescriptor; - -use std::io; - -/// Pair of connected sockets. -/// -/// We currently don't allow sending any data through this pair, so this can be just a dummy. -#[derive(Debug)] -pub struct SocketPair; - -impl FileDescriptor for SocketPair { - fn name(&self) -> &'static str { - "socketpair" - } - - fn dup(&mut self) -> io::Result> { - Ok(Box::new(SocketPair)) - } - - fn close<'tcx>( - self: Box, - _communicate_allowed: bool, - ) -> InterpResult<'tcx, io::Result> { - Ok(Ok(0)) - } -} diff --git a/src/tools/miri/src/shims/unix/linux/foreign_items.rs b/src/tools/miri/src/shims/unix/linux/foreign_items.rs index d13ada0f4cf..7e600f4c54b 100644 --- a/src/tools/miri/src/shims/unix/linux/foreign_items.rs +++ b/src/tools/miri/src/shims/unix/linux/foreign_items.rs @@ -3,15 +3,12 @@ use rustc_target::spec::abi::Abi; use crate::machine::SIGRTMAX; use crate::machine::SIGRTMIN; +use crate::shims::unix::*; use crate::*; use shims::foreign_items::EmulateForeignItemResult; -use shims::unix::fs::EvalContextExt as _; use shims::unix::linux::fd::EvalContextExt as _; use shims::unix::linux::mem::EvalContextExt as _; use shims::unix::linux::sync::futex; -use shims::unix::mem::EvalContextExt as _; -use shims::unix::sync::EvalContextExt as _; -use shims::unix::thread::EvalContextExt as _; pub fn is_dyn_sym(name: &str) -> bool { matches!(name, "getrandom") diff --git a/src/tools/miri/src/shims/unix/macos/foreign_items.rs b/src/tools/miri/src/shims/unix/macos/foreign_items.rs index 3af01eb44d8..53a02bf5e0b 100644 --- a/src/tools/miri/src/shims/unix/macos/foreign_items.rs +++ b/src/tools/miri/src/shims/unix/macos/foreign_items.rs @@ -1,10 +1,9 @@ use rustc_span::Symbol; use rustc_target::spec::abi::Abi; +use crate::shims::unix::*; use crate::*; use shims::foreign_items::EmulateForeignItemResult; -use shims::unix::fs::EvalContextExt as _; -use shims::unix::thread::EvalContextExt as _; pub fn is_dyn_sym(_name: &str) -> bool { false diff --git a/src/tools/miri/src/shims/unix/mod.rs b/src/tools/miri/src/shims/unix/mod.rs index 638473da02b..2bc41e1a62d 100644 --- a/src/tools/miri/src/shims/unix/mod.rs +++ b/src/tools/miri/src/shims/unix/mod.rs @@ -1,7 +1,9 @@ pub mod foreign_items; +mod fd; mod fs; mod mem; +mod socket; mod sync; mod thread; @@ -9,7 +11,15 @@ mod freebsd; mod linux; mod macos; -pub use fs::{DirHandler, FileHandler}; +pub use fd::{FdTable, FileDescriptor}; +pub use fs::DirTable; +// All the unix-specific extension traits +pub use fd::EvalContextExt as _; +pub use fs::EvalContextExt as _; +pub use mem::EvalContextExt as _; +pub use socket::EvalContextExt as _; +pub use sync::EvalContextExt as _; +pub use thread::EvalContextExt as _; // Make up some constants. const UID: u32 = 1000; diff --git a/src/tools/miri/src/shims/unix/socket.rs b/src/tools/miri/src/shims/unix/socket.rs new file mode 100644 index 00000000000..aa06425ffa1 --- /dev/null +++ b/src/tools/miri/src/shims/unix/socket.rs @@ -0,0 +1,66 @@ +use std::io; + +use crate::shims::unix::*; +use crate::*; + +/// Pair of connected sockets. +/// +/// We currently don't allow sending any data through this pair, so this can be just a dummy. +/// FIXME: show proper errors when trying to send/receive +#[derive(Debug)] +struct SocketPair; + +impl FileDescriptor for SocketPair { + fn name(&self) -> &'static str { + "socketpair" + } + + fn dup(&mut self) -> io::Result> { + Ok(Box::new(SocketPair)) + } + + fn close<'tcx>( + self: Box, + _communicate_allowed: bool, + ) -> InterpResult<'tcx, io::Result> { + Ok(Ok(0)) + } +} + +impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {} +pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { + /// Currently this function this function is a stub. Eventually we need to + /// properly implement an FD type for sockets and have this function create + /// two sockets and associated FDs such that writing to one will produce + /// data that can be read from the other. + /// + /// For more information on the arguments see the socketpair manpage: + /// + fn socketpair( + &mut self, + domain: &OpTy<'tcx, Provenance>, + type_: &OpTy<'tcx, Provenance>, + protocol: &OpTy<'tcx, Provenance>, + sv: &OpTy<'tcx, Provenance>, + ) -> InterpResult<'tcx, Scalar> { + let this = self.eval_context_mut(); + + let _domain = this.read_scalar(domain)?.to_i32()?; + let _type_ = this.read_scalar(type_)?.to_i32()?; + let _protocol = this.read_scalar(protocol)?.to_i32()?; + let sv = this.deref_pointer(sv)?; + + // FIXME: fail on unsupported inputs + + let fds = &mut this.machine.fds; + let sv0 = fds.insert_fd(Box::new(SocketPair)); + let sv0 = Scalar::try_from_int(sv0, sv.layout.size).unwrap(); + let sv1 = fds.insert_fd(Box::new(SocketPair)); + let sv1 = Scalar::try_from_int(sv1, sv.layout.size).unwrap(); + + this.write_scalar(sv0, &sv)?; + this.write_scalar(sv1, &sv.offset(sv.layout.size, sv.layout, this)?)?; + + Ok(Scalar::from_i32(0)) + } +}