Skip to content

fix(fuse): handle fallocate with non-zero offsets - #1176

Merged
szbr9486 merged 2 commits into
CurvineIO:mainfrom
ljy1-lixing:fix/fuse-fallocate-nonzero-offset
Jul 20, 2026
Merged

fix(fuse): handle fallocate with non-zero offsets#1176
szbr9486 merged 2 commits into
CurvineIO:mainfrom
ljy1-lixing:fix/fuse-fallocate-nonzero-offset

Conversation

@ljy1-lixing

Copy link
Copy Markdown
Contributor

Summary

Fix valid FUSE fallocate() calls returning EIO when the allocation range starts at a non-zero offset.

Curvine forwarded the POSIX offset and length directly into an internal resize API that only accepted offset == 0 and interpreted length as the final file size. As a result, allocating a range at EOF or inside an existing file failed before reaching the resize path.

The fix normalizes the POSIX range into Curvine's target-length representation, preserves the current file size for ranges already covered by the file, and handles FALLOC_FL_KEEP_SIZE without exposing a larger logical size.

Minimal Reproducer

#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <linux/falloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#define INITIAL_SIZE 4096
#define ALLOC_LEN 4096

static void die(const char *what)
{
    fprintf(stderr, "FAIL: %s: errno=%d (%s)\n",
            what, errno, strerror(errno));
    exit(EXIT_FAILURE);
}

int main(int argc, char **argv)
{
    const char *path;
    int mode = 0;
    int fd;
    struct stat st;
    off_t offset;
    off_t expected_size;

    if (argc < 2 || argc > 3) {
        fprintf(stderr, "Usage: %s FILE [keep-size]\n", argv[0]);
        return 2;
    }

    path = argv[1];
    if (argc == 3) {
        if (strcmp(argv[2], "keep-size") != 0) {
            fprintf(stderr, "Unknown mode: %s\n", argv[2]);
            return 2;
        }
        mode = FALLOC_FL_KEEP_SIZE;
    }

    unlink(path);
    fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0700);
    if (fd == -1)
        die("open");

    if (ftruncate(fd, INITIAL_SIZE) == -1)
        die("ftruncate");

    if (fstat(fd, &st) == -1)
        die("fstat before fallocate");

    offset = st.st_size;
    expected_size = mode == FALLOC_FL_KEEP_SIZE
                    ? st.st_size
                    : st.st_size + ALLOC_LEN;

    if (fallocate(fd, mode, offset, ALLOC_LEN) == -1) {
        fprintf(stderr,
                "FAIL: fallocate(mode=%d, offset=%lld, len=%d): "
                "errno=%d (%s)\n",
                mode, (long long)offset, ALLOC_LEN,
                errno, strerror(errno));
        close(fd);
        unlink(path);
        return EXIT_FAILURE;
    }

    if (fstat(fd, &st) == -1)
        die("fstat after fallocate");

    if (st.st_size != expected_size) {
        fprintf(stderr, "FAIL: size=%lld, expected=%lld\n",
                (long long)st.st_size, (long long)expected_size);
        close(fd);
        unlink(path);
        return EXIT_FAILURE;
    }

    printf("PASS: fallocate(mode=%d, offset=%lld, len=%d), size=%lld\n",
           mode, (long long)offset, ALLOC_LEN, (long long)st.st_size);

    close(fd);
    unlink(path);
    return EXIT_SUCCESS;
}

Run both supported modes on a Curvine FUSE mount:

gcc -Wall -Wextra -O2 /tmp/fallocate_repro.c -o /tmp/fallocate_repro
/tmp/fallocate_repro /mnt/curvine/fallocate-default-repro
/tmp/fallocate_repro /mnt/curvine/fallocate-keep-repro keep-size

Before the fix, both calls failed with EIO:

FAIL: fallocate(mode=0, offset=4096, len=4096): errno=5 (Input/output error)
FAIL: fallocate(mode=1, offset=4096, len=4096): errno=5 (Input/output error)

Root Cause

The FUSE request provides a POSIX allocation range as offset plus length. Curvine's internal FileAllocOpts resize path uses a different representation:

  • off was required to be zero.
  • len represented the target logical file length, not the range length.

The FUSE handler copied the request fields directly:

off = offset
len = length

For any valid non-zero offset, FileAllocOpts::validate() rejected the operation. The internal error was then returned through FUSE as EIO.

Removing only the zero-offset validation would not be sufficient. For example, allocating [4096, 8192) requires a target length of 8192, while passing len = 4096 could leave the file unchanged or even route the operation as a shrink when the existing file was larger.

The handler also needs the latest active-writer length. Master metadata may lag behind bytes already accepted by an open writer, so calculating the target from Master state alone could still produce an incorrect file size.

FALLOC_FL_KEEP_SIZE requires that the allocation endpoint is not exposed through st_size. Curvine allocates distributed storage lazily when data is written, so this mode does not need a logical metadata resize; writes into the requested range continue to allocate blocks normally.

Changes

  • Validate zero-length, overflowing, oversized, and unsupported allocation ranges at the FUSE boundary with appropriate errno values.
  • Calculate the allocation endpoint with checked offset + length arithmetic.
  • Merge Master file length with the active writer length before normalizing the request.
  • Convert default allocation ranges extending past EOF into an internal zero-offset target-length resize.
  • Return success without shrinking the file when the requested range is already inside the current logical size.
  • Preserve logical file size for FALLOC_FL_KEEP_SIZE under Curvine's lazy allocation model.
  • Add unit tests covering EOF extension, allocation inside an existing file, KEEP_SIZE, invalid ranges, and unsupported modes.
  • Add FUSE regression cases for non-zero-offset default and KEEP_SIZE allocations.

Verification

  • Minimal C reproducer passes in default mode with the file growing from 4096 to 8192 bytes.
  • Minimal C reproducer passes with FALLOC_FL_KEEP_SIZE and the file remaining 4096 bytes.
  • Valid non-zero-offset allocations inside sparse and existing file ranges succeed.
  • Invalid descriptors, zero lengths, negative ranges, oversized ranges, and unsupported modes retain their expected errno behavior.
  • Targeted fallocate unit tests pass: 4 passed, 0 failed.
  • cargo fmt --check passes.
  • cargo build --locked --release -p curvine-fuse passes.
  • git diff --check passes.

@yangcx000
yangcx000 self-requested a review July 17, 2026 14:08
@szbr9486
szbr9486 merged commit 34eafa5 into CurvineIO:main Jul 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants