diff --git a/src/helpers.rs b/src/helpers.rs index 01fc8e0df3f..766a3cba734 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -37,6 +37,7 @@ const UNIX_IO_ERROR_TABLE: &[(std::io::ErrorKind, &str)] = { (NotFound, "ENOENT"), (Interrupted, "EINTR"), (InvalidInput, "EINVAL"), + (InvalidFilename, "ENAMETOOLONG"), (TimedOut, "ETIMEDOUT"), (AlreadyExists, "EEXIST"), (WouldBlock, "EWOULDBLOCK"), diff --git a/src/shims/unix/foreign_items.rs b/src/shims/unix/foreign_items.rs index 2a051fb7753..5eb2d0a6cac 100644 --- a/src/shims/unix/foreign_items.rs +++ b/src/shims/unix/foreign_items.rs @@ -161,6 +161,11 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx // fadvise is only informational, we can ignore it. this.write_null(dest)?; } + "realpath" => { + let [path, resolved_path] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?; + let result = this.realpath(path, resolved_path)?; + this.write_pointer(result, dest)?; + } // Time related shims "gettimeofday" => { diff --git a/src/shims/unix/fs.rs b/src/shims/unix/fs.rs index c9f35c04891..76c17098791 100644 --- a/src/shims/unix/fs.rs +++ b/src/shims/unix/fs.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::collections::BTreeMap; +use std::convert::TryInto; use std::fs::{ read_dir, remove_dir, remove_file, rename, DirBuilder, File, FileType, OpenOptions, ReadDir, }; @@ -1662,6 +1663,67 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx this.set_last_error(enotty)?; Ok(0) } + + fn realpath( + &mut self, + path_op: &OpTy<'tcx, Provenance>, + processed_path_op: &OpTy<'tcx, Provenance>, + ) -> InterpResult<'tcx, Pointer>> { + let this = self.eval_context_mut(); + this.assert_target_os_is_unix("realpath"); + + let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?; + let processed_ptr = this.read_pointer(processed_path_op)?; + + // Reject if isolation is enabled. + if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op { + this.reject_in_isolation("`realpath`", reject_with)?; + let eacc = this.eval_libc("EACCES")?; + this.set_last_error(eacc)?; + return Ok(Pointer::null()); + } + + let result = std::fs::canonicalize(pathname); + match result { + Ok(resolved) => { + let path_max = this + .eval_libc_i32("PATH_MAX")? + .try_into() + .expect("PATH_MAX does not fit in u64"); + let dest = if this.ptr_is_null(processed_ptr)? { + // POSIX says behavior when passing a null pointer is implementation-defined, + // but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer + // similarly to: + // + // "If resolved_path is specified as NULL, then realpath() uses + // malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold + // the resolved pathname, and returns a pointer to this buffer. The + // caller should deallocate this buffer using free(3)." + // + this.alloc_os_str_as_c_str(resolved.as_os_str(), MiriMemoryKind::C.into())? + } else { + let (wrote_path, _) = + this.write_path_to_c_str(&resolved, processed_ptr, path_max)?; + + if !wrote_path { + // Note that we do not explicitly handle `FILENAME_MAX` + // (different from `PATH_MAX` above) as it is Linux-specific and + // seems like a bit of a mess anyway: . + let enametoolong = this.eval_libc("ENAMETOOLONG")?; + this.set_last_error(enametoolong)?; + return Ok(Pointer::null()); + } + processed_ptr + }; + + Ok(dest) + } + Err(e) => { + this.set_last_error_from_io_error(e.kind())?; + Ok(Pointer::null()) + } + } + } } /// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when diff --git a/src/shims/unix/macos/foreign_items.rs b/src/shims/unix/macos/foreign_items.rs index 21c7762c3ca..fb545d8b584 100644 --- a/src/shims/unix/macos/foreign_items.rs +++ b/src/shims/unix/macos/foreign_items.rs @@ -73,6 +73,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx let result = this.ftruncate64(fd, length)?; this.write_scalar(Scalar::from_i32(result), dest)?; } + "realpath$DARWIN_EXTSN" => { + let [path, resolved_path] = + this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?; + let result = this.realpath(path, resolved_path)?; + this.write_pointer(result, dest)?; + } // Environment related shims "_NSGetEnviron" => { diff --git a/tests/pass/fs.rs b/tests/pass/fs.rs index 9d59fedb20e..a8025007bf5 100644 --- a/tests/pass/fs.rs +++ b/tests/pass/fs.rs @@ -24,6 +24,7 @@ fn main() { test_errors(); test_rename(); test_directory(); + test_canonicalize(); test_dup_stdout_stderr(); // These all require unix, if the test is changed to no longer `ignore-windows`, move these to a unix test @@ -365,6 +366,24 @@ fn test_rename() { remove_file(&path2).unwrap(); } +fn test_canonicalize() { + use std::fs::canonicalize; + let dir_path = prepare_dir("miri_test_fs_dir"); + create_dir(&dir_path).unwrap(); + let path = dir_path.join("test_file"); + drop(File::create(&path).unwrap()); + + let p = canonicalize(format!("{}/./test_file", dir_path.to_string_lossy())).unwrap(); + assert_eq!(p.to_string_lossy().find('.'), None); + + remove_dir_all(&dir_path).unwrap(); + + // Make sure we get an error for long paths. + use std::convert::TryInto; + let too_long = "x/".repeat(libc::PATH_MAX.try_into().unwrap()); + assert!(canonicalize(too_long).is_err()); +} + fn test_directory() { let dir_path = prepare_dir("miri_test_fs_dir"); // Creating a directory should succeed. diff --git a/tests/pass/libc.rs b/tests/pass/libc.rs index 9b83ab45b0c..2735e5b25bc 100644 --- a/tests/pass/libc.rs +++ b/tests/pass/libc.rs @@ -1,16 +1,141 @@ //@ignore-target-windows: No libc on Windows //@compile-flags: -Zmiri-disable-isolation +#![feature(io_error_more)] #![feature(rustc_private)] use std::fs::{remove_file, File}; use std::os::unix::io::AsRawFd; +use std::path::PathBuf; -fn tmp() -> std::path::PathBuf { +fn tmp() -> PathBuf { std::env::var("MIRI_TEMP") - .map(std::path::PathBuf::from) + .map(|tmp| { + // MIRI_TEMP is set outside of our emulated + // program, so it may have path separators that don't + // correspond to our target platform. We normalize them here + // before constructing a `PathBuf` + return PathBuf::from(tmp.replace("\\", "/")); + }) .unwrap_or_else(|_| std::env::temp_dir()) } +/// Test allocating variant of `realpath`. +fn test_posix_realpath_alloc() { + use std::ffi::OsString; + use std::ffi::{CStr, CString}; + use std::fs::{remove_file, File}; + use std::os::unix::ffi::OsStrExt; + use std::os::unix::ffi::OsStringExt; + + let buf; + let path = tmp().join("miri_test_libc_posix_realpath_alloc"); + let c_path = CString::new(path.as_os_str().as_bytes()).expect("CString::new failed"); + + // Cleanup before test. + remove_file(&path).ok(); + // Create file. + drop(File::create(&path).unwrap()); + unsafe { + let r = libc::realpath(c_path.as_ptr(), std::ptr::null_mut()); + assert!(!r.is_null()); + buf = CStr::from_ptr(r).to_bytes().to_vec(); + libc::free(r as *mut _); + } + let canonical = PathBuf::from(OsString::from_vec(buf)); + assert_eq!(path.file_name(), canonical.file_name()); + + // Cleanup after test. + remove_file(&path).unwrap(); +} + +/// Test non-allocating variant of `realpath`. +fn test_posix_realpath_noalloc() { + use std::ffi::{CStr, CString}; + use std::fs::{remove_file, File}; + use std::os::unix::ffi::OsStrExt; + + let path = tmp().join("miri_test_libc_posix_realpath_noalloc"); + let c_path = CString::new(path.as_os_str().as_bytes()).expect("CString::new failed"); + + let mut v = vec![0; libc::PATH_MAX as usize]; + + // Cleanup before test. + remove_file(&path).ok(); + // Create file. + drop(File::create(&path).unwrap()); + unsafe { + let r = libc::realpath(c_path.as_ptr(), v.as_mut_ptr()); + assert!(!r.is_null()); + } + let c = unsafe { CStr::from_ptr(v.as_ptr()) }; + let canonical = PathBuf::from(c.to_str().expect("CStr to str")); + + assert_eq!(path.file_name(), canonical.file_name()); + + // Cleanup after test. + remove_file(&path).unwrap(); +} + +/// Test failure cases for `realpath`. +fn test_posix_realpath_errors() { + use std::convert::TryInto; + use std::ffi::CString; + use std::fs::{create_dir_all, remove_dir_all}; + use std::io::ErrorKind; + use std::os::unix::ffi::OsStrExt; + use std::os::unix::fs::symlink; + + // Test non-existent path returns an error. + let c_path = CString::new("./nothing_to_see_here").expect("CString::new failed"); + let r = unsafe { libc::realpath(c_path.as_ptr(), std::ptr::null_mut()) }; + assert!(r.is_null()); + let e = std::io::Error::last_os_error(); + assert_eq!(e.raw_os_error(), Some(libc::ENOENT)); + assert_eq!(e.kind(), ErrorKind::NotFound); + + // Test that a long path returns an error. + // + // Linux first checks if the path exists and macos does not. + // Using an existing path ensures all platforms return `ENAMETOOLONG` given a long path. + // + // Rather than creating a bunch of directories, we create two directories containing symlinks. + // Sadly we can't avoid creating directories and instead use a path like "./././././" or "./../../" as linux + // appears to collapse "." and ".." before checking path length. + let path = tmp().join("posix_realpath_errors"); + // Cleanup before test. + remove_dir_all(&path).ok(); + + // The directories we will put symlinks in. + let x = path.join("x/"); + let y = path.join("y/"); + + // The symlinks in each directory pointing to each other. + let yx_sym = y.join("x"); + let xy_sym = x.join("y"); + + // Create directories. + create_dir_all(&x).expect("dir x"); + create_dir_all(&y).expect("dir y"); + + // Create symlinks between directories. + symlink(&x, &yx_sym).expect("symlink x"); + symlink(&y, &xy_sym).expect("symlink y "); + + // This path exists due to the symlinks created above. + let too_long = path.join("x/y/".repeat(libc::PATH_MAX.try_into().unwrap())); + + let c_path = CString::new(too_long.into_os_string().as_bytes()).expect("CString::new failed"); + let r = unsafe { libc::realpath(c_path.as_ptr(), std::ptr::null_mut()) }; + let e = std::io::Error::last_os_error(); + + assert!(r.is_null()); + assert_eq!(e.raw_os_error(), Some(libc::ENAMETOOLONG)); + assert_eq!(e.kind(), ErrorKind::InvalidFilename); + + // Cleanup after test. + remove_dir_all(&path).ok(); +} + #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn test_posix_fadvise() { use std::convert::TryInto; @@ -336,6 +461,10 @@ fn main() { test_posix_gettimeofday(); + test_posix_realpath_alloc(); + test_posix_realpath_noalloc(); + test_posix_realpath_errors(); + #[cfg(any(target_os = "linux"))] test_sync_file_range();