Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/shims/unix/foreign_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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" => {
Expand Down
130 changes: 129 additions & 1 deletion src/shims/unix/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdio.h.html>.
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 <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/posix/tempname.c#L175>
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::<String>();

// 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::<PathBuf>(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
Expand Down
13 changes: 13 additions & 0 deletions tests/fail/fs/mkstemp_immutable_arg.rs
Original file line number Diff line number Diff line change
@@ -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
}
20 changes: 20 additions & 0 deletions tests/fail/fs/mkstemp_immutable_arg.stderr
Original file line number Diff line number Diff line change
@@ -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

45 changes: 45 additions & 0 deletions tests/pass/libc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<char>>().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();
Expand Down