From 87218db0408a2df2172a93fc9106a93ac827ca57 Mon Sep 17 00:00:00 2001 From: flash Date: Wed, 1 Jul 2026 21:02:27 +0200 Subject: [PATCH 1/2] feat: add -x flag to strip xattr keys on non-client writes File managers like Nautilus copy all xattrs (including user.openvfs.data) when doing copy+paste. This causes copied files to inherit the original's fileId and immutable state, which is incorrect. The new -x flag accepts a comma-separated list of msgpack keys that should be cleared when a non-client process writes user.openvfs.data: openvfsfuse -x fileid,immutable -o owner /mountpoint When a process other than the registered desktop client writes user.openvfs.data, the specified keys are set to "" in the msgpack payload before writing. This ensures copied files get fresh identity on the next sync while preserving other attributes like size and etag. The desktop client PID (from the VERSION handshake) is exempt, so cloudd/sync clients can still write all keys. --- src/openvfsfuse/main.cpp | 18 ++++++++++++++++-- src/openvfsfuse/openvfsfuse.cpp | 32 ++++++++++++++++++++++++++++++++ src/openvfsfuse/openvfsfuse.h | 1 + 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/openvfsfuse/main.cpp b/src/openvfsfuse/main.cpp index 507aab7..6cbe07c 100644 --- a/src/openvfsfuse/main.cpp +++ b/src/openvfsfuse/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -42,7 +43,7 @@ namespace { void usage(char *name) { std::cerr << "Usage:" << std::endl // - << name << " [-h] | [-f] [-p] [-d] -i config-file -o ownerId /directory-mountpoint" << std::endl // + << name << " [-h] | [-f] [-p] [-d] [-x key1,key2,...] -i config-file -o ownerId /directory-mountpoint" << std::endl // << "Type 'man openvfsfuse' for more details" << std::endl; } @@ -57,7 +58,7 @@ std::optional processArgs(int argc, char *argv[]) bool got_p = false; - while ((res = getopt(argc, argv, "hpfdi:o:")) != -1) { + while ((res = getopt(argc, argv, "hpfdi:o:x:")) != -1) { switch (res) { case 'h': usage(argv[0]); @@ -90,6 +91,19 @@ std::optional processArgs(int argc, char *argv[]) case 'o': out.owner = optarg; break; + case 'x': { + // Parse comma-separated list of msgpack keys to strip on non-client setxattr + std::string keys(optarg); + std::istringstream ss(keys); + std::string key; + while (std::getline(ss, key, ',')) { + if (!key.empty()) { + out.stripXattrKeys.push_back(key); + } + } + std::cout << "openVFSfuse stripping xattr keys: " << keys << std::endl; + break; + } default: assert(false); break; diff --git a/src/openvfsfuse/openvfsfuse.cpp b/src/openvfsfuse/openvfsfuse.cpp index 470ab80..4774f80 100644 --- a/src/openvfsfuse/openvfsfuse.cpp +++ b/src/openvfsfuse/openvfsfuse.cpp @@ -55,6 +55,7 @@ class VFSFuseContext , _rootHandle(open(_mountPoint.c_str(), 0)) , _appsNoHydrateFull(args.appsNoHydrateFull) , _appsNoHydrateEndsWith(args.appsNoHydrateEndsWith) + , _stripXattrKeys(args.stripXattrKeys) { assert(!_instance); _instance = this; @@ -99,6 +100,8 @@ class VFSFuseContext || std::ranges::find_if(_appsNoHydrateEndsWith, [app](const auto &a) { return app.ends_with(a); }) != _appsNoHydrateEndsWith.cend(); } + const auto &stripXattrKeys() const { return _stripXattrKeys; } + private: static VFSFuseContext *_instance; std::filesystem::path _mountPoint; @@ -106,6 +109,7 @@ class VFSFuseContext int _rootHandle; std::vector _appsNoHydrateFull; std::vector _appsNoHydrateEndsWith; + std::vector _stripXattrKeys; }; VFSFuseContext *VFSFuseContext::_instance = nullptr; @@ -671,6 +675,34 @@ static int openVFSfuse_fsync(const char *orig_path, int isdatasync, struct fuse_ /* xattr operations are optional and can safely be left unimplemented */ static int openVFSfuse_setxattr(const char *orig_path, const char *name, const char *value, size_t size, int flags) { + // When a non-client process writes user.openvfs.data, strip keys listed + // in stripXattrKeys (configured via -x flag). This prevents file managers + // from copying identity/protection state on copy+paste. + if (std::string_view(name) == OpenVFS::Constants::XAttributeNames::Data + && !VFSFuseContext::instance().stripXattrKeys().empty()) { + auto *context = fuse_get_context(); + if (context && context->pid != _jobs.desktopClientPid()) { + try { + auto j = nlohmann::json::from_msgpack(std::vector(value, value + size)); + bool modified = false; + for (const auto &key : VFSFuseContext::instance().stripXattrKeys()) { + if (j.contains(key) && j[key].is_string() && !j[key].get().empty()) { + j[key] = ""; + modified = true; + } + } + if (modified) { + auto cleaned = nlohmann::json::to_msgpack(j); + const auto path = getInternalPath(orig_path); + openvfsfuse_log(path, "setxattr", 0, "stripped protected keys from copy by pid %d", context->pid); + return Xattr::setxattr(path, name, reinterpret_cast(cleaned.data()), cleaned.size(), flags); + } + } catch (...) { + // If msgpack parsing fails, pass through unchanged + } + } + } + const auto path = getInternalPath(orig_path); return Xattr::setxattr(path, name, value, size, flags); } diff --git a/src/openvfsfuse/openvfsfuse.h b/src/openvfsfuse/openvfsfuse.h index bdd0deb..72d8f90 100644 --- a/src/openvfsfuse/openvfsfuse.h +++ b/src/openvfsfuse/openvfsfuse.h @@ -23,6 +23,7 @@ struct openVFSfuse_Args std::vector fuseArgv; std::vector appsNoHydrateFull; // these apps are not permitted to cause a dehydration std::vector appsNoHydrateEndsWith; + std::vector stripXattrKeys; // msgpack keys stripped from user.openvfs.data on non-client writes }; int initializeOpenVFSFuse(openVFSfuse_Args &openVFSArgs); From 8e45db7c03cc1460b584ade31b3f04dadb46f70e Mon Sep 17 00:00:00 2001 From: flash Date: Thu, 2 Jul 2026 19:56:47 +0200 Subject: [PATCH 2/2] Address review: explain selective key stripping, use erase instead of empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review by @dragotin: "Why not completely remove the user xattrs?" We strip selectively because user-set metadata (comments, tags, custom properties) should survive copy+paste. Only system-managed per-file identity fields need removal: - fileid: unique per file, must not be duplicated - immutable: protection state is per-file, not inheritable via copy - syncerror: transient sync state Fields like etag, pinstate, size are harmless to copy. Also: use json::erase() instead of setting to "" — cleaner removal. --- src/openvfsfuse/openvfsfuse.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/openvfsfuse/openvfsfuse.cpp b/src/openvfsfuse/openvfsfuse.cpp index 4774f80..4a2576a 100644 --- a/src/openvfsfuse/openvfsfuse.cpp +++ b/src/openvfsfuse/openvfsfuse.cpp @@ -677,7 +677,12 @@ static int openVFSfuse_setxattr(const char *orig_path, const char *name, const c { // When a non-client process writes user.openvfs.data, strip keys listed // in stripXattrKeys (configured via -x flag). This prevents file managers - // from copying identity/protection state on copy+paste. + // from copying per-file identity fields (fileid, immutable, syncerror) + // on copy+paste, while preserving user-set metadata (comments, tags, etag). + // + // We selectively remove keys rather than stripping all openvfs xattrs + // because some fields (etag, pinstate, size) are harmless or even useful + // to keep, and future user-defined metadata should survive copies. if (std::string_view(name) == OpenVFS::Constants::XAttributeNames::Data && !VFSFuseContext::instance().stripXattrKeys().empty()) { auto *context = fuse_get_context(); @@ -686,15 +691,15 @@ static int openVFSfuse_setxattr(const char *orig_path, const char *name, const c auto j = nlohmann::json::from_msgpack(std::vector(value, value + size)); bool modified = false; for (const auto &key : VFSFuseContext::instance().stripXattrKeys()) { - if (j.contains(key) && j[key].is_string() && !j[key].get().empty()) { - j[key] = ""; + if (j.contains(key)) { + j.erase(key); modified = true; } } if (modified) { auto cleaned = nlohmann::json::to_msgpack(j); const auto path = getInternalPath(orig_path); - openvfsfuse_log(path, "setxattr", 0, "stripped protected keys from copy by pid %d", context->pid); + openvfsfuse_log(path, "setxattr", 0, "stripped keys from copy by pid %d", context->pid); return Xattr::setxattr(path, name, reinterpret_cast(cleaned.data()), cleaned.size(), flags); } } catch (...) {