diff --git a/src/shims/unix/foreign_items.rs b/src/shims/unix/foreign_items.rs index 5eb2d0a6ca..83815cccb0 100644 --- a/src/shims/unix/foreign_items.rs +++ b/src/shims/unix/foreign_items.rs @@ -166,6 +166,11 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx let result = this.realpath(path, resolved_path)?; this.write_pointer(result, dest)?; } + "mkstemp" => { + let [template] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?; + let result = this.mkstemp(template)?; + this.write_scalar(Scalar::from_i32(result), dest)?; + } // Time related shims "gettimeofday" => { diff --git a/src/shims/unix/fs.rs b/src/shims/unix/fs.rs index 36be1ec4f6..9b00579d87 100644 --- a/src/shims/unix/fs.rs +++ b/src/shims/unix/fs.rs @@ -5,7 +5,7 @@ use std::fs::{ read_dir, remove_dir, remove_file, rename, DirBuilder, File, FileType, OpenOptions, ReadDir, }; use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::time::SystemTime; use log::trace; @@ -14,6 +14,7 @@ use rustc_data_structures::fx::FxHashMap; use rustc_middle::ty::{self, layout::LayoutOf}; use rustc_target::abi::{Align, Size}; +use crate::shims::os_str::bytes_to_os_str; use crate::*; use shims::os_str::os_str_to_bytes; use shims::time::system_time_to_duration; @@ -1724,6 +1725,133 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx } } } + fn mkstemp(&mut self, template_op: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, i32> { + use rand::seq::SliceRandom; + + // POSIX defines the template string. + const TEMPFILE_TEMPLATE_STR: &str = "XXXXXX"; + + let this = self.eval_context_mut(); + this.assert_target_os_is_unix("mkstemp"); + + // POSIX defines the maximum number of attempts before failure. + // + // `mkstemp()` relies on `tmpnam()` which in turn relies on `TMP_MAX`. + // POSIX says this about `TMP_MAX`: + // * Minimum number of unique filenames generated by `tmpnam()`. + // * Maximum number of times an application can call `tmpnam()` reliably. + // * The value of `TMP_MAX` is at least 25. + // * On XSI-conformant systems, the value of `TMP_MAX` is at least 10000. + // See . + let max_attempts = this.eval_libc("TMP_MAX")?.to_u32()?; + + // Get the raw bytes from the template -- as a byte slice, this is a string in the target + // (and the target is unix, so a byte slice is the right representation). + let template_ptr = this.read_pointer(template_op)?; + let mut template = this.eval_context_ref().read_c_str(template_ptr)?.to_owned(); + let template_bytes = template.as_mut_slice(); + + // Reject if isolation is enabled. + if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op { + this.reject_in_isolation("`mkstemp`", reject_with)?; + let eacc = this.eval_libc("EACCES")?; + this.set_last_error(eacc)?; + return Ok(-1); + } + + // Get the bytes of the suffix we expect in _target_ encoding. + let suffix_bytes = TEMPFILE_TEMPLATE_STR.as_bytes(); + + // At this point we have one `&[u8]` that represents the template and one `&[u8]` + // that represents the expected suffix. + + // Now we figure out the index of the slice we expect to contain the suffix. + let start_pos = template_bytes.len().saturating_sub(suffix_bytes.len()); + let end_pos = template_bytes.len(); + let last_six_char_bytes = &template_bytes[start_pos..end_pos]; + + // If we don't find the suffix, it is an error. + if last_six_char_bytes != suffix_bytes { + let einval = this.eval_libc("EINVAL")?; + this.set_last_error(einval)?; + return Ok(-1); + } + + // At this point we know we have 6 ASCII 'X' characters as a suffix. + + // From + const SUBSTITUTIONS: &[char; 62] = &[ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', + 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', + 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + ]; + + // The file is opened with specific options, which Rust does not expose in a portable way. + // So we use specific APIs depending on the host OS. + let mut fopts = OpenOptions::new(); + fopts.read(true).write(true).create_new(true); + + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + fopts.mode(0o600); + // Do not allow others to read or modify this file. + fopts.custom_flags(libc::O_EXCL); + } + #[cfg(windows)] + { + use std::os::windows::fs::OpenOptionsExt; + // Do not allow others to read or modify this file. + fopts.share_mode(0); + } + + // If the generated file already exists, we will try again `max_attempts` many times. + for _ in 0..max_attempts { + let rng = this.machine.rng.get_mut(); + + // Generate a random unique suffix. + let unique_suffix = SUBSTITUTIONS.choose_multiple(rng, 6).collect::(); + + // Replace the template string with the random string. + template_bytes[start_pos..end_pos].copy_from_slice(unique_suffix.as_bytes()); + + // Write the modified template back to the passed in pointer to maintain POSIX semantics. + this.write_bytes_ptr(template_ptr, template_bytes.iter().copied())?; + + // To actually open the file, turn this into a host OsString. + let p = bytes_to_os_str(template_bytes)?.to_os_string(); + + let possibly_unique = std::env::temp_dir().join::(p.into()); + + let file = fopts.open(&possibly_unique); + + match file { + Ok(f) => { + let fh = &mut this.machine.file_handler; + let fd = fh.insert_fd(Box::new(FileHandle { file: f, writable: true })); + return Ok(fd); + } + Err(e) => + match e.kind() { + // If the random file already exists, keep trying. + ErrorKind::AlreadyExists => continue, + // Any other errors are returned to the caller. + _ => { + // "On error, -1 is returned, and errno is set to + // indicate the error" + this.set_last_error_from_io_error(e.kind())?; + return Ok(-1); + } + }, + } + } + + // We ran out of attempts to create the file, return an error. + let eexist = this.eval_libc("EEXIST")?; + this.set_last_error(eexist)?; + Ok(-1) + } } /// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when diff --git a/tests/fail/fs/mkstemp_immutable_arg.rs b/tests/fail/fs/mkstemp_immutable_arg.rs new file mode 100644 index 0000000000..5be1fb394b --- /dev/null +++ b/tests/fail/fs/mkstemp_immutable_arg.rs @@ -0,0 +1,13 @@ +//@ignore-target-windows: No libc on Windows +//@compile-flags: -Zmiri-disable-isolation + +#![feature(rustc_private)] + +fn main() { + test_mkstemp_immutable_arg(); +} + +fn test_mkstemp_immutable_arg() { + let s: *mut libc::c_char = b"fooXXXXXX\0" as *const _ as *mut _; + let _fd = unsafe { libc::mkstemp(s) }; //~ ERROR: Undefined Behavior: writing to alloc1 which is read-only +} diff --git a/tests/fail/fs/mkstemp_immutable_arg.stderr b/tests/fail/fs/mkstemp_immutable_arg.stderr new file mode 100644 index 0000000000..0bd91f90a1 --- /dev/null +++ b/tests/fail/fs/mkstemp_immutable_arg.stderr @@ -0,0 +1,20 @@ +error: Undefined Behavior: writing to ALLOC which is read-only + --> $DIR/mkstemp_immutable_arg.rs:LL:CC + | +LL | let _fd = unsafe { libc::mkstemp(s) }; + | ^^^^^^^^^^^^^^^^ writing to ALLOC which is read-only + | + = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior + = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information + = note: backtrace: + = note: inside `test_mkstemp_immutable_arg` at $DIR/mkstemp_immutable_arg.rs:LL:CC +note: inside `main` at $DIR/mkstemp_immutable_arg.rs:LL:CC + --> $DIR/mkstemp_immutable_arg.rs:LL:CC + | +LL | test_mkstemp_immutable_arg(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to previous error + diff --git a/tests/pass/libc.rs b/tests/pass/libc.rs index c7331b110e..468da0845a 100644 --- a/tests/pass/libc.rs +++ b/tests/pass/libc.rs @@ -407,11 +407,56 @@ fn test_isatty() { } } +fn test_posix_mkstemp() { + use std::ffi::CString; + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + use std::os::unix::io::FromRawFd; + use std::path::Path; + + let valid_template = "fooXXXXXX"; + // C needs to own this as `mkstemp(3)` says: + // "Since it will be modified, `template` must not be a string constant, but + // should be declared as a character array." + // There seems to be no `as_mut_ptr` on `CString` so we need to use `into_raw`. + let ptr = CString::new(valid_template).unwrap().into_raw(); + let fd = unsafe { libc::mkstemp(ptr) }; + // Take ownership back in Rust to not leak memory. + let slice = unsafe { CString::from_raw(ptr) }; + assert!(fd > 0); + let osstr = OsStr::from_bytes(slice.to_bytes()); + let path: &Path = osstr.as_ref(); + let name = path.file_name().unwrap().to_string_lossy(); + assert!(name.ne("fooXXXXXX")); + assert!(name.starts_with("foo")); + assert_eq!(name.len(), 9); + assert_eq!( + name.chars().skip(3).filter(char::is_ascii_alphanumeric).collect::>().len(), + 6 + ); + let file = unsafe { File::from_raw_fd(fd) }; + assert!(file.set_len(0).is_ok()); + + let invalid_templates = vec!["foo", "barXX", "XXXXXXbaz", "whatXXXXXXever", "X"]; + for t in invalid_templates { + let ptr = CString::new(t).unwrap().into_raw(); + let fd = unsafe { libc::mkstemp(ptr) }; + let _ = unsafe { CString::from_raw(ptr) }; + // "On error, -1 is returned, and errno is set to + // indicate the error" + assert_eq!(fd, -1); + let e = std::io::Error::last_os_error(); + assert_eq!(e.raw_os_error(), Some(libc::EINVAL)); + assert_eq!(e.kind(), std::io::ErrorKind::InvalidInput); + } +} + fn main() { #[cfg(any(target_os = "linux", target_os = "freebsd"))] test_posix_fadvise(); test_posix_gettimeofday(); + test_posix_mkstemp(); test_posix_realpath_alloc(); test_posix_realpath_noalloc();