From 8e5937ca2556fd485de117992210b0db2d002687 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Sat, 25 Oct 2025 12:58:19 -0400 Subject: [PATCH] Implement posix_fallocate `posix_fallocate` ensures that a byte range in a file is allocated so that subsequent writes don't fail. Unlike ftruncate, posix_fallocate does not shrink files. The Linux syscall fallocate is similar to posix_fallocate except with far more control over how byte ranges are allocated (e.g. it supports file holes and other features). This MR doesn't implement fallocate as it requires syscall and redoxfs support. Finally, I changed the flags for flock from usize to c_int. That matches what we have in libc and also avoids some silly, needless type casting. --- src/header/fcntl/mod.rs | 21 ++ src/header/sys_file/mod.rs | 14 +- src/platform/linux/mod.rs | 7 +- src/platform/pal/mod.rs | 4 + src/platform/redox/mod.rs | 64 ++++-- src/platform/redox/path.rs | 26 ++- tests/Makefile | 1 + .../bins_dynamic/fcntl/posix_fallocate.stderr | 0 .../bins_dynamic/fcntl/posix_fallocate.stdout | 0 .../bins_static/fcntl/posix_fallocate.stderr | 0 .../bins_static/fcntl/posix_fallocate.stdout | 0 tests/fcntl/posix_fallocate.c | 217 ++++++++++++++++++ 12 files changed, 329 insertions(+), 25 deletions(-) create mode 100644 tests/expected/bins_dynamic/fcntl/posix_fallocate.stderr create mode 100644 tests/expected/bins_dynamic/fcntl/posix_fallocate.stdout create mode 100644 tests/expected/bins_static/fcntl/posix_fallocate.stderr create mode 100644 tests/expected/bins_static/fcntl/posix_fallocate.stdout create mode 100644 tests/fcntl/posix_fallocate.c diff --git a/src/header/fcntl/mod.rs b/src/header/fcntl/mod.rs index 82716818dc..52fa719d0b 100644 --- a/src/header/fcntl/mod.rs +++ b/src/header/fcntl/mod.rs @@ -4,6 +4,8 @@ #![deny(unsafe_op_in_unsafe_fn)] +use core::num::NonZeroU64; + use crate::{ c_str::CStr, error::ResultExt, @@ -15,6 +17,8 @@ use crate::{ pub use self::sys::*; +use super::errno::EINVAL; + #[cfg(target_os = "linux")] #[path = "linux.rs"] pub mod sys; @@ -90,3 +94,20 @@ pub unsafe extern "C" fn open(path: *const c_char, oflag: c_int, mut __valist: . #[unsafe(no_mangle)] pub unsafe extern "C" fn cbindgen_stupid_struct_user_for_fcntl(a: flock) {} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn posix_fallocate(fd: c_int, offset: off_t, length: off_t) -> c_int { + // Length can't be zero and offset must be positive. + let Ok(offset) = offset.try_into() else { + return EINVAL; + }; + let Some(length) = length.try_into().ok().and_then(NonZeroU64::new) else { + return EINVAL; + }; + + // posix_fallocate does not set errno but instead returns it. + Sys::posix_fallocate(fd, offset, length) + .err() + .map(|e| e.0) + .unwrap_or_default() +} diff --git a/src/header/sys_file/mod.rs b/src/header/sys_file/mod.rs index e173b6c350..6dd5e50631 100644 --- a/src/header/sys_file/mod.rs +++ b/src/header/sys_file/mod.rs @@ -7,14 +7,14 @@ use crate::{ platform::{Pal, Sys, types::*}, }; -pub const LOCK_SH: usize = 1; -pub const LOCK_EX: usize = 2; -pub const LOCK_NB: usize = 4; -pub const LOCK_UN: usize = 8; +pub const LOCK_SH: c_int = 1; +pub const LOCK_EX: c_int = 2; +pub const LOCK_NB: c_int = 4; +pub const LOCK_UN: c_int = 8; -pub const L_SET: usize = 0; -pub const L_INCR: usize = 1; -pub const L_XTND: usize = 2; +pub const L_SET: c_int = 0; +pub const L_INCR: c_int = 1; +pub const L_XTND: c_int = 2; /// See . #[unsafe(no_mangle)] diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index ff7199e4bd..4bf23aa6cf 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -1,4 +1,4 @@ -use core::{arch::asm, ptr}; +use core::{arch::asm, num::NonZeroU64, ptr}; use super::{ERRNO, Pal, types::*}; use crate::{ @@ -560,6 +560,11 @@ impl Pal for Sys { e_raw(unsafe { syscall!(PIPE2, fildes.as_mut_ptr(), flags) }).map(|_| ()) } + fn posix_fallocate(fd: c_int, offset: u64, length: NonZeroU64) -> Result<()> { + let length = length.get(); + e_raw(unsafe { syscall!(FALLOCATE, fd, 0, offset, length) }).map(|_| ()) + } + fn posix_getdents(fildes: c_int, buf: &mut [u8]) -> Result { let current_offset = Self::lseek(fildes, 0, SEEK_CUR)? as u64; let bytes_read = Self::getdents(fildes, buf, current_offset)?; diff --git a/src/platform/pal/mod.rs b/src/platform/pal/mod.rs index 5654ab1d5f..39fb912c10 100644 --- a/src/platform/pal/mod.rs +++ b/src/platform/pal/mod.rs @@ -1,3 +1,5 @@ +use core::num::NonZeroU64; + use super::types::*; use crate::{ c_str::CStr, @@ -217,6 +219,8 @@ pub trait Pal { fn pipe2(fildes: Out<[c_int; 2]>, flags: c_int) -> Result<()>; + fn posix_fallocate(fd: c_int, offset: u64, length: NonZeroU64) -> Result<()>; + fn posix_getdents(fildes: c_int, buf: &mut [u8]) -> Result; unsafe fn rlct_clone(stack: *mut usize) -> Result; diff --git a/src/platform/redox/mod.rs b/src/platform/redox/mod.rs index 63d4eff81c..e8a970337f 100644 --- a/src/platform/redox/mod.rs +++ b/src/platform/redox/mod.rs @@ -1,6 +1,7 @@ use core::{ convert::TryFrom, mem::{self, MaybeUninit, size_of}, + num::NonZeroU64, ptr, slice, str, }; use redox_rt::{ @@ -16,7 +17,7 @@ use syscall::{ use self::{ exec::Executable, - path::{canonicalize, openat2, openat2_path}, + path::{FileLock, canonicalize, openat2, openat2_path}, }; use super::{ERRNO, Pal, Read, types::*}; use crate::{ @@ -26,14 +27,15 @@ use crate::{ header::{ dirent::dirent, errno::{ - EBADF, EBADFD, EBADR, EEXIST, EFAULT, EINTR, EINVAL, EIO, ENAMETOOLONG, ENOENT, ENOMEM, - ENOSYS, EOPNOTSUPP, EPERM, ERANGE, + EBADF, EBADFD, EBADR, EEXIST, EFAULT, EFBIG, EINTR, EINVAL, EIO, ENAMETOOLONG, ENOENT, + ENOMEM, ENOSYS, EOPNOTSUPP, EPERM, ERANGE, }, fcntl::{self, AT_FDCWD, AT_SYMLINK_NOFOLLOW, O_CREAT, O_RDONLY, O_RDWR}, limits, pthread::{pthread_cancel, pthread_create}, signal::{NSIG, SIGEV_NONE, SIGEV_SIGNAL, SIGEV_THREAD, SIGRTMIN, sigevent}, stdio::RENAME_NOREPLACE, + sys_file, sys_mman::{MAP_ANONYMOUS, MAP_FAILED, PROT_READ, PROT_WRITE}, sys_random, sys_resource::{RLIM_INFINITY, rlimit, rusage}, @@ -56,6 +58,19 @@ use crate::{ pub use redox_rt::proc::FdGuard; +mod clone; +mod epoll; +mod event; +mod exec; +mod extra; +mod libcscheme; +mod libredox; +pub(crate) mod path; +mod ptrace; +pub(crate) mod signal; +mod socket; +mod timer; + static mut BRK_CUR: *mut c_void = ptr::null_mut(); static mut BRK_END: *mut c_void = ptr::null_mut(); @@ -72,19 +87,6 @@ fn cvt_uid(id: c_int) -> Result> { Ok(Some(id.try_into().map_err(|_| Errno(EINVAL))?)) } -mod clone; -mod epoll; -mod event; -mod exec; -mod extra; -mod libcscheme; -mod libredox; -pub(crate) mod path; -mod ptrace; -pub(crate) mod signal; -mod socket; -mod timer; - macro_rules! path_from_c_str { ($c_str:expr) => {{ match $c_str.to_str() { @@ -820,6 +822,36 @@ impl Pal for Sys { Ok(()) } + fn posix_fallocate(fd: c_int, offset: u64, length: NonZeroU64) -> Result<()> { + // Redox doesn't actually have flock yet but presumably the file will need to be locked to + // avoid accidentally truncating it if the length changes. + let _guard = FileLock::lock(fd, sys_file::LOCK_EX)?; + + // posix_fallocate is less nuanced than the Linux syscall fallocate. + // If the byte range is already allocated, posix_fallocate doesn't do any extra work. + // If the byte range is unallocated; free, uninitialized bytes are reserved. + // posix_fallocate does not shrink files. + // + // The main purpose of it is to ensure subsequent writes to a byte range don't fail. + let length = length.get(); + let total_offset = offset.checked_add(length).ok_or(Errno(EFBIG))?; + + let mut stat = syscall::Stat::default(); + syscall::fstat(fd as usize, &mut stat)?; + // The difference between total_offset and the file size is the number of bytes to + // allocate. So, if it's negative then the file is already large enough and we don't + // need to do any extra work. + if let Some(total_len) = total_offset + .checked_sub(stat.st_size) + .and_then(|diff| stat.st_size.checked_add(diff)) + { + let total_len: usize = total_len.try_into().map_err(|_| Errno(EFBIG))?; + syscall::ftruncate(fd as usize, total_len)?; + } + + Ok(()) + } + fn posix_getdents(fildes: c_int, buf: &mut [u8]) -> Result { let current_offset = Self::lseek(fildes, 0, SEEK_CUR)? as u64; let bytes_read = Self::getdents(fildes, buf, current_offset)?; diff --git a/src/platform/redox/path.rs b/src/platform/redox/path.rs index 2f4d76f92e..9482e6d836 100644 --- a/src/platform/redox/path.rs +++ b/src/platform/redox/path.rs @@ -13,7 +13,7 @@ use super::{FdGuard, Pal, Sys, libcscheme}; use crate::{ error::Errno, fs::File, - header::{fcntl, limits}, + header::{fcntl, limits, sys_file}, out::Out, sync::Mutex, }; @@ -210,6 +210,30 @@ fn get_parent_path(path: &str) -> Option<&str> { }) } +pub struct FileLock(c_int); + +impl FileLock { + pub fn lock(fd: c_int, op: c_int) -> Result { + if op & sys_file::LOCK_SH | sys_file::LOCK_EX == 0 { + return Err(Error::new(EINVAL)); + } + + Sys::flock(fd, op)?; + Ok(Self(fd)) + } + + pub fn unlock(self) -> Result<()> { + Sys::flock(self.0, sys_file::LOCK_UN).map_err(Into::into) + } +} + +impl Drop for FileLock { + fn drop(&mut self) { + let fd = self.0; + self.0 = -1; + let _ = Sys::flock(self.0, sys_file::LOCK_UN); + } +} /// Resolve `path` under `dirfd`. /// /// See [`openat2`] for more information. diff --git a/tests/Makefile b/tests/Makefile index 08ccf04764..b6d9780d8d 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -35,6 +35,7 @@ EXPECT_NAMES=\ error \ fcntl/create \ fcntl/fcntl \ + fcntl/posix_fallocate \ features \ fnmatch \ glob \ diff --git a/tests/expected/bins_dynamic/fcntl/posix_fallocate.stderr b/tests/expected/bins_dynamic/fcntl/posix_fallocate.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_dynamic/fcntl/posix_fallocate.stdout b/tests/expected/bins_dynamic/fcntl/posix_fallocate.stdout new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_static/fcntl/posix_fallocate.stderr b/tests/expected/bins_static/fcntl/posix_fallocate.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_static/fcntl/posix_fallocate.stdout b/tests/expected/bins_static/fcntl/posix_fallocate.stdout new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fcntl/posix_fallocate.c b/tests/fcntl/posix_fallocate.c new file mode 100644 index 0000000000..bb5fdded95 --- /dev/null +++ b/tests/fcntl/posix_fallocate.c @@ -0,0 +1,217 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +__attribute__((nonnull)) +int run_test( + int filefd, + const char error_msg[], + off_t offset, + off_t length, + int dirfd, + const char file_name[], + off_t expected_size +) { + // posix_fallocate does not set errno. + int result = posix_fallocate(filefd, offset, length); + if (result != 0) { + const char* error = strerror(result); + fprintf(stderr, "%s: %s\n", error_msg, error); + return -1; + } + + struct stat stat = {0}; + if (fstatat(dirfd, file_name, &stat, 0) == -1) { + perror("fstatat"); + return -1; + } + + if (stat.st_size != expected_size) { + fprintf( + stderr, + "Expected: %zd\nActual: %zd\n", + expected_size, + stat.st_size + ); + return -1; + } + + return 0; +} + +int main(void) { + int result = EXIT_FAILURE; + + char dir_template[] = "/tmp/pfalloctest.XXXXXX"; + size_t dlen = sizeof(dir_template) - 1; + if (!mkdtemp(dir_template)) { + perror("mkdtemp"); + goto bye; + } + int dirfd = open(dir_template, O_DIRECTORY); + if (dirfd == -1) { + perror("open"); + goto rmtemp; + } + + // Failure conditions. + // These are mostly a gut check for whatever backing syscall is used + // in relibc. If it ever changes, we have to make sure the correct + // errors are returned. If they're not, we have to add checks to our + // relibc implementation. Linux's backing syscall (SYS_fallocate) + // already does these checks. + // + // Directory: + if (posix_fallocate(dirfd, 0, 1000) == 0) { + fputs("posix_fallocate succeeded on a directory\n", stderr); + goto rmtemp; + } + + // Socket: + int sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock == -1) { + perror("socket"); + goto rmtemp; + } + if (posix_fallocate(sock, 0, 1000) == 0) { + fputs("posix_fallocate succeeded on a socket\n", stderr); + close(sock); + goto rmtemp; + } + close(sock); + + // Pipe: + int pipefd[2] = {0}; + if (pipe(pipefd) == -1) { + perror("pipe"); + goto rmtemp; + } + if (posix_fallocate(pipefd[0], 0, 1000) == 0) { + fputs("posix_fallocate succeeded on a pipe\n", stderr); + close(pipefd[0]); + close(pipefd[1]); + goto rmtemp; + } + close(pipefd[0]); + close(pipefd[1]); + + // Success conditions. + const char name[] = "commander_keen"; + char path[PATH_MAX] = {0}; + memcpy(path, dir_template, dlen); + path[dlen] = '/'; + memcpy(&path[dlen + 1], name, sizeof(name)); + + int file = open(path, O_CREAT | O_RDWR); + if (file == -1) { + perror("open"); + goto rmtemp; + } + + // Expand an empty file. + if ( + run_test( + file, + "posix_fallocate failed expanding an empty file", + 0, + 1000, + dirfd, + name, + 1000 + ) == -1 + ) { + fputs("FAILED: Expanding empty file test\n", stderr); + goto rmtempfile; + } + + // Don't shrink a file. + if ( + run_test( + file, + "posix_fallocate failed on already allocated file", + 0, + 1, + dirfd, + name, + 1000 + ) == -1 + ) { + fputs("FAILED: posix_fallocate should not shrink files\n", stderr); + goto rmtempfile; + } + + // Don't overwrite allocated byte ranges. + if (ftruncate(file, 0) == -1) { + perror("ftruncate"); + goto rmtempfile; + } + const char msg[] = "If you're reading this you must play Commander Keen"; + if (write(file, msg, sizeof(msg)) != sizeof(msg)) { + perror("write"); + goto rmtempfile; + } + + if ( + run_test( + file, + "posix_fallocate failed on an allocated range", + 0, + sizeof(msg), + dirfd, + name, + sizeof(msg) + ) == -1 + ) { + fputs( + "FAILED: posix_fallocate don't overwrite allocated ranges test\n", + stderr + ); + goto rmtempfile; + } + // And now check that the range wasn't overwritten. + char buf[sizeof(msg)] = {0}; + if (lseek(file, 0, SEEK_SET) == -1) { + perror("lseek"); + goto rmtempfile; + } + if (read(file, buf, sizeof(msg)) != sizeof(msg)) { + perror("read"); + goto rmtempfile; + } + if (strncmp(msg, buf, sizeof(msg)) != 0) { + fputs("FAILED: posix_fallocate overwrote/destroyed a file\n", stderr); + goto rmtempfile; + } + + // Offset + length that goes beyond file end expands file. + if ( + run_test( + file, + "posix_fallocate failed on file expansion test", + sizeof(msg), + 1000, + dirfd, + name, + sizeof(msg) + 1000 + ) == -1 + ) { + fputs("FAILED: posix_fallocate file expansion test\n", stderr); + goto rmtempfile; + } + + result = EXIT_SUCCESS; +rmtempfile: + close(file); + unlink(path); +rmtemp: + rmdir(dir_template); +bye: + return result; +} +