Summary
On Apple platforms, rustix::fs::fallocate(fd, FallocateFlags::empty(), 0, len) can return Ok(()) after allocating only a small part of len. The implementation then calls ftruncate and leaves a sparse file whose later writes are not protected from ENOSPC.
Apple documents this behavior explicitly: F_PREALLOCATE may allocate less than requested on success when F_ALLOCATEALL is not set, and reports the actual amount through fstore_t.fst_bytesalloc.
The current rustix Apple backend first uses F_ALLOCATECONTIG, checks only whether fcntl returned -1, and does not inspect fst_bytesalloc before calling ftruncate:
|
#[cfg(apple)] |
|
pub(crate) fn fallocate( |
|
fd: BorrowedFd<'_>, |
|
mode: FallocateFlags, |
|
offset: u64, |
|
len: u64, |
|
) -> io::Result<()> { |
|
let offset: i64 = offset.try_into().map_err(|_e| io::Errno::INVAL)?; |
|
let len = len as i64; |
|
|
|
assert!(mode.is_empty()); |
|
|
|
let new_len = offset.checked_add(len).ok_or(io::Errno::FBIG)?; |
|
let mut store = c::fstore_t { |
|
fst_flags: c::F_ALLOCATECONTIG, |
|
fst_posmode: c::F_PEOFPOSMODE, |
|
fst_offset: 0, |
|
fst_length: new_len, |
|
fst_bytesalloc: 0, |
|
}; |
|
unsafe { |
|
if c::fcntl(borrowed_fd(fd), c::F_PREALLOCATE, &store) == -1 { |
|
// Unable to allocate contiguous disk space; attempt to allocate |
|
// non-contiguously. |
|
store.fst_flags = c::F_ALLOCATEALL; |
|
let _ = ret_c_int(c::fcntl(borrowed_fd(fd), c::F_PREALLOCATE, &store))?; |
|
} |
|
ret(c::ftruncate(borrowed_fd(fd), new_len)) |
|
} |
Environment
- rustix: 1.1.5, direct dependency and direct call
- macOS: 15.7.4 (24G517)
- Darwin: 24.6.0, arm64
- filesystem: APFS
Minimal reproduction
Cargo.toml:
[package]
name = "rustix-fallocate-repro"
version = "0.1.0"
edition = "2021"
[dependencies]
rustix = { version = "=1.1.5", features = ["fs"] }
src/main.rs:
use rustix::fs::{fallocate, FallocateFlags};
use std::fs::OpenOptions;
use std::os::unix::fs::MetadataExt;
fn main() -> std::io::Result<()> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open("rustix-fallocate-repro.bin")?;
let requested = 128 * 1024 * 1024;
let result = fallocate(&file, FallocateFlags::empty(), 0, requested);
file.sync_all()?;
let metadata = file.metadata()?;
println!("result={result:?}");
println!("logical_size={}", metadata.len());
println!("allocated_size={}", metadata.blocks() * 512);
Ok(())
}
On the normal APFS data volume:
requested_bytes: 134217728
fallocate_result: Ok(())
after_fallocate: logical_size=134217728 allocated_size=3088384
after_sync: logical_size=134217728 allocated_size=3096576
The call reports success after reserving about 3 MiB of the requested 128 MiB.
Insufficient-space reproduction
I also ran the same direct rustix call on a disposable 128 MiB APFS disk image. The image had 132,767,744 bytes available and the call requested 209,715,200 bytes:
requested_bytes: 209715200
before: logical_size=0 allocated_size=0 available_space=132767744
fallocate_result: Ok(())
after_fallocate: logical_size=209715200 allocated_size=104857600 available_space=27910144
after_sync: logical_size=209715200 allocated_size=104857600 available_space=27910144
The call again reports success, consumes about 100 MiB, and extends logical EOF to 200 MiB even though it could not reserve the requested space.
F_ALLOCATEALL control
Using the same APFS volumes and calling fcntl(F_PREALLOCATE) with F_ALLOCATEALL directly produces the expected all-or-nothing behavior:
- with sufficient space, a 128 MiB request returns success with
fst_bytesalloc = 134217728, and st_blocks * 512 >= 134217728;
- on the limited image, a 200 MiB request returns
-1 with ENOSPC, fst_bytesalloc = 0, logical size remains zero, and allocated size remains zero.
The relevant macOS fcntl(2) text says:
upon success, the space that is allocated can be the size requested, larger than the size requested, or (if the F_ALLOCATEALL flag is not provided) smaller than the space requested.
It also defines F_ALLOCATEALL as:
Allocate all requested space or no space at all.
Expected behavior
If rustix fallocate returns Ok(()), the requested range should be fully reserved. An oversized request should return ENOSPC without being converted into a successful sparse logical extension.
Suggested direction
Use F_ALLOCATEALL for the Apple emulation and validate fst_bytesalloc before calling ftruncate.
Keeping F_ALLOCATECONTIG as a fast path is difficult to reconcile with all-or-nothing semantics: it may allocate only part of the request, and a later attempt to reserve the remainder can fail, leaving a partial reservation even though rustix returns an error.
Related downstream evidence: AsterCommunity/AsterFs#1
Summary
On Apple platforms,
rustix::fs::fallocate(fd, FallocateFlags::empty(), 0, len)can returnOk(())after allocating only a small part oflen. The implementation then callsftruncateand leaves a sparse file whose later writes are not protected fromENOSPC.Apple documents this behavior explicitly:
F_PREALLOCATEmay allocate less than requested on success whenF_ALLOCATEALLis not set, and reports the actual amount throughfstore_t.fst_bytesalloc.The current rustix Apple backend first uses
F_ALLOCATECONTIG, checks only whetherfcntlreturned-1, and does not inspectfst_bytesallocbefore callingftruncate:rustix/src/backend/libc/fs/syscalls.rs
Lines 1797 to 1825 in 287214b
Environment
Minimal reproduction
Cargo.toml:src/main.rs:On the normal APFS data volume:
The call reports success after reserving about 3 MiB of the requested 128 MiB.
Insufficient-space reproduction
I also ran the same direct rustix call on a disposable 128 MiB APFS disk image. The image had 132,767,744 bytes available and the call requested 209,715,200 bytes:
The call again reports success, consumes about 100 MiB, and extends logical EOF to 200 MiB even though it could not reserve the requested space.
F_ALLOCATEALLcontrolUsing the same APFS volumes and calling
fcntl(F_PREALLOCATE)withF_ALLOCATEALLdirectly produces the expected all-or-nothing behavior:fst_bytesalloc = 134217728, andst_blocks * 512 >= 134217728;-1withENOSPC,fst_bytesalloc = 0, logical size remains zero, and allocated size remains zero.The relevant macOS
fcntl(2)text says:It also defines
F_ALLOCATEALLas:Expected behavior
If rustix
fallocatereturnsOk(()), the requested range should be fully reserved. An oversized request should returnENOSPCwithout being converted into a successful sparse logical extension.Suggested direction
Use
F_ALLOCATEALLfor the Apple emulation and validatefst_bytesallocbefore callingftruncate.Keeping
F_ALLOCATECONTIGas a fast path is difficult to reconcile with all-or-nothing semantics: it may allocate only part of the request, and a later attempt to reserve the remainder can fail, leaving a partial reservation even though rustix returns an error.Related downstream evidence: AsterCommunity/AsterFs#1