diff --git a/src/shims/files.rs b/src/shims/files.rs new file mode 100644 index 0000000000..1d533fffad --- /dev/null +++ b/src/shims/files.rs @@ -0,0 +1,411 @@ +use std::any::Any; +use std::collections::BTreeMap; +use std::io::{IsTerminal, Read, SeekFrom, Write}; +use std::ops::Deref; +use std::rc::{Rc, Weak}; +use std::{fs, io}; + +use rustc_abi::Size; + +use crate::shims::unix::UnixFileDescription; +use crate::*; + +/// Represents an open file description. +pub trait FileDescription: std::fmt::Debug + Any { + fn name(&self) -> &'static str; + + /// Reads as much as possible into the given buffer `ptr`. + /// `len` indicates how many bytes we should try to read. + /// `dest` is where the return value should be stored: number of bytes read, or `-1` in case of error. + fn read<'tcx>( + &self, + _self_ref: &FileDescriptionRef, + _communicate_allowed: bool, + _ptr: Pointer, + _len: usize, + _dest: &MPlaceTy<'tcx>, + _ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx> { + throw_unsup_format!("cannot read from {}", self.name()); + } + + /// Writes as much as possible from the given buffer `ptr`. + /// `len` indicates how many bytes we should try to write. + /// `dest` is where the return value should be stored: number of bytes written, or `-1` in case of error. + fn write<'tcx>( + &self, + _self_ref: &FileDescriptionRef, + _communicate_allowed: bool, + _ptr: Pointer, + _len: usize, + _dest: &MPlaceTy<'tcx>, + _ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx> { + throw_unsup_format!("cannot write to {}", self.name()); + } + + /// Seeks to the given offset (which can be relative to the beginning, end, or current position). + /// Returns the new position from the start of the stream. + fn seek<'tcx>( + &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, + _ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx, io::Result<()>> { + throw_unsup_format!("cannot close {}", self.name()); + } + + fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { + throw_unsup_format!("obtaining metadata is only supported on file-backed file descriptors"); + } + + fn is_tty(&self, _communicate_allowed: bool) -> bool { + // Most FDs are not tty's and the consequence of a wrong `false` are minor, + // so we use a default impl here. + false + } + + fn as_unix<'tcx>(&self) -> InterpResult<'tcx, &dyn UnixFileDescription> { + throw_unsup_format!("Not a unix file descriptor: {}", self.name()); + } +} + +impl dyn FileDescription { + #[inline(always)] + pub fn downcast(&self) -> Option<&T> { + (self as &dyn Any).downcast_ref() + } +} + +impl FileDescription for io::Stdin { + fn name(&self) -> &'static str { + "stdin" + } + + fn read<'tcx>( + &self, + _self_ref: &FileDescriptionRef, + communicate_allowed: bool, + ptr: Pointer, + len: usize, + dest: &MPlaceTy<'tcx>, + ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx> { + let mut bytes = vec![0; len]; + 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")?; + } + let result = Read::read(&mut { self }, &mut bytes); + match result { + Ok(read_size) => ecx.return_read_success(ptr, &bytes, read_size, dest), + Err(e) => ecx.set_last_error_and_return(e, dest), + } + } + + fn is_tty(&self, communicate_allowed: bool) -> bool { + communicate_allowed && self.is_terminal() + } +} + +impl FileDescription for io::Stdout { + fn name(&self) -> &'static str { + "stdout" + } + + fn write<'tcx>( + &self, + _self_ref: &FileDescriptionRef, + _communicate_allowed: bool, + ptr: Pointer, + len: usize, + dest: &MPlaceTy<'tcx>, + ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx> { + let bytes = ecx.read_bytes_ptr_strip_provenance(ptr, Size::from_bytes(len))?; + // 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(); + match result { + Ok(write_size) => ecx.return_write_success(write_size, dest), + Err(e) => ecx.set_last_error_and_return(e, dest), + } + } + + fn is_tty(&self, communicate_allowed: bool) -> bool { + communicate_allowed && self.is_terminal() + } +} + +impl FileDescription for io::Stderr { + fn name(&self) -> &'static str { + "stderr" + } + + fn write<'tcx>( + &self, + _self_ref: &FileDescriptionRef, + _communicate_allowed: bool, + ptr: Pointer, + len: usize, + dest: &MPlaceTy<'tcx>, + ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx> { + let bytes = ecx.read_bytes_ptr_strip_provenance(ptr, Size::from_bytes(len))?; + // We allow writing to stderr even with isolation enabled. + // No need to flush, stderr is not buffered. + let result = Write::write(&mut { self }, bytes); + match result { + Ok(write_size) => ecx.return_write_success(write_size, dest), + Err(e) => ecx.set_last_error_and_return(e, dest), + } + } + + fn is_tty(&self, communicate_allowed: bool) -> bool { + communicate_allowed && self.is_terminal() + } +} + +/// Like /dev/null +#[derive(Debug)] +pub struct NullOutput; + +impl FileDescription for NullOutput { + fn name(&self) -> &'static str { + "stderr and stdout" + } + + fn write<'tcx>( + &self, + _self_ref: &FileDescriptionRef, + _communicate_allowed: bool, + _ptr: Pointer, + len: usize, + dest: &MPlaceTy<'tcx>, + ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx> { + // We just don't write anything, but report to the user that we did. + ecx.return_write_success(len, dest) + } +} + +/// Structure contains both the file description and its unique identifier. +#[derive(Clone, Debug)] +pub struct FileDescWithId { + id: FdId, + file_description: Box, +} + +#[derive(Clone, Debug)] +pub struct FileDescriptionRef(Rc>); + +impl Deref for FileDescriptionRef { + type Target = dyn FileDescription; + + fn deref(&self) -> &Self::Target { + &*self.0.file_description + } +} + +impl FileDescriptionRef { + pub fn new(fd: impl FileDescription, id: FdId) -> Self { + FileDescriptionRef(Rc::new(FileDescWithId { id, file_description: Box::new(fd) })) + } + + pub fn close<'tcx>( + self, + communicate_allowed: bool, + ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx, io::Result<()>> { + // Destroy this `Rc` using `into_inner` so we can call `close` instead of + // implicitly running the destructor of the file description. + let id = self.get_id(); + match Rc::into_inner(self.0) { + Some(fd) => { + // Remove entry from the global epoll_event_interest table. + ecx.machine.epoll_interests.remove(id); + + fd.file_description.close(communicate_allowed, ecx) + } + None => interp_ok(Ok(())), + } + } + + pub fn downgrade(&self) -> WeakFileDescriptionRef { + WeakFileDescriptionRef { weak_ref: Rc::downgrade(&self.0) } + } + + pub fn get_id(&self) -> FdId { + self.0.id + } +} + +/// Holds a weak reference to the actual file description. +#[derive(Clone, Debug, Default)] +pub struct WeakFileDescriptionRef { + weak_ref: Weak>, +} + +impl WeakFileDescriptionRef { + pub fn upgrade(&self) -> Option { + if let Some(file_desc_with_id) = self.weak_ref.upgrade() { + return Some(FileDescriptionRef(file_desc_with_id)); + } + None + } +} + +impl VisitProvenance for WeakFileDescriptionRef { + fn visit_provenance(&self, _visit: &mut VisitWith<'_>) { + // A weak reference can never be the only reference to some pointer or place. + // Since the actual file description is tracked by strong ref somewhere, + // it is ok to make this a NOP operation. + } +} + +/// A unique id for file descriptions. While we could use the address, considering that +/// is definitely unique, the address would expose interpreter internal state when used +/// for sorting things. So instead we generate a unique id per file description that stays +/// the same even if a file descriptor is duplicated and gets a new integer file descriptor. +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd)] +pub struct FdId(usize); + +/// The file descriptor table +#[derive(Debug)] +pub struct FdTable { + pub fds: BTreeMap, + /// Unique identifier for file description, used to differentiate between various file description. + next_file_description_id: FdId, +} + +impl VisitProvenance for FdTable { + fn visit_provenance(&self, _visit: &mut VisitWith<'_>) { + // All our FileDescription instances do not have any tags. + } +} + +impl FdTable { + fn new() -> Self { + FdTable { fds: BTreeMap::new(), next_file_description_id: FdId(0) } + } + pub(crate) fn init(mute_stdout_stderr: bool) -> FdTable { + let mut fds = FdTable::new(); + fds.insert_new(io::stdin()); + if mute_stdout_stderr { + assert_eq!(fds.insert_new(NullOutput), 1); + assert_eq!(fds.insert_new(NullOutput), 2); + } else { + assert_eq!(fds.insert_new(io::stdout()), 1); + assert_eq!(fds.insert_new(io::stderr()), 2); + } + fds + } + + pub fn new_ref(&mut self, fd: impl FileDescription) -> FileDescriptionRef { + let file_handle = FileDescriptionRef::new(fd, self.next_file_description_id); + self.next_file_description_id = FdId(self.next_file_description_id.0.strict_add(1)); + file_handle + } + + /// Insert a new file description to the FdTable. + pub fn insert_new(&mut self, fd: impl FileDescription) -> i32 { + let fd_ref = self.new_ref(fd); + self.insert(fd_ref) + } + + pub fn insert(&mut self, fd_ref: FileDescriptionRef) -> i32 { + self.insert_with_min_num(fd_ref, 0) + } + + /// Insert a file description, giving it a file descriptor that is at least `min_fd_num`. + pub fn insert_with_min_num(&mut self, file_handle: FileDescriptionRef, min_fd_num: 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_num..).zip(min_fd_num..).find_map(|((fd_num, _fd), counter)| { + if *fd_num != 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_num = 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_num, _)| fd_num.strict_add(1)).unwrap_or(min_fd_num) + }); + + self.fds.try_insert(new_fd_num, file_handle).unwrap(); + new_fd_num + } + + pub fn get(&self, fd_num: i32) -> Option { + let fd = self.fds.get(&fd_num)?; + Some(fd.clone()) + } + + pub fn remove(&mut self, fd_num: i32) -> Option { + self.fds.remove(&fd_num) + } + + pub fn is_fd_num(&self, fd_num: i32) -> bool { + self.fds.contains_key(&fd_num) + } +} + +impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} +pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { + /// Helper to implement `FileDescription::read`: + /// This is only used when `read` is successful. + /// `actual_read_size` should be the return value of some underlying `read` call that used + /// `bytes` as its output buffer. + /// The length of `bytes` must not exceed either the host's or the target's `isize`. + /// `bytes` is written to `buf` and the size is written to `dest`. + fn return_read_success( + &mut self, + buf: Pointer, + bytes: &[u8], + actual_read_size: usize, + dest: &MPlaceTy<'tcx>, + ) -> InterpResult<'tcx> { + let this = self.eval_context_mut(); + // If reading to `bytes` did not fail, we write those bytes to the buffer. + // Crucially, if fewer than `bytes.len()` bytes were read, only write + // that much into the output buffer! + this.write_bytes_ptr(buf, bytes[..actual_read_size].iter().copied())?; + + // The actual read size is always less than what got originally requested so this cannot fail. + this.write_int(u64::try_from(actual_read_size).unwrap(), dest)?; + interp_ok(()) + } + + /// Helper to implement `FileDescription::write`: + /// This function is only used when `write` is successful, and writes `actual_write_size` to `dest` + fn return_write_success( + &mut self, + actual_write_size: usize, + dest: &MPlaceTy<'tcx>, + ) -> InterpResult<'tcx> { + let this = self.eval_context_mut(); + // The actual write size is always less than what got originally requested so this cannot fail. + this.write_int(u64::try_from(actual_write_size).unwrap(), dest)?; + interp_ok(()) + } +} diff --git a/src/shims/io_error.rs b/src/shims/io_error.rs index 0cbb4850b7..23515020b2 100644 --- a/src/shims/io_error.rs +++ b/src/shims/io_error.rs @@ -87,6 +87,7 @@ const WINDOWS_IO_ERROR_TABLE: &[(&str, std::io::ErrorKind)] = { ("ERROR_ACCESS_DENIED", PermissionDenied), ("ERROR_FILE_NOT_FOUND", NotFound), ("ERROR_INVALID_PARAMETER", InvalidInput), + ("ERROR_FILE_EXISTS", AlreadyExists), ] }; diff --git a/src/shims/mod.rs b/src/shims/mod.rs index b9317ac1a1..61681edcf7 100644 --- a/src/shims/mod.rs +++ b/src/shims/mod.rs @@ -2,6 +2,7 @@ mod alloc; mod backtrace; +mod files; #[cfg(unix)] mod native_lib; mod unix; @@ -18,7 +19,8 @@ pub mod panic; pub mod time; pub mod tls; -pub use self::unix::{DirTable, EpollInterestTable, FdTable}; +pub use self::files::FdTable; +pub use self::unix::{DirTable, EpollInterestTable}; /// What needs to be done after emulating an item (a shim or an intrinsic) is done. pub enum EmulateItemResult { diff --git a/src/shims/unix/fd.rs b/src/shims/unix/fd.rs index 27bdd508f7..67a9bb7c7c 100644 --- a/src/shims/unix/fd.rs +++ b/src/shims/unix/fd.rs @@ -1,15 +1,12 @@ //! 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 std::ops::Deref; -use std::rc::{Rc, Weak}; +use std::io::{self, ErrorKind}; use rustc_abi::Size; use crate::helpers::check_min_arg_count; +use crate::shims::files::FileDescription; use crate::shims::unix::linux::epoll::EpollReadyEvents; use crate::shims::unix::*; use crate::*; @@ -21,40 +18,8 @@ pub(crate) enum FlockOp { Unlock, } -/// Represents an open file description. -pub trait FileDescription: std::fmt::Debug + Any { - fn name(&self) -> &'static str; - - /// Reads as much as possible into the given buffer `ptr`. - /// `len` indicates how many bytes we should try to read. - /// `dest` is where the return value should be stored: number of bytes read, or `-1` in case of error. - fn read<'tcx>( - &self, - _self_ref: &FileDescriptionRef, - _communicate_allowed: bool, - _ptr: Pointer, - _len: usize, - _dest: &MPlaceTy<'tcx>, - _ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx> { - throw_unsup_format!("cannot read from {}", self.name()); - } - - /// Writes as much as possible from the given buffer `ptr`. - /// `len` indicates how many bytes we should try to write. - /// `dest` is where the return value should be stored: number of bytes written, or `-1` in case of error. - fn write<'tcx>( - &self, - _self_ref: &FileDescriptionRef, - _communicate_allowed: bool, - _ptr: Pointer, - _len: usize, - _dest: &MPlaceTy<'tcx>, - _ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx> { - throw_unsup_format!("cannot write to {}", self.name()); - } - +/// Represents unix-specific file descriptions. +pub trait UnixFileDescription: FileDescription { /// Reads as much as possible into the given buffer `ptr` from a given offset. /// `len` indicates how many bytes we should try to read. /// `dest` is where the return value should be stored: number of bytes read, or `-1` in case of error. @@ -86,24 +51,6 @@ pub trait FileDescription: std::fmt::Debug + Any { throw_unsup_format!("cannot pwrite to {}", self.name()); } - /// Seeks to the given offset (which can be relative to the beginning, end, or current position). - /// Returns the new position from the start of the stream. - fn seek<'tcx>( - &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, - _ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx, io::Result<()>> { - throw_unsup_format!("cannot close {}", self.name()); - } - fn flock<'tcx>( &self, _communicate_allowed: bool, @@ -112,311 +59,12 @@ pub trait FileDescription: std::fmt::Debug + Any { throw_unsup_format!("cannot flock {}", self.name()); } - fn is_tty(&self, _communicate_allowed: bool) -> bool { - // Most FDs are not tty's and the consequence of a wrong `false` are minor, - // so we use a default impl here. - false - } - /// Check the readiness of file description. fn get_epoll_ready_events<'tcx>(&self) -> InterpResult<'tcx, EpollReadyEvents> { throw_unsup_format!("{}: epoll does not support this file description", self.name()); } } -impl dyn FileDescription { - #[inline(always)] - pub fn downcast(&self) -> Option<&T> { - (self as &dyn Any).downcast_ref() - } -} - -impl FileDescription for io::Stdin { - fn name(&self) -> &'static str { - "stdin" - } - - fn read<'tcx>( - &self, - _self_ref: &FileDescriptionRef, - communicate_allowed: bool, - ptr: Pointer, - len: usize, - dest: &MPlaceTy<'tcx>, - ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx> { - let mut bytes = vec![0; len]; - 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")?; - } - let result = Read::read(&mut { self }, &mut bytes); - match result { - Ok(read_size) => ecx.return_read_success(ptr, &bytes, read_size, dest), - Err(e) => ecx.set_last_error_and_return(e, dest), - } - } - - fn is_tty(&self, communicate_allowed: bool) -> bool { - communicate_allowed && self.is_terminal() - } -} - -impl FileDescription for io::Stdout { - fn name(&self) -> &'static str { - "stdout" - } - - fn write<'tcx>( - &self, - _self_ref: &FileDescriptionRef, - _communicate_allowed: bool, - ptr: Pointer, - len: usize, - dest: &MPlaceTy<'tcx>, - ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx> { - let bytes = ecx.read_bytes_ptr_strip_provenance(ptr, Size::from_bytes(len))?; - // 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(); - match result { - Ok(write_size) => ecx.return_write_success(write_size, dest), - Err(e) => ecx.set_last_error_and_return(e, dest), - } - } - - fn is_tty(&self, communicate_allowed: bool) -> bool { - communicate_allowed && self.is_terminal() - } -} - -impl FileDescription for io::Stderr { - fn name(&self) -> &'static str { - "stderr" - } - - fn write<'tcx>( - &self, - _self_ref: &FileDescriptionRef, - _communicate_allowed: bool, - ptr: Pointer, - len: usize, - dest: &MPlaceTy<'tcx>, - ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx> { - let bytes = ecx.read_bytes_ptr_strip_provenance(ptr, Size::from_bytes(len))?; - // We allow writing to stderr even with isolation enabled. - // No need to flush, stderr is not buffered. - let result = Write::write(&mut { self }, bytes); - match result { - Ok(write_size) => ecx.return_write_success(write_size, dest), - Err(e) => ecx.set_last_error_and_return(e, dest), - } - } - - fn is_tty(&self, communicate_allowed: bool) -> bool { - communicate_allowed && self.is_terminal() - } -} - -/// Like /dev/null -#[derive(Debug)] -pub struct NullOutput; - -impl FileDescription for NullOutput { - fn name(&self) -> &'static str { - "stderr and stdout" - } - - fn write<'tcx>( - &self, - _self_ref: &FileDescriptionRef, - _communicate_allowed: bool, - _ptr: Pointer, - len: usize, - dest: &MPlaceTy<'tcx>, - ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx> { - // We just don't write anything, but report to the user that we did. - ecx.return_write_success(len, dest) - } -} - -/// Structure contains both the file description and its unique identifier. -#[derive(Clone, Debug)] -pub struct FileDescWithId { - id: FdId, - file_description: Box, -} - -#[derive(Clone, Debug)] -pub struct FileDescriptionRef(Rc>); - -impl Deref for FileDescriptionRef { - type Target = dyn FileDescription; - - fn deref(&self) -> &Self::Target { - &*self.0.file_description - } -} - -impl FileDescriptionRef { - fn new(fd: impl FileDescription, id: FdId) -> Self { - FileDescriptionRef(Rc::new(FileDescWithId { id, file_description: Box::new(fd) })) - } - - pub fn close<'tcx>( - self, - communicate_allowed: bool, - ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx, io::Result<()>> { - // Destroy this `Rc` using `into_inner` so we can call `close` instead of - // implicitly running the destructor of the file description. - let id = self.get_id(); - match Rc::into_inner(self.0) { - Some(fd) => { - // Remove entry from the global epoll_event_interest table. - ecx.machine.epoll_interests.remove(id); - - fd.file_description.close(communicate_allowed, ecx) - } - None => interp_ok(Ok(())), - } - } - - pub fn downgrade(&self) -> WeakFileDescriptionRef { - WeakFileDescriptionRef { weak_ref: Rc::downgrade(&self.0) } - } - - pub fn get_id(&self) -> FdId { - self.0.id - } -} - -/// Holds a weak reference to the actual file description. -#[derive(Clone, Debug, Default)] -pub struct WeakFileDescriptionRef { - weak_ref: Weak>, -} - -impl WeakFileDescriptionRef { - pub fn upgrade(&self) -> Option { - if let Some(file_desc_with_id) = self.weak_ref.upgrade() { - return Some(FileDescriptionRef(file_desc_with_id)); - } - None - } -} - -impl VisitProvenance for WeakFileDescriptionRef { - fn visit_provenance(&self, _visit: &mut VisitWith<'_>) { - // A weak reference can never be the only reference to some pointer or place. - // Since the actual file description is tracked by strong ref somewhere, - // it is ok to make this a NOP operation. - } -} - -/// A unique id for file descriptions. While we could use the address, considering that -/// is definitely unique, the address would expose interpreter internal state when used -/// for sorting things. So instead we generate a unique id per file description that stays -/// the same even if a file descriptor is duplicated and gets a new integer file descriptor. -#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd)] -pub struct FdId(usize); - -/// The file descriptor table -#[derive(Debug)] -pub struct FdTable { - pub fds: BTreeMap, - /// Unique identifier for file description, used to differentiate between various file description. - next_file_description_id: FdId, -} - -impl VisitProvenance for FdTable { - fn visit_provenance(&self, _visit: &mut VisitWith<'_>) { - // All our FileDescription instances do not have any tags. - } -} - -impl FdTable { - fn new() -> Self { - FdTable { fds: BTreeMap::new(), next_file_description_id: FdId(0) } - } - pub(crate) fn init(mute_stdout_stderr: bool) -> FdTable { - let mut fds = FdTable::new(); - fds.insert_new(io::stdin()); - if mute_stdout_stderr { - assert_eq!(fds.insert_new(NullOutput), 1); - assert_eq!(fds.insert_new(NullOutput), 2); - } else { - assert_eq!(fds.insert_new(io::stdout()), 1); - assert_eq!(fds.insert_new(io::stderr()), 2); - } - fds - } - - pub fn new_ref(&mut self, fd: impl FileDescription) -> FileDescriptionRef { - let file_handle = FileDescriptionRef::new(fd, self.next_file_description_id); - self.next_file_description_id = FdId(self.next_file_description_id.0.strict_add(1)); - file_handle - } - - /// Insert a new file description to the FdTable. - pub fn insert_new(&mut self, fd: impl FileDescription) -> i32 { - let fd_ref = self.new_ref(fd); - self.insert(fd_ref) - } - - pub fn insert(&mut self, fd_ref: FileDescriptionRef) -> i32 { - self.insert_with_min_num(fd_ref, 0) - } - - /// Insert a file description, giving it a file descriptor that is at least `min_fd_num`. - fn insert_with_min_num(&mut self, file_handle: FileDescriptionRef, min_fd_num: 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_num..).zip(min_fd_num..).find_map(|((fd_num, _fd), counter)| { - if *fd_num != 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_num = 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_num, _)| fd_num.strict_add(1)).unwrap_or(min_fd_num) - }); - - self.fds.try_insert(new_fd_num, file_handle).unwrap(); - new_fd_num - } - - pub fn get(&self, fd_num: i32) -> Option { - let fd = self.fds.get(&fd_num)?; - Some(fd.clone()) - } - - pub fn remove(&mut self, fd_num: i32) -> Option { - self.fds.remove(&fd_num) - } - - pub fn is_fd_num(&self, fd_num: i32) -> bool { - self.fds.contains_key(&fd_num) - } -} - impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { fn dup(&mut self, old_fd_num: i32) -> InterpResult<'tcx, Scalar> { @@ -472,7 +120,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { throw_unsup_format!("unsupported flags {:#x}", op); }; - let result = fd.flock(this.machine.communicate(), parsed_op)?; + let result = fd.as_unix()?.flock(this.machine.communicate(), parsed_op)?; drop(fd); // return `0` if flock is successful let result = result.map(|()| 0i32); @@ -602,7 +250,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let Ok(offset) = u64::try_from(offset) else { return this.set_last_error_and_return(LibcError("EINVAL"), dest); }; - fd.pread(communicate, offset, buf, count, dest, this)? + fd.as_unix()?.pread(communicate, offset, buf, count, dest, this)? } }; interp_ok(()) @@ -642,46 +290,9 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let Ok(offset) = u64::try_from(offset) else { return this.set_last_error_and_return(LibcError("EINVAL"), dest); }; - fd.pwrite(communicate, buf, count, offset, dest, this)? + fd.as_unix()?.pwrite(communicate, buf, count, offset, dest, this)? } }; interp_ok(()) } - - /// Helper to implement `FileDescription::read`: - /// This is only used when `read` is successful. - /// `actual_read_size` should be the return value of some underlying `read` call that used - /// `bytes` as its output buffer. - /// The length of `bytes` must not exceed either the host's or the target's `isize`. - /// `bytes` is written to `buf` and the size is written to `dest`. - fn return_read_success( - &mut self, - buf: Pointer, - bytes: &[u8], - actual_read_size: usize, - dest: &MPlaceTy<'tcx>, - ) -> InterpResult<'tcx> { - let this = self.eval_context_mut(); - // If reading to `bytes` did not fail, we write those bytes to the buffer. - // Crucially, if fewer than `bytes.len()` bytes were read, only write - // that much into the output buffer! - this.write_bytes_ptr(buf, bytes[..actual_read_size].iter().copied())?; - - // The actual read size is always less than what got originally requested so this cannot fail. - this.write_int(u64::try_from(actual_read_size).unwrap(), dest)?; - interp_ok(()) - } - - /// Helper to implement `FileDescription::write`: - /// This function is only used when `write` is successful, and writes `actual_write_size` to `dest` - fn return_write_success( - &mut self, - actual_write_size: usize, - dest: &MPlaceTy<'tcx>, - ) -> InterpResult<'tcx> { - let this = self.eval_context_mut(); - // The actual write size is always less than what got originally requested so this cannot fail. - this.write_int(u64::try_from(actual_write_size).unwrap(), dest)?; - interp_ok(()) - } } diff --git a/src/shims/unix/fs.rs b/src/shims/unix/fs.rs index 091def7ac6..ace79d61cb 100644 --- a/src/shims/unix/fs.rs +++ b/src/shims/unix/fs.rs @@ -2,7 +2,8 @@ use std::borrow::Cow; use std::fs::{ - DirBuilder, File, FileType, OpenOptions, ReadDir, read_dir, remove_dir, remove_file, rename, + DirBuilder, File, FileType, Metadata, OpenOptions, ReadDir, read_dir, remove_dir, remove_file, + rename, }; use std::io::{self, ErrorKind, IsTerminal, Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; @@ -11,12 +12,11 @@ use std::time::SystemTime; use rustc_abi::Size; use rustc_data_structures::fx::FxHashMap; -use self::fd::FlockOp; use self::shims::time::system_time_to_duration; use crate::helpers::check_min_arg_count; +use crate::shims::files::{EvalContextExt as _, FileDescription, FileDescriptionRef}; use crate::shims::os_str::bytes_to_os_str; -use crate::shims::unix::fd::FileDescriptionRef; -use crate::shims::unix::*; +use crate::shims::unix::fd::{FlockOp, UnixFileDescription}; use crate::*; #[derive(Debug)] @@ -66,6 +66,55 @@ impl FileDescription for FileHandle { } } + fn seek<'tcx>( + &self, + communicate_allowed: bool, + offset: SeekFrom, + ) -> InterpResult<'tcx, io::Result> { + assert!(communicate_allowed, "isolation should have prevented even opening a file"); + interp_ok((&mut &self.file).seek(offset)) + } + + fn close<'tcx>( + self: Box, + communicate_allowed: bool, + _ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx, io::Result<()>> { + 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(); + // Now we actually close the file and return the result. + drop(*self); + interp_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); + interp_ok(Ok(())) + } + } + + fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { + interp_ok(self.file.metadata()) + } + + fn is_tty(&self, communicate_allowed: bool) -> bool { + communicate_allowed && self.file.is_terminal() + } + + fn as_unix<'tcx>(&self) -> InterpResult<'tcx, &dyn UnixFileDescription> { + interp_ok(self) + } +} + +impl UnixFileDescription for FileHandle { fn pread<'tcx>( &self, communicate_allowed: bool, @@ -128,41 +177,6 @@ impl FileDescription for FileHandle { } } - fn seek<'tcx>( - &self, - communicate_allowed: bool, - offset: SeekFrom, - ) -> InterpResult<'tcx, io::Result> { - assert!(communicate_allowed, "isolation should have prevented even opening a file"); - interp_ok((&mut &self.file).seek(offset)) - } - - fn close<'tcx>( - self: Box, - communicate_allowed: bool, - _ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx, io::Result<()>> { - 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(); - // Now we actually close the file and return the result. - drop(*self); - interp_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); - interp_ok(Ok(())) - } - } - fn flock<'tcx>( &self, communicate_allowed: bool, @@ -257,10 +271,6 @@ impl FileDescription for FileHandle { compile_error!("flock is supported only on UNIX and Windows hosts"); } } - - fn is_tty(&self, communicate_allowed: bool) -> bool { - communicate_allowed && self.file.is_terminal() - } } impl<'tcx> EvalContextExtPrivate<'tcx> for crate::MiriInterpCx<'tcx> {} @@ -1675,16 +1685,7 @@ impl FileMetadata { return interp_ok(Err(LibcError("EBADF"))); }; - let file = &fd - .downcast::() - .ok_or_else(|| { - err_unsup_format!( - "obtaining metadata is only supported on file-backed file descriptors" - ) - })? - .file; - - let metadata = file.metadata(); + let metadata = fd.metadata()?; drop(fd); FileMetadata::from_meta(ecx, metadata) } diff --git a/src/shims/unix/linux/epoll.rs b/src/shims/unix/linux/epoll.rs index de108665e9..12456dd120 100644 --- a/src/shims/unix/linux/epoll.rs +++ b/src/shims/unix/linux/epoll.rs @@ -5,8 +5,7 @@ use std::rc::{Rc, Weak}; use std::time::Duration; use crate::concurrency::VClock; -use crate::shims::unix::fd::{FdId, FileDescriptionRef, WeakFileDescriptionRef}; -use crate::shims::unix::*; +use crate::shims::files::{FdId, FileDescription, FileDescriptionRef, WeakFileDescriptionRef}; use crate::*; /// An `Epoll` file descriptor connects file handles and epoll events @@ -595,7 +594,7 @@ fn check_and_update_one_event_interest<'tcx>( ecx: &MiriInterpCx<'tcx>, ) -> InterpResult<'tcx, bool> { // Get the bitmask of ready events for a file description. - let ready_events_bitmask = fd_ref.get_epoll_ready_events()?.get_event_bitmask(ecx); + let ready_events_bitmask = fd_ref.as_unix()?.get_epoll_ready_events()?.get_event_bitmask(ecx); let epoll_event_interest = interest.borrow(); // This checks if any of the events specified in epoll_event_interest.events // match those in ready_events. diff --git a/src/shims/unix/linux/eventfd.rs b/src/shims/unix/linux/eventfd.rs index 63b7d37b13..0f6a383fbb 100644 --- a/src/shims/unix/linux/eventfd.rs +++ b/src/shims/unix/linux/eventfd.rs @@ -4,9 +4,9 @@ use std::io; use std::io::ErrorKind; use crate::concurrency::VClock; -use crate::shims::unix::fd::{FileDescriptionRef, WeakFileDescriptionRef}; +use crate::shims::files::{FileDescription, FileDescriptionRef, WeakFileDescriptionRef}; +use crate::shims::unix::fd::UnixFileDescription; use crate::shims::unix::linux::epoll::{EpollReadyEvents, EvalContextExt as _}; -use crate::shims::unix::*; use crate::*; /// Maximum value that the eventfd counter can hold. @@ -37,17 +37,6 @@ impl FileDescription for Event { "event" } - fn get_epoll_ready_events<'tcx>(&self) -> InterpResult<'tcx, EpollReadyEvents> { - // We only check the status of EPOLLIN and EPOLLOUT flags for eventfd. If other event flags - // need to be supported in the future, the check should be added here. - - interp_ok(EpollReadyEvents { - epollin: self.counter.get() != 0, - epollout: self.counter.get() != MAX_COUNTER, - ..EpollReadyEvents::new() - }) - } - fn close<'tcx>( self: Box, _communicate_allowed: bool, @@ -121,6 +110,23 @@ impl FileDescription for Event { let weak_eventfd = self_ref.downgrade(); eventfd_write(num, buf_place, dest, weak_eventfd, ecx) } + + fn as_unix<'tcx>(&self) -> InterpResult<'tcx, &dyn UnixFileDescription> { + interp_ok(self) + } +} + +impl UnixFileDescription for Event { + fn get_epoll_ready_events<'tcx>(&self) -> InterpResult<'tcx, EpollReadyEvents> { + // We only check the status of EPOLLIN and EPOLLOUT flags for eventfd. If other event flags + // need to be supported in the future, the check should be added here. + + interp_ok(EpollReadyEvents { + epollin: self.counter.get() != 0, + epollout: self.counter.get() != MAX_COUNTER, + ..EpollReadyEvents::new() + }) + } } impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} diff --git a/src/shims/unix/mod.rs b/src/shims/unix/mod.rs index c8c25c636e..0ece24da6c 100644 --- a/src/shims/unix/mod.rs +++ b/src/shims/unix/mod.rs @@ -16,7 +16,7 @@ mod solarish; // All the Unix-specific extension traits pub use self::env::{EvalContextExt as _, UnixEnvVars}; -pub use self::fd::{EvalContextExt as _, FdTable, FileDescription}; +pub use self::fd::{EvalContextExt as _, UnixFileDescription}; pub use self::fs::{DirTable, EvalContextExt as _}; pub use self::linux::epoll::EpollInterestTable; pub use self::mem::EvalContextExt as _; diff --git a/src/shims/unix/unnamed_socket.rs b/src/shims/unix/unnamed_socket.rs index 8ccce7c198..d7348e9ea9 100644 --- a/src/shims/unix/unnamed_socket.rs +++ b/src/shims/unix/unnamed_socket.rs @@ -10,9 +10,11 @@ use std::io::{ErrorKind, Read}; use rustc_abi::Size; use crate::concurrency::VClock; -use crate::shims::unix::fd::{FileDescriptionRef, WeakFileDescriptionRef}; +use crate::shims::files::{ + EvalContextExt as _, FileDescription, FileDescriptionRef, WeakFileDescriptionRef, +}; +use crate::shims::unix::fd::UnixFileDescription; use crate::shims::unix::linux::epoll::{EpollReadyEvents, EvalContextExt as _}; -use crate::shims::unix::*; use crate::*; /// The maximum capacity of the socketpair buffer in bytes. @@ -60,52 +62,6 @@ impl FileDescription for AnonSocket { "socketpair" } - fn get_epoll_ready_events<'tcx>(&self) -> InterpResult<'tcx, EpollReadyEvents> { - // We only check the status of EPOLLIN, EPOLLOUT, EPOLLHUP and EPOLLRDHUP flags. - // If other event flags need to be supported in the future, the check should be added here. - - let mut epoll_ready_events = EpollReadyEvents::new(); - - // Check if it is readable. - if let Some(readbuf) = &self.readbuf { - if !readbuf.borrow().buf.is_empty() { - epoll_ready_events.epollin = true; - } - } else { - // Without a read buffer, reading never blocks, so we are always ready. - epoll_ready_events.epollin = true; - } - - // Check if is writable. - if let Some(peer_fd) = self.peer_fd().upgrade() { - if let Some(writebuf) = &peer_fd.downcast::().unwrap().readbuf { - let data_size = writebuf.borrow().buf.len(); - let available_space = MAX_SOCKETPAIR_BUFFER_CAPACITY.strict_sub(data_size); - if available_space != 0 { - epoll_ready_events.epollout = true; - } - } else { - // Without a write buffer, writing never blocks. - epoll_ready_events.epollout = true; - } - } else { - // Peer FD has been closed. This always sets both the RDHUP and HUP flags - // as we do not support `shutdown` that could be used to partially close the stream. - epoll_ready_events.epollrdhup = true; - epoll_ready_events.epollhup = true; - // Since the peer is closed, even if no data is available reads will return EOF and - // writes will return EPIPE. In other words, they won't block, so we mark this as ready - // for read and write. - epoll_ready_events.epollin = true; - epoll_ready_events.epollout = true; - // If there is data lost in peer_fd, set EPOLLERR. - if self.peer_lost_data.get() { - epoll_ready_events.epollerr = true; - } - } - interp_ok(epoll_ready_events) - } - fn close<'tcx>( self: Box, _communicate_allowed: bool, @@ -251,6 +207,58 @@ impl FileDescription for AnonSocket { ecx.return_write_success(actual_write_size, dest) } + + fn as_unix<'tcx>(&self) -> InterpResult<'tcx, &dyn UnixFileDescription> { + interp_ok(self) + } +} + +impl UnixFileDescription for AnonSocket { + fn get_epoll_ready_events<'tcx>(&self) -> InterpResult<'tcx, EpollReadyEvents> { + // We only check the status of EPOLLIN, EPOLLOUT, EPOLLHUP and EPOLLRDHUP flags. + // If other event flags need to be supported in the future, the check should be added here. + + let mut epoll_ready_events = EpollReadyEvents::new(); + + // Check if it is readable. + if let Some(readbuf) = &self.readbuf { + if !readbuf.borrow().buf.is_empty() { + epoll_ready_events.epollin = true; + } + } else { + // Without a read buffer, reading never blocks, so we are always ready. + epoll_ready_events.epollin = true; + } + + // Check if is writable. + if let Some(peer_fd) = self.peer_fd().upgrade() { + if let Some(writebuf) = &peer_fd.downcast::().unwrap().readbuf { + let data_size = writebuf.borrow().buf.len(); + let available_space = MAX_SOCKETPAIR_BUFFER_CAPACITY.strict_sub(data_size); + if available_space != 0 { + epoll_ready_events.epollout = true; + } + } else { + // Without a write buffer, writing never blocks. + epoll_ready_events.epollout = true; + } + } else { + // Peer FD has been closed. This always sets both the RDHUP and HUP flags + // as we do not support `shutdown` that could be used to partially close the stream. + epoll_ready_events.epollrdhup = true; + epoll_ready_events.epollhup = true; + // Since the peer is closed, even if no data is available reads will return EOF and + // writes will return EPIPE. In other words, they won't block, so we mark this as ready + // for read and write. + epoll_ready_events.epollin = true; + epoll_ready_events.epollout = true; + // If there is data lost in peer_fd, set EPOLLERR. + if self.peer_lost_data.get() { + epoll_ready_events.epollerr = true; + } + } + interp_ok(epoll_ready_events) + } } impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} diff --git a/src/shims/windows/foreign_items.rs b/src/shims/windows/foreign_items.rs index c145cf3ceb..b7602556a8 100644 --- a/src/shims/windows/foreign_items.rs +++ b/src/shims/windows/foreign_items.rs @@ -7,13 +7,12 @@ use rustc_span::Symbol; use self::shims::windows::handle::{Handle, PseudoHandle}; use crate::shims::os_str::bytes_to_os_str; -use crate::shims::windows::handle::HandleError; use crate::shims::windows::*; use crate::*; // The NTSTATUS STATUS_INVALID_HANDLE (0xC0000008) encoded as a HRESULT by setting the N bit. // (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/0642cb2f-2075-4469-918c-4441e69c548a) -const STATUS_INVALID_HANDLE: u32 = 0xD0000008; +// const STATUS_INVALID_HANDLE: u32 = 0xD0000008; pub fn is_dyn_sym(name: &str) -> bool { // std does dynamic detection for these symbols @@ -141,6 +140,14 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let result = this.GetUserProfileDirectoryW(token, buf, size)?; this.write_scalar(result, dest)?; } + "GetCurrentProcess" => { + let [] = + this.check_shim(abi, ExternAbi::System { unwind: false }, link_name, args)?; + this.write_scalar( + Handle::Pseudo(PseudoHandle::CurrentProcess).to_scalar(this), + dest, + )?; + } "GetCurrentProcessId" => { let [] = this.check_shim(abi, ExternAbi::System { unwind: false }, link_name, args)?; @@ -150,71 +157,54 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // File related shims "NtWriteFile" => { - if !this.frame_in_std() { - throw_unsup_format!( - "`NtWriteFile` support is crude and just enough for stdout to work" - ); - } - let [ handle, - _event, - _apc_routine, - _apc_context, + event, + apc_routine, + apc_context, io_status_block, buf, n, byte_offset, - _key, + key, ] = this.check_shim(abi, ExternAbi::System { unwind: false }, link_name, args)?; - let handle = this.read_target_isize(handle)?; - let buf = this.read_pointer(buf)?; - let n = this.read_scalar(n)?.to_u32()?; - let byte_offset = this.read_target_usize(byte_offset)?; // is actually a pointer - let io_status_block = this - .deref_pointer_as(io_status_block, this.windows_ty_layout("IO_STATUS_BLOCK"))?; - - if byte_offset != 0 { - throw_unsup_format!( - "`NtWriteFile` `ByteOffset` parameter is non-null, which is unsupported" - ); - } - - let written = if handle == -11 || handle == -12 { - // stdout/stderr - use io::Write; - - let buf_cont = - this.read_bytes_ptr_strip_provenance(buf, Size::from_bytes(u64::from(n)))?; - let res = if this.machine.mute_stdout_stderr { - Ok(buf_cont.len()) - } else if handle == -11 { - io::stdout().write(buf_cont) - } else { - io::stderr().write(buf_cont) - }; - // We write at most `n` bytes, which is a `u32`, so we cannot have written more than that. - res.ok().map(|n| u32::try_from(n).unwrap()) - } else { - throw_unsup_format!( - "on Windows, writing to anything except stdout/stderr is not supported" - ) - }; - // We have to put the result into io_status_block. - if let Some(n) = written { - let io_status_information = - this.project_field_named(&io_status_block, "Information")?; - this.write_scalar( - Scalar::from_target_usize(n.into(), this), - &io_status_information, - )?; - } - // Return whether this was a success. >= 0 is success. - // For the error code we arbitrarily pick 0xC0000185, STATUS_IO_DEVICE_ERROR. - this.write_scalar( - Scalar::from_u32(if written.is_some() { 0 } else { 0xC0000185u32 }), - dest, + let res = this.NtWriteFile( + handle, + event, + apc_routine, + apc_context, + io_status_block, + buf, + n, + byte_offset, + key, + )?; + this.write_scalar(res, dest)?; + } + "NtReadFile" => { + let [ + handle, + event, + apc_routine, + apc_context, + io_status_block, + buf, + n, + byte_offset, + key, + ] = this.check_shim(abi, ExternAbi::System { unwind: false }, link_name, args)?; + let res = this.NtReadFile( + handle, + event, + apc_routine, + apc_context, + io_status_block, + buf, + n, + byte_offset, + key, )?; + this.write_scalar(res, dest)?; } "GetFullPathNameW" => { let [filename, size, buffer, filepart] = @@ -246,6 +236,46 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { }; this.write_scalar(result, dest)?; } + "CreateFileW" => { + let [ + file_name, + desired_access, + share_mode, + security_attributes, + creation_disposition, + flags_and_attributes, + template_file, + ] = this.check_shim(abi, ExternAbi::System { unwind: false }, link_name, args)?; + let handle = this.CreateFileW( + file_name, + desired_access, + share_mode, + security_attributes, + creation_disposition, + flags_and_attributes, + template_file, + )?; + this.write_scalar(handle.to_scalar(this), dest)?; + } + "GetFileInformationByHandle" => { + let [handle, info] = + this.check_shim(abi, ExternAbi::System { unwind: false }, link_name, args)?; + let res = this.GetFileInformationByHandle(handle, info)?; + this.write_scalar(res, dest)?; + } + "DeleteFileW" => { + let [file_name] = + this.check_shim(abi, ExternAbi::System { unwind: false }, link_name, args)?; + let res = this.DeleteFileW(file_name)?; + this.write_scalar(res, dest)?; + } + "SetFilePointerEx" => { + let [file, distance_to_move, new_file_pointer, move_method] = + this.check_shim(abi, ExternAbi::System { unwind: false }, link_name, args)?; + let res = + this.SetFilePointerEx(file, distance_to_move, new_file_pointer, move_method)?; + this.write_scalar(res, dest)?; + } // Allocation "HeapAlloc" => { @@ -511,54 +541,40 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let [handle, name] = this.check_shim(abi, ExternAbi::System { unwind: false }, link_name, args)?; - let handle = this.read_scalar(handle)?; + let handle = this.read_handle(handle)?; let name = this.read_wide_str(this.read_pointer(name)?)?; - let thread = match Handle::try_from_scalar(handle, this)? { - Ok(Handle::Thread(thread)) => Ok(thread), - Ok(Handle::Pseudo(PseudoHandle::CurrentThread)) => Ok(this.active_thread()), - Ok(_) | Err(HandleError::InvalidHandle) => - this.invalid_handle("SetThreadDescription")?, - Err(HandleError::ThreadNotFound(e)) => Err(e), - }; - let res = match thread { - Ok(thread) => { - // FIXME: use non-lossy conversion - this.set_thread_name(thread, String::from_utf16_lossy(&name).into_bytes()); - Scalar::from_u32(0) - } - Err(_) => Scalar::from_u32(STATUS_INVALID_HANDLE), + let thread = match handle { + Handle::Thread(thread) => thread, + Handle::Pseudo(PseudoHandle::CurrentThread) => this.active_thread(), + _ => this.invalid_handle("SetThreadDescription")?, }; - - this.write_scalar(res, dest)?; + // FIXME: use non-lossy conversion + this.set_thread_name(thread, String::from_utf16_lossy(&name).into_bytes()); + this.write_scalar(Scalar::from_u32(0), dest)?; } "GetThreadDescription" => { let [handle, name_ptr] = this.check_shim(abi, ExternAbi::System { unwind: false }, link_name, args)?; - let handle = this.read_scalar(handle)?; + let handle = this.read_handle(handle)?; let name_ptr = this.deref_pointer(name_ptr)?; // the pointer where we should store the ptr to the name - let thread = match Handle::try_from_scalar(handle, this)? { - Ok(Handle::Thread(thread)) => Ok(thread), - Ok(Handle::Pseudo(PseudoHandle::CurrentThread)) => Ok(this.active_thread()), - Ok(_) | Err(HandleError::InvalidHandle) => - this.invalid_handle("GetThreadDescription")?, - Err(HandleError::ThreadNotFound(e)) => Err(e), - }; - let (name, res) = match thread { - Ok(thread) => { - // Looks like the default thread name is empty. - let name = this.get_thread_name(thread).unwrap_or(b"").to_owned(); - let name = this.alloc_os_str_as_wide_str( - bytes_to_os_str(&name)?, - MiriMemoryKind::WinLocal.into(), - )?; - (Scalar::from_maybe_pointer(name, this), Scalar::from_u32(0)) - } - Err(_) => (Scalar::null_ptr(this), Scalar::from_u32(STATUS_INVALID_HANDLE)), + let thread = match handle { + Handle::Thread(thread) => thread, + Handle::Pseudo(PseudoHandle::CurrentThread) => this.active_thread(), + _ => this.invalid_handle("GetThreadDescription")?, }; + // Looks like the default thread name is empty. + let name = this.get_thread_name(thread).unwrap_or(b"").to_owned(); + let name = this.alloc_os_str_as_wide_str( + bytes_to_os_str(&name)?, + MiriMemoryKind::WinLocal.into(), + )?; + let name = Scalar::from_maybe_pointer(name, this); + let res = Scalar::from_u32(0); + this.write_scalar(name, &name_ptr)?; this.write_scalar(res, dest)?; } @@ -638,12 +654,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { "GetStdHandle" => { let [which] = this.check_shim(abi, ExternAbi::System { unwind: false }, link_name, args)?; - let which = this.read_scalar(which)?.to_i32()?; - // We just make this the identity function, so we know later in `NtWriteFile` which - // one it is. This is very fake, but libtest needs it so we cannot make it a - // std-only shim. - // FIXME: this should return real HANDLEs when io support is added - this.write_scalar(Scalar::from_target_isize(which.into(), this), dest)?; + let res = this.GetStdHandle(which)?; + this.write_scalar(res, dest)?; } "CloseHandle" => { let [handle] = @@ -658,11 +670,11 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { this.check_shim(abi, ExternAbi::System { unwind: false }, link_name, args)?; this.check_no_isolation("`GetModuleFileNameW`")?; - let handle = this.read_target_usize(handle)?; + let handle = this.read_handle(handle)?; let filename = this.read_pointer(filename)?; let size = this.read_scalar(size)?.to_u32()?; - if handle != 0 { + if handle != Handle::Null { throw_unsup_format!("`GetModuleFileNameW` only supports the NULL handle"); } diff --git a/src/shims/windows/fs.rs b/src/shims/windows/fs.rs new file mode 100644 index 0000000000..f88dd91706 --- /dev/null +++ b/src/shims/windows/fs.rs @@ -0,0 +1,647 @@ +use std::fs::{File, Metadata, OpenOptions}; +use std::io; +use std::io::{IsTerminal, Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; +use std::time::SystemTime; + +use rustc_abi::Size; + +use crate::shims::files::{EvalContextExt as _, FileDescription, FileDescriptionRef}; +use crate::shims::time::system_time_to_duration; +use crate::shims::windows::handle::{EvalContextExt as _, Handle, PseudoHandle}; +use crate::*; + +#[derive(Debug)] +pub struct FileHandle { + pub(crate) file: File, + pub(crate) writable: bool, +} + +impl FileDescription for FileHandle { + fn name(&self) -> &'static str { + "file" + } + + fn read<'tcx>( + &self, + _self_ref: &FileDescriptionRef, + communicate_allowed: bool, + ptr: Pointer, + len: usize, + dest: &MPlaceTy<'tcx>, + ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx> { + assert!(communicate_allowed, "isolation should have prevented even opening a file"); + let mut bytes = vec![0; len]; + let result = (&mut &self.file).read(&mut bytes); + match result { + Ok(read_size) => ecx.return_read_success(ptr, &bytes, read_size, dest), + Err(e) => ecx.set_last_error_and_return(e, dest), + } + } + + fn write<'tcx>( + &self, + _self_ref: &FileDescriptionRef, + communicate_allowed: bool, + ptr: Pointer, + len: usize, + dest: &MPlaceTy<'tcx>, + ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx> { + assert!(communicate_allowed, "isolation should have prevented even opening a file"); + let bytes = ecx.read_bytes_ptr_strip_provenance(ptr, Size::from_bytes(len))?; + let result = (&mut &self.file).write(bytes); + match result { + Ok(write_size) => ecx.return_write_success(write_size, dest), + Err(e) => ecx.set_last_error_and_return(e, dest), + } + } + + fn seek<'tcx>( + &self, + communicate_allowed: bool, + offset: SeekFrom, + ) -> InterpResult<'tcx, io::Result> { + assert!(communicate_allowed, "isolation should have prevented even opening a file"); + interp_ok((&mut &self.file).seek(offset)) + } + + fn close<'tcx>( + self: Box, + communicate_allowed: bool, + _ecx: &mut MiriInterpCx<'tcx>, + ) -> InterpResult<'tcx, io::Result<()>> { + 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(); + // Now we actually close the file and return the result. + drop(*self); + interp_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); + interp_ok(Ok(())) + } + } + + fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { + interp_ok(self.file.metadata()) + } + + fn is_tty(&self, communicate_allowed: bool) -> bool { + communicate_allowed && self.file.is_terminal() + } +} + +#[derive(Debug)] +pub struct DirHandle { + pub(crate) path: PathBuf, +} + +impl FileDescription for DirHandle { + fn name(&self) -> &'static str { + "directory" + } + + fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { + interp_ok(self.path.metadata()) + } +} + +#[derive(Debug)] +pub struct MetadataHandle { + pub(crate) path: PathBuf, +} + +impl FileDescription for MetadataHandle { + fn name(&self) -> &'static str { + "metadata-only" + } + + fn metadata<'tcx>(&self) -> InterpResult<'tcx, io::Result> { + interp_ok(self.path.metadata()) + } +} + +impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} +#[allow(non_snake_case)] +pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { + fn CreateFileW( + &mut self, + file_name: &OpTy<'tcx>, // LPCWSTR + desired_access: &OpTy<'tcx>, // DWORD + share_mode: &OpTy<'tcx>, // DWORD + security_attributes: &OpTy<'tcx>, // LPSECURITY_ATTRIBUTES + creation_disposition: &OpTy<'tcx>, // DWORD + flags_and_attributes: &OpTy<'tcx>, // DWORD + template_file: &OpTy<'tcx>, // HANDLE + ) -> InterpResult<'tcx, Handle> { + // ^ Returns HANDLE + let this = self.eval_context_mut(); + this.assert_target_os("windows", "CreateFileW"); + this.check_no_isolation("`CreateFileW`")?; + + let file_name = + String::from_utf16_lossy(&this.read_wide_str(this.read_pointer(file_name)?)?); + let file_name = Path::new(&file_name); + let desired_access = this.read_scalar(desired_access)?.to_u32()?; + let share_mode = this.read_scalar(share_mode)?.to_u32()?; + let security_attributes = this.read_pointer(security_attributes)?; + let creation_disposition = this.read_scalar(creation_disposition)?.to_u32()?; + let flags_and_attributes = this.read_scalar(flags_and_attributes)?.to_u32()?; + let template_file = this.read_target_usize(template_file)?; + + let generic_read = this.eval_windows_u32("c", "GENERIC_READ"); + let generic_write = this.eval_windows_u32("c", "GENERIC_WRITE"); + + if desired_access & !(generic_read | generic_write) != 0 { + throw_unsup_format!("CreateFileW: Unsupported access mode: {desired_access}"); + } + + let file_share_delete = this.eval_windows_u32("c", "FILE_SHARE_DELETE"); + let file_share_read = this.eval_windows_u32("c", "FILE_SHARE_READ"); + let file_share_write = this.eval_windows_u32("c", "FILE_SHARE_WRITE"); + + if share_mode & !(file_share_delete | file_share_read | file_share_write) != 0 + || share_mode == 0 + { + throw_unsup_format!("CreateFileW: Unsupported share mode: {share_mode}"); + } + if !this.ptr_is_null(security_attributes)? { + throw_unsup_format!("CreateFileW: Security attributes are not supported"); + } + + let create_always = this.eval_windows_u32("c", "CREATE_ALWAYS"); + let create_new = this.eval_windows_u32("c", "CREATE_NEW"); + let open_always = this.eval_windows_u32("c", "OPEN_ALWAYS"); + let open_existing = this.eval_windows_u32("c", "OPEN_EXISTING"); + let truncate_existing = this.eval_windows_u32("c", "TRUNCATE_EXISTING"); + + if ![create_always, create_new, open_always, open_existing, truncate_existing] + .contains(&creation_disposition) + { + throw_unsup_format!( + "CreateFileW: Unsupported creation disposition: {creation_disposition}" + ); + } + + let file_attribute_normal = this.eval_windows_u32("c", "FILE_ATTRIBUTE_NORMAL"); + // This must be passed to allow getting directory handles. If not passed, we error on trying + // to open directories below + let file_flag_backup_semantics = this.eval_windows_u32("c", "FILE_FLAG_BACKUP_SEMANTICS"); + let file_flag_open_reparse_point = + this.eval_windows_u32("c", "FILE_FLAG_OPEN_REPARSE_POINT"); + + let flags_and_attributes = match flags_and_attributes { + 0 => file_attribute_normal, + _ => flags_and_attributes, + }; + if !(file_attribute_normal | file_flag_backup_semantics | file_flag_open_reparse_point) + & flags_and_attributes + != 0 + { + throw_unsup_format!( + "CreateFileW: Unsupported flags_and_attributes: {flags_and_attributes}" + ); + } + + if flags_and_attributes & file_flag_open_reparse_point != 0 + && creation_disposition == create_always + { + throw_machine_stop!(TerminationInfo::Abort("Invalid CreateFileW argument combination: FILE_FLAG_OPEN_REPARSE_POINT with CREATE_ALWAYS".to_string())); + } + + if template_file != 0 { + throw_unsup_format!("CreateFileW: Template files are not supported"); + } + + let desired_read = desired_access & generic_read != 0; + let desired_write = desired_access & generic_write != 0; + let exists = file_name.exists(); + let is_dir = file_name.is_dir(); + + if flags_and_attributes == file_attribute_normal && is_dir { + this.set_last_error(IoError::WindowsError("ERROR_ACCESS_DENIED"))?; + return interp_ok(Handle::Invalid); + } + + let mut options = OpenOptions::new(); + if desired_read { + options.read(true); + } + if desired_write { + options.write(true); + } + + if creation_disposition == create_always { + if file_name.exists() { + this.set_last_error(IoError::WindowsError("ERROR_ALREADY_EXISTS"))?; + } + options.create(true); + options.truncate(true); + } else if creation_disposition == create_new { + options.create_new(true); + if !desired_write { + options.append(true); + } + } else if creation_disposition == open_always { + if file_name.exists() { + this.set_last_error(IoError::WindowsError("ERROR_ALREADY_EXISTS"))?; + } + options.create(true); + } else if creation_disposition == open_existing { + // Nothing + } else if creation_disposition == truncate_existing { + options.truncate(true); + } + + let handle = if is_dir && exists { + let fh = &mut this.machine.fds; + let fd = fh.insert_new(DirHandle { path: file_name.into() }); + Ok(Handle::File(fd)) + } else if creation_disposition == open_existing && desired_access == 0 { + // Windows supports handles with no permissions. These allow things such as reading + // metadata, but not file content. + let fh = &mut this.machine.fds; + let fd = fh.insert_new(MetadataHandle { path: file_name.into() }); + Ok(Handle::File(fd)) + } else { + options.open(file_name).map(|file| { + let fh = &mut this.machine.fds; + let fd = fh.insert_new(FileHandle { file, writable: desired_write }); + Handle::File(fd) + }) + }; + + match handle { + Ok(handle) => interp_ok(handle), + Err(e) => { + this.set_last_error(e)?; + interp_ok(Handle::Invalid) + } + } + } + + fn GetFileInformationByHandle( + &mut self, + file: &OpTy<'tcx>, // HANDLE + file_information: &OpTy<'tcx>, // LPBY_HANDLE_FILE_INFORMATION + ) -> InterpResult<'tcx, Scalar> { + // ^ Returns BOOL (i32 on Windows) + let this = self.eval_context_mut(); + this.assert_target_os("windows", "GetFileInformationByHandle"); + this.check_no_isolation("`GetFileInformationByHandle`")?; + + let file = this.read_handle(file)?; + let file_information = this.deref_pointer_as( + file_information, + this.windows_ty_layout("BY_HANDLE_FILE_INFORMATION"), + )?; + + let fd = if let Handle::File(fd) = file { + fd + } else { + this.invalid_handle("GetFileInformationByHandle")? + }; + + let Some(desc) = this.machine.fds.get(fd) else { + this.invalid_handle("GetFileInformationByHandle")? + }; + + let metadata = match desc.metadata()? { + Ok(meta) => meta, + Err(e) => { + this.set_last_error(e)?; + return interp_ok(this.eval_windows("c", "FALSE")); + } + }; + + let size = metadata.len(); + + let file_type = metadata.file_type(); + let attributes = if file_type.is_dir() { + this.eval_windows_u32("c", "FILE_ATTRIBUTE_DIRECTORY") + } else if file_type.is_file() { + this.eval_windows_u32("c", "FILE_ATTRIBUTE_NORMAL") + } else { + this.eval_windows_u32("c", "FILE_ATTRIBUTE_DEVICE") + }; + + let created = extract_windows_epoch(metadata.created())?.unwrap_or((0, 0)); + let accessed = extract_windows_epoch(metadata.accessed())?.unwrap_or((0, 0)); + let written = extract_windows_epoch(metadata.modified())?.unwrap_or((0, 0)); + + this.write_int_fields_named(&[("dwFileAttributes", attributes.into())], &file_information)?; + write_filetime_field(this, &file_information, "ftCreationTime", created)?; + write_filetime_field(this, &file_information, "ftLastAccessTime", accessed)?; + write_filetime_field(this, &file_information, "ftLastWriteTime", written)?; + this.write_int_fields_named( + &[ + ("dwVolumeSerialNumber", 0), + ("nFileSizeHigh", (size >> 32).into()), + ("nFileSizeLow", (size & 0xFFFFFFFF).into()), + ("nNumberOfLinks", 1), + ("nFileIndexHigh", 0), + ("nFileIndexLow", 0), + ], + &file_information, + )?; + + interp_ok(this.eval_windows("c", "TRUE")) + } + + fn DeleteFileW( + &mut self, + file_name: &OpTy<'tcx>, // LPCWSTR + ) -> InterpResult<'tcx, Scalar> { + // ^ Returns BOOL (i32 on Windows) + let this = self.eval_context_mut(); + this.assert_target_os("windows", "DeleteFileW"); + this.check_no_isolation("`DeleteFileW`")?; + let file_name = + String::from_utf16_lossy(&this.read_wide_str(this.read_pointer(file_name)?)?); + let file_name = Path::new(&file_name); + match std::fs::remove_file(file_name) { + Ok(_) => interp_ok(this.eval_windows("c", "TRUE")), + Err(e) => { + this.set_last_error(e)?; + interp_ok(this.eval_windows("c", "FALSE")) + } + } + } + + fn NtWriteFile( + &mut self, + handle: &OpTy<'tcx>, // HANDLE + event: &OpTy<'tcx>, // HANDLE + apc_routine: &OpTy<'tcx>, // PIO_APC_ROUTINE + apc_ctx: &OpTy<'tcx>, // PVOID + io_status_block: &OpTy<'tcx>, // PIO_STATUS_BLOCK + buf: &OpTy<'tcx>, // PVOID + n: &OpTy<'tcx>, // ULONG + byte_offset: &OpTy<'tcx>, // PLARGE_INTEGER + key: &OpTy<'tcx>, // PULONG + ) -> InterpResult<'tcx, Scalar> { + // ^ Returns NTSTATUS (u32 on Windows) + let this = self.eval_context_mut(); + let handle = this.read_handle(handle)?; + let event = this.read_handle(event)?; + let apc_routine = this.read_pointer(apc_routine)?; + let apc_ctx = this.read_pointer(apc_ctx)?; + let buf = this.read_pointer(buf)?; + let n = this.read_scalar(n)?.to_u32()?; + let byte_offset = this.read_target_usize(byte_offset)?; // is actually a pointer + let key = this.read_pointer(key)?; + let io_status_block = + this.deref_pointer_as(io_status_block, this.windows_ty_layout("IO_STATUS_BLOCK"))?; + + if event != Handle::Null { + throw_unsup_format!( + "`NtWriteFile` `Event` parameter is non-null, which is unsupported" + ); + } + + if !this.ptr_is_null(apc_routine)? { + throw_unsup_format!( + "`NtWriteFile` `ApcRoutine` parameter is not null, which is unsupported" + ); + } + + if !this.ptr_is_null(apc_ctx)? { + throw_unsup_format!( + "`NtWriteFile` `ApcContext` parameter is not null, which is unsupported" + ); + } + + if byte_offset != 0 { + throw_unsup_format!( + "`NtWriteFile` `ByteOffset` parameter is non-null, which is unsupported" + ); + } + + if !this.ptr_is_null(key)? { + throw_unsup_format!("`NtWriteFile` `Key` parameter is not null, which is unsupported"); + } + + let written = match handle { + Handle::Pseudo(pseudo @ (PseudoHandle::Stdout | PseudoHandle::Stderr)) => { + // stdout/stderr + let buf_cont = + this.read_bytes_ptr_strip_provenance(buf, Size::from_bytes(u64::from(n)))?; + let res = if this.machine.mute_stdout_stderr { + Ok(buf_cont.len()) + } else if pseudo == PseudoHandle::Stdout { + io::Write::write(&mut io::stdout(), buf_cont) + } else { + io::Write::write(&mut io::stderr(), buf_cont) + }; + // We write at most `n` bytes, which is a `u32`, so we cannot have written more than that. + res.ok().map(|n| u32::try_from(n).unwrap()) + } + Handle::File(fd) => { + let Some(desc) = this.machine.fds.get(fd) else { + this.invalid_handle("NtWriteFile")? + }; + + let errno_layout = this.machine.layouts.u32; + let out_place = this.allocate(errno_layout, MiriMemoryKind::Machine.into())?; + desc.write(&desc, this.machine.communicate(), buf, n as usize, &out_place, this)?; + let written = this.read_scalar(&out_place)?.to_u32()?; + this.deallocate_ptr(out_place.ptr(), None, MiriMemoryKind::Machine.into())?; + Some(written) + } + _ => this.invalid_handle("NtWriteFile")?, + }; + + // We have to put the result into io_status_block. + if let Some(n) = written { + let io_status_information = + this.project_field_named(&io_status_block, "Information")?; + this.write_scalar(Scalar::from_target_usize(n.into(), this), &io_status_information)?; + } + + // Return whether this was a success. >= 0 is success. + // For the error code we arbitrarily pick 0xC0000185, STATUS_IO_DEVICE_ERROR. + interp_ok(Scalar::from_u32(if written.is_some() { 0 } else { 0xC0000185u32 })) + } + + fn NtReadFile( + &mut self, + handle: &OpTy<'tcx>, // HANDLE + event: &OpTy<'tcx>, // HANDLE + apc_routine: &OpTy<'tcx>, // PIO_APC_ROUTINE + apc_ctx: &OpTy<'tcx>, // PVOID + io_status_block: &OpTy<'tcx>, // PIO_STATUS_BLOCK + buf: &OpTy<'tcx>, // PVOID + n: &OpTy<'tcx>, // ULONG + byte_offset: &OpTy<'tcx>, // PLARGE_INTEGER + key: &OpTy<'tcx>, // PULONG + ) -> InterpResult<'tcx, Scalar> { + // ^ Returns NTSTATUS (u32 on Windows) + let this = self.eval_context_mut(); + let handle = this.read_handle(handle)?; + let event = this.read_handle(event)?; + let apc_routine = this.read_pointer(apc_routine)?; + let apc_ctx = this.read_pointer(apc_ctx)?; + let buf = this.read_pointer(buf)?; + let n = this.read_scalar(n)?.to_u32()?; + let byte_offset = this.read_target_usize(byte_offset)?; // is actually a pointer + let key = this.read_pointer(key)?; + let io_status_block = + this.deref_pointer_as(io_status_block, this.windows_ty_layout("IO_STATUS_BLOCK"))?; + + if event != Handle::Null { + throw_unsup_format!( + "`NtWriteFile` `Event` parameter is non-null, which is unsupported" + ); + } + + if !this.ptr_is_null(apc_routine)? { + throw_unsup_format!( + "`NtWriteFile` `ApcRoutine` parameter is not null, which is unsupported" + ); + } + + if !this.ptr_is_null(apc_ctx)? { + throw_unsup_format!( + "`NtWriteFile` `ApcContext` parameter is not null, which is unsupported" + ); + } + + if byte_offset != 0 { + throw_unsup_format!( + "`NtWriteFile` `ByteOffset` parameter is non-null, which is unsupported" + ); + } + + if !this.ptr_is_null(key)? { + throw_unsup_format!("`NtWriteFile` `Key` parameter is not null, which is unsupported"); + } + + let read = match handle { + Handle::Pseudo(PseudoHandle::Stdin) => { + // stdout/stderr + let mut buf_cont = vec![0u8; n as usize]; + let res = io::Read::read(&mut io::stdin(), &mut buf_cont); + this.write_bytes_ptr(buf, buf_cont)?; + // We write at most `n` bytes, which is a `u32`, so we cannot have written more than that. + res.ok().map(|n| u32::try_from(n).unwrap()) + } + Handle::File(fd) => { + let Some(desc) = this.machine.fds.get(fd) else { + this.invalid_handle("NtReadFile")? + }; + + let errno_layout = this.machine.layouts.u32; + let out_place = this.allocate(errno_layout, MiriMemoryKind::Machine.into())?; + desc.read(&desc, this.machine.communicate(), buf, n as usize, &out_place, this)?; + let read = this.read_scalar(&out_place)?.to_u32()?; + this.deallocate_ptr(out_place.ptr(), None, MiriMemoryKind::Machine.into())?; + Some(read) + } + _ => this.invalid_handle("NtReadFile")?, + }; + + // We have to put the result into io_status_block. + if let Some(n) = read { + let io_status_information = + this.project_field_named(&io_status_block, "Information")?; + this.write_scalar(Scalar::from_target_usize(n.into(), this), &io_status_information)?; + } + + // Return whether this was a success. >= 0 is success. + // For the error code we arbitrarily pick 0xC0000185, STATUS_IO_DEVICE_ERROR. + interp_ok(Scalar::from_u32(if read.is_some() { 0 } else { 0xC0000185u32 })) + } + + fn SetFilePointerEx( + &mut self, + file: &OpTy<'tcx>, // HANDLE + dist_to_move: &OpTy<'tcx>, // LARGE_INTEGER + new_fp: &OpTy<'tcx>, // PLARGE_INTEGER + move_method: &OpTy<'tcx>, // DWORD + ) -> InterpResult<'tcx, Scalar> { + // ^ Returns BOOL (i32 on Windows) + let this = self.eval_context_mut(); + let file = this.read_handle(file)?; + let dist_to_move = this.read_scalar(dist_to_move)?.to_i64()?; + let move_method = this.read_scalar(move_method)?.to_u32()?; + + let fd = match file { + Handle::File(fd) => fd, + _ => this.invalid_handle("SetFilePointerEx")?, + }; + + let Some(desc) = this.machine.fds.get(fd) else { + throw_unsup_format!("`SetFilePointerEx` is only supported on file backed handles"); + }; + + let file_begin = this.eval_windows_u32("c", "FILE_BEGIN"); + let file_current = this.eval_windows_u32("c", "FILE_CURRENT"); + let file_end = this.eval_windows_u32("c", "FILE_END"); + + let seek = if move_method == file_begin { + SeekFrom::Start(dist_to_move.try_into().unwrap()) + } else if move_method == file_current { + SeekFrom::Current(dist_to_move) + } else if move_method == file_end { + SeekFrom::End(dist_to_move) + } else { + throw_unsup_format!("Invalid move method: {move_method}") + }; + + match desc.seek(this.machine.communicate(), seek)? { + Ok(n) => { + this.write_scalar( + Scalar::from_i64(n.try_into().unwrap()), + &this.deref_pointer(new_fp)?, + )?; + interp_ok(this.eval_windows("c", "TRUE")) + } + Err(e) => { + this.set_last_error(e)?; + interp_ok(this.eval_windows("c", "FALSE")) + } + } + } +} + +/// Windows FILETIME is measured in 100-nanosecs since 1601 +fn extract_windows_epoch<'tcx>( + time: io::Result, +) -> InterpResult<'tcx, Option<(u32, u32)>> { + // (seconds in a year) * (369 years between 1970 and 1601) * 10 million (nanoseconds/second / 100) + const TIME_TO_EPOCH: u64 = 31_556_926 * 369 * 10_000_000; + match time.ok() { + Some(time) => { + let duration = system_time_to_duration(&time)?; + let secs = duration.as_secs().saturating_mul(10_000_000); + let nanos_hundred: u64 = (duration.subsec_nanos() / 100).into(); + let total = secs.saturating_add(nanos_hundred).saturating_add(TIME_TO_EPOCH); + #[allow(clippy::cast_possible_truncation)] + interp_ok(Some((total as u32, (total >> 32) as u32))) + } + None => interp_ok(None), + } +} + +fn write_filetime_field<'tcx>( + cx: &mut MiriInterpCx<'tcx>, + val: &MPlaceTy<'tcx>, + name: &str, + (low, high): (u32, u32), +) -> InterpResult<'tcx> { + cx.write_int_fields_named( + &[("dwLowDateTime", low.into()), ("dwHighDateTime", high.into())], + &cx.project_field_named(val, name)?, + ) +} diff --git a/src/shims/windows/handle.rs b/src/shims/windows/handle.rs index 3d872b65a6..60512b2d11 100644 --- a/src/shims/windows/handle.rs +++ b/src/shims/windows/handle.rs @@ -1,4 +1,5 @@ use std::mem::variant_count; +use std::panic::Location; use rustc_abi::HasDataLayout; @@ -8,6 +9,10 @@ use crate::*; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum PseudoHandle { CurrentThread, + CurrentProcess, + Stdin, + Stdout, + Stderr, } /// Miri representation of a Windows `HANDLE` @@ -16,20 +21,34 @@ pub enum Handle { Null, Pseudo(PseudoHandle), Thread(ThreadId), + File(i32), + Invalid, } impl PseudoHandle { const CURRENT_THREAD_VALUE: u32 = 0; + const STDIN_VALUE: u32 = 1; + const STDOUT_VALUE: u32 = 2; + const STDERR_VALUE: u32 = 3; + const CURRENT_PROCESS_VALUE: u32 = 4; fn value(self) -> u32 { match self { Self::CurrentThread => Self::CURRENT_THREAD_VALUE, + Self::CurrentProcess => Self::CURRENT_PROCESS_VALUE, + Self::Stdin => Self::STDIN_VALUE, + Self::Stdout => Self::STDOUT_VALUE, + Self::Stderr => Self::STDERR_VALUE, } } fn from_value(value: u32) -> Option { match value { Self::CURRENT_THREAD_VALUE => Some(Self::CurrentThread), + Self::CURRENT_PROCESS_VALUE => Some(Self::CurrentProcess), + Self::STDIN_VALUE => Some(Self::Stdin), + Self::STDOUT_VALUE => Some(Self::Stdout), + Self::STDERR_VALUE => Some(Self::Stderr), _ => None, } } @@ -47,12 +66,16 @@ impl Handle { const NULL_DISCRIMINANT: u32 = 0; const PSEUDO_DISCRIMINANT: u32 = 1; const THREAD_DISCRIMINANT: u32 = 2; + const FILE_DISCRIMINANT: u32 = 3; + const INVALID_DISCRIMINANT: u32 = 7; fn discriminant(self) -> u32 { match self { Self::Null => Self::NULL_DISCRIMINANT, Self::Pseudo(_) => Self::PSEUDO_DISCRIMINANT, Self::Thread(_) => Self::THREAD_DISCRIMINANT, + Self::File(_) => Self::FILE_DISCRIMINANT, + Self::Invalid => Self::INVALID_DISCRIMINANT, } } @@ -61,11 +84,15 @@ impl Handle { Self::Null => 0, Self::Pseudo(pseudo_handle) => pseudo_handle.value(), Self::Thread(thread) => thread.to_u32(), + #[expect(clippy::cast_sign_loss)] + Self::File(fd) => fd as u32, + Self::Invalid => 0x1FFFFFFF, } } fn packed_disc_size() -> u32 { - // ceil(log2(x)) is how many bits it takes to store x numbers + // We ensure that INVALID_HANDLE_VALUE (0xFFFFFFFF) decodes to Handle::Invalid + // see https://devblogs.microsoft.com/oldnewthing/20230914-00/?p=108766 let variant_count = variant_count::(); // however, std's ilog2 is floor(log2(x)) @@ -93,7 +120,7 @@ impl Handle { assert!(discriminant < 2u32.pow(disc_size)); // make sure the data fits into `data_size` bits - assert!(data < 2u32.pow(data_size)); + assert!(data <= 2u32.pow(data_size)); // packs the data into the lower `data_size` bits // and packs the discriminant right above the data @@ -105,6 +132,9 @@ impl Handle { Self::NULL_DISCRIMINANT if data == 0 => Some(Self::Null), Self::PSEUDO_DISCRIMINANT => Some(Self::Pseudo(PseudoHandle::from_value(data)?)), Self::THREAD_DISCRIMINANT => Some(Self::Thread(ThreadId::new_unchecked(data))), + #[expect(clippy::cast_possible_wrap)] + Self::FILE_DISCRIMINANT => Some(Self::File(data as i32)), + Self::INVALID_DISCRIMINANT => Some(Self::Invalid), _ => None, } } @@ -171,21 +201,72 @@ impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} #[allow(non_snake_case)] pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { + #[track_caller] + fn read_handle(&self, handle: &OpTy<'tcx>) -> InterpResult<'tcx, Handle> { + let this = self.eval_context_ref(); + let handle = this.read_scalar(handle)?; + match Handle::try_from_scalar(handle, this)? { + Ok(handle) => interp_ok(handle), + Err(HandleError::InvalidHandle) => + throw_machine_stop!(TerminationInfo::Abort(format!( + "invalid handle {} at {}", + handle.to_target_isize(this)?, + Location::caller(), + ))), + Err(HandleError::ThreadNotFound(_)) => + throw_machine_stop!(TerminationInfo::Abort(format!( + "invalid thread ID: {}", + Location::caller() + ))), + } + } + fn invalid_handle(&mut self, function_name: &str) -> InterpResult<'tcx, !> { throw_machine_stop!(TerminationInfo::Abort(format!( "invalid handle passed to `{function_name}`" ))) } + fn GetStdHandle(&mut self, which: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> { + let this = self.eval_context_mut(); + let which = this.read_scalar(which)?.to_i32()?; + + let stdin = this.eval_windows("c", "STD_INPUT_HANDLE").to_i32()?; + let stdout = this.eval_windows("c", "STD_OUTPUT_HANDLE").to_i32()?; + let stderr = this.eval_windows("c", "STD_ERROR_HANDLE").to_i32()?; + + let handle = if which == stdin { + Handle::Pseudo(PseudoHandle::Stdin) + } else if which == stdout { + Handle::Pseudo(PseudoHandle::Stdout) + } else if which == stderr { + Handle::Pseudo(PseudoHandle::Stderr) + } else { + throw_unsup_format!("Invalid argument to `GetStdHandle`: {which}") + }; + interp_ok(handle.to_scalar(this)) + } + fn CloseHandle(&mut self, handle_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> { let this = self.eval_context_mut(); - let handle = this.read_scalar(handle_op)?; - let ret = match Handle::try_from_scalar(handle, this)? { - Ok(Handle::Thread(thread)) => { + let ret = match this.read_handle(handle_op)? { + Handle::Thread(thread) => { this.detach_thread(thread, /*allow_terminated_joined*/ true)?; this.eval_windows("c", "TRUE") } + Handle::File(fd) => + if let Some(file) = this.machine.fds.get(fd) { + let err = file.close(this.machine.communicate(), this)?; + if let Err(e) = err { + this.set_last_error(e)?; + this.eval_windows("c", "FALSE") + } else { + this.eval_windows("c", "TRUE") + } + } else { + this.invalid_handle("CloseHandle")? + }, _ => this.invalid_handle("CloseHandle")?, }; diff --git a/src/shims/windows/mod.rs b/src/shims/windows/mod.rs index 892bd6924f..442c5a0dd1 100644 --- a/src/shims/windows/mod.rs +++ b/src/shims/windows/mod.rs @@ -1,12 +1,14 @@ pub mod foreign_items; mod env; +mod fs; mod handle; mod sync; mod thread; // All the Windows-specific extension traits pub use self::env::{EvalContextExt as _, WindowsEnvVars}; +pub use self::fs::EvalContextExt as _; pub use self::handle::EvalContextExt as _; pub use self::sync::EvalContextExt as _; pub use self::thread::EvalContextExt as _; diff --git a/src/shims/windows/thread.rs b/src/shims/windows/thread.rs index efc1c2286b..2c4d5c2297 100644 --- a/src/shims/windows/thread.rs +++ b/src/shims/windows/thread.rs @@ -62,14 +62,14 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { ) -> InterpResult<'tcx, Scalar> { let this = self.eval_context_mut(); - let handle = this.read_scalar(handle_op)?; + let handle = this.read_handle(handle_op)?; let timeout = this.read_scalar(timeout_op)?.to_u32()?; - let thread = match Handle::try_from_scalar(handle, this)? { - Ok(Handle::Thread(thread)) => thread, + let thread = match handle { + Handle::Thread(thread) => thread, // Unlike on posix, the outcome of joining the current thread is not documented. // On current Windows, it just deadlocks. - Ok(Handle::Pseudo(PseudoHandle::CurrentThread)) => this.active_thread(), + Handle::Pseudo(PseudoHandle::CurrentThread) => this.active_thread(), _ => this.invalid_handle("WaitForSingleObject")?, }; diff --git a/tests/pass/shims/fs.rs b/tests/pass/shims/fs.rs index 81151f4ac4..707c8aa635 100644 --- a/tests/pass/shims/fs.rs +++ b/tests/pass/shims/fs.rs @@ -1,8 +1,8 @@ -//@ignore-target: windows # File handling is not implemented yet //@compile-flags: -Zmiri-disable-isolation #![feature(io_error_more)] #![feature(io_error_uncategorized)] +#![cfg_attr(windows, allow(unused))] use std::collections::BTreeMap; use std::ffi::OsString; @@ -19,19 +19,22 @@ mod utils; fn main() { test_path_conversion(); test_file(); - test_file_clone(); test_file_create_new(); test_seek(); - test_metadata(); - test_file_set_len(); - test_file_sync(); - test_errors(); - test_rename(); - test_directory(); - test_canonicalize(); - test_from_raw_os_error(); - #[cfg(unix)] - test_pread_pwrite(); + #[cfg(not(windows))] + { + test_file_clone(); + test_metadata(); + test_file_set_len(); + test_file_sync(); + test_errors(); + test_rename(); + test_directory(); + test_canonicalize(); + test_from_raw_os_error(); + #[cfg(unix)] + test_pread_pwrite(); + } } fn test_path_conversion() { @@ -144,10 +147,10 @@ fn test_metadata() { let path = utils::prepare_with_content("miri_test_fs_metadata.txt", bytes); // Test that metadata of an absolute path is correct. - check_metadata(bytes, &path).unwrap(); + check_metadata(bytes, &path).expect("Absolute path metadata"); // Test that metadata of a relative path is correct. std::env::set_current_dir(path.parent().unwrap()).unwrap(); - check_metadata(bytes, Path::new(path.file_name().unwrap())).unwrap(); + check_metadata(bytes, Path::new(path.file_name().unwrap())).expect("Relative path metadata"); // Removing file should succeed. remove_file(&path).unwrap();