diff --git a/src/header/unistd/mod.rs b/src/header/unistd/mod.rs index 2ba89019ff..46617a3b2e 100644 --- a/src/header/unistd/mod.rs +++ b/src/header/unistd/mod.rs @@ -14,7 +14,8 @@ use crate::{ error::{Errno, ResultExt}, header::{ crypt::{crypt_data, crypt_r}, - errno, fcntl, limits, + errno::{self, ENAMETOOLONG}, + fcntl, limits, stdlib::getenv, sys_ioctl, sys_resource, sys_time, sys_utsname, termios, time::timespec, @@ -835,6 +836,25 @@ pub unsafe extern "C" fn readlink( .or_minus_one_errno() } +/// See . +#[no_mangle] +pub unsafe extern "C" fn readlinkat( + dirfd: c_int, + pathname: *const c_char, + buf: *mut c_char, + len: size_t, +) -> ssize_t { + let pathname = CStr::from_ptr(pathname); + let mut buf = slice::from_raw_parts_mut(buf.cast(), len); + Sys::readlinkat(dirfd, pathname, &mut buf) + .map(|read| { + read.try_into() + .map_err(|_| Errno(ENAMETOOLONG)) + .or_minus_one_errno() + }) + .or_minus_one_errno() +} + /// See . #[no_mangle] pub unsafe extern "C" fn rmdir(path: *const c_char) -> c_int { diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index c4abb5baad..1a2c538e6a 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -585,6 +585,18 @@ impl Pal for Sys { }) } + fn readlinkat(dirfd: c_int, pathname: CStr, out: &mut [u8]) -> Result { + e_raw(unsafe { + syscall!( + READLINKAT, + dirfd, + pathname.as_ptr(), + out.as_mut_ptr(), + out.len() + ) + }) + } + fn rename(old: CStr, new: CStr) -> Result<()> { e_raw(unsafe { syscall!(RENAMEAT, AT_FDCWD, old.as_ptr(), AT_FDCWD, new.as_ptr()) }) .map(|_| ()) diff --git a/src/platform/pal/mod.rs b/src/platform/pal/mod.rs index a18fcb6bdb..f0717374fc 100644 --- a/src/platform/pal/mod.rs +++ b/src/platform/pal/mod.rs @@ -228,6 +228,8 @@ pub trait Pal { fn readlink(pathname: CStr, out: &mut [u8]) -> Result; + fn readlinkat(dirfd: c_int, pathname: CStr, out: &mut [u8]) -> Result; + fn rename(old: CStr, new: CStr) -> Result<()>; fn rmdir(path: CStr) -> Result<()>; diff --git a/src/platform/redox/mod.rs b/src/platform/redox/mod.rs index b83431b2ef..01d4f9f576 100644 --- a/src/platform/redox/mod.rs +++ b/src/platform/redox/mod.rs @@ -25,8 +25,7 @@ use crate::{ EBADF, EBADFD, EBADR, EINTR, EINVAL, EIO, ENAMETOOLONG, ENOENT, ENOMEM, ENOSYS, EOPNOTSUPP, EPERM, ERANGE, }, - fcntl::{self, AT_EMPTY_PATH, AT_FDCWD, AT_SYMLINK_NOFOLLOW, O_NOFOLLOW, O_PATH, O_RDONLY}, - limits, + fcntl, limits, sys_mman::{MAP_ANONYMOUS, MAP_FAILED, PROT_READ, PROT_WRITE}, sys_random, sys_resource::{rlimit, rusage, RLIM_INFINITY}, @@ -86,7 +85,10 @@ macro_rules! path_from_c_str { }}; } -use self::{exec::Executable, path::canonicalize}; +use self::{ + exec::Executable, + path::{canonicalize, cap_path_at}, +}; static CLONE_LOCK: RwLock<()> = RwLock::new(()); @@ -285,48 +287,15 @@ impl Pal for Sys { } unsafe fn fstatat( - fildes: c_int, + dirfd: 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()) { - Some(path) if !path.is_empty() => Ok(path), - _ if flags & AT_EMPTY_PATH == AT_EMPTY_PATH => return Sys::fstat(fildes, buf), - _ => Err(Errno(ENOENT)), - }?; - - let oflags = if flags & AT_SYMLINK_NOFOLLOW == AT_SYMLINK_NOFOLLOW { - O_RDONLY | O_PATH | O_NOFOLLOW - } else { - 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: - // * Switch open to openat. - let file = File::open(path.as_c_str().into(), oflags)?; + let path = CStr::from_nullable_ptr(path) + .and_then(|cs| str::from_utf8(cs.to_bytes()).ok()) + .ok_or(Errno(ENOENT))?; + let file = cap_path_at(dirfd, path, flags, 0)?; Sys::fstat(*file, buf) } @@ -568,7 +537,7 @@ impl Pal for Sys { } unsafe fn setrlimit(resource: c_int, rlim: *const rlimit) -> Result<()> { - //TOOD + // TODO eprintln!( "relibc setrlimit({}, {:p}): not implemented", resource, rlim @@ -900,6 +869,12 @@ impl Pal for Sys { Self::read(*file, out) } + fn readlinkat(dirfd: c_int, path: CStr, out: &mut [u8]) -> Result { + let path = str::from_utf8(path.to_bytes()).map_err(|_| Errno(ENOENT))?; + let file = cap_path_at(dirfd, path, 0, fcntl::O_SYMLINK)?; + Sys::read(*file, out) + } + fn rename(oldpath: CStr, newpath: CStr) -> Result<()> { let newpath = newpath.to_str().map_err(|_| Errno(EINVAL))?; let newpath = canonicalize(newpath).map_err(|_| Errno(EINVAL))?; diff --git a/src/platform/redox/path.rs b/src/platform/redox/path.rs index 65a4fbdc5f..3f7b4ca437 100644 --- a/src/platform/redox/path.rs +++ b/src/platform/redox/path.rs @@ -1,14 +1,21 @@ use alloc::{ borrow::ToOwned, boxed::Box, + ffi::CString, string::{String, ToString}, vec::Vec, }; +use core::{ffi::c_int, str}; use redox_rt::signal::tmp_disable_signals; use syscall::{data::Stat, error::*, flag::*}; -use super::{libcscheme, FdGuard}; -use crate::sync::Mutex; +use super::{libcscheme, FdGuard, Pal, Sys}; +use crate::{ + error::Errno, + fs::File, + header::{fcntl, limits}, + sync::Mutex, +}; pub use redox_path::{canonicalize_using_cwd, RedoxPath}; @@ -205,3 +212,48 @@ fn get_parent_path(path: &str) -> Option<&str> { } }) } + +// TODO: Fold into openat or something. +pub fn cap_path_at( + dirfd: c_int, + path: &str, + at_flags: c_int, + oflags: c_int, +) -> Result { + // Ideally, the function calling this fn would check AT_EMPTY_PATH and just call fstat or + // whatever with the fd. + if path.is_empty() && at_flags & fcntl::AT_EMPTY_PATH != fcntl::AT_EMPTY_PATH { + return Err(Errno(ENOENT)); + } + + let oflags = if at_flags & fcntl::AT_SYMLINK_NOFOLLOW == fcntl::AT_SYMLINK_NOFOLLOW { + fcntl::O_CLOEXEC | fcntl::O_NOFOLLOW | fcntl::O_PATH | fcntl::O_SYMLINK | oflags + } else { + fcntl::O_CLOEXEC | fcntl::O_RDONLY | oflags + }; + + // Absolute paths are passed without processing unless RESOLVE_BENEATH is used. + // canonicalize_using_cwd checks that path is absolute so a third branch that does so here + // isn't needed. + let path = if dirfd == fcntl::AT_FDCWD { + // The special constant AT_FDCWD indicates that we should use the cwd. + let mut buf = [0; limits::PATH_MAX]; + let len = 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]) }; + + canonicalize_using_cwd(Some(cwd), path).ok_or(Errno(EBADF))? + } else { + let mut buf = [0; limits::PATH_MAX]; + let len = Sys::fpath(dirfd, &mut buf)?; + // SAFETY: fpath checks then copies valid UTF8. + let dir = unsafe { str::from_utf8_unchecked(&buf[..len]) }; + + canonicalize_using_cwd(Some(dir), path).ok_or(Errno(EBADF))? + }; + let path = CString::new(path).map_err(|_| Errno(ENOENT))?; + + // TODO: + // * Switch open to openat. + File::open(path.as_c_str().into(), oflags) +} diff --git a/tests/Makefile b/tests/Makefile index 7164358616..1eb8d2ee83 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -135,11 +135,12 @@ EXPECT_NAMES=\ unistd/fsync \ unistd/ftruncate \ unistd/getopt \ + unistd/getopt_long \ unistd/pipe \ + unistd/readlinkat \ unistd/rmdir \ unistd/sleep \ unistd/swab \ - unistd/getopt_long \ unistd/write \ wchar/fgetwc \ wchar/fwide \ diff --git a/tests/expected/bins_dynamic/unistd/readlinkat.stderr b/tests/expected/bins_dynamic/unistd/readlinkat.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_dynamic/unistd/readlinkat.stdout b/tests/expected/bins_dynamic/unistd/readlinkat.stdout new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_static/unistd/readlinkat.stderr b/tests/expected/bins_static/unistd/readlinkat.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_static/unistd/readlinkat.stdout b/tests/expected/bins_static/unistd/readlinkat.stdout new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unistd/readlinkat.c b/tests/unistd/readlinkat.c new file mode 100644 index 0000000000..9009543438 --- /dev/null +++ b/tests/unistd/readlinkat.c @@ -0,0 +1,146 @@ +#include +#include +#include +#include +#include +#include +#include + +__attribute__((nonnull)) +static int run_test( + int dir, + const char name[], + char buf[PATH_MAX], + const char expected[] +) { + if (readlinkat(dir, name, buf, PATH_MAX) == -1) { + perror("readlinkat"); + return -1; + } + if (strncmp(expected, buf, PATH_MAX) != 0) { + fprintf( + stderr, + "readlinkat mismatch\n\tExpected: %s\n\tActual: %s\n", + expected, + buf + ); + return -1; + } + + return 0; +} + +int main(void) { + int status = EXIT_FAILURE; + + char template[] = "/tmp/rlatest.XXXXXX"; + if (!mkdtemp(template)) { + perror("mkdtemp"); + goto bye; + } + int dir = open(template, O_DIRECTORY); + if (dir == -1) { + perror("open"); + goto rmtempdir; + } + + // Set up file and link. + size_t len = sizeof(template) - 1; + const char file_name[] = "miku"; + char file_path[PATH_MAX] = {0}; + memcpy(file_path, template, len); + file_path[len] = '/'; + memcpy(&file_path[len + 1], file_name, sizeof(file_name)); + int file = open(file_path, O_CREAT | O_WRONLY); + if (file == -1) { + perror("open"); + goto close_dir; + } + + // Writing a sentinel message to the target file is useful in case + // readlinkat reads the file instead of the link. If that happens, + // it will self-evident instead of printing an empty string. + // (n.b. this totally happened to me so I KNOW it's useful) + const char msg[] = "readlinkat read the file instead of the link...oops"; + const ssize_t msg_len = sizeof(msg); + assert(msg_len < PATH_MAX); + if (write(file, msg, msg_len) < msg_len) { + perror("write"); + goto rmfiles; + } + if (close(file) == -1) { + perror("close"); + goto rmfiles; + } + file = -1; + + const char link_name[] = "link"; + char link_path[PATH_MAX] = {0}; + memcpy(link_path, template, len); + link_path[len] = '/'; + memcpy(&link_path[len + 1], link_name, len); + if (symlink(file_path, link_path) == -1) { + perror("symlink"); + goto rmfiles; + } + + // Relative path + char buf[PATH_MAX] = {0}; + if (run_test(dir, link_name, buf, file_path) == -1) { + fputs("Context: Basic test (relative path)\n", stderr); + goto rmfiles; + } + + // Absolute path + memset(buf, 0, PATH_MAX); + if (run_test(dir, link_path, buf, file_path) == -1) { + fputs("Context: Absolute path\n", stderr); + goto rmfiles; + } + + // AT_FDCWD + memset(buf, 0, PATH_MAX); + char old_cwd[PATH_MAX] = {0}; + if (!getcwd(old_cwd, PATH_MAX)) { + perror("getcwd"); + goto rmfiles; + } + if (chdir(template) == -1) { + perror("chdir"); + goto rmfiles; + } + if (run_test(AT_FDCWD, link_name, buf, file_path) == -1) { + fputs("Context: AT_FDCWD\n", stderr); + goto rmfiles; + } + if (chdir(old_cwd) == -1) { + perror("chdir"); + goto rmfiles; + } + + // Not a dir + memset(buf, 0, PATH_MAX); + file = open(file_path, O_PATH); + if (file == -1) { + perror("open"); + goto rmfiles; + } + if (readlinkat(file, "", buf, PATH_MAX) != -1) { + fputs("Context: Using a file for dirfd should fail\n", stderr); + fprintf(stderr, "readlinkat wrote this into buf: %s\n", buf); + goto close_file; + } + + status = EXIT_SUCCESS; +close_file: + close(file); +rmfiles: + unlink(file_path); + unlink(link_path); +close_dir: + close(dir); +rmtempdir: + rmdir(template); +bye: + return status; +}