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; +} +