Implement renameat, renameat2

Like the other "at" functions, renameat renames files with respect with
a directory descriptor. "renameat2" is Linux specific but really nice -
it adds a flag which supports atomically swapping two files or failing
if the target already exists. The latter is easily supported in relibc.
The former requires redoxfs support.

Besides the impl itself, I refactored "cap_path_at" into what it really
is: a mini openat2-like function.
This commit is contained in:
Josh Megnauth
2025-10-03 22:09:55 -04:00
parent 56386f1e26
commit f5d432644f
12 changed files with 401 additions and 31 deletions
+10
View File
@@ -24,6 +24,16 @@ pub const _IOFBF: c_int = 0;
pub const _IOLBF: c_int = 1;
pub const _IONBF: c_int = 2;
// renameat2
// (uint instead of int is intentional)
/// Rename but don't replace the target if it exists.
pub const RENAME_NOREPLACE: c_uint = 0x01;
/// Atomically swap two files.
#[cfg(target_os = "linux")]
pub const RENAME_EXCHANGE: c_uint = 0x02;
#[cfg(target_os = "linux")]
pub const RENAME_WHITEOUT: c_uint = 0x04;
// /dev/tty + nul
pub const L_ctermid: usize = 9;
// form of name is /XXXXXX, so 7
+29
View File
@@ -1034,6 +1034,35 @@ pub unsafe extern "C" fn rename(oldpath: *const c_char, newpath: *const c_char)
.or_minus_one_errno()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn renameat(
old_dir: c_int,
old_path: *const c_char,
new_dir: c_int,
new_path: *const c_char,
) -> c_int {
let old_path = CStr::from_ptr(old_path);
let new_path = CStr::from_ptr(new_path);
Sys::renameat(old_dir, old_path, new_dir, new_path)
.map(|()| 0)
.or_minus_one_errno()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn renameat2(
old_dir: c_int,
old_path: *const c_char,
new_dir: c_int,
new_path: *const c_char,
flags: c_uint,
) -> c_int {
let old_path = CStr::from_ptr(old_path);
let new_path = CStr::from_ptr(new_path);
Sys::renameat2(old_dir, old_path, new_dir, new_path, flags)
.map(|()| 0)
.or_minus_one_errno()
}
/// Rewind `stream` back to the beginning of it
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rewind(stream: *mut FILE) {
+33
View File
@@ -686,6 +686,39 @@ impl Pal for Sys {
.map(|_| ())
}
fn renameat(old_dir: c_int, old_path: CStr, new_dir: c_int, new_path: CStr) -> Result<()> {
e_raw(unsafe {
syscall!(
RENAMEAT,
old_dir,
old_path.as_ptr(),
new_dir,
new_path.as_ptr()
)
})
.map(|_| ())
}
fn renameat2(
old_dir: c_int,
old_path: CStr,
new_dir: c_int,
new_path: CStr,
flags: c_uint,
) -> Result<()> {
e_raw(unsafe {
syscall!(
RENAMEAT2,
old_dir,
old_path.as_ptr(),
new_dir,
new_path.as_ptr(),
flags
)
})
.map(|_| ())
}
fn rmdir(path: CStr) -> Result<()> {
e_raw(unsafe { syscall!(UNLINKAT, AT_FDCWD, path.as_ptr(), AT_REMOVEDIR) }).map(|_| ())
}
+8
View File
@@ -232,6 +232,14 @@ pub trait Pal {
fn readlinkat(dirfd: c_int, pathname: CStr, out: &mut [u8]) -> Result<usize>;
fn rename(old: CStr, new: CStr) -> Result<()>;
fn renameat(old_dir: c_int, old_path: CStr, new_dir: c_int, new_path: CStr) -> Result<()>;
fn renameat2(
old_dir: c_int,
old_path: CStr,
new_dir: c_int,
new_path: CStr,
flags: c_uint,
) -> Result<()>;
fn rmdir(path: CStr) -> Result<()>;
+49 -14
View File
@@ -14,6 +14,11 @@ use syscall::{
dirent::{DirentHeader, DirentKind},
};
use self::{
exec::Executable,
path::{canonicalize, openat2, openat2_path},
};
use super::{ERRNO, Pal, Read, types::*};
use crate::{
c_str::{CStr, CString},
error::{self, Errno, Result, ResultExt},
@@ -21,13 +26,14 @@ use crate::{
header::{
dirent::dirent,
errno::{
EBADF, EBADFD, EBADR, EFAULT, EINTR, EINVAL, EIO, ENAMETOOLONG, ENOENT, ENOMEM, ENOSYS,
EOPNOTSUPP, EPERM, ERANGE,
EBADF, EBADFD, EBADR, EEXIST, EFAULT, EINTR, EINVAL, EIO, ENAMETOOLONG, ENOENT, ENOMEM,
ENOSYS, EOPNOTSUPP, EPERM, ERANGE,
},
fcntl::{self, AT_FDCWD, O_CREAT, O_RDONLY, O_RDWR},
fcntl::{self, AT_FDCWD, AT_SYMLINK_NOFOLLOW, O_CREAT, O_RDONLY, O_RDWR},
limits,
pthread::{pthread_cancel, pthread_create, pthread_self},
pthread::{pthread_cancel, pthread_create},
signal::{NSIG, SIGEV_NONE, SIGEV_SIGNAL, SIGEV_THREAD, SIGRTMIN, sigevent},
stdio::RENAME_NOREPLACE,
sys_mman::{MAP_ANONYMOUS, MAP_FAILED, PROT_READ, PROT_WRITE},
sys_random,
sys_resource::{RLIM_INFINITY, rlimit, rusage},
@@ -50,8 +56,6 @@ use crate::{
pub use redox_rt::proc::FdGuard;
use super::{ERRNO, Pal, Read, types::*};
static mut BRK_CUR: *mut c_void = ptr::null_mut();
static mut BRK_END: *mut c_void = ptr::null_mut();
@@ -93,11 +97,6 @@ macro_rules! path_from_c_str {
}};
}
use self::{
exec::Executable,
path::{canonicalize, cap_path_at},
};
static CLONE_LOCK: RwLock<()> = RwLock::new(());
/// Redox syscall implementation of the platform abstraction layer.
@@ -273,7 +272,7 @@ impl Pal for Sys {
let path = path
.and_then(|cs| str::from_utf8(cs.to_bytes()).ok())
.ok_or(Errno(ENOENT))?;
let file = cap_path_at(dirfd, path, flags, 0)?;
let file = openat2(dirfd, path, flags, 0)?;
syscall::fchmod(*file as usize, mode as u16)?;
Ok(())
}
@@ -316,7 +315,7 @@ impl Pal for Sys {
let path = path
.and_then(|cs| str::from_utf8(cs.to_bytes()).ok())
.ok_or(Errno(ENOENT))?;
let file = cap_path_at(dirfd, path, flags, 0)?;
let file = openat2(dirfd, path, flags, 0)?;
Sys::fstat(*file, buf)
}
@@ -923,7 +922,7 @@ impl Pal for Sys {
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)?;
let file = openat2(dirfd, path, 0, fcntl::O_RDONLY | fcntl::O_SYMLINK)?;
Sys::read(*file, out)
}
@@ -939,6 +938,42 @@ impl Pal for Sys {
Ok(())
}
fn renameat(old_dir: c_int, old_path: CStr, new_dir: c_int, new_path: CStr) -> Result<()> {
Sys::renameat2(old_dir, old_path, new_dir, new_path, 0)
}
fn renameat2(
old_dir: c_int,
old_path: CStr,
new_dir: c_int,
new_path: CStr,
flags: c_uint,
) -> Result<()> {
const MASK: c_uint = !RENAME_NOREPLACE;
if MASK & flags != 0 {
return Err(Errno(EOPNOTSUPP));
}
let new_path = new_path.to_str().map_err(|_| Errno(EINVAL))?;
let target = openat2_path(new_dir, new_path, 0)?;
// Fail if the target exists with RENAME_NOREPLACE.
if flags & RENAME_NOREPLACE != 0
&& let Ok(fd) =
libredox::open(&target, fcntl::O_PATH | fcntl::O_CLOEXEC, 0).map(FdGuard::new)
{
return Err(Errno(EEXIST));
}
let old_path = old_path.to_str().map_err(|_| Errno(EINVAL))?;
// oflags are the same as Sys::rename above.
let source = openat2(old_dir, old_path, 0, fcntl::O_NOFOLLOW | fcntl::O_PATH)?;
// I'm avoiding Sys::rename to avoid reallocating a CString from a String.
syscall::frename(*source as usize, target)
.map(|_| ())
.map_err(Into::into)
}
fn rmdir(path: CStr) -> Result<()> {
let path = path.to_str().map_err(|_| Errno(EINVAL))?;
let canon = canonicalize(path)?;
+44 -17
View File
@@ -210,46 +210,73 @@ 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> {
/// Resolve `path` under `dirfd`.
///
/// See [`openat2`] for more information.
pub(super) fn openat2_path(dirfd: c_int, path: &str, at_flags: c_int) -> Result<String, 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 {
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(Out::from_mut(&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))?
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))?
};
canonicalize_using_cwd(Some(dir), path).ok_or(Errno(EBADF))
}
}
/// Canonicalize and open `path` with respect to `dirfd`.
///
/// This unexported openat2 is similar to the Linux syscall but with a different interface. The
/// naming is mostly for convenience - it's not a drop in replacement for openat2.
///
/// # Arguments
/// * `dirfd` is a directory descriptor to which `path` is resolved.
/// * `path` is a relative or absolute path. Relative paths are resolved in relation to `dirfd`
/// while absolute paths skip `dirfd`.
/// * `at_flags` constrains how `path` is resolved.
/// * `oflags` are flags that are passed to open.
///
/// # Constants
/// `at_flags`:
/// * AT_EMPTY_PATH returns the path at `dirfd` itself if `path` is empty. If `path` is not
/// empty, it's resolved w.r.t `dirfd` like normal.
///
/// `dirfd`:
/// `AT_FDCWD` is a special constant for `dirfd` that resolves `path` under the current working
/// directory.
pub(super) fn openat2(
dirfd: c_int,
path: &str,
at_flags: c_int,
oflags: c_int,
) -> Result<File, Errno> {
let path = openat2_path(dirfd, path, at_flags)?;
let path = CString::new(path).map_err(|_| Errno(ENOENT))?;
// Translate at flags into open flags; openat will do this on its own most likely.
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 | oflags
};
// TODO:
// * Switch open to openat.
File::open(path.as_c_str().into(), oflags)
+1
View File
@@ -64,6 +64,7 @@ EXPECT_NAMES=\
stdio/popen \
stdio/printf \
stdio/rename \
stdio/renameat \
stdio/scanf \
stdio/setvbuf \
stdio/sprintf \
+227
View File
@@ -0,0 +1,227 @@
#define _GNU_SOURCE
#include <fcntl.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int temp_file(
const char dir[],
size_t dlen,
char buf[PATH_MAX],
const char name[],
size_t nlen,
bool create
) {
memcpy(buf, dir, dlen);
buf[dlen] = '/';
memcpy(&buf[dlen + 1], name, nlen);
if (create) {
int fd = open(buf, O_CREAT);
if (fd == -1) {
perror("open (file A)");
close(fd);
return -1;
}
close(fd);
}
return 0;
}
int main(void) {
int result = EXIT_FAILURE;
char dir_template[] = "/tmp/mvattest.XXXXXX";
size_t dlen = sizeof(dir_template) - 1;
if (!mkdtemp(dir_template)) {
perror("mkdtemp");
goto bye;
}
int dirfd = open(dir_template, O_DIRECTORY | O_PATH);
if (dirfd == -1) {
perror("open (temp directory)");
goto bye;
}
char dir_template2[] = "/tmp/mvattest2.XXXXXX";
size_t dlen2 = sizeof(dir_template2) - 1;
if (!mkdtemp(dir_template2)) {
perror("mkdtemp");
goto close_dir1;
}
int dirfd2 = open(dir_template2, O_DIRECTORY | O_PATH);
if (dirfd2 == -1) {
perror("open (temp directory)");
goto close_dir1;
}
// File A
const char file_a[] = "power";
char file_a_path[PATH_MAX] = {0};
if (temp_file(dir_template, dlen, file_a_path, file_a, sizeof(file_a), true) == -1) {
goto close_dir2;
}
// File B
const char file_b[] = "nyanko";
char file_b_path[PATH_MAX] = {0};
if (
temp_file(
dir_template,
dlen,
file_b_path,
file_b,
sizeof(file_b),
false
) == -1
) {
goto remove_ab;
}
// File A but in directory 2
char file_a_dir2[PATH_MAX] = {0};
if (
temp_file(
dir_template2,
dlen2,
file_a_dir2,
file_a,
sizeof(file_a),
false
) == -1
) {
goto remove_all;
}
// Move file A to file B normally (same dir)
if (renameat(dirfd, file_a, dirfd, file_b) == -1) {
perror("renameat (A -> B, basic)");
goto remove_all;
}
if (access(file_b_path, F_OK) == -1) {
perror("access (file B, basic)");
goto remove_all;
}
// Move file B to A (absolute path; same dir)
if (renameat(dirfd, file_b_path, dirfd, file_a) == -1) {
perror("renameat (B -> A, absolute path)");
goto remove_all;
}
if (access(file_a_path, F_OK) == -1) {
perror("access (file A, absolute path)");
goto remove_all;
}
// Move A to B (both absolute)
if (renameat(dirfd, file_a_path, dirfd, file_b_path) == -1) {
perror("renameat (A -> B, both absolute)");
goto remove_all;
}
if (access(file_b_path, F_OK) == -1) {
perror("access (file B, both absolute)");
goto remove_all;
}
// Move B to B
if (renameat(dirfd, file_b, dirfd, file_b) == -1) {
perror("renameat (B -> B)");
goto remove_all;
}
if (access(file_b_path, F_OK) == -1) {
perror("access (file B)");
goto remove_all;
}
// Move file B to A (AT_FDCWD)
char cwd[PATH_MAX] = {0};
if (!getcwd(cwd, PATH_MAX)) {
perror("getcwd");
goto remove_all;
}
if (chdir(dir_template) == -1) {
perror("chdir");
goto remove_all;
}
if (renameat(AT_FDCWD, file_b, dirfd, file_a) == -1) {
perror("renameat (B -> A, AT_FDCWD)");
goto remove_all;
}
if (access(file_a_path, F_OK) == -1) {
perror("access (file A, AT_FDCWD)");
goto remove_all;
}
// Reset, though it doesn't really matter.
if (chdir(cwd) == -1) {
perror("chdir");
goto remove_all;
}
// Move to different directory
if (renameat(dirfd, file_a, dirfd2, file_a) == -1) {
perror("renameat (A -> B, different dirs)");
goto remove_all;
}
if (access(file_a_dir2, F_OK) == -1) {
perror("access (file A in dir2)");
goto remove_all;
}
// Move non-existing file
if (renameat(dirfd, "aki", dirfd, "denji") == 0) {
// Wut?
fputs("renameat succeeded at moving a non-existing file\n", stderr);
goto remove_all;
}
// RENAME_NOREPLACE
// Create file B in dir 1 first because we moved it earlier.
int fd = open(file_b_path, O_CREAT);
if (fd == -1) {
perror("open (file B in dir 1)");
close(fd);
goto remove_all;
}
close(fd);
if (renameat2(dirfd, file_b, dirfd2, file_a, RENAME_NOREPLACE) == 0) {
fputs("renameat2 (B -> A, noreplace) succeeded\n", stderr);
goto remove_all;
}
if (access(file_a_dir2, F_OK) == -1) {
fputs("RENAME_NOREPLACE ate file A in dir 2\n", stderr);
goto remove_all;
}
if (access(file_b_path, F_OK) == -1) {
fputs("RENAME_NOREPLACE ate file B in dir 1\n", stderr);
goto remove_all;
}
// TODO: RENAME_EXCHANGE (Needs frename support in Redox)
// Notes for later:
// * Write a message to both files
// * Swap
// * Check if the files swapped correctly by strcmp messages
result = EXIT_SUCCESS;
remove_all:
remove(file_a_dir2);
remove_ab:
remove(file_a_path);
remove(file_b_path);
rmdir(dir_template);
rmdir(dir_template2);
close_dir2:
close(dirfd2);
close_dir1:
close(dirfd);
bye:
return result;
}