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.
This commit is contained in:
Josh Megnauth
2025-10-25 12:58:19 -04:00
committed by Josh Megnauth
parent 27355b953b
commit 8e5937ca25
12 changed files with 329 additions and 25 deletions
+21
View File
@@ -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()
}
+7 -7
View File
@@ -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 <https://man7.org/linux/man-pages/man2/flock.2.html>.
#[unsafe(no_mangle)]
+6 -1
View File
@@ -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<usize> {
let current_offset = Self::lseek(fildes, 0, SEEK_CUR)? as u64;
let bytes_read = Self::getdents(fildes, buf, current_offset)?;
+4
View File
@@ -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<usize>;
unsafe fn rlct_clone(stack: *mut usize) -> Result<pthread::OsTid, Errno>;
+48 -16
View File
@@ -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<Option<u32>> {
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<usize> {
let current_offset = Self::lseek(fildes, 0, SEEK_CUR)? as u64;
let bytes_read = Self::getdents(fildes, buf, current_offset)?;
+25 -1
View File
@@ -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<Self> {
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.
+1
View File
@@ -35,6 +35,7 @@ EXPECT_NAMES=\
error \
fcntl/create \
fcntl/fcntl \
fcntl/posix_fallocate \
features \
fnmatch \
glob \
+217
View File
@@ -0,0 +1,217 @@
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
__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;
}