Skip to content

Apple fallocate can return Ok after only a partial F_PREALLOCATE allocation #1682

Description

@AptS-1547

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions