From fb5079bb7a44c567200bee0b5c5b5c8abda80f25 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Mon, 9 Mar 2026 05:37:08 -0500 Subject: FUSE VFS: lazy copy-on-write to prevent Overwrite folder pollution Defer COW from open() to first write(). Wine/Proton opens mod files O_RDWR for metadata ops (SetFileTime, etc.) without actually writing, which was copying SKSE plugin .ini/.log files into Overwrite needlessly. Co-Authored-By: Claude Opus 4.6 --- src/src/vfs/mo2filesystem.cpp | 97 ++++++++++++++++++++++++++++++++----------- src/src/vfs/mo2filesystem.h | 1 + 2 files changed, 74 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp index 35a0098..a81db75 100644 --- a/src/src/vfs/mo2filesystem.cpp +++ b/src/src/vfs/mo2filesystem.cpp @@ -889,30 +889,34 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) std::string realPath = snap.real_path; const bool writable = isWritableOpen(fi->flags); bool isBacking = snap.is_backing; + bool cowPending = false; + + // Lazy COW: don't copy the file on open — wait until an actual write() + // occurs. This avoids polluting the overwrite folder when Wine/Proton + // opens files O_RDWR for metadata ops without actually writing to them + // (common with SKSE plugin .ini/.log files). + // + // Files already in staging/overwrite don't need COW, so open them R/W + // immediately. For mod/base-game files opened writable, open read-only + // for now and mark COW as pending. + int fd = -1; if (writable) { - try { + const std::string stagedPath = ctx->overwrite->stagingPath(path); + const std::string owPath = ctx->overwrite->overwritePath(path); + + bool alreadyStaged = (realPath == stagedPath || realPath == owPath); + if (alreadyStaged) { + // File is already in staging/overwrite — open read-write directly. + fd = open(realPath.c_str(), O_RDWR); + } else { + // Mod or base-game file — open read-only, defer COW to write time. if (isBacking && ctx->backing_dir_fd >= 0) { - realPath = ctx->overwrite->copyOnWriteFromFd(ctx->backing_dir_fd, path); + fd = openat(ctx->backing_dir_fd, realPath.c_str(), O_RDONLY); } else { - realPath = ctx->overwrite->copyOnWrite(realPath, path); + fd = open(realPath.c_str(), O_RDONLY); } - isBacking = false; - updateFileNode(ctx, path, realPath, "Staging"); - } catch (...) { - fuse_reply_err(req, EIO); - return; - } - } - - int fd = -1; - - if (writable) { - const int openFlags = O_RDWR; - if (isBacking && ctx->backing_dir_fd >= 0) { - fd = openat(ctx->backing_dir_fd, realPath.c_str(), openFlags); - } else { - fd = open(realPath.c_str(), openFlags); + cowPending = true; } if (fd < 0) { fuse_reply_err(req, EIO); @@ -928,6 +932,7 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) of.real_path = realPath; of.writable = writable; of.is_backing = isBacking; + of.cow_pending = cowPending; of.relative_path = path; ctx->open_files[fh] = std::move(of); } @@ -1004,7 +1009,9 @@ void mo2_write(fuse_req_t req, fuse_ino_t /*ino*/, const char* buf, size_t size, int fd = -1; std::string relativePath; std::string realPath; - bool writable = false; + bool writable = false; + bool cowPending = false; + bool isBacking = false; { std::shared_lock lock(ctx->open_files_mutex); auto it = ctx->open_files.find(fi->fh); @@ -1014,6 +1021,8 @@ void mo2_write(fuse_req_t req, fuse_ino_t /*ino*/, const char* buf, size_t size, } fd = it->second.fd; writable = it->second.writable; + cowPending = it->second.cow_pending; + isBacking = it->second.is_backing; relativePath = it->second.relative_path; realPath = it->second.real_path; } @@ -1023,6 +1032,45 @@ void mo2_write(fuse_req_t req, fuse_ino_t /*ino*/, const char* buf, size_t size, return; } + // Lazy COW: this is the first actual write — perform the deferred copy + // now and reopen the file descriptor read-write on the staging copy. + if (cowPending) { + try { + std::string newPath; + if (isBacking && ctx->backing_dir_fd >= 0) { + newPath = ctx->overwrite->copyOnWriteFromFd(ctx->backing_dir_fd, relativePath); + } else { + newPath = ctx->overwrite->copyOnWrite(realPath, relativePath); + } + + int newFd = open(newPath.c_str(), O_RDWR); + if (newFd < 0) { + fuse_reply_err(req, EIO); + return; + } + + // Close the old read-only fd and update the open file record. + if (fd >= 0) close(fd); + fd = newFd; + + { + std::scoped_lock lock(ctx->open_files_mutex); + auto it = ctx->open_files.find(fi->fh); + if (it != ctx->open_files.end()) { + it->second.fd = newFd; + it->second.real_path = newPath; + it->second.is_backing = false; + it->second.cow_pending = false; + } + } + realPath = newPath; + updateFileNode(ctx, relativePath, newPath, "Staging"); + } catch (...) { + fuse_reply_err(req, EIO); + return; + } + } + if (fd < 0) { fuse_reply_err(req, EBADF); return; @@ -1247,10 +1295,11 @@ void mo2_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set, if (it->second.fd >= 0) { close(it->second.fd); } - it->second.fd = newFd; - it->second.real_path = target; - it->second.writable = true; - it->second.is_backing = false; + it->second.fd = newFd; + it->second.real_path = target; + it->second.writable = true; + it->second.is_backing = false; + it->second.cow_pending = false; } } } diff --git a/src/src/vfs/mo2filesystem.h b/src/src/vfs/mo2filesystem.h index 7ab0206..a1e1508 100644 --- a/src/src/vfs/mo2filesystem.h +++ b/src/src/vfs/mo2filesystem.h @@ -33,6 +33,7 @@ struct Mo2FsContext std::string real_path; bool writable = false; bool is_backing = false; + bool cow_pending = false; // true if opened writable but COW deferred std::string relative_path; }; -- cgit v1.3.1