Skip to content

Commit 9f4612a

Browse files
committed
Auto merge of rust-lang#2457 - RalfJung:realpath, r=RalfJung
Add shim for realpath on unix Salvaged from rust-lang/miri#2294 by `@LegNeato`
2 parents 25df001 + 8356f4c commit 9f4612a

File tree

7 files changed

+190
-2
lines changed

7 files changed

+190
-2
lines changed

src/helpers.rs

+2
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@ const UNIX_IO_ERROR_TABLE: &[(std::io::ErrorKind, &str)] = {
3737
(NotFound, "ENOENT"),
3838
(Interrupted, "EINTR"),
3939
(InvalidInput, "EINVAL"),
40+
(InvalidFilename, "ENAMETOOLONG"),
4041
(TimedOut, "ETIMEDOUT"),
4142
(AlreadyExists, "EEXIST"),
4243
(WouldBlock, "EWOULDBLOCK"),
4344
(DirectoryNotEmpty, "ENOTEMPTY"),
45+
(FilesystemLoop, "ELOOP"),
4446
]
4547
};
4648

src/shims/os_str.rs

+13
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,19 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
250250
this.write_os_str_to_wide_str(&os_str, ptr, size)
251251
}
252252

253+
/// Allocate enough memory to store a Path as a null-terminated sequence of bytes,
254+
/// adjusting path separators if needed.
255+
fn alloc_path_as_c_str(
256+
&mut self,
257+
path: &Path,
258+
memkind: MemoryKind<MiriMemoryKind>,
259+
) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
260+
let this = self.eval_context_mut();
261+
let os_str = this
262+
.convert_path_separator(Cow::Borrowed(path.as_os_str()), PathConversion::HostToTarget);
263+
this.alloc_os_str_as_c_str(&os_str, memkind)
264+
}
265+
253266
fn convert_path_separator<'a>(
254267
&self,
255268
os_str: Cow<'a, OsStr>,

src/shims/unix/foreign_items.rs

+5
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
161161
// fadvise is only informational, we can ignore it.
162162
this.write_null(dest)?;
163163
}
164+
"realpath" => {
165+
let [path, resolved_path] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
166+
let result = this.realpath(path, resolved_path)?;
167+
this.write_pointer(result, dest)?;
168+
}
164169

165170
// Time related shims
166171
"gettimeofday" => {

src/shims/unix/fs.rs

+62
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::borrow::Cow;
22
use std::collections::BTreeMap;
3+
use std::convert::TryInto;
34
use std::fs::{
45
read_dir, remove_dir, remove_file, rename, DirBuilder, File, FileType, OpenOptions, ReadDir,
56
};
@@ -1662,6 +1663,67 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
16621663
this.set_last_error(enotty)?;
16631664
Ok(0)
16641665
}
1666+
1667+
fn realpath(
1668+
&mut self,
1669+
path_op: &OpTy<'tcx, Provenance>,
1670+
processed_path_op: &OpTy<'tcx, Provenance>,
1671+
) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
1672+
let this = self.eval_context_mut();
1673+
this.assert_target_os_is_unix("realpath");
1674+
1675+
let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
1676+
let processed_ptr = this.read_pointer(processed_path_op)?;
1677+
1678+
// Reject if isolation is enabled.
1679+
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1680+
this.reject_in_isolation("`realpath`", reject_with)?;
1681+
let eacc = this.eval_libc("EACCES")?;
1682+
this.set_last_error(eacc)?;
1683+
return Ok(Pointer::null());
1684+
}
1685+
1686+
let result = std::fs::canonicalize(pathname);
1687+
match result {
1688+
Ok(resolved) => {
1689+
let path_max = this
1690+
.eval_libc_i32("PATH_MAX")?
1691+
.try_into()
1692+
.expect("PATH_MAX does not fit in u64");
1693+
let dest = if this.ptr_is_null(processed_ptr)? {
1694+
// POSIX says behavior when passing a null pointer is implementation-defined,
1695+
// but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer
1696+
// similarly to:
1697+
//
1698+
// "If resolved_path is specified as NULL, then realpath() uses
1699+
// malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold
1700+
// the resolved pathname, and returns a pointer to this buffer. The
1701+
// caller should deallocate this buffer using free(3)."
1702+
// <https://man7.org/linux/man-pages/man3/realpath.3.html>
1703+
this.alloc_path_as_c_str(&resolved, MiriMemoryKind::C.into())?
1704+
} else {
1705+
let (wrote_path, _) =
1706+
this.write_path_to_c_str(&resolved, processed_ptr, path_max)?;
1707+
1708+
if !wrote_path {
1709+
// Note that we do not explicitly handle `FILENAME_MAX`
1710+
// (different from `PATH_MAX` above) as it is Linux-specific and
1711+
// seems like a bit of a mess anyway: <https://eklitzke.org/path-max-is-tricky>.
1712+
let enametoolong = this.eval_libc("ENAMETOOLONG")?;
1713+
this.set_last_error(enametoolong)?;
1714+
return Ok(Pointer::null());
1715+
}
1716+
processed_ptr
1717+
};
1718+
1719+
Ok(dest)
1720+
}
1721+
Err(e) => {
1722+
this.set_last_error_from_io_error(e.kind())?;
1723+
Ok(Pointer::null())
1724+
}
1725+
}
1726+
}
16651727
}
16661728

16671729
/// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when

src/shims/unix/macos/foreign_items.rs

+6
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
7373
let result = this.ftruncate64(fd, length)?;
7474
this.write_scalar(Scalar::from_i32(result), dest)?;
7575
}
76+
"realpath$DARWIN_EXTSN" => {
77+
let [path, resolved_path] =
78+
this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
79+
let result = this.realpath(path, resolved_path)?;
80+
this.write_pointer(result, dest)?;
81+
}
7682

