From b43bede93826f459e210957c8fd12fc1dd77d678 Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Thu, 30 Jun 2022 13:10:31 -0400 Subject: [PATCH 1/2] Add shim for `realpath` on unix --- src/helpers.rs | 1 + src/shims/unix/foreign_items.rs | 5 + src/shims/unix/fs.rs | 62 ++++++++++++ src/shims/unix/macos/foreign_items.rs | 6 ++ tests/pass/fs.rs | 19 ++++ tests/pass/libc.rs | 133 +++++++++++++++++++++++++- 6 files changed, 224 insertions(+), 2 deletions(-) diff --git a/src/helpers.rs b/src/helpers.rs index 01fc8e0df3..766a3cba73 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 2a051fb775..5eb2d0a6ca 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 c9f35c0489..76c1709879 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 21c7762c3c..fb545d8b58 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 9d59fedb20..a8025007bf 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 9b83ab45b0..2735e5b25b 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(); From 8356f4cc23ea4c2403011b6c2c619738fce7e32a Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Tue, 2 Aug 2022 18:08:43 -0400 Subject: [PATCH 2/2] output realpath as a path, and remove a bogus test --- src/helpers.rs | 1 + src/shims/os_str.rs | 13 ++++++++++++ src/shims/unix/fs.rs | 2 +- tests/pass/libc.rs | 48 -------------------------------------------- 4 files changed, 15 insertions(+), 49 deletions(-) diff --git a/src/helpers.rs b/src/helpers.rs index 766a3cba73..acc2367afa 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -42,6 +42,7 @@ const UNIX_IO_ERROR_TABLE: &[(std::io::ErrorKind, &str)] = { (AlreadyExists, "EEXIST"), (WouldBlock, "EWOULDBLOCK"), (DirectoryNotEmpty, "ENOTEMPTY"), + (FilesystemLoop, "ELOOP"), ] }; diff --git a/src/shims/os_str.rs b/src/shims/os_str.rs index 71824bee34..b9f3a435ea 100644 --- a/src/shims/os_str.rs +++ b/src/shims/os_str.rs @@ -250,6 +250,19 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx this.write_os_str_to_wide_str(&os_str, ptr, size) } + /// Allocate enough memory to store a Path as a null-terminated sequence of bytes, + /// adjusting path separators if needed. + fn alloc_path_as_c_str( + &mut self, + path: &Path, + memkind: MemoryKind, + ) -> InterpResult<'tcx, Pointer>> { + let this = self.eval_context_mut(); + let os_str = this + .convert_path_separator(Cow::Borrowed(path.as_os_str()), PathConversion::HostToTarget); + this.alloc_os_str_as_c_str(&os_str, memkind) + } + fn convert_path_separator<'a>( &self, os_str: Cow<'a, OsStr>, diff --git a/src/shims/unix/fs.rs b/src/shims/unix/fs.rs index 76c1709879..36be1ec4f6 100644 --- a/src/shims/unix/fs.rs +++ b/src/shims/unix/fs.rs @@ -1700,7 +1700,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx // 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())? + this.alloc_path_as_c_str(&resolved, MiriMemoryKind::C.into())? } else { let (wrote_path, _) = this.write_path_to_c_str(&resolved, processed_ptr, path_max)?; diff --git a/tests/pass/libc.rs b/tests/pass/libc.rs index 2735e5b25b..c7331b110e 100644 --- a/tests/pass/libc.rs +++ b/tests/pass/libc.rs @@ -23,7 +23,6 @@ fn tmp() -> PathBuf { 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; @@ -51,7 +50,6 @@ fn test_posix_realpath_alloc() { /// 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"); @@ -78,12 +76,8 @@ fn test_posix_realpath_noalloc() { /// 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"); @@ -92,48 +86,6 @@ fn test_posix_realpath_errors() { 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"))]