Implement fstatat
Linux's variant uses the syscall as intended. Redox's variant uses fpath to build a path to pass to fstat from the file descriptor plus the file name. Unlike the syscall, this isn't atomic so the liminal space between fpath/getcwd and fstat is subject to TOCTOU. Beyond fstatat, I moved the stat test to its correct location and added an assert since the output of the test is unchecked. I also added AT_FDCWD which seems to be -100 across Unixes. The other AT_* constants are unimplemented for now.
This commit is contained in:
committed by
Josh Megnauth
parent
92bc01247a
commit
c899feb774
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<c_int>;
|
||||
|
||||
@@ -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(())
|
||||
|
||||
+2
-1
@@ -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 \
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
#include <assert.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#include <errno.h>
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
#define _TEST_HELPERS
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
Reference in New Issue
Block a user