Skip to content

Truncate through the descriptor when there is one - #66

Open
Ebrathul wants to merge 2 commits into
opencloud-eu:mainfrom
Ebrathul:fix/truncate-through-fd
Open

Truncate through the descriptor when there is one#66
Ebrathul wants to merge 2 commits into
opencloud-eu:mainfrom
Ebrathul:fix/truncate-through-fd

Conversation

@Ebrathul

@Ebrathul Ebrathul commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Found while running openvfs against my own account with my own tool and the desktop-client
plug-in from nextcloud/desktop#10635 and working through what a sync root has to
survive before it can hold ordinary files, read-only ones included.

openVFSfuse_truncate() ignores fuse_file_info and always truncates by path:

static int openVFSfuse_truncate(const char *orig_path, off_t size, fuse_file_info *)
{
    int res;

    const auto path = getInternalPath(orig_path);
    res = truncate(path.c_str(), size);

So the file's mode is re-checked on every call. Truncating through a descriptor
already open for writing fails with EACCES once the file has been made
read-only, where POSIX requires it to succeed — the permission check belongs to
open(), not to each operation on the descriptor it returned.

libfuse passes fi when the truncation arrives through an open descriptor and
null when it comes from a path-based truncate(), so the two cases are already
distinguishable; the information was simply unused.

Reproduction

Write through a descriptor, make the file read-only, then truncate through the
same descriptor. Same script on tmpfs for comparison:

fd = os.open(p, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
os.write(fd, b"0123456789")
os.chmod(p, 0o444)
os.ftruncate(fd, 4)
tmpfs:   ftruncate-after-chmod OK
openvfs: ftruncate-after-chmod FAILED errno=13

The change

Use the descriptor when it is there:

res = fi ? ftruncate(fi->fh, size) : truncate(path.c_str(), size);

The log uses an intmax_t conversion for off_t, and the failure path saves
errno before debug logging so formatting or syslog cannot change the error
returned to FUSE.

The null case is the previous behaviour untouched, so a plain truncate() on a
path — including on a dehydrated placeholder — goes exactly where it went before.
Verified both ways: ftruncate() through a descriptor now succeeds on a
read-only file, and truncate -s 3 on a path still shortens the file to three
bytes.

I did not change what happens when a dehydrated placeholder is truncated by
path. Truncating a file to zero arguably should not hydrate it first, but that is
issue #38's territory.

No test, for the same reason as the sibling pull request: main has no
harness that mounts a filesystem, and #64 is already introducing one for the
socket layer. The script above is the whole reproduction.

Relationship to the other pull requests

Same shape of defect as the write() half of my other pull request — an
operation given a descriptor and using the path instead — but an independent fix
for an independent symptom, so it is here on its own rather than as a third
commit there. Either can go in without the other.

Independent of #61#64, none of which touch openVFSfuse_truncate(). Applies
cleanly to main at cbdeeef, and I also cherry-picked it onto #64's head and
built there — ctest green including #64's new socketthreadtest, and
ftruncate() through a descriptor on a read-only file verified again on that
combination. Tested, not assumed.

Environment

Linux 7.1.8 (CachyOS), libfuse 3.18.2, Btrfs.

Applied to a fresh clone of main at cbdeeef and built there. Built
RelWithDebInfo with gcc 16.2.1, gcc 15 and clang 22 — clean on all three,
ctest green, clang-format clean. The ftruncate(fi->fh, size) line warns under
-Wconversion exactly as the existing pread(fi->fh, ...) in read() does, and
no other way.

openVFSfuse_truncate() ignored fuse_file_info and always truncated by
path, so the file's mode was re-checked on every call. Truncating
through a descriptor that is already open for writing then failed with
EACCES once the file had been made read-only, where POSIX requires it to
succeed -- the same shape as the write() problem.

libfuse passes fi when the truncation comes from an open descriptor and
null when it comes from a path-based truncate(), so the null case keeps
the previous behaviour.

Assisted-by: Claude Opus 5 (Anthropic)
Use the printf length modifier matching the intmax_t conversion and
preserve truncate errors across debug logging.

Assisted-by: Codex:GPT-5
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.

1 participant