7783
// Environment related shims
7884
"_NSGetEnviron" => {

tests/pass/fs.rs

+19
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ fn main() {
2424
test_errors();
2525
test_rename();
2626
test_directory();
27+
test_canonicalize();
2728
test_dup_stdout_stderr();
2829

2930
// 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() {
365366
remove_file(&path2).unwrap();
366367
}
367368

369+
fn test_canonicalize() {
370+
use std::fs::canonicalize;
371+
let dir_path = prepare_dir("miri_test_fs_dir");
372+
create_dir(&dir_path).unwrap();
373+
let path = dir_path.join("test_file");
374+
drop(File::create(&path).unwrap());
375+
376+
let p = canonicalize(format!("{}/./test_file", dir_path.to_string_lossy())).unwrap();
377+
assert_eq!(p.to_string_lossy().find('.'), None);
378+
379+
remove_dir_all(&dir_path).unwrap();
380+
381+
// Make sure we get an error for long paths.
382+
use std::convert::TryInto;
383+
let too_long = "x/".repeat(libc::PATH_MAX.try_into().unwrap());
384+
assert!(canonicalize(too_long).is_err());
385+
}
386+
368387
fn test_directory() {
369388
let dir_path = prepare_dir("miri_test_fs_dir");
370389
// Creating a directory should succeed.

tests/pass/libc.rs

+83-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,93 @@
11
//@ignore-target-windows: No libc on Windows
22
//@compile-flags: -Zmiri-disable-isolation
3+
#![feature(io_error_more)]
34
#![feature(rustc_private)]
45

56
use std::fs::{remove_file, File};
67
use std::os::unix::io::AsRawFd;
8+
use std::path::PathBuf;
79

8-
fn tmp() -> std::path::PathBuf {
10+
fn tmp() -> PathBuf {
911
std::env::var("MIRI_TEMP")
10-
.map(std::path::PathBuf::from)
12+
.map(|tmp| {
13+
// MIRI_TEMP is set outside of our emulated
14+
// program, so it may have path separators that don't
15+
// correspond to our target platform. We normalize them here
16+
// before constructing a `PathBuf`
17+
return PathBuf::from(tmp.replace("\\", "/"));
18+
})
1119
.unwrap_or_else(|_| std::env::temp_dir())
1220
}
1321

22+
/// Test allocating variant of `realpath`.
23+
fn test_posix_realpath_alloc() {
24+
use std::ffi::OsString;
25+
use std::ffi::{CStr, CString};
26+
use std::os::unix::ffi::OsStrExt;
27+
use std::os::unix::ffi::OsStringExt;
28+
29+
let buf;
30+
let path = tmp().join("miri_test_libc_posix_realpath_alloc");
31+
let c_path = CString::new(path.as_os_str().as_bytes()).expect("CString::new failed");
32+
33+
// Cleanup before test.
34+
remove_file(&path).ok();
35+
// Create file.
36+
drop(File::create(&path).unwrap());
37+
unsafe {
38+
let r = libc::realpath(c_path.as_ptr(), std::ptr::null_mut());
39+
assert!(!r.is_null());
40+
buf = CStr::from_ptr(r).to_bytes().to_vec();
41+
libc::free(r as *mut _);
42+
}
43+
let canonical = PathBuf::from(OsString::from_vec(buf));
44+
assert_eq!(path.file_name(), canonical.file_name());
45+
46+
// Cleanup after test.
47+
remove_file(&path).unwrap();
48+
}
49+
50+
/// Test non-allocating variant of `realpath`.
51+
fn test_posix_realpath_noalloc() {
52+
use std::ffi::{CStr, CString};
53+
use std::os::unix::ffi::OsStrExt;
54+
55+
let path = tmp().join("miri_test_libc_posix_realpath_noalloc");
56+
let c_path = CString::new(path.as_os_str().as_bytes()).expect("CString::new failed");
57+
58+
let mut v = vec![0; libc::PATH_MAX as usize];
59+
60+
// Cleanup before test.
61+
remove_file(&path).ok();
62+
// Create file.
63+
drop(File::create(&path).unwrap());
64+
unsafe {
65+
let r = libc::realpath(c_path.as_ptr(), v.as_mut_ptr());
66+
assert!(!r.is_null());
67+
}
68+
let c = unsafe { CStr::from_ptr(v.as_ptr()) };
69+
let canonical = PathBuf::from(c.to_str().expect("CStr to str"));
70+
71+
assert_eq!(path.file_name(), canonical.file_name());
72+
73+
// Cleanup after test.
74+
remove_file(&path).unwrap();
75+
}
76+
77+
/// Test failure cases for `realpath`.
78+
fn test_posix_realpath_errors() {
79+
use std::ffi::CString;
80+
use std::io::ErrorKind;
81+
82+
// Test non-existent path returns an error.
83+
let c_path = CString::new("./nothing_to_see_here").expect("CString::new failed");
84+
let r = unsafe { libc::realpath(c_path.as_ptr(), std::ptr::null_mut()) };
85+
assert!(r.is_null());
86+
let e = std::io::Error::last_os_error();
87+
assert_eq!(e.raw_os_error(), Some(libc::ENOENT));
88+
assert_eq!(e.kind(), ErrorKind::NotFound);
89+
}
90+
1491
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
1592
fn test_posix_fadvise() {
1693
use std::convert::TryInto;
@@ -336,6 +413,10 @@ fn main() {
336413

337414
test_posix_gettimeofday();
338415

416+
test_posix_realpath_alloc();
417+
test_posix_realpath_noalloc();
418+
test_posix_realpath_errors();
419+
339420
#[cfg(any(target_os = "linux"))]
340421
test_sync_file_range();
341422

0 commit comments

Comments
 (0)