From f5d432644fb322796ae0a252444c193e442e301a Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Fri, 3 Oct 2025 22:09:55 -0400 Subject: [PATCH] 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. --- src/header/stdio/constants.rs | 10 + src/header/stdio/mod.rs | 29 +++ src/platform/linux/mod.rs | 33 +++ src/platform/pal/mod.rs | 8 + src/platform/redox/mod.rs | 63 +++-- src/platform/redox/path.rs | 61 +++-- tests/Makefile | 1 + .../bins_dynamic/stdio/renameat.stderr | 0 .../bins_dynamic/stdio/renameat.stdout | 0 .../bins_static/stdio/renameat.stderr | 0 .../bins_static/stdio/renameat.stdout | 0 tests/stdio/renameat.c | 227 ++++++++++++++++++ 12 files changed, 401 insertions(+), 31 deletions(-) create mode 100644 tests/expected/bins_dynamic/stdio/renameat.stderr create mode 100644 tests/expected/bins_dynamic/stdio/renameat.stdout create mode 100644 tests/expected/bins_static/stdio/renameat.stderr create mode 100644 tests/expected/bins_static/stdio/renameat.stdout create mode 100644 tests/stdio/renameat.c diff --git a/src/header/stdio/constants.rs b/src/header/stdio/constants.rs index 4c3e32ae75..9900493dc6 100644 --- a/src/header/stdio/constants.rs +++ b/src/header/stdio/constants.rs @@ -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 diff --git a/src/header/stdio/mod.rs b/src/header/stdio/mod.rs index 510f325ecc..ff1b23286b 100644 --- a/src/header/stdio/mod.rs +++ b/src/header/stdio/mod.rs @@ -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) { diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 88bacb52e1..ff7199e4bd 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -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(|_| ()) } diff --git a/src/platform/pal/mod.rs b/src/platform/pal/mod.rs index ca2e67fbdf..5654ab1d5f 100644 --- a/src/platform/pal/mod.rs +++ b/src/platform/pal/mod.rs @@ -232,6 +232,14 @@ pub trait Pal { fn readlinkat(dirfd: c_int, pathname: CStr, out: &mut [u8]) -> Result; 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<()>; diff --git a/src/platform/redox/mod.rs b/src/platform/redox/mod.rs index 8021ddf4b7..d0df5000ec 100644 --- a/src/platform/redox/mod.rs +++ b/src/platform/redox/mod.rs @@ -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 { 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)?; diff --git a/src/platform/redox/path.rs b/src/platform/redox/path.rs index 40af6f0154..1cd198f677 100644 --- a/src/platform/redox/path.rs +++ b/src/platform/redox/path.rs @@ -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 { +/// Resolve `path` under `dirfd`. +/// +/// See [`openat2`] for more information. +pub(super) fn openat2_path(dirfd: c_int, path: &str, at_flags: 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 { + 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 { + 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) diff --git a/tests/Makefile b/tests/Makefile index 38b29de8f0..4e6e3e7e73 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -64,6 +64,7 @@ EXPECT_NAMES=\ stdio/popen \ stdio/printf \ stdio/rename \ + stdio/renameat \ stdio/scanf \ stdio/setvbuf \ stdio/sprintf \ diff --git a/tests/expected/bins_dynamic/stdio/renameat.stderr b/tests/expected/bins_dynamic/stdio/renameat.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_dynamic/stdio/renameat.stdout b/tests/expected/bins_dynamic/stdio/renameat.stdout new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_static/stdio/renameat.stderr b/tests/expected/bins_static/stdio/renameat.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_static/stdio/renameat.stdout b/tests/expected/bins_static/stdio/renameat.stdout new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/stdio/renameat.c b/tests/stdio/renameat.c new file mode 100644 index 0000000000..2287039553 --- /dev/null +++ b/tests/stdio/renameat.c @@ -0,0 +1,227 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include + +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; +}