Skip to content

Commit 086c8ee

Browse files
committed
Add shim for realpath on unix
1 parent d09db16 commit 086c8ee

File tree

6 files changed

+223
-1
lines changed

6 files changed

+223
-1
lines changed

src/helpers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ 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"),

src/shims/unix/foreign_items.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
138138
let result = this.readlink(pathname, buf, bufsize)?;
139139
this.write_scalar(Scalar::from_machine_isize(result, this), dest)?;
140140
}
141+
"realpath" => {
142+
let [path, resolved_path] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
143+
let result = this.realpath(path, resolved_path)?;
144+
this.write_pointer(result, dest)?;
145+
}
141146

142147
// Time related shims
143148
"gettimeofday" => {

src/shims/unix/fs.rs

Lines changed: 62 additions & 0 deletions
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
};
@@ -1659,6 +1660,67 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
16591660
}
16601661
}
16611662
}
1663+
1664+
fn realpath(
1665+
&mut self,
1666+
path_op: &OpTy<'tcx, Tag>,
1667+
processed_path_op: &OpTy<'tcx, Tag>,
1668+
) -> InterpResult<'tcx, Pointer<Option<Tag>>> {
1669+
let this = self.eval_context_mut();
1670+
this.assert_target_os_is_unix("realpath");
1671+
1672+
let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
1673+
let processed_ptr = this.read_pointer(processed_path_op)?;
1674+
1675+
// Reject if isolation is enabled.
1676+
if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1677+
this.reject_in_isolation("`realpath`", reject_with)?;
1678+
let eacc = this.eval_libc("EACCES")?;
1679+
this.set_last_error(eacc)?;
1680+
return Ok(Pointer::null());
1681+
}
1682+
1683+
let result = std::fs::canonicalize(pathname);
1684+
match result {
1685+
Ok(resolved) => {
1686+
let path_max = this
1687+
.eval_libc_i32("PATH_MAX")?
1688+
.try_into()
1689+
.expect("PATH_MAX does not fit in u64");
1690+
let dest = if this.ptr_is_null(processed_ptr)? {
1691+
// POSIX says behavior when passing a null pointer is implementation-defined,
1692+
// but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer
1693+
// similarly to:
1694+
//
1695+
// "If resolved_path is specified as NULL, then realpath() uses
1696+
// malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold
1697+
// the resolved pathname, and returns a pointer to this buffer. The
1698+
// caller should deallocate this buffer using free(3)."
1699+
// <https://man7.org/linux/man-pages/man3/realpath.3.html>
1700+
this.alloc_os_str_as_c_str(resolved.as_os_str(), MiriMemoryKind::C.into())?
1701+
} else {
1702+
let (wrote_path, _) =
1703+
this.write_path_to_c_str(&resolved, processed_ptr, path_max)?;
1704+
1705+
if !wrote_path {
1706+
// Note that we do not explicitly handle `FILENAME_MAX`
1707+
// (different from `PATH_MAX` above) as it is Linux-specific and
1708+
// seems like a bit of a mess anyway: <https://eklitzke.org/path-max-is-tricky>.
1709+
let enametoolong = this.eval_libc("ENAMETOOLONG")?;
1710+
this.set_last_error(enametoolong)?;
1711+
return Ok(Pointer::null());
1712+
}
1713+
processed_ptr
1714+
};
1715+
1716+
Ok(dest)
1717+
}
1718+
Err(e) => {
1719+
this.set_last_error_from_io_error(e.kind())?;
1720+
Ok(Pointer::null())
1721+
}
1722+
}
1723+
}
16621724
}
16631725

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

src/shims/unix/macos/foreign_items.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
6767
let result = this.ftruncate64(fd, length)?;
6868
this.write_scalar(Scalar::from_i32(result), dest)?;
6969
}
70+
"realpath" | "realpath$DARWIN_EXTSN" => {
71+
let [path, resolved_path] =
72+
this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
73+
let result = this.realpath(path, resolved_path)?;
74+
this.write_pointer(result, dest)?;
75+
}
7076

7177
// Environment related shims
7278
"_NSGetEnviron" => {

tests/pass/fs.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ fn main() {
2525
test_errors();
2626
test_rename();
2727
test_directory();
28+
test_canonicalize();
2829
test_dup_stdout_stderr();
2930

3031
// These all require unix, if the test is changed to no longer `ignore-windows`, move these to a unix test
@@ -364,6 +365,24 @@ fn test_rename() {
364365
remove_file(&path2).unwrap();
365366
}
366367

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

tests/pass/libc.rs

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,142 @@
11
// ignore-windows: No libc on Windows
22
// compile-flags: -Zmiri-disable-isolation
33

4+
#![feature(io_error_more)]
45
#![feature(rustc_private)]
56

67
extern crate libc;
78

8-
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
99
fn tmp() -> std::path::PathBuf {
1010
std::env::var("MIRI_TEMP")
1111
.map(std::path::PathBuf::from)
1212
.unwrap_or_else(|_| std::env::temp_dir())
1313
}
1414

15+
/// Test allocating variant of `realpath`.
16+
fn test_posix_realpath_alloc() {
17+
use std::ffi::OsString;
18+
use std::ffi::{CStr, CString};
19+
use std::fs::{remove_file, File};
20+
use std::os::unix::ffi::OsStrExt;
21+
use std::os::unix::ffi::OsStringExt;
22+
use std::path::PathBuf;
23+
24+
let buf;
25+
let path = tmp().join("miri_test_libc_posix_realpath_alloc");
26+
let c_path = CString::new(path.as_os_str().as_bytes()).expect("CString::new failed");
27+
28+
// Cleanup before test.
29+
remove_file(&path).ok();
30+
// Create file.
31+
drop(File::create(&path).unwrap());
32+
unsafe {
33+
let r = libc::realpath(c_path.as_ptr(), std::ptr::null_mut());
34+
assert!(!r.is_null());
35+
buf = CStr::from_ptr(r).to_bytes().to_vec();
36+
libc::free(r as *mut _);
37+
}
38+
let canonical = PathBuf::from(OsString::from_vec(buf));
39+
// Cleanup after test.
40+
remove_file(&path).unwrap();
41+
42+
assert_eq!(path.file_name(), canonical.file_name());
43+
}
44+
45+
/// Test non-allocating variant of `realpath`.
46+
fn test_posix_realpath_noalloc() {
47+
use std::ffi::{CStr, CString};
48+
use std::fs::{remove_file, File};
49+
use std::os::unix::ffi::OsStrExt;
50+
use std::path::PathBuf;
51+
52+
let path = tmp().join("miri_test_libc_posix_realpath_noalloc");
53+
let c_path = CString::new(path.as_os_str().as_bytes()).expect("CString::new failed");
54+
55+
let mut v = vec![0; libc::PATH_MAX as usize];
56+
57+
// Cleanup before test.
58+
remove_file(&path).ok();
59+
// Create file.
60+
drop(File::create(&path).unwrap());
61+
unsafe {
62+
let r = libc::realpath(c_path.as_ptr(), v.as_mut_ptr());
63+
assert!(!r.is_null());
64+
}
65+
let c = unsafe { CStr::from_ptr(v.as_ptr()) };
66+
let canonical = PathBuf::from(c.to_str().expect("CStr to str"));
67+
// Cleanup after test.
68+
remove_file(&path).unwrap();
69+
70+
assert_eq!(path.file_name(), canonical.file_name());
71+
}
72+
73+
/// Test failure cases for `realpath`.
74+
fn test_posix_realpath_errors() {
75+
use std::convert::TryInto;
76+
use std::ffi::CString;
77+
use std::fs::{create_dir_all, remove_dir, remove_file};
78+
use std::io::ErrorKind;
79+
use std::os::unix::ffi::OsStrExt;
80+
use std::os::unix::fs::symlink;
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+
// Test that a long path returns an error.
91+
//
92+
// Linux first checks if the path to exists and macos does not.
93+
// Using an existing path ensures all platforms return `ENAMETOOLONG` given a long path.
94+
//
95+
// Rather than creating a bunch of directories, we create two directories containing symlinks.
96+
// Sadly we can't avoid creating directories via a path like "./././././" or"/../../" as linux
97+
// appears to collapse "." and ".." before checking path length.
98+
let path = tmp();
99+
100+
// The directories we will put symlinks in.
101+
let x = path.join("x/");
102+
let y = path.join("y/");
103+
104+
// The symlinks in each directory pointing to each other.
105+
let yx_sym = y.join("x");
106+
let xy_sym = x.join("y");
107+
108+
// Cleanup before test.
109+
remove_file(&yx_sym).ok();
110+
remove_file(&xy_sym).ok();
111+
remove_dir(&x).ok();
112+
remove_dir(&y).ok();
113+
114+
// Create directories.
115+
create_dir_all(&x).expect("dir x");
116+
create_dir_all(&y).expect("dir y");
117+
118+
// Create symlinks between directories.
119+
symlink(&x, &yx_sym).expect("symlink x");
120+
symlink(&y, &xy_sym).expect("symlink y ");
121+
122+
// This pass exists due to the symlinks created above but is too long.
123+
let too_long = path.join("x/y/".repeat(libc::PATH_MAX.try_into().unwrap()));
124+
125+
let c_path = CString::new(too_long.into_os_string().as_bytes()).expect("CString::new failed");
126+
let r = unsafe { libc::realpath(c_path.as_ptr(), std::ptr::null_mut()) };
127+
let e = std::io::Error::last_os_error();
128+
129+
// Cleanup after test.
130+
remove_file(&yx_sym).ok();
131+
remove_file(&xy_sym).ok();
132+
remove_dir(&x).ok();
133+
remove_dir(&y).ok();
134+
135+
assert!(r.is_null());
136+
assert_eq!(e.raw_os_error(), Some(libc::ENAMETOOLONG));
137+
assert_eq!(e.kind(), ErrorKind::InvalidFilename);
138+
}
139+
15140
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
16141
fn test_posix_fadvise() {
17142
use std::convert::TryInto;
@@ -317,6 +442,10 @@ fn main() {
317442

318443
test_posix_gettimeofday();
319444

445+
test_posix_realpath_alloc();
446+
test_posix_realpath_noalloc();
447+
test_posix_realpath_errors();
448+
320449
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
321450
test_sync_file_range();
322451

0 commit comments

Comments
 (0)