aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-03-09 05:37:08 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-03-09 05:37:08 -0500
commitfb5079bb7a44c567200bee0b5c5b5c8abda80f25 (patch)
treeaf24a0ba249b862c5326e7442c18ae29b7fbf9d6 /src
parentf8df9f41a1eb6fab6fdff241dd2b9e4871b0af2b (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/src/vfs/mo2filesystem.cpp97
-rw-r--r--src/src/vfs/mo2filesystem.h1
2 files changed, 74 insertions, 24 deletions
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;
- if (writable) {
- try {
- if (isBacking && ctx->backing_dir_fd >= 0) {
- realPath = ctx->overwrite->copyOnWriteFromFd(ctx->backing_dir_fd, path);
- } else {
- realPath = ctx->overwrite->copyOnWrite(realPath, path);
- }
- isBacking = false;
- updateFileNode(ctx, path, realPath, "Staging");
- } catch (...) {
- fuse_reply_err(req, EIO);
- return;
- }
- }
-
+ // 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) {
- const int openFlags = O_RDWR;
- if (isBacking && ctx->backing_dir_fd >= 0) {
- fd = openat(ctx->backing_dir_fd, realPath.c_str(), openFlags);
+ 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 {
- fd = open(realPath.c_str(), openFlags);
+ // Mod or base-game file — open read-only, defer COW to write time.
+ if (isBacking && ctx->backing_dir_fd >= 0) {
+ fd = openat(ctx->backing_dir_fd, realPath.c_str(), O_RDONLY);
+ } else {
+ fd = open(realPath.c_str(), O_RDONLY);
+ }
+ 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;
};