Skip to content

Truncate through the descriptor when there is one - #66

Merged
dragotin merged 2 commits into
opencloud-eu:mainfrom
Ebrathul:fix/truncate-through-fd
Oct 20, 2026
Merged

dragotin merged 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
// fi is set when the truncation goes through an open descriptor and null
// for a path-based truncate(). Truncating through a descriptor must not be
// re-checked against the file's mode, so use it when it is there.
res = fi ? ftruncate(fi->fh, size) : truncate(path.c_str(), size);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not work. ftruncate() returns -1 on error, which is non zero, and according to several source interpreted as true in modern C++.

Please rewrite to explicitely check the return val of ftruncate on -1 or 0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reviewing. I want to make sure I understand the issue. The conditional expression tests fi to select ftruncate() or truncate(); it assigns the selected call's return value to res. The next line checks res == -1, saves errno, and returns the error. This appears to match libfuse's passthrough truncate() implementation. Is there a failure case that this check misses, or were you asking for separate if/else branches to make the check clearer? I'm happy to change the presentation if that's the concern.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sorry, completely my fault. You're right. Forget my comment.

@dragotin
dragotin merged commit f53c2cd into opencloud-eu:main Oct 20, 2026
1 check 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