Merge branch 'readlinkat' into 'master'

Implement readlinkat

See merge request redox-os/relibc!708
This commit is contained in:
Jeremy Soller
2025-09-17 21:10:24 -06:00
11 changed files with 254 additions and 46 deletions
+21 -1
View File
@@ -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 <https://pubs.opengroup.org/onlinepubs/9799919799/functions/readlink.html>.
#[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 <https://pubs.opengroup.org/onlinepubs/9799919799/functions/rmdir.html>.
#[no_mangle]
pub unsafe extern "C" fn rmdir(path: *const c_char) -> c_int {
+12
View File
@@ -585,6 +585,18 @@ impl Pal for Sys {
})
}
fn readlinkat(dirfd: c_int, pathname: CStr, out: &mut [u8]) -> Result<usize> {
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(|_| ())
+2
View File
@@ -228,6 +228,8 @@ pub trait Pal {
fn readlink(pathname: CStr, out: &mut [u8]) -> Result<usize>;
fn readlinkat(dirfd: c_int, pathname: CStr, out: &mut [u8]) -> Result<usize>;
fn rename(old: CStr, new: CStr) -> Result<()>;
fn rmdir(path: CStr) -> Result<()>;
+17 -42
View File
@@ -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<usize> {
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))?;
+54 -2
View File
@@ -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<File, Errno> {
// 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)
}
+2 -1
View File
@@ -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 \
+146
View File
@@ -0,0 +1,146 @@
#include <assert.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
__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;
}