From 22a34667237ce32f0c15c767e9bac628b5af1fd0 Mon Sep 17 00:00:00 2001 From: Ebrathul <54629189+Ebrathul@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:33:36 +0200 Subject: [PATCH 1/3] Write through the descriptor from open(), not a fresh one openVFSfuse_write() discarded fuse_file_info and re-opened the backing file with O_WRONLY on every call. openVFSfuse_read() already reads from fi->fh and openVFSfuse_release() closes it, so write() was the only operation not using the descriptor open() had stored. Two consequences. An open()/close() pair ran per write call. And the mode bits were re-evaluated on every write, so a descriptor stayed writable only as long as the file's mode allowed a fresh open: writing through an fd that was opened before the file was made read-only failed with EACCES, where POSIX requires it to succeed. Assisted-by: Claude Opus 5 (Anthropic) --- src/openvfsfuse/openvfsfuse.cpp | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/openvfsfuse/openvfsfuse.cpp b/src/openvfsfuse/openvfsfuse.cpp index 032a3aa..a148feb 100644 --- a/src/openvfsfuse/openvfsfuse.cpp +++ b/src/openvfsfuse/openvfsfuse.cpp @@ -638,31 +638,18 @@ static int openVFSfuse_read(const char *orig_path, char *buf, size_t size, off_t static int openVFSfuse_write(const char *orig_path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi) { - int fd; int res; const auto path = getInternalPath(orig_path); - (void)fi; - fd = open(path.c_str(), O_WRONLY); - if (fd == -1) { + openvfsfuse_log(path, "write", 0, "write %d bytes at offset %d", size, offset); + res = pwrite(fi->fh, buf, size, offset); + if (res == -1) { res = -errno; - openvfsfuse_log(path, "write", -1, "write %d bytes to %s at offset %d", size, path.c_str(), offset); - - return res; + openvfsfuse_log(path, "write", -1, "write %d bytes at offset %d", size, offset); } else { - openvfsfuse_log(path, "write", 0, "write %d bytes to %s at offset %d", size, path.c_str(), offset); + openvfsfuse_log(path, "write", 0, "%d bytes written at offset %d", res, offset); } - res = pwrite(fd, buf, size, offset); - - if (res == -1) - res = -errno; - else - openvfsfuse_log(path, "write", 0, "%d bytes written to %s at offset %d", res, path.c_str(), offset); - - close(fd); - - return res; } From 9968eeab0203c936e981817f1507e8556c5a4b02 Mon Sep 17 00:00:00 2001 From: Ebrathul <54629189+Ebrathul@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:33:53 +0200 Subject: [PATCH 2/3] Implement create() so a read-only file can be created and written Without a create operation libfuse falls back to mknod() plus a separate open(). openVFSfuse_mknod() creates the file with the mode the caller asked for -- applied literally, because main() sets umask(0) -- and closes it. The open() that follows is then a real permission check against a file that is already read-only, and fails with EACCES, leaving a zero-length file behind. Creating a file with a mode that has no owner-write bit and writing to the returned descriptor is ordinary: git creates every loose object with mkstemp mode 0444, and cp -p reproduces the source mode. On a local filesystem one open() call does both halves and the mode is never consulted for the descriptor it returns. Doing the same here means implementing create(), which creates and opens in one step and hands back the descriptor. mknod() is left alone: after this change nothing routes an O_CREAT open through it, and mknod(0444) followed by open(O_WRONLY) failing is correct. Assisted-by: Claude Opus 5 (Anthropic) --- src/openvfsfuse/openvfsfuse.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/openvfsfuse/openvfsfuse.cpp b/src/openvfsfuse/openvfsfuse.cpp index a148feb..62f560f 100644 --- a/src/openvfsfuse/openvfsfuse.cpp +++ b/src/openvfsfuse/openvfsfuse.cpp @@ -502,6 +502,26 @@ static int openVFSfuse_utimens(const char *orig_path, const struct timespec ts[2 return 0; } +static int openVFSfuse_create(const char *orig_path, mode_t mode, struct fuse_file_info *fi) +{ + const auto path = getInternalPath(orig_path); + + // A file that does not exist yet cannot be a placeholder, so it needs + // neither hydration nor placeholder attributes: getxattr() reports a + // hydrated default for a file that carries none. + const int res = open(path.c_str(), fi->flags, mode); + openvfsfuse_log(path, "create", res, "create %o", mode); + + if (res == -1) { + return -errno; + } + + lchown(path.c_str(), fuse_get_context()->uid, fuse_get_context()->gid); + fi->fh = res; + + return 0; +} + static int openVFSfuse_open(const char *orig_path, struct fuse_file_info *fi) { int res{0}; @@ -781,6 +801,7 @@ int initializeOpenVFSFuse(openVFSfuse_Args &openVFSArgs) openVFSfuse_oper.truncate = openVFSfuse_truncate; openVFSfuse_oper.utimens = openVFSfuse_utimens; openVFSfuse_oper.open = openVFSfuse_open; + openVFSfuse_oper.create = openVFSfuse_create; openVFSfuse_oper.read = openVFSfuse_read; openVFSfuse_oper.write = openVFSfuse_write; openVFSfuse_oper.statfs = openVFSfuse_statfs; From c71f30b1666a1812139f832f1d8eecdb2d797580 Mon Sep 17 00:00:00 2001 From: Ebrathul <54629189+Ebrathul@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:56:14 +0200 Subject: [PATCH 3/3] Harden descriptor-backed create and write Report ownership failures from create and remove the incomplete file instead of returning a descriptor with unexpected ownership. Preserve syscall errors across debug logging and pass correctly typed values to the variadic logger. Assisted-by: Codex:GPT-5 --- src/openvfsfuse/openvfsfuse.cpp | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/openvfsfuse/openvfsfuse.cpp b/src/openvfsfuse/openvfsfuse.cpp index 62f560f..5c39373 100644 --- a/src/openvfsfuse/openvfsfuse.cpp +++ b/src/openvfsfuse/openvfsfuse.cpp @@ -20,6 +20,7 @@ #include "socketthread.h" #include +#include #include #include #include @@ -506,17 +507,24 @@ static int openVFSfuse_create(const char *orig_path, mode_t mode, struct fuse_fi { const auto path = getInternalPath(orig_path); - // A file that does not exist yet cannot be a placeholder, so it needs - // neither hydration nor placeholder attributes: getxattr() reports a - // hydrated default for a file that carries none. const int res = open(path.c_str(), fi->flags, mode); - openvfsfuse_log(path, "create", res, "create %o", mode); - if (res == -1) { - return -errno; + const auto error = errno; + openvfsfuse_log(path, "create", -1, "create %o", static_cast(mode)); + return -error; } - lchown(path.c_str(), fuse_get_context()->uid, fuse_get_context()->gid); + if (fchown(res, fuse_get_context()->uid, fuse_get_context()->gid) == -1) { + const auto error = errno; + const auto cleanupError = unlink(path.c_str()) == -1 ? errno : 0; + close(res); + openvfsfuse_log(path, "create", -1, "set ownership after create %o", static_cast(mode)); + if (cleanupError != 0) { + openvfsfuse_log(path, "create", -1, "failed to remove incomplete file: errno %d", cleanupError); + } + return -error; + } + openvfsfuse_log(path, "create", 0, "create %o", static_cast(mode)); fi->fh = res; return 0; @@ -661,13 +669,13 @@ static int openVFSfuse_write(const char *orig_path, const char *buf, size_t size int res; const auto path = getInternalPath(orig_path); - openvfsfuse_log(path, "write", 0, "write %d bytes at offset %d", size, offset); + openvfsfuse_log(path, "write", 0, "write %zu bytes at offset %jd", size, static_cast(offset)); res = pwrite(fi->fh, buf, size, offset); if (res == -1) { res = -errno; - openvfsfuse_log(path, "write", -1, "write %d bytes at offset %d", size, offset); + openvfsfuse_log(path, "write", -1, "write %zu bytes at offset %jd", size, static_cast(offset)); } else { - openvfsfuse_log(path, "write", 0, "%d bytes written at offset %d", res, offset); + openvfsfuse_log(path, "write", 0, "%d bytes written at offset %jd", res, static_cast(offset)); } return res;