diff --git a/src/header/fcntl/linux.rs b/src/header/fcntl/linux.rs index 0f151a5805..f7838e4a8c 100644 --- a/src/header/fcntl/linux.rs +++ b/src/header/fcntl/linux.rs @@ -19,3 +19,9 @@ pub const FD_CLOEXEC: c_int = 0x8_0000; // Defined for compatibility pub const O_NDELAY: c_int = O_NONBLOCK; + +// Flags for capability based "at" functions +pub const AT_FDCWD: c_int = -100; +pub const AT_SYMLINK_NOFOLLOW: c_int = 0x100; +pub const AT_REMOVEDIR: c_int = 0x200; +pub const AT_EMPTY_PATH: c_int = 0x1000; diff --git a/src/header/fcntl/redox.rs b/src/header/fcntl/redox.rs index ac15f21005..f7ff4db45c 100644 --- a/src/header/fcntl/redox.rs +++ b/src/header/fcntl/redox.rs @@ -27,3 +27,6 @@ pub const O_NOCTTY: c_int = 0x00000200; // Defined for compatibility pub const O_NDELAY: c_int = O_NONBLOCK; + +// Flags for capability based "at" functions +pub const AT_FDCWD: c_int = -100; diff --git a/src/header/sys_stat/mod.rs b/src/header/sys_stat/mod.rs index 916e494d7a..2ff4cdfa90 100644 --- a/src/header/sys_stat/mod.rs +++ b/src/header/sys_stat/mod.rs @@ -86,6 +86,18 @@ pub unsafe extern "C" fn fstat(fildes: c_int, buf: *mut stat) -> c_int { Sys::fstat(fildes, buf).map(|()| 0).or_minus_one_errno() } +#[no_mangle] +pub unsafe extern "C" fn fstatat( + fildes: c_int, + path: *const c_char, + buf: *mut stat, + flags: c_int, +) -> c_int { + Sys::fstatat(fildes, path, buf, flags) + .map(|()| 0) + .or_minus_one_errno() +} + #[no_mangle] pub unsafe extern "C" fn __fxstat(_ver: c_int, fildes: c_int, buf: *mut stat) -> c_int { fstat(fildes, buf) diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 93c6614edc..c4abb5baad 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -7,6 +7,7 @@ use crate::{ header::{ dirent::dirent, errno::EINVAL, + fcntl::{AT_EMPTY_PATH, AT_FDCWD, AT_REMOVEDIR, AT_SYMLINK_NOFOLLOW}, signal::SIGCHLD, sys_resource::{rlimit, rusage}, sys_stat::{stat, S_IFIFO}, @@ -26,10 +27,6 @@ mod ptrace; mod signal; mod socket; -const AT_FDCWD: c_int = -100; -const AT_EMPTY_PATH: c_int = 0x1000; -const AT_REMOVEDIR: c_int = 0x200; - const SYS_CLONE: usize = 56; const CLONE_VM: usize = 0x0100; const CLONE_FS: usize = 0x0200; @@ -185,6 +182,15 @@ impl Pal for Sys { e_raw(unsafe { syscall!(NEWFSTATAT, fildes, empty_ptr, buf, AT_EMPTY_PATH) }).map(|_| ()) } + unsafe fn fstatat( + fildes: c_int, + path: *const c_char, + buf: *mut stat, + flags: c_int, + ) -> Result<()> { + e_raw(unsafe { syscall!(NEWFSTATAT, fildes, path, buf, flags) }).map(|_| ()) + } + unsafe fn fstatvfs(fildes: c_int, buf: *mut statvfs) -> Result<()> { let mut kbuf = linux_statfs::default(); let kbuf_ptr = &mut kbuf as *mut linux_statfs; diff --git a/src/platform/pal/mod.rs b/src/platform/pal/mod.rs index f27cc635a8..a18fcb6bdb 100644 --- a/src/platform/pal/mod.rs +++ b/src/platform/pal/mod.rs @@ -74,6 +74,13 @@ pub trait Pal { unsafe fn fstat(fildes: c_int, buf: *mut stat) -> Result<()>; + unsafe fn fstatat( + fildes: c_int, + path: *const c_char, + buf: *mut stat, + flags: c_int, + ) -> Result<()>; + unsafe fn fstatvfs(fildes: c_int, buf: *mut statvfs) -> Result<()>; fn fcntl(fildes: c_int, cmd: c_int, arg: c_ulonglong) -> Result; diff --git a/src/platform/redox/mod.rs b/src/platform/redox/mod.rs index e1dc1820db..51f9d1bf31 100644 --- a/src/platform/redox/mod.rs +++ b/src/platform/redox/mod.rs @@ -25,7 +25,8 @@ use crate::{ EBADF, EBADFD, EBADR, EINTR, EINVAL, EIO, ENAMETOOLONG, ENOENT, ENOMEM, ENOSYS, EOPNOTSUPP, EPERM, ERANGE, }, - fcntl, limits, + fcntl::{self, AT_FDCWD, O_RDONLY}, + limits, sys_mman::{MAP_ANONYMOUS, MAP_FAILED, PROT_READ, PROT_WRITE}, sys_random, sys_resource::{rlimit, rusage, RLIM_INFINITY}, @@ -283,6 +284,56 @@ impl Pal for Sys { Ok(()) } + unsafe fn fstatat( + fildes: c_int, + path: *const c_char, + buf: *mut stat, + flags: c_int, + ) -> Result<()> { + let path = + match CStr::from_nullable_ptr(path).and_then(|cs| str::from_utf8(cs.to_bytes()).ok()) { + // TODO: AT_EMPTY_PATH + Some(path) if !path.is_empty() => Ok(path), + None | Some(_) => Err(Errno(ENOENT)), + }?; + // TODO: We need an AT_SYMLINK_NOFOLLOW constant for Redox. Unlike AT_FDCWD, it varies + // across OSes, so I don't want to define it here myself. + // O_NOFOLLOW may be incorrect too (see the rename.c unit test). + // let oflags = if flags & AT_SYMLINK_NOFOLLOW { + // O_RDONLY | O_NOFOLLOW + // } else { + // O_RDONLY + // }; + let oflags = O_RDONLY; + + // Absolute paths are passed to fstat without processing. + // canonicalize_using_cwd checks that path is absolute so a third branch that does so here + // isn't needed. + let path = if fildes == AT_FDCWD { + // The special constant AT_FDCWD indicates that we should use the cwd. + let mut buf = [0; limits::PATH_MAX]; + let len = path::getcwd(&mut buf).ok_or(Errno(ENAMETOOLONG))?; + // SAFETY: Redox's cwd is stored as a str. + let cwd = unsafe { str::from_utf8_unchecked(&buf[..len]) }; + + path::canonicalize_using_cwd(Some(cwd), path).ok_or(Errno(EBADF))? + } else { + let mut buf = [0; limits::PATH_MAX]; + let len = Sys::fpath(fildes, &mut buf)?; + // SAFETY: fpath checks then copies valid UTF8. + let dir = unsafe { str::from_utf8_unchecked(&buf[..len]) }; + + path::canonicalize_using_cwd(Some(dir), path).ok_or(Errno(EBADF))? + }; + let path = CString::new(path).map_err(|_| Errno(ENOENT))?; + + // TODO: + // * If AT_SYMLINK_NOFOLLOW is set, call lstat instead. + // * Switch open to openat. + let file = File::open(path.as_c_str().into(), oflags)?; + Sys::fstat(*file, buf) + } + unsafe fn fstatvfs(fildes: c_int, buf: *mut statvfs) -> Result<()> { libredox::fstatvfs(fildes as usize, buf)?; Ok(()) diff --git a/tests/Makefile b/tests/Makefile index 02f8fd4cec..912949c35a 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -108,6 +108,7 @@ EXPECT_NAMES=\ string/stpcpy \ string/stpncpy \ strings \ + sys_stat/fstatat \ sys_syslog/syslog \ time/asctime \ time/constants \ @@ -250,6 +251,7 @@ NAMES=\ stdlib/realpath \ sys_epoll/epoll \ sys_resource/constants \ + sys_stat/stat \ sys_statvfs/statvfs \ sys_utsname/uname \ time/gettimeofday \ @@ -262,7 +264,6 @@ NAMES=\ unistd/link \ unistd/pathconf \ unistd/setid \ - unistd/stat \ unistd/sysconf \ pthread/main \ pthread/cleanup \ diff --git a/tests/expected/bins_dynamic/sys_stat/fstatat.stderr b/tests/expected/bins_dynamic/sys_stat/fstatat.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_dynamic/sys_stat/fstatat.stdout b/tests/expected/bins_dynamic/sys_stat/fstatat.stdout new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_static/sys_stat/fstatat.stderr b/tests/expected/bins_static/sys_stat/fstatat.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_static/sys_stat/fstatat.stdout b/tests/expected/bins_static/sys_stat/fstatat.stdout new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/sys_stat/fstatat.c b/tests/sys_stat/fstatat.c new file mode 100644 index 0000000000..99a6a118c2 --- /dev/null +++ b/tests/sys_stat/fstatat.c @@ -0,0 +1,283 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +// Create file `dir/name` and fill it with `contents`. +__attribute__((nonnull)) +static const char* mktestfile( + const char dir[], + const char name[], + const char contents[] +) { + size_t dir_len = strlen(dir); + size_t name_len = strlen(name); + + // dir + / name + \0 + char* path = malloc(dir_len + name_len + 2); + if (!path) { + perror("malloc"); + return NULL; + } + // memcpy is faster/recommended but I'm lazy. + strcpy(path, dir); + strcat(path, "/"); + strcat(path, name); + + int fd = creat(path, 0700); + if (fd < 0) { + perror("creat"); + free(path); + return NULL; + } + + size_t contents_len = strlen(contents); + // Assumes that the handful of bytes of contents are fully written. + if (write(fd, contents, contents_len) < 0) { + perror("write"); + free(path); + close(fd); + return NULL; + } + + close(fd); + return path; +} + +// Create a link, `dir/name`, to `target` and return its path. +__attribute__((nonnull)) +static const char* mktestlink( + const char dir[], + const char name[], + const char target[] +) { + char* path = malloc(strlen(dir) + strlen(name) + 2); + if (!path) { + perror("malloc"); + return NULL; + } + + strcpy(path, dir); + strcat(path, "/"); + strcat(path, name); + + if (link(target, path) == -1) { + perror("link"); + free(path); + return NULL; + } + + return path; +} + +__attribute__((nonnull)) +static int run_test( + int fd, + int flags, + const char name[], + const char path[], + size_t expected +) { + struct stat stat = {0}; + + if (fstatat(fd, name, &stat, flags) == -1) { + perror("fstatat"); + return -1; + } + if ((size_t) stat.st_size != expected) { + fprintf( + stderr, + "fstatat invalid stats\n\tFile: %s\n\tExpected: %zu Actual: %ld\n", + path, + expected, + stat.st_size + ); + return -1; + } + + return 0; +} + +int main(void) { + int status = EXIT_FAILURE; + + char dir_template_A[] = "/tmp/fsatest.XXXXXXX"; + char* dir_A = mkdtemp(dir_template_A); + if (!dir_A) { + perror("mkdtemp (dir A)"); + goto bye; + } + + char dir_template_B[] = "/tmp/fsatest.XXXXXXX"; + char* dir_B = mkdtemp(dir_template_B); + if (!dir_B) { + perror("mkdtemp (dir B)"); + goto clean_dir_a; + } + + // File in directory A + const char name[] = "file"; + const char cont_A[] = "Flying Dutchman"; + const char* file_A = mktestfile(dir_A, name, cont_A); + if (!file_A) { + goto clean_dir_b; + } + // Link from directory A to file A + const char link_A_name[] = "alink"; + const char* link_A = mktestlink(dir_A, link_A_name, file_A); + if (!link_A) { + goto clean_file_a; + } + + // File in directory B + const char cont_B[] = "Every Villain is Lemons"; + const char* file_B = mktestfile(dir_B, name, cont_B); + if (!file_B) { + goto unlink_link_A; + } + const char link_B_name[] = "blink"; + // Link from directory A to file B in directory B + const char* link_B = mktestlink(dir_A, link_B_name, file_B); + if (!link_B) { + goto unlink_file_B; + } + + // TESTS + // The size of the file is used as a proxy for checking that stat worked. + int dir_a_fd = open(dir_A, O_DIRECTORY); + if (dir_a_fd == -1) { + perror("open (dir A)"); + goto unlink_link_B; + } + + // fstatat works (basic) + size_t len_cont_A = strlen(cont_A); + if (run_test(dir_a_fd, 0, name, file_A, len_cont_A) == -1) { + goto close_dir_a_fd; + } + + // fstatat follows symlinks (same dir) + if (run_test(dir_a_fd, 0, link_A_name, link_A, len_cont_A) == -1) { + fprintf(stderr, "Context: link %s -> %s\n", link_A, file_A); + goto close_dir_a_fd; + } + + // fstatat follows symlinks (to diff dir) + size_t len_cont_B = strlen(cont_B); + if (run_test(dir_a_fd, 0, link_B_name, link_B, len_cont_B) == -1) { + fprintf(stderr, "Context: link %s -> %s\n", link_B, file_B); + goto close_dir_a_fd; + } + + // TODO: AT_SYMLINK_NOFOLLOW (no Redox support) + + // TODO: O_SEARCH (no Redox support) + + // AT_FDCWD + char old_cwd[PATH_MAX] = {0}; + if (!getcwd(old_cwd, PATH_MAX)) { + perror("getcwd"); + goto close_dir_a_fd; + } + if (chdir(dir_A) == -1) { + perror("chdir"); + goto close_dir_a_fd; + } + if (run_test(AT_FDCWD, 0, name, "./", len_cont_A) == -1) { + fputs("Context: AT_FDCWD\n", stderr); + goto close_dir_a_fd; + } + if (chdir(old_cwd) == -1) { + perror("chdir"); + goto close_dir_a_fd; + } + + // Absolute path + if (run_test(dir_a_fd, 0, file_A, file_A, len_cont_A) == -1) { + fprintf(stderr, "Context: absolute path %s\n", file_A); + goto close_dir_a_fd; + } + + // Relative path that traverses dir boundaries + // The path isn't resolved beneath the directory but rather resolved + // relative to it. Directory traversal is allowed if AT_RESOLVE_BENEATH + // isn't used. + const char nested_name[] = "nested"; + char* nested_dir = malloc(strlen(dir_A) + strlen(nested_name) + 2); + if (!nested_dir) { + perror("malloc"); + goto close_dir_a_fd; + } + strcpy(nested_dir, dir_A); + strcat(nested_dir, "/"); + strcat(nested_dir, nested_name); + if (mkdir(nested_dir, 0700) == -1) { + perror("mkdir"); + goto clean_nested_dir; + } + + int nested_fd = open(nested_dir, O_DIRECTORY); + if (nested_fd < 0) { + perror("open"); + goto remove_nested_dir; + } + char rel_nested[sizeof(name) + 5] = "../"; + strcat(rel_nested, name); + if (run_test(nested_fd, 0, rel_nested, rel_nested, len_cont_A) == -1) { + fprintf( + stderr, + "Context: relative path from %s to ../%s\n", + nested_dir, + name + ); + goto close_nested_dir; + } + + // TODO: AT_RESOLVE_BENEATH + + // TODO: AT_EMPTY_PATH + + // TODO: Swapped directories resolves correctly + + // Failure conditions: + // Empty path + struct stat stat = {0}; + if (fstatat(dir_a_fd, "", &stat, 0) == 0) { + fputs("Context: empty path should fail\n", stderr); + goto close_dir_a_fd; + } + + // Clean up is LIFO where the last created resource is the first cleaned. + status = EXIT_SUCCESS; +close_nested_dir: + close(nested_fd); +remove_nested_dir: + rmdir(nested_dir); +clean_nested_dir: + free((void*) nested_dir); +close_dir_a_fd: + close(dir_a_fd); +unlink_link_B: + unlink(link_B); + free((void*) link_B); +unlink_file_B: + unlink(file_B); + free((void*) file_B); +unlink_link_A: + unlink(link_A); + free((void*) link_A); +clean_file_a: + unlink(file_A); + free((void*) file_A); +clean_dir_b: + rmdir(dir_B); +clean_dir_a: + rmdir(dir_A); +bye: + return status; +} diff --git a/tests/unistd/stat.c b/tests/sys_stat/stat.c similarity index 76% rename from tests/unistd/stat.c rename to tests/sys_stat/stat.c index be856646bd..95fadb1751 100644 --- a/tests/unistd/stat.c +++ b/tests/sys_stat/stat.c @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -9,9 +9,9 @@ int main(void) { printf("sizeof(struct stat): %ld\n", sizeof(struct stat)); - struct stat buf; + struct stat buf = {0}; - int stat_status = stat("unistd/stat.c", &buf); + int stat_status = stat("sys_stat/stat.c", &buf); ERROR_IF(stat, stat_status, == -1); UNEXP_IF(stat, stat_status, != 0); @@ -23,4 +23,9 @@ int main(void) { printf("st_nlink: %lu\n", buf.st_nlink); printf("st_uid: %u\n", buf.st_uid); printf("st_gid: %u\n", buf.st_gid); + + assert(buf.st_size > 600); + assert(buf.st_nlink == 1); + + return EXIT_SUCCESS; } diff --git a/tests/test_helpers.h b/tests/test_helpers.h index 35c1eb465e..de4a360951 100644 --- a/tests/test_helpers.h +++ b/tests/test_helpers.h @@ -2,6 +2,7 @@ #define _TEST_HELPERS #include +#include #include #include #include