Merge branch 'impl-posix_fallocate' into 'master'

Implement posix_fallocate

See merge request redox-os/relibc!751
This commit is contained in:
Jeremy Soller
2025-11-29 06:44:34 -07:00
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;
}