use std::collections::HashMap; use std::fs::{File, OpenOptions, remove_file}; use std::io::{Read, Write}; use rustc::ty::layout::Size; use crate::stacked_borrows::Tag; use crate::*; #[derive(Debug)] pub struct FileHandle { file: File, } pub struct FileHandler { handles: HashMap, low: i32, } impl Default for FileHandler { fn default() -> Self { FileHandler { handles: Default::default(), // 0, 1 and 2 are reserved for stdin, stdout and stderr low: 3, } } } impl<'mir, 'tcx> EvalContextExt<'mir, 'tcx> for crate::MiriEvalContext<'mir, 'tcx> {} pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx> { fn open( &mut self, path_op: OpTy<'tcx, Tag>, flag_op: OpTy<'tcx, Tag>, ) -> InterpResult<'tcx, i32> { let this = self.eval_context_mut(); this.check_no_isolation("open")?; let flag = this.read_scalar(flag_op)?.to_i32()?; let mut options = OpenOptions::new(); let o_rdonly = this.eval_libc_i32("O_RDONLY")?; let o_wronly = this.eval_libc_i32("O_WRONLY")?; let o_rdwr = this.eval_libc_i32("O_RDWR")?; // The first two bits of the flag correspond to the access mode in linux, macOS and // windows. We need to check that in fact the access mode flags for the current platform // only use these two bits, otherwise we are in an unsupported platform and should error. if (o_rdonly | o_wronly | o_rdwr) & !0b11 != 0 { throw_unsup_format!("Access mode flags on this platform are unsupported"); } // Now we check the access mode let access_mode = flag & 0b11; if access_mode == o_rdonly { options.read(true); } else if access_mode == o_wronly { options.write(true); } else if access_mode == o_rdwr { options.read(true).write(true); } else { throw_unsup_format!("Unsupported access mode {:#x}", access_mode); } // We need to check that there aren't unsupported options in `flag`. For this we try to // reproduce the content of `flag` in the `mirror` variable using only the supported // options. let mut mirror = access_mode; let o_append = this.eval_libc_i32("O_APPEND")?; if flag & o_append != 0 { options.append(true); mirror |= o_append; } let o_trunc = this.eval_libc_i32("O_TRUNC")?; if flag & o_trunc != 0 { options.truncate(true); mirror |= o_trunc; } let o_creat = this.eval_libc_i32("O_CREAT")?; if flag & o_creat != 0 { options.create(true); mirror |= o_creat; } let o_cloexec = this.eval_libc_i32("O_CLOEXEC")?; if flag & o_cloexec != 0 { // We do not need to do anything for this flag because `std` already sets it. // (Technically we do not support *not* setting this flag, but we ignore that.) mirror |= o_cloexec; } // If `flag` is not equal to `mirror`, there is an unsupported option enabled in `flag`, // then we throw an error. if flag != mirror { throw_unsup_format!("unsupported flags {:#x}", flag & !mirror); } let path_bytes = this .memory .read_c_str(this.read_scalar(path_op)?.not_undef()?)?; let path = std::str::from_utf8(path_bytes) .map_err(|_| err_unsup_format!("{:?} is not a valid utf-8 string", path_bytes))?; let fd = options.open(path).map(|file| { let mut fh = &mut this.machine.file_handler; fh.low += 1; fh.handles.insert(fh.low, FileHandle { file }).unwrap_none(); fh.low }); this.consume_result(fd) } fn fcntl( &mut self, fd_op: OpTy<'tcx, Tag>, cmd_op: OpTy<'tcx, Tag>, _arg1_op: Option>, ) -> InterpResult<'tcx, i32> { let this = self.eval_context_mut(); this.check_no_isolation("fcntl")?; let fd = this.read_scalar(fd_op)?.to_i32()?; let cmd = this.read_scalar(cmd_op)?.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. let fd_cloexec = this.eval_libc_i32("FD_CLOEXEC")?; this.get_handle_and(fd, |_| Ok(fd_cloexec)) } else { throw_unsup_format!("The {:#x} command is not supported for `fcntl`)", cmd); } } fn close(&mut self, fd_op: OpTy<'tcx, Tag>) -> InterpResult<'tcx, i32> { let this = self.eval_context_mut(); this.check_no_isolation("close")?; let fd = this.read_scalar(fd_op)?.to_i32()?; this.remove_handle_and(fd, |handle, this| { this.consume_result(handle.file.sync_all().map(|_| 0i32)) }) } fn read( &mut self, fd_op: OpTy<'tcx, Tag>, buf_op: OpTy<'tcx, Tag>, count_op: OpTy<'tcx, Tag>, ) -> InterpResult<'tcx, i64> { let this = self.eval_context_mut(); this.check_no_isolation("read")?; let count = this.read_scalar(count_op)?.to_usize(&*this.tcx)?; // Reading zero bytes should not change `buf` if count == 0 { return Ok(0); } let fd = this.read_scalar(fd_op)?.to_i32()?; let buf_scalar = this.read_scalar(buf_op)?.not_undef()?; // Remove the file handle to avoid borrowing issues this.remove_handle_and(fd, |mut handle, this| { // Don't use `?` to avoid returning before reinserting the handle let bytes = this.force_ptr(buf_scalar).and_then(|buf| { this.memory .get_mut(buf.alloc_id)? .get_bytes_mut(&*this.tcx, buf, Size::from_bytes(count)) .map(|buffer| handle.file.read(buffer)) }); // Reinsert the file handle this.machine.file_handler.handles.insert(fd, handle).unwrap_none(); this.consume_result(bytes?.map(|bytes| bytes as i64)) }) } fn write( &mut self, fd_op: OpTy<'tcx, Tag>, buf_op: OpTy<'tcx, Tag>, count_op: OpTy<'tcx, Tag>, ) -> InterpResult<'tcx, i64> { let this = self.eval_context_mut(); this.check_no_isolation("write")?; let count = this.read_scalar(count_op)?.to_usize(&*this.tcx)?; // Writing zero bytes should not change `buf` if count == 0 { return Ok(0); } let fd = this.read_scalar(fd_op)?.to_i32()?; let buf = this.force_ptr(this.read_scalar(buf_op)?.not_undef()?)?; this.remove_handle_and(fd, |mut handle, this| { let bytes = this.memory.get(buf.alloc_id).and_then(|alloc| { alloc .get_bytes(&*this.tcx, buf, Size::from_bytes(count)) .map(|bytes| handle.file.write(bytes).map(|bytes| bytes as i64)) }); this.machine.file_handler.handles.insert(fd, handle).unwrap_none(); this.consume_result(bytes?) }) } fn unlink( &mut self, path_op: OpTy<'tcx, Tag>) -> InterpResult<'tcx, i32> { let this = self.eval_context_mut(); this.check_no_isolation("unlink")?; let path_bytes = this .memory .read_c_str(this.read_scalar(path_op)?.not_undef()?)?; let path = std::str::from_utf8(path_bytes) .map_err(|_| err_unsup_format!("{:?} is not a valid utf-8 string", path_bytes))?; let result = remove_file(path).map(|_| 0); this.consume_result(result) } /// Helper function that gets a `FileHandle` immutable reference and allows to manipulate it /// using the `f` closure. /// /// If the `fd` file descriptor does not correspond to a file, this functions returns `Ok(-1)` /// and sets `Evaluator::last_error` to `libc::EBADF` (invalid file descriptor). /// /// This function uses `T: From` instead of `i32` directly because some IO related /// functions return different integer types (like `read`, that returns an `i64`) fn get_handle_and>(&mut self, fd: i32, f: F) -> InterpResult<'tcx, T> where F: Fn(&FileHandle) -> InterpResult<'tcx, T>, { let this = self.eval_context_mut(); if let Some(handle) = this.machine.file_handler.handles.get(&fd) { f(handle) } else { let ebadf = this.eval_libc("EBADF")?; this.set_last_error(ebadf)?; Ok((-1).into()) } } /// Helper function that removes a `FileHandle` and allows to manipulate it using the `f` /// closure. This function is quite useful when you need to modify a `FileHandle` but you need /// to modify `MiriEvalContext` at the same time, so you can modify the handle and reinsert it /// using `f`. /// /// If the `fd` file descriptor does not correspond to a file, this functions returns `Ok(-1)` /// and sets `Evaluator::last_error` to `libc::EBADF` (invalid file descriptor). /// /// This function uses `T: From` instead of `i32` directly because some IO related /// functions return different integer types (like `read`, that returns an `i64`) fn remove_handle_and>(&mut self, fd: i32, mut f: F) -> InterpResult<'tcx, T> where F: FnMut(FileHandle, &mut MiriEvalContext<'mir, 'tcx>) -> InterpResult<'tcx, T>, { let this = self.eval_context_mut(); if let Some(handle) = this.machine.file_handler.handles.remove(&fd) { f(handle, this) } else { let ebadf = this.eval_libc("EBADF")?; this.set_last_error(ebadf)?; Ok((-1).into()) } } /// Helper function that consumes an `std::io::Result` and returns an /// `InterpResult<'tcx,T>::Ok` instead. It is expected that the result can be converted to an /// OS error using `std::io::Error::raw_os_error`. /// /// This function uses `T: From` instead of `i32` directly because some IO related /// functions return different integer types (like `read`, that returns an `i64`) fn consume_result>( &mut self, result: std::io::Result, ) -> InterpResult<'tcx, T> { match result { Ok(ok) => Ok(ok), Err(e) => { self.eval_context_mut().set_last_error_from_io_error(e)?; Ok((-1).into()) } } } }