diff --git a/src/lib.rs b/src/lib.rs index cea99d86ea..9f4e605b6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ pub use crate::shims::intrinsics::EvalContextExt as IntrinsicsEvalContextExt; pub use crate::shims::tls::{EvalContextExt as TlsEvalContextExt, TlsData}; pub use crate::shims::dlsym::{Dlsym, EvalContextExt as DlsymEvalContextExt}; pub use crate::shims::env::{EnvVars, EvalContextExt as EnvEvalContextExt}; +pub use crate::shims::io::{FileHandler, EvalContextExt as FileEvalContextExt}; pub use crate::operator::EvalContextExt as OperatorEvalContextExt; pub use crate::range_map::RangeMap; pub use crate::helpers::{EvalContextExt as HelpersEvalContextExt}; diff --git a/src/machine.rs b/src/machine.rs index 340dd109e1..19be5b547b 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -96,6 +96,8 @@ pub struct Evaluator<'tcx> { /// If enabled, the `env_vars` field is populated with the host env vars during initialization /// and random number generation is delegated to the host. pub(crate) communicate: bool, + + pub(crate) file_handler: FileHandler, } impl<'tcx> Evaluator<'tcx> { @@ -110,6 +112,7 @@ impl<'tcx> Evaluator<'tcx> { last_error: 0, tls: TlsData::default(), communicate, + file_handler: Default::default(), } } } diff --git a/src/shims/foreign_items.rs b/src/shims/foreign_items.rs index fedb6354b8..b8609fd2ef 100644 --- a/src/shims/foreign_items.rs +++ b/src/shims/foreign_items.rs @@ -446,6 +446,26 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx this.write_scalar(Scalar::from_int(result, dest.layout.size), dest)?; } + "open" | "open64" => { + let result = this.open(args[0], args[1])?; + this.write_scalar(Scalar::from_int(result, dest.layout.size), dest)?; + } + + "fcntl" => { + let result = this.fcntl(args[0], args[1], args.get(2).cloned())?; + this.write_scalar(Scalar::from_int(result, dest.layout.size), dest)?; + } + + "close" | "close$NOCANCEL" => { + let result = this.close(args[0])?; + this.write_scalar(Scalar::from_int(result, dest.layout.size), dest)?; + } + + "read" => { + let result = this.read(args[0], args[1], args[2])?; + this.write_scalar(Scalar::from_int(result, dest.layout.size), dest)?; + } + "write" => { let fd = this.read_scalar(args[0])?.to_i32()?; let buf = this.read_scalar(args[1])?.not_undef()?; @@ -929,6 +949,14 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx } return Ok(None); } + + fn eval_libc_i32(&mut self, name: &str) -> InterpResult<'tcx, i32> { + self + .eval_context_mut() + .eval_path_scalar(&["libc", name])? + .ok_or_else(|| err_unsup_format!("Path libc::{} cannot be resolved.", name).into()) + .and_then(|scalar| scalar.to_i32()) + } } // Shims the linux 'getrandom()' syscall. diff --git a/src/shims/io.rs b/src/shims/io.rs new file mode 100644 index 0000000000..0d1adcce65 --- /dev/null +++ b/src/shims/io.rs @@ -0,0 +1,213 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; + +use rustc::ty::layout::Size; + +use crate::stacked_borrows::Tag; +use crate::*; + +pub struct FileHandle { + file: File, + flag: i32, +} + +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(); + + if !this.machine.communicate { + throw_unsup_format!("`open` not available when isolation is enabled") + } + + let flag = this.read_scalar(flag_op)?.to_i32()?; + + if flag != this.eval_libc_i32("O_RDONLY")? && flag != this.eval_libc_i32("O_CLOEXEC")? { + throw_unsup_format!("Unsupported flag {:#x}", flag); + } + + 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 = File::open(path).map(|file| { + let mut fh = &mut this.machine.file_handler; + fh.low += 1; + fh.handles.insert(fh.low, FileHandle { file, flag }); + fh.low + }); + + this.consume_result(fd) + } + + fn fcntl( + &mut self, + fd_op: OpTy<'tcx, Tag>, + cmd_op: OpTy<'tcx, Tag>, + arg_op: Option>, + ) -> InterpResult<'tcx, i32> { + let this = self.eval_context_mut(); + + if !this.machine.communicate { + throw_unsup_format!("`open` not available when isolation is enabled") + } + + let fd = this.read_scalar(fd_op)?.to_i32()?; + let cmd = this.read_scalar(cmd_op)?.to_i32()?; + + if cmd == this.eval_libc_i32("F_SETFD")? { + // This does not affect the file itself. Certain flags might require changing the file + // or the way it is accessed somehow. + let flag = this.read_scalar(arg_op.unwrap())?.to_i32()?; + // The only usage of this in stdlib at the moment is to enable the `FD_CLOEXEC` flag. + let fd_cloexec = this.eval_libc_i32("FD_CLOEXEC")?; + if let Some(FileHandle { flag: old_flag, .. }) = + this.machine.file_handler.handles.get_mut(&fd) + { + if flag ^ *old_flag == fd_cloexec { + *old_flag = flag; + } else { + throw_unsup_format!("Unsupported arg {:#x} for `F_SETFD`", flag); + } + } + Ok(0) + } else if cmd == this.eval_libc_i32("F_GETFD")? { + this.get_handle_and(fd, |handle| Ok(handle.flag)) + } else { + throw_unsup_format!("Unsupported command {:#x}", cmd); + } + } + + fn close(&mut self, fd_op: OpTy<'tcx, Tag>) -> InterpResult<'tcx, i32> { + let this = self.eval_context_mut(); + + if !this.machine.communicate { + throw_unsup_format!("`open` not available when isolation is enabled") + } + + 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(); + + if !this.machine.communicate { + throw_unsup_format!("`open` not available when isolation is enabled") + } + + let tcx = &{ this.tcx.tcx }; + + let fd = this.read_scalar(fd_op)?.to_i32()?; + let buf = this.force_ptr(this.read_scalar(buf_op)?.not_undef()?)?; + let count = this.read_scalar(count_op)?.to_usize(&*this.tcx)?; + + // Remove the file handle to avoid borrowing issues + this.remove_handle_and( + fd, + |mut handle, this| { + let bytes = handle + .file + .read(this.memory_mut().get_mut(buf.alloc_id)?.get_bytes_mut( + tcx, + buf, + Size::from_bytes(count), + )?) + .map(|bytes| bytes as i64); + // Reinsert the file handle + this.machine.file_handler.handles.insert(fd, handle); + this.consume_result(bytes) + }, + ) + } + + /// Helper function that gets a `FileHandle` immutable reference and allows to manipulate it + /// using `f`. + /// + /// If the `fd` file descriptor does not corresponds 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 { + this.machine.last_error = this.eval_libc_i32("EBADF")? as u32; + 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 corresponds 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 { + this.machine.last_error = this.eval_libc_i32("EBADF")? as u32; + 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().machine.last_error = e.raw_os_error().unwrap() as u32; + Ok((-1).into()) + } + } + } +} diff --git a/src/shims/mod.rs b/src/shims/mod.rs index 4ccdbdc0d7..3df5b839e5 100644 --- a/src/shims/mod.rs +++ b/src/shims/mod.rs @@ -3,6 +3,7 @@ pub mod env; pub mod foreign_items; pub mod intrinsics; pub mod tls; +pub mod io; use rustc::{mir, ty}; diff --git a/tests/hello.txt b/tests/hello.txt new file mode 100644 index 0000000000..8ab686eafe --- /dev/null +++ b/tests/hello.txt @@ -0,0 +1 @@ +Hello, World! diff --git a/tests/run-pass/file_read.rs b/tests/run-pass/file_read.rs new file mode 100644 index 0000000000..a60640d1b3 --- /dev/null +++ b/tests/run-pass/file_read.rs @@ -0,0 +1,13 @@ +// ignore-windows: File handling is not implemented yet +// compile-flags: -Zmiri-disable-isolation + +use std::fs::File; +use std::io::Read; + +fn main() { + // FIXME: create the file and delete it when `rm` is implemented. + let mut file = File::open("./tests/hello.txt").unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + assert_eq!("Hello, World!\n", contents); +